Launch is a native macOS Launchpad-style launcher.
The app is intentionally small:
Launch executable entry only
LaunchApp AppKit/SwiftUI app domains
LaunchCore pure rules checked by LaunchpadCheck and LaunchpadCoreTests
LaunchCoreimportsFoundationonly.LaunchcreatesNSApplication, installsAppDelegate, then runs AppKit.LaunchAppowns AppKit, SwiftUI, persistence, permissions, input, and UI.- Pure rules go in
LaunchCore; UI and system integration stay out of it. - No top-level
Adaptersbucket. System-facing code lives in its product domain. - Owning-type folders are allowed for large AppKit coordinators.
AppDelegate/is one such domain;App/stays focused on app state, lifecycle, and actions.
Current folders:
Sources/Launch/main.swift
Sources/LaunchApp/App
Sources/LaunchApp/AppDelegate
Sources/LaunchApp/Appearance
Sources/LaunchApp/Catalog
Sources/LaunchApp/Input
Sources/LaunchApp/Launcher
Sources/LaunchApp/Layout
Sources/LaunchApp/Permissions
Sources/LaunchApp/Settings
Sources/LaunchApp/System
Sources/LaunchCore
Sources/LaunchCheck
Keep source files focused. Production Swift files should stay under 300 lines unless they are temporary phase work; split large files by product domain before adding new behavior.
Swift module style:
- Do not create barrel/export files. SwiftPM target members see each other's
internal declarations directly, so an
index.swift/export layer adds noise. main.swiftis reserved for executable entry points only.- Each domain folder should have one obvious entry file named after the owning
type or screen, such as
AppDelegate.swift,LauncherView.swift,SettingsView.swift, orLayoutStore.swift. - Default domain budget is five Swift files. If a domain needs more, either split a real subdomain or keep the exception explicit with clear entry files.
- Prefer Swift access control over export modules: keep helpers
privateor file-internal by default, and expose only the types used across domains.
LaunchCore contains rules that should be runnable without a GUI:
LaunchApp: app identity, name, path.AppCatalog: app bundle scanning and display name lookup.LayoutOrder: root grid order mutations.LaunchFolder/FolderLayout: create folders, add apps, remove apps, dissolve small folders.TrackpadIntent: convert raw gesture deltas/ratios into launcher intents.
If a rule has meaningful branches, add a LaunchpadCheck assertion or a focused
LaunchpadCoreTests case.
AppState is the single observable UI model.
It stores:
- app catalog and hidden apps
- folders and root order
- current page, selection, grid drag state
- search query
- permission states
- appearance and display settings
- launcher visibility and page drag offset
Actions are split by domain:
App/AppState.swift stored state
Catalog/AppState+Catalog.swift app scan/source/import actions
Input/AppState+Input.swift keyboard/search/page actions
Layout/AppState+Layout.swift order/folder actions
Permissions/AppState+Permissions.swift
System/AppState+System.swift app launch/Finder/trash/Dock actions
AppState does not own windows directly. AppKit side effects are exposed through
LauncherActions, wired by AppDelegate.
AppDelegate owns process-level objects:
AppStateLauncherLifecycleIconCache- menu bar status item/menu
- settings window
- global hot key monitor
- hot corner monitor
- trackpad monitor
- launcher mouse monitor
AppDelegate wires LauncherActions:
AppState action
-> LauncherActions
-> AppDelegate / LauncherLifecycle / AppSystemAdapter
This keeps SwiftUI views calling AppState while AppKit remains behind one boundary.
LauncherLifecycle owns window visibility and presentation state.
Internal phases:
hidden -> showing -> shown -> hiding -> hidden
The phase and transition token prevent stale hide/show animation completions from ordering the window out after a newer transition has started.
menu / hotkey / gesture / hot corner
-> AppDelegate
-> LauncherLifecycle.show
-> cancel old transition token
-> phase = showing
-> remember previous app
-> clear search, folder, keyboard selection
-> launcherVisible = true
-> apply window mode and menu bar/Dock presentation
-> prepare presentation layer
-> order launcher window front
-> enable launcher mouse monitor
-> animate scale/alpha
-> phase = shown
Opening does not focus search automatically. The launcher can receive keyboard navigation without grabbing typed input until the user clicks search.
ESC / toggle / spread
-> LauncherLifecycle.hide
-> phase = hiding
-> disable launcher mouse monitor
-> animate scale/alpha out
-> if transition token is still current:
launcherVisible = false
restore menu bar/Dock
order window out
reset presentation
reactivate previous app
phase = hidden
dismiss() is immediate. It disables input, restores presentation options,
sets launcherVisible = false, orders the window out, and resets presentation.
Use it for non-animated cleanup paths.
app icon click
-> AppState.launch
-> LauncherActions.launch
-> LauncherLifecycle.launch
-> AppSystemAdapter.launch
-> animate/dismiss launcher without reactivating previous app
The previous app is not restored after launching a new app, otherwise it can steal focus from the launched app.
Input is split by source:
GlobalHotKeyAdapter: Carbon global hot keys.HotCornerMonitor: pointer polling.TrackpadGestureResolver: resolves Automatic, 3/4/5-finger, and Disabled modes against system gesture conflicts.TrackpadGestureMonitor: local/global AppKit gesture monitors plus optional private multitouch contact counts.LauncherMouseMonitor: launcher-only empty-space page drag. Icon drag is SwiftUI gesture state.AppDelegate.handleLauncherKey: keyboard navigation and type-to-search.SearchFocusController: AppKit search field refs and focus state.
Trackpad flow:
NSEvent / MultitouchSupport
-> TrackpadIntent
-> AppDelegate
-> LauncherLifecycle or AppState.changePage
Mouse flow:
local mouse monitor
-> page drag when on launcher background
-> back off while an icon drag is active
SwiftUI tap / drag gestures
-> empty-space dismiss
-> folder dim dismiss
-> icon drag / folder create / folder add / reorder
Grid drag uses LaunchpadLayoutMetrics to map pointer coordinates to icon
targets and reorder slots.
LauncherView renders from AppState.
Main surface:
LauncherBackgroundView
LauncherSearchBarRepresentable
PagedGridView or search results grid
LauncherPageControl
FolderOverlay when openFolder != nil
Folder overlay uses macOS 26 GlassEffectContainer / glassEffect when
available. Older systems fall back to ultraThinMaterial.
Search is an NSViewRepresentable AppKit search bar because SwiftUI focus and
hit testing were too fragile for this app.
Launcher rendering is split by domain under Sources/LaunchApp/Launcher:
root composition, grids, controls, item views, folder overlay, and search bar.
Refresh flow:
AppState.refreshApps
-> CatalogStore.scanApps
-> LayoutCleanup.cleanup
-> save folders and order
-> ensure selection
Visible item flow:
apps - hidden - folder members
+ folders with resolved children
-> saved root order
-> current page slice
Search hides folders and ranks apps only.
Folder flow:
drop app on app
-> FolderLayout.createFolder
-> save folders/order
-> open overlay
drop app on folder
-> FolderLayout.addApp
-> save folders/order
remove app from folder
-> FolderLayout.removeApp
-> dissolve folder when only one app remains
MVP persistence is UserDefaults:
- app source paths
- hidden app ids
- root order
- folders
- grid layout
- display mode
- appearance settings
- window browsing mode
Move to files only when import/export or backup becomes a user feature.
Default checks:
swift build
swift run LaunchpadCheck
swift test
Scripts/build-app.sh
swift run Launchpad
swift run Launchpad is the only check that proves AppKit startup does not crash.
AppStateremains the central model. Split only when a domain has real independent lifecycle or a second implementation.- Private MultitouchSupport is best-effort. Keep public
NSEventfallback. - Logging is intentionally direct while input/lifecycle behavior is still being tuned.
- Folder UX still lacks internal reorder and drag-out removal.