desktop-notes
The battery/desktop dogfood app: a local-first notes app whose one
main.go runs unchanged inside the OS WebView or over plain HTTP.
What it shows
- The same screens, entity, and hooks in both hosts.
Run(app) opens
the native window; --serve :8080 (or $PORT, which gofastr dev
injects) serves the identical app to a browser. The desktop battery
stays registered in both modes; its boot gate is armed only by Run.
- One owner-scoped entity (
notes) with SearchFields on title and
body, so ?q= free-text search works on the API and the list.
- The entity screens come from
app.EntityUI (framework/entityui):
the list with its search box, view state and sort/page state riding
the page's own query string, /notes/create the create form, and
/notes/{id} the record page whose Edit tab holds the editor
(saving is a form RPC to POST/PUT /api/notes that navigates back).
No islands: every state change is a query-param navigation or a
form RPC.
- Every core capability once: File > New note (menu Navigate),
File > Export all (save dialog, Markdown file, native event), the
record page's Copy link, a saved note fires a notification (gated
by the owner's
notify_on_save preference), and the window title
follows the open note.
- A settings window:
desktop.PreferencesScreen, the battery's form
over the declared preferences (notify_on_save, export_folder,
stored in the app state; no settings entity). Config.Settings
gives it three entry points: the app menu's own Settings item
(cmd+,), the File menu's Settings… row, and the page's
windows.openSettings().
- A tray icon:
Config.Tray puts the app in the menu bar with the
app icon as a template image, a menu (Show Notes, New note,
Settings, Quit) sharing the main menu's item model, and
CloseHidesWindow so the red button hides the window instead of
quitting; Show Notes (RoleShow) brings it back.
- A plugin capability:
systeminfo.cpuCount, registered from a plain
framework.Plugin Init. It appears in the manifest, the generated
bridge, and gofastr desktop types output with no host change.
Run it
go run ./examples/desktop-notes # native window (darwin/arm64)
go run ./examples/desktop-notes --serve :8080 # same app in a browser
gofastr desktop run --pkg ./examples/desktop-notes # build + run, dev tools on
gofastr desktop build --id dev.gofastr.desktop-notes \
--name Notes --scheme gofastr-notes --pkg ./examples/desktop-notes # Notes.app on macOS; Notes.exe on Windows
--serve mode is also what gofastr dev drives (it sets $PORT), so
rebuild-on-save and livereload work on this example.
Where the data lives
os.UserConfigDir()/dev.gofastr.desktop-notes/ (macOS:
~/Library/Application Support/dev.gofastr.desktop-notes/):
app.db (SQLite), secret (session key), identity (the local user
the owner scoping keys on). Delete the directory to reset the app.
GOFASTR_DESKTOP_DATA_DIR overrides the base directory (tests use it).
The tests come in two shapes. main_test.go drives the --serve shape
through framework.TestHarness. harness_test.go drives the window
shape through desktoptest.Run: the real Run flow with the fake
shell as the OS, so the File menu, the tray, the settings window, the
export dialog, and the save notification are all exercised the way a
user reaches them.
What needs a bundle
On macOS, notifications need a signed .app because the system refuses
UNUserNotificationCenter from go run. An ad-hoc signature is enough,
and the build command applies one by default when codesign is on PATH.
Windows notifications use tray balloons. Windows deep links need a
build made with --scheme; the app registers the scheme for the current
user when it starts. On hosts without a native shell, Run reports the named
unsupported error, and a configured tray logs a Warn.