Pairing decrypts a Chromium browser's cookie store (PBKDF2-HMAC-SHA1 against its Keychain key, then AES-128-CBC) and injects the result into WKHTTPCookieStore. Only the configured apps' hosts and their sign-in hosts survive the filter. Browsers are offered most-recently-used first, since the first entry becomes the default and someone with four Chromium browsers installed wants the one they actually browse in. WKWebView defines window.Notification but it does nothing: constructing one throws no error and shows no banner, so a page believes it notified you. Measured on the machine as `api=function shim=no` before the shim was made unconditional; `from page: Odoo - Test notification -> raised` after. Anything on a page can be right-clicked away. The rule is re-asserted on every navigation, because the injected script only carries a snapshot from when the view was built and a selector added since would otherwise come back on reload. The top bar is gone. Navigation lives beside the cog, the nav carries the traffic lights, and two-finger swipe goes back and forward. The seed is now the real app list, scoped to exact hosts so a Drive link inside Gmail switches rather than being swallowed.
116 lines
5.3 KiB
Markdown
116 lines
5.3 KiB
Markdown
# Work
|
|
|
|
A desktop browser for one thing only: the web tools you work in. A left nav lists them,
|
|
clicking one shows it, and links between them navigate inside the app. Everything else
|
|
opens in your real browser.
|
|
|
|
Tauri 2 · Rust · React 19 · Tailwind CSS 4 · macOS
|
|
|
|
There is no address bar, no tab strip, and no way to reach a site that is not on the list.
|
|
That is the point.
|
|
|
|
## How it works
|
|
|
|
**Every app is its own webview.** Not an iframe — Google, Microsoft and most SaaS send
|
|
`X-Frame-Options: DENY`, so an iframe-based version of this app cannot exist. Each tool
|
|
gets a real child webview (`Window::add_child`), and they all stay loaded, so switching
|
|
keeps your scroll position, your half-typed draft, and anything counting down.
|
|
|
|
**The shell measures, Rust positions.** A child webview is a native view that takes no part
|
|
in CSS layout. The React shell leaves an empty `<div id="stage">`, measures it with a
|
|
`ResizeObserver`, and reports the rect; Rust sizes the active webview to it and hides the
|
|
rest. It also means a native view paints over anything the shell draws, which is why
|
|
opening Settings hides the stage first.
|
|
|
|
**Links are routed by intent, not by URL alone.** An injected script catches genuine user
|
|
clicks and `target=_blank`, and only those. A URL outside the app's own hosts is handed to
|
|
Rust over a made-up `workapp-route:` scheme — deliberately not Tauri IPC, which would mean
|
|
granting google.com the ability to call into this app. Rust then decides:
|
|
|
|
| Target | What happens |
|
|
|---|---|
|
|
| The current app's hosts | Nothing — ordinary navigation |
|
|
| A known identity provider | Stays inside, so SSO can complete |
|
|
| Another app on your list | Switches to that app and navigates it |
|
|
| Anything else | Opens in your default browser |
|
|
|
|
Redirects are never blocked. `on_navigation` returns `true` for everything that is not the
|
|
sentinel, because a strict navigation filter breaks every OAuth chain the moment it bounces
|
|
through `accounts.google.com`.
|
|
|
|
**Sessions persist.** Each webview keeps its cookies across restarts, so you log into a
|
|
tool once. Every app also claims a real Chrome user agent by default, because Google
|
|
refuses logins from anything it identifies as an embedded webview.
|
|
|
|
**Pairing imports a browser's cookies.** Settings lists the browsers installed, most
|
|
recently used first, and pairing decrypts that browser's cookie store — PBKDF2-HMAC-SHA1
|
|
against its Keychain key, then AES-128-CBC — and injects the result into WebKit's shared
|
|
jar. Only cookies for the apps on your list and their sign-in hosts are read; everything
|
|
else is dropped before anything is written. macOS asks for Keychain permission the first
|
|
time, which is the consent gate and cannot be skipped.
|
|
|
|
Google and Microsoft increasingly tie a session to the browser that created it, so those
|
|
may still ask you to sign in once. After that the persistent jar keeps it.
|
|
|
|
**Notifications work, with one gap.** WKWebView *defines* `window.Notification` but it is
|
|
inert: constructing one throws nothing and shows nothing, so a page believes it notified
|
|
you and you never hear about it. It is replaced with a shim that forwards to a real macOS
|
|
notification carrying the app's name. Service-worker push in the background is not
|
|
covered — only notifications a page raises while it is open.
|
|
|
|
**Anything on a page can be hidden.** Right-click it and choose *Hide this element*, or
|
|
use the eye button in the nav to point at one (arrow-up widens the selection to the
|
|
parent, Escape cancels). Selectors are stored per app, listed in Settings, editable, and
|
|
reversible. The rule is re-asserted on every navigation and re-added if a single-page app
|
|
rewrites `<head>`, and cached in the page's own storage so a reload hides it before the
|
|
first paint rather than after it has flashed on screen.
|
|
|
|
## Apps and groups
|
|
|
|
An app owns the **exact host** of its URL — `mail.google.com`, not `google.com` — or Gmail
|
|
and Drive would each swallow the other's links. Where two scopes match, the longest wins.
|
|
Extra hosts can be added per app.
|
|
|
|
Groups are for the nav only. Deleting one keeps its apps, ungrouped: deleting a folder
|
|
should never be a way to lose the things inside it.
|
|
|
|
The nav collapses to a 72px icon rail that is still clickable, so switching apps never
|
|
requires expanding it first. It is that wide because the macOS traffic lights have to fit
|
|
inside it: there is no title bar and no toolbar, so the nav carries the drag strip.
|
|
|
|
Back, forward, reload and hide-an-element sit next to the cog at the top of the nav.
|
|
There is no toolbar — two-finger swipe goes back and forward.
|
|
|
|
## Running it
|
|
|
|
```bash
|
|
npm install && npm run tauri dev
|
|
```
|
|
|
|
## Shipping it
|
|
|
|
Build, replace the copy in `/Applications`, and relaunch — one command:
|
|
|
|
```bash
|
|
npm run ship
|
|
```
|
|
|
|
No disk image; nothing here is being distributed.
|
|
|
|
## Configuration
|
|
|
|
`apps.json`, under `~/Library/Application Support/com.vincent.workapp/`. Hand-editing it is
|
|
supported — missing fields fall back to their defaults, and an app with no `scope` gets its
|
|
URL's host.
|
|
|
|
## Tests
|
|
|
|
```bash
|
|
cd src-tauri && cargo test
|
|
```
|
|
|
|
Routing and config are pure and carry real tests: scope matching and its precedence,
|
|
identity-provider passthrough, the routing decisions, sentinel round-trips, and config
|
|
round-trips. Webview orchestration and bounds sync have no seam a unit test can reach and
|
|
are verified by running the app.
|