Skip to content

Build the Desktop Tray App ​

services/desktop (@tt/desktop) is a small Tauri app for macOS and Windows: an icon in the menu bar / system tray that opens a popup where a worker adds a time entry.

It is not a second frontend. The popup is a webview pointed at the deployed web client's /tray route (services/client/src/views/TrayQuickAdd.vue), so the form, validation, project and task pickers, offline queue and sign-in are exactly the ones the web app ships, and the session cookie is a first-party cookie on the API's own origin — no bearer tokens, no CORS changes. The Rust shell only owns what a browser tab cannot: the tray icon, positioning the popup next to it, hiding it on focus loss, and telling the page when it has just been shown.

Where things live ​

PathContents
services/desktop/src-tauri/src/lib.rsThe whole shell: tray icon + menu, popup window, show/hide/toggle, URL guard
services/desktop/src-tauri/tauri.conf.jsonProduct name, identifier, bundle targets, icons
services/desktop/src-tauri/capabilities/default.jsonWhat the remote page may call over IPC, and from which known origins (the configured server's origin is added at startup)
services/desktop/src-tauri/icons/Bundle icons (generated) and the tray icons
services/desktop/ui/index.htmlPlaceholder frontendDist; never shown
services/client/src/views/TrayQuickAdd.vueThe page the popup loads
services/client/src/components/EntryQuickForm.vueThe quick-add form, shared with the "Nouvelle saisie" sheet
services/client/src/lib/desktop.tsThe page's side of the bridge: hide the popup, listen for "shown"
.github/workflows/desktop-build.ymlManual workflow that produces the .dmg and Windows installer

Prerequisites ​

  • The repo's usual toolchain (Node 22, pnpm) — pnpm install installs the Tauri CLI.
  • A stable Rust toolchain: https://rustup.rs.
  • Tauri's platform dependencies, see https://tauri.app/start/prerequisites/:
    • macOS: Xcode Command Line Tools.
    • Windows: Microsoft C++ Build Tools and the WebView2 runtime (already on Windows 11).
    • Linux / WSL (for cargo check and dev runs only — there is no Linux release): libwebkit2gtk-4.1-dev, libgtk-3-dev, libayatana-appindicator3-dev, librsvg2-dev.

Run it against your local client ​

The popup loads a URL, so point it at the Vite dev server and run the shell:

bash
pnpm dev:client                                   # client on http://127.0.0.1:35173
TIM_DESKTOP_URL=http://127.0.0.1:35173 pnpm --filter @tt/desktop tauri:dev

Click the tray icon: the popup opens on /tray, and the client's /login page if you are signed out. Hot reload works as usual because it is the client's own dev server.

tauri dev compiles the Rust side on first run (a few minutes); later runs are incremental.

Compile-check without running ​

bash
pnpm --filter @tt/desktop cargo:check

This is the Rust equivalent of pnpm typecheck and works on Linux/WSL too. It is not part of the CI pipeline (the runners have no webkit), so run it before opening a PR that touches services/desktop.

Build installers ​

Locally, on the platform you want an installer for:

bash
pnpm --filter @tt/desktop tauri:build

Output lands in services/desktop/src-tauri/target/release/bundle/ — dmg/ and macos/ on macOS, nsis/ on Windows.

You cannot build a macOS app from Windows or Linux, which is what the Desktop app (manual) workflow is for: trigger it from the Actions tab, pick the server URL, and download the artifacts from the run. It is workflow_dispatch only — macOS runners bill at 10× the Linux rate — so it never runs on push or PR.

Builds are unsigned. On macOS the first launch is right-click → Open; on Windows, SmartScreen's "More info → Run anyway". Signing and notarization need an Apple Developer account and a code-signing certificate; wire them into the workflow (APPLE_CERTIFICATE, APPLE_ID, … per the Tauri docs) when there is one.

Point it at another server ​

The URL the popup loads is TIM_DESKTOP_URL + /tray, resolved in this order:

  1. TIM_DESKTOP_URL in the environment of the running app (handy for testing a build against staging without rebuilding);
  2. TIM_DESKTOP_URL at build time, baked into the binary (what the workflow input sets);
  3. https://tim.ovh.

Two things must agree with that URL:

  • The page may only call the shell from an origin a capability grants. capabilities/default.json covers tim.ovh, *.tim.ovh (organization subdomains) and the local dev server, checked at build time; at startup the shell grants the same permissions to the origin of whatever TIM_DESKTOP_URL resolved to (grant_server_ipc in lib.rs), so a self-hosted server needs no edit. A page served from an origin neither covers — one the server redirects to, say — still loads, but its calls to hide the popup are refused: it then relies on the shell's hide-on-blur, and the "saved" confirmation lingers until the next click. Add such an origin to the file.
  • The client route must exist on that server (/tray shipped with this feature), which just means the client must be at least as new as the desktop app.

How the page and the shell talk ​

withGlobalTauri is on, so the shell injects window.__TAURI__ into the page — the client needs no @tauri-apps/api dependency. services/client/src/lib/desktop.ts wraps the two calls the page makes:

  • hideDesktopPopup() after a save (and on Escape / the close button) — core:window:allow-hide;
  • onDesktopPopupShown(handler) to reset the form every time the shell shows the popup again — core:event:allow-listen. The webview lives for days; without this the entry's default day would be the day the app was launched.

Everything degrades to a no-op in a normal browser tab, so /tray is also just a page.

The shell, for its part, checks the webview's URL each time it shows the popup. If the page has wandered off /tray (an expired session that signed back in and landed on the app home) and is not on one of the sign-in pages, it navigates back.

Icons ​

pnpm --filter @tt/desktop tauri:icon regenerates the bundle icon set from services/client/public/logo_hq.png; delete the android/ and ios/ folders it also produces. The tray icon (tray-44.png, scaled to 18pt by macOS) is rasterized from services/client/public/icons/safari-pinned-tab.svg — a monochrome mask, which is what macOS wants for a template icon that follows the menu bar's light/dark tint. Windows and Linux show the colour 32x32.png instead.

TT Time Tracker — Internal Documentation