-
Notifications
You must be signed in to change notification settings - Fork 75
/
Copy pathApp.tsx
253 lines (228 loc) · 8.69 KB
/
App.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
import React, { StrictMode } from 'react';
import ReactDOM from 'react-dom/client';
import Terminal from './components/Terminal';
import Editor from './components/Editor';
import Plot from './components/Plot';
import Files from './components/Files';
import { Readline } from 'xterm-readline';
import { WebR } from '../webR/webr-main';
import { bufferToBase64 } from '../webR/utils';
import { CanvasMessage, PagerMessage, ViewMessage, BrowseMessage } from '../webR/webr-chan';
import { Panel, PanelGroup, PanelResizeHandle, ImperativePanelHandle } from 'react-resizable-panels';
import './App.css';
import { NamedObject, WebRDataJsAtomic } from '../webR/robj';
const webR = new WebR({
RArgs: [],
REnv: {
R_HOME: '/usr/lib/R',
FONTCONFIG_PATH: '/etc/fonts',
R_ENABLE_JIT: '0',
COLORTERM: 'truecolor',
},
});
(globalThis as any).webR = webR;
export interface TerminalInterface {
println: Readline['println'];
read: Readline['read'];
write: Readline['write'];
}
export interface FilesInterface {
refreshFilesystem: () => Promise<void>;
openFileInEditor: (name: string, path: string, readOnly: boolean) => Promise<void>;
openDataInEditor: (title: string, data: NamedObject<WebRDataJsAtomic<string>> ) => void;
openHtmlInEditor: (src: string, path: string) => void;
}
export interface PlotInterface {
resize: (direction: "width" | "height", px: number) => void;
newPlot: () => void;
drawImage: (img: ImageBitmap) => void;
}
const terminalInterface: TerminalInterface = {
println: (msg: string) => { console.log(msg); },
read: () => Promise.reject(new Error('Unable to read from webR terminal.')),
write: (msg: string) => { console.log(msg); },
};
const filesInterface: FilesInterface = {
refreshFilesystem: () => Promise.resolve(),
openFileInEditor: () => { throw new Error('Unable to open file, editor not initialised.'); },
openDataInEditor: () => { throw new Error('Unable to view data, editor not initialised.'); },
openHtmlInEditor: () => { throw new Error('Unable to view HTML, editor not initialised.'); },
};
const plotInterface: PlotInterface = {
resize: () => { return; },
newPlot: () => { return; },
drawImage: () => {
throw new Error('Unable to plot, plotting not initialised.');
},
};
function handleCanvasMessage(msg: CanvasMessage) {
if (msg.data.event === 'canvasImage') {
plotInterface.drawImage(msg.data.image);
} else if (msg.data.event === 'canvasNewPage') {
plotInterface.newPlot();
}
}
async function handlePagerMessage(msg: PagerMessage) {
const { path, title, deleteFile } = msg.data;
await filesInterface.openFileInEditor(title, path, true);
if (deleteFile) {
await webR.FS.unlink(path);
}
}
async function handleBrowseMessage(msg: BrowseMessage) {
const { url } = msg.data;
const root = url.split('/').slice(0, -1).join('/');
const decoder = new TextDecoder('utf8');
let content = decoder.decode(await webR.FS.readFile(url));
// Replace relative URLs in HTML output with the contents of the VFS.
/* TODO: This should really be handled by a custom print method sending the
* entire R object reference to the main thread, rather than performing
* regex on HTML -- famously a bad idea because HTML is context-free.
* Saying that, this does seem to work reasonably well for now.
*
* Since we don't load the `webr` support package by default, the
* alternative looks to be using hacks to register a bunch of custom S3
* generics like `print.htmlwidget` in the "webr_shim" namespace, and
* then maintain the `search()` order as other packages are loaded so
* that our namespace is always at the front, messy.
*/
const jsRegex = /<script.*src=["'`](.+\.js)["'`].*>.*<\/script>/g;
const jsMatches = Array.from(content.matchAll(jsRegex) || []);
const jsContent: {[idx: number]: string} = {};
await Promise.all(jsMatches.map((match, idx) => {
return webR.FS.readFile(`${root}/${match[1]}`)
.then((file) => bufferToBase64(file))
.then((enc) => {
jsContent[idx] = "data:text/javascript;base64," + enc;
});
}));
jsMatches.forEach((match, idx) => {
content = content.replace(match[0], `
<script type="text/javascript" src="${jsContent[idx]}"></script>
`);
});
let injectedBaseStyle = false;
const cssBaseStyle = `<style>body{font-family: sans-serif;}</style>`;
const cssRegex = /<link.*href=["'`](.+\.css)["'`].*>/g;
const cssMatches = Array.from(content.matchAll(cssRegex) || []);
const cssContent: {[idx: number]: string} = {};
await Promise.all(cssMatches.map((match, idx) => {
return webR.FS.readFile(`${root}/${match[1]}`)
.then((file) => bufferToBase64(file))
.then((enc) => {
cssContent[idx] = "data:text/css;base64," + enc;
});
}));
cssMatches.forEach((match, idx) => {
let cssHtml = `<link rel="stylesheet" href="${cssContent[idx]}"/>`;
if (!injectedBaseStyle){
cssHtml = cssBaseStyle + cssHtml;
injectedBaseStyle = true;
}
content = content.replace(match[0], cssHtml);
});
filesInterface.openHtmlInEditor(content, url);
}
function handleViewMessage(msg: ViewMessage) {
const { title, data } = msg.data;
filesInterface.openDataInEditor(title, data);
}
const onPanelResize = (size: number) => {
plotInterface.resize("width", size * window.innerWidth / 100);
};
function App() {
const rightPanelRef = React.useRef<ImperativePanelHandle | null>(null);
React.useEffect(() => {
window.addEventListener("resize", () => {
if (!rightPanelRef.current) return;
onPanelResize(rightPanelRef.current.getSize());
});
}, []);
return (
<div className='repl'>
<PanelGroup direction="horizontal">
<Panel defaultSize={50} minSize={10}>
<PanelGroup autoSaveId="conditional" direction="vertical">
<Editor
webR={webR}
terminalInterface={terminalInterface}
filesInterface={filesInterface}
/>
<PanelResizeHandle />
<Terminal webR={webR} terminalInterface={terminalInterface} />
</PanelGroup>
</Panel>
<PanelResizeHandle />
<Panel ref={rightPanelRef} onResize={onPanelResize} minSize={10}>
<PanelGroup direction="vertical">
<Files webR={webR} filesInterface={filesInterface} />
<PanelResizeHandle />
<Plot webR={webR} plotInterface={plotInterface} />
</PanelGroup>
</Panel>
</PanelGroup>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root')!);
root.render(<StrictMode><App /></StrictMode>);
void (async () => {
await webR.init();
// Set the default graphics device, browser, and pager
await webR.evalRVoid('webr::viewer_install()');
await webR.evalRVoid('webr::pager_install()');
await webR.evalRVoid(`
webr::canvas_install(
width = getOption("webr.fig.width", 504),
height = getOption("webr.fig.height", 504)
)
`);
// shim function from base R with implementations for webR
// see ?webr::shim_install for details.
await webR.evalRVoid('webr::shim_install()');
// If supported, show a menu when prompted for missing package installation
const showMenu = crossOriginIsolated;
await webR.evalRVoid('options(webr.show_menu = show_menu)', { env: { show_menu: !!showMenu } });
await webR.evalRVoid('webr::global_prompt_install()', { withHandlers: false });
// Additional options for running packages under wasm
await webR.evalRVoid('options(rgl.printRglwidget = TRUE)');
// Clear the loading message
terminalInterface.write('\x1b[2K\r');
for (; ;) {
const output = await webR.read();
switch (output.type) {
case 'stdout':
terminalInterface.println(output.data as string);
break;
case 'stderr':
terminalInterface.println(`\x1b[1;31m${output.data as string}\x1b[m`);
break;
case 'prompt':
void filesInterface.refreshFilesystem();
terminalInterface.read(output.data as string).then((command) => {
webR.writeConsole(command);
}, (reason) => {
console.error(reason);
throw new Error(`An error occurred reading from the R console terminal.`);
});
break;
case 'canvas':
handleCanvasMessage(output as CanvasMessage);
break;
case 'pager':
await handlePagerMessage(output as PagerMessage);
break;
case 'view':
handleViewMessage(output as ViewMessage);
break;
case 'browse':
void handleBrowseMessage(output as BrowseMessage);
break;
case 'closed':
throw new Error('The webR communication channel has been closed');
default:
console.error(`Unimplemented output type: ${output.type}`);
console.error(output.data);
}
}
})();