Rewrite shannoncoat as a native Swift menu-bar app

Replaces the original shannoncoat.sh entirely with in-process Swift. The
script stopped earning its keep once the rest went native, and being a
persistent process rather than a one-shot CLI is what later makes live
window tracking possible at all.

- ProfileStore: JSON config at ~/.shannoncoat/<name>.json (was hand-rolled
  YAML), with ~ expansion, paths relative to the config dir, and
  name/dir-collision validation in one place.
- ProcessInspector: sysctl(KERN_PROC_ALL/KERN_PROCARGS2) enumeration
  instead of shelling out to pgrep.
- ClaudeControl: launch via Process, focus via direct Accessibility calls
  (unhide, un-minimize, poll-for-window, AXRaise) instead of AppleScript,
  and a quit that respects Claude's own termination handling rather than
  unconditionally SIGKILLing after a flat 5s timeout.
- LiveState: NSWorkspace notification-driven state with no polling timer,
  so the menu bar reflects reality within about a second — including
  changes made outside the app entirely.
- ManageWindow: one non-modal window replacing what would otherwise be a
  string of separate popup alerts, with inline add/remove and directory
  pickers.
- LaunchAtLogin / UpdateChecker: SMAppService login-item toggle, and a
  minimal VERSION-file update check that alerts on a manual check and
  posts a quiet notification on the automatic one.

Also drops the standalone CLI in favour of GUI-only, and adds a
placeholder app icon drawn from vector shapes rather than composited on
top of Claude's own icon assets.

Claude Desktop is located at runtime rather than assumed: a path the user
picked previously, then /Applications, then a per-user ~/Applications
install, each accepted only if it actually contains the executable. If
none match, launching a profile asks the user to locate it once and
remembers the answer — a launch that silently does nothing gives them no
way to work out what's wrong.
This commit is contained in:
Claude Opus 5
2026-08-04 21:56:22 +06:00
committed by bdeshi
parent 815f0306c7
commit 135db9b308
18 changed files with 1842 additions and 182 deletions
+41
View File
@@ -0,0 +1,41 @@
name: Release
# Two ways in: a manually pushed version tag, or a direct call from
# tag-on-version-bump.yml (which fires on VERSION changes on main). The
# workflow_call path exists because pushes made with the default
# GITHUB_TOKEN don't trigger other workflows' `on.push` — a tag pushed by
# that workflow wouldn't reach this one's `tags:` trigger on its own.
on:
push:
tags:
- "*"
workflow_call:
inputs:
tag:
required: true
type: string
permissions:
contents: write
jobs:
build:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag || github.ref }}
- name: Build shannoncoat.app
run: ./build.sh
- name: Zip app bundle
run: ditto -c -k --sequesterRsrc --keepParent ".build/shannoncoat.app" "shannoncoat.app.zip"
- name: Publish release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ inputs.tag || github.ref_name }}
files: |
shannoncoat.app.zip
generate_release_notes: true
+49
View File
@@ -0,0 +1,49 @@
name: Tag on version bump
on:
push:
branches: [main]
paths: ["VERSION"]
permissions:
contents: write
jobs:
tag:
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.tag.outputs.tag }}
created: ${{ steps.tag.outputs.created }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# Guards against re-tagging on a no-op push (e.g. VERSION edited then
# reverted to the same value in a later commit) — only pushes a tag
# that doesn't exist yet.
- name: Create tag if new
id: tag
run: |
VERSION="$(tr -d '[:space:]' < VERSION)"
TAG="$VERSION"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
if git rev-parse "$TAG" >/dev/null 2>&1; then
echo "Tag $TAG already exists, skipping."
echo "created=false" >> "$GITHUB_OUTPUT"
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -a "$TAG" -m "Release $TAG"
git push origin "$TAG"
echo "created=true" >> "$GITHUB_OUTPUT"
release:
needs: tag
if: needs.tag.outputs.created == 'true'
uses: ./.github/workflows/release.yml
with:
tag: ${{ needs.tag.outputs.tag }}
permissions:
contents: write