@rightkit/system-ui 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/LICENSE-APACHE +201 -0
- package/LICENSE-MIT +21 -0
- package/README.md +36 -2
- package/dist/activityLease.d.ts +1 -0
- package/dist/activityLease.js +20 -0
- package/dist/api.d.ts +1011 -0
- package/dist/api.js +296 -0
- package/dist/chart.d.ts +14 -0
- package/dist/chart.js +94 -0
- package/dist/components/Chart.d.ts +40 -0
- package/dist/components/Chart.js +109 -0
- package/dist/env.d.ts +60 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +28 -0
- package/dist/kept.d.ts +3 -0
- package/dist/kept.js +21 -0
- package/dist/lib/chatKind.d.ts +4 -0
- package/dist/lib/chatKind.js +15 -0
- package/dist/lib/legal.d.ts +26 -0
- package/dist/lib/legal.js +39 -0
- package/dist/lib/onboardingSettings.d.ts +2 -0
- package/dist/lib/onboardingSettings.js +17 -0
- package/dist/lib/qr.d.ts +9 -0
- package/dist/lib/qr.js +286 -0
- package/dist/lib/rk.d.ts +6 -0
- package/dist/lib/rk.js +30 -0
- package/dist/metrics.d.ts +24 -0
- package/dist/metrics.js +76 -0
- package/dist/onboarding/flow.d.ts +31 -0
- package/dist/onboarding/flow.js +81 -0
- package/dist/onboarding/gate.d.ts +8 -0
- package/dist/onboarding/gate.js +52 -0
- package/dist/styles.css +8 -0
- package/dist/views/Activity.d.ts +3 -0
- package/dist/views/Activity.js +111 -0
- package/dist/views/Apps.d.ts +1 -0
- package/dist/views/Apps.js +478 -0
- package/dist/views/Chat.d.ts +1 -0
- package/dist/views/Chat.js +255 -0
- package/dist/views/ChromeSnapshots.d.ts +6 -0
- package/dist/views/ChromeSnapshots.js +19 -0
- package/dist/views/Cleanup.d.ts +4 -0
- package/dist/views/Cleanup.js +291 -0
- package/dist/views/DriveHealth.d.ts +14 -0
- package/dist/views/DriveHealth.js +81 -0
- package/dist/views/Duplicates.d.ts +2 -0
- package/dist/views/Duplicates.js +99 -0
- package/dist/views/LauncherSettings.d.ts +63 -0
- package/dist/views/LauncherSettings.js +242 -0
- package/dist/views/NearbySettings.d.ts +117 -0
- package/dist/views/NearbySettings.js +237 -0
- package/dist/views/Storage.d.ts +9 -0
- package/dist/views/Storage.js +916 -0
- package/dist/views/activity.css +94 -0
- package/dist/views/apps.css +98 -0
- package/dist/views/chat.css +61 -0
- package/dist/views/cleanup.css +49 -0
- package/dist/views/health.css +17 -0
- package/dist/views/membrane.css +23 -0
- package/dist/views/monitor.css +99 -0
- package/dist/views/settings.css +176 -0
- package/dist/views/settingsKit.d.ts +14 -0
- package/dist/views/settingsKit.js +43 -0
- package/dist/views/storage.css +121 -0
- package/dist/views/storageEstimate.d.ts +6 -0
- package/dist/views/storageEstimate.js +7 -0
- package/dist/visible.d.ts +3 -0
- package/dist/visible.js +42 -0
- package/package.json +110 -3
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Badge, Notice } from "@rightkit/react-shell/react";
|
|
3
|
+
import { bytes, isWindows } from "../api.js";
|
|
4
|
+
const day = (secs) => new Date(secs * 1000).toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" });
|
|
5
|
+
const celsius = (value) => (value == null ? null : `${Math.round(value)} °C`);
|
|
6
|
+
/** Warning as the notch counts it: failed self-assessment, a critical warning, or media errors. */
|
|
7
|
+
export function isWarning(reading) {
|
|
8
|
+
return reading.passed === false || (reading.critical_warning ?? 0) !== 0 || (reading.media_errors ?? 0) > 0;
|
|
9
|
+
}
|
|
10
|
+
const NEEDS_ADMIN = "Temperature and wear need Pulse to run as administrator on Windows";
|
|
11
|
+
/** Windows only: the drive answered, but not one metric came with it (the unelevated paths gave nothing). */
|
|
12
|
+
function lacksMetrics(reading) {
|
|
13
|
+
return (isWindows &&
|
|
14
|
+
reading.temperature_c == null &&
|
|
15
|
+
reading.wear_percent == null &&
|
|
16
|
+
reading.written_bytes == null &&
|
|
17
|
+
reading.power_on_hours == null &&
|
|
18
|
+
reading.media_errors == null);
|
|
19
|
+
}
|
|
20
|
+
/** Temperature, wear and writes, for whichever of them the drive reports. */
|
|
21
|
+
function readingParts(reading) {
|
|
22
|
+
return [
|
|
23
|
+
celsius(reading.temperature_c),
|
|
24
|
+
reading.wear_percent == null ? null : `${reading.wear_percent}% worn`,
|
|
25
|
+
reading.written_bytes == null ? null : `${bytes(reading.written_bytes)} written`,
|
|
26
|
+
].filter((part) => part !== null);
|
|
27
|
+
}
|
|
28
|
+
/** One drive's health on its volume card. Never blocks the card's scan action. */
|
|
29
|
+
export function DriveHealthLine({ card, toolAvailable }) {
|
|
30
|
+
if (!card)
|
|
31
|
+
return null;
|
|
32
|
+
if (!card.disk)
|
|
33
|
+
return _jsx("span", { className: "health-line muted small", children: "Health not read for this volume" });
|
|
34
|
+
if (card.status === "unknown") {
|
|
35
|
+
return (_jsx("span", { className: "health-line muted small", children: toolAvailable ? "Health not read yet" : "Install smartmontools for drive health" }));
|
|
36
|
+
}
|
|
37
|
+
if (card.status === "unavailable") {
|
|
38
|
+
return (_jsxs("span", { className: "health-line small", children: [_jsx("span", { className: "muted", children: "Health unavailable through this connection" }), card.latest && (_jsxs("span", { className: "muted", children: ["\u00B7 last reading ", day(card.latest.at), ": ", readingParts(card.latest).join(" · ")] }))] }));
|
|
39
|
+
}
|
|
40
|
+
if (!card.latest)
|
|
41
|
+
return null;
|
|
42
|
+
const warning = card.status === "warning";
|
|
43
|
+
return (_jsxs("span", { className: "health-line small", children: [_jsx(Badge, { tone: warning ? "warn" : "ok", children: warning ? "Warning" : "OK" }), readingParts(card.latest).map((part) => (_jsx("span", { className: "muted", children: part }, part))), lacksMetrics(card.latest) && _jsx("span", { className: "muted", children: NEEDS_ADMIN })] }));
|
|
44
|
+
}
|
|
45
|
+
function Metric({ label, value }) {
|
|
46
|
+
return (_jsxs("div", { className: "health-metric", children: [_jsx("span", { className: "muted small", children: label }), _jsx("span", { className: "strong", children: value })] }));
|
|
47
|
+
}
|
|
48
|
+
/** A line of timestamped values; needs two readings to draw. */
|
|
49
|
+
function Sparkline({ label, unit, points }) {
|
|
50
|
+
if (points.length === 0)
|
|
51
|
+
return null;
|
|
52
|
+
const last = points[points.length - 1];
|
|
53
|
+
const values = points.map((p) => p.value);
|
|
54
|
+
const low = Math.min(...values);
|
|
55
|
+
const high = Math.max(...values);
|
|
56
|
+
const first = points[0].at;
|
|
57
|
+
const span = high - low || 1;
|
|
58
|
+
const width = 260;
|
|
59
|
+
const height = 44;
|
|
60
|
+
const x = (at) => {
|
|
61
|
+
const range = last.at - first;
|
|
62
|
+
return range === 0 ? width / 2 : 2 + ((at - first) / range) * (width - 4);
|
|
63
|
+
};
|
|
64
|
+
const y = (value) => height - 4 - ((value - low) / span) * (height - 8);
|
|
65
|
+
const path = points.map((p, i) => `${i === 0 ? "M" : "L"}${x(p.at).toFixed(1)},${y(p.value).toFixed(1)}`).join(" ");
|
|
66
|
+
return (_jsxs("div", { className: "health-spark", children: [_jsxs("div", { className: "small", children: [_jsx("span", { className: "muted", children: label }), " ", _jsxs("span", { className: "strong", children: [last.value, unit] }), " ", _jsxs("span", { className: "muted", children: [low, "\u2013", high, unit, " \u00B7 ", day(first), first !== last.at ? ` – ${day(last.at)}` : ""] })] }), points.length < 2 ? (_jsx("span", { className: "muted small", children: "One reading so far; the line appears with the next." })) : (_jsx("svg", { viewBox: `0 0 ${width} ${height}`, preserveAspectRatio: "none", role: "img", "aria-label": `${label} over time`, children: _jsx("path", { d: path, fill: "none", stroke: "var(--rk-accent)", strokeWidth: 1.5, vectorEffect: "non-scaling-stroke" }) }))] }));
|
|
67
|
+
}
|
|
68
|
+
function series(history, pick) {
|
|
69
|
+
return history.flatMap((r) => {
|
|
70
|
+
const value = pick(r);
|
|
71
|
+
return value == null ? [] : [{ at: r.at, value }];
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/** The detail panel that opens from a drive's card. */
|
|
75
|
+
export function DriveHealthPanel({ card, alerts, toolAvailable, }) {
|
|
76
|
+
if (!card)
|
|
77
|
+
return null;
|
|
78
|
+
const latest = card.latest;
|
|
79
|
+
const ownAlerts = alerts.filter((alert) => alert.disk === card.disk);
|
|
80
|
+
return (_jsxs("section", { className: "card-block health-panel", "aria-label": "Drive health", children: [_jsxs("div", { className: "block-head", children: [_jsx("span", { className: "strong", children: card.model ?? card.disk ?? "Drive" }), _jsx("span", { className: "muted small", children: card.mount })] }), !card.disk && _jsx("div", { className: "muted small", children: "Pulse could not match this volume to a physical drive." }), card.disk && !toolAvailable && _jsx("div", { className: "muted small", children: "Install smartmontools for drive health." }), card.status === "unavailable" && (_jsx(Notice, { className: "small", action: latest ? _jsxs("span", { className: "muted", children: ["Last good reading: ", day(latest.at)] }) : undefined, children: "Health unavailable through this connection." })), latest && lacksMetrics(latest) && _jsxs("div", { className: "muted small", children: [NEEDS_ADMIN, "."] }), latest && (_jsxs("div", { className: "health-grid", children: [_jsx(Metric, { label: "Health", value: isWarning(latest) ? "Warning" : "OK" }), _jsx(Metric, { label: "Temperature", value: celsius(latest.temperature_c) ?? "—" }), _jsx(Metric, { label: "Wear", value: latest.wear_percent == null ? "—" : `${latest.wear_percent}%` }), _jsx(Metric, { label: "Written", value: latest.written_bytes == null ? "—" : bytes(latest.written_bytes) }), _jsx(Metric, { label: "Power-on time", value: latest.power_on_hours == null ? "—" : `${latest.power_on_hours.toLocaleString()} h` }), _jsx(Metric, { label: "Critical warnings", value: latest.critical_warning == null ? "—" : String(latest.critical_warning) }), _jsx(Metric, { label: "Media errors", value: latest.media_errors == null ? "—" : String(latest.media_errors) })] })), latest && (_jsxs("div", { className: "health-section", children: [_jsx("span", { className: "muted small", children: "Self-tests" }), latest.self_tests.length === 0 && _jsx("span", { className: "muted small", children: "No self-test results reported." }), latest.self_tests.map((test, index) => (_jsxs("div", { className: "health-test small", children: [_jsx("span", { children: test.kind }), _jsxs("span", { className: "muted", children: [test.result, test.power_on_hours != null ? ` · at ${test.power_on_hours.toLocaleString()} h` : ""] })] }, `${test.kind}-${index}`)))] })), _jsx(Sparkline, { label: "Temperature", unit: " \u00B0C", points: series(card.history, (r) => r.temperature_c) }), _jsx(Sparkline, { label: "Wear", unit: "%", points: series(card.history, (r) => r.wear_percent) }), _jsxs("div", { className: "health-section", children: [_jsx("span", { className: "muted small", children: "Alerts" }), ownAlerts.length === 0 && _jsx("span", { className: "muted small", children: "No alerts." }), ownAlerts.slice(0, 10).map((alert) => (_jsxs("span", { className: "small", children: [day(alert.at), " \u00B7 ", alert.message] }, alert.id)))] })] }));
|
|
81
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Badge, Button, ConfirmDialog, EmptyState, Input, Notice } from "@rightkit/react-shell/react";
|
|
4
|
+
import { Copy } from "lucide-react";
|
|
5
|
+
import { api, bytes, isWindows } from "../api.js";
|
|
6
|
+
// Windows moves extra copies to the Recycle Bin; the Mac moves them to the Trash.
|
|
7
|
+
const TRASH = isWindows ? "Recycle Bin" : "Trash";
|
|
8
|
+
const shortPath = (path) => path.replace(/^\/Users\/[^/]+/, "~");
|
|
9
|
+
const copies = (n) => `${n} cop${n === 1 ? "y" : "ies"}`;
|
|
10
|
+
/** The last result and folder, kept while the user is on another tab. */
|
|
11
|
+
let cached = null;
|
|
12
|
+
/** Exact-content duplicates under a folder. Extra copies move to the Trash; the kept copy stays. */
|
|
13
|
+
export function Duplicates() {
|
|
14
|
+
const [folder, setFolder] = useState(cached?.folder ?? "");
|
|
15
|
+
const [draft, setDraft] = useState(cached?.folder ?? "");
|
|
16
|
+
const [report, setReport] = useState(cached?.report ?? null);
|
|
17
|
+
const [removed, setRemoved] = useState(new Set(cached?.removed ?? []));
|
|
18
|
+
const [busy, setBusy] = useState(false);
|
|
19
|
+
const [error, setError] = useState(null);
|
|
20
|
+
const [notice, setNotice] = useState(null);
|
|
21
|
+
const [pending, setPending] = useState(null);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!cached?.folder) {
|
|
24
|
+
api
|
|
25
|
+
.homePath()
|
|
26
|
+
.then((home) => {
|
|
27
|
+
setFolder((current) => current || home);
|
|
28
|
+
setDraft((current) => current || home);
|
|
29
|
+
})
|
|
30
|
+
.catch(() => { });
|
|
31
|
+
}
|
|
32
|
+
}, []);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
cached = report && folder ? { folder, report, removed: [...removed] } : null;
|
|
35
|
+
}, [folder, report, removed]);
|
|
36
|
+
// Looks under the chosen folder. Core bounds the file count, bytes read and time; the
|
|
37
|
+
// hub runs it off the UI thread, so the rest of the window stays responsive.
|
|
38
|
+
const find = async () => {
|
|
39
|
+
const path = draft.trim();
|
|
40
|
+
if (!path)
|
|
41
|
+
return;
|
|
42
|
+
setBusy(true);
|
|
43
|
+
setError(null);
|
|
44
|
+
setNotice(null);
|
|
45
|
+
setPending(null);
|
|
46
|
+
try {
|
|
47
|
+
const next = await api.duplicatesScan(path);
|
|
48
|
+
setFolder(path);
|
|
49
|
+
setReport(next);
|
|
50
|
+
setRemoved(new Set());
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
setReport(null);
|
|
54
|
+
setError(String(e));
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
setBusy(false);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const moveToTrash = async (items) => {
|
|
61
|
+
setPending(null);
|
|
62
|
+
setBusy(true);
|
|
63
|
+
setError(null);
|
|
64
|
+
setNotice(null);
|
|
65
|
+
try {
|
|
66
|
+
const result = await api.duplicatesTrash(items.map((item) => ({ kept: item.kept, path: item.path })));
|
|
67
|
+
const movedPaths = result.moved.map((m) => m.path);
|
|
68
|
+
setRemoved((prev) => new Set([...prev, ...movedPaths]));
|
|
69
|
+
setNotice(`Moved ${copies(result.moved.length)} to the ${TRASH}, ${bytes(result.moved_bytes)}. The kept copies are untouched.` +
|
|
70
|
+
(result.skipped.length ? ` ${result.skipped.length} skipped.` : ""));
|
|
71
|
+
if (result.skipped.length) {
|
|
72
|
+
setError(result.skipped.map((s) => `${shortPath(s.path)}: ${s.reason}`).join("\n"));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
setError(String(e));
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
setBusy(false);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const groups = (report?.groups ?? [])
|
|
83
|
+
.map((group) => ({
|
|
84
|
+
...group,
|
|
85
|
+
extras: group.extras.filter((path) => !removed.has(path)),
|
|
86
|
+
}))
|
|
87
|
+
.filter((group) => group.extras.length > 0);
|
|
88
|
+
const reclaimable = groups.reduce((sum, group) => sum + group.extras.length * group.size_bytes, 0);
|
|
89
|
+
const itemsFor = (kept, size, paths) => paths.map((path) => ({ kept, path, bytes: size }));
|
|
90
|
+
return (_jsxs("div", { className: "duplicates", children: [_jsxs("div", { className: "toolbar dup-controls", children: [_jsx(Input, { className: "search dup-path", "aria-label": "Folder to check for duplicates", value: draft, onChange: (e) => setDraft(e.target.value), onKeyDown: (e) => {
|
|
91
|
+
if (e.key === "Enter")
|
|
92
|
+
find();
|
|
93
|
+
}, placeholder: "/Users/you/Documents" }), _jsx(Button, { size: "sm", variant: "ghost", onClick: () => api.homePath().then((home) => setDraft(home)).catch(() => { }), disabled: busy, children: "Home" }), _jsxs(Button, { size: "sm", onClick: find, disabled: busy || !draft.trim(), children: [_jsx(Copy, { size: 12 }), " ", busy ? "Looking…" : "Find copies"] })] }), busy && _jsxs("div", { className: "muted small", children: ["Reading files in ", shortPath(draft.trim()), " to compare them. This can take a little while."] }), notice && _jsx(Notice, { className: "small", children: notice }), error && _jsx("div", { className: "error", style: { whiteSpace: "pre-wrap" }, children: error }), report && !busy && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "muted small", children: [groups.length === 0
|
|
94
|
+
? `No extra copies in ${shortPath(folder)}.`
|
|
95
|
+
: `${copies(groups.reduce((n, g) => n + g.extras.length, 0))} in ${groups.length} group${groups.length === 1 ? "" : "s"} · ${bytes(reclaimable)} can be freed by moving the extras to the ${TRASH}`, ` · checked ${report.files_considered.toLocaleString()} files`, report.truncated ? " · the folder is very large, so only part of it was checked" : "", report.skipped.length ? ` · ${report.skipped.length} skipped (links, cloud placeholders, unreadable)` : ""] }), report.diagnostics.map((line) => (_jsx("div", { className: "muted small", children: line }, line)))] })), !report && !busy && !error && _jsx(EmptyState, { icon: _jsx(Copy, { size: 22 }), title: "Choose a folder, then find copies" }), groups.map((group) => {
|
|
96
|
+
const all = itemsFor(group.kept_path, group.size_bytes, group.extras);
|
|
97
|
+
return (_jsxs("section", { className: "card-block dup-group", children: [_jsxs("div", { className: "block-head", children: [_jsxs("span", { className: "strong", children: [copies(group.extras.length + 1), " \u00B7 ", bytes(group.size_bytes), " each"] }), _jsxs(Button, { size: "sm", onClick: () => setPending({ title: `${copies(all.length)} of ${shortPath(group.kept_path)}`, items: all }), disabled: busy, children: ["Move ", all.length, " extra", all.length === 1 ? "" : "s", " to ", TRASH] })] }), _jsxs("div", { className: "dup-row", children: [_jsx(Badge, { tone: "ok", children: "Keep" }), _jsx("span", { className: "name small", title: group.kept_path, children: shortPath(group.kept_path) }), _jsx("span", {}), _jsx("span", {})] }), group.extras.map((path) => (_jsxs("div", { className: "dup-row sub", children: [_jsx("span", {}), _jsx("span", { className: "name small", title: path, children: shortPath(path) }), _jsx("span", { className: "size small", children: bytes(group.size_bytes) }), _jsxs(Button, { size: "sm", variant: "ghost", onClick: () => setPending({ title: shortPath(path), items: itemsFor(group.kept_path, group.size_bytes, [path]) }), disabled: busy, children: ["Move to ", TRASH] })] }, path)))] }, group.kept_path));
|
|
98
|
+
}), pending && (_jsx(ConfirmDialog, { title: pending.items.length === 1 ? `Move this copy to the ${TRASH}?` : `Move ${pending.items.length} copies to the ${TRASH}?`, description: `${pending.title}: ${copies(pending.items.length)}, ${bytes(pending.items.reduce((s, i) => s + i.bytes, 0))} will move to the ${TRASH}. The copy you keep stays where it is. Nothing is deleted: you can put them back from the ${TRASH}.`, confirmLabel: `Move to ${TRASH}`, onConfirm: () => moveToTrash(pending.items), onCancel: () => setPending(null) }))] }));
|
|
99
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** The launcher's user-made items. Stored by the notch as one JSON string. */
|
|
2
|
+
export interface LauncherConfig {
|
|
3
|
+
pinnedApps: string[];
|
|
4
|
+
fileFolders: string[];
|
|
5
|
+
appHotkeys: {
|
|
6
|
+
path: string;
|
|
7
|
+
hotkey: string;
|
|
8
|
+
}[];
|
|
9
|
+
quicklinks: {
|
|
10
|
+
name: string;
|
|
11
|
+
keyword: string;
|
|
12
|
+
template: string;
|
|
13
|
+
}[];
|
|
14
|
+
snippets: {
|
|
15
|
+
name: string;
|
|
16
|
+
keyword: string;
|
|
17
|
+
body: string;
|
|
18
|
+
}[];
|
|
19
|
+
commands: {
|
|
20
|
+
name: string;
|
|
21
|
+
keyword: string;
|
|
22
|
+
command: string;
|
|
23
|
+
hotkey: string;
|
|
24
|
+
}[];
|
|
25
|
+
notesEnabled: boolean;
|
|
26
|
+
/** Where "ask" hands a question: the Claude desktop app, or nowhere. */
|
|
27
|
+
/** Short aliases for Apple Shortcuts (Mac). */
|
|
28
|
+
shortcutAliases: ShortcutAlias[];
|
|
29
|
+
/** Global hotkeys that run one Apple Shortcut without opening the launcher (Mac). */
|
|
30
|
+
shortcutHotkeys: ShortcutHotkey[];
|
|
31
|
+
}
|
|
32
|
+
export interface ShortcutHotkey {
|
|
33
|
+
/** The shortcut's name, as the Shortcuts app shows it. */
|
|
34
|
+
shortcut: string;
|
|
35
|
+
/** Such as "ctrl+opt+s": at least one modifier and a key. */
|
|
36
|
+
hotkey: string;
|
|
37
|
+
}
|
|
38
|
+
/** The shortcut that already holds `hotkey` besides `shortcut`, or null. */
|
|
39
|
+
export declare function hotkeyTakenBy(list: ShortcutHotkey[], shortcut: string, hotkey: string): string | null;
|
|
40
|
+
export interface ShortcutAlias {
|
|
41
|
+
/** The shortcut's name, as the Shortcuts app shows it. */
|
|
42
|
+
shortcut: string;
|
|
43
|
+
/** 1 to 16 letters or digits, unique without regard to case. */
|
|
44
|
+
alias: string;
|
|
45
|
+
}
|
|
46
|
+
/** The problem with the alias at `index`, or null when it is usable. */
|
|
47
|
+
export declare function aliasProblem(list: ShortcutAlias[], index: number): string | null;
|
|
48
|
+
export declare function parseLauncherConfig(raw: unknown): LauncherConfig;
|
|
49
|
+
/** A launcher chord's name from its published value: "optionShiftSpace" -> "Option+Shift+Space",
|
|
50
|
+
* "ctrlSpace" -> "Ctrl+Space". Covers the Mac's stand-down chords without a list to keep in step. */
|
|
51
|
+
export declare const chordName: (raw: string) => string;
|
|
52
|
+
/** The launcher's typed-text rows and the Alt+Space answer: shared keys, shown on both platforms.
|
|
53
|
+
* `s` is the notch's published settings; a row shows only when its key is published. */
|
|
54
|
+
export declare function LauncherSearchRows({ s, set }: {
|
|
55
|
+
s: Record<string, string | number | boolean>;
|
|
56
|
+
set: (key: string, value: unknown) => void;
|
|
57
|
+
}): import("react").JSX.Element;
|
|
58
|
+
export declare function LauncherLists({ raw, shortcutNames, onSave }: {
|
|
59
|
+
raw: unknown;
|
|
60
|
+
/** The Apple Shortcuts the notch found (`launcherShortcutNames`), Mac only. */
|
|
61
|
+
shortcutNames?: string[];
|
|
62
|
+
onSave: (next: LauncherConfig) => void;
|
|
63
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Button, Checkbox, Select, Toggle, Input } from "@rightkit/react-shell/react";
|
|
4
|
+
import { formatChord } from "@rightkit/platform-ui";
|
|
5
|
+
import { SettingRow, SettingsItem } from "@rightkit/settings-ui/core";
|
|
6
|
+
import { useSettingsSearch } from "@rightkit/settings-ui/headless";
|
|
7
|
+
import { windowShortcutFrom } from "./settingsKit.js";
|
|
8
|
+
import { isWindows } from "../api.js";
|
|
9
|
+
/** The shortcut that already holds `hotkey` besides `shortcut`, or null. */
|
|
10
|
+
export function hotkeyTakenBy(list, shortcut, hotkey) {
|
|
11
|
+
if (!hotkey)
|
|
12
|
+
return null;
|
|
13
|
+
return list.find((h) => h.shortcut !== shortcut && h.hotkey === hotkey)?.shortcut ?? null;
|
|
14
|
+
}
|
|
15
|
+
/** The problem with the alias at `index`, or null when it is usable. */
|
|
16
|
+
export function aliasProblem(list, index) {
|
|
17
|
+
const shortcut = list[index].shortcut.trim();
|
|
18
|
+
const alias = list[index].alias.trim();
|
|
19
|
+
if (!shortcut || !alias)
|
|
20
|
+
return "Enter the shortcut name and an alias.";
|
|
21
|
+
if (!/^[A-Za-z0-9]{1,16}$/.test(alias))
|
|
22
|
+
return "The alias is 1 to 16 letters or digits.";
|
|
23
|
+
const taken = list.some((other, j) => j !== index && other.alias.trim().toLowerCase() === alias.toLowerCase());
|
|
24
|
+
return taken ? "Another alias already uses this." : null;
|
|
25
|
+
}
|
|
26
|
+
const blank = () => ({
|
|
27
|
+
pinnedApps: [],
|
|
28
|
+
fileFolders: [],
|
|
29
|
+
appHotkeys: [],
|
|
30
|
+
quicklinks: [],
|
|
31
|
+
snippets: [],
|
|
32
|
+
commands: [],
|
|
33
|
+
notesEnabled: true,
|
|
34
|
+
shortcutAliases: [],
|
|
35
|
+
shortcutHotkeys: [],
|
|
36
|
+
});
|
|
37
|
+
export function parseLauncherConfig(raw) {
|
|
38
|
+
if (typeof raw !== "string" || raw === "")
|
|
39
|
+
return blank();
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(raw);
|
|
42
|
+
return {
|
|
43
|
+
pinnedApps: parsed.pinnedApps ?? [],
|
|
44
|
+
fileFolders: parsed.fileFolders ?? [],
|
|
45
|
+
appHotkeys: parsed.appHotkeys ?? [],
|
|
46
|
+
quicklinks: parsed.quicklinks ?? [],
|
|
47
|
+
snippets: parsed.snippets ?? [],
|
|
48
|
+
commands: parsed.commands ?? [],
|
|
49
|
+
notesEnabled: parsed.notesEnabled ?? true,
|
|
50
|
+
shortcutAliases: parsed.shortcutAliases ?? [],
|
|
51
|
+
shortcutHotkeys: parsed.shortcutHotkeys ?? [],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return blank();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** A line that adds one value to a list. */
|
|
59
|
+
function AddLine({ placeholder, onAdd }) {
|
|
60
|
+
const [text, setText] = useState("");
|
|
61
|
+
const add = () => {
|
|
62
|
+
const value = text.trim();
|
|
63
|
+
if (!value)
|
|
64
|
+
return;
|
|
65
|
+
onAdd(value);
|
|
66
|
+
setText("");
|
|
67
|
+
};
|
|
68
|
+
return (_jsxs("div", { className: "ck-line", children: [_jsx(Input, { className: "ck-input", "aria-label": placeholder, value: text, placeholder: placeholder, onChange: (e) => setText(e.target.value), onKeyDown: (e) => { if (e.key === "Enter")
|
|
69
|
+
add(); } }), _jsx(Button, { size: "sm", variant: "secondary", disabled: !text.trim(), onClick: add, children: "Add" })] }));
|
|
70
|
+
}
|
|
71
|
+
/** Two fields and an Add button for a new shortcut alias; it adds only a usable alias. */
|
|
72
|
+
function AliasAdd({ existing, onAdd }) {
|
|
73
|
+
const [shortcut, setShortcut] = useState("");
|
|
74
|
+
const [alias, setAlias] = useState("");
|
|
75
|
+
const candidate = [...existing, { shortcut, alias }];
|
|
76
|
+
const typed = shortcut.trim() !== "" || alias.trim() !== "";
|
|
77
|
+
const problem = typed ? aliasProblem(candidate, candidate.length - 1) : null;
|
|
78
|
+
const add = () => {
|
|
79
|
+
if (problem !== null || !typed)
|
|
80
|
+
return;
|
|
81
|
+
onAdd({ shortcut: shortcut.trim(), alias: alias.trim() });
|
|
82
|
+
setShortcut("");
|
|
83
|
+
setAlias("");
|
|
84
|
+
};
|
|
85
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "ck-line", children: [_jsx(Input, { className: "ck-input", "aria-label": "Shortcut name", placeholder: "Toggle Speakers", value: shortcut, style: { flex: "2 1 220px" }, onChange: (e) => setShortcut(e.target.value) }), _jsx(Input, { className: "ck-input", "aria-label": "Alias", placeholder: "ts", value: alias, style: { flex: "1 1 110px" }, onChange: (e) => setAlias(e.target.value), onKeyDown: (e) => { if (e.key === "Enter")
|
|
86
|
+
add(); } }), _jsx(Button, { size: "sm", variant: "secondary", disabled: problem !== null || !typed, onClick: add, children: "Add" })] }), problem ? _jsx("div", { className: "ck-sub", role: "status", children: problem }) : null] }));
|
|
87
|
+
}
|
|
88
|
+
/** "ctrl+opt+s" as "Control Option S". */
|
|
89
|
+
function hotkeyText(raw) {
|
|
90
|
+
const names = { ctrl: "Control", opt: "Option", shift: "Shift", cmd: "Command" };
|
|
91
|
+
return raw.split("+").map((p) => names[p] ?? (p.length === 1 ? p.toUpperCase() : p[0].toUpperCase() + p.slice(1))).join(" ");
|
|
92
|
+
}
|
|
93
|
+
/** One Apple Shortcut: its name, its hotkey, a recorder to set it and a button to clear it. */
|
|
94
|
+
function ShortcutHotkeyLine({ name, hotkey, takenBy, onChange }) {
|
|
95
|
+
const [recording, setRecording] = useState(false);
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (!recording)
|
|
98
|
+
return;
|
|
99
|
+
const onKey = (e) => {
|
|
100
|
+
e.preventDefault();
|
|
101
|
+
e.stopPropagation();
|
|
102
|
+
if (e.code === "Escape") {
|
|
103
|
+
setRecording(false);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const shortcut = windowShortcutFrom(e);
|
|
107
|
+
if (!shortcut)
|
|
108
|
+
return;
|
|
109
|
+
onChange(shortcut);
|
|
110
|
+
setRecording(false);
|
|
111
|
+
};
|
|
112
|
+
window.addEventListener("keydown", onKey, true);
|
|
113
|
+
return () => window.removeEventListener("keydown", onKey, true);
|
|
114
|
+
}, [recording, onChange]);
|
|
115
|
+
return (_jsxs("div", { className: "ck-list", children: [_jsxs("div", { className: "ck-line", children: [_jsx("span", { className: "ck-sub", style: { flex: "2 1 220px" }, children: name }), _jsx("span", { className: "ck-sub ck-num ck-keys", style: { flex: "1 1 110px" }, children: recording ? "Press a shortcut…" : hotkey ? hotkeyText(hotkey) : "None" }), _jsx(Button, { size: "sm", variant: "secondary", onClick: () => setRecording(!recording), children: recording ? "Cancel" : "Set" }), _jsx(Button, { size: "sm", variant: "ghost", disabled: !hotkey, onClick: () => onChange(""), children: "Clear" })] }), takenBy ? _jsxs("div", { className: "ck-sub", role: "status", children: [takenBy, " already uses this hotkey."] }) : null] }));
|
|
116
|
+
}
|
|
117
|
+
function Section({ title, note, children }) {
|
|
118
|
+
const { highlight } = useSettingsSearch();
|
|
119
|
+
return (_jsxs(SettingsItem, { className: "ck-set ck-stack", text: `${title} ${note ?? ""}`, children: [_jsxs("div", { className: "ck-text", children: [_jsx("strong", { children: highlight(title) }), note && _jsx("div", { className: "ck-sub", children: highlight(note) })] }), _jsx("div", { className: "ck-list", children: children })] }));
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Edits the launcher's lists: folders for file search, pinned apps, app and
|
|
123
|
+
* command hotkeys, quicklinks, snippets and custom commands. Changes are held
|
|
124
|
+
* until Save, then sent as one `launcherConfig` value.
|
|
125
|
+
*/
|
|
126
|
+
const ENGINES = [
|
|
127
|
+
{ value: "google", label: "Google" },
|
|
128
|
+
{ value: "bing", label: "Bing" },
|
|
129
|
+
{ value: "duckduckgo", label: "DuckDuckGo" },
|
|
130
|
+
];
|
|
131
|
+
/** A launcher chord's name from its published value: "optionShiftSpace" -> "Option+Shift+Space",
|
|
132
|
+
* "ctrlSpace" -> "Ctrl+Space". Covers the Mac's stand-down chords without a list to keep in step. */
|
|
133
|
+
export const chordName = (raw) => formatChord(raw.replace(/([a-z])([A-Z])/g, "$1+$2"), "unknown").join("+");
|
|
134
|
+
/** The launcher's typed-text rows and the Alt+Space answer: shared keys, shown on both platforms.
|
|
135
|
+
* `s` is the notch's published settings; a row shows only when its key is published. */
|
|
136
|
+
export function LauncherSearchRows({ s, set }) {
|
|
137
|
+
const has = (key) => key in s;
|
|
138
|
+
const webOn = s.launcherWebSearch !== false;
|
|
139
|
+
const conflict = String(s.launcherHotkeyConflict ?? "");
|
|
140
|
+
const alt = conflict === "pulse" ? "Pulse" : conflict === "other" ? "Kept for the other app" : "Not asked yet";
|
|
141
|
+
const chord = isWindows ? "Alt+Space" : "Option+Space";
|
|
142
|
+
return (_jsxs(_Fragment, { children: [has("launcherWebSearch") ? (_jsx(SettingRow, { className: "ck-set", label: "Web search row", description: "A closing row that searches the web for what you typed, in your default browser. On by default.", children: _jsx(Toggle, { checked: webOn, onChange: (v) => set("launcherWebSearch", v), label: "Web search row" }) })) : null, has("launcherWebSearchEngine") ? (_jsx(SettingRow, { className: "ck-set", label: "Search engine", description: "Which engine the web search row uses. Google by default.", children: _jsx(Select, { label: "Search engine", disabled: !webOn, value: String(s.launcherWebSearchEngine || "google"), onChange: (v) => set("launcherWebSearchEngine", v), options: ENGINES }) })) : null, has("launcherHotkeyConflict") ? (_jsxs(SettingRow, { className: "ck-set", label: chord, description: "Who gets the chord when another program already holds it. Pulse asks once.", children: [_jsx("span", { className: "ck-sub", role: "status", children: alt }), _jsx(Button, { size: "sm", variant: "secondary", disabled: conflict === "", onClick: () => set("launcherHotkeyConflict", ""), children: "Ask again" })] })) : null] }));
|
|
143
|
+
}
|
|
144
|
+
export function LauncherLists({ raw, shortcutNames = [], onSave }) {
|
|
145
|
+
const key = typeof raw === "string" ? raw : "";
|
|
146
|
+
const [draft, setDraft] = useState(() => parseLauncherConfig(key));
|
|
147
|
+
const [dirty, setDirty] = useState(false);
|
|
148
|
+
// The notch's copy changed (or our save landed): show it.
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
setDraft(parseLauncherConfig(key));
|
|
151
|
+
setDirty(false);
|
|
152
|
+
}, [key]);
|
|
153
|
+
const edit = (next) => {
|
|
154
|
+
setDraft(next);
|
|
155
|
+
setDirty(true);
|
|
156
|
+
};
|
|
157
|
+
const input = (value, placeholder, onChange, flex = 1) => (_jsx(Input, { className: "ck-input", "aria-label": placeholder, value: value, placeholder: placeholder, style: { flex: `${flex} 1 ${flex * 110}px` }, onChange: (e) => onChange(e.target.value) }));
|
|
158
|
+
const removeAt = (list, index) => list.filter((_, i) => i !== index);
|
|
159
|
+
const hotkeyNote = "Needs a modifier: for example opt+1, cmd+shift+k or ctrl+opt+d.";
|
|
160
|
+
return (_jsxs(_Fragment, { children: [(_jsxs(Section, { title: "Folders for file search", note: isWindows ? "The launcher searches file names only inside these folders. Empty turns file search off." : "Pulse searches file names itself, with no Spotlight index, only inside these folders. Empty turns file search off. Command space stays taken until Spotlight's shortcut is turned off in System Settings.", children: [draft.fileFolders.map((path, i) => (_jsxs("div", { className: "ck-line", children: [_jsx("span", { className: "ck-sub ck-path", children: path }), _jsx(Button, { size: "sm", variant: "ghost", onClick: () => edit({ ...draft, fileFolders: removeAt(draft.fileFolders, i) }), children: "Remove" })] }, `folder-${i}`))), _jsx(AddLine, { placeholder: isWindows ? "C:\\Users\\you\\Documents" : "/Users/you/Documents", onAdd: (v) => edit({ ...draft, fileFolders: [...draft.fileFolders, v] }) })] })), _jsxs(Section, { title: "Pinned apps", note: isWindows ? "Pin an app in the launcher with Alt+P. Pinned apps show first when nothing is typed." : "Pin an app in the launcher with Command-P. Pinned apps show first when nothing is typed.", children: [draft.pinnedApps.map((path, i) => (_jsxs("div", { className: "ck-line", children: [_jsx("span", { className: "ck-sub ck-path", children: path }), _jsx(Button, { size: "sm", variant: "ghost", onClick: () => edit({ ...draft, pinnedApps: removeAt(draft.pinnedApps, i) }), children: "Remove" })] }, `pin-${i}`))), _jsx(AddLine, { placeholder: isWindows ? "C:\\Program Files\\App\\app.exe" : "/Applications/Safari.app", onAdd: (v) => edit({ ...draft, pinnedApps: [...draft.pinnedApps, v] }) })] }), !isWindows ? (_jsxs(Section, { title: "App hotkeys", note: `Press to show the app, or hide it when it is in front. ${hotkeyNote}`, children: [draft.appHotkeys.map((item, i) => (_jsxs("div", { className: "ck-line", children: [input(item.path, "/Applications/Safari.app", (v) => {
|
|
161
|
+
const next = [...draft.appHotkeys];
|
|
162
|
+
next[i] = { ...item, path: v };
|
|
163
|
+
edit({ ...draft, appHotkeys: next });
|
|
164
|
+
}, 2), input(item.hotkey, "opt+1", (v) => {
|
|
165
|
+
const next = [...draft.appHotkeys];
|
|
166
|
+
next[i] = { ...item, hotkey: v };
|
|
167
|
+
edit({ ...draft, appHotkeys: next });
|
|
168
|
+
}), _jsx(Button, { size: "sm", variant: "ghost", onClick: () => edit({ ...draft, appHotkeys: removeAt(draft.appHotkeys, i) }), children: "Remove" })] }, `app-hotkey-${i}`))), _jsx(Button, { size: "sm", variant: "secondary", onClick: () => edit({ ...draft, appHotkeys: [...draft.appHotkeys, { path: "", hotkey: "" }] }), children: "Add an app hotkey" })] })) : null, _jsxs(Section, { title: "Quicklinks", note: "{query} is replaced by the text after the keyword. {clipboard} and {date} are also filled in.", children: [draft.quicklinks.map((item, i) => (_jsxs("div", { className: "ck-line", children: [input(item.name, "Name", (v) => {
|
|
169
|
+
const next = [...draft.quicklinks];
|
|
170
|
+
next[i] = { ...item, name: v };
|
|
171
|
+
edit({ ...draft, quicklinks: next });
|
|
172
|
+
}, 1), input(item.keyword, "Keyword", (v) => {
|
|
173
|
+
const next = [...draft.quicklinks];
|
|
174
|
+
next[i] = { ...item, keyword: v };
|
|
175
|
+
edit({ ...draft, quicklinks: next });
|
|
176
|
+
}, 1), input(item.template, "https://github.com/search?q={query}", (v) => {
|
|
177
|
+
const next = [...draft.quicklinks];
|
|
178
|
+
next[i] = { ...item, template: v };
|
|
179
|
+
edit({ ...draft, quicklinks: next });
|
|
180
|
+
}, 3), _jsx(Button, { size: "sm", variant: "ghost", onClick: () => edit({ ...draft, quicklinks: removeAt(draft.quicklinks, i) }), children: "Remove" })] }, `quicklink-${i}`))), _jsx(Button, { size: "sm", variant: "secondary", onClick: () => edit({ ...draft, quicklinks: [...draft.quicklinks, { name: "", keyword: "", template: "" }] }), children: "Add a quicklink" })] }), _jsxs(Section, { title: "Snippets", note: "Markdown text pasted into the app you were using. {date}, {clipboard} and {argument} are filled in. {snippet:Name} includes another snippet by its name or keyword; a snippet that includes itself is left as written. To expand a snippet wherever you type, give it a keyword that starts with a symbol, such as ;sig, and turn on \u201CExpand snippet keywords as you type\u201D above.", children: [draft.snippets.map((item, i) => (_jsxs("div", { className: "ck-list", children: [_jsxs("div", { className: "ck-line", children: [input(item.name, "Name", (v) => {
|
|
181
|
+
const next = [...draft.snippets];
|
|
182
|
+
next[i] = { ...item, name: v };
|
|
183
|
+
edit({ ...draft, snippets: next });
|
|
184
|
+
}), input(item.keyword, "Keyword", (v) => {
|
|
185
|
+
const next = [...draft.snippets];
|
|
186
|
+
next[i] = { ...item, keyword: v };
|
|
187
|
+
edit({ ...draft, snippets: next });
|
|
188
|
+
}), _jsx(Button, { size: "sm", variant: "ghost", onClick: () => edit({ ...draft, snippets: removeAt(draft.snippets, i) }), children: "Remove" })] }), _jsx("textarea", { className: "ck-input", "aria-label": "Snippet text", rows: 4, value: item.body, placeholder: "Thanks,\n{clipboard}", onChange: (e) => {
|
|
189
|
+
const next = [...draft.snippets];
|
|
190
|
+
next[i] = { ...item, body: e.target.value };
|
|
191
|
+
edit({ ...draft, snippets: next });
|
|
192
|
+
} })] }, `snippet-${i}`))), _jsx(Button, { size: "sm", variant: "secondary", onClick: () => edit({ ...draft, snippets: [...draft.snippets, { name: "", keyword: "", body: "" }] }), children: "Add a snippet" })] }), !isWindows ? (_jsxs(Section, { title: "Shortcut aliases", note: "Type an alias in the launcher to put that Apple Shortcut first. Use the shortcut's exact name from the Shortcuts app. An alias is 1 to 16 letters or digits, and each is unique (case does not count).", children: [draft.shortcutAliases.map((item, i) => {
|
|
193
|
+
const problem = aliasProblem(draft.shortcutAliases, i);
|
|
194
|
+
return (_jsxs("div", { className: "ck-list", children: [_jsxs("div", { className: "ck-line", children: [input(item.shortcut, "Shortcut name", (v) => {
|
|
195
|
+
const next = [...draft.shortcutAliases];
|
|
196
|
+
next[i] = { ...item, shortcut: v };
|
|
197
|
+
edit({ ...draft, shortcutAliases: next });
|
|
198
|
+
}, 2), input(item.alias, "Alias", (v) => {
|
|
199
|
+
const next = [...draft.shortcutAliases];
|
|
200
|
+
next[i] = { ...item, alias: v };
|
|
201
|
+
edit({ ...draft, shortcutAliases: next });
|
|
202
|
+
}, 1), _jsx(Button, { size: "sm", variant: "ghost", onClick: () => edit({ ...draft, shortcutAliases: removeAt(draft.shortcutAliases, i) }), children: "Remove" })] }), problem ? _jsx("div", { className: "ck-sub", role: "status", children: problem }) : null] }, `alias-${i}`));
|
|
203
|
+
}), _jsx(AliasAdd, { existing: draft.shortcutAliases, onAdd: (value) => edit({ ...draft, shortcutAliases: [...draft.shortcutAliases, value] }) })] })) : null, !isWindows ? (_jsx(Section, { title: "Shortcut hotkeys", note: "A global hotkey that runs one Apple Shortcut without opening the launcher. Needs at least one of Control, Option, Shift or Command; Escape cancels. Press Set, then the keys. Save launcher lists to apply. None by default.", children: (() => {
|
|
204
|
+
const names = [...shortcutNames];
|
|
205
|
+
draft.shortcutHotkeys.forEach((h) => { if (!names.includes(h.shortcut))
|
|
206
|
+
names.push(h.shortcut); });
|
|
207
|
+
if (names.length === 0) {
|
|
208
|
+
return _jsx("div", { className: "ck-sub", role: "status", children: "No shortcuts found. They are listed while Apple Shortcuts is on and this window is open." });
|
|
209
|
+
}
|
|
210
|
+
return names.map((name) => {
|
|
211
|
+
const hotkey = draft.shortcutHotkeys.find((h) => h.shortcut === name)?.hotkey ?? "";
|
|
212
|
+
return (_jsx(ShortcutHotkeyLine, { name: name, hotkey: hotkey, takenBy: hotkeyTakenBy(draft.shortcutHotkeys, name, hotkey), onChange: (next) => edit({
|
|
213
|
+
...draft,
|
|
214
|
+
shortcutHotkeys: [
|
|
215
|
+
...draft.shortcutHotkeys.filter((h) => h.shortcut !== name),
|
|
216
|
+
...(next ? [{ shortcut: name, hotkey: next }] : []),
|
|
217
|
+
],
|
|
218
|
+
}) }, `shortcut-hotkey-${name}`));
|
|
219
|
+
});
|
|
220
|
+
})() })) : null, isWindows ? (_jsx(Section, { title: "Notes", note: "Type \u201Cnotes\u201D in the launcher to list or search your notes, \u201Cnote text\u201D to start one with that text, and \u201Cnotes trash\u201D for deleted ones, which are kept for 30 days. Each note is a Markdown file on this PC and opens in the program Windows uses for it; Alt+D deletes the selected note.", children: _jsx(Checkbox, { className: "ck-line", checked: draft.notesEnabled, onChange: (on) => edit({ ...draft, notesEnabled: on }), label: "Notes in the launcher" }) })) : (_jsx(Section, { title: "Notes", note: "Type \u201Cnotes\u201D in the launcher to list, search or start a note; \u201Cnote text\u201D starts one with that text. Notes are kept on this Mac only, and deleted ones can be restored for 30 days with \u201Cnotes trash\u201D.", children: _jsx(Checkbox, { className: "ck-line", checked: draft.notesEnabled, onChange: (on) => edit({ ...draft, notesEnabled: on }), label: "Notes in the launcher" }) })), (_jsxs(Section, { title: "Custom commands", note: isWindows ? "Run with cmd.exe /C; the output shows in the launcher. A command is stopped after 30 seconds." : "Run with /bin/zsh -lc; the output shows in the launcher. Optional hotkey runs it from anywhere.", children: [draft.commands.map((item, i) => (_jsxs("div", { className: "ck-line", children: [input(item.name, "Name", (v) => {
|
|
221
|
+
const next = [...draft.commands];
|
|
222
|
+
next[i] = { ...item, name: v };
|
|
223
|
+
edit({ ...draft, commands: next });
|
|
224
|
+
}), input(item.keyword, "Keyword", (v) => {
|
|
225
|
+
const next = [...draft.commands];
|
|
226
|
+
next[i] = { ...item, keyword: v };
|
|
227
|
+
edit({ ...draft, commands: next });
|
|
228
|
+
}), input(item.command, isWindows ? "ipconfig" : "df -h /", (v) => {
|
|
229
|
+
const next = [...draft.commands];
|
|
230
|
+
next[i] = { ...item, command: v };
|
|
231
|
+
edit({ ...draft, commands: next });
|
|
232
|
+
}, 2), isWindows ? null : input(item.hotkey, "hotkey", (v) => {
|
|
233
|
+
const next = [...draft.commands];
|
|
234
|
+
next[i] = { ...item, hotkey: v };
|
|
235
|
+
edit({ ...draft, commands: next });
|
|
236
|
+
}), _jsx(Button, { size: "sm", variant: "ghost", onClick: () => edit({ ...draft, commands: removeAt(draft.commands, i) }), children: "Remove" })] }, `command-${i}`))), _jsx(Button, { size: "sm", variant: "secondary", onClick: () => edit({ ...draft, commands: [...draft.commands, { name: "", keyword: "", command: "", hotkey: "" }] }), children: "Add a command" })] })), _jsxs("div", { className: "ck-set ck-savebar", children: [_jsx("span", { className: "ck-sub", role: "status", children: dirty ? "Not saved yet." : "Launcher lists are saved." }), _jsx(Button, { size: "sm", variant: "secondary", disabled: !dirty
|
|
237
|
+
|| draft.shortcutAliases.some((_, i) => aliasProblem(draft.shortcutAliases, i) !== null)
|
|
238
|
+
|| draft.shortcutHotkeys.some((h) => hotkeyTakenBy(draft.shortcutHotkeys, h.shortcut, h.hotkey) !== null), onClick: () => onSave({
|
|
239
|
+
...draft,
|
|
240
|
+
shortcutAliases: draft.shortcutAliases.map((a) => ({ shortcut: a.shortcut.trim(), alias: a.alias.trim() })),
|
|
241
|
+
}), children: "Save launcher lists" })] })] }));
|
|
242
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
export type TrustChoice = "allow" | "ask" | "deny";
|
|
2
|
+
/** One device with a trust record (`trust.devices` in share-state.json). */
|
|
3
|
+
export interface TrustDevice {
|
|
4
|
+
fingerprint: string;
|
|
5
|
+
/** The owner's name for it; empty until given. */
|
|
6
|
+
label: string;
|
|
7
|
+
/** What the device calls itself (a claim). */
|
|
8
|
+
alias: string;
|
|
9
|
+
model: string | null;
|
|
10
|
+
kind: string | null;
|
|
11
|
+
state: TrustChoice;
|
|
12
|
+
present: boolean;
|
|
13
|
+
/** It proved its key by certificate (identity); a callback proof is not this. */
|
|
14
|
+
verified: boolean;
|
|
15
|
+
/** How: its client certificate, or by answering at its address; null when never. */
|
|
16
|
+
proof: "certificate" | "callback" | null;
|
|
17
|
+
firstSeenMs: number;
|
|
18
|
+
lastSeenMs: number;
|
|
19
|
+
refused: number;
|
|
20
|
+
}
|
|
21
|
+
/** What the sharing service reports (hub/src-tauri/src/share.rs). */
|
|
22
|
+
export interface ShareState {
|
|
23
|
+
running: boolean;
|
|
24
|
+
error: string | null;
|
|
25
|
+
alias?: string;
|
|
26
|
+
saveDir?: string;
|
|
27
|
+
devices: {
|
|
28
|
+
fingerprint: string;
|
|
29
|
+
alias: string;
|
|
30
|
+
deviceType?: string | null;
|
|
31
|
+
ip: string;
|
|
32
|
+
state?: TrustChoice;
|
|
33
|
+
}[];
|
|
34
|
+
/** Per-device Allow / Ask / Deny; new devices start as `defaultState`. */
|
|
35
|
+
trust?: {
|
|
36
|
+
defaultState: "ask" | "deny";
|
|
37
|
+
devices: TrustDevice[];
|
|
38
|
+
};
|
|
39
|
+
warnings: string[];
|
|
40
|
+
/** macOS Local Network access: "granted" once a multicast send worked, "blocked" when macOS refuses. */
|
|
41
|
+
localNetwork: "unknown" | "granted" | "blocked";
|
|
42
|
+
/** Chat (pulse_core::chat): chats on this computer and on linked ones (over ssh). */
|
|
43
|
+
chat?: {
|
|
44
|
+
enabled: boolean;
|
|
45
|
+
/** Deliver chat messages to Claude through the the host app plugin: the effective state. */
|
|
46
|
+
claudeHookDelivery?: boolean;
|
|
47
|
+
/** The setting as written: auto (default), on or off. */
|
|
48
|
+
claudeHookDeliveryMode?: "auto" | "on" | "off";
|
|
49
|
+
/** List terminal chats active in the last 6 hours (default on). */
|
|
50
|
+
terminalChats?: boolean;
|
|
51
|
+
/** List and message Claude chats in other Claude profiles (default on). */
|
|
52
|
+
chatClaudeProfiles?: boolean;
|
|
53
|
+
/** List, read and reach Muse chats, and run the host app's Muse hook (default on). */
|
|
54
|
+
chatMuse?: boolean;
|
|
55
|
+
/** Each message a chat sends carries the user's last three typed turns (default on). */
|
|
56
|
+
stampUserWords?: boolean;
|
|
57
|
+
/** Start a turn in an idle Codex chat when a message arrives (default on). */
|
|
58
|
+
codexWakeIdle?: boolean;
|
|
59
|
+
/** Start a turn in an idle Claude Desktop chat when a message arrives (default on). */
|
|
60
|
+
claudeWakeIdle?: boolean;
|
|
61
|
+
/** Start a turn in an idle Muse chat when a message arrives (default on; Muse has no supported wake entry yet). */
|
|
62
|
+
museWakeIdle?: boolean;
|
|
63
|
+
/** Wake an idle Codex chat with one short line; the message goes in as hidden context (default on). */
|
|
64
|
+
codexHiddenDelivery?: boolean;
|
|
65
|
+
/** Show receipts of messages this chat sent (delivered, seen, released, ...) in chat hooks (default on). */
|
|
66
|
+
chatReceiptNotices?: boolean;
|
|
67
|
+
active?: boolean;
|
|
68
|
+
localChats?: number;
|
|
69
|
+
links?: {
|
|
70
|
+
device: string;
|
|
71
|
+
ssh: string;
|
|
72
|
+
chats: number | null;
|
|
73
|
+
error: string | null;
|
|
74
|
+
lastOkMs?: number | null;
|
|
75
|
+
lastErrorMs?: number | null;
|
|
76
|
+
}[];
|
|
77
|
+
activity?: {
|
|
78
|
+
sentMs?: number;
|
|
79
|
+
receivedMs?: number;
|
|
80
|
+
sent?: number;
|
|
81
|
+
received?: number;
|
|
82
|
+
lastOutcome?: string | null;
|
|
83
|
+
};
|
|
84
|
+
chats?: {
|
|
85
|
+
name: string;
|
|
86
|
+
kind: string;
|
|
87
|
+
status: string;
|
|
88
|
+
device: string;
|
|
89
|
+
local: boolean;
|
|
90
|
+
cwd?: string;
|
|
91
|
+
updatedMs?: number | null;
|
|
92
|
+
liveness?: string;
|
|
93
|
+
unread?: number;
|
|
94
|
+
evicted?: number;
|
|
95
|
+
}[];
|
|
96
|
+
lastError?: string | null;
|
|
97
|
+
device?: string;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/** Polls the sharing service; also refreshes when the service says something changed. */
|
|
101
|
+
export declare function useShareState(): ShareState | null;
|
|
102
|
+
/** General > Nearby sharing: send and receive files with LocalSend and other the host app Macs. */
|
|
103
|
+
export declare function NearbyGroup({ s, set }: {
|
|
104
|
+
s: Record<string, unknown>;
|
|
105
|
+
set: (key: string, value: unknown) => void;
|
|
106
|
+
}): import("react").JSX.Element;
|
|
107
|
+
/**
|
|
108
|
+
* The Permissions row for macOS Local Network access (Windows Firewall on Windows). Neither
|
|
109
|
+
* system can be asked for its state, so it is read from whether multicast sends are refused.
|
|
110
|
+
* With nearby sharing off nothing sends, so there is nothing to check: the row reads Off,
|
|
111
|
+
* as the Mac's does.
|
|
112
|
+
*/
|
|
113
|
+
export declare function LocalNetworkRow({ enabled, nearby, needs }: {
|
|
114
|
+
enabled: boolean;
|
|
115
|
+
nearby?: boolean;
|
|
116
|
+
needs?: string[];
|
|
117
|
+
}): import("react").JSX.Element | null;
|