@basthon/console-react 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/lib/components/app.d.ts +15 -0
  2. package/lib/components/app.js +44 -0
  3. package/lib/components/basthon.svg +62 -0
  4. package/lib/components/button.d.ts +7 -0
  5. package/lib/components/button.js +9 -0
  6. package/lib/components/editor.d.ts +11 -0
  7. package/lib/components/editor.js +60 -0
  8. package/lib/components/footer.d.ts +7 -0
  9. package/lib/components/footer.js +8 -0
  10. package/lib/components/graphics.d.ts +6 -0
  11. package/lib/components/graphics.js +51 -0
  12. package/lib/components/header.d.ts +9 -0
  13. package/lib/components/header.js +15 -0
  14. package/lib/components/main-container.d.ts +14 -0
  15. package/lib/components/main-container.js +25 -0
  16. package/lib/components/styles.module.scss +394 -0
  17. package/lib/components/terminal.d.ts +5 -0
  18. package/lib/components/terminal.js +41 -0
  19. package/lib/gui/ace.d.ts +4 -0
  20. package/lib/gui/ace.js +17 -0
  21. package/lib/gui/editor.d.ts +35 -0
  22. package/lib/gui/editor.js +113 -0
  23. package/lib/gui/graphics.d.ts +44 -0
  24. package/lib/gui/graphics.js +185 -0
  25. package/lib/gui/gui.d.ts +87 -0
  26. package/lib/gui/gui.js +188 -0
  27. package/lib/gui/terminal.d.ts +67 -0
  28. package/lib/gui/terminal.js +242 -0
  29. package/lib/hooks/async-effect.d.ts +7 -0
  30. package/lib/hooks/async-effect.js +17 -0
  31. package/lib/hooks/basthon.d.ts +16 -0
  32. package/lib/hooks/basthon.js +55 -0
  33. package/lib/hooks/dispatch.d.ts +4 -0
  34. package/lib/hooks/dispatch.js +4 -0
  35. package/lib/hooks/gui.d.ts +3 -0
  36. package/lib/hooks/gui.js +3 -0
  37. package/lib/index.d.ts +2 -0
  38. package/lib/index.js +2 -0
  39. package/lib/state-manager.d.ts +38 -0
  40. package/lib/state-manager.js +101 -0
  41. package/package.json +1 -1
