mirror of
https://github.com/wavetermdev/backup.git
synced 2026-08-05 13:57:07 -07:00
working on wave OSC escapes, modes for the terminal (#46)
This commit is contained in:
@@ -64,10 +64,11 @@ const atoms = {
|
||||
|
||||
type SubjectWithRef<T> = rxjs.Subject<T> & { refCount: number; release: () => void };
|
||||
|
||||
const orefSubjects = new Map<string, SubjectWithRef<any>>();
|
||||
// key is "eventType" or "eventType|oref"
|
||||
const eventSubjects = new Map<string, SubjectWithRef<WSEventType>>();
|
||||
|
||||
function getORefSubject(oref: string): SubjectWithRef<any> {
|
||||
let subject = orefSubjects.get(oref);
|
||||
function getSubjectInternal(subjectKey: string): SubjectWithRef<WSEventType> {
|
||||
let subject = eventSubjects.get(subjectKey);
|
||||
if (subject == null) {
|
||||
subject = new rxjs.Subject<any>() as any;
|
||||
subject.refCount = 0;
|
||||
@@ -75,15 +76,23 @@ function getORefSubject(oref: string): SubjectWithRef<any> {
|
||||
subject.refCount--;
|
||||
if (subject.refCount === 0) {
|
||||
subject.complete();
|
||||
orefSubjects.delete(oref);
|
||||
eventSubjects.delete(subjectKey);
|
||||
}
|
||||
};
|
||||
orefSubjects.set(oref, subject);
|
||||
eventSubjects.set(subjectKey, subject);
|
||||
}
|
||||
subject.refCount++;
|
||||
return subject;
|
||||
}
|
||||
|
||||
function getEventSubject(eventType: string): SubjectWithRef<WSEventType> {
|
||||
return getSubjectInternal(eventType);
|
||||
}
|
||||
|
||||
function getEventORefSubject(eventType: string, oref: string): SubjectWithRef<WSEventType> {
|
||||
return getSubjectInternal(eventType + "|" + oref);
|
||||
}
|
||||
|
||||
const blockCache = new Map<string, Map<string, any>>();
|
||||
|
||||
function useBlockCache<T>(blockId: string, name: string, makeFn: () => T): T {
|
||||
@@ -129,16 +138,20 @@ function getBackendWSHostPort(): string {
|
||||
let globalWS: WSControl = null;
|
||||
|
||||
function handleWSEventMessage(msg: WSEventType) {
|
||||
if (msg.oref == null) {
|
||||
if (msg.eventtype == null) {
|
||||
console.log("unsupported event", msg);
|
||||
return;
|
||||
}
|
||||
// we send to two subjects just eventType and eventType|oref
|
||||
// we don't use getORefSubject here because we don't want to create a new subject
|
||||
const subject = orefSubjects.get(msg.oref);
|
||||
if (subject == null) {
|
||||
return;
|
||||
const eventSubject = eventSubjects.get(msg.eventtype);
|
||||
if (eventSubject != null) {
|
||||
eventSubject.next(msg);
|
||||
}
|
||||
const eventOrefSubject = eventSubjects.get(msg.eventtype + "|" + msg.oref);
|
||||
if (eventOrefSubject != null) {
|
||||
eventOrefSubject.next(msg);
|
||||
}
|
||||
subject.next(msg.data);
|
||||
}
|
||||
|
||||
function handleWSMessage(msg: any) {
|
||||
@@ -161,11 +174,20 @@ function sendWSCommand(command: WSCommandType) {
|
||||
globalWS.pushMessage(command);
|
||||
}
|
||||
|
||||
// more code that could be moved into an init
|
||||
// here we want to set up a "waveobj:update" handler
|
||||
const waveobjUpdateSubject = getEventSubject("waveobj:update");
|
||||
waveobjUpdateSubject.subscribe((msg: WSEventType) => {
|
||||
const update: WaveObjUpdate = msg.data;
|
||||
WOS.updateWaveObject(update);
|
||||
});
|
||||
|
||||
export {
|
||||
WOS,
|
||||
atoms,
|
||||
getBackendHostPort,
|
||||
getORefSubject,
|
||||
getEventORefSubject,
|
||||
getEventSubject,
|
||||
globalStore,
|
||||
globalWS,
|
||||
initWS,
|
||||
|
||||
@@ -18,6 +18,9 @@ const MaxFileSize = 1024 * 1024 * 10; // 10MB
|
||||
|
||||
function DirNav({ cwdAtom }: { cwdAtom: jotai.WritableAtom<string, [string], void> }) {
|
||||
const [cwd, setCwd] = jotai.useAtom(cwdAtom);
|
||||
if (cwd == null || cwd == "") {
|
||||
return null;
|
||||
}
|
||||
let splitNav = [cwd];
|
||||
let remaining = cwd;
|
||||
|
||||
@@ -170,7 +173,9 @@ function PreviewView({ blockId }: { blockId: string }) {
|
||||
) {
|
||||
specializedView = <StreamingPreview fileInfo={fileInfo} />;
|
||||
} else if (fileInfo == null) {
|
||||
specializedView = <CenteredDiv>File Not Found</CenteredDiv>;
|
||||
specializedView = (
|
||||
<CenteredDiv>File Not Found{util.isBlank(fileName) ? null : JSON.stringify(fileName)}</CenteredDiv>
|
||||
);
|
||||
} else if (fileInfo.size > MaxFileSize) {
|
||||
specializedView = <CenteredDiv>File Too Large to Preview</CenteredDiv>;
|
||||
} else if (mimeType === "text/markdown") {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { WOS, getBackendHostPort, getORefSubject, sendWSCommand } from "@/store/global";
|
||||
import { WOS, getBackendHostPort, getEventORefSubject, sendWSCommand } from "@/store/global";
|
||||
import * as services from "@/store/services";
|
||||
import { base64ToArray } from "@/util/util";
|
||||
import { FitAddon } from "@xterm/addon-fit";
|
||||
import type { ITheme } from "@xterm/xterm";
|
||||
import { Terminal } from "@xterm/xterm";
|
||||
import clsx from "clsx";
|
||||
import * as React from "react";
|
||||
|
||||
import { debounce } from "throttle-debounce";
|
||||
@@ -67,6 +68,8 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
|
||||
const connectElemRef = React.useRef<HTMLDivElement>(null);
|
||||
const termRef = React.useRef<Terminal>(null);
|
||||
const initialLoadRef = React.useRef<InitialLoadDataType>({ loaded: false, heldData: [] });
|
||||
const htmlElemFocusRef = React.useRef<HTMLInputElement>(null);
|
||||
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
||||
React.useEffect(() => {
|
||||
console.log("terminal created");
|
||||
const newTerm = new Terminal({
|
||||
@@ -82,10 +85,6 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
|
||||
newTerm.loadAddon(newFitAddon);
|
||||
newTerm.open(connectElemRef.current);
|
||||
newFitAddon.fit();
|
||||
// services.BlockService.SendCommand(blockId, {
|
||||
// command: "controller:input",
|
||||
// termsize: { rows: newTerm.rows, cols: newTerm.cols },
|
||||
// });
|
||||
sendWSCommand({
|
||||
wscommand: "setblocktermsize",
|
||||
blockid: blockId,
|
||||
@@ -98,9 +97,10 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
|
||||
});
|
||||
|
||||
// block subject
|
||||
const blockSubject = getORefSubject(WOS.makeORef("block", blockId));
|
||||
blockSubject.subscribe((data) => {
|
||||
const blockSubject = getEventORefSubject("block:ptydata", WOS.makeORef("block", blockId));
|
||||
blockSubject.subscribe((msg: WSEventType) => {
|
||||
// base64 decode
|
||||
const data = msg.data;
|
||||
const decodedData = base64ToArray(data.ptydata);
|
||||
if (initialLoadRef.current.loaded) {
|
||||
newTerm.write(decodedData);
|
||||
@@ -150,9 +150,39 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.code === "Escape" && event.metaKey) {
|
||||
// reset term:mode
|
||||
const metaCmd: BlockSetMetaCommand = { command: "setmeta", meta: { "term:mode": null } };
|
||||
services.BlockService.SendCommand(blockId, metaCmd);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
let termMode = blockData?.meta?.["term:mode"] ?? "term";
|
||||
if (termMode != "term" && termMode != "html") {
|
||||
termMode = "term";
|
||||
}
|
||||
return (
|
||||
<div className="view-term">
|
||||
<div className={clsx("view-term", "term-mode-" + termMode)}>
|
||||
<div key="conntectElem" className="term-connectelem" ref={connectElemRef}></div>
|
||||
<div
|
||||
key="htmlElem"
|
||||
className="term-htmlelem"
|
||||
onClick={() => {
|
||||
if (htmlElemFocusRef.current != null) {
|
||||
htmlElemFocusRef.current.focus();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div key="htmlElemFocus" className="term-htmlelem-focus">
|
||||
<input type="text" ref={htmlElemFocusRef} onKeyDown={handleKeyDown} />
|
||||
</div>
|
||||
<div key="htmlElemContent" className="term-htmlelem-content">
|
||||
HTML MODE
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,6 +7,12 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
border-left: 4px solid transparent;
|
||||
padding-left: 4px;
|
||||
|
||||
&:focus-within {
|
||||
border-left: 4px solid var(--accent-color);
|
||||
}
|
||||
|
||||
.term-header {
|
||||
display: flex;
|
||||
@@ -25,6 +31,53 @@
|
||||
overflow: hidden;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.term-htmlelem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
|
||||
.term-htmlelem-focus {
|
||||
height: 0;
|
||||
width: 0;
|
||||
input {
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.term-htmlelem-content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
&.term-mode-term {
|
||||
.term-connectelem {
|
||||
display: flex;
|
||||
}
|
||||
.term-htmlelem {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.term-mode-html {
|
||||
.term-connectelem {
|
||||
display: none;
|
||||
}
|
||||
.term-htmlelem {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.view-codeedit {
|
||||
|
||||
+11
-1
@@ -3,7 +3,17 @@
|
||||
|
||||
import base64 from "base64-js";
|
||||
|
||||
function isBlank(str: string): boolean {
|
||||
return str == null || str == "";
|
||||
}
|
||||
|
||||
function base64ToString(b64: string): string {
|
||||
if (b64 == null) {
|
||||
return null;
|
||||
}
|
||||
if (b64 == "") {
|
||||
return "";
|
||||
}
|
||||
const stringBytes = base64.toByteArray(b64);
|
||||
return new TextDecoder().decode(stringBytes);
|
||||
}
|
||||
@@ -22,4 +32,4 @@ function base64ToArray(b64: string): Uint8Array {
|
||||
return rtnArr;
|
||||
}
|
||||
|
||||
export { base64ToArray, base64ToString, stringToBase64 };
|
||||
export { base64ToArray, base64ToString, isBlank, stringToBase64 };
|
||||
|
||||
Reference in New Issue
Block a user