GhostTileis a Swift Package with three targets:GhostTileCore: shared process, config, signing, logging, and helper logic.ghosttile: CLI built withswift-argument-parser.GhostTileApp: macOS SwiftUI app and menu bar UI.
Sources/GhostTileCore: core logic used by both the app and CLI.Sources/ghosttile: CLI entrypoint and subcommands.Sources/GhostTileApp: SwiftUI views, app lifecycle, and status bar integration.Resources: app bundle resources,Info.plist, icons, andghosthide.m.justfile: local build, packaging, install, and release helpers.docs/dev/roadmap.md: shipped work and near-term follow-up.
AppManager: thin façade delegating to focused types below.AppResolver: resolves apps from bundle ID, path, name, or running app.AppPreparationManager: binary backup, Mach-O patching, entitlements, codesign.AppRestoreManager: binary restoration and cleanup.AppLauncher: launch/quit/focus, SIP and Apple-first-party checks.ShellRunner: command execution with stderr capture.ManagedAppRecord/ManagedAppStateReader: shared core model and snapshot factory.Config: reads/writes~/.config/ghosttile/config.json.
- Service implementations live in focused subfolders under
Sources/GhostTileApp/Services. Services/ManagedApps: managed app list, snapshots, config watching.Services/Permissions: System Settings permission guidance and helper overlay.Services/Attention: attention/notification observation and delivery.Services/DockVisibility: auto-hide, reapply, notification sending.Services/AppActions: high-level hide/launch/remove workflows.Services/CLI: consolidated CLI binary path resolution.Services/Shortcuts,Services/Sponsors,Services/Updates,Services/Config: focused support services.
AppViewModel: app-wide coordinator for loading state, errors, workspace observers.MainWindowViewModel: query filtering, managed/running app lists, counts.OverviewViewModel: selection, arrow navigation, search.SettingsViewModel: CLI install status, version checking, launch-at-login.
ManagedAppItem: standalone UI wrapper overManagedAppRecord(icon + category).IconTileView,SearchFieldView,SectionHeaderView,StatusPill: reusable primitives.SettingsSectionCard,SettingsRowIcon: settings-specific chrome.
CLIShared: helper functions for JSON output and managed app resolution.ManagePrepareCommands,QueryCommands,FocusCommand,RestoreCommand,VisibilityCommands: individual command files.
swift build: build all targets in debug.swift build -c release --product GhostTileApp: build the app binary.swift build -c release --product ghosttile: build the CLI binary.just build: assembleGhostTile.app, compileghosthide.dylib, and codesign the bundle.just run: rebuild and open the app bundle.just build-cli: build only the CLI in release.
- Use
jjfor source control operations in this repo: status, diff, log, change descriptions, bookmark movement, and Git pushes. - Use
gitonly when a task specifically needs GitHub/Git compatibility thatjjdoes not cover. - Abandon only empty jj changes with no description, and never abandon user work or non-empty changes without explicit confirmation.
- Bump the app version + build with
just set-version(auto-bumps patch + build by default; pass explicit values likejust set-version 2.1.0 23for major/minor). TheVERSIONfile at repo root is the single source — the recipe also mirrors it intoproject.yml,Resources/Info.plist, andBuildInfo.swift. - Write release notes to
releases/<version>.htmlas an HTML body fragment with no wrapper tags. - Run
just buildto verify the release still builds. - Run
just release-dry-runif you want a local package check without signing or notarization. - Run
just releaseto sign, notarize, zip, updatedocs/appcast.xml, update thedocs/index.htmldownload link (just update-download), and upload the draft GitHub release. - Before publishing, verify the exact release archive that will be uploaded:
bash scripts/verify-release-archive.sh dist/GhostTile-<version>.zip notarized. This extracts the archive and runscodesign,stapler validate, andspctl -avagainst the extractedGhostTile.app. - Publish the draft GitHub release (
gh release edit v<version> --draft=false) so thev<version>tag exists and the binary download URL works. - Commit and push
docs/appcast.xml+docs/index.htmltomainso Sparkle clients and the marketing site see the new version. - Update the Homebrew tap via
just update-cask, then commit + push../tap/Casks/ghosttile.rb. - Update the official Homebrew/homebrew-cask fork via
just update-homebrew-cask(edits../../github/homebrew-cask/Casks/g/ghosttile.rb; setHOMEBREW_CASK_DIRto override). Commit on the fork branch, then push and open a PR toHomebrew/homebrew-caskmanually.
BuildInfo.cliVersion and BuildInfo.cliBuild are independent from app releases and not covered by just set-version. Leave them unchanged for app-only releases; edit BuildInfo.swift by hand only when the bundled CLI behavior or support payload changes and installed users should reinstall the CLI.
Sparkle release notes come from releases/<version>.html. Do not publish a release with an empty appcast description.
- Only add comments that explain why, not what. If the code is self-explanatory, skip the comment.
- Keep one top-level Swift
enumorstructper file, with the filename matching the type name.
- Put new app service code in a feature subfolder under
Sources/GhostTileApp/Services; do not add new service files directly at the Services root. - Prefer changes in
GhostTileCorewhen logic is shared between the app and CLI. - Use the existing service/view-model split. Don't route new behavior through
AppViewModel— put logic in focused services orAppOperations. ManagedAppItemis the UI-facing app type.ManagedAppRecordis the core type. Don't mix them.- CLI commands should use
AppManagerfacade methods or core types directly, not app-layer services. - Keep UI work aligned with the existing SwiftUI/AppKit mix. The app uses
NSWorkspace,NSStatusItem,NSOpenPanel, and distributed notifications. - Treat binary modification, codesigning, App Management permissions, and privileged file operations as high-risk paths. Small behavior changes here can break the main workflow.
- Do not remove the fallback paths for protected apps unless you have validated both GUI and CLI flows.
- When touching config behavior, verify both app-driven and CLI-driven updates. The GUI reflects
~/.config/ghosttile/config.jsonchanges viaConfigWatcherwithout a restart.
- Always run
just formatandjust lintbefore committing. - Always run
swift buildafter code changes. - Add a focused unit test or UI test for regression fixes when automation can reasonably cover the behavior.
- If you change packaging or resources, also run
just build. - If you change app/core interaction, verify at least one CLI path and one GUI path conceptually, even if you cannot execute the full macOS workflow in automation.
- Look for crashes from forced unwraps around
NSRunningApplication, bundle URLs, and resource loading. - Check for stale UI state caused by async timing, file watchers, or
Thread.sleepcoordination. - Watch for regressions in protected-app flows: SIP detection, hardened runtime checks, backup/restore, and admin fallback behavior.
- Keep user-facing errors actionable. Prefer explicit recovery instructions over silent failure.