mirror of
https://github.com/wavetermdev/waveterm
synced 2026-05-24 09:18:27 +00:00
fallback to canvas renderer if webgl is not available, debug toggle for testing (#3081)
This commit is contained in:
parent
134b38807f
commit
c126306da1
8 changed files with 142 additions and 20 deletions
|
|
@ -41,7 +41,7 @@ import * as jotai from "jotai";
|
|||
import * as React from "react";
|
||||
import { getBlockingCommand } from "./shellblocking";
|
||||
import { computeTheme, DefaultTermTheme } from "./termutil";
|
||||
import { TermWrap } from "./termwrap";
|
||||
import { TermWrap, WebGLSupported } from "./termwrap";
|
||||
|
||||
export class TermViewModel implements ViewModel {
|
||||
viewType: string;
|
||||
|
|
@ -155,7 +155,7 @@ export class TermViewModel implements ViewModel {
|
|||
if (isCmd) {
|
||||
const blockMeta = get(this.blockAtom)?.meta;
|
||||
let cmdText = blockMeta?.["cmd"];
|
||||
let cmdArgs = blockMeta?.["cmd:args"];
|
||||
const cmdArgs = blockMeta?.["cmd:args"];
|
||||
if (cmdArgs != null && Array.isArray(cmdArgs) && cmdArgs.length > 0) {
|
||||
cmdText += " " + cmdArgs.join(" ");
|
||||
}
|
||||
|
|
@ -242,7 +242,7 @@ export class TermViewModel implements ViewModel {
|
|||
});
|
||||
this.termTransparencyAtom = useBlockAtom(blockId, "termtransparencyatom", () => {
|
||||
return jotai.atom<number>((get) => {
|
||||
let value = get(getOverrideConfigAtom(this.blockId, "term:transparency")) ?? 0.5;
|
||||
const value = get(getOverrideConfigAtom(this.blockId, "term:transparency")) ?? 0.5;
|
||||
return boundNumber(value, 0, 1);
|
||||
});
|
||||
});
|
||||
|
|
@ -293,6 +293,13 @@ export class TermViewModel implements ViewModel {
|
|||
}
|
||||
}
|
||||
|
||||
if (get(getSettingsKeyAtom("debug:webglstatus"))) {
|
||||
const webglButton = this.getWebGlIconButton(get);
|
||||
if (webglButton) {
|
||||
rtn.push(webglButton);
|
||||
}
|
||||
}
|
||||
|
||||
if (blockData?.meta?.["controller"] != "cmd" && shellProcStatus != "done") {
|
||||
return rtn;
|
||||
}
|
||||
|
|
@ -438,6 +445,38 @@ export class TermViewModel implements ViewModel {
|
|||
return null;
|
||||
}
|
||||
|
||||
getWebGlIconButton(get: jotai.Getter): IconButtonDecl | null {
|
||||
if (!WebGLSupported) {
|
||||
return {
|
||||
elemtype: "iconbutton",
|
||||
icon: "microchip",
|
||||
iconColor: "var(--error-color)",
|
||||
title: "WebGL not supported",
|
||||
noAction: true,
|
||||
};
|
||||
}
|
||||
if (!this.termRef.current?.webglEnabledAtom) {
|
||||
return null;
|
||||
}
|
||||
const webglEnabled = get(this.termRef.current.webglEnabledAtom);
|
||||
if (webglEnabled) {
|
||||
return {
|
||||
elemtype: "iconbutton",
|
||||
icon: "microchip",
|
||||
iconColor: "var(--success-color)",
|
||||
title: "WebGL enabled (click to disable)",
|
||||
click: () => this.toggleWebGl(),
|
||||
};
|
||||
}
|
||||
return {
|
||||
elemtype: "iconbutton",
|
||||
icon: "microchip",
|
||||
iconColor: "var(--secondary-text-color)",
|
||||
title: "WebGL disabled (click to enable)",
|
||||
click: () => this.toggleWebGl(),
|
||||
};
|
||||
}
|
||||
|
||||
get viewComponent(): ViewComponent {
|
||||
return TerminalView as ViewComponent;
|
||||
}
|
||||
|
|
@ -478,6 +517,22 @@ export class TermViewModel implements ViewModel {
|
|||
});
|
||||
}
|
||||
|
||||
getTermRenderer(): "webgl" | "canvas" {
|
||||
return this.termRef.current?.getTermRenderer() ?? "canvas";
|
||||
}
|
||||
|
||||
isWebGlEnabled(): boolean {
|
||||
return this.termRef.current?.isWebGlEnabled() ?? false;
|
||||
}
|
||||
|
||||
toggleWebGl() {
|
||||
if (!this.termRef.current) {
|
||||
return;
|
||||
}
|
||||
const renderer = this.termRef.current.getTermRenderer() === "webgl" ? "canvas" : "webgl";
|
||||
this.termRef.current.setTermRenderer(renderer);
|
||||
}
|
||||
|
||||
triggerRestartAtom() {
|
||||
globalStore.set(this.isRestarting, true);
|
||||
setTimeout(() => {
|
||||
|
|
@ -544,7 +599,7 @@ export class TermViewModel implements ViewModel {
|
|||
console.log("search is open, not giving focus");
|
||||
return true;
|
||||
}
|
||||
let termMode = globalStore.get(this.termMode);
|
||||
const termMode = globalStore.get(this.termMode);
|
||||
if (termMode == "term") {
|
||||
if (this.termRef?.current?.terminal) {
|
||||
this.termRef.current.terminal.focus();
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
import * as services from "@/store/services";
|
||||
import { PLATFORM, PlatformMacOS } from "@/util/platformutil";
|
||||
import { base64ToArray, fireAndForget } from "@/util/util";
|
||||
import { CanvasAddon } from "@xterm/addon-canvas";
|
||||
import { SearchAddon } from "@xterm/addon-search";
|
||||
import { SerializeAddon } from "@xterm/addon-serialize";
|
||||
import { WebLinksAddon } from "@xterm/addon-web-links";
|
||||
|
|
@ -49,14 +50,14 @@ const MaxRepaintTransactionMs = 2000;
|
|||
function detectWebGLSupport(): boolean {
|
||||
try {
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("webgl");
|
||||
const ctx = canvas.getContext("webgl2");
|
||||
return !!ctx;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const WebGLSupported = detectWebGLSupport();
|
||||
export const WebGLSupported = detectWebGLSupport();
|
||||
let loggedWebGL = false;
|
||||
|
||||
type TermWrapOptions = {
|
||||
|
|
@ -84,7 +85,11 @@ export class TermWrap {
|
|||
multiInputCallback: (data: string) => void;
|
||||
sendDataHandler: (data: string) => void;
|
||||
onSearchResultsDidChange?: (result: { resultIndex: number; resultCount: number }) => void;
|
||||
private toDispose: TermTypes.IDisposable[] = [];
|
||||
toDispose: TermTypes.IDisposable[] = [];
|
||||
webglAddon: WebglAddon | null = null;
|
||||
canvasAddon: CanvasAddon | null = null;
|
||||
webglContextLossDisposable: TermTypes.IDisposable | null = null;
|
||||
webglEnabledAtom: jotai.PrimitiveAtom<boolean>;
|
||||
pasteActive: boolean = false;
|
||||
lastUpdated: number;
|
||||
promptMarkers: TermTypes.IMarker[] = [];
|
||||
|
|
@ -142,6 +147,7 @@ export class TermWrap {
|
|||
this.promptMarkers = [];
|
||||
this.shellIntegrationStatusAtom = jotai.atom(null) as jotai.PrimitiveAtom<ShellIntegrationStatus | null>;
|
||||
this.lastCommandAtom = jotai.atom(null) as jotai.PrimitiveAtom<string | null>;
|
||||
this.webglEnabledAtom = jotai.atom(false) as jotai.PrimitiveAtom<boolean>;
|
||||
this.terminal = new Terminal(options);
|
||||
this.fitAddon = new FitAddon();
|
||||
this.fitAddon.scrollbarWidth = 6; // this needs to match scrollbar width in term.scss
|
||||
|
|
@ -179,19 +185,7 @@ export class TermWrap {
|
|||
}
|
||||
)
|
||||
);
|
||||
if (WebGLSupported && waveOptions.useWebGl) {
|
||||
const webglAddon = new WebglAddon();
|
||||
this.toDispose.push(
|
||||
webglAddon.onContextLoss(() => {
|
||||
webglAddon.dispose();
|
||||
})
|
||||
);
|
||||
this.terminal.loadAddon(webglAddon);
|
||||
if (!loggedWebGL) {
|
||||
console.log("loaded webgl!");
|
||||
loggedWebGL = true;
|
||||
}
|
||||
}
|
||||
this.setTermRenderer(WebGLSupported && waveOptions.useWebGl ? "webgl" : "canvas");
|
||||
// Register OSC handlers
|
||||
this.terminal.parser.registerOscHandler(7, (data: string) => {
|
||||
return handleOsc7Command(data, this.blockId, this.loaded);
|
||||
|
|
@ -307,6 +301,60 @@ export class TermWrap {
|
|||
this.terminal.options.cursorBlink = cursorBlink ?? false;
|
||||
}
|
||||
|
||||
setTermRenderer(renderer: "webgl" | "canvas") {
|
||||
if (renderer === "webgl") {
|
||||
if (this.webglAddon != null) {
|
||||
return;
|
||||
}
|
||||
if (!WebGLSupported) {
|
||||
renderer = "canvas";
|
||||
if (this.canvasAddon != null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (this.canvasAddon != null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (this.webglAddon != null) {
|
||||
this.webglContextLossDisposable?.dispose();
|
||||
this.webglContextLossDisposable = null;
|
||||
this.webglAddon.dispose();
|
||||
this.webglAddon = null;
|
||||
globalStore.set(this.webglEnabledAtom, false);
|
||||
}
|
||||
if (this.canvasAddon != null) {
|
||||
this.canvasAddon.dispose();
|
||||
this.canvasAddon = null;
|
||||
}
|
||||
if (renderer === "webgl") {
|
||||
const addon = new WebglAddon();
|
||||
this.webglContextLossDisposable = addon.onContextLoss(() => {
|
||||
this.setTermRenderer("canvas");
|
||||
});
|
||||
this.terminal.loadAddon(addon);
|
||||
this.webglAddon = addon;
|
||||
globalStore.set(this.webglEnabledAtom, true);
|
||||
if (!loggedWebGL) {
|
||||
console.log("loaded webgl!");
|
||||
loggedWebGL = true;
|
||||
}
|
||||
} else {
|
||||
const addon = new CanvasAddon();
|
||||
this.terminal.loadAddon(addon);
|
||||
this.canvasAddon = addon;
|
||||
}
|
||||
}
|
||||
|
||||
getTermRenderer(): "webgl" | "canvas" {
|
||||
return this.webglAddon != null ? "webgl" : "canvas";
|
||||
}
|
||||
|
||||
isWebGlEnabled(): boolean {
|
||||
return this.webglAddon != null;
|
||||
}
|
||||
|
||||
resetCompositionState() {
|
||||
this.isComposing = false;
|
||||
this.composingData = "";
|
||||
|
|
@ -422,6 +470,8 @@ export class TermWrap {
|
|||
} catch (_) {}
|
||||
});
|
||||
this.promptMarkers = [];
|
||||
this.webglContextLossDisposable?.dispose();
|
||||
this.webglContextLossDisposable = null;
|
||||
this.terminal.dispose();
|
||||
this.toDispose.forEach((d) => {
|
||||
try {
|
||||
|
|
|
|||
1
frontend/types/gotypes.d.ts
vendored
1
frontend/types/gotypes.d.ts
vendored
|
|
@ -1395,6 +1395,7 @@ declare global {
|
|||
"debug:*"?: boolean;
|
||||
"debug:pprofport"?: number;
|
||||
"debug:pprofmemprofilerate"?: number;
|
||||
"debug:webglstatus"?: boolean;
|
||||
"tsunami:*"?: boolean;
|
||||
"tsunami:scaffoldpath"?: string;
|
||||
"tsunami:sdkreplacepath"?: string;
|
||||
|
|
|
|||
10
package-lock.json
generated
10
package-lock.json
generated
|
|
@ -22,6 +22,7 @@
|
|||
"@table-nav/react": "^0.0.7",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tanstack/react-virtual": "^3.13.19",
|
||||
"@xterm/addon-canvas": "^0.7.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-search": "^0.15.0",
|
||||
"@xterm/addon-serialize": "^0.13.0",
|
||||
|
|
@ -10664,6 +10665,15 @@
|
|||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@xterm/addon-canvas": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/addon-canvas/-/addon-canvas-0.7.0.tgz",
|
||||
"integrity": "sha512-LF5LYcfvefJuJ7QotNRdRSPc9YASAVDeoT5uyXS/nZshZXjYplGXRECBGiznwvhNL2I8bq1Lf5MzRwstsYQ2Iw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@xterm/xterm": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@xterm/addon-fit": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz",
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@
|
|||
"@table-nav/react": "^0.0.7",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tanstack/react-virtual": "^3.13.19",
|
||||
"@xterm/addon-canvas": "^0.7.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-search": "^0.15.0",
|
||||
"@xterm/addon-serialize": "^0.13.0",
|
||||
|
|
|
|||
|
|
@ -121,6 +121,7 @@ const (
|
|||
ConfigKey_DebugClear = "debug:*"
|
||||
ConfigKey_DebugPprofPort = "debug:pprofport"
|
||||
ConfigKey_DebugPprofMemProfileRate = "debug:pprofmemprofilerate"
|
||||
ConfigKey_DebugWebGlStatus = "debug:webglstatus"
|
||||
|
||||
ConfigKey_TsunamiClear = "tsunami:*"
|
||||
ConfigKey_TsunamiScaffoldPath = "tsunami:scaffoldpath"
|
||||
|
|
|
|||
|
|
@ -172,6 +172,7 @@ type SettingsType struct {
|
|||
DebugClear bool `json:"debug:*,omitempty"`
|
||||
DebugPprofPort *int `json:"debug:pprofport,omitempty"`
|
||||
DebugPprofMemProfileRate *int `json:"debug:pprofmemprofilerate,omitempty"`
|
||||
DebugWebGlStatus bool `json:"debug:webglstatus,omitempty"`
|
||||
|
||||
TsunamiClear bool `json:"tsunami:*,omitempty"`
|
||||
TsunamiScaffoldPath string `json:"tsunami:scaffoldpath,omitempty"`
|
||||
|
|
|
|||
|
|
@ -325,6 +325,9 @@
|
|||
"debug:pprofmemprofilerate": {
|
||||
"type": "integer"
|
||||
},
|
||||
"debug:webglstatus": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"tsunami:*": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue