mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 22:25:24 +00:00
feat: harden plugin api for separate repos, signed updates and generic external login
This commit is contained in:
1 parent
fb9c168b6b
commit
80944301dd
310 files changed
+6577
-4927
No files matched your search
@@ -116,7 +116,7 @@ export interface SessionGuestsV1 {
|
||||
recordJoin: (shareId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export const SESSION_GUESTS_KEY = "sessions.sharing.guests";
|
||||
export const SESSION_GUESTS_KEY = "session-sharing.guests";
|
||||
|
||||
export interface RecordingSink {
|
||||
/** Appends one batch; the first batch starts with the asciicast header. */
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// The terminal surfaces need xterm's own stylesheet.
|
||||
import "@xterm/xterm/css/xterm.css";
|
||||
import { lazy, Suspense, type ComponentType } from "react";
|
||||
import {
|
||||
Braces,
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useSyncExternalStore } from "react";
|
||||
|
||||
/**
|
||||
* The terminal theme the host editor is previewing, so an open terminal for
|
||||
* that host can show it before the host is saved. Null when nothing is being
|
||||
* previewed.
|
||||
*/
|
||||
let preview: string | null = null;
|
||||
const listeners = new Set<() => void>();
|
||||
|
||||
export function setThemePreview(theme: string | null): void {
|
||||
if (theme === preview) return;
|
||||
preview = theme;
|
||||
for (const listener of listeners) listener();
|
||||
}
|
||||
|
||||
function subscribe(listener: () => void): () => void {
|
||||
listeners.add(listener);
|
||||
return () => {
|
||||
listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
export function useThemePreview(): string | null {
|
||||
return useSyncExternalStore(
|
||||
subscribe,
|
||||
() => preview,
|
||||
() => preview,
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useMemo, useState, useEffect } from "react";
|
||||
import { setThemePreview } from "../look/theme-preview";
|
||||
import { Info, Palette, X, Zap } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
@@ -13,7 +14,6 @@ import {
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
useTabsSafe,
|
||||
} from "@termix/plugin-sdk/ui";
|
||||
import {
|
||||
useSettings,
|
||||
@@ -69,7 +69,8 @@ export function HostTerminalSettings({
|
||||
const { t } = useTranslation();
|
||||
const client = useTerminalClientSettings();
|
||||
const userSettings = useSettings("user");
|
||||
const { setPreviewTerminalTheme } = useTabsSafe();
|
||||
// The preview ends when the editor section goes away.
|
||||
useEffect(() => () => setThemePreview(null), []);
|
||||
|
||||
const stored = ((editorForm?.pluginSettings as HostPluginSettings)?.[
|
||||
PLUGIN_ID
|
||||
@@ -222,7 +223,7 @@ export function HostTerminalSettings({
|
||||
onChange={(e) => {
|
||||
const newTheme = e.target.value;
|
||||
setField("theme", newTheme);
|
||||
setPreviewTerminalTheme(newTheme);
|
||||
setThemePreview(newTheme);
|
||||
if (newTheme === "custom" && !form.customThemeColors) {
|
||||
setField("customThemeColors", {
|
||||
...TERMINAL_THEMES.termixDark.colors,
|
||||
|
||||
@@ -119,6 +119,7 @@ import {
|
||||
isElectron,
|
||||
} from "@termix/plugin-sdk/ui";
|
||||
import {
|
||||
notifyHostsChanged,
|
||||
useTranslation,
|
||||
invokeAction,
|
||||
usePluginApi,
|
||||
@@ -1558,9 +1559,7 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
onClick: () => {
|
||||
void enableHostAutoTmux(api, hostConfig.id as number)
|
||||
.then(() => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("termix:hosts-changed"),
|
||||
);
|
||||
notifyHostsChanged();
|
||||
toast.success(
|
||||
t("terminal.autoTmuxEnabled", { host: hostLabel }),
|
||||
);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { lazy, Suspense } from "react";
|
||||
import { useThemePreview } from "../look/theme-preview";
|
||||
import { TerminalSquare } from "lucide-react";
|
||||
import type { TerminalHandle, TerminalHostConfig } from "./Terminal";
|
||||
import { useIsMobile, useTabsSafe } from "@termix/plugin-sdk/ui";
|
||||
import { useIsMobile } from "@termix/plugin-sdk/ui";
|
||||
import {
|
||||
useTranslation,
|
||||
invokeAction,
|
||||
@@ -63,7 +64,7 @@ export function TerminalTabContent({
|
||||
const tab = tabRecord as unknown as TerminalTabRecord;
|
||||
const host = hostRecord as unknown as Host | undefined;
|
||||
const { t } = useTranslation();
|
||||
const { previewTerminalTheme } = useTabsSafe();
|
||||
const previewTerminalTheme = useThemePreview();
|
||||
const isMobile = useIsMobile();
|
||||
const useSshTitle = readHostTerminalSettings(
|
||||
hostTerminalValues(host),
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
useIsMobile,
|
||||
} from "@termix/plugin-sdk/ui";
|
||||
import {
|
||||
usePluginUiPreferences,
|
||||
useHostActions,
|
||||
useTabs,
|
||||
useTranslation,
|
||||
@@ -57,8 +58,6 @@ import {
|
||||
} from "@termix/plugin-sdk/frontend";
|
||||
type SelectedToolbarDensity = ToolbarDensity;
|
||||
|
||||
const DENSITY_STORAGE_KEY = "termix-terminal-toolbar-density";
|
||||
|
||||
/** Plugins contribute toolbar buttons here. Declared by the ssh-terminal plugin. */
|
||||
const TOOLBAR_SLOT_ID = TERMINAL_TOOLBAR_SLOT;
|
||||
|
||||
@@ -69,16 +68,12 @@ const DENSITY_OPTIONS: { value: ToolbarDensity }[] = [
|
||||
];
|
||||
const DENSITIES = DENSITY_OPTIONS.map((option) => option.value);
|
||||
|
||||
function readStoredDensity(): ToolbarDensity {
|
||||
if (typeof window === "undefined") return "labeled";
|
||||
try {
|
||||
const stored = window.localStorage.getItem(DENSITY_STORAGE_KEY);
|
||||
return DENSITIES.includes(stored as ToolbarDensity)
|
||||
? (stored as ToolbarDensity)
|
||||
: "labeled";
|
||||
} catch {
|
||||
return "labeled";
|
||||
}
|
||||
const LEGACY_DENSITY_KEY = "termix-terminal-toolbar-density";
|
||||
|
||||
function asDensity(value: unknown): ToolbarDensity {
|
||||
return DENSITIES.includes(value as ToolbarDensity)
|
||||
? (value as ToolbarDensity)
|
||||
: "labeled";
|
||||
}
|
||||
|
||||
const CONTROL =
|
||||
@@ -151,8 +146,28 @@ export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({
|
||||
const settings = useMemo(() => readToolbarSettings(host), [host]);
|
||||
const { anchor } = settings;
|
||||
const rememberDensity = settings.density === null;
|
||||
// The remembered density is this plugin's UI preference, so an interface
|
||||
// preset sets it too.
|
||||
const { values: uiPrefs, set: setUiPref } = usePluginUiPreferences<{
|
||||
toolbarDensity: ToolbarDensity;
|
||||
}>();
|
||||
const preferredDensity = asDensity(uiPrefs.toolbarDensity);
|
||||
useEffect(() => {
|
||||
// Before 2.9 the density lived in this browser's localStorage.
|
||||
try {
|
||||
const legacy = window.localStorage.getItem(LEGACY_DENSITY_KEY);
|
||||
if (legacy === null) return;
|
||||
window.localStorage.removeItem(LEGACY_DENSITY_KEY);
|
||||
if (DENSITIES.includes(legacy as ToolbarDensity)) {
|
||||
setUiPref("toolbarDensity", legacy as ToolbarDensity);
|
||||
}
|
||||
} catch {
|
||||
// Storage can be unavailable in hardened browser contexts.
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
const [density, setDensity] = useState<SelectedToolbarDensity>(
|
||||
() => settings.density ?? readStoredDensity(),
|
||||
() => settings.density ?? preferredDensity,
|
||||
);
|
||||
const [responsiveDensity, setResponsiveDensity] =
|
||||
useState<SelectedToolbarDensity>(() => density);
|
||||
@@ -217,26 +232,12 @@ export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({
|
||||
setPosition(next);
|
||||
}, [anchor]);
|
||||
|
||||
// A preset (or another tab) changing the saved density applies here too.
|
||||
useEffect(() => {
|
||||
if (!rememberDensity) return;
|
||||
if (desktopViewportReady !== true || isMobile !== false) return;
|
||||
try {
|
||||
window.localStorage.setItem(DENSITY_STORAGE_KEY, density);
|
||||
} catch {
|
||||
// Storage can be unavailable in hardened browser contexts.
|
||||
}
|
||||
}, [density, desktopViewportReady, isMobile, rememberDensity]);
|
||||
|
||||
// Picking an interface preset seeds this key, so pick the new value up
|
||||
// without needing the tab to remount.
|
||||
useEffect(() => {
|
||||
if (!rememberDensity) return;
|
||||
if (desktopViewportReady !== true || isMobile !== false) return;
|
||||
const handler = () => setDensity(readStoredDensity());
|
||||
window.addEventListener("terminalToolbarDensityChanged", handler);
|
||||
return () =>
|
||||
window.removeEventListener("terminalToolbarDensityChanged", handler);
|
||||
}, [desktopViewportReady, isMobile, rememberDensity]);
|
||||
setDensity(preferredDensity);
|
||||
}, [preferredDensity, desktopViewportReady, isMobile, rememberDensity]);
|
||||
useEffect(() => {
|
||||
if (desktopViewportReady !== true || isMobile !== false) return;
|
||||
const measurement = measurementRef.current;
|
||||
@@ -375,6 +376,7 @@ export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({
|
||||
|
||||
const setAndStoreDensity = (next: ToolbarDensity) => {
|
||||
setDensity(next);
|
||||
if (rememberDensity) setUiPref("toolbarDensity", next);
|
||||
};
|
||||
const reportImageResult = (
|
||||
action: () => void | Promise<void>,
|
||||
@@ -692,7 +694,10 @@ export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({
|
||||
}}
|
||||
>
|
||||
{collapsed ? (
|
||||
<div className="terminal-toolbar-collapsed-shell flex rounded-sm border border-border bg-background/90 p-0.5 shadow-lg backdrop-blur-sm">
|
||||
<div
|
||||
data-toolbar-collapsed
|
||||
className="relative flex rounded-sm border border-border bg-background/90 p-0.5 shadow-lg backdrop-blur-sm"
|
||||
>
|
||||
{expandButton}
|
||||
{grabButton()}
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user