The shared shell, scripts and template for doom1337hvh's small Mac apps, such as MusicNotch and Foldaway. Each app keeps its own repository and releases, and pulls AppShell in with Swift Package Manager, so they all look, build and ship the same way, and a fix here reaches every app.
Sources/AppShell, a Swift package the apps import:SettingsWindowandSettingsTab: the Settings window, with a row of tabs at the top, slightly raised but for the selected one, which is pressed in. It reopens on the tab used last..general(menuBar:)(Launch at login, Show menu bar icon, then the app's own) comes first and.about()(icon, version, GitHub, licences) last.MenuBar: the menu bar icon and its menu: the app's items, then Settings… and Quit. Hiding it is a setting in General.LoginItem,AppShell.quitIfTranslocated()(the "Move to Applications" check),AppShell.demoScene(the-demo <scene>argument for README clips), andNSMenuItem("Title") { … }for menu items that run a closure.
scripts/:build.sh,release.shandcreate-signing-cert.sh, which every app runs through its own tinyscripts/build.shand so on; andnew-app.sh, which starts a new app.template/: whatnew-app.shcopies: a working menu bar app with General, a tab of its own and About, a README in the house format with TODOs to fill in, the release notes, a placeholder icon script, and a CLAUDE.md that tells Claude Code to build on AppShell and keep the house format.
scripts/new-app.sh Clipboardy --symbol doc.on.clipboard --in ~/DocumentsThis makes ~/Documents/Clipboardy, a menu bar app that already builds and runs, with the
bundle ID io.github.doom1337hvh.Clipboardy and a placeholder icon drawn from the symbol. Then:
cd ~/Documents/Clipboardy && scripts/create-signing-cert.sh && scripts/build.sh --installFill in the TODOs (grep -rn TODO .), draw the real icon in scripts/make-icon.swift, and
publish the folder on GitHub (GitHub Desktop: Add Local Repository, then Publish Repository).
Or ask Claude Code for a new app: with the new-mac-app skill in ~/.claude/skills, it runs
this and fills in the rest the same way.
Every app follows it; the template already does.
Repository
Package.swift depends on AppShell (branch main; Package.resolved pins the commit)
Package.resolved committed
Resources/Info.plist name, bundle ID io.github.<owner>.<Name>, version, GitHubRepository
Resources/AppIcon.icns drawn by scripts/make-icon.swift
Sources/<Name>/ main.swift, AppDelegate.swift, Preferences.swift, settings tabs, the rest
scripts/ build.sh, release.sh, create-signing-cert.sh and appshell.sh (which run
AppShell's), hooks/ for the app's own build steps, make-icon.swift,
make-video.swift for the README's clips
docs/ icon.png, demo.gif, clips/*.gif, RELEASING.md
README.md, LICENSE MIT
CLAUDE.md the house rules for Claude Code in this app
README, in this order: centred icon, name, one-line tagline plus "Free, open source, and
light on battery.", the main demo GIF, the Download · Install · In action · Settings links, a
short intro, then Features, In action (one collapsed <details> clip each), Requirements,
Install, Updating, Permissions, Settings, Battery, Troubleshooting, Unofficial APIs (if any),
Uninstall, Build from source (with How it works), License.
The app
- A menu bar app with no Dock icon (
LSUIElement), Apple silicon, macOS 14 or later. - The menu: the app's own items, then Settings… (⌘,) and Quit (⌘Q).
- Settings: General first, a tab per topic, About last. Each tab is a grouped form 500 points wide; every setting has a label and, if it needs one, a line underneath saying what it does.
- Opening the app again while it runs shows Settings, so it's reachable with the icon hidden.
- Opened from Downloads, it asks to be moved to Applications and quits.
- It never connects to the internet: no analytics, accounts or update checks.
- README clips are recorded from the real app with
-demo <scene>; nothing is saved meanwhile.
Releases: signed with the app's own " Local Signing" certificate every time, so
people's permissions carry over; scripts/release.sh writes the zip, the DMG and
SHA256SUMS.txt; tags are v<version>. See the app's docs/RELEASING.md.
The template's AppDelegate.swift shows all of it:
private lazy var menuBar: MenuBar = MenuBar(symbol: "sparkles", items: { [weak self] menu in
guard let self else { return }
menu.addItem(NSMenuItem("Turn On Clipboardy", isOn: prefs.isEnabled) { [weak self] in
self?.prefs.isEnabled.toggle()
})
}, showSettings: { [weak self] in
self?.settings.show()
})
private lazy var settings: SettingsWindow = SettingsWindow { [weak self] in
guard let self else { return [] }
return [
.general(menuBar: menuBar),
SettingsTab("Clipboardy", symbol: "sparkles") { MainSettings(prefs: self.prefs) },
.about(),
]
}A tab's content is just its Sections; AppShell lays them out. .general and .about take
extra content too, such as MusicNotch's "Where it appears" section or its media adapter status,
and .about(thirdParty:) lists the licences of what the app ships from others.
An app's own build steps go in scripts/hooks/prepare.sh (before swift build) and
scripts/hooks/bundle.sh (once the app is put together in $STAGED, before signing); see
scripts/build.sh. MusicNotch builds and adds its media adapter framework there.
Apps get a change to AppShell once it's pushed and they run swift package update AppShell
(then commit their Package.resolved). To try a change in an app before pushing it:
swift package edit AppShell --path ~/Documents/AppShellThe app then builds with your copy until swift package unedit AppShell.