Skip to content

Commit a68e51c

Browse files
authored
fix: output formatting and running state (#485)
* fix: fix program output text formatting * fix: formatting * fix: running state
1 parent cce5b62 commit a68e51c

File tree

7 files changed

+164
-145
lines changed

7 files changed

+164
-145
lines changed

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

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import React from 'react'
22

33
import { mergeStyleSets, useTheme } from '@fluentui/react'
44
import type { StatusState } from '~/store'
5-
import { OutputLine } from './OutputLine'
5+
import { EvalEventKind } from '~/services/api'
6+
import { splitImageAndText } from './utils'
67

78
interface Props {
89
status?: StatusState
@@ -16,11 +17,27 @@ interface Props {
1617
export const FallbackOutput: React.FC<Props> = ({ fontFamily, fontSize, status }) => {
1718
const theme = useTheme()
1819
const styles = mergeStyleSets({
19-
container: {
20+
root: {
2021
flex: '1 1 auto',
2122
boxSizing: 'border-box',
2223
padding: '0, 15px',
2324
},
25+
content: {
26+
whiteSpace: 'pre-wrap',
27+
display: 'table',
28+
width: '100%',
29+
30+
font: 'inherit',
31+
border: 'none',
32+
margin: 0,
33+
float: 'left',
34+
},
35+
stderr: {
36+
color: theme.palette.red,
37+
},
38+
image: {
39+
display: 'block',
40+
},
2441
programExitMsg: {
2542
marginTop: '1rem',
2643
display: 'inline-block',
@@ -29,8 +46,29 @@ export const FallbackOutput: React.FC<Props> = ({ fontFamily, fontSize, status }
2946
})
3047

3148
return (
32-
<div className={styles.container} style={{ fontFamily, fontSize: `${fontSize}px` }}>
33-
{status?.events?.map((event, i) => <OutputLine key={i} event={event} />)}
49+
<div className={styles.root} style={{ fontFamily, fontSize: `${fontSize}px` }}>
50+
<div className={styles.content}>
51+
{status?.events?.map(({ Kind: kind, Message: msg }, i) => {
52+
if (kind === EvalEventKind.Stderr) {
53+
return (
54+
<span key={i} className={styles.stderr}>
55+
{msg}
56+
</span>
57+
)
58+
}
59+
60+
// Image content and text can come mixed due to output buffering
61+
return splitImageAndText(msg).map(({ isImage, data }, j) => (
62+
<React.Fragment key={`${i}.${j}`}>
63+
{isImage ? (
64+
<img className={styles.image} key={i} src={`data:image;base64,${data}`} alt="Image output" />
65+
) : (
66+
data
67+
)}
68+
</React.Fragment>
69+
))
70+
})}
71+
</div>
3472
{!status?.running && <span className={styles.programExitMsg}>Program exited.</span>}
3573
</div>
3674
)

web/src/components/features/inspector/FallbackOutput/OutputLine.tsx

Lines changed: 0 additions & 56 deletions
This file was deleted.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
const imageSectionPrefix = 'IMAGE:'
2+
const base64RegEx = /[A-Za-z0-9+/]+={0,2}$/
3+
const imageRegEx = /(IMAGE:[A-Za-z0-9+/]+={0,2})/
4+
5+
export const isImageLine = (message: string): [boolean, string] => {
6+
if (!message.startsWith(imageSectionPrefix)) {
7+
return [false, message]
8+
}
9+
10+
const payload = message.substring(imageSectionPrefix.length).trim()
11+
return [base64RegEx.test(payload), payload]
12+
}
13+
14+
interface TextChunk {
15+
isImage?: boolean
16+
data: string
17+
}
18+
19+
/**
20+
* Finds all embedded base64 image segments and tokenizes content.
21+
*
22+
* Used to support embedded images in Go program stdout like go.dev/play does.
23+
*/
24+
export const splitImageAndText = (data: string): TextChunk[] =>
25+
data.split(imageRegEx).map((str) => {
26+
const [isImage, data] = isImageLine(str)
27+
return { isImage, data }
28+
})

web/src/store/dispatchers/build/dispatch.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,8 @@ import { buildGoTestFlags, requiresWasmEnvironment } from '~/lib/sourceutil'
55
import client, { type EvalEvent, EvalEventKind } from '~/services/api'
66
import { isProjectRequiresGoMod } from '~/services/examples'
77

8-
import { type DispatchFn, type StateProvider } from '../../helpers'
9-
import {
10-
newAddNotificationAction,
11-
newRemoveNotificationAction,
12-
NotificationIDs,
13-
NotificationType,
14-
} from '../../notifications'
8+
import type { DispatchFn, StateProvider } from '../../helpers'
9+
import { newRemoveNotificationAction, NotificationIDs } from '../../notifications'
1510
import {
1611
newErrorAction,
1712
newLoadingAction,
@@ -22,8 +17,13 @@ import {
2217

2318
import { type Dispatcher } from '../utils'
2419
import { type BulkFileUpdatePayload, WorkspaceAction } from '~/store/workspace/actions'
25-
import { fetchWasmWithProgress, lastElem, hasProgramTimeoutError, newStdoutHandler, runTimeoutNs} from './utils'
26-
import { goModMissingNotification, goEnvChangedNotification, goProgramExitNotification, wasmErrorNotification } from './notifications'
20+
import { fetchWasmWithProgress, lastElem, hasProgramTimeoutError, newStdoutHandler, runTimeoutNs } from './utils'
21+
import {
22+
goModMissingNotification,
23+
goEnvChangedNotification,
24+
goProgramExitNotification,
25+
wasmErrorNotification,
26+
} from './notifications'
2727

2828
const dispatchEvalEvents = (dispatch: DispatchFn, events: EvalEvent[]) => {
2929
// TODO: support cancellation

web/src/store/dispatchers/build/notifications.ts

Lines changed: 74 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -6,80 +6,86 @@ import {
66
NotificationIDs,
77
NotificationType,
88
newAddNotificationAction,
9-
newRemoveNotificationAction
9+
newRemoveNotificationAction,
1010
} from '../../notifications'
1111
import type { DispatchFn } from '../../helpers'
1212

13-
export const goModMissingNotification = (dispatch: DispatchFn) => newAddNotificationAction({
14-
id: NotificationIDs.GoModMissing,
15-
type: NotificationType.Error,
16-
title: 'Go.mod file is missing',
17-
description: 'Go.mod file is required to import sub-packages.',
18-
canDismiss: true,
19-
actions: [
20-
{
21-
key: 'ok',
22-
label: 'Create go.mod',
23-
primary: true,
24-
onClick: () => {
25-
dispatch<FileUpdatePayload[]>({
26-
type: WorkspaceAction.ADD_FILE,
27-
payload: [
28-
{
29-
filename: goModFile,
30-
content: goModTemplate,
31-
},
32-
],
33-
})
13+
export const goModMissingNotification = (dispatch: DispatchFn) =>
14+
newAddNotificationAction({
15+
id: NotificationIDs.GoModMissing,
16+
type: NotificationType.Error,
17+
title: 'Go.mod file is missing',
18+
description: 'Go.mod file is required to import sub-packages.',
19+
canDismiss: true,
20+
actions: [
21+
{
22+
key: 'ok',
23+
label: 'Create go.mod',
24+
primary: true,
25+
onClick: () => {
26+
dispatch<FileUpdatePayload[]>({
27+
type: WorkspaceAction.ADD_FILE,
28+
payload: [
29+
{
30+
filename: goModFile,
31+
content: goModTemplate,
32+
},
33+
],
34+
})
35+
},
3436
},
35-
},
36-
],
37-
})
38-
39-
export const goEnvChangedNotification = (dispatch: DispatchFn) => newAddNotificationAction({
40-
id: NotificationIDs.GoTargetSwitched,
41-
type: NotificationType.Warning,
42-
title: 'Go environment temporarily changed',
43-
description: 'This program will be executed using WebAssembly as Go program contains "//go:build" tag.',
44-
canDismiss: true,
45-
actions: [
46-
{
47-
key: 'ok',
48-
label: 'Ok',
49-
primary: true,
50-
onClick: () => dispatch(newRemoveNotificationAction(NotificationIDs.GoTargetSwitched)),
51-
},
52-
],
53-
})
37+
],
38+
})
5439

55-
export const goProgramExitNotification = (code: number) => newAddNotificationAction({
56-
id: NotificationIDs.WASMAppExitError,
57-
type: NotificationType.Warning,
58-
title: 'Go program finished',
59-
description: `Go program exited with non zero code: ${code}`,
60-
canDismiss: true,
61-
})
40+
export const goEnvChangedNotification = (dispatch: DispatchFn) =>
41+
newAddNotificationAction({
42+
id: NotificationIDs.GoTargetSwitched,
43+
type: NotificationType.Warning,
44+
title: 'Go environment temporarily changed',
45+
description: 'This program will be executed using WebAssembly as Go program contains "//go:build" tag.',
46+
canDismiss: true,
47+
actions: [
48+
{
49+
key: 'ok',
50+
label: 'Ok',
51+
primary: true,
52+
onClick: () => dispatch(newRemoveNotificationAction(NotificationIDs.GoTargetSwitched)),
53+
},
54+
],
55+
})
6256

63-
export const wasmErrorNotification = (err: any) => newAddNotificationAction({
64-
id: NotificationIDs.WASMAppExitError,
65-
type: NotificationType.Error,
66-
title: 'Failed to run WebAssembly program',
67-
description: err.toString(),
68-
canDismiss: true,
69-
})
57+
export const goProgramExitNotification = (code: number) =>
58+
newAddNotificationAction({
59+
id: NotificationIDs.WASMAppExitError,
60+
type: NotificationType.Warning,
61+
title: 'Go program finished',
62+
description: `Go program exited with non zero code: ${code}`,
63+
canDismiss: true,
64+
})
7065

66+
export const wasmErrorNotification = (err: any) =>
67+
newAddNotificationAction({
68+
id: NotificationIDs.WASMAppExitError,
69+
type: NotificationType.Error,
70+
title: 'Failed to run WebAssembly program',
71+
description: err.toString(),
72+
canDismiss: true,
73+
})
7174

72-
export const downloadProgressNotification = (progress?: Required<Pick<NotificationProgress, 'total' | 'current'>>, updateOnly?: boolean) => newAddNotificationAction(
73-
{
74-
id: NotificationIDs.WASMAppDownload,
75-
type: NotificationType.Info,
76-
title: 'Downloading compiled application',
77-
description: progress ? `${formatBytes(progress.current)} / ${formatBytes(progress.total)}` : undefined,
78-
canDismiss: false,
79-
progress: progress ?? {
80-
indeterminate: true,
75+
export const downloadProgressNotification = (
76+
progress?: Required<Pick<NotificationProgress, 'total' | 'current'>>,
77+
updateOnly?: boolean,
78+
) =>
79+
newAddNotificationAction(
80+
{
81+
id: NotificationIDs.WASMAppDownload,
82+
type: NotificationType.Info,
83+
title: 'Downloading compiled application',
84+
description: progress ? `${formatBytes(progress.current)} / ${formatBytes(progress.total)}` : undefined,
85+
canDismiss: false,
86+
progress: progress ?? {
87+
indeterminate: true,
88+
},
8189
},
82-
},
83-
updateOnly,
84-
)
85-
90+
updateOnly,
91+
)

web/src/store/dispatchers/build/utils.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { wrapResponseWithProgress } from '~/utils/http'
66
import type { DispatchFn } from '../../helpers'
77
import { newRemoveNotificationAction, NotificationIDs } from '../../notifications'
88
import { newProgramWriteAction } from '../../actions'
9-
import { downloadProgressNotification} from './notifications'
9+
import { downloadProgressNotification } from './notifications'
1010

1111
/**
1212
* Go program execution timeout in nanoseconds
@@ -42,10 +42,13 @@ export const fetchWasmWithProgress = async (dispatch: DispatchFn, fileName: stri
4242
cancelAnimationFrame(prevRafID)
4343
prevRafID = requestAnimationFrame(() => {
4444
dispatch(
45-
downloadProgressNotification({
46-
total: totalBytes,
47-
current: currentBytes,
48-
}, true)
45+
downloadProgressNotification(
46+
{
47+
total: totalBytes,
48+
current: currentBytes,
49+
},
50+
true,
51+
),
4952
)
5053
})
5154
})
@@ -60,11 +63,11 @@ export const fetchWasmWithProgress = async (dispatch: DispatchFn, fileName: stri
6063

6164
const decoder = new TextDecoder()
6265
export const newStdoutHandler = (dispatch: DispatchFn) => {
63-
return (data: ArrayBuffer, isStderr: boolean) => {
66+
return (data: ArrayBufferLike, isStderr: boolean) => {
6467
dispatch(
6568
newProgramWriteAction({
6669
Kind: isStderr ? EvalEventKind.Stderr : EvalEventKind.Stdout,
67-
Message: decoder.decode(data),
70+
Message: decoder.decode(data as ArrayBuffer),
6871
Delay: 0,
6972
}),
7073
)

web/src/store/reducers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,8 @@ const reducers = {
9494
[ActionType.EVAL_EVENT]: (s: StatusState, a: Action<EvalEvent>) => ({
9595
lastError: null,
9696
loading: false,
97-
running: true,
9897
dirty: true,
98+
running: s.running,
9999
events: s.events ? s.events.concat(a.payload) : [a.payload],
100100
}),
101101
[ActionType.EVAL_FINISH]: (s: StatusState, _: Action) => ({

0 commit comments

Comments
 (0)