@@ -0,0 +1,242 @@
1
+ import $ from "jquery";
2
+ import "jquery.terminal";
3
+ import "jquery.terminal/css/jquery.terminal.css";
4
+ /**
5
+ * Wrapper class arround JQTerminal.
6
+ */
7
+ export default class Terminal {
8
+ constructor(element, kernel) {
9
+ // A buffer to handle multiline terminal input.
10
+ this._buffer = [];
11
+ // tell wether we are waiting for user input during
12
+ // program execution (Python's input())
13
+ this._inputMode = false;
14
+ // array of destroy callbacks
15
+ this._destroyCb = [];
16
+ this._elem = element;
17
+ this._kernel = kernel;
18
+ // just to be safe
19
+ this.reset_buffer();
20
+ this._term = $(element).terminal(this._interpreter.bind(this), {
21
+ greetings: this.banner(),
22
+ prompt: kernel.ps1(),
23
+ completionEscape: false,
24
+ completion: async (command, callback) => {
25
+ await kernel.ready();
26
+ return (await kernel.complete(command))?.[0] ?? [];
27
+ },
28
+ });
29
+ // this prevents JQuery Terminal to redraw terminal and remove
30
+ // script wrapping feature
31
+ // @ts-ignore
32
+ // term.resize = () => {};
33
+ /* Event connections */
34
+ const events = {
35
+ "eval.finished": (data) => {
36
+ if (data.interactive && "result" in data) {
37
+ this.echo(data.result["text/plain"] + "\n");
38
+ }
39
+ this._term.set_prompt(this._kernel.ps1());
40
+ this.wakeup();
41
+ },
42
+ "eval.output": (data) => {
43
+ switch (data.stream) {
44
+ case "stdout":
45
+ this.echo(data.content);
46
+ break;
47
+ case "stderr":
48
+ this.error(data.content);
49
+ break;
50
+ }
51
+ },
52
+ "eval.error": (data) => {
53
+ this._term.set_prompt(this._kernel.ps1());
54
+ this.wakeup();
55
+ },
56
+ "eval.input": (data) => {
57
+ this._inputMode = true;
58
+ const prompt = data?.content?.prompt;
59
+ if (prompt != null)
60
+ this.echo(prompt);
61
+ this._inputResolve = data.resolve;
62
+ this.wakeup();
63
+ this.focus();
64
+ },
65
+ };
66
+ for (const event in events) {
67
+ const callback = events[event];
68
+ this._kernel.addEventListener(event, callback);
69
+ // remember to remove listeners on destroy
70
+ this._destroyCb.push(() => this._kernel.removeEventListener(event, callback));
71
+ }
72
+ }
73
+ /**
74
+ * Mimic CPython's REPL banner.
75
+ */
76
+ banner() {
77
+ let banner = this._kernel.banner();
78
+ banner = banner.replace("on WebAssembly/Emscripten", "");
79
+ return banner;
80
+ }
81
+ /**
82
+ * Reset the multiline input buffer.
83
+ */
84
+ reset_buffer() {
85
+ this._buffer = [];
86
+ }
87
+ async _interpreter(command) {
88
+ // user answers to its program's input
89
+ if (this._inputMode) {
90
+ this._term.set_prompt("");
91
+ this._inputMode = false;
92
+ this.pause();
93
+ this._inputResolve?.(command);
94
+ return;
95
+ }
96
+ // user interacts with REPL
97
+ this._buffer.push(command);
98
+ const source = this._buffer.join("\n");
99
+ if (await this._kernel.more(source)) {
100
+ this._term.set_prompt(this._kernel.ps2());
101
+ }
102
+ else {
103
+ this._term.set_prompt(this._kernel.ps1());
104
+ this.reset_buffer();
105
+ this.launch(source, true);
106
+ }
107
+ }
108
+ /**
109
+ * Force waiting for input to end.
110
+ */
111
+ async _stopWaitInput() {
112
+ if (!this._inputMode)
113
+ return;
114
+ this._inputResolve?.();
115
+ this._inputMode = false;
116
+ }
117
+ /**
118
+ * Print a string in the terminal.
119
+ */
120
+ echo(message) {
121
+ // JQuery.terminal adds a trailing newline that is not easy
122
+ // to remove with the current API. Doing our best here.
123
+ message = message.toString();
124
+ // prompt can be polluted with previous echo
125
+ // _prompt_tail is the line part just before the prompt
126
+ const term = this._term;
127
+ let prompt_tail = this._prompt_tail ?? "";
128
+ let prompt = term.get_prompt();
129
+ if (prompt.startsWith(prompt_tail))
130
+ prompt = prompt.slice(prompt_tail.length);
131
+ else
132
+ prompt_tail = "";
133
+ // here, prompt and _prompt_tail should be correctly isolated
134
+ const lines = message.split("\n");
135
+ lines[0] = prompt_tail + lines[0];
136
+ prompt_tail = lines.pop() ?? "";
137
+ term.set_prompt(prompt_tail + prompt);
138
+ if (lines.length) {
139
+ // term.echo("") does not display blank line
140
+ // so we replace this call with term.echo('\b')
141
+ term.echo(lines.join("\n") || "\b");
142
+ }
143
+ this._prompt_tail = prompt_tail;
144
+ }
145
+ /**
146
+ * Print error in terminal (in red).
147
+ */
148
+ error(message) {
149
+ // JQuery.terminal adds a trailing newline that is not easy
150
+ // to remove with the current API. Doing our best here.
151
+ message = message.toString();
152
+ return this._term.error(message.replace(/\n+$/, ""));
153
+ }
154
+ /**
155
+ * Pause the terminal execution (usefull to wait for event or promise).
156
+ */
157
+ pause() {
158
+ this._term.pause();
159
+ }
160
+ /**
161
+ * Resume the terminal execution (see `pause`).
162
+ */
163
+ wakeup() {
164
+ this._term.resume();
165
+ }
166
+ /**
167
+ * Recover the terminal as it was at start.
168
+ */
169
+ clear() {
170
+ this._term.clear();
171
+ this.echo(this.banner() + "\n");
172
+ this.reset_buffer();
173
+ this._stopWaitInput();
174
+ }
175
+ /**
176
+ * Launch code in terminal.
177
+ */
178
+ launch(code, interactive = true) {
179
+ this._stopWaitInput();
180
+ if (!interactive)
181
+ this.showScript(code);
182
+ this.pause();
183
+ this._term.set_prompt("");
184
+ this._kernel.dispatchEvent("eval.request", {
185
+ code: code,
186
+ interactive: interactive,
187
+ });
188
+ }
189
+ /**
190
+ * Give focus to terminal.
191
+ */
192
+ focus() {
193
+ this._term.focus();
194
+ }
195
+ /**
196
+ * Show script in terminal with content as dropdown
197
+ * (should improve readability).
198
+ */
199
+ showScript(code) {
200
+ code = `# script executed\n${code}`;
201
+ let script = `${this._kernel.ps1()}`;
202
+ script += code.split("\n").join(`\n${this._kernel.ps2()}`);
203
+ script += "\n";
204
+ this.echo(script);
205
+ const output = this._elem.querySelector(".terminal-output")?.lastElementChild;
206
+ const header = output?.firstElementChild;
207
+ if (header == null)
208
+ return;
209
+ header.classList.add("exec-script-header");
210
+ for (const e of header.children)
211
+ e.style.backgroundColor = "inherit";
212
+ function hideShow() {
213
+ const converter = {
214
+ none: "block",
215
+ "": "none",
216
+ block: "none",
217
+ };
218
+ if (output == null)
219
+ return;
220
+ for (const e of output.children) {
221
+ if (e === header)
222
+ continue;
223
+ const style = e.style;
224
+ style.display = converter[style.display];
225
+ }
226
+ }
227
+ header.addEventListener("click", hideShow);
228
+ // remember to remove listeners on destroy
229
+ this._destroyCb.push(() => header.removeEventListener("click", hideShow));
230
+ hideShow();
231
+ }
232
+ /**
233
+ * Destroy all created by terminal.
234
+ */
235
+ destroy() {
236
+ this._destroyCb.forEach((cb) => cb());
237
+ this._destroyCb = [];
238
+ this._term.destroy();
239
+ this._term.empty();
240
+ this._term.off();
241
+ }
242
+ }
@@ -0,0 +1,7 @@
1
+ import { type MutableRefObject } from "react";
2
+ type AsyncEffectCallback = () => Promise<void>;
3
+ /**
4
+ * Custom hook to use async mount/unmount.
5
+ */
6
+ declare const useAsyncEffect: (onMount: AsyncEffectCallback, onUnmount?: AsyncEffectCallback, deps?: readonly unknown[], syncTo?: MutableRefObject<Promise<void>>) => MutableRefObject<Promise<void>>;
7
+ export { useAsyncEffect };
@@ -0,0 +1,17 @@
1
+ import { useRef, useEffect } from "react";
2
+ /**
3
+ * Custom hook to use async mount/unmount.
4
+ */
5
+ const useAsyncEffect = (onMount, onUnmount, deps, syncTo) => {
6
+ const promise = syncTo ?? useRef(Promise.resolve());
7
+ useEffect(() => {
8
+ promise.current = promise.current.then(onMount);
9
+ if (onUnmount == null)
10
+ return;
11
+ return () => {
12
+ promise.current = promise.current.then(onUnmount);
13
+ };
14
+ }, deps);
15
+ return promise;
16
+ };
17
+ export { useAsyncEffect };
@@ -0,0 +1,16 @@
1
+ import { FC, PropsWithChildren } from "react";
2
+ import { GUIBase } from "@basthon/gui-base";
3
+ declare const useGUI: () => GUIBase;
4
+ interface BasthonContextProps {
5
+ GUI?: typeof GUIBase;
6
+ rootPath?: string;
7
+ kernel?: string;
8
+ pyodideURLs?: string[];
9
+ noCheckpointsInit?: boolean;
10
+ }
11
+ /**
12
+ * This (pseudo) context is in charge of basthon
13
+ * kernel loading.
14
+ */
15
+ declare const BasthonContext: FC<PropsWithChildren<BasthonContextProps>>;
16
+ export { BasthonContext, useGUI, type BasthonContextProps };
@@ -0,0 +1,55 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext } from "react";
3
+ const GUIContext = createContext(null);
4
+ const useGUI = () => useContext(GUIContext);
5
+ let gui;
6
+ /**
7
+ * This (pseudo) context is in charge of basthon
8
+ * kernel loading.
9
+ */
10
+ const BasthonContext = (props) => {
11
+ if (props.GUI == null)
12
+ throw Error("Basthon GUI class not set");
13
+ const rootPath = props.rootPath ?? "assets";
14
+ const pyodideURLs = props.pyodideURLs;
15
+ const noCheckpointsInit = props.noCheckpointsInit ?? false;
16
+ let kernel = props.kernel;
17
+ // set kernel
18
+ if (kernel == null) {
19
+ const url = new URL(window.location.href);
20
+ const params = url.searchParams;
21
+ kernel = params.get("kernel")?.toLowerCase() ?? "python3";
22
+ }
23
+ // set legacy
24
+ let legacy = false;
25
+ if (kernel.endsWith("-legacy")) {
26
+ kernel = kernel.slice(0, -"-legacy".length);
27
+ legacy = true;
28
+ }
29
+ // set language
30
+ kernel =
31
+ {
32
+ // shortcuts
33
+ py: "python3",
34
+ python: "python3",
35
+ "python3-old": "python3", // for backward compatibility
36
+ js: "javascript",
37
+ }[kernel] ?? kernel;
38
+ const language = kernel;
39
+ // transition phase: we force legacy mode for pyhton3 kernel
40
+ legacy = legacy === true || language === "python3";
41
+ if (gui == null) {
42
+ gui = new props.GUI({
43
+ kernelOptions: {
44
+ rootPath,
45
+ language,
46
+ legacy,
47
+ pyodideURLs,
48
+ },
49
+ noCheckpointsInit,
50
+ });
51
+ gui.init();
52
+ }
53
+ return (_jsx(GUIContext.Provider, { value: gui, children: props.children }));
54
+ };
55
+ export { BasthonContext, useGUI };
@@ -0,0 +1,4 @@
1
+ import { type Dispatch, type Action } from "../state-manager";
2
+ declare const DispatchContext: import("react").Context<Dispatch<Action> | null>;
3
+ declare const useDispatch: () => Dispatch<Action>;
4
+ export { DispatchContext, useDispatch };
@@ -0,0 +1,4 @@
1
+ import { createContext, useContext } from "react";
2
+ const DispatchContext = createContext(null);
3
+ const useDispatch = () => useContext(DispatchContext);
4
+ export { DispatchContext, useDispatch };
@@ -0,0 +1,3 @@
1
+ import GUI from "../gui/gui";
2
+ declare const useGUI: () => GUI;
3
+ export { useGUI };
@@ -0,0 +1,3 @@
1
+ import { useGUI as useGUIBase } from "./basthon";
2
+ const useGUI = () => useGUIBase();
3
+ export { useGUI };
package/lib/index.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import ConsoleApp from "./components/app";
2
+ export { ConsoleApp };
package/lib/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import ConsoleApp from "./components/app";
2
+ export { ConsoleApp };
@@ -0,0 +1,38 @@
1
+ import { type Dispatch } from "react";
2
+ type Language = "python3" | "sql" | "javascript" | "ocaml" | "xcas";
3
+ type Theme = "light" | "dark";
4
+ type ViewMode = "default" | "reversed" | "terminal-only" | "script-only";
5
+ type RightPanel = "terminal" | "graphics";
6
+ interface State {
7
+ ready: boolean;
8
+ language: Language;
9
+ theme: Theme;
10
+ viewMode: ViewMode;
11
+ rightPanel: RightPanel;
12
+ graphicsDirty: boolean;
13
+ editorContent: string;
14
+ }
15
+ interface Action {
16
+ type: "init" | "set-theme" | "switch-theme" | "set-view-mode" | "switch-view" | "set-right-panel" | "graphics-dirty" | "set-editor-content";
17
+ payload?: Theme | ViewMode | RightPanel | State | string;
18
+ }
19
+ declare const stateManager: {
20
+ useReducer: (language: Language) => readonly [{
21
+ language: Language;
22
+ ready: boolean;
23
+ theme: Theme;
24
+ viewMode: ViewMode;
25
+ rightPanel: RightPanel;
26
+ graphicsDirty: boolean;
27
+ editorContent: string;
28
+ }, Dispatch<Action>];
29
+ ready: (state: State) => boolean;
30
+ language: (state: State) => Language;
31
+ theme: (state: State) => Theme;
32
+ viewMode: (state: State) => ViewMode;
33
+ rightPanel: (state: State) => RightPanel;
34
+ graphicsDirty: (state: State) => boolean;
35
+ editorContent: (state: State) => string;
36
+ };
37
+ export { type Dispatch, type Action, type Language, type Theme, type ViewMode, type RightPanel, };
38
+ export default stateManager;
@@ -0,0 +1,101 @@
1
+ import { useEffect } from "react";
2
+ /* we use immer to fake mutable state */
3
+ import { useImmerReducer } from "use-immer";
4
+ import { Storage } from "@basthon/checkpoints";
5
+ /* this initial state */
6
+ const initialState = {
7
+ ready: false,
8
+ language: "python3",
9
+ theme: "dark",
10
+ viewMode: "default",
11
+ rightPanel: "terminal",
12
+ graphicsDirty: false,
13
+ editorContent: "",
14
+ };
15
+ /* copy throught JSON, should be sufficient for what we do */
16
+ function copy(obj) {
17
+ return JSON.parse(JSON.stringify(obj));
18
+ }
19
+ /* states to store */
20
+ const storedStates = ["theme", "viewMode", "rightPanel", "editorContent"];
21
+ /* get state from storage */
22
+ const stateFromStorage = async (language) => {
23
+ const storage = new Storage(`console.${language}.states`);
24
+ await storage.ready();
25
+ const state = { ...initialState, language };
26
+ for (let key of storedStates) {
27
+ const storageValue = await storage.get(key);
28
+ if (storageValue != null)
29
+ state[key] = storageValue;
30
+ }
31
+ return state;
32
+ };
33
+ /* save state to storage */
34
+ const stateToStorage = async (state) => {
35
+ const storage = new Storage(`console.${state.language}.states`);
36
+ await storage.ready();
37
+ for (let key of storedStates)
38
+ await storage.set(key, state[key]);
39
+ };
40
+ /* where our global logic takes place */
41
+ const reducer = (state, action) => {
42
+ const { type, payload } = action;
43
+ switch (type) {
44
+ case "init": {
45
+ const state = copy(payload);
46
+ state.ready = true;
47
+ return state;
48
+ }
49
+ case "set-theme":
50
+ state.theme = payload;
51
+ break;
52
+ case "switch-theme":
53
+ state.theme = state.theme === "light" ? "dark" : "light";
54
+ break;
55
+ case "switch-view":
56
+ state.viewMode = state.viewMode === "default" ? "reversed" : "default";
57
+ break;
58
+ case "set-view-mode":
59
+ state.viewMode = payload;
60
+ break;
61
+ case "set-right-panel":
62
+ state.rightPanel = payload;
63
+ if (state.rightPanel === "graphics")
64
+ state.graphicsDirty = false;
65
+ break;
66
+ case "graphics-dirty":
67
+ if (state.rightPanel !== "graphics")
68
+ state.graphicsDirty = true;
69
+ break;
70
+ case "set-editor-content":
71
+ state.editorContent = payload;
72
+ break;
73
+ default:
74
+ throw Error(`Internal error: unrecognized action type: '${type}'!`);
75
+ }
76
+ stateToStorage(copy(state));
77
+ };
78
+ /* stateManager exposed as API */
79
+ const stateManager = {
80
+ useReducer: (language) => {
81
+ const [state, dispatch] = useImmerReducer(reducer, {
82
+ ...initialState,
83
+ language,
84
+ });
85
+ // dispatch init from storage
86
+ useEffect(() => {
87
+ stateFromStorage(language).then((payload) => {
88
+ dispatch({ type: "init", payload });
89
+ });
90
+ }, []);
91
+ return [state, dispatch];
92
+ },
93
+ ready: (state) => state.ready,
94
+ language: (state) => state.language,
95
+ theme: (state) => state.theme,
96
+ viewMode: (state) => state.viewMode,
97
+ rightPanel: (state) => state.rightPanel,
98
+ graphicsDirty: (state) => state.graphicsDirty,
99
+ editorContent: (state) => state.editorContent,
100
+ };
101
+ export default stateManager;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@basthon/console-react",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Basthon - Console React Component",
5
5
  "homepage": "https://basthon.fr",
6
6
  "bugs": {