85 lines
2.4 KiB
TypeScript
85 lines
2.4 KiB
TypeScript
|
|
import { useCallback, useSyncExternalStore } from 'react';
|
||
|
|
|
||
|
|
import { STORAGE_KEY_TERMINAL_HOST_TREE_COLLAPSED } from '../../infrastructure/config/storageKeys';
|
||
|
|
import { localStorageAdapter } from '../../infrastructure/persistence/localStorageAdapter';
|
||
|
|
|
||
|
|
type Listener = () => void;
|
||
|
|
|
||
|
|
export const TERMINAL_HOST_TREE_MIN_WIDTH = 176;
|
||
|
|
export const TERMINAL_HOST_TREE_DEFAULT_WIDTH = 220;
|
||
|
|
export const TERMINAL_HOST_TREE_MAX_WIDTH = 360;
|
||
|
|
|
||
|
|
export function clampTerminalHostTreeWidth(width: number): number {
|
||
|
|
return Math.max(
|
||
|
|
TERMINAL_HOST_TREE_MIN_WIDTH,
|
||
|
|
Math.min(TERMINAL_HOST_TREE_MAX_WIDTH, width),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function readIsOpen(): boolean {
|
||
|
|
const stored = localStorageAdapter.readString(STORAGE_KEY_TERMINAL_HOST_TREE_COLLAPSED);
|
||
|
|
// Legacy key stores "collapsed"; open is the inverse.
|
||
|
|
if (stored === 'true') return false;
|
||
|
|
if (stored === 'false') return true;
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
class TerminalHostTreeStore {
|
||
|
|
private isOpen = readIsOpen();
|
||
|
|
/** Live sidebar width (0 when collapsed) for top-tab alignment. */
|
||
|
|
private layoutWidth = 0;
|
||
|
|
private listeners = new Set<Listener>();
|
||
|
|
|
||
|
|
getIsOpen = () => this.isOpen;
|
||
|
|
|
||
|
|
getLayoutWidth = () => this.layoutWidth;
|
||
|
|
|
||
|
|
setIsOpen = (open: boolean) => {
|
||
|
|
if (this.isOpen === open) return;
|
||
|
|
this.isOpen = open;
|
||
|
|
localStorageAdapter.writeString(
|
||
|
|
STORAGE_KEY_TERMINAL_HOST_TREE_COLLAPSED,
|
||
|
|
open ? 'false' : 'true',
|
||
|
|
);
|
||
|
|
this.listeners.forEach((listener) => listener());
|
||
|
|
};
|
||
|
|
|
||
|
|
setLayoutWidth = (width: number) => {
|
||
|
|
const next = Math.max(0, Math.round(width));
|
||
|
|
if (this.layoutWidth === next) return;
|
||
|
|
this.layoutWidth = next;
|
||
|
|
this.listeners.forEach((listener) => listener());
|
||
|
|
};
|
||
|
|
|
||
|
|
toggle = () => {
|
||
|
|
this.setIsOpen(!this.isOpen);
|
||
|
|
};
|
||
|
|
|
||
|
|
subscribe = (listener: Listener) => {
|
||
|
|
this.listeners.add(listener);
|
||
|
|
return () => this.listeners.delete(listener);
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
export const terminalHostTreeStore = new TerminalHostTreeStore();
|
||
|
|
|
||
|
|
export const useTerminalHostTreeOpen = () => {
|
||
|
|
return useSyncExternalStore(
|
||
|
|
terminalHostTreeStore.subscribe,
|
||
|
|
terminalHostTreeStore.getIsOpen,
|
||
|
|
terminalHostTreeStore.getIsOpen,
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
export const useToggleTerminalHostTree = () => {
|
||
|
|
return useCallback(() => terminalHostTreeStore.toggle(), []);
|
||
|
|
};
|
||
|
|
|
||
|
|
export const useTerminalHostTreeLayoutWidth = () => {
|
||
|
|
return useSyncExternalStore(
|
||
|
|
terminalHostTreeStore.subscribe,
|
||
|
|
terminalHostTreeStore.getLayoutWidth,
|
||
|
|
terminalHostTreeStore.getLayoutWidth,
|
||
|
|
);
|
||
|
|
};
|