import { useQueryClient } from "@tanstack/react-query"; import { Button } from "@unom/ui/button"; import { Pencil, Plus, RefreshCw, Trash2 } from "lucide-react"; import { type FC, type MouseEvent, type ReactNode, useEffect, useState } from "react"; import { getGetDisplayStateQueryKey, getGetDisplaySettingsQueryKey, useCreateCustomPreset, useDeleteCustomPreset, useGetDisplaySettings, useGetDisplayState, useReleaseDisplay, useSetDisplayLayout, useSetDisplaySettings, useUpdateCustomPreset, } from "@/api/gen/display/display"; import type { ApiDisplayInfo, CustomPreset, DisplayPolicy, EffectivePolicy, GameSession, Identity, KeepAlive, LayoutMode, ModeConflict, Preset, Topology, } from "@/api/gen/model"; import { ApiError } from "@/api/fetcher"; import { QueryState } from "@/components/query-state"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { cn } from "@/lib/utils"; import { m } from "@/paraglide/messages"; /** * Container: the host's virtual-display management policy (design/display-management.md). Lets the * operator pick a one-click preset OR set every option by hand — all WITHOUT any client connected * (this is the host's *next-connect* behavior). The live-display list + multi-monitor arrangement * table below act on whatever is currently streaming. */ export const DisplaySection: FC = () => { const qc = useQueryClient(); const q = useGetDisplaySettings(); const save = useSetDisplaySettings(); // Local edit buffer, seeded once from the server and re-seeded after every successful apply. const [draft, setDraft] = useState(null); useEffect(() => { if (q.data && draft === null) setDraft(q.data.settings); }, [q.data, draft]); // Apply a policy (a one-click preset, or the hand-edited Custom draft). A change takes effect on // the next connect; a live session keeps the display it opened on. const apply = (policy: DisplayPolicy) => save.mutate( { data: policy }, { onSuccess: (res) => { setDraft(res.settings); qc.invalidateQueries({ queryKey: getGetDisplaySettingsQueryKey() }); }, }, ); return (
{m.display_config_title()}

{m.host_displays_help()}

{q.data && draft && ( )}
{m.display_live()}
); }; /** Preset display order — Default first (the safe baseline), the situational ones, then Custom. */ const PRESET_ORDER = [ "default", "shared-desktop", "hotdesk", "workstation", "gaming-rig", "custom", ] as const; const DisplayForm: FC<{ draft: DisplayPolicy; setDraft: (p: DisplayPolicy) => void; presets: { id: string; summary: string; fields: EffectivePolicy }[]; customPresets: CustomPreset[]; apply: (p: DisplayPolicy) => void; busy: boolean; error?: string; }> = ({ draft, setDraft, presets, customPresets, apply, busy, error }) => { const qc = useQueryClient(); const createPreset = useCreateCustomPreset(); const updatePreset = useUpdateCustomPreset(); const deletePreset = useDeleteCustomPreset(); const invalidateSettings = () => qc.invalidateQueries({ queryKey: getGetDisplaySettingsQueryKey() }); const presetBusy = createPreset.isPending || updatePreset.isPending || deletePreset.isPending; const presetError = apiErrorMessage( createPreset.error ?? updatePreset.error ?? deletePreset.error, ); const preset: Preset = draft.preset ?? "custom"; const isCustom = preset === "custom"; // The Custom fields (defaults filled): the edit buffer when preset === "custom", and what a // preset→Custom switch is seeded from, so you customize starting from the current behavior. const customFields: EffectivePolicy = { keep_alive: draft.keep_alive ?? { mode: "duration", seconds: 10 }, topology: draft.topology ?? "auto", mode_conflict: draft.mode_conflict ?? "separate", identity: draft.identity ?? "per-client", layout: draft.layout ?? { mode: "auto-row", positions: {} }, max_displays: draft.max_displays ?? 4, }; const effective: EffectivePolicy = (isCustom ? undefined : presets.find((p) => p.id === preset)?.fields) ?? customFields; // The five named presets apply in ONE click; "Custom" reveals the fields, seeded from the current // effective behavior (nothing changes until you Save). const pickPreset = (id: string) => { if (id === "custom") { setDraft({ version: 1, preset: "custom", keep_alive: effective.keep_alive, topology: effective.topology, mode_conflict: effective.mode_conflict, identity: effective.identity, layout: effective.layout, max_displays: effective.max_displays, // Game-session is orthogonal to the preset — carry it through the Custom switch. game_session: draft.game_session ?? "auto", }); } else { apply({ ...draft, preset: id as Preset }); } }; // Applying a custom preset writes a `Custom` policy carrying its saved fields + game-session (the // one axis a preset DOES set) — the host has no separate apply route (design/gamemode-and-…). const applyCustomPreset = (p: CustomPreset) => apply({ version: 1, preset: "custom", ...p.fields, game_session: p.game_session ?? "auto", }); // A custom card is "current" when the in-force policy is a Custom one whose fields + game-session // value-match this preset (there is no id on DisplayPolicy — match by value). const customSelected = (p: CustomPreset): boolean => isCustom && (draft.game_session ?? "auto") === (p.game_session ?? "auto") && deepEqual(effective, p.fields); const anyCustomSelected = customPresets.some(customSelected); // Save the currently-in-force behavior (built-in OR hand-edited) as a new named preset. const saveAsPreset = () => { const name = prompt(m.display_preset_name())?.trim(); if (!name) return; // cancelled or empty createPreset.mutate( { data: { name, fields: effective, game_session: draft.game_session ?? "auto" }, }, { onSuccess: invalidateSettings }, ); }; const renamePreset = (p: CustomPreset) => { const name = prompt(m.display_preset_name(), p.name)?.trim(); if (!name) return; updatePreset.mutate( { id: p.id, data: { name, fields: p.fields, game_session: p.game_session ?? "auto" } }, { onSuccess: invalidateSettings }, ); }; const updatePresetToCurrent = (p: CustomPreset) => updatePreset.mutate( { id: p.id, data: { name: p.name, fields: effective, game_session: draft.game_session ?? "auto" }, }, { onSuccess: invalidateSettings }, ); const removePreset = (p: CustomPreset) => { if (!confirm(m.display_preset_delete_confirm())) return; deletePreset.mutate({ id: p.id }, { onSuccess: invalidateSettings }); }; const ka = customFields.keep_alive; // The duration value, remembered across the Off/Keep toggle so switching back restores it. const [keepSecs, setKeepSecs] = useState(ka.mode === "duration" ? ka.seconds : 300); return (
{/* One-click presets — a 2-up grid so each has room to breathe */}
{PRESET_ORDER.map((id) => { const p = presets.find((x) => x.id === id); const fields = id === "custom" ? undefined : p?.fields; const summary = id === "custom" ? m.display_custom_desc() : p?.summary; // The built-in "Custom" card is the hand-edit mode; when the active Custom policy // value-matches a saved preset, that preset's card owns the "current" ring instead. const selected = preset === id && !(id === "custom" && anyCustomSelected); const soon = DISABLED_PRESETS.has(id); const disabled = busy || soon; const pick = () => { if (!disabled) pickPreset(id); }; return ( { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); pick(); } }} className={cn( "flex h-full flex-col p-4", disabled ? "cursor-not-allowed opacity-60" : "cursor-pointer", selected && "ring-2 ring-primary", )} >
{(PRESET_LABEL[id] ?? (() => id))()} {soon && ( {m.display_preset_soon()} )} {selected && ( {m.display_preset_current()} )}
{summary && (

{summary}

)} {fields && (
{fmtKeepAlive(fields.keep_alive)} {tr(TOPOLOGY_LABEL, fields.topology)} {tr(CONFLICT_LABEL, fields.mode_conflict)} {tr(IDENTITY_LABEL, fields.identity)}
)}
); })}
{/* Custom presets — the operator's saved field-bundles, rendered like the built-ins but editable/deletable, plus a "Save as preset" that captures the current effective behavior. */}
{customPresets.length > 0 && (
{customPresets.map((p) => ( applyCustomPreset(p)} onRename={() => renamePreset(p)} onUpdate={() => updatePresetToCurrent(p)} onDelete={() => removePreset(p)} /> ))}
)} {presetError && (

{presetError}

)}
{/* Custom: every option by hand */} {isCustom && (
{ka.mode === "duration" && (
{ const n = Math.max(0, Number(e.target.value) || 0); setKeepSecs(n); setDraft({ ...draft, keep_alive: { mode: "duration", seconds: n } }); }} /> {m.display_keep_alive_seconds()}
)}
setDraft({ ...draft, topology: v as Topology })} /> setDraft({ ...draft, mode_conflict: v as ModeConflict })} /> setDraft({ ...draft, identity: v as Identity })} /> setDraft({ ...draft, layout: { mode: v as LayoutMode, positions: draft.layout?.positions ?? {} }, }) } /> setDraft({ ...draft, max_displays: Math.min(16, Math.max(1, Number(e.target.value) || 1)), }) } />
)} {/* Game-session routing — orthogonal to the preset/lifecycle axes, so it lives outside the Custom block and applies immediately on change (like a preset click). */}
{ const next = { ...draft, game_session: v as GameSession }; setDraft(next); apply(next); }} />
{/* What's in force right now */}
{m.display_effective()}: {fmtKeepAlive(effective.keep_alive)} {tr(TOPOLOGY_LABEL, effective.topology)} {tr(CONFLICT_LABEL, effective.mode_conflict)} {tr(IDENTITY_LABEL, effective.identity)} {tr(LAYOUT_LABEL, effective.layout.mode)} {`${effective.max_displays}×`} {(draft.game_session ?? "auto") === "dedicated" && ( {m.display_game_session_dedicated()} )}

