Skip to content
This repository was archived by the owner on Oct 17, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .githooks/commit-msg
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ file="$1"
subject="$(sed -n '/^[^#]/{p;q;}' "$file")"

if ! printf '%s\n' "$subject" | grep -Eq '^(feat|fix|chore|docs|perf|refactor|test|build|ci|style|revert)(\([a-z0-9._/-]+\))?!?: [^ ].*[^.]$'; then
echo "commit-msg: subject must be a Conventional Commit, e.g. 'fix(cpu): handle tick wraparound'" >&2
echo "commit-msg: subject must be a Conventional Commit, e.g. 'fix(menu): keep the item on screen'" >&2
echo "commit-msg: got: $subject" >&2
exit 1
fi
Expand Down
6 changes: 6 additions & 0 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,18 @@ remote="$1"
default="$(git symbolic-ref --quiet "refs/remotes/$remote/HEAD" 2>/dev/null | sed "s|^refs/remotes/$remote/||")" || default=""
[ -n "$default" ] || default="$(git config init.defaultBranch || echo main)"

branches=0
while read -r _local_ref _local_sha remote_ref _remote_sha; do
if [ "$remote_ref" = "refs/heads/$default" ] && git ls-remote --exit-code --heads "$remote" "$default" >/dev/null 2>&1; then
echo "pre-push: refusing to push to $default; open a pull request (scripts/agent pr)" >&2
exit 1
fi
case "$remote_ref" in refs/heads/*) branches=$((branches + 1)) ;; esac
done

# A tag points at a merge commit CI already checked; checking the working tree again would test
# whatever happens to be checked out, not what's tagged.
[ "$branches" -gt 0 ] || exit 0

echo "pre-push: full check"
exec scripts/task check
5 changes: 4 additions & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@

## Look at sceptically

<!-- Where this is weakest: assumptions, untested paths, accuracy arguments. -->
<!-- Where this is weakest: assumptions, untested paths, what you couldn't try. -->

## Checklist

- [ ] `scripts/task check` passes locally
- [ ] Bug fixes include the test that would have caught them
- [ ] User-facing changes are under `## [Unreleased]` in `CHANGELOG.md`
<!-- standard:begin checklist -->
- [ ] UI changes include before/after screenshots
<!-- standard:end checklist -->
2 changes: 1 addition & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Common Command has no package dependencies, so GitHub Actions is the only ecosystem to watch.
# No Swift package dependencies, so GitHub Actions is the only ecosystem to watch.
version: 2
updates:
- package-ecosystem: github-actions
Expand Down
23 changes: 23 additions & 0 deletions .github/workflows/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Dependabot

on: pull_request

permissions:
contents: read

jobs:
auto-merge:
# Minor and patch updates merge themselves once the required check passes; majors wait for a person.
if: github.event.pull_request.user.login == 'dependabot[bot]'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- id: meta
uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0
- if: steps.meta.outputs.update-type != 'version-update:semver-major'
run: gh pr merge --auto --squash "$PR"
env:
PR: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
20 changes: 11 additions & 9 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,19 @@ jobs:
contents: write
steps:
- uses: actions/checkout@v4
- run: scripts/task check
- name: Package
- name: Check the tag matches the app's version
run: |
# ditto keeps the bundle's signature and extended attributes intact.
# The asset name carries no version, so releases/latest/download/CommonCommand.zip
# always points at the newest build.
ditto -c -k --keepParent "build/Common Command.app" CommonCommand.zip
shasum -a 256 CommonCommand.zip > CommonCommand.zip.sha256
version="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' Resources/Info.plist)"
[ "v$version" = "$GITHUB_REF_NAME" ] || { echo "Info.plist says $version, the tag is $GITHUB_REF_NAME"; exit 1; }
- run: scripts/task check
- run: scripts/task package
- name: Publish
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "$GITHUB_REF_NAME" --verify-tag --generate-notes --title "Common Command ${GITHUB_REF_NAME#v}" \
CommonCommand.zip CommonCommand.zip.sha256
version="${GITHUB_REF_NAME#v}"
name="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleDisplayName' Resources/Info.plist)"
# The notes are this version's CHANGELOG section, the same text the site shows.
awk -v v="$version" 'index($0, "## [" v "]") == 1 { on = 1; next } on && /^## / { exit } on && /^\[/ { exit } on { print }' CHANGELOG.md > notes.md
[ -s notes.md ] || { echo "CHANGELOG.md has no section for $version"; exit 1; }
gh release create "$GITHUB_REF_NAME" --verify-tag --title "$name $version" --notes-file notes.md dist/*
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.build/
dist/
build/
*.xcodeproj
.swiftpm/
Expand Down
28 changes: 18 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,46 @@
# Working in this repository

<!-- standard:begin about -->
Common Command is a native macOS launcher, a Spotlight replacement, written in Swift (AppKit plus SwiftUI) with no package dependencies. It is part of the Common Tools family with Common Stats, Common Keys, Common Bar, and Common Glyph.
<!-- standard:end about -->

## The seam

Do all automation through `scripts/task`. CI and the git hooks call nothing else.

```sh
scripts/task fmt # format in place
scripts/task fmt:check # swift format lint --strict, format only
scripts/task lint # fmt:check, then a debug build with warnings as errors
scripts/task test # unit and performance tests
scripts/task build # universal build/Common Command.app
scripts/task check # lint + test + build
scripts/task fmt # format in place
scripts/task fmt:check # swift format lint --strict
scripts/task lint # fmt:check, then a debug build with warnings as errors
scripts/task test [name] # the Swift Testing suites; a name filters them
scripts/task build # universal "build/Common Command.app", signed (see scripts/dev-cert)
scripts/task package # dist/CommonCommand.zip and checksums, as a release publishes them
scripts/task run # build, quit the running copy, open the new one
scripts/task check # lint + test + build: what CI runs
```

## Workflow

- Never commit to `main`. It only moves forward when a pull request is merged; the `pre-push` hook and branch protection both enforce this.
- One unit of work gets one worktree, one branch, and one pull request: `scripts/agent start <type>/<slug>`, then work in the printed path, `scripts/agent commit`, `scripts/agent pr`, and `scripts/agent done` after the merge.
- Branch types: `feat fix chore docs perf refactor test`.
- Run `scripts/task check` until it is green before opening a pull request. Never use `--no-verify`.
- In the pull request description, state the problem, the approach, and where a reviewer should be sceptical.
- One unit of work gets one worktree, one branch, and one pull request: `scripts/agent start <type>/<slug>`, then work in the printed path and finish with `scripts/agent ship "<type>(scope): subject" "<why>" --body-file <pr.md>`, where the body states the problem, the approach, and where a reviewer should be sceptical. It commits, checks, opens the pull request with auto-merge, keeps it rebased while CI runs, and cleans up after the merge. If it stops on a failed check or a conflict, fix the cause and run it again.
- Branch types: `feat fix chore docs perf refactor test`. The branch's first commit names the pull request and the squash commit on `main`.
- Never use `--no-verify`.
- User-facing changes go under `## [Unreleased]` in `CHANGELOG.md` in the same pull request. The site shows each release's section as "What's new".
- Release with `scripts/release <version>`. Never tag by hand.
- This repository follows the Common Tools app standard (`standard/` in oddurs/common-tools). Change shared files there and apply them to every app; only the regions between `standard:begin` and `standard:end` belong to this app.

## Commits

Conventional Commits: `type(scope): subject`. Write the subject in the imperative, at most 72 characters, with no trailing period. The body says why. Never add assistant or tool attribution: no co-author trailers, no "generated with" footers. The `commit-msg` hook rejects them.

## Code

<!-- standard:begin code -->
- `Sources/CommandKit` holds matching, ranking, usage, the calculator, link detection, and the catalogs, and contains no UI. `Sources/CommonCommand` holds the app shell: hotkey, panel, menu bar item, Settings, and the Spotlight file query.
- Speed is the product. Ranking runs on the main thread on every keystroke and must stay well under a millisecond for a real catalog; `PerformanceTests` enforces budgets. Don't loosen a budget to make a change pass. Anything slower (Spotlight, directory scans) runs off the main thread and never blocks the list.
- Public APIs only, and no permissions: the hotkey is Carbon's `RegisterEventHotKey`, files come from `MDQuery`, panes from the System Settings extension bundles. Don't add anything that needs Accessibility, Input Monitoring, or Full Disk Access.
- The panel is a non-activating `NSPanel`. Don't call `NSApp.activate()` when showing it; the user's app must stay frontmost.
- A bug fix comes with the test that would have caught it. Ranking changes need a test showing the order they fix.
- `CommonTools.swift` is shared word for word across the Common Tools apps. Change it in all of them together.
- For visual review, use `swift build && .build/debug/CommonCommand --snapshot out.png "query" [--theme light|dark]` and inspect `out.png`. It renders offscreen; never drive the live screen with synthesized keys or the global shortcut to check a design.
<!-- standard:end code -->
41 changes: 31 additions & 10 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,30 @@ Thanks for helping. Common Command stays small on purpose, so open an issue befo

```sh
scripts/setup # wire the git hooks, once
scripts/dev-cert # optional, once: a stable signing identity, so permissions survive rebuilds
scripts/agent doctor # check tools and repository state
```

You need macOS 14 or later and Swift 6 or later. The Command Line Tools are enough.
You need macOS 14.0 or later and Swift 6 or later. The Command Line Tools are enough; Xcode is not required.

## Workflow

Every change goes through a pull request. `main` only moves forward when a pull request is merged; branch protection and the `pre-push` hook both enforce this.
Every change goes through a pull request, and pull requests merge themselves. `main` only moves forward when a pull request is merged; branch protection and the `pre-push` hook both enforce this.

Each change gets its own worktree, branch, and pull request:

```sh
scripts/agent start fix/calc-percent-precedence # creates ../.worktrees/common-command/fix/calc-percent-precedence
cd ../.worktrees/common-command/fix/calc-percent-precedence
scripts/agent start fix/short-description # creates ../.worktrees/common-command/fix/short-description
cd ../.worktrees/common-command/fix/short-description
# ... make the change ...
scripts/agent commit "fix(calc): apply percent before multiplication"
scripts/agent pr # runs the full check, pushes, opens the PR
scripts/agent done # after merge: removes the worktree and branch
scripts/task run # build it and open it
scripts/agent ship "fix(scope): say what changed" "Why it changed." --body-file pr.md
```

`ship` commits, runs the full check, opens the pull request with auto-merge, and waits. The pull request squash-merges itself once the `required` check passes. While it waits, `ship` rebases onto `main` whenever `main` moves and checks again. If a check fails or the rebase conflicts, it stops and says so: fix it, then run `scripts/agent ship` again. `commit`, `pr`, and `done` still work one step at a time.

The pull request takes its title from the branch's first commit, and that title becomes the squash commit on `main`, so make the first commit the one that names the change. The body states the problem, the approach, and where a reviewer should be sceptical.

Branch names are `<type>/<slug>`, where type is one of `feat fix chore docs perf refactor test`.

## Commits
Expand All @@ -37,11 +41,28 @@ Don't add tool or assistant attribution lines (co-author trailers, "generated wi
## Checks

```sh
scripts/task check # lint + test + build, the same thing CI runs
scripts/task check # lint + test + build, the same thing CI runs
scripts/task test <name> # just the tests whose name matches
```

A branch must pass this before it becomes a pull request. A bug fix comes with the test that would have caught it.

<!-- standard:begin checks -->
Behaviour that depends on AppKit or Spotlight belongs in the app target; matching, ranking, and parsing belong in CommandKit, where the tests can reach them.
<!-- standard:end checks -->

## Releases

```sh
scripts/release 1.2.0
```

A branch must pass this before it becomes a pull request. A bug fix comes with the test that would have caught it. Behaviour that depends on AppKit or Spotlight belongs in the app target; matching, ranking, and parsing belong in CommandKit, where the tests can reach them.
Add user-facing changes under `## [Unreleased]` in `CHANGELOG.md` as you make them. `scripts/release` moves them under the new version with today's date, bumps the version in `Resources/Info.plist`, merges that through a pull request, tags the merge, and waits until the download is live. The release notes are that CHANGELOG section. Don't tag by hand.

## The standard

Common Command follows the Common Tools app standard, kept in [oddurs/common-tools](https://github.com/oddurs/common-tools/tree/main/standard). The repository files, scripts, and settings are the same in every app. Only the parts between `standard:begin` and `standard:end` markers belong to this app. Change shared files there and apply them to every app, rather than editing one copy.

## Review

This is a solo-maintained project, so branch protection requires zero approvals and the maintainer merges their own pull requests once CI is green. The `required` status check and up-to-date branches are still enforced.
This is a solo-maintained project, so branch protection requires zero approvals, and pull requests merge themselves once the `required` check is green. Branches don't have to be up to date with `main` to merge; `ship` keeps them current instead.
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,18 +93,22 @@ mv "build/Common Command.app" /Applications/

```sh
scripts/setup # once: wire the git hooks
scripts/dev-cert # optional, once: a stable signing identity, so permissions survive rebuilds
scripts/agent doctor # check tools and repository state
```

| Command | Does |
| --- | --- |
| `scripts/task run` | build, quit the running copy, open the new one |
| `scripts/task check` | lint + test + build, exactly what CI runs |
| `scripts/task test <name>` | only the tests whose name matches |
| `scripts/agent start <type>/<slug>` | new worktree and branch from `origin/main` |
| `scripts/agent commit "<type>: subject"` | Conventional Commit, checked by the hook |
| `scripts/agent pr` | full check, push, open the pull request |
| `scripts/agent ship "<type>: subject"` | commit, check, open the PR with auto-merge, wait for the merge, clean up |
| `scripts/agent pr` | check, push, open the PR with auto-merge, without waiting |
| `scripts/agent sync` | rebase onto `origin/main` |
| `scripts/agent done` | after merge: remove the worktree and branch |
| `scripts/agent done` | after a merge: remove the worktree and branch, update the main checkout |
| `scripts/agent list` | worktrees, branches, and pull request state |
| `scripts/release <version>` | a release: dated notes, version bump, tag, published download, checked |

See [CONTRIBUTING.md](CONTRIBUTING.md) for the workflow.

Expand Down
Binary file added Resources/CommonTools/battery.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Resources/CommonTools/shot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions Resources/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>Common Command</string>
<key>CFBundleExecutable</key>
Expand All @@ -10,6 +12,8 @@
<string>AppIcon</string>
<key>CFBundleIdentifier</key>
<string>com.oddurs.commoncommand</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Common Command</string>
<key>CFBundlePackageType</key>
Expand All @@ -18,6 +22,8 @@
<string>0.1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.utilities</string>
<key>LSMinimumSystemVersion</key>
<string>14.0</string>
<key>LSUIElement</key>
Expand Down
2 changes: 2 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ Report it privately through [GitHub Security Advisories](https://github.com/oddu

You'll get an acknowledgement within a week. If the report is confirmed, a fix ships in a new release and the advisory is published with credit, unless you'd rather stay anonymous.

<!-- standard:begin surface -->
Common Command runs unprivileged and asks for no permissions. It makes no network connections of its own; web searches and links open in your browser. It reads the folders apps live in, the System Settings extension bundles, and file names from the Spotlight index of your home folder. It keeps a history of what you opened, and the queries you opened it with, in `~/Library/Application Support/Common Command/usage.json`. Reports about how queries reach Spotlight or how the calculator parses input are especially welcome.
<!-- standard:end surface -->
12 changes: 9 additions & 3 deletions Sources/CommonCommand/CommonTools.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import AppKit
import SwiftUI

/// The Common Tools family: Common Stats, Common Keys, Common Bar, Common Glyph, and Common Command. This file is
/// identical in every app, so each Settings window ends the same way; change it in all of them together.
/// The Common Tools family. This file is identical in every app, so each Settings window ends the same
/// way. It comes from the app standard in oddurs/common-tools: change it there, not here.
struct CommonTool: Identifiable {
let id: String
let name: String
Expand All @@ -17,14 +17,20 @@ struct CommonTool: Identifiable {
id: "keys", name: "Common Keys", summary: "Fixes words typed on the wrong keyboard",
bundleIdentifier: "com.oddurs.langsmart"),
CommonTool(
id: "bar", name: "Common Bar", summary: "Hides menu bar icons behind one line",
id: "bar", name: "Common Bar", summary: "Hides the menu bar icons you rarely use",
bundleIdentifier: "com.oddurs.reasonal"),
CommonTool(
id: "glyph", name: "Common Glyph", summary: "Your own image in the menu bar",
bundleIdentifier: "com.oddurs.commonglyph"),
CommonTool(
id: "command", name: "Common Command", summary: "Opens apps, files, and settings from the keyboard",
bundleIdentifier: "com.oddurs.commoncommand"),
CommonTool(
id: "shot", name: "Common Shot", summary: "Screenshots straight to the clipboard",
bundleIdentifier: "com.oddurs.commonshot"),
CommonTool(
id: "battery", name: "Common Battery", summary: "The battery in the menu bar, with alerts",
bundleIdentifier: "com.oddurs.commonbattery"),
]

static let website = URL(string: "https://oddurs.github.io/common-tools/")!
Expand Down
Loading
Loading