Developing Floe
Prototype macOS launcher that runs unmodified Raycast extensions: a SwiftUI panel plus a Bun process per running command.
Contribution requirements, review expectations, and security reporting are in the shared Thaw/Floe policies.
Requires macOS 26 or later, Bun, Rust/Cargo, and Xcode 27 for the vendored ThawUI Swift 6.4 manifest. Extensions run under your user account without sandboxing; only install extensions you trust.
Run
cd runtime && bun install && cd ..
./scripts/build-fend.sh
open Floe.xcodeproj # ⌘R in Xcodeor install it as /Applications/Floe.app with ./scripts/devrun.sh (--debug, --no-launch). The script signs
the app with the first valid Apple Development certificate in the keychain (FLOE_SIGN_IDENTITY names another), so
Accessibility and the Keychain stay granted from one build to the next. With no certificate the build is ad hoc and
macOS asks again each time, while System Settings goes on showing the old grant as switched on.
⌃⌥Space toggles the panel. ↑↓ select, ↵ runs the primary action, ⌘K opens the action menu, Esc goes back.
Floe.xcodeproj is generated from project.yml by XcodeGen; edit the spec and run xcodegen generate
rather than changing project settings in Xcode. Package.swift stays for swift run and the CLI checks below.
The Run scheme has two disabled environment variables: FLOE_AUTORUN opens a command at launch,
FLOE_DEBUG logs focus changes.
Settings
⌘, in the panel, “Floe Settings” in search, or the menu bar icon. General has the launcher hotkey, launch at login
and whether to include Raycast’s installed extensions. Each extension has an on/off switch, its preferences, and
per-command aliases and hotkeys. Password preferences are kept in the Keychain; a build run from Xcode is
signed ad hoc, so macOS asks again after each rebuild, which devrun.sh avoids.
Commands with required preferences or arguments ask for them in the panel before they run.
Settings is a process of its own. The launcher starts this same executable again with --settings
(SettingsProcess.swift), one at a time, and that process ends when its window closes, which gives back the memory
the settings pages took. The launcher builds no settings view. Being the same executable, the settings process
has the app’s defaults, Keychain items and permissions. The two keep in step with a few distributed notifications,
all listed in ProcessLink.swift: whichever saves the settings, the snippets or the quicklinks says so and the
other reads them again, and the launcher does for Settings what only it can: the updater, the clipboard history,
rescans of scripts and extensions, and quitting. A message is acted on only when it names the other process of the
pair as its sender. The welcome window at first launch is still the launcher’s.
Layout
-
Sources/Floe: the app: panel, hotkey, app index, and a renderer for the JSON tree the host sends. One folder per area, and no Swift file outside a folder:App: the entry point and app delegate (main.swift), the main menu, thefloe://link router, the debug options, the update rules and the generated credits. Also the launcher’s side of Settings: the process handle (SettingsProcess.swift), the messages (ProcessLink.swift) and what the launcher does with them.Launcher: the panel’s model (Model.swift, with oneModel+….swiftper view it drives and one for the keys) and its root views (Views.swift,IconView.swift), its layout and appearance, the Actions menus, the catalog of apps and commands with its scans (Catalog.swift, which also holdsPaths), and Markdown.Search: ranking and fuzzy matching, the command lookup the App Intents use, andSearchProviders(below).BuiltIn: what Floe does itself: the calculator, system commands and toggles, System Settings panes, emoji, clipboard history, snippets and their expander, quicklinks, the calendar, the file search and its index of file names (FileIndexService.swift; the index itself is Rust, inVendor/FendCore/rust/src/index.rs), browser tabs, menu bar items, script commands and Thaw’s actions, with the AppleScript runner and the selection and pasteboard helpers.AI: Ask AI and the sources that answer a prompt. An answer can be followed up:AskAIModelkeeps the turns of one conversation in memory while the answer view is open and drops them when it closes; none of it is saved or logged. A question travels with its earlier turns as anAIConversation, and each source is given them its own way (AISources.live): an API as user and assistant messages, Apple Intelligence as the transcript of a session made for that one request, and a command line tool, which starts fresh every time, written out in its prompt (AIConversation.replayedPrompt).AIConversation.Limitcaps what is sent again, oldest turns dropped first: 8 turns and 12,000 UTF-8 bytes, or 6,000 for Apple Intelligence’s 4,096 tokens. An extension’sAI.askis a conversation with no earlier turns.Extensions: a running command (Session.swiftand its extensions), what it asks the app for (HostRequest.swift), the manifest, the store, menu bar commands, the background scheduler, OAuth, hot reload, and the views an extension’s forms and errors are drawn with.Settings: the settings model (Settings.swift), its window, pages and sections, settings search entries, import and export, and the Keychain and preference stores.SettingsMode.swiftis the settings process, andSettingsCatalog.swiftthe commands, scripts and apps it lists.Sync: settings sync through iCloud’s key-value store, written to be moved to Thaw as it is. It knows records, not Floe’s settings (see “Settings sync”).Picker:Floe --pick.PreferredApps,ThawandDroppyCodeare described below.Tests/FloeTestshas the same folders.
-
runtime/host.ts: bundles a command, renders it with a custom React reconciler, speaks NDJSON on stdio. -
runtime/api/index.ts: the@raycast/apistand-in. -
extensions/: one folder per extension (hellois a sample,diagnosticsfails on purpose to exercise the error screen). -
Sources/Floe/Search/SearchProviders: what the root search is made of. Each provider answers a query with rows to rank, to pin on top, to list under a section or to append (SearchProvider.swift);RootSearch.swiftlists the providers and merges what they answer.LauncherModel.refresh()only builds the context they read.- A scope (
SearchScope.swift) is a search of its own inside the root search:files invoice,clipboard meetingandmenu wifishow only that scope’s rows. It triggers on its keyword, a space and some text; the keyword alone is an ordinary search. A scope may deliver rows later through anAsyncStream, each batch replacing the last;SearchUpdatesdrops batches that arrive for a query that has been replaced. - A source (
SearchSource.swift) is a scope that may also add up to three rows to an ordinary search, in a section of its own below the ranked rows and above the appended ones. Each is off until its switch in Settings › Privacy is on (AppSettings.searchSources), and its keyword works only while it is.SourceSearchasks the enabled sources after the ranked rows are shown, for queries of three characters or more, and follows each with its ownSearchUpdates. The files source isFileSearchScope; the tabs source isTabSearchSource. BuiltIn/BrowserTabs.swiftholds the browsers whose tabs can be listed, oneBrowserAppentry each, with the scripts that list and switch tabs. Add a browser only after reading its scripting definition (sdef /Applications/<App>.app). The script runner is passed in, so tests never talk to a browser; do not run these scripts from a test.
- A scope (
-
Sources/Floe/PreferredApps: the apps Floe hands things to instead of doing their work. A role (AppRole.swift) is a kind of app the user has a preferred one of: what it is handed, the known apps offered by name and what stands in when nothing is chosen.PreferredApps.swiftdecides which app a role resolves to and what it is handed, without opening anything; it reads the Mac’s apps throughAppLookup, which tests replace. Add a known app only with a bundle identifier read from an installed copy’sInfo.plist.- The terminal and the editor are handed files and folders, so they share one path:
NSWorkspaceopens the items with the app. Notes is handed text, which each app takes its own way, so it has its own adapter (Notes.swift): a link for Antinote or a custom app, a script for Apple Notes. A new role is a case ofAppRoleplus, when it takes text, an adapter like that one. - The clipboard role is handed nothing: Clipboard History opens the chosen app, or a link, in place of Floe’s own
history (
ClipboardApp.swift).ClipboardApps.destinationdecides where the command goes and never falls back to Floe’s history;recordsis the one place that says whether copies are saved (the role is Floe and the switch is on). An app whose history opens from a link goes inClipboardApps.links, with a link confirmed from the installed copy.
- The terminal and the editor are handed files and folders, so they share one path:
-
Sources/Floe/Thaw: code ported from Thaw: hotkeys (key codes, Carbon registry, recorder), the HUD, the About page, onboarding and permissions, settings search, the Sparkle updater with its consent sheet, and the App Intents that Shortcuts and Spotlight use (FloeIntents.swift). -
Sources/Floe/DroppyCode: code ported from Droppy Code. Each file keeps Droppy Code’s copyright and credit line and says what Floe changed;LICENSESholds its license and third-party notices.LoginEnvironment.swiftreads the login shell’s environment, which extensions start with, andShell.swiftruns a tool with a timeout.AI.askis answered by a one-shot run of a command line tool (TextGeneration.swift:claude,codex,opencodeorpi, each with no tools of its own) or by a streamed request to an OpenAI-compatible API (ChatCompletionStream.swift). Settings › General › AI picks between them; the choice and the request an extension makes are inExtensions/HostRequest.swift, and which tool answers is inAI/AITool.swift.ToolTextStream.swiftreads a tool’s JSON lines as they are printed. What a line means is inClaudeTextStream.swift,OpencodeTextStream.swiftandPiTextStream.swift, beside each tool’s arguments.HangWatchdog.swiftsamples the app when its main thread stops answering for four seconds and writes the stacks to~/Library/Logs/Floe.DirectoryWatcher.swiftwatches a folder tree with FSEvents.Extensions/HotReload.swiftuses it to restart an open view command when a file under its extension’ssrc/orassets/, or itspackage.json, is saved. Only local extensions with asrc/folder are watched, never the ones Raycast installed.
-
CREDITS.mdandSources/Floe/App/Credits.swiftare written byscripts/generate-credits.py; run it after changing a dependency. -
Vendor/ThawUI: design system copied from thaw-app/Thaw (commit inVendor/ThawUI/UPSTREAM). -
Vendor/ThawConcurrency: Thaw’s timeout and one-shot continuation helpers, copied the same way.
License
AGPL-3.0. ThawUI and the other code from Thaw stay under GPL-3.0, which the AGPL allows combining with. The code from Droppy Code is AGPL-3.0 with the attribution terms in LICENSES/DroppyCode-LICENSE. LICENSES/README.md says which license covers which folder. Extensions under extensions/ keep their own licenses.
Adding an extension
Extensions live in ~/Library/Application Support/Floe/Extensions; Raycast’s installed ones are picked up
from ~/.config/raycast/extensions. To add a store extension, copy its folder from
https://github.com/raycast/extensions there and run bun install --ignore-scripts inside it (a full install:
some extensions import dev dependencies at runtime). Per-extension storage, preferences and cached bundles go to
…/Floe/Data/<name>.
The app is self-contained: the build copies runtime/ and Bun into Contents/Resources/runtime. Set
FLOE_ROOT to a checkout (or use swift run) to work against the checkout’s runtime and its extensions/
samples instead, including diagnostics, which fails on purpose to exercise the error screen.
Keys
| Key | Where | Does |
|---|---|---|
| ↑↓, Page Up/Down, Home/End | lists, action menu | move |
| ↵ / ⌘↵ | lists | first / second action |
| ⌘K | lists | action menu; type to search it, → and ← in and out of submenus |
| ⌘⇧F | root | add or remove a favorite |
files , clipboard , menu then text | root | search only files, the clipboard history or menu bar items |
| ↵ | menu bar search | open the item’s menu (needs Accessibility) |
| ↵ / ⌘↵ / ⌘R | Ask AI | ask what is typed, or copy the answer when the field is empty / paste the answer / ask again |
| Esc, ⌘[ | commands | back |
| ⌘, | anywhere | settings |
| ↵, ⌘⇧C | error screen | try again, copy details |
Tests
swift test # Swift Testing suites in Tests/FloeTests
bun --config=runtime/bunfig.toml test ./runtime # runtime suites in runtime/tests
./scripts/coverage.sh --summary # both, with coverage per measured fileThe Swift tests import the app’s module directly and never launch it. Logic lives in files that can run
in a test (Ranking, Manifest, ViewState, MarkdownParser, Shortcuts, PropFormat, Preferences,
MenuBarLogic, Settings, Session, CommandLookup, ThawHUDPlacement, UpdateLogic, the onboarding sequencer,
permission state and the settings search index); views, the process, the Keychain and Accessibility code are excluded
from coverage in sonar-project.properties, which also states the rule. New decision logic belongs in a
measured file with a suite beside it.
Code style
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.
Localization
Every piece of interface text Floe writes is in one String Catalog, Sources/Floe/Resources/Localizable.xcstrings.
English is the source and, for now, the only language. The English text is the key, so the app reads the same with
or without the catalog; the catalog only adds to it the plural forms and the numbering of placeholders.
How a string gets there. The compiler finds it (SWIFT_EMIT_LOC_STRINGS in project.yml), and a build writes what
it found beside the object files. The Xcode app merges that into the catalog after a build; xcodebuild does not, so
from the command line run:
./scripts/sync-strings.sh # build, then add new strings and drop the ones no longer in the code
./scripts/sync-strings.sh --check # build, then fail if the catalog would changeCommit the catalog with the change that adds the string. Never write a key into the catalog by hand.
How to write one:
- A SwiftUI label written as a literal (
Text("Favorite"),Toggle("Enabled", isOn:),.help("Open Settings")) is already a key. Leave it as it is. - A plain
String(a HUD line, a row title, a menu item, an alert, a failure message) isString(localized: "Copied Path", bundle: .floe).Bundle.floe(App/Localization.swift) is the bundle the catalog is in: the app in an Xcode build, the module’s bundle in a SwiftPM build and in the tests. A bareString(localized:)reads the main bundle and finds nothing in aswift buildbinary. Use the initializer itself, not a helper around it: the compiler only takes thecomment:from the real one. - A value goes in by interpolation, so the translator gets the whole sentence with a placeholder:
String(localized: "Quitting \(app.name)", bundle: .floe, comment: "The placeholder is an app's name."). Never join pieces. Where the wording depends on a case, write one whole sentence per case (AIEndpoint.problem). - A count takes its plural forms from the catalog, not from an
if:String(localized: "\(total) items", bundle: .floe), and in the catalog the key%lld itemsvaries by plural withoneandother(see that entry; Xcode’s catalog editor offers “Vary by Plural”).LocalizationCatalogTestslists the plural keys, so a new one is added there too. An integer in a localized string is written with the locale’s grouping: 1,000 items. comment:is one plain sentence for the translator wherever the English is ambiguous out of context: a single word (“Open”, “Run”, “Copy”), anything with a placeholder, and a sentence that quotes a word the user types. A SwiftUI literal has nocomment:to give, so its comment is written on the entry in the catalog; a sync keeps it.- One English word with two meanings is two keys, since another language may need two words. The second names its
use and gives the English apart:
String(localized: "None (tint)", defaultValue: "None", bundle: .floe). The System Settings panes and the Restart command are written this way. - Words that only find something in a search are one string, a list separated by commas, which a translator replaces
with as many terms as the language needs (
String.keywordList,String.searchTerms). - A label that is not text (
100%,0°) or that only passes text through (%@) is marked “Don’t translate” in the catalog.
What is not localized, and so is a plain String or Text(verbatim:): what an extension renders or declares, the
user’s own data (app, file, quicklink, snippet and shortcut names, SSH aliases, the query), log lines and the output
of the command line checks, ids, stored settings keys, URLs, key names, the generated credits, release notes, and
prompts sent to an AI tool. Scope keywords (files, clipboard, menu, tabs, ssh, shortcuts, ask, note)
are commands and stay English; a sentence that mentions one says so in its comment. Text that is not Floe’s must
never become a key: no LocalizedStringKey(someString). Where a view only takes a key (ThawUI’s ThawEmptyState),
pass .verbatim(text), which makes the text an argument of the key.
Search matches a title in the language it is shown in, since the row is built with the localized title, and keywords are localized lists of extra terms. Nothing is keyed on a title: ids, settings keys and scope keywords are separate constants.
To try it without a translation, add a second language to the catalog with one string visibly changed, build, and
run with that language: .build/debug/Floe --search "" -AppleLanguages "(fr)", or the same arguments to
Floe.app/Contents/MacOS/Floe from an Xcode build. Remove the language afterwards. Text that SwiftUI resolves itself
(the literals in the first point above) is read from the main bundle, so it only changes in the Xcode build; a SwiftPM
binary shows those in English.
Translations come from Crowdin, as Thaw’s do: https://crowdin.com/project/floe. crowdin.yml points Crowdin at the
catalog, and translated languages arrive as pull requests from Crowdin that change that one file. Nobody edits a
translation in the repository: .github/workflows/block-translation.yml closes a pull request that only changes the
catalog and is not Crowdin’s, with a note that sends its author to the project. The Credits page has a “Help
translate” button that opens it (translate in FloeLinks). Still to do once languages exist: the translators list
in CREDITS.md, which Thaw’s scripts/generate-credits.py writes from a Crowdin export.
Releases and updates
Floe updates itself with Sparkle, the same way Thaw does. The pieces:
SUFeedURLandSUPublicEDKeyinproject.yml(the Info.plist source). The feed ishttps://thaw-app.github.io/Floe/appcast.xml, served from this repository’sgh-pagesbranch.Sources/Floe/Thaw/Updates.swiftwraps Sparkle. WhileSUPublicEDKeyis empty the app builds no updater: “Check for Updates…” is absent from the status menu, the About page has no updates card, and General has no “Automatically check for updates” switch. The rules are inApp/UpdateLogic.swift.- With a key, the first time Settings opens a sheet asks whether to check automatically. Sparkle does nothing before that answer except a check the user starts.
.github/workflows/release.ymlbuilds an existing tag, notarizes it, zips the app, signs an appcast item for it with theSPARKLE_ED25519_PRIVATE_KEYsecret (prodenvironment), attaches the ZIP to the GitHub Release beside the DMG, and pushesappcast.xmltogh-pagesafter the release is published. A tag whose Info.plist has no key skips the Sparkle steps and ships the DMG alone.
Switching updates on, once:
swift build, then.build/artifacts/sparkle/Sparkle/bin/generate_keys --account floe. The private key stays in your login Keychain; the command prints the public key.- Put the public key in
SUPublicEDKeyinproject.ymland runxcodegen generate. .build/artifacts/sparkle/Sparkle/bin/generate_keys --account floe -x ~/floe-sparkle.key, then./scripts/set-secrets.shand answer@~/floe-sparkle.keyforSPARKLE_ED25519_PRIVATE_KEY. Delete the file.- After the first release has pushed
appcast.xml, turn on GitHub Pages for thaw-app/Floe: Settings, Pages, deploy from thegh-pagesbranch, folder/.
Each release:
- Raise
CFBundleShortVersionStringandCFBundleVersioninproject.yml(andsonar.projectVersion), runxcodegen generate, commit. Sparkle orders builds byCFBundleVersion, a whole number that must go up every time; the workflow stops if it does not. - Tag the commit with the version (
0.2.0, or0.2.0-beta.1for the beta channel) and push the tag. ./scripts/release.shpicks the tag and dispatches the workflow. Try a dry run first: it builds and reports the appcast diff and publishes nothing.
Pushing the appcast needs “Publish release” checked, because the appcast links to the release’s ZIP. The
Sparkle tools version in release.yml (sparkle-version and its checksum) must match Package.resolved.
To rehearse an update with a Debug build, serve an appcast locally and point the build at it:
defaults write com.thaw.floe FloeDebugFeedURL http://localhost:8000/appcast.xml. Release builds
ignore that key, and without it a Debug build refuses to check.
Settings sync
Off by default; the switch is in Settings › General. Sources/Floe/Sync is the engine and names no type of Floe’s:
SyncRecord.swift (a record and the merge, with no store or clock in it), SyncStore.swift (the store’s protocol
and how a record is written into it), UbiquitousSyncStore.swift (iCloud’s store, and the two checks that say
whether it may be used), MemorySyncStore.swift (a store and a pretend cloud for tests) and SyncEngine.swift.
Floe’s part is Settings/SettingsSync.swift, the table of what syncs and what stays on one Mac, and
App/SettingsSyncService.swift, which runs in the launcher. A stored setting in neither list of the table fails
SettingsSyncTests and is not synced.
- One record per item: an alias, a hotkey, a hidden result, a snippet, a quicklink, or a whole setting such as the layout. Favorites, snippets and quicklinks have one record per member and one more for their order.
- The newest change wins, by the clock of the Mac that made it; a tie goes to the higher device id. A change made after a record was seen is stamped later than that record, so a Mac with a fast clock cannot pin a value. Two Macs that change the same record without seeing each other are ordered by their clocks, which may be wrong.
- A removal is a record too, forgotten after 90 days. A Mac away for longer can bring the value back.
- Changes are dated while sync is off as well (
Sync.jsonin the support folder), so switching it on merges by age. A setting still at its default counts as never changed and is never uploaded. This keeps a fresh Mac’s empty local cache from overwriting a cloud value before the first download. An explicit edit back to a default still syncs. - Removing the cloud copy leaves a clear marker. Each Mac remembers markers it has actually seen, not its local clock when it enabled sync. A deliberate join accepts an existing marker from the cache or first download; a restart with sync already on still obeys an unseen removal.
- iCloud’s store takes 1 MB, 1024 keys and keys of 64 bytes. The engine stops writing at 90% of the first two and says so in the status; a longer key is stored under a digest. The number of keys is what runs out first: a test’s heavy user (150 aliases, 60 hotkeys, 60 hidden results, 40 favorites, 200 snippets, 57 quicklinks) is 587 records and 220 KB.
- No test touches iCloud. Nothing here has run against the real store, on a signed build or between two Macs.
A build signed ad hoc has no entitlement for iCloud, says “Unavailable” and never opens the store
(SyncAvailability.system asks the running process). Two files hold the entitlements:
Resources/Floe.entitlementsis what every build is signed with. It holds what the hardened runtime of a release needs to send Apple events and to ask for Calendar, Reminders and Contacts. Without them a release is refused with no prompt (tried: an app with the hardened runtime and no Apple events entitlement gets -1743).Resources/Floe-iCloud.entitlementsadds iCloud’s key-value store and time-sensitive notifications. Those two need the provisioning profile, and an app signed with them and without it does not launch.
To ship sync, in this order:
- developer.apple.com, Identifiers: open
com.thaw.floe, or register it as an explicit App ID if it is not there, and switch on iCloud. The key-value store needs no container. - Profiles: add a Developer ID profile for that App ID with the Developer ID Application certificate the release
uses, name it
Floe Developer ID, and download the.provisionprofile. - Add it to the
prodenvironment as a secret, for exampleAPPLE_PROVISIONING_PROFILE:base64 -i Floe_Developer_ID.provisionprofile | gh secret set APPLE_PROVISIONING_PROFILE --env prod. project.ymlalready setsCODE_SIGN_ENTITLEMENTS: $(FLOE_ENTITLEMENTS)andPROVISIONING_PROFILE_SPECIFIER: $(FLOE_PROFILE)on the Floe target, with the first file and no profile as the defaults. On the target and not on the command line, where they would reach the package targets too.- org-ci has what this needs from the commit that adds provisioning profiles:
configure-signinginstalls the profile and outputs its name,buildtakes extra build settings, andexport-and-packagenames the profile for a bundle identifier. release.ymlandbuild-dmg.ymlpass the secret toconfigure-signing, passFLOE_ENTITLEMENTS=Resources/Floe-iCloud.entitlementsandFLOE_PROFILEto the build when a profile was installed, and check the exported app before it is notarized. Without the secret they sign as before.- Before publishing, check the exported app:
codesign -d --entitlements - Floe.appshowscom.apple.developer.ubiquity-kvstore-identifierwith the team id in front, andFloe.app/Contents/embedded.provisionprofileexists. Then install it on two Macs and try the switch.
Read from the code: the build action passes a fixed list of settings (DEVELOPMENT_TEAM, CODE_SIGN_STYLE=Manual,
the Developer ID identity, the hardened runtime) and has no input for more, and the export action writes
signingStyle manual with no provisioningProfiles. So both need the change in step 5. From Apple’s documentation
or from memory, and not tried here: that this entitlement needs a provisioning profile outside the App Store,
that an app signed with it and without the profile is refused at launch, that the key-value store needs no
container, where Xcode looks for profiles, that an empty CODE_SIGN_ENTITLEMENTS means none, that settings given
on the command line reach package targets, and that an export with manual signing must be told the profile.
Checks
bun runtime/smoke.ts extensions/hello planets # host only
.build/debug/Floe --selftest hacker-news frontpage # Swift ↔ Bun round trip
.build/debug/Floe --search cal # ranked root results
.build/debug/Floe --icon-check # every extension’s icon, rendered off screen
.build/debug/Floe --menubar [query] # menu bar items Floe can open
.build/debug/Floe --panel-snapshot /tmp/p # root, menu bar and answer views, drawn off screen
FLOE_BENCH_DUMP=/tmp/s .build/debug/Floe --bench-settings # settings page timings and snapshots
bun runtime/survey.ts ~/.config/raycast/extensions # compatibility across many extensions
FLOE_SETTINGS_CLOSE_AFTER=3 .build/debug/Floe --settings --page about # the settings process alone; exits 0 when its window closes
FLOE_OPEN_SETTINGS=5,20 swift run # the launcher opens Settings by itself after 5 and 20 seconds
printf ‘a\nb\n’ | FLOE_PICK_AUTO=1 .build/debug/Floe --pick --query b # the picker, without its panel
FLOE_AUTORUN=hacker-news/frontpage swift run # open straight into a commandPicker
Floe --pick lends the search panel to a script, the way dmenu does. It reads one item per line on standard
input (blank lines are dropped), shows them under the search field, and prints the line you choose:
ls ~/Projects | Floe --pick --prompt “Open project” | xargs -I{} code ~/Projects/{}Floe here is the binary inside the app, Floe.app/Contents/MacOS/Floe. --prompt <text> sets the search
field’s placeholder, --query <text> is the text it starts with, and --index prints the chosen line’s position
in the input, counting from 0 and counting blank lines, instead of its text.
| Exit status | Meaning |
|---|---|
| 0 | A line was chosen with Return or a double click, and printed. |
| 1 | Nothing was chosen: Escape, the panel lost focus, or the input had no items. |
| 64 | Standard input is a terminal, or an option is wrong. |
It is its own short process (Picker/PickerPanel.swift, with the logic in Picker/Picker.swift): it reads the Appearance
settings and starts nothing else of the app. FLOE_PICK_AUTO=1 prints the first match for --query without
showing the panel, and FLOE_PICK_SNAPSHOT=<file> saves a picture of the panel drawn off screen.
Menu bar item search
“Search Menu Bar Items” (root search, or its hotkey in Settings › General) is drawn with the launcher’s own field, rows and bottom bar at the launcher’s size, so the panel does not change shape on the way in. It does what Thaw 3’s inspector search panel does. Thaw finds and opens items through MenuBarModel and its own runtime; Floe reads each app’s extras menu bar through the Accessibility API and opens an item by pressing it, so it needs only the Accessibility permission. It shows no previews of the items, so it never asks for Screen Recording; items Thaw keeps hidden may not open from here.