Coding standards
How code in Floe is written. The contribution guidelines cover issues, pull requests and sign-off, Architecture maps where things live, and Development has the build and test commands.
Where new code goes
The aim is code where no feature reaches into another. Each part knows only what it was handed.
- Decisions are plain code. What to show, what to run and what to do next is worked out by values and functions
that touch nothing outside themselves, and return what should happen.
PanelLifetimereturns effects and never acts;Ranking,CommandLookupandUpdateLogicwork the same way. These carry the tests. - A feature is handed what it uses. A feature model takes its stores and an environment, a struct of closures for
the system, the disk and the other processes, through
init.ShellEnvironment,AppLookup,ModeHostandActionHostare environments;ClipboardHistoryModelis given its store. A settings page is handed its stores from the process’sSettingsStores. A feature does not read.shared, and a test hands it fakes. - Each state has one owner. One process writes a store; the others keep a copy and read it again when told. The
stores every process saves announce and reload through
SharedStores, not through hooks set in each process. - Each process is wired in one place.
main.swiftandLauncherWindowfor Floe’s own process andSettingsModefor settings make the concrete stores and connect features, stores and messages. They are the only code that names the shared instances. LauncherModelroutes. It keeps the query, the results and which mode is on screen, and hands work to the feature that owns it. New work goes in a feature of its own, not in the model. A feature that owns rows is aRowOwner: it says what Return, its Actions menu and its keys do on them.LauncherFeaturesholds the features and is the one place that connects them to the model.
Reuse before writing
- If you’d copy a block, pull it into a shared function.
- Before writing your own ordered set, debounce, timeout or lock, check what Swift and Apple already provide, such as
Synchronization.Mutex,ObservationandContinuousClock. - Adding a package changes
Package.resolvedorruntime/bun.lockand the dependency scan. Propose it in the issue or pull request and say what it replaces. - Code ported from Droppy Code keeps its copyright and credit line in each file, which also says what Floe changed.
Comments
Write comments as plain sentences. No headings, bold, bullet lists or tables inside code comments. Say why the code does something when the code can’t show it, such as a macOS quirk or an ordering that matters. Don’t narrate what the next line does.
Writing
This covers documentation, the changelog, pull request descriptions and the text the app shows.
- Write for the person reading, in plain words. Say what they can do and what happens, and leave out how the code does it unless they need that.
- Use sentence case for headings.
- Don’t use em dashes or en dashes. Use a period, a comma, a colon or parentheses.
- Refer to a person you don’t know as they.
- Link to other files in the repository with relative links.
- Interface text goes in the String Catalog. See Localization.
Tests
- Use Swift Testing for Swift and
bun testfor the runtime. Don’t add XCTest. - Where the logic can be isolated, write the failing test first, and add a regression test for each bug fix.
- New decision logic goes in a measured file with a suite beside it.
sonar-project.propertieslists what is excluded from coverage and why. - New code needs at least 80% test coverage on SonarCloud. Don’t add files to the coverage exclusions to get there.
- Tests never talk to a real browser, the Keychain or another app. Hand the code a fake through its environment.
Formatting and lint
Floe uses SwiftLint and SwiftFormat
with Thaw’s rules, in .swiftlint.yml and .swiftformat. CI runs SwiftLint in strict mode. Before a commit:
swiftformat .
swiftlint lint --strictTests and Vendor/ are not linted. The size and complexity limits apply to new code only; what predates them is
listed in .swiftlint.baseline.
Both targets build in the Swift 6 language mode with the main actor as the default isolation and with approachable
concurrency, which in this mode is two upcoming features, InferIsolatedConformances and
NonisolatedNonsendingByDefault. Package.swift names them; project.yml sets SWIFT_VERSION,
SWIFT_STRICT_CONCURRENCY, SWIFT_APPROACHABLE_CONCURRENCY and SWIFT_DEFAULT_ACTOR_ISOLATION; keep the two in
step. A type with no annotation is main actor. Values, parsers and scanners that a worker uses are nonisolated (an
extension of one needs the word again), and shared state is an actor or a Mutex.
Unmarked async code runs where it is called: a nonisolated async function called from the main actor runs on the
main thread, and so does a closure of an unmarked async function type. Code that must leave says @concurrent: a
function that starts a process, reads or parses a file, waits on another app or decodes a picture, the function type
of a seam the main actor calls for such work, and a Task in a main actor type that must not start there. The
compiler does not check this, so a test calls each one it can reach from the main actor and looks at where the work
ran (WhatLeavesTheMainActorTests). A helper that only waits for something else stays unmarked.
@unchecked Sendable, nonisolated(unsafe) and MainActor.assumeIsolated each carry a line saying why they are
safe. In a test, what @Test(arguments:) reads is nonisolated.