Skip to content

Commit

Permalink
feat: 增加 terminal 输出
Browse files Browse the repository at this point in the history
  • Loading branch information
xjq7 committed Oct 28, 2022
1 parent cf0abbf commit b02c09b
Show file tree
Hide file tree
Showing 12 changed files with 211 additions and 18 deletions.
4 changes: 3 additions & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
"monaco-editor": "^0.34.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-use": "^17.4.0"
"react-use": "^17.4.0",
"xterm": "^5.0.0",
"xterm-addon-web-links": "^0.7.0"
},
"devDependencies": {
"@types/lodash": "^4.14.186",
Expand Down
16 changes: 16 additions & 0 deletions client/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 9 additions & 2 deletions client/src/components/Button/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,15 @@ interface Props
const Component: React.FC<PropsWithChildren<Props>> = (
props: PropsWithChildren<Props>
) => {
const { type, size, children, loading, className, onClick, ...restProps } =
props;
const {
type = 'primary',
size,
children,
loading,
className,
onClick,
...restProps
} = props;

const typeCls = useMemo(() => {
switch (type) {
Expand Down
46 changes: 46 additions & 0 deletions client/src/components/Dropdown/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import classNames from 'classnames';
import noop from 'lodash/noop';
import { PropsWithChildren } from 'react';
import { CommonProps } from '../type';

export interface Option {
label: string;
value: any;
}

interface Props extends CommonProps {
options: Option[];
onChange?(v: Option): void;
optionStyle?: string;
}

function Component(props: PropsWithChildren<Props>) {
const { children, optionStyle, options = [], onChange = noop } = props;
return (
<div className="dropdown">
<label tabIndex={0}>{children}</label>
<ul
tabIndex={0}
className={classNames(
'dropdown-content menu p-2 shadow bg-base-100 rounded-box w-52',
optionStyle
)}
>
{options.map((option) => {
return (
<li
key={option.value}
onClick={() => {
onChange(option);
}}
>
<a>{option.label}</a>
</li>
);
})}
</ul>
</div>
);
}

export default Component;
7 changes: 7 additions & 0 deletions client/src/components/type.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import React from 'react';

export type ISize = 'lg' | 'md' | 'sm' | 'xs';

export type Itype = 'primary' | 'secondary' | 'accent';

export interface CommonProps {
className?: string;
style?: React.CSSProperties;
}
1 change: 1 addition & 0 deletions client/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as ReactDOM from 'react-dom/client';
import App from './App';
import rem from '~utils/rem';
import './userWorker';
import 'xterm/css/xterm.css';
import './tailwind.output.css';

rem();
Expand Down
25 changes: 25 additions & 0 deletions client/src/pages/demo/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useEffect } from 'react';
import { Terminal } from 'xterm';

function Component() {
useEffect(() => {
var term = new Terminal({ rows: 10 });
term.open(document.querySelector('#terminal') as HTMLElement);
term.write(
`\u001b[01m\u001b[Kcode.c:\u001b[m\u001b[K In function '\u001b[01m\u001b[Kint main()\u001b[m\u001b[K':\r\n\u001b[01m\u001b[Kcode.c:6:1:\u001b[m\u001b[K \u001b[01;31m\u001b[Kerror: \u001b[m\u001b[Kexpected '\u001b[01m\u001b[K;\u001b[m\u001b[K' before '\u001b[01m\u001b[K}\u001b[m\u001b[K' token\r\n }\r\n\u001b[01;32m\u001b[K ^\u001b[m\u001b[K\r\n` +
`\u001b[01m\u001b[Kcode.c:\u001b[m\u001b[K In function '\u001b[01m\u001b[Kint main()\u001b[m\u001b[K':\r\n\u001b[01m\u001b[Kcode.c:6:1:\u001b[m\u001b[K \u001b[01;31m\u001b[Kerror: \u001b[m\u001b[Kexpected '\u001b[01m\u001b[K;\u001b[m\u001b[K' before '\u001b[01m\u001b[K}\u001b[m\u001b[K' token\r\n }\r\n\u001b[01;32m\u001b[K ^\u001b[m\u001b[K\r\n` +
`\u001b[01m\u001b[Kcode.c:\u001b[m\u001b[K In function '\u001b[01m\u001b[Kint main()\u001b[m\u001b[K':\r\n\u001b[01m\u001b[Kcode.c:6:1:\u001b[m\u001b[K \u001b[01;31m\u001b[Kerror: \u001b[m\u001b[Kexpected '\u001b[01m\u001b[K;\u001b[m\u001b[K' before '\u001b[01m\u001b[K}\u001b[m\u001b[K' token\r\n }\r\n\u001b[01;32m\u001b[K ^\u001b[m\u001b[K\r\n` +
`\u001b[01m\u001b[Kcode.c:\u001b[m\u001b[K In function '\u001b[01m\u001b[Kint main()\u001b[m\u001b[K':\r\n\u001b[01m\u001b[Kcode.c:6:1:\u001b[m\u001b[K \u001b[01;31m\u001b[Kerror: \u001b[m\u001b[Kexpected '\u001b[01m\u001b[K;\u001b[m\u001b[K' before '\u001b[01m\u001b[K}\u001b[m\u001b[K' token\r\n }\r\n\u001b[01;32m\u001b[K ^\u001b[m\u001b[K\r\n` +
`\u001b[01m\u001b[Kcode.c:\u001b[m\u001b[K In function '\u001b[01m\u001b[Kint main()\u001b[m\u001b[K':\r\n\u001b[01m\u001b[Kcode.c:6:1:\u001b[m\u001b[K \u001b[01;31m\u001b[Kerror: \u001b[m\u001b[Kexpected '\u001b[01m\u001b[K;\u001b[m\u001b[K' before '\u001b[01m\u001b[K}\u001b[m\u001b[K' token\r\n }\r\n\u001b[01;32m\u001b[K ^\u001b[m\u001b[K\r\n` +
`\u001b[01m\u001b[Kcode.c:\u001b[m\u001b[K In function '\u001b[01m\u001b[Kint main()\u001b[m\u001b[K':\r\n\u001b[01m\u001b[Kcode.c:6:1:\u001b[m\u001b[K \u001b[01;31m\u001b[Kerror: \u001b[m\u001b[Kexpected '\u001b[01m\u001b[K;\u001b[m\u001b[K' before '\u001b[01m\u001b[K}\u001b[m\u001b[K' token\r\n }\r\n\u001b[01;32m\u001b[K ^\u001b[m\u001b[K\r\n` +
`\u001b[01m\u001b[Kcode.c:\u001b[m\u001b[K In function '\u001b[01m\u001b[Kint main()\u001b[m\u001b[K':\r\n\u001b[01m\u001b[Kcode.c:6:1:\u001b[m\u001b[K \u001b[01;31m\u001b[Kerror: \u001b[m\u001b[Kexpected '\u001b[01m\u001b[K;\u001b[m\u001b[K' before '\u001b[01m\u001b[K}\u001b[m\u001b[K' token\r\n }\r\n\u001b[01;32m\u001b[K ^\u001b[m\u001b[K\r\n`
);
return () => {
term.dispose();
};
}, []);

return <div id="terminal"></div>;
}

export default Component;
79 changes: 75 additions & 4 deletions client/src/pages/editor/components/Operator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { template } from '~components/CodeEditorMonaco/const';
import { toast } from '~components/Toast';
import useClangFormat from '~hooks/useClangFormat/useClangFormat';
import { CodeType } from '~utils/codeType';
import { parseConsoleOutput, saveAsFile } from '~utils/helper';
import { parseConsoleOutput, saveAsFile, TerminalType } from '~utils/helper';
import { runCode } from '../service';
import Tab from '~components/Tab';
import TextArea from '~components/Textarea';
Expand All @@ -16,6 +16,10 @@ import storage from '~utils/storage';
import { CodeStorageKey } from '~constant/storage';
import styles from './operator.module.less';
import { editor } from 'monaco-editor';
import { Terminal } from 'xterm';
import { WebLinksAddon } from 'xterm-addon-web-links';
import Dropdown, { Option } from '~components/Dropdown';
import useWindowSize from 'react-use/lib/useWindowSize';

enum DisplayType {
input,
Expand All @@ -39,21 +43,60 @@ function Operator(props: Props) {

const [saveDisabled, setSaveDisabled] = useState(true);

const [terminalType, setTerminalType] = useState(TerminalType.terminal);

const termRef = useRef<Terminal>();

const onCodeFormatDone = (result: string) => {
getEditor()?.setValue(result);
};

const [clangFormat] = useClangFormat({ onCodeFormatDone });

const { width } = useWindowSize();

const hiddenTerminalOutput = useMemo(() => width < 600, [width]);

const output = useMemo(() => {
let output = '';
if (data?.code) {
output = data?.message || '';
} else {
output = data?.output || '';
}
return parseConsoleOutput(output);
}, [data]);
return parseConsoleOutput(output, terminalType);
}, [data, terminalType]);

const handleTerminalChange = (option: Option) => {
setTerminalType(option.value);
};

useEffect(() => {
if (hiddenTerminalOutput) {
setTerminalType(TerminalType.plain);
}
}, [hiddenTerminalOutput]);

useEffect(() => {
if (terminalType !== TerminalType.terminal) return;
var term = new Terminal({
rows: 13,
allowProposedApi: true,
disableStdin: true,
});
term.loadAddon(new WebLinksAddon());
term.open(document.querySelector('#terminal') as HTMLElement);
termRef.current = term;
return () => {
term.dispose();
};
}, [terminalType]);

useEffect(() => {
if (terminalType !== TerminalType.terminal) return;
termRef.current?.reset();
termRef.current?.write(output.join('\n'));
}, [output, terminalType]);

const [timesPrevent, setTimesPrevent] = useState(false);

Expand Down Expand Up @@ -115,14 +158,29 @@ function Operator(props: Props) {
inputRef.current = e.target.value;
}}
className={'w-full h-full mt-1'}
style={{ display: display === DisplayType.input ? 'block' : 'none' }}
style={{
display: display === DisplayType.input ? 'block' : 'none',
height: 231,
}}
placeholder="stdin..."
border
/>
);
};

const renderOutput = () => {
if (terminalType === TerminalType.terminal) {
return (
<div
className={styles.terminal_container}
style={{
display: display === DisplayType.input ? 'none' : 'block',
}}
>
<div id="terminal"></div>
</div>
);
}
return (
<div
className={classnames(styles.output, 'mt-1', 'text-gray-600')}
Expand All @@ -146,6 +204,19 @@ function Operator(props: Props) {
return (
<div className={styles.container}>
<div className={classnames(styles.operator, 'pt-2')}>
{!hiddenTerminalOutput && (
<Dropdown
optionStyle="w-36"
options={[
{ label: 'plain', value: TerminalType.plain },
{ label: 'terminal', value: TerminalType.terminal },
]}
onChange={handleTerminalChange}
>
<Button className="mr-2">终端样式</Button>
</Dropdown>
)}

{output.length !== 0 && (
<Tooltip className="mr-2" tips="将运行输出保存到本地文件">
<Button
Expand Down
7 changes: 7 additions & 0 deletions client/src/pages/editor/components/operator.module.less
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@
width: 100%;
height: 220px;
margin-top: -26px;
.terminal_container {
margin-top: 4px;
border-radius: 8px;
padding: 12px 0 0 15px;
background: black;
}

.output {
white-space: normal;
word-break: break-all;
Expand Down
17 changes: 15 additions & 2 deletions client/src/utils/helper.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
export function parseConsoleOutput(output: string) {
export enum TerminalType {
plain,
terminal,
}

export function parseConsoleOutput(output: string, type: TerminalType) {
if (!output) return [];
// 换行解析
let splitAsEnter = output.split(/%0A/).map((str) => decodeURI(str));
let splitAsEnter = output.split(/%0A/).map((str) => {
if (type === TerminalType.plain) {
str = encodeURI(str);
str = str.replace(/%1B%5B.*?m.*?%1B%5BK|%1B%5B.*?m|%0D/g, '');
}

return decodeURI(str);
});

return splitAsEnter;
}

Expand Down
4 changes: 4 additions & 0 deletions client/src/vite-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ interface ImportMetaEnv {
interface ImportMeta {
readonly env: ImportMetaEnv;
}

interface Window {
readonly terminal: any;
}
12 changes: 3 additions & 9 deletions server/src/docker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,23 +214,17 @@ export async function run2(params: {
}

function formatOutput(outputString: string): string {
if (outputString.length > 4200) {
if (outputString.length > 8200) {
outputString =
outputString.slice(0, 2000) +
outputString.slice(outputString.length - 2000);
outputString.slice(0, 4000) +
outputString.slice(outputString.length - 4000);
}

// hack 当遇到数组跟对象时, toString 方法的输出会是 {} => [object Object] [1,2] => 1,2
// https://github.com/xjq7/runcode/issues/4
if (isType('Object', 'Array')(outputString)) {
outputString = JSON.stringify(outputString);
}
outputString = encodeURI(outputString);

outputString = outputString.replace(
/%1B%5B.*?m.*?%1B%5BK|%1B%5B.*?m|%0D/g,
''
);

let outputStringArr = outputString.split('%0A');
if (outputStringArr.length > 200) {
Expand Down

0 comments on commit b02c09b

Please sign in to comment.