From 2a0b934d04e0c3b7466e97f954a843dceb81cfe7 Mon Sep 17 00:00:00 2001
From: Vincent
Date: Tue, 1 Sep 2026 16:04:54 +0200
Subject: [PATCH] Ship to Desktop and /Applications; restructure Settings; add
reset
Version pinned to 0.0.1 and staying on 0.x while this is in beta.
`npm run ship` now exports twice from one build: /Applications/Work.app
to test here, and ~/Desktop/Work-.zip to send someone. One
build, so a tester runs byte for byte what was verified rather than a
second build that drifted. Packed with ditto rather than zip - a plain
zip mangles the bundle's symlinks and it will not open on the far end.
Settings: groups move up under apps, and the hidden-element list moves
into its own window behind a one-line summary. It had grown to 21
selectors of framework class names and was most of the page, in front of
the things people actually open Settings for.
Reset puts apps, groups, hidden elements and zoom back to defaults, and
leaves sessions alone - those live in WebKit's own store, and throwing
them away would mean signing back into every tool to undo a change to
the nav.
---
README.md | 13 ++-
package.json | 2 +-
scripts/ship.sh | 25 ++--
src-tauri/Cargo.lock | 2 +-
src-tauri/Cargo.toml | 2 +-
src-tauri/src/commands.rs | 20 ++++
src-tauri/src/lib.rs | 1 +
src-tauri/tauri.conf.json | 10 +-
src/App.tsx | 12 ++
src/api.ts | 1 +
src/components/Settings.tsx | 223 ++++++++++++++++++++++--------------
11 files changed, 210 insertions(+), 101 deletions(-)
diff --git a/README.md b/README.md
index f0198e9..0c797a3 100644
--- a/README.md
+++ b/README.md
@@ -98,7 +98,18 @@ Build, replace the copy in `/Applications`, and relaunch — one command:
npm run ship
```
-No disk image; nothing here is being distributed.
+One build, two destinations, every time:
+
+- `/Applications/Work.app` — the copy tested here
+- `~/Desktop/Work-.zip` — the copy to send someone else
+
+Both come from the same build, so a tester runs byte for byte what was just verified
+rather than a second build that drifted. The zip is made with `ditto`, not `zip`: a plain
+zip mangles the bundle's symlinks and the app will not open on the other machine.
+
+The app is ad-hoc signed, so the first launch on someone else's Mac needs
+**right-click → Open** rather than a double-click. Gatekeeper refuses it silently
+otherwise.
## Configuration
diff --git a/package.json b/package.json
index 288db41..14462ec 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "work-app",
"private": true,
- "version": "0.1.0",
+ "version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/scripts/ship.sh b/scripts/ship.sh
index 8a43f15..a1cb8b9 100755
--- a/scripts/ship.sh
+++ b/scripts/ship.sh
@@ -1,25 +1,30 @@
#!/usr/bin/env bash
#
-# Build, install to /Applications, relaunch.
+# Build once, then put the result in both places, every time:
#
-# The standing workflow for seeing a change: this replaces the installed app
-# rather than producing a disk image, because nothing here is being distributed.
+# /Applications/Work.app the copy being tested here
+# ~/Desktop/Work-.zip the copy to send someone else
+#
+# One build, two destinations — so the thing a tester runs is byte for byte the
+# thing that was just tested here, rather than a second build that drifted.
set -euo pipefail
cd "$(dirname "$0")/.."
APP="Work.app"
BUILT="src-tauri/target/release/bundle/macos/$APP"
DEST="/Applications/$APP"
+VERSION="$(node -p "require('./package.json').version")"
+ZIP="$HOME/Desktop/Work-$VERSION.zip"
-echo "==> building"
+echo "==> building $VERSION"
npm run tauri build -- --bundles app
echo "==> quitting the running copy"
osascript -e 'quit app "Work"' >/dev/null 2>&1 || true
# A hung window would otherwise keep the bundle busy while it is replaced.
-pkill -x Work >/dev/null 2>&1 || true
+pkill -f "Work.app/Contents/MacOS" >/dev/null 2>&1 || true
for _ in 1 2 3 4 5 6 7 8 9 10; do
- pgrep -x Work >/dev/null || break
+ pgrep -f "Work.app/Contents/MacOS" >/dev/null || break
sleep 0.3
done
@@ -27,6 +32,12 @@ echo "==> installing to $DEST"
rm -rf "$DEST"
cp -R "$BUILT" "$DEST"
+# ditto rather than zip: it keeps the bundle's symlinks and resource forks,
+# which a plain zip mangles into an app that will not open on the other end.
+echo "==> packaging to $ZIP"
+rm -f "$ZIP"
+ditto -c -k --sequesterRsrc --keepParent "$BUILT" "$ZIP"
+
echo "==> launching"
open "$DEST"
-echo "==> done"
+echo "==> done — $DEST and $ZIP"
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index dc9c4f0..4e4aeba 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -4911,7 +4911,7 @@ checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
[[package]]
name = "work-app"
-version = "0.1.0"
+version = "0.0.1"
dependencies = [
"block2",
"mac-notification-sys",
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 61ef287..3d3e1bb 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "work-app"
-version = "0.1.0"
+version = "0.0.1"
description = "A browser for the tools you work in"
authors = ["Vincent"]
edition = "2021"
diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs
index ca3ce51..393d4ba 100644
--- a/src-tauri/src/commands.rs
+++ b/src-tauri/src/commands.rs
@@ -767,6 +767,26 @@ pub fn app_reports(state: State<'_, AppState>) -> Vec<(String, String)> {
.collect()
}
+/// Puts the app list, groups, hidden elements and zoom back to defaults.
+///
+/// Settings only. Sessions are left alone — they live in WebKit's own store,
+/// not in `apps.json`, and throwing them away would mean signing back into
+/// every tool to undo a change to the nav.
+#[tauri::command]
+pub fn reset_config(app: AppHandle, state: State<'_, AppState>) -> Result {
+ let old = state.cfg();
+ for a in &old.apps {
+ webviews::destroy(&app, &a.id);
+ }
+
+ *state.config.lock().unwrap() = config::seed();
+ *state.active.lock().unwrap() = None;
+ *state.booted.lock().unwrap() = false;
+ state.unread.lock().unwrap().clear();
+ state.persist()?;
+ Ok(state.cfg())
+}
+
// ------------------------------------------------------------ passwords
/// Writes the offered login to the macOS Keychain.
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index eb154df..c3f8b7b 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -114,6 +114,7 @@ pub fn run() {
commands::delete_group,
commands::set_nav_collapsed,
commands::set_theme,
+ commands::reset_config,
commands::unread_counts,
commands::focus_window,
commands::save_password,
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index 52d2d2f..2ad63b0 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Work",
- "version": "0.1.0",
+ "version": "0.0.1",
"identifier": "com.vincent.workapp",
"build": {
"beforeDevCommand": "npm run dev",
@@ -22,11 +22,15 @@
"hiddenTitle": true
}
],
- "security": { "csp": null }
+ "security": {
+ "csp": null
+ }
},
"bundle": {
"active": true,
- "targets": ["app"],
+ "targets": [
+ "app"
+ ],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
diff --git a/src/App.tsx b/src/App.tsx
index 61199b4..ca875da 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -271,6 +271,18 @@ export default function App() {
onConfig={setConfig}
onTheme={onTheme}
onClose={() => { setSettingsOpen(false); setFocusHidden(null); }}
+ onReset={async () => {
+ // Rust closed every view and forgot which app was showing, so the
+ // shell has to build the defaults again from nothing.
+ const fresh = await api.getConfig();
+ setConfig(fresh);
+ setUnread({});
+ booted.current = false;
+ const first = [...fresh.apps].sort((a, b) => a.order - b.order)[0];
+ setActiveId(first?.id ?? null);
+ report();
+ await api.bootstrap();
+ }}
/>
)}
diff --git a/src/api.ts b/src/api.ts
index 99e656a..2a348f4 100644
--- a/src/api.ts
+++ b/src/api.ts
@@ -60,3 +60,4 @@ export const unreadCounts = () => invoke<[string, number][]>("unread_counts");
export const stageSnapshot = () => invoke("stage_snapshot");
export const savePassword = () => invoke("save_password");
export const discardPassword = () => invoke("discard_password");
+export const resetConfig = () => invoke("reset_config");
diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx
index b3c91e7..3f9a004 100644
--- a/src/components/Settings.tsx
+++ b/src/components/Settings.tsx
@@ -6,6 +6,7 @@ import type { Config, Group, WorkApp } from "../types";
import { Trash } from "./icons";
import {
BTN,
+ BTN_QUIET,
BTN_PRIMARY,
Badge,
Dialog,
@@ -29,10 +30,12 @@ interface Props {
onConfig: (c: Config) => void;
onTheme: (t: Theme) => void;
onClose: () => void;
+ /** Settings were reset; the shell has to rebuild every app's view. */
+ onReset: () => void;
}
export default function Settings({
- config, theme, activeId, focusHiddenFor, onConfig, onTheme, onClose,
+ config, theme, activeId, focusHiddenFor, onConfig, onTheme, onClose, onReset,
}: Props) {
const [name, setName] = useState("");
const [url, setUrl] = useState("");
@@ -40,6 +43,8 @@ export default function Settings({
const [groupName, setGroupName] = useState("");
const [error, setError] = useState(null);
const [confirmDelete, setConfirmDelete] = useState(null);
+ const [hiddenOpen, setHiddenOpen] = useState(false);
+ const [confirmReset, setConfirmReset] = useState(false);
const [notifyStatus, setNotifyStatus] = useState(null);
const [reports, setReports] = useState<[string, string][]>([]);
@@ -56,7 +61,7 @@ export default function Settings({
useEffect(() => {
if (!focusHiddenFor) return;
- document.getElementById("hidden-elements")?.scrollIntoView({ behavior: "smooth" });
+ setHiddenOpen(true);
}, [focusHiddenFor]);
const run = async (fn: () => Promise) => {
@@ -86,6 +91,7 @@ export default function Settings({
const withHidden = orderedApps.filter((a) => a.hidden.length > 0);
+ const totalHidden = withHidden.reduce((n, a) => n + a.hidden.length, 0);
return (
<>
@@ -170,58 +176,59 @@ export default function Settings({
+ {/* ---------------------------------------------------- groups */}
+
+ Groups
+
+
+
{/* ------------------------------------------- hidden elements */}
-
- Hidden elements
- {withHidden.length === 0 ? (
-
- Nothing hidden. Right-click anything in an app and choose Hide this
- element — or use the eye button in the nav to pick one.
-
- ) : (
-
- {withHidden.map((app) => (
-
-
-
- {app.name}
- {app.hidden.length}
-
-
- {app.hidden.map((sel, i) => (
-
- {
- const next = [...app.hidden];
- next[i] = e.target.value;
- run(() => api.setHidden(app.id, next));
- }}
- title="Edit the selector to widen or narrow what it hides"
- className={`${INPUT} h-[26px] w-full flex-1 font-mono text-[11px]`}
- />
-
-
- ))}
-
-
- ))}
-
- )}
+
+
+ Hidden elements
+
+
- Changes apply immediately, without a reload. A selector that stops matching
- after the site changes simply hides nothing.
+ Right-click anything in an app and choose Hide this element. The
+ full list opens in its own window — there is usually a lot of it, and it is
+ not what you came to Settings for.
+
{/* ------------------------------------------------------ zoom */}
Zoom
@@ -305,43 +312,6 @@ export default function Settings({
- {/* ---------------------------------------------------- groups */}
-
- Groups
-