round trip a message to the backend that updates the terminal fe component

This commit is contained in:
sawka
2024-05-14 16:53:03 -07:00
parent 50ccd66d49
commit 35c6b232fc
16 changed files with 1135 additions and 27 deletions
+135
View File
@@ -0,0 +1,135 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
.wave-button {
background: none;
border: none;
cursor: pointer;
outline: inherit;
display: flex;
padding: 6px 16px;
align-items: center;
gap: 4px;
border-radius: 6px;
height: auto;
line-height: 1.5;
white-space: nowrap;
user-select: none;
color: var(--main-text-color);
background: var(--accent-color);
i {
fill: var(--main-text-color);
}
&.primary {
color: var(--main-text-color);
background: var(--accent-color);
i {
fill: var(--main-text-color);
}
}
&.primary.danger {
background: var(--error-color);
}
&.primary.outlined {
background: none;
border: 1px solid var(--accent-color);
i {
fill: var(--accent-color);
}
}
&.primary.greyoutlined {
background: none;
border: 1px solid var(--secondary-text-color);
i {
fill: var(--secondary-text-color);
}
}
&.primary.outlined,
&.primary.greyoutlined {
&.hover-danger:hover {
color: var(--main-text-color);
border: 1px solid var(--error-color);
background: var(--error-color);
}
}
&.primary.outlined.danger {
background: none;
border: 1px solid var(--error-color);
i {
fill: var(--error-color);
}
}
&.greytext {
color: var(--secondary-text-color);
}
&.primary.ghost {
background: none;
i {
fill: var(--accent-color);
}
}
&.primary.ghost.danger {
background: none;
i {
fill: var(--app-error-color);
}
}
&.secondary {
color: var(--main-text-color);
background: var(--highlight-bg-color);
i {
fill: var(--main-text-color);
}
}
&.secondary.outlined {
background: none;
border: 1px solid var(--main-text-color);
}
&.secondary.outlined.danger {
background: none;
border: 1px solid var(--error-color);
}
&.secondary.ghost {
background: none;
}
&.secondary.danger {
color: var(--error-color);
}
&.small {
padding: 4px 8px;
font-size: 12px;
border-radius: 3.6px;
}
&.term-inline {
padding: 2px 8px;
border-radius: 3px;
}
&.disabled {
opacity: 0.5;
}
&.link-button {
cursor: pointer;
}
}
+55
View File
@@ -0,0 +1,55 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as React from "react";
import { clsx } from "clsx";
import "./button.less";
interface ButtonProps {
children: React.ReactNode;
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
disabled?: boolean;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
style?: React.CSSProperties;
autoFocus?: boolean;
className?: string;
termInline?: boolean;
title?: string;
}
class Button extends React.Component<ButtonProps> {
static defaultProps = {
style: {},
className: "primary",
};
handleClick(e) {
if (this.props.onClick && !this.props.disabled) {
this.props.onClick(e);
}
}
render() {
const { leftIcon, rightIcon, children, disabled, style, autoFocus, termInline, className, title } = this.props;
return (
<button
className={clsx("wave-button", { disabled }, { "term-inline": termInline }, className)}
onClick={this.handleClick.bind(this)}
disabled={disabled}
style={style}
autoFocus={autoFocus}
title={title}
>
{leftIcon && <span className="icon-left">{leftIcon}</span>}
{children}
{rightIcon && <span className="icon-right">{rightIcon}</span>}
</button>
);
}
}
export { Button };
export type { ButtonProps };
+8 -5
View File
@@ -1,5 +1,8 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
.markdown {
color: var(--main-color);
color: var(--main-text-color);
font-family: var(--markdown-font);
font-size: 14px;
overflow-wrap: break-word;
@@ -9,13 +12,13 @@
}
.title {
color: var(--main-color);
color: var(--main-text-color);
margin-top: 16px;
margin-bottom: 8px;
}
strong {
color: var(--main-color);
color: var(--main-text-color);
}
a {
@@ -24,7 +27,7 @@
table {
tr th {
color: var(--main-color);
color: var(--main-text-color);
}
}
@@ -61,7 +64,7 @@
code {
font: var(--fixed-font);
color: var(--main-color);
color: var(--main-text-color);
border-radius: 4px;
background-color: var(--panel-bg-color);
padding: 0.15em 0.5em;
+40 -1
View File
@@ -4,6 +4,9 @@
import * as jotai from "jotai";
import { atomFamily } from "jotai/utils";
import { v4 as uuidv4 } from "uuid";
import * as rxjs from "rxjs";
import type { WailsEvent } from "@wailsio/runtime/types/events";
import { Events } from "@wailsio/runtime";
const globalStore = jotai.createStore();
@@ -42,4 +45,40 @@ const atoms = {
blockAtomFamily,
};
export { globalStore, atoms };
type SubjectWithRef<T> = rxjs.Subject<T> & { refCount: number; release: () => void };
const blockSubjects = new Map<string, SubjectWithRef<any>>();
function getBlockSubject(blockId: string): SubjectWithRef<any> {
let subject = blockSubjects.get(blockId);
if (subject == null) {
subject = new rxjs.Subject<any>() as any;
subject.refCount = 0;
subject.release = () => {
subject.refCount--;
if (subject.refCount === 0) {
subject.complete();
blockSubjects.delete(blockId);
}
};
blockSubjects.set(blockId, subject);
}
subject.refCount++;
return subject;
}
Events.On("block:ptydata", (event: any) => {
const data = event?.data;
if (data?.blockid == null) {
console.log("block:ptydata with null blockid");
return;
}
// we don't use getBlockSubject here because we don't want to create a new subject
const subject = blockSubjects.get(data.blockid);
if (subject == null) {
return;
}
subject.next(data);
});
export { globalStore, atoms, getBlockSubject };
+41 -2
View File
@@ -6,6 +6,10 @@ import * as jotai from "jotai";
import { Terminal } from "@xterm/xterm";
import type { ITheme } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
import { Button } from "@/element/button";
import * as BlockService from "@/bindings/pkg/service/blockservice/BlockService";
import { getBlockSubject } from "@/store/global";
import { base64ToArray } from "@/util/util";
import "./view.less";
import "/public/xterm.css";
@@ -40,6 +44,7 @@ function getThemeFromCSSVars(el: Element): ITheme {
const TerminalView = ({ blockId }: { blockId: string }) => {
const connectElemRef = React.useRef<HTMLDivElement>(null);
const [term, setTerm] = React.useState<Terminal>(null);
React.useEffect(() => {
if (!connectElemRef.current) {
@@ -57,13 +62,47 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
term.loadAddon(fitAddon);
term.open(connectElemRef.current);
fitAddon.fit();
term.write("Hello, world!");
term.write("Hello, world!\r\n");
setTerm(term);
return () => {
term.dispose();
};
}, [connectElemRef.current]);
React.useEffect(() => {
if (!term) {
return;
}
const blockSubject = getBlockSubject(blockId);
blockSubject.subscribe((data) => {
// base64 decode
const decodedData = base64ToArray(data.ptydata);
term.write(decodedData);
});
return () => {
blockSubject.release();
};
}, [term]);
return <div key="conntectElem" className="view-term term-connectelem" ref={connectElemRef}></div>;
async function handleRunClick() {
try {
await BlockService.StartBlock(blockId);
await BlockService.SendCommand(blockId, { command: "message", message: "Run clicked" });
} catch (e) {
console.log("run click error: ", e);
}
}
return (
<div className="view-term">
<div className="term-header">
<div>Terminal</div>
<Button className="term-inline" onClick={() => handleRunClick()}>
Run
</Button>
</div>
<div key="conntectElem" className="term-connectelem" ref={connectElemRef}></div>
</div>
);
};
export { TerminalView };
+17
View File
@@ -7,6 +7,23 @@
width: 100%;
height: 100%;
overflow: hidden;
.term-header {
display: flex;
flex-direction: row;
padding: 4px 10px;
height: 35px;
gap: 10px;
align-items: center;
flex-shrink: 0;
border-bottom: 1px solid var(--border-color);
}
.term-connectelem {
flex-grow: 1;
min-height: 0;
overflow: hidden;
}
}
.view-preview {