SPB Git

Repository Public

terminals.ts 3.9 KB 93 lines typescript

1import { create } from 'zustand';2import { consumeMods, NO_MODS, tapMod, type ModName, type Mods } from '@/components/terminal/keys';34export type TermStatus =5  | 'idle' | 'connecting' | 'open' | 'reconnecting' | 'offline'6  | 'ended' | 'recoverable' | 'notfound' | 'forbidden' | 'unauthorized';7export interface TermConn {8  status: TermStatus;9  latencyMs: number | null;10  exitCode: number | null;11  /** Human-readable reason while reconnecting (e.g. "Runtime unavailable"). */12  detail: string | null;13  /** The user scrolled up into the history. */14  scrolledUp: boolean;15  /** Output arrived while scrolled up. */16  unread: boolean;17  /** Number of successful re-attachments (drives the "Back online" banner). */18  reconnects: number;19}20export const IDLE_CONN: TermConn = { status: 'idle', latencyMs: null, exitCode: null, detail: null, scrolledUp: false, unread: false, reconnects: 0 };2122interface TerminalsState {23  openIds: string[];24  activeId: string | null;25  splitId: string | null;26  conns: Record<string, TermConn>;27  mods: Mods;28  open: (id: string, activate?: boolean) => void;29  close: (id: string) => void;30  activate: (id: string | null) => void;31  setSplit: (id: string | null) => void;32  prune: (existing: Set<string>) => void;33  setConn: (id: string, patch: Partial<TermConn>) => void;34  tapMod: (name: ModName, doubleTap: boolean) => void;35  consumeMods: () => void;36}3738const KEY = 'cr.tabs';39function load(): Pick<TerminalsState, 'openIds' | 'activeId' | 'splitId'> {40  try {41    const raw = typeof window === 'undefined' ? null : localStorage.getItem(KEY);42    const v = raw ? (JSON.parse(raw) as { openIds?: unknown; activeId?: unknown }) : null;43    const openIds = Array.isArray(v?.openIds) ? v.openIds.filter((x): x is string => typeof x === 'string') : [];44    const activeId = typeof v?.activeId === 'string' && openIds.includes(v.activeId) ? v.activeId : (openIds[0] ?? null);45    return { openIds, activeId, splitId: null };46  } catch {47    return { openIds: [], activeId: null, splitId: null };48  }49}50function persist(s: TerminalsState): void {51  try {52    // Layout only (which tabs are open). Session ids are not credentials.53    localStorage.setItem(KEY, JSON.stringify({ openIds: s.openIds, activeId: s.activeId }));54  } catch {55    /* storage unavailable */56  }57}5859export const useTerminals = create<TerminalsState>((set, get) => ({60  ...load(),61  conns: {},62  mods: NO_MODS,63  open: (id, activate = true) => {64    set((s) => ({ openIds: s.openIds.includes(id) ? s.openIds : [...s.openIds, id], activeId: activate ? id : s.activeId, splitId: activate && s.splitId === id ? null : s.splitId }));65    persist(get());66  },67  close: (id) => {68    set((s) => {69      const i = s.openIds.indexOf(id);70      const openIds = s.openIds.filter((x) => x !== id);71      const activeId = s.activeId === id ? (openIds[Math.min(i, openIds.length - 1)] ?? null) : s.activeId;72      const { [id]: _gone, ...conns } = s.conns;73      return { openIds, activeId, splitId: s.splitId === id || s.splitId === activeId ? null : s.splitId, conns };74    });75    persist(get());76  },77  activate: (id) => {78    set((s) => ({ activeId: id, splitId: s.splitId === id ? null : s.splitId }));79    persist(get());80  },81  setSplit: (id) => set((s) => ({ splitId: id && id !== s.activeId ? id : null })),82  prune: (existing) => {83    const s = get();84    const openIds = s.openIds.filter((id) => existing.has(id));85    if (openIds.length === s.openIds.length) return;86    set({ openIds, activeId: s.activeId && existing.has(s.activeId) ? s.activeId : (openIds[0] ?? null), splitId: s.splitId && existing.has(s.splitId) ? s.splitId : null });87    persist(get());88  },89  setConn: (id, patch) => set((s) => ({ conns: { ...s.conns, [id]: { ...(s.conns[id] ?? IDLE_CONN), ...patch } } })),90  tapMod: (name, doubleTap) => set((s) => ({ mods: { ...s.mods, [name]: tapMod(s.mods[name], doubleTap) } })),91  consumeMods: () => set((s) => ({ mods: consumeMods(s.mods) })),92}));93
Files main
Menu

Theme