Skip to content

Commit a2c81fc

Browse files
authored
feat: fallback mode without xterm.js (#463)
* feat: add terminal emulation toggle * feat: add warning * feat: implement fallback output * fix: improve settings message
1 parent e20d46e commit a2c81fc

File tree

8 files changed

+159
-24
lines changed

8 files changed

+159
-24
lines changed

web/src/components/features/inspector/Console/Console.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const config: ITerminalOptions = {
3131
convertEol: true,
3232
}
3333

34-
interface Props {
34+
export interface ConsoleProps {
3535
status?: StatusState
3636
fontFamily: string
3737
fontSize: number
@@ -85,7 +85,7 @@ const CopyButton: React.FC<{
8585
/**
8686
* Console is Go program events output component based on xterm.js
8787
*/
88-
export const Console: React.FC<Props> = ({ fontFamily, fontSize, status, backend }) => {
88+
export const Console: React.FC<ConsoleProps> = ({ fontFamily, fontSize, status, backend }) => {
8989
const theme = useXtermTheme()
9090
const [offset, setOffset] = useState(0)
9191
const [isFocused, setIsFocused] = useState(false)
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import React from 'react'
2+
3+
import { mergeStyleSets, useTheme } from '@fluentui/react'
4+
import type { StatusState } from '~/store'
5+
import { OutputLine } from './OutputLine'
6+
7+
interface Props {
8+
status?: StatusState
9+
fontFamily: string
10+
fontSize: number
11+
}
12+
13+
/**
14+
* Fallback console without terminal escape sequence emulation.
15+
*/
16+
export const FallbackOutput: React.FC<Props> = ({ fontFamily, fontSize, status }) => {
17+
const theme = useTheme()
18+
const styles = mergeStyleSets({
19+
container: {
20+
flex: '1 1 auto',
21+
boxSizing: 'border-box',
22+
padding: '0, 15px',
23+
},
24+
programExitMsg: {
25+
marginTop: '1rem',
26+
display: 'inline-block',
27+
color: theme.semanticColors.disabledText,
28+
},
29+
})
30+
31+
return (
32+
<div className={styles.container} style={{ fontFamily, fontSize: `${fontSize}px` }}>
33+
{status?.events?.map((event, i) => <OutputLine key={i} event={event} />)}
34+
{!status?.running && <span className={styles.programExitMsg}>Program exited.</span>}
35+
</div>
36+
)
37+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import React from 'react'
2+
3+
import { mergeStyleSets } from '@fluentui/react'
4+
import type { EvalEvent } from '~/services/api'
5+
6+
const imageSectionPrefix = 'IMAGE:'
7+
const base64RegEx = /^[A-Za-z0-9+/]+={0,2}$/
8+
9+
const isImageLine = (message: string) => {
10+
if (!message?.startsWith(imageSectionPrefix)) {
11+
return [false, null]
12+
}
13+
14+
const payload = message.substring(imageSectionPrefix.length).trim()
15+
return [base64RegEx.test(payload), payload]
16+
}
17+
18+
const styles = mergeStyleSets({
19+
container: {
20+
display: 'table',
21+
width: '100%',
22+
'&[data-event-kind="stderr"]': {
23+
color: '#e22',
24+
},
25+
},
26+
delay: {
27+
color: '#666',
28+
marginRight: '5px',
29+
float: 'right',
30+
},
31+
message: {
32+
font: 'inherit',
33+
border: 'none',
34+
margin: 0,
35+
float: 'left',
36+
},
37+
})
38+
39+
interface Props {
40+
event: EvalEvent
41+
}
42+
43+
export const OutputLine: React.FC<Props> = ({ event }) => {
44+
const [isImage, payload] = isImageLine(event.Message)
45+
46+
return (
47+
<div className={styles.container} data-event-kind={event.Kind}>
48+
{isImage ? (
49+
<img src={`data:image;base64,${payload}`} alt="Image output" />
50+
) : (
51+
<pre className={styles.message}>{event.Message}</pre>
52+
)}
53+
</div>
54+
)
55+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './FallbackOutput'

web/src/components/features/inspector/InspectorPanel/InspectorPanel.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Resizable } from 're-resizable'
44
import clsx from 'clsx'
55
import { VscChevronDown, VscChevronUp, VscSplitHorizontal, VscSplitVertical } from 'react-icons/vsc'
66

7-
import { ConnectedRunOutput } from '../RunOutput'
7+
import { RunOutput } from '../RunOutput'
88
import { PanelHeader } from '~/components/elements/panel/PanelHeader'
99
import { LayoutType, DEFAULT_PANEL_HEIGHT, DEFAULT_PANEL_WIDTH_PERCENT } from '~/styles/layout'
1010
import './InspectorPanel.css'
@@ -148,7 +148,7 @@ export const InspectorPanel: React.FC<Props> = ({
148148
}}
149149
/>
150150
<div className="InspectorPanel__container" hidden={isCollapsed}>
151-
<ConnectedRunOutput />
151+
<RunOutput />
152152
</div>
153153
</Resizable>
154154
)

web/src/components/features/inspector/RunOutput/RunOutput.tsx

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,15 @@
11
import React, { useMemo } from 'react'
2+
import { useSelector } from 'react-redux'
23
import { Link, MessageBar, MessageBarType, useTheme } from '@fluentui/react'
34

4-
import { Console } from '~/components/features/inspector/Console'
5-
import { connect, type StatusState } from '~/store'
6-
import type { TerminalState } from '~/store/terminal'
7-
import type { MonacoSettings } from '~/services/config'
5+
import { Console, type ConsoleProps } from '~/components/features/inspector/Console'
6+
import { FallbackOutput } from 'components/features/inspector/FallbackOutput'
7+
import { type State } from '~/store'
88
import { DEFAULT_FONT, getDefaultFontFamily, getFontFamily } from '~/services/fonts'
99

1010
import './RunOutput.css'
1111
import { splitStringUrls } from './parser'
1212

13-
interface StateProps {
14-
status?: StatusState
15-
monaco?: MonacoSettings
16-
terminal: TerminalState
17-
}
18-
1913
const linkStyle = {
2014
root: {
2115
// Fluent UI adds padding with :nth-child selector.
@@ -34,8 +28,18 @@ const highlightLinks = (str: string) =>
3428
),
3529
)
3630

37-
const RunOutput: React.FC<StateProps & {}> = ({ status, monaco, terminal }) => {
31+
const ConsoleWrapper: React.FC<ConsoleProps & { disableTerminal?: boolean }> = (props) => {
32+
if (props.disableTerminal) {
33+
return <FallbackOutput {...props} />
34+
}
35+
36+
return <Console {...props} />
37+
}
38+
39+
export const RunOutput: React.FC = () => {
3840
const theme = useTheme()
41+
const { status, monaco, terminal } = useSelector<State, State>((state) => state)
42+
3943
const { fontSize, renderingBackend } = terminal.settings
4044
const styles = useMemo(() => {
4145
const { palette } = theme
@@ -69,15 +73,15 @@ const RunOutput: React.FC<StateProps & {}> = ({ status, monaco, terminal }) => {
6973
Press &quot;Run&quot; to compile program.
7074
</div>
7175
) : (
72-
<Console fontFamily={fontFamily} fontSize={fontSize} status={status} backend={renderingBackend} />
76+
<ConsoleWrapper
77+
fontFamily={fontFamily}
78+
fontSize={fontSize}
79+
status={status}
80+
backend={renderingBackend}
81+
disableTerminal={terminal.settings.disableTerminalEmulation}
82+
/>
7383
)}
7484
</div>
7585
</div>
7686
)
7787
}
78-
79-
export const ConnectedRunOutput = connect<StateProps, {}>(({ status, monaco, terminal }) => ({
80-
status,
81-
monaco,
82-
terminal,
83-
}))(RunOutput)

web/src/components/features/settings/SettingsModal.tsx

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
import React from 'react'
2-
import { Checkbox, Dropdown, getTheme, type IPivotStyles, PivotItem, Text, TextField } from '@fluentui/react'
2+
import {
3+
Checkbox,
4+
Dropdown,
5+
getTheme,
6+
type IPivotStyles,
7+
MessageBar,
8+
MessageBarType,
9+
PivotItem,
10+
Text,
11+
TextField,
12+
} from '@fluentui/react'
313

414
import { AnimatedPivot } from '~/components/elements/tabs/AnimatedPivot'
515
import { ThemeableComponent } from '~/components/utils/ThemeableComponent'
@@ -8,7 +18,7 @@ import { SettingsProperty } from './SettingsProperty'
818
import { DEFAULT_FONT } from '~/services/fonts'
919
import type { MonacoSettings } from '~/services/config'
1020
import type { RenderingBackend, TerminalSettings } from '~/store/terminal'
11-
import { connect, type StateDispatch, type MonacoParamsChanges, type SettingsState } from '~/store'
21+
import { connect, type MonacoParamsChanges, type SettingsState, type StateDispatch } from '~/store'
1222

1323
import { cursorBlinkOptions, cursorLineOptions, fontOptions, terminalBackendOptions } from './options'
1424
import { controlKeyLabel } from '~/utils/dom'
@@ -38,6 +48,7 @@ type Props = StateProps &
3848

3949
interface SettingsModalState {
4050
isOpen?: boolean
51+
hideTerminalSettings: boolean
4152
}
4253

4354
const modalStyles = {
@@ -60,6 +71,7 @@ class SettingsModal extends ThemeableComponent<Props, SettingsModalState> {
6071
super(props)
6172
this.state = {
6273
isOpen: props.isOpen,
74+
hideTerminalSettings: !!this.props.terminal?.disableTerminalEmulation,
6375
}
6476
}
6577

@@ -89,6 +101,10 @@ class SettingsModal extends ThemeableComponent<Props, SettingsModalState> {
89101
}
90102

91103
private touchTerminalSettings(changes: Partial<TerminalSettings>) {
104+
if ('disableTerminalEmulation' in changes) {
105+
this.setState({ hideTerminalSettings: !!changes.disableTerminalEmulation })
106+
}
107+
92108
if (!this.changes.terminal) {
93109
this.changes.terminal = changes
94110
return
@@ -328,6 +344,7 @@ class SettingsModal extends ThemeableComponent<Props, SettingsModalState> {
328344
control={
329345
<Dropdown
330346
options={terminalBackendOptions}
347+
disabled={this.state.hideTerminalSettings}
331348
defaultSelectedKey={this.props.terminal?.renderingBackend}
332349
onChange={(_, val) => {
333350
this.touchTerminalSettings({
@@ -337,6 +354,26 @@ class SettingsModal extends ThemeableComponent<Props, SettingsModalState> {
337354
/>
338355
}
339356
/>
357+
<SettingsProperty
358+
key="termEmulationEnabled"
359+
title="Emulate Terminal"
360+
control={
361+
<Checkbox
362+
label="Enables ANSI terminal escape sequences support using xterm.js."
363+
defaultChecked={!this.props.terminal?.disableTerminalEmulation}
364+
onChange={(_, val) => {
365+
this.touchTerminalSettings({
366+
disableTerminalEmulation: !val,
367+
})
368+
}}
369+
/>
370+
}
371+
/>
372+
<div>
373+
<MessageBar messageBarType={MessageBarType.warning}>
374+
Disable <u>Emulate Terminal</u> feature if you having troubles copying text from output.
375+
</MessageBar>
376+
</div>
340377
</PivotItem>
341378
</AnimatedPivot>
342379
</Dialog>

web/src/store/terminal/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export enum RenderingBackend {
77
export interface TerminalSettings {
88
fontSize: number
99
renderingBackend: RenderingBackend
10+
disableTerminalEmulation?: boolean
1011
}
1112

1213
export const defaultTerminalSettings: TerminalSettings = {

0 commit comments

Comments
 (0)