overmux 0.0.8 → 0.0.10
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 +21 -0
- package/dist/bin.js +59 -21
- package/dist/bin.js.map +1 -1
- package/dist/docs/100-introduction/300-why-i-built-overmux.md +1 -1
- package/dist/docs/200-getting-started/200-install-overmux-desktop.md +13 -0
- package/dist/docs/400-reference/500-server/100-resources.md +25 -0
- package/dist/docs/400-reference/500-server/500-api.md +4 -2
- package/dist/docs/400-reference/600-client/010-commands.md +0 -29
- package/dist/docs/400-reference/600-client/030-native-web-view.md +166 -0
- package/dist/docs/400-reference/600-client/300-tech-stack-recommendations.md +1 -0
- package/dist/docs/400-reference/700-cli/600-docs.md +1 -1
- package/dist/exports/client.d.ts +44 -8
- package/dist/exports/client.d.ts.map +1 -1
- package/dist/exports/client.js +212 -40
- package/dist/exports/client.js.map +1 -1
- package/dist/exports/{index-Cz3xkCa4.d.ts → index-Dk11nKIv.d.ts} +2 -2
- package/dist/exports/index-Dk11nKIv.d.ts.map +1 -0
- package/dist/exports/index.d.ts +1 -1
- package/dist/exports/index.js +1 -1
- package/dist/exports/index.js.map +1 -1
- package/dist/internal/server/coordinator/server-child.js +75 -22
- package/dist/internal/server/coordinator/server-child.js.map +1 -1
- package/docs/100-introduction/300-why-i-built-overmux.md +1 -1
- package/docs/200-getting-started/200-install-overmux-desktop.md +13 -0
- package/docs/400-reference/500-server/100-resources.md +25 -0
- package/docs/400-reference/500-server/500-api.md +4 -2
- package/docs/400-reference/600-client/010-commands.md +0 -29
- package/docs/400-reference/600-client/030-native-web-view.md +166 -0
- package/docs/400-reference/600-client/300-tech-stack-recommendations.md +1 -0
- package/docs/400-reference/700-cli/600-docs.md +1 -1
- package/package.json +2 -2
- package/src/internal/client/client-definition.ts +9 -13
- package/src/internal/client/host/desktop-host.ts +28 -2
- package/src/internal/client/native-web-view.tsx +158 -0
- package/src/internal/client/overmux-react.ts +121 -27
- package/src/internal/client/shortcuts.tsx +17 -31
- package/src/internal/server/runtime/runtime-resources.ts +99 -20
- package/src/internal/server/websocket-connection.ts +27 -2
- package/src/public/client.ts +5 -0
- package/src/public/config.ts +1 -1
- package/dist/exports/index-Cz3xkCa4.d.ts.map +0 -1
package/dist/exports/client.js
CHANGED
|
@@ -3,7 +3,7 @@ import { a as notificationSchema, c as parseDeepLink, l as resolveOvermuxReturnT
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { QueryClient, QueryClientProvider, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
5
5
|
import { Component, createContext, createElement, useCallback, useContext, useEffect, useMemo, useReducer, useRef, useState, useSyncExternalStore } from "react";
|
|
6
|
-
import {
|
|
6
|
+
import { formatShortcutBinding, keyBindingSchema, keyBindingSequence, matchesKeyBinding, matchesShortcutBindingPrefix, normalizedShortcutBinding, shortcutBindingSchema } from "@overmux/keybindings";
|
|
7
7
|
import { createPortal } from "react-dom";
|
|
8
8
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
9
9
|
//#region src/internal/shared/background-notifications.ts
|
|
@@ -320,7 +320,6 @@ const writeClipboardText = async (text) => {
|
|
|
320
320
|
};
|
|
321
321
|
//#endregion
|
|
322
322
|
//#region src/internal/client/client-definition.ts
|
|
323
|
-
const shortcutBindingSchema = z.union([keyBindingSchema, z.array(keyBindingSchema).min(2)]);
|
|
324
323
|
const commandBindingSchema = z.union([shortcutBindingSchema, z.object({
|
|
325
324
|
binding: shortcutBindingSchema,
|
|
326
325
|
when: z.object({ media: z.string().trim().min(1) })
|
|
@@ -329,7 +328,6 @@ const chordPrefixSchema = z.object({
|
|
|
329
328
|
binding: keyBindingSchema,
|
|
330
329
|
unmatched: z.literal("replay-to-focused-input")
|
|
331
330
|
});
|
|
332
|
-
const formatShortcutBinding = (binding) => typeof binding === "string" ? formatKeyBinding(binding) : binding.map(formatKeyBinding).join(" ");
|
|
333
331
|
const validateBindings = (bindings) => bindings?.forEach((binding) => commandBindingSchema.parse(binding));
|
|
334
332
|
const defineCommandRegistry = () => (commands) => Object.fromEntries(Object.entries(commands).map(([id, command]) => {
|
|
335
333
|
validateBindings(command.defaultBindings);
|
|
@@ -452,9 +450,95 @@ const useCanonicalInput = (input) => {
|
|
|
452
450
|
const key = inputKey(input);
|
|
453
451
|
return useMemo(() => key ? JSON.parse(key) : void 0, [key]);
|
|
454
452
|
};
|
|
455
|
-
const
|
|
453
|
+
const pendingResourceInvalidations = /* @__PURE__ */ new WeakSet();
|
|
454
|
+
const invalidateResource = ({ queryClient, queryKey }) => {
|
|
455
|
+
const query = queryClient.getQueryCache().find({
|
|
456
|
+
exact: true,
|
|
457
|
+
queryKey
|
|
458
|
+
});
|
|
459
|
+
if (query && query.state.fetchStatus !== "idle") {
|
|
460
|
+
pendingResourceInvalidations.add(query);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
if (query) pendingResourceInvalidations.delete(query);
|
|
464
|
+
queryClient.invalidateQueries({
|
|
465
|
+
exact: true,
|
|
466
|
+
queryKey
|
|
467
|
+
});
|
|
468
|
+
};
|
|
469
|
+
const flushResourceInvalidation = ({ queryClient, queryKey }) => {
|
|
470
|
+
const query = queryClient.getQueryCache().find({
|
|
471
|
+
exact: true,
|
|
472
|
+
queryKey
|
|
473
|
+
});
|
|
474
|
+
if (!query || query.state.fetchStatus !== "idle") return;
|
|
475
|
+
if (pendingResourceInvalidations.delete(query)) queryClient.invalidateQueries({
|
|
476
|
+
exact: true,
|
|
477
|
+
queryKey
|
|
478
|
+
});
|
|
479
|
+
};
|
|
480
|
+
const useResourceInvalidation = ({ queryKey, skipped }) => {
|
|
481
|
+
const [id, canonicalInput] = queryKey;
|
|
456
482
|
const { manifest, overmuxServerApi } = useRuntime();
|
|
457
483
|
const queryClient = useQueryClient();
|
|
484
|
+
const invalidate = useCallback(() => invalidateResource({
|
|
485
|
+
queryClient,
|
|
486
|
+
queryKey
|
|
487
|
+
}), [queryClient, queryKey]);
|
|
488
|
+
useEffect(() => {
|
|
489
|
+
if (skipped) return;
|
|
490
|
+
let active = true;
|
|
491
|
+
const cache = queryClient.getQueryCache();
|
|
492
|
+
const observedQuery = cache.find({
|
|
493
|
+
exact: true,
|
|
494
|
+
queryKey
|
|
495
|
+
});
|
|
496
|
+
const unsubscribe = cache.subscribe((event) => {
|
|
497
|
+
if (event.type === "updated" && event.query === observedQuery && event.query.state.fetchStatus === "idle" && pendingResourceInvalidations.has(event.query)) queueMicrotask(() => {
|
|
498
|
+
if (active) flushResourceInvalidation({
|
|
499
|
+
queryClient,
|
|
500
|
+
queryKey
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
flushResourceInvalidation({
|
|
505
|
+
queryClient,
|
|
506
|
+
queryKey
|
|
507
|
+
});
|
|
508
|
+
return () => {
|
|
509
|
+
active = false;
|
|
510
|
+
unsubscribe();
|
|
511
|
+
};
|
|
512
|
+
}, [
|
|
513
|
+
queryClient,
|
|
514
|
+
queryKey,
|
|
515
|
+
skipped
|
|
516
|
+
]);
|
|
517
|
+
useEffect(() => {
|
|
518
|
+
if (skipped || !manifest.resources.includes(id)) return;
|
|
519
|
+
let active = true;
|
|
520
|
+
const unsubscribe = overmuxServerApi.subscribeResource({
|
|
521
|
+
id,
|
|
522
|
+
input: canonicalInput,
|
|
523
|
+
onError: () => active && invalidate(),
|
|
524
|
+
onInvalidate: () => active && invalidate()
|
|
525
|
+
});
|
|
526
|
+
return () => {
|
|
527
|
+
active = false;
|
|
528
|
+
unsubscribe();
|
|
529
|
+
};
|
|
530
|
+
}, [
|
|
531
|
+
manifest.resources,
|
|
532
|
+
canonicalInput,
|
|
533
|
+
id,
|
|
534
|
+
invalidate,
|
|
535
|
+
overmuxServerApi,
|
|
536
|
+
skipped
|
|
537
|
+
]);
|
|
538
|
+
return invalidate;
|
|
539
|
+
};
|
|
540
|
+
const useResourceById = (id, input, skipped) => {
|
|
541
|
+
const { overmuxServerApi } = useRuntime();
|
|
458
542
|
const canonicalInput = useCanonicalInput(input);
|
|
459
543
|
const queryKey = useMemo(() => [id, canonicalInput], [canonicalInput, id]);
|
|
460
544
|
const query = useQuery({
|
|
@@ -465,32 +549,10 @@ const useResourceById = (id, input, skipped) => {
|
|
|
465
549
|
}),
|
|
466
550
|
queryKey
|
|
467
551
|
});
|
|
468
|
-
|
|
469
|
-
id,
|
|
470
|
-
input: canonicalInput,
|
|
471
|
-
onError: () => void queryClient.invalidateQueries({
|
|
472
|
-
exact: true,
|
|
473
|
-
queryKey
|
|
474
|
-
}),
|
|
475
|
-
onInvalidate: () => void queryClient.invalidateQueries({
|
|
476
|
-
exact: true,
|
|
477
|
-
queryKey
|
|
478
|
-
})
|
|
479
|
-
}) : void 0, [
|
|
480
|
-
manifest.resources,
|
|
481
|
-
canonicalInput,
|
|
482
|
-
id,
|
|
483
|
-
overmuxServerApi,
|
|
484
|
-
queryClient,
|
|
552
|
+
const refetch = useResourceInvalidation({
|
|
485
553
|
queryKey,
|
|
486
554
|
skipped
|
|
487
|
-
|
|
488
|
-
const refetch = useCallback(() => {
|
|
489
|
-
queryClient.invalidateQueries({
|
|
490
|
-
exact: true,
|
|
491
|
-
queryKey
|
|
492
|
-
});
|
|
493
|
-
}, [queryClient, queryKey]);
|
|
555
|
+
});
|
|
494
556
|
if (skipped) return;
|
|
495
557
|
if (query.status === "pending") return {
|
|
496
558
|
refetch,
|
|
@@ -855,12 +917,6 @@ const installDeepLinkNavigation = ({ getInstance, navigate = defaultNavigate })
|
|
|
855
917
|
};
|
|
856
918
|
//#endregion
|
|
857
919
|
//#region src/internal/client/shortcuts.tsx
|
|
858
|
-
const bindingSequence = (binding) => typeof binding === "string" ? [binding] : [...binding];
|
|
859
|
-
const normalizedBinding = (binding) => bindingSequence(binding).map((key) => platformBinding(key));
|
|
860
|
-
const sequenceMatches = (binding, pending, event) => {
|
|
861
|
-
const sequence = normalizedBinding(binding);
|
|
862
|
-
return sequence.length > pending.length && pending.every((key, index) => sequence[index] === key) && matchesKeyBinding(sequence[pending.length], event);
|
|
863
|
-
};
|
|
864
920
|
const isTextInput = (element) => element instanceof HTMLInputElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement || element instanceof HTMLElement && (element.isContentEditable || element.getAttribute("role") === "textbox");
|
|
865
921
|
const acceptsTextInput = (bindings) => {
|
|
866
922
|
if (!(document.activeElement instanceof Element)) return false;
|
|
@@ -868,7 +924,7 @@ const acceptsTextInput = (bindings) => {
|
|
|
868
924
|
if (!terminal) return false;
|
|
869
925
|
if (terminal.closest("[data-copy-mode]")) return true;
|
|
870
926
|
return bindings.some((binding) => {
|
|
871
|
-
const sequence =
|
|
927
|
+
const sequence = keyBindingSequence(binding);
|
|
872
928
|
return sequence.length > 1 || sequence[0]?.startsWith("F") || sequence[0]?.includes("+");
|
|
873
929
|
});
|
|
874
930
|
};
|
|
@@ -966,8 +1022,8 @@ const ShortcutHost = ({ children, definition, registrations, registrationRevisio
|
|
|
966
1022
|
window.addEventListener("focusin", refreshFocus);
|
|
967
1023
|
return () => window.removeEventListener("focusin", refreshFocus);
|
|
968
1024
|
}, []);
|
|
1025
|
+
const shortcuts = flattenShortcuts(definition, (media) => window.matchMedia(media).matches);
|
|
969
1026
|
useEffect(() => {
|
|
970
|
-
const shortcuts = flattenShortcuts(definition, (media) => window.matchMedia(media).matches);
|
|
971
1027
|
let pending = [];
|
|
972
1028
|
let pendingEvents = [];
|
|
973
1029
|
let pendingTarget;
|
|
@@ -997,7 +1053,11 @@ const ShortcutHost = ({ children, definition, registrations, registrationRevisio
|
|
|
997
1053
|
clearPending();
|
|
998
1054
|
return;
|
|
999
1055
|
}
|
|
1000
|
-
const matches = shortcuts.flatMap((shortcut) => shortcut.bindings.filter((binding) =>
|
|
1056
|
+
const matches = shortcuts.flatMap((shortcut) => shortcut.bindings.filter((binding) => matchesShortcutBindingPrefix({
|
|
1057
|
+
binding,
|
|
1058
|
+
input: event,
|
|
1059
|
+
pending
|
|
1060
|
+
}) && canRunShortcut(shortcut, event, registrations)).map((binding) => ({
|
|
1001
1061
|
binding,
|
|
1002
1062
|
shortcut
|
|
1003
1063
|
})));
|
|
@@ -1009,14 +1069,14 @@ const ShortcutHost = ({ children, definition, registrations, registrationRevisio
|
|
|
1009
1069
|
}
|
|
1010
1070
|
return;
|
|
1011
1071
|
}
|
|
1012
|
-
const complete = matches.find(({ binding }) =>
|
|
1072
|
+
const complete = matches.find(({ binding }) => normalizedShortcutBinding(binding).length === pending.length + 1);
|
|
1013
1073
|
if (complete) {
|
|
1014
1074
|
clearPending();
|
|
1015
1075
|
if (runShortcut(complete.shortcut, event, registrations)) event.stopPropagation();
|
|
1016
1076
|
return;
|
|
1017
1077
|
}
|
|
1018
1078
|
if (!pending.length) pendingTarget = definition.chordPrefixes?.some((prefix) => matchesKeyBinding(prefix.binding, event)) ? selectShortcutInputTarget(inputTargets, document.activeElement) : void 0;
|
|
1019
|
-
pending = [...pending,
|
|
1079
|
+
pending = [...pending, normalizedShortcutBinding(matches[0].binding)[pending.length]];
|
|
1020
1080
|
pendingEvents = [...pendingEvents, event];
|
|
1021
1081
|
event.preventDefault();
|
|
1022
1082
|
event.stopPropagation();
|
|
@@ -1842,6 +1902,118 @@ const OvermuxHost = ({ definition }) => /* @__PURE__ */ jsx(OvermuxThemeScope, {
|
|
|
1842
1902
|
children: location.pathname === "/_overmux/settings" ? /* @__PURE__ */ jsx(HostedSettingsPage, {}) : location.pathname === "/_overmux/logout" ? /* @__PURE__ */ jsx(HostedLogoutPage, {}) : /* @__PURE__ */ jsx(OvermuxRuntime, { definition })
|
|
1843
1903
|
});
|
|
1844
1904
|
//#endregion
|
|
1845
|
-
|
|
1905
|
+
//#region src/internal/client/native-web-view.tsx
|
|
1906
|
+
const command = async (view, input, url) => {
|
|
1907
|
+
try {
|
|
1908
|
+
await view.bridge.command(input);
|
|
1909
|
+
} catch (error) {
|
|
1910
|
+
view.report({
|
|
1911
|
+
url,
|
|
1912
|
+
code: "ERR_DESKTOP_BRIDGE",
|
|
1913
|
+
message: String(error)
|
|
1914
|
+
});
|
|
1915
|
+
}
|
|
1916
|
+
};
|
|
1917
|
+
const trackBounds = (element, view) => {
|
|
1918
|
+
let previous = "";
|
|
1919
|
+
let frame = 0;
|
|
1920
|
+
const measure = () => {
|
|
1921
|
+
const rect = element.getBoundingClientRect();
|
|
1922
|
+
const visible = !document.hidden && element.checkVisibility({
|
|
1923
|
+
opacityProperty: true,
|
|
1924
|
+
visibilityProperty: true
|
|
1925
|
+
});
|
|
1926
|
+
const bounds = {
|
|
1927
|
+
x: Math.round(rect.x),
|
|
1928
|
+
y: Math.round(rect.y),
|
|
1929
|
+
width: visible ? Math.max(0, Math.round(rect.width)) : 0,
|
|
1930
|
+
height: visible ? Math.max(0, Math.round(rect.height)) : 0
|
|
1931
|
+
};
|
|
1932
|
+
const key = JSON.stringify(bounds);
|
|
1933
|
+
if (key !== previous) {
|
|
1934
|
+
previous = key;
|
|
1935
|
+
view.bridge.setBounds(view.id, bounds);
|
|
1936
|
+
}
|
|
1937
|
+
};
|
|
1938
|
+
const tick = () => {
|
|
1939
|
+
measure();
|
|
1940
|
+
frame = requestAnimationFrame(tick);
|
|
1941
|
+
};
|
|
1942
|
+
tick();
|
|
1943
|
+
document.addEventListener("visibilitychange", measure);
|
|
1944
|
+
return () => {
|
|
1945
|
+
cancelAnimationFrame(frame);
|
|
1946
|
+
document.removeEventListener("visibilitychange", measure);
|
|
1947
|
+
};
|
|
1948
|
+
};
|
|
1949
|
+
const NativeWebView = ({ url, className, style, fallback, onLoadError, allowedHttpOrigins = [], passthroughBindings = [] }) => {
|
|
1950
|
+
const bridge = typeof window === "undefined" || window.overmuxHost?.version !== 1 || window.overmuxHost.nativeWebView?.version !== 1 ? void 0 : window.overmuxHost.nativeWebView;
|
|
1951
|
+
const element = useRef(null);
|
|
1952
|
+
const mounted = useRef(void 0);
|
|
1953
|
+
const latest = useRef({
|
|
1954
|
+
url,
|
|
1955
|
+
onLoadError
|
|
1956
|
+
});
|
|
1957
|
+
latest.current = {
|
|
1958
|
+
url,
|
|
1959
|
+
onLoadError
|
|
1960
|
+
};
|
|
1961
|
+
const origins = JSON.stringify(allowedHttpOrigins);
|
|
1962
|
+
const bindings = JSON.stringify(passthroughBindings);
|
|
1963
|
+
useEffect(() => {
|
|
1964
|
+
if (!bridge || !element.current) return;
|
|
1965
|
+
const view = {
|
|
1966
|
+
id: crypto.randomUUID(),
|
|
1967
|
+
active: true,
|
|
1968
|
+
bridge,
|
|
1969
|
+
report: (error) => {
|
|
1970
|
+
queueMicrotask(() => {
|
|
1971
|
+
if (view.active) latest.current.onLoadError?.(error);
|
|
1972
|
+
});
|
|
1973
|
+
}
|
|
1974
|
+
};
|
|
1975
|
+
mounted.current = view;
|
|
1976
|
+
const unsubscribe = bridge.onLoadError(({ id, error }) => {
|
|
1977
|
+
if (id === view.id) view.report(error);
|
|
1978
|
+
});
|
|
1979
|
+
command(view, {
|
|
1980
|
+
type: "create",
|
|
1981
|
+
id: view.id
|
|
1982
|
+
}, latest.current.url);
|
|
1983
|
+
const stopTracking = trackBounds(element.current, view);
|
|
1984
|
+
return () => {
|
|
1985
|
+
view.active = false;
|
|
1986
|
+
mounted.current = void 0;
|
|
1987
|
+
stopTracking();
|
|
1988
|
+
unsubscribe();
|
|
1989
|
+
command(view, {
|
|
1990
|
+
type: "destroy",
|
|
1991
|
+
id: view.id
|
|
1992
|
+
}, latest.current.url);
|
|
1993
|
+
};
|
|
1994
|
+
}, [bridge]);
|
|
1995
|
+
useEffect(() => {
|
|
1996
|
+
const view = mounted.current;
|
|
1997
|
+
if (view) command(view, {
|
|
1998
|
+
type: "configure",
|
|
1999
|
+
id: view.id,
|
|
2000
|
+
url,
|
|
2001
|
+
allowedHttpOrigins: JSON.parse(origins),
|
|
2002
|
+
passthroughBindings: JSON.parse(bindings)
|
|
2003
|
+
}, url);
|
|
2004
|
+
}, [
|
|
2005
|
+
bindings,
|
|
2006
|
+
bridge,
|
|
2007
|
+
url,
|
|
2008
|
+
origins
|
|
2009
|
+
]);
|
|
2010
|
+
return bridge ? /* @__PURE__ */ jsx("div", {
|
|
2011
|
+
ref: element,
|
|
2012
|
+
className,
|
|
2013
|
+
style
|
|
2014
|
+
}) : /* @__PURE__ */ jsx(Fragment, { children: fallback });
|
|
2015
|
+
};
|
|
2016
|
+
//#endregion
|
|
2017
|
+
export { NativeWebView, OvermuxHost, OvermuxPortal, OvermuxThemeScope, createOvermuxHooks, defineCommandRegistry, defineOvermuxClient, disableBackgroundNotifications, enableBackgroundNotifications, formatShortcutBinding, readClipboardText, skipToken, useCommand, useCommands, useShortcutInputTarget, writeClipboardText };
|
|
1846
2018
|
|
|
1847
2019
|
//# sourceMappingURL=client.js.map
|