mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-07 19:28:44 +01:00
64084d3e27
Removes global atoms dependency from emain by moving WOS to grab the globalAtoms from window, if present. Also removes interdependency between wshrpcutil and wps Also adds showmenubar setting for Windows and Linux
273 lines
9.3 KiB
TypeScript
273 lines
9.3 KiB
TypeScript
// Copyright 2024, Command Line Inc.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
import { getFileSubject } from "@/app/store/wps";
|
|
import { sendWSCommand } from "@/app/store/ws";
|
|
import { RpcApi } from "@/app/store/wshclientapi";
|
|
import { WindowRpcClient } from "@/app/store/wshrpcutil";
|
|
import { PLATFORM, WOS, atoms, fetchWaveFile, globalStore, openLink } from "@/store/global";
|
|
import * as services from "@/store/services";
|
|
import * as util from "@/util/util";
|
|
import { base64ToArray, fireAndForget } from "@/util/util";
|
|
import { SerializeAddon } from "@xterm/addon-serialize";
|
|
import { WebLinksAddon } from "@xterm/addon-web-links";
|
|
import { WebglAddon } from "@xterm/addon-webgl";
|
|
import * as TermTypes from "@xterm/xterm";
|
|
import { Terminal } from "@xterm/xterm";
|
|
import debug from "debug";
|
|
import { debounce } from "throttle-debounce";
|
|
import { FitAddon } from "./fitaddon";
|
|
|
|
const dlog = debug("wave:termwrap");
|
|
|
|
const TermFileName = "term";
|
|
const TermCacheFileName = "cache:term:full";
|
|
const MinDataProcessedForCache = 100 * 1024;
|
|
|
|
// detect webgl support
|
|
function detectWebGLSupport(): boolean {
|
|
try {
|
|
const canvas = document.createElement("canvas");
|
|
const ctx = canvas.getContext("webgl");
|
|
return !!ctx;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
const WebGLSupported = detectWebGLSupport();
|
|
let loggedWebGL = false;
|
|
|
|
type TermWrapOptions = {
|
|
keydownHandler?: (e: KeyboardEvent) => boolean;
|
|
useWebGl?: boolean;
|
|
};
|
|
|
|
export class TermWrap {
|
|
blockId: string;
|
|
ptyOffset: number;
|
|
dataBytesProcessed: number;
|
|
terminal: Terminal;
|
|
connectElem: HTMLDivElement;
|
|
fitAddon: FitAddon;
|
|
serializeAddon: SerializeAddon;
|
|
mainFileSubject: SubjectWithRef<WSFileEventData>;
|
|
loaded: boolean;
|
|
heldData: Uint8Array[];
|
|
handleResize_debounced: () => void;
|
|
isRunning: boolean;
|
|
hasResized: boolean;
|
|
|
|
constructor(
|
|
blockId: string,
|
|
connectElem: HTMLDivElement,
|
|
options: TermTypes.ITerminalOptions & TermTypes.ITerminalInitOnlyOptions,
|
|
waveOptions: TermWrapOptions
|
|
) {
|
|
this.loaded = false;
|
|
this.blockId = blockId;
|
|
this.ptyOffset = 0;
|
|
this.dataBytesProcessed = 0;
|
|
this.hasResized = false;
|
|
this.terminal = new Terminal(options);
|
|
this.fitAddon = new FitAddon();
|
|
this.fitAddon.noScrollbar = PLATFORM == "darwin";
|
|
this.serializeAddon = new SerializeAddon();
|
|
this.terminal.loadAddon(this.fitAddon);
|
|
this.terminal.loadAddon(this.serializeAddon);
|
|
this.terminal.loadAddon(
|
|
new WebLinksAddon((e, uri) => {
|
|
e.preventDefault();
|
|
switch (PLATFORM) {
|
|
case "darwin":
|
|
if (e.metaKey) {
|
|
fireAndForget(() => openLink(uri));
|
|
}
|
|
break;
|
|
default:
|
|
if (e.ctrlKey) {
|
|
fireAndForget(() => openLink(uri));
|
|
}
|
|
break;
|
|
}
|
|
})
|
|
);
|
|
if (WebGLSupported && waveOptions.useWebGl) {
|
|
const webglAddon = new WebglAddon();
|
|
webglAddon.onContextLoss(() => {
|
|
webglAddon.dispose();
|
|
});
|
|
this.terminal.loadAddon(webglAddon);
|
|
if (!loggedWebGL) {
|
|
console.log("loaded webgl!");
|
|
loggedWebGL = true;
|
|
}
|
|
}
|
|
this.terminal.parser.registerOscHandler(7, (data: string) => {
|
|
if (!this.loaded) {
|
|
return false;
|
|
}
|
|
if (data == null || data.length == 0) {
|
|
return false;
|
|
}
|
|
if (data.startsWith("file://")) {
|
|
data = data.substring(7);
|
|
const nextSlashIdx = data.indexOf("/");
|
|
if (nextSlashIdx == -1) {
|
|
return false;
|
|
}
|
|
data = data.substring(nextSlashIdx);
|
|
}
|
|
setTimeout(() => {
|
|
services.ObjectService.UpdateObjectMeta(WOS.makeORef("block", this.blockId), { "cmd:cwd": data });
|
|
}, 0);
|
|
return true;
|
|
});
|
|
this.terminal.attachCustomKeyEventHandler(waveOptions.keydownHandler);
|
|
this.connectElem = connectElem;
|
|
this.mainFileSubject = null;
|
|
this.heldData = [];
|
|
this.handleResize_debounced = debounce(50, this.handleResize.bind(this));
|
|
this.terminal.open(this.connectElem);
|
|
this.handleResize();
|
|
this.isRunning = true;
|
|
}
|
|
|
|
async initTerminal() {
|
|
this.terminal.onData(this.handleTermData.bind(this));
|
|
this.mainFileSubject = getFileSubject(this.blockId, TermFileName);
|
|
this.mainFileSubject.subscribe(this.handleNewFileSubjectData.bind(this));
|
|
try {
|
|
await this.loadInitialTerminalData();
|
|
} finally {
|
|
this.loaded = true;
|
|
}
|
|
this.runProcessIdleTimeout();
|
|
}
|
|
|
|
setIsRunning(isRunning: boolean) {
|
|
this.isRunning = isRunning;
|
|
}
|
|
|
|
dispose() {
|
|
this.terminal.dispose();
|
|
this.mainFileSubject.release();
|
|
}
|
|
|
|
handleTermData(data: string) {
|
|
const b64data = util.stringToBase64(data);
|
|
RpcApi.ControllerInputCommand(WindowRpcClient, { blockid: this.blockId, inputdata64: b64data });
|
|
}
|
|
|
|
addFocusListener(focusFn: () => void) {
|
|
this.terminal.textarea.addEventListener("focus", focusFn);
|
|
}
|
|
|
|
handleNewFileSubjectData(msg: WSFileEventData) {
|
|
if (msg.fileop == "truncate") {
|
|
this.terminal.clear();
|
|
this.heldData = [];
|
|
} else if (msg.fileop == "append") {
|
|
const decodedData = base64ToArray(msg.data64);
|
|
if (this.loaded) {
|
|
this.doTerminalWrite(decodedData, null);
|
|
} else {
|
|
this.heldData.push(decodedData);
|
|
}
|
|
} else {
|
|
console.log("bad fileop for terminal", msg);
|
|
return;
|
|
}
|
|
}
|
|
|
|
doTerminalWrite(data: string | Uint8Array, setPtyOffset?: number): Promise<void> {
|
|
let resolve: () => void = null;
|
|
let prtn = new Promise<void>((presolve, _) => {
|
|
resolve = presolve;
|
|
});
|
|
this.terminal.write(data, () => {
|
|
if (setPtyOffset != null) {
|
|
this.ptyOffset = setPtyOffset;
|
|
} else {
|
|
this.ptyOffset += data.length;
|
|
this.dataBytesProcessed += data.length;
|
|
}
|
|
resolve();
|
|
});
|
|
return prtn;
|
|
}
|
|
|
|
async loadInitialTerminalData(): Promise<void> {
|
|
let startTs = Date.now();
|
|
const { data: cacheData, fileInfo: cacheFile } = await fetchWaveFile(this.blockId, TermCacheFileName);
|
|
let ptyOffset = 0;
|
|
if (cacheFile != null) {
|
|
ptyOffset = cacheFile.meta["ptyoffset"] ?? 0;
|
|
if (cacheData.byteLength > 0) {
|
|
this.doTerminalWrite(cacheData, ptyOffset);
|
|
}
|
|
}
|
|
const { data: mainData, fileInfo: mainFile } = await fetchWaveFile(this.blockId, TermFileName, ptyOffset);
|
|
console.log(
|
|
`terminal loaded cachefile:${cacheData?.byteLength ?? 0} main:${mainData?.byteLength ?? 0} bytes, ${Date.now() - startTs}ms`
|
|
);
|
|
if (mainFile != null) {
|
|
await this.doTerminalWrite(mainData, null);
|
|
}
|
|
}
|
|
|
|
async resyncController(reason: string) {
|
|
dlog("resync controller", this.blockId, reason);
|
|
const tabId = globalStore.get(atoms.activeTabId);
|
|
const rtOpts: RuntimeOpts = { termsize: { rows: this.terminal.rows, cols: this.terminal.cols } };
|
|
try {
|
|
await RpcApi.ControllerResyncCommand(WindowRpcClient, {
|
|
tabid: tabId,
|
|
blockid: this.blockId,
|
|
rtopts: rtOpts,
|
|
});
|
|
} catch (e) {
|
|
console.log(`error controller resync (${reason})`, this.blockId, e);
|
|
}
|
|
}
|
|
|
|
handleResize() {
|
|
const oldRows = this.terminal.rows;
|
|
const oldCols = this.terminal.cols;
|
|
this.fitAddon.fit();
|
|
if (oldRows !== this.terminal.rows || oldCols !== this.terminal.cols) {
|
|
const termSize: TermSize = { rows: this.terminal.rows, cols: this.terminal.cols };
|
|
const wsCommand: SetBlockTermSizeWSCommand = {
|
|
wscommand: "setblocktermsize",
|
|
blockid: this.blockId,
|
|
termsize: termSize,
|
|
};
|
|
sendWSCommand(wsCommand);
|
|
}
|
|
dlog("resize", `${this.terminal.rows}x${this.terminal.cols}`, `${oldRows}x${oldCols}`, this.hasResized);
|
|
if (!this.hasResized) {
|
|
this.hasResized = true;
|
|
this.resyncController("initial resize");
|
|
}
|
|
}
|
|
|
|
processAndCacheData() {
|
|
if (this.dataBytesProcessed < MinDataProcessedForCache) {
|
|
return;
|
|
}
|
|
const serializedOutput = this.serializeAddon.serialize();
|
|
console.log("idle timeout term", this.dataBytesProcessed, serializedOutput.length);
|
|
services.BlockService.SaveTerminalState(this.blockId, serializedOutput, "full", this.ptyOffset);
|
|
this.dataBytesProcessed = 0;
|
|
}
|
|
|
|
runProcessIdleTimeout() {
|
|
setTimeout(() => {
|
|
window.requestIdleCallback(() => {
|
|
this.processAndCacheData();
|
|
this.runProcessIdleTimeout();
|
|
});
|
|
}, 5000);
|
|
}
|
|
}
|