{m.display_pending_note()}

{error &&

{error}

}
); }; /** A labeled config field — label, then the control, then optional help. The single source of the * label→control→help spacing so every field (keep-alive, the button groups, max-displays) lines up. */ const Field: FC<{ label: string; help?: string; children: ReactNode }> = ({ label, help, children, }) => (
{children} {help &&

{help}

}
); /** A [`Field`] whose control is a row of mutually-exclusive option buttons (topology / conflict / …). */ const Choice: FC<{ label: string; help?: string; value: string; options: readonly string[]; labels: Record string>; disabled: boolean; onPick: (v: string) => void; }> = ({ label, help, value, options, labels, disabled, onPick }) => (
{options.map((o) => ( ))}
); /** * One saved custom preset — the same interactive card as the built-ins (click to apply → writes a * `Custom` policy carrying `preset.fields`), plus rename / update-to-current / delete affordances * (each stops propagation so it doesn't also fire the card's apply). Field badges mirror the * built-ins; the game-session badge shows only when it isn't the default `auto`. */ const CustomPresetCard: FC<{ preset: CustomPreset; selected: boolean; busy: boolean; onApply: () => void; onRename: () => void; onUpdate: () => void; onDelete: () => void; }> = ({ preset, selected, busy, onApply, onRename, onUpdate, onDelete }) => { const fields = preset.fields; const stop = (fn: () => void) => (e: MouseEvent) => { e.stopPropagation(); if (!busy) fn(); }; return ( !busy && onApply()} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); if (!busy) onApply(); } }} className={cn( "flex h-full flex-col p-4", busy ? "cursor-not-allowed opacity-60" : "cursor-pointer", selected && "ring-2 ring-primary", )} >
{preset.name}
{selected && {m.display_preset_current()}}
{fmtKeepAlive(fields.keep_alive)} {tr(TOPOLOGY_LABEL, fields.topology)} {tr(CONFLICT_LABEL, fields.mode_conflict)} {tr(IDENTITY_LABEL, fields.identity)} {(preset.game_session ?? "auto") !== "auto" && ( {tr(GAME_SESSION_LABEL, preset.game_session)} )}
); }; /** * The host's live/kept virtual displays, polled from `/display/state`, each with a Release button * for lingering/pinned ones (active displays can't be released — that's session control). */ const LiveDisplays: FC = () => { const qc = useQueryClient(); const state = useGetDisplayState({ query: { refetchInterval: 2_000 } }); const release = useReleaseDisplay(); const displays = state.data?.displays ?? []; const kept = displays.filter((d) => d.state !== "active"); const doRelease = (slot?: number) => release.mutate( { data: { slot: slot ?? null } }, { onSuccess: () => qc.invalidateQueries({ queryKey: getGetDisplayStateQueryKey() }) }, ); return (
{kept.length > 0 && (
)} {displays.length === 0 ? (

{m.display_none_live()}

) : (
    {displays.map((d) => ( doRelease(d.slot)} /> ))}
)}
); }; /** * The multi-monitor **arrangement** editor (design/display-management.md §6.2): an x/y table over the * live displays that carry a stable identity slot (the manual-layout key). Saving writes * `PUT /display/layout`, which switches the host to a manual layout and applies from the next connect. * Shown only for a ≥2-display group — arranging a single display is moot. */ const DisplayArrangement: FC<{ displays: ApiDisplayInfo[] }> = ({ displays }) => { const qc = useQueryClient(); const saveLayout = useSetDisplayLayout(); // Only displays with a stable identity slot can be pinned (shared/anonymous ones have no key). const arrangeable = displays.filter((d) => d.identity_slot != null); // Local edit buffer keyed by identity-slot string → {x, y}, seeded once from the current positions. const [pos, setPos] = useState | null>(null); useEffect(() => { if (pos === null && arrangeable.length > 0) { const seed: Record = {}; for (const d of arrangeable) seed[String(d.identity_slot)] = { x: d.x, y: d.y }; setPos(seed); } }, [arrangeable, pos]); if (arrangeable.length < 2) return null; const cur = pos ?? {}; const setXY = (slot: number, key: "x" | "y", val: number) => { const k = String(slot); setPos({ ...cur, [k]: { ...(cur[k] ?? { x: 0, y: 0 }), [key]: val } }); }; const onSave = () => saveLayout.mutate( { data: { positions: cur } }, { onSuccess: () => qc.invalidateQueries({ queryKey: getGetDisplayStateQueryKey() }) }, ); return (

{m.display_arrange()}

{m.display_arrange_help()}

{arrangeable.map((d) => { const slot = d.identity_slot as number; const p = cur[String(slot)] ?? { x: d.x, y: d.y }; return (
{d.mode} #{slot} setXY(slot, "x", Math.trunc(Number(e.target.value) || 0))} /> setXY(slot, "y", Math.trunc(Number(e.target.value) || 0))} />
); })}
{saveLayout.error && (

{apiErrorMessage(saveLayout.error)}

)}
); }; const DisplayRow: FC<{ d: ApiDisplayInfo; busy: boolean; onRelease: () => void }> = ({ d, busy, onRelease, }) => { const active = d.state === "active"; const stateLabel = d.state === "active" ? m.display_state_active() : d.state === "pinned" ? m.display_state_pinned() : m.display_state_lingering(); return (
  • {d.mode} {stateLabel} {active && d.sessions > 0 && ( {m.display_sessions({ count: d.sessions })} )}
    {d.backend} {d.expires_in_ms != null ? ` · ${m.display_expires_in({ sec: Math.ceil(d.expires_in_ms / 1000) })}` : ""}
    {!active && ( )}
  • ); }; /** The server's `{ error }` message from a thrown `ApiError` (its `.data` body), for inline display. */ const apiErrorMessage = (err: unknown): string | undefined => { if (err instanceof ApiError) { const data = err.data as { error?: string } | undefined; return data?.error ?? err.message; } return err ? String(err) : undefined; }; /** Presets the host can't honor yet (one-click apply would 400) are surfaced but disabled. Empty * now that `gaming-rig` (`keep_alive: forever`) ships: the display is Pinned (Linux + Windows) and * freed via Release. */ const DISABLED_PRESETS: ReadonlySet = new Set(); const PRESET_LABEL: Record string> = { custom: m.display_preset_custom, default: m.display_preset_default, "gaming-rig": m.display_preset_gaming_rig, "shared-desktop": m.display_preset_shared_desktop, hotdesk: m.display_preset_hotdesk, workstation: m.display_preset_workstation, }; const TOPOLOGY_LABEL: Record string> = { auto: m.display_topology_auto, extend: m.display_topology_extend, primary: m.display_topology_primary, exclusive: m.display_topology_exclusive, }; const CONFLICT_LABEL: Record string> = { separate: m.display_conflict_separate, steal: m.display_conflict_steal, join: m.display_conflict_join, reject: m.display_conflict_reject, }; const IDENTITY_LABEL: Record string> = { shared: m.display_identity_shared, "per-client": m.display_identity_per_client, "per-client-mode": m.display_identity_per_client_mode, }; const LAYOUT_LABEL: Record string> = { "auto-row": m.display_layout_auto_row, manual: m.display_layout_manual, }; const GAME_SESSION_LABEL: Record string> = { auto: m.display_game_session_auto, dedicated: m.display_game_session_dedicated, }; /** Structural equality for the value-match of a custom preset's fields against the effective policy * (handles the nested `keep_alive` variants + `layout.positions` map; key order doesn't matter). */ const deepEqual = (a: unknown, b: unknown): boolean => { if (a === b) return true; if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false; const ak = Object.keys(a as object); const bk = Object.keys(b as object); if (ak.length !== bk.length) return false; return ak.every((k) => deepEqual((a as Record)[k], (b as Record)[k]), ); }; /** Look up a localized label, tolerating an unknown/undefined key (falls back to the raw value). */ const tr = (map: Record string>, key: string | null | undefined): string => { const fn = key == null ? undefined : map[key]; return fn ? fn() : String(key ?? ""); }; const fmtKeepAlive = (k: KeepAlive): string => { switch (k.mode) { case "off": return m.display_keep_alive_off(); case "duration": return `${k.seconds} ${m.display_keep_alive_seconds()}`; case "forever": return "∞"; } };