securityos/components/apps/Terminal/useTerminal.ts

201 lines
5.9 KiB
TypeScript

import { config, PROMPT_CHARACTER } from "components/apps/Terminal/config";
import { autoComplete } from "components/apps/Terminal/functions";
import type {
FitAddon,
LocalEcho,
OnKeyEvent,
} from "components/apps/Terminal/types";
import useCommandInterpreter from "components/apps/Terminal/useCommandInterpreter";
import extensions from "components/system/Files/FileEntry/extensions";
import { useFileSystem } from "contexts/fileSystem";
import { useProcesses } from "contexts/process";
import { useSession } from "contexts/session";
import useResizeObserver from "hooks/useResizeObserver";
import { extname } from "path";
import {
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from "react";
import { HOME, PACKAGE_DATA, PREVENT_SCROLL } from "utils/constants";
import { haltEvent, isFirefox, loadFiles } from "utils/functions";
import type { IDisposable, Terminal } from "xterm";
const { alias, author, license, version } = PACKAGE_DATA;
export const displayLicense = `${license} License`;
export const displayVersion = (): string => {
const { __NEXT_DATA__: { buildId } = {} } = window;
return `${version}${buildId ? `-${buildId}` : ""}`;
};
const useTerminal = (
id: string,
url: string,
containerRef: React.MutableRefObject<HTMLDivElement | null>,
setLoading: React.Dispatch<React.SetStateAction<boolean>>,
loading: boolean
): void => {
const {
url: setUrl,
processes: { [id]: { closing = false, libs = [] } = {} },
} = useProcesses();
const cd = useRef(url || HOME);
const { readdir } = useFileSystem();
const [terminal, setTerminal] = useState<Terminal>();
const [fitAddon, setFitAddon] = useState<FitAddon>();
const [localEcho, setLocalEcho] = useState<LocalEcho>();
const [initialCommand, setInitialCommand] = useState("");
const [prompted, setPrompted] = useState(false);
const processCommand = useCommandInterpreter(id, cd, terminal, localEcho);
const autoFit = useCallback(() => fitAddon?.fit(), [fitAddon]);
const { foregroundId } = useSession();
useEffect(() => {
if (url) {
if (localEcho) {
localEcho.handleCursorInsert(url.includes(" ") ? `"${url}"` : url);
} else {
const fileExtension = extname(url).toLowerCase();
const { command: extCommand = "" } = extensions[fileExtension] || {};
if (extCommand) setInitialCommand(`${extCommand} ${url}`);
}
setUrl(id, "");
}
}, [id, localEcho, setUrl, url]);
useEffect(() => {
loadFiles(libs).then(() => {
if (window.Terminal) setTerminal(new window.Terminal(config));
});
}, [libs]);
useEffect(() => {
if (
terminal &&
loading &&
containerRef.current &&
window.FitAddon &&
window.LocalEchoController
) {
const newFitAddon = new window.FitAddon.FitAddon();
const newLocalEcho = new window.LocalEchoController(undefined, {
historySize: 1000,
});
terminal.loadAddon(newLocalEcho);
terminal.loadAddon(newFitAddon);
terminal.open(containerRef.current);
newFitAddon.fit();
setFitAddon(newFitAddon);
setLocalEcho(newLocalEcho);
containerRef.current.addEventListener("contextmenu", (event) => {
haltEvent(event);
const textSelection = terminal.getSelection();
if (textSelection) {
navigator.clipboard?.writeText(textSelection);
terminal.clearSelection();
} else {
navigator.clipboard
?.readText?.()
.then((clipboardText) =>
newLocalEcho.handleCursorInsert(clipboardText)
);
}
});
containerRef.current
?.closest("section")
?.addEventListener(
"focus",
() => terminal?.textarea?.focus(PREVENT_SCROLL),
{ passive: true }
);
setLoading(false);
if (isFirefox()) terminal.options.letterSpacing = 0;
}
return () => {
if (terminal && closing) terminal.dispose();
};
}, [closing, containerRef, loading, setLoading, terminal]);
useEffect(() => {
let currentOnKey: IDisposable;
if (terminal && localEcho) {
terminal.textarea?.setAttribute("enterkeyhint", "send");
currentOnKey = terminal.onKey(
({ domEvent: { ctrlKey, code } }: OnKeyEvent) => {
if (ctrlKey && code === "KeyV") {
navigator.clipboard
?.readText?.()
.then((clipboardText) =>
localEcho.handleCursorInsert(clipboardText)
);
}
}
);
}
return () => currentOnKey?.dispose();
}, [localEcho, terminal]);
useEffect(() => {
if (localEcho && terminal && !prompted) {
const prompt = (): Promise<void> =>
localEcho
.read(`\r\n${cd.current}${PROMPT_CHARACTER}`)
.then((command) => processCommand.current?.(command).then(prompt));
localEcho.println(`${alias} [Version ${displayVersion()}]`);
localEcho.println(`Por ${author.name}. ${displayLicense}.`);
if (initialCommand) {
localEcho.println(
`\r\n${cd.current}${PROMPT_CHARACTER}${initialCommand}\r\n`
);
localEcho.history.entries = [initialCommand];
processCommand.current(initialCommand).then(prompt);
} else {
prompt();
}
setPrompted(true);
terminal.focus();
autoFit();
readdir(cd.current).then((files) => autoComplete(files, localEcho));
}
}, [
autoFit,
initialCommand,
localEcho,
processCommand,
prompted,
readdir,
terminal,
]);
useLayoutEffect(() => {
if (id === foregroundId && !loading) {
terminal?.textarea?.focus(PREVENT_SCROLL);
}
}, [foregroundId, id, loading, terminal]);
useResizeObserver(containerRef.current, autoFit);
};
export default useTerminal;