* Add an opt-in settings.json mirror for preferences and window management * Let Snippets and Notes use a chosen folder, from Settings or settings.json * Keep a Notes or Snippets folder change from reusing the old folder's contents * format fix
9.3 KiB
Tinycast
A native macOS menu-bar launcher: fuzzy app launcher, global and per-app hotkeys, a text/image
clipboard history, an inline calculator, a floating note, snippets, quicklinks, window management
and an emoji picker. It also runs Raycast extensions natively, in JavaScriptCore.
SwiftUI + AppKit, running as an accessory with no Dock icon (LSUIElement). Zero third-party
dependencies.
Posture: latest-only, always
Tinycast targets one macOS — the current stable release — and nothing else. macOS 26+, the Xcode 26 toolchain, Swift 6 language mode. There is no compatibility floor to defend, no shim layer and no deprecation debt, and that is the single largest reason the codebase stays as small as it does.
Write code as if the platform released yesterday:
- Prefer the modern Apple API, always. Observation over
ObservableObject. Swift Concurrency overDispatchQueueor completion handlers.SMAppServiceover login-item shims. Structured concurrency over detached bookkeeping. - Migrate, never wrap. When an API gains a modern replacement, adopt it and delete the old call site. A wrapper that preserves an old spelling is the thing this project has spent the most effort removing.
- A deprecated API is a defect, not a warning to live with.
- No compatibility layers, no legacy workarounds, no older architectural patterns. Delete rather than deprecate; raising the minimum macOS deletes the code that supported the old one.
- Never introduce backwards compatibility unless explicitly asked for it. No version flags, no migration scaffolding, no "just in case" fallbacks. The codebase carries no migration, and adding one needs an explicit task saying so.
Carbon is a deliberate capability-gap dependency rather than inertia: nothing modern registers a system-wide chord, and HIToolbox's TIS APIs remain the public input-source mechanism. Full reasoning in standards.md.
Where things are
| Folder | Holds |
|---|---|
Tinycast/App/ |
@main, AppDelegate, AppCore — the composition root |
Tinycast/DesignSystem/ |
shared visual primitives; Theme.swift is the only design-token source |
Tinycast/Platform/ |
system shims: Permissions, AppPaths, Signposts, NotificationToken, … |
Tinycast/Palette/ |
the palette shell: panel, window controller, RootPaletteView, PaletteScreen |
Tinycast/Windows/ |
the non-palette AppKit surfaces: Dialog/, HUD/, About/, AppWindowController |
Tinycast/Features/ |
one folder per feature; larger ones split Model/ Service/ UI/ Settings/ |
Tests/ |
the standalone harnesses — one Swift file each, no XCTest target |
Scripts/ |
every executable script: test runner, data generators, packaging, linting, editor setup |
| Read it before you | Doc |
|---|---|
| change how anything is wired or owned | architecture.md |
| write Swift — naming, style, concurrency, budgets, comments | standards.md |
| claim a change is done | testing.md |
| build, run or regenerate data | development.md |
| add or restyle any view | ui.md |
| touch one feature's internals | features/ — each opens with its invariants |
| package or ship a build | release.md |
Non-negotiables
Never break these without an explicit task to do so. Anything feature-specific lives in that
feature's doc, under its own ## Invariants.
AppCoreis the sole owner. New long-lived state goes onAppCore, wired instart()— never a competing singleton. Views reach a feature's coordinator through@Environment, notAppCore.- A file under
Features/*/Model/may not import AppKit or SwiftUI, and takes every environment fact — clock, filesystem, home directory, rates — as an injected parameter. The harnesses compile the shipped sources, so this is enforced by compilation rather than convention. - Swift 6 language mode: data-race violations are hard errors.
@MainActoris the default, cross-actor model types areSendable, and heavy or IO-bound work goes off-main asnonisolatedfunctions driven byTask.detached. Do not add a second actor. - Dark is the baseline, and a colour's dark branch is the literal it always was.
Theme.Colorsresolves per appearance throughramp/adaptive; every dark value is theColor.white.opacity(…)the forced-dark build shipped, restated rather than re-derived. Retune a light branch freely — change a dark one only when the task is to change Dark.AppAppearancedrivesNSApp.appearance, and.systemmaps tonilso AppKit follows macOS on its own. - Tinycast presents its own dialogs — never
NSAlertor a system popover. A question goes throughDialogController, a report through a HUD viaHUDPresenter. - A networked feature fetches on a private
.ephemeral,urlCache = nilsession, neverURLSession.shared, so its own cache file stays the only copy on disk.CurrencyRateStoreis the reference — copy it rather than inventing a second shape. A flag that grants a capability is never carried by a backup or bysettings.json:snippetsEnabledis excluded from settings backups so an import cannot grant keystroke listening. - Extensions stay inside
Features/Extensions/. Every view, row, menu, geometry and sizing constant an extension needs is written and owned there — never added toDesignSystem/, never bolted ontoTheme, and never lifted somewhere another feature can build on it. Another surface may render one as an opaque box —LauncherScreendoes exactly that withExtensionArgumentsAccessory— but it never reaches inside one. An extension renders untrusted third-party code whose shape we do not control, so it must never be able to force a change on a launcher surface. Duplicating a view or a piece of layout maths to keep it here is the correct trade, and the one place the no-duplication rule yields. What is shared:Theme's base tokens (spacing, radius, colour),InterfaceMetricsas the view over those same base tokens,PopoverMenuItemas a data shape, andPlatform/. What is never shared: anything with "how an extension looks or moves" in it.ExtensionActionsPanelandExtensionGridGeometryexist precisely because the palette's own menu and the emoji grid must stay free to change without them. AppEntry.Kindis the only thing that says what an entry is. One case per launcher section and perVisibilityStorecategory — never re-derive a category by sniffing an entry ID. Which pane lists a command is a separate fact, andSettingsTab.ownedCommandsis the only place that states it.- Generated files are never hand-edited.
EmojiData.generated.swiftcomes fromnode Scripts/gen-emoji.js,CurrencyData.generated.swiftfromnode Scripts/gen-currencies.js,CountryZoneData.generated.swiftfromnode Scripts/gen-countries.js, andResources/RaycastRuntime.generated.jsfromScripts/raycast-runtime/build.mjs— the runtime is committed so building the app never needs Node. DesignSystem/Scrolling/EdgeDissolve.swiftandThinScrollbar.swiftare off-limits. Both are tuned by eye against the palette's floating bars, so any edit is a visual regression. Needing to touch one to fix a scroll bug means the real fix belongs elsewhere.
Conventions worth knowing up front
- A new preference also gets a
SettingsFileKeyand its binding inSettingsFileSchema, so the opt-insettings.jsonmirror carries it; the exhaustive switch fails the build until it is bound. See settings-file.md. - A type's suffix says what it is —
Store,Coordinator,Controller,Manager,Engine,Policyand the rest each name a specific responsibility. Semantic correctness always wins over suffix consistency: pick the suffix that describes the type honestly, add a new one when none fits, and never rename a well-named type just to match the table. Full table: standards.md#naming. - Comments are rare, one line, and explain the why — the gotcha or invariant, never the what. Never two in a row, never extended into a block: if one line can't carry it, name a function, constant or type instead. Cap 100 characters, delete rather than update, and never comment a change you just made. Nothing lints this; get it right the first time. Full rules: standards.md#comments.
- Debug builds are their own channel —
Tinycast Dev.app/com.tinycast.app.dev— so a local run never shares prefs, caches, TCC grants or the login item with an installed copy. Anything newly persisted must stay keyed byBundle.main.bundleIdentifier. - XcodeGen owns the project.
Tinycast.xcodeprojis committed but generated fromproject.yml; after editing it, runxcodegen generateand commit both. No SwiftPM, and neverBundle.module.
Before you finish
Each item is explained in testing.md.
./Scripts/run-tests.shpasses.- The Debug build compiles with no new warnings.
./Scripts/lint.shis clean.grep -rln 'import AppKit\|import SwiftUI\|import Cocoa' Tinycast/Features/*/Model/returns nothing.- Any doc your change made wrong is fixed in the same commit.