Initial commit: a Copper-style panel for prompts and photos
Keeps text prompts and photos in a floating always-on-top panel and
copies them back to the clipboard on click. Double-Shift captures the
selection from any app; sections, search, copy-as-list, merge, and
local JSON persistence mirror shadcn's Copper workflow.
💘 Generated with Crush
Assisted-by: Crush:kimi-k3
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
# AGENTS.md
|
||||
|
||||
Prompt is a native macOS app written in Swift/SwiftUI. Single executable
|
||||
SwiftPM package, no external dependencies (all Apple frameworks). It is a
|
||||
Copper-style (shadcn) capture panel: prompts and photos live in sections,
|
||||
clicking an item copies it back to the clipboard.
|
||||
|
||||
## Architecture
|
||||
|
||||
- `Sources/Prompt/App.swift` — `@main` entry using a bare
|
||||
`Settings { EmptyView() }` scene; the app owns its `NSStatusItem` manually
|
||||
(same reason as SimpleTopBar: placement control on notched Macs, seeded via
|
||||
`NSStatusItem Preferred Position Prompt`). Left click toggles the panel,
|
||||
right click shows the menu (accessibility state, launch at login, open data
|
||||
folder, quit). Contains the single-instance guard and `LoginItem`
|
||||
(`SMAppService`).
|
||||
- `Sources/Prompt/Store.swift` — `@MainActor` `ObservableObject` singleton.
|
||||
Data model: `Section` → `[PromptItem]`, item content is
|
||||
`.text(String)` or `.image(String)` (filename). Persists to
|
||||
`~/Library/Application Support/Prompt/store.json`; images are PNG files in
|
||||
`Images/` next to it, cached in an `NSCache`. Owns all mutations (add,
|
||||
toggle done, edit, delete, move, merge, clear done) and clipboard writes
|
||||
(`copy`, `copyAsList`). Saves on every mutation.
|
||||
- `Sources/Prompt/PanelView.swift` — the panel UI. Header (section switcher,
|
||||
search, add photos, close), item list, composer. Composer `# Name` creates/
|
||||
switches sections. Click = copy; Cmd+click = multi-select; right-click =
|
||||
context menu (Copy, Copy as List, Done, Edit, Open, Merge, Move To, Delete).
|
||||
Drop target for images/files.
|
||||
- `Sources/Prompt/PanelController.swift` — owns the floating `NSPanel`
|
||||
(`.nonactivatingPanel`, `.floating`, right-edge docked, frame autosaved as
|
||||
`PromptPanel`), the Cmd+V image-paste monitor, and `EditWindowController`
|
||||
(separate edit window with a `TextEditor`).
|
||||
- `Sources/Prompt/HotkeyManager.swift` — double-Shift detection via a
|
||||
CGEvent tap on `flagsChanged` (left Shift ×2 = capture, right Shift ×2 =
|
||||
toggle panel), plus Carbon `RegisterEventHotKey` fallbacks (Cmd+Shift+Space
|
||||
toggle, Cmd+Shift+C capture) that work without Accessibility.
|
||||
- `Sources/Prompt/Capture.swift` — selection capture: backs up every
|
||||
pasteboard item, simulates Cmd+C, polls `changeCount` for ≤0.5s, reads the
|
||||
string, restores the backup. Runs off the main thread (hotkey dispatches to
|
||||
a global queue); only the store append hops back to main.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
swift build # debug build
|
||||
swift run Prompt # dev run (menu bar icon appears)
|
||||
./build-app.sh 0.1.0 # release build + .app bundle + icon + codesign
|
||||
./release.sh 0.1.0 # build-app.sh + dist/Prompt.zip + notarize + sha256
|
||||
scripts/make-icon.sh # regenerate Prompt.app/Contents/Resources/Icon.icns
|
||||
```
|
||||
|
||||
There is no test target and no linter/formatter configured. Verification =
|
||||
`swift build` and manual run.
|
||||
|
||||
## Bundle build & signing
|
||||
|
||||
`build-app.sh` does: release build → handwritten `Info.plist` (NOT generated
|
||||
from `Package.swift`) → `make-icon.sh` → `codesign`. It resolves a
|
||||
`Developer ID Application` identity from the login keychain and falls back to
|
||||
ad-hoc signing (`-`) if none is present. `LSUIElement` is true → no Dock icon.
|
||||
Bundle id is `dev.tzgyn.prompt`.
|
||||
|
||||
## Release process
|
||||
|
||||
1. `./release.sh <version>` — builds, zips `Prompt.app` to `dist/Prompt.zip`,
|
||||
and prints its sha256 (it does not tag or upload). Notarizes when the
|
||||
`PROMPT_NOTARY_*` env vars (or the shared Kapture App Store Connect key at
|
||||
`~/.config/kapture/AuthKey_K5KXMMSB28.p8`) are present; set
|
||||
`PROMPT_SKIP_NOTARY=1` to skip.
|
||||
2. Commit and push `main`.
|
||||
3. `tea releases create --tag v<version> --asset dist/Prompt.zip --repo TZGyn/prompt --login gitea.tzgyn.dev`.
|
||||
4. Bump `version`/`sha256` in `~/.cache/homebrew-tap/Casks/prompt.rb`, push,
|
||||
then `git -C /opt/homebrew/Library/Taps/tzgyn/homebrew-tap pull --ff-only`.
|
||||
|
||||
**Always print the upgrade script after any release or version bump:**
|
||||
|
||||
```bash
|
||||
brew update
|
||||
brew upgrade --cask prompt
|
||||
```
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Never bind bare keys as `.keyboardShortcut` in the panel** (Space, Delete,
|
||||
plain Cmd+C). SwiftUI key equivalents are matched before text input and
|
||||
would steal characters from the composer TextField. Only Cmd-modified
|
||||
shortcuts that TextFields don't use are bound (Cmd+F/K/N, Shift+Cmd+C).
|
||||
This is why Copy has no in-panel shortcut: click-to-copy is the primary
|
||||
gesture, matching Copper's intent.
|
||||
- The panel is a `.nonactivatingPanel`; its `canBecomeKey` override is what
|
||||
lets the composer TextField take focus. Don't replace it with a plain
|
||||
NSWindow config without rechecking that typing works without the app
|
||||
becoming active.
|
||||
- The Cmd+V paste monitor only intercepts when the pasteboard has an image
|
||||
AND no string, so pasting text into the composer is never swallowed.
|
||||
- CGEvent tap callbacks run off-main; `HotkeyManager` hops to main before
|
||||
firing. The tap is re-enabled on `tapDisabledByTimeout`.
|
||||
- `Store` is `@MainActor`; anything reaching it from monitors/callbacks must
|
||||
go through `Task { @MainActor in }`.
|
||||
- `captureSelection()` blocks up to 0.5s polling the pasteboard — keep it off
|
||||
the main thread (the hotkey handler already dispatches to a global queue).
|
||||
- The double-Shift tap needs Accessibility; without it the event tap silently
|
||||
fails and only the Carbon fallbacks work. The panel banner and the status
|
||||
menu both surface the permission state.
|
||||
- `.gitignore` covers `.build/`, `Prompt.app/`, `dist/` — build artifacts
|
||||
should never be committed.
|
||||
Reference in New Issue
Block a user