Z ZPI 文档 v0.3.8
介绍页

源码

packages/desktop/src/renderer/TerminalPane.tsx

tsx120 行查看原文
import { FitAddon } from "@xterm/addon-fit";
import { WebLinksAddon } from "@xterm/addon-web-links";
import { Terminal } from "@xterm/xterm";
import { useEffect, useRef } from "react";
import "@xterm/xterm/css/xterm.css";
import { observeAppearance } from "@zpi-dev/ui";
import type { TerminalProfile } from "../shared/bridge.ts";
import { openBrowser, paneTask, terminalExited, terminalListeners, terminalOutput } from "./pane-store.ts";
import { report } from "./store.ts";
import { terminalCssColor, terminalOptions, terminalTheme } from "./terminal-presentation.ts";
import { unwrap } from "./unwrap.ts";
export function TerminalPane({
  id,
  sessionId,
  visible,
  cwd,
  profile,
}: {
  id: string;
  sessionId: string;
  visible: boolean;
  cwd: string;
  profile?: TerminalProfile;
}) {
  const root = useRef<HTMLDivElement>(null),
    terminal = useRef<Terminal | undefined>(undefined),
    fit = useRef<FitAddon | undefined>(undefined);
  useEffect(() => {
    const element = root.current;
    if (!element) return;
    const openLink = (event: MouseEvent, url: string) => {
      event.preventDefault();
      if (/^https?:\/\//i.test(url)) paneTask(openBrowser(url, sessionId));
    };
    const term = new Terminal({
      ...terminalOptions(profile),
      linkHandler: { allowNonHttpProtocols: false, activate: openLink },
    });
    const fitting = new FitAddon();
    terminal.current = term;
    fit.current = fitting;
    term.loadAddon(fitting);
    term.loadAddon(new WebLinksAddon(openLink));
    term.open(element);
    term.write(terminalOutput.get(id) ?? "");
    term.options.disableStdin = terminalExited.has(id);
    terminalListeners.set(id, (event) => {
      term.write(event.data);
      if (event.exited) term.options.disableStdin = true;
    });
    const input = term.onData((data) => {
      if (!terminalExited.has(id)) paneTask(window.zpi.terminalInput(id, data).then(unwrap));
    });
    const resize = term.onResize(({ cols, rows }) =>
      paneTask(window.zpi.resizeTerminal(id, cols, rows).then(unwrap)),
    );
    let frame = 0;
    const fitSize = () => {
      cancelAnimationFrame(frame);
      frame = requestAnimationFrame(() => {
        if (element.clientWidth && element.clientHeight) fitting.fit();
      });
    };
    const observer = new ResizeObserver(fitSize);
    observer.observe(element);
    const theme = () => {
      const styles = getComputedStyle(document.documentElement);
      const dark = document.documentElement.dataset.theme === "dark";
      term.options.theme = terminalTheme(
        dark,
        terminalCssColor(styles, "--color-bg", dark ? "#161616" : "#f8f8f8"),
        terminalCssColor(styles, "--color-text", dark ? "#d4d4d4" : "#262626"),
        profile,
      );
      fitSize();
    };
    theme();
    const unsubscribeAppearance = observeAppearance(theme);
    fitSize();
    const copy = (event: KeyboardEvent) => {
      if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "c" && term.hasSelection()) {
        event.preventDefault();
        void window.zpi.copyText(term.getSelection()).then(unwrap).catch(report);
      }
    };
    term.attachCustomKeyEventHandler((event) => {
      if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "c" && term.hasSelection()) {
        copy(event);
        return false;
      }
      return true;
    });
    return () => {
      observer.disconnect();
      cancelAnimationFrame(frame);
      input.dispose();
      resize.dispose();
      terminalListeners.delete(id);
      unsubscribeAppearance();
      term.dispose();
      terminal.current = undefined;
      fit.current = undefined;
    };
  }, [id, sessionId, profile]);
  useEffect(() => {
    if (!visible) return;
    const frame = requestAnimationFrame(() => {
      fit.current?.fit();
      if (!document.querySelector('dialog[open], [role="dialog"], [role="menu"], .settings-screen'))
        terminal.current?.focus();
    });
    return () => cancelAnimationFrame(frame);
  }, [id, visible]);
  return (
    <div className="terminal-pane" title={cwd}>
      <div ref={root} className="terminal-surface" data-testid="terminal-surface" />
    </div>
  );
}