@basthon/console-react 0.5.6 → 0.6.8
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.
- package/lib/components/app.d.ts +6 -1
- package/lib/components/app.js +11 -5
- package/lib/components/button.js +6 -3
- package/lib/components/main-container.d.ts +8 -2
- package/lib/components/main-container.js +8 -3
- package/lib/components/styles.module.scss +33 -2
- package/lib/gui/editor.d.ts +1 -0
- package/lib/gui/editor.js +18 -3
- package/lib/gui/gui.d.ts +23 -0
- package/lib/gui/gui.js +44 -6
- package/lib/gui/terminal.d.ts +8 -2
- package/lib/gui/terminal.js +42 -8
- package/lib/hooks/state.d.ts +4 -0
- package/lib/hooks/state.js +4 -0
- package/lib/index.d.ts +9 -2
- package/lib/index.js +7 -1
- package/lib/state-manager.d.ts +6 -2
- package/lib/state-manager.js +15 -7
- package/package.json +4 -6
- package/patches/jquery.terminal-npm-2.45.2-065beff1cb.patch +0 -13
package/lib/components/app.d.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import { FC, PropsWithChildren } from "react";
|
|
1
|
+
import { FC, PropsWithChildren, type ReactNode } from "react";
|
|
2
2
|
import { type BasthonContextProps } from "@basthon/gui-react";
|
|
3
3
|
import { type Theme } from "../state-manager";
|
|
4
|
+
import { type PanelTitles } from "./main-container";
|
|
4
5
|
interface InnerAppProps {
|
|
5
6
|
initFromStorage?: boolean;
|
|
7
|
+
storedStates?: string[];
|
|
6
8
|
forceTheme?: Theme;
|
|
7
9
|
compactHeader?: boolean;
|
|
10
|
+
header?: ReactNode;
|
|
11
|
+
panelTitles?: PanelTitles;
|
|
8
12
|
onChangeEditor?: () => void;
|
|
9
13
|
}
|
|
10
14
|
interface AppProps extends BasthonContextProps, InnerAppProps {
|
|
@@ -13,4 +17,5 @@ interface AppProps extends BasthonContextProps, InnerAppProps {
|
|
|
13
17
|
* Main app setting up state manager, header, main container and footer.
|
|
14
18
|
*/
|
|
15
19
|
declare const ConsoleApp: FC<PropsWithChildren<AppProps>>;
|
|
20
|
+
export { type AppProps };
|
|
16
21
|
export default ConsoleApp;
|
package/lib/components/app.js
CHANGED
|
@@ -5,36 +5,42 @@ import GUI from "../gui/gui";
|
|
|
5
5
|
import { useGUI } from "../hooks/gui";
|
|
6
6
|
import stateManager from "../state-manager";
|
|
7
7
|
import { DispatchContext } from "../hooks/dispatch";
|
|
8
|
+
import { StateContext } from "../hooks/state";
|
|
8
9
|
import { Header } from "./header";
|
|
9
10
|
import { MainContainer } from "./main-container";
|
|
10
11
|
import { Footer } from "./footer";
|
|
11
12
|
import * as styles from "./styles.module.scss";
|
|
12
|
-
const InnerApp = ({ initFromStorage, forceTheme, compactHeader, onChangeEditor, children, }) => {
|
|
13
|
+
const InnerApp = ({ initFromStorage, storedStates, forceTheme, compactHeader, header, panelTitles, onChangeEditor, children, }) => {
|
|
13
14
|
const gui = useGUI();
|
|
14
15
|
// state
|
|
15
|
-
const [state, dispatch] = stateManager.useReducer(gui.language, initFromStorage ?? true);
|
|
16
|
+
const [state, dispatch] = stateManager.useReducer(gui.language, initFromStorage ?? true, storedStates);
|
|
16
17
|
const language = stateManager.language(state);
|
|
17
18
|
const theme = forceTheme != null ? forceTheme : stateManager.theme(state);
|
|
18
19
|
// set data-theme for basthon-loader footer
|
|
19
20
|
// FIXME: remove this when basthon-loader is a plain react component
|
|
20
21
|
useEffect(() => {
|
|
22
|
+
gui.setTheme(theme);
|
|
21
23
|
document.body.setAttribute("data-theme", theme);
|
|
22
24
|
return () => {
|
|
23
25
|
document.body.removeAttribute("data-theme");
|
|
24
26
|
};
|
|
25
27
|
}, [theme]);
|
|
26
28
|
// build component
|
|
27
|
-
return (
|
|
29
|
+
return (_jsx(DispatchContext.Provider, { value: dispatch, children: _jsxs(StateContext.Provider, { value: { ...state, theme }, children: [children, _jsxs("div", { className: styles.root, children: [header !== undefined ? (header) : (_jsx(Header, { language: language, theme: theme, compact: compactHeader })), stateManager.ready(state) && (_jsx(MainContainer, { language: language, theme: theme, viewMode: stateManager.viewMode(state), rightPanel: stateManager.rightPanel(state), graphicsDirty: stateManager.graphicsDirty(state), editorContent: stateManager.editorContent(state), onChangeEditor: onChangeEditor, compactHeader: compactHeader, panelTitles: panelTitles })), _jsx(Footer, { theme: theme })] })] }) }));
|
|
28
30
|
};
|
|
29
31
|
/**
|
|
30
32
|
* Main app setting up state manager, header, main container and footer.
|
|
31
33
|
*/
|
|
32
|
-
const ConsoleApp = ({ initFromStorage, forceTheme, compactHeader, onChangeEditor, children, ...props }) => {
|
|
33
|
-
|
|
34
|
+
const ConsoleApp = ({ initFromStorage, storedStates, forceTheme, compactHeader, header, panelTitles, onChangeEditor, children, ...props }) => {
|
|
35
|
+
// custom GUI classes should extend the console's GUI
|
|
36
|
+
props.GUI ?? (props.GUI = GUI);
|
|
34
37
|
const innerProps = {
|
|
35
38
|
initFromStorage,
|
|
39
|
+
storedStates,
|
|
36
40
|
forceTheme,
|
|
37
41
|
compactHeader,
|
|
42
|
+
header,
|
|
43
|
+
panelTitles,
|
|
38
44
|
onChangeEditor,
|
|
39
45
|
};
|
|
40
46
|
return (_jsx(BasthonContext, { ...props, children: _jsx(InnerApp, { ...innerProps, children: children }) }));
|
package/lib/components/button.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import * as styles from "./styles.module.scss";
|
|
3
|
-
const Button = ({ children, titled, blink, ...props }) => {
|
|
4
|
-
|
|
3
|
+
const Button = ({ children, titled, blink, className: extraClassName, ...props }) => {
|
|
4
|
+
const classNames = [styles.btn];
|
|
5
5
|
if (blink === true)
|
|
6
|
-
|
|
6
|
+
classNames.push(styles["btn-eye-catcher"]);
|
|
7
|
+
if (extraClassName != null)
|
|
8
|
+
classNames.push(extraClassName);
|
|
9
|
+
const className = classNames.join(" ");
|
|
7
10
|
return (_jsx("button", { className: className, "data-titled": titled, ...props, children: children }));
|
|
8
11
|
};
|
|
9
12
|
export { Button };
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import { FC } from "react";
|
|
1
|
+
import { FC, type ReactNode } from "react";
|
|
2
2
|
import { type Language, type Theme, type ViewMode, type RightPanel } from "../state-manager";
|
|
3
|
+
interface PanelTitles {
|
|
4
|
+
editor?: ReactNode;
|
|
5
|
+
terminal?: ReactNode;
|
|
6
|
+
graphics?: ReactNode;
|
|
7
|
+
}
|
|
3
8
|
interface MainContainerProps {
|
|
4
9
|
language: Language;
|
|
5
10
|
theme: Theme;
|
|
@@ -9,6 +14,7 @@ interface MainContainerProps {
|
|
|
9
14
|
editorContent: string;
|
|
10
15
|
onChangeEditor?: () => void;
|
|
11
16
|
compactHeader?: boolean;
|
|
17
|
+
panelTitles?: PanelTitles;
|
|
12
18
|
}
|
|
13
19
|
declare const MainContainer: FC<MainContainerProps>;
|
|
14
|
-
export { MainContainer };
|
|
20
|
+
export { MainContainer, type PanelTitles };
|
|
@@ -11,13 +11,18 @@ import { faCogs, faFolderOpen, faDownload, faShareAltSquare, faEyeSlash, faUndoA
|
|
|
11
11
|
const MainContainer = (props) => {
|
|
12
12
|
const gui = useGUI();
|
|
13
13
|
const dispatch = useDispatch();
|
|
14
|
-
|
|
14
|
+
const titles = props.panelTitles;
|
|
15
|
+
const rightTitle = props.rightPanel === "graphics" ? titles?.graphics : titles?.terminal;
|
|
16
|
+
return (_jsxs("div", { className: styles.container, "data-theme": props.theme, "data-compact-header": props.compactHeader, "data-titled": titles != null, children: [_jsxs("div", { className: styles["left-panel"], "data-view-mode": props.viewMode, children: [titles?.editor != null && (_jsx("div", { className: styles["panel-title"], children: titles.editor })), _jsx(Editor, { id: "editor", language: props.language, theme: props.theme, content: props.editorContent, onSetContent: (content) => dispatch({ type: "set-editor-content", payload: content }), onChange: () => {
|
|
17
|
+
gui.editorChanged();
|
|
18
|
+
props.onChangeEditor?.();
|
|
19
|
+
} }), _jsxs("div", { className: styles["buttons-container"], children: [_jsxs(Button, { titled: true, id: "run", title: "Ex\u00E9cuter le script", onClick: () => gui.runScript(), children: [_jsx(FontAwesomeIcon, { icon: faCogs, className: styles["btn-logo"] }), "Ex\u00E9cuter"] }), _jsx(Button, { id: "open", title: "Ouvrir un script, charger un module ou un fichier", onClick: () => gui.openFile(), children: _jsx(FontAwesomeIcon, { icon: faFolderOpen, className: styles["btn-logo"] }) }), _jsx(Button, { id: "download", title: "T\u00E9l\u00E9charger le script", onClick: () => gui.download(), children: _jsx(FontAwesomeIcon, { icon: faDownload, className: styles["btn-logo"] }) }), _jsx(Button, { id: "share", title: "Partager ce code", onClick: () => gui.share(), children: _jsx(FontAwesomeIcon, { icon: faShareAltSquare, className: styles["btn-logo"] }) }), props.viewMode === "script-only" && (_jsx(Button, { id: "hide-editor", title: "Afficher seulement la console", onClick: () => dispatch({
|
|
15
20
|
type: "set-view-mode",
|
|
16
21
|
payload: "terminal-only",
|
|
17
|
-
}), children: _jsx(FontAwesomeIcon, { icon: faEyeSlash, className: styles["btn-logo"] }) })), _jsx(Button, { title: "Revenir \u00E0 une version pr\u00E9c\u00E9dente du script", onClick: () => gui.rollback(), children: _jsx(FontAwesomeIcon, { icon: faUndoAlt, className: styles["btn-logo"] }) })] })] }), _jsxs("div", { className: styles["right-panel"], "data-view-mode": props.viewMode, children: [_jsx(Terminal, { "data-right-panel": props.rightPanel }), _jsx(Graphics, { "data-right-panel": props.rightPanel, onContentUpdate: () => dispatch({ type: "graphics-dirty" }) }), _jsxs("div", { className: styles["buttons-container"], children: [_jsx(Button, { title: "Red\u00E9marrer le noyau", onClick: () => gui.restart(), children: _jsx(FontAwesomeIcon, { icon: faBroom, className: styles["btn-logo"] }) }), _jsx(Button, { title: "Afficher la console", onClick: () => dispatch({ type: "set-right-panel", payload: "terminal" }), children: _jsx(FontAwesomeIcon, { icon: faTerminal, className: styles["btn-logo"] }) }), _jsx(Button, { title: "Afficher la vue graphique", onClick: () => dispatch({ type: "set-right-panel", payload: "graphics" }), blink: props.graphicsDirty, children: _jsx(FontAwesomeIcon, { icon: faImage, className: styles["btn-logo"] }) }), _jsx(Button, { title: "Changer le th\u00E8me (sombre/lumineux)", onClick: () => dispatch({ type: "switch-theme" }), children: _jsx(FontAwesomeIcon, { icon: faAdjust, className: styles["btn-logo"] }) }), ["default", "reversed"].includes(props.viewMode) && (_jsxs(_Fragment, { children: [_jsx(Button, { title: "\u00C9changer l'\u00E9diteur et la console", onClick: () => dispatch({ type: "switch-view" }), children: _jsx(FontAwesomeIcon, { icon: faExchangeAlt, className: styles["btn-logo"] }) }), _jsx(Button, { title: "Afficher seulement l'\u00E9diteur", onClick: () => dispatch({
|
|
22
|
+
}), children: _jsx(FontAwesomeIcon, { icon: faEyeSlash, className: styles["btn-logo"] }) })), _jsx(Button, { id: "rollback", title: "Revenir \u00E0 une version pr\u00E9c\u00E9dente du script", onClick: () => gui.rollback(), children: _jsx(FontAwesomeIcon, { icon: faUndoAlt, className: styles["btn-logo"] }) })] })] }), _jsxs("div", { className: styles["right-panel"], "data-view-mode": props.viewMode, children: [rightTitle != null && (_jsx("div", { className: styles["panel-title"], children: rightTitle })), _jsx(Terminal, { id: "shell", "data-right-panel": props.rightPanel }), _jsx(Graphics, { id: "graphics", "data-right-panel": props.rightPanel, onContentUpdate: () => dispatch({ type: "graphics-dirty" }) }), _jsxs("div", { className: styles["buttons-container"], children: [_jsx(Button, { id: "raz", title: "Red\u00E9marrer le noyau", onClick: () => gui.restart(), children: _jsx(FontAwesomeIcon, { icon: faBroom, className: styles["btn-logo"] }) }), _jsx(Button, { id: "btn_shell", title: "Afficher la console", onClick: () => dispatch({ type: "set-right-panel", payload: "terminal" }), children: _jsx(FontAwesomeIcon, { icon: faTerminal, className: styles["btn-logo"] }) }), _jsx(Button, { id: "btn_graph_view", title: "Afficher la vue graphique", onClick: () => dispatch({ type: "set-right-panel", payload: "graphics" }), blink: props.graphicsDirty, children: _jsx(FontAwesomeIcon, { icon: faImage, className: styles["btn-logo"] }) }), _jsx(Button, { id: "darklight", title: "Changer le th\u00E8me (sombre/lumineux)", onClick: () => dispatch({ type: "switch-theme" }), children: _jsx(FontAwesomeIcon, { icon: faAdjust, className: styles["btn-logo"] }) }), ["default", "reversed"].includes(props.viewMode) && (_jsxs(_Fragment, { children: [_jsx(Button, { id: "switch", title: "\u00C9changer l'\u00E9diteur et la console", onClick: () => dispatch({ type: "switch-view" }), children: _jsx(FontAwesomeIcon, { icon: faExchangeAlt, className: styles["btn-logo"] }) }), _jsx(Button, { id: "hide-console", title: "Afficher seulement l'\u00E9diteur", onClick: () => dispatch({
|
|
18
23
|
type: "set-view-mode",
|
|
19
24
|
payload: "script-only",
|
|
20
|
-
}), children: _jsx(FontAwesomeIcon, { icon: faEyeSlash, className: styles["btn-logo"] }) })] })), props.viewMode === "terminal-only" && (_jsx(Button, { title: "Afficher l'\u00E9diteur et la console", onClick: () => dispatch({
|
|
25
|
+
}), children: _jsx(FontAwesomeIcon, { icon: faEyeSlash, className: styles["btn-logo"] }) })] })), props.viewMode === "terminal-only" && (_jsx(Button, { id: "show-editor-console", title: "Afficher l'\u00E9diteur et la console", onClick: () => dispatch({
|
|
21
26
|
type: "set-view-mode",
|
|
22
27
|
payload: "default",
|
|
23
28
|
}), children: _jsx(FontAwesomeIcon, { icon: faColumns, className: styles["btn-logo"] }) }))] })] })] }));
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
--logo-height: 12vh;
|
|
12
12
|
--term-height: 60vh;
|
|
13
13
|
--term-font-size: 12pt;
|
|
14
|
-
--term-font:
|
|
15
|
-
monospace;
|
|
14
|
+
--term-font:
|
|
15
|
+
"Monaco", "Menlo", "Ubuntu Mono", "Consolas", "source-code-pro", monospace;
|
|
16
16
|
--term-script-color: var(--python-grey);
|
|
17
17
|
--main-container-width: 90%;
|
|
18
18
|
--main-container-height: 75vh;
|
|
@@ -114,6 +114,7 @@ textarea:focus {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
.left-panel {
|
|
117
|
+
position: relative;
|
|
117
118
|
width: var(--left-container-width);
|
|
118
119
|
margin: auto;
|
|
119
120
|
float: left;
|
|
@@ -151,6 +152,36 @@ textarea:focus {
|
|
|
151
152
|
}
|
|
152
153
|
}
|
|
153
154
|
|
|
155
|
+
.panel-title {
|
|
156
|
+
z-index: 100;
|
|
157
|
+
position: absolute;
|
|
158
|
+
top: -1.3vw;
|
|
159
|
+
left: 1.5vw;
|
|
160
|
+
margin: 0;
|
|
161
|
+
padding: 0vw 0.7vw 0.2vw 0.7vw;
|
|
162
|
+
color: #fff;
|
|
163
|
+
background-color: var(--python-blue);
|
|
164
|
+
border-radius: var(--corner-radius);
|
|
165
|
+
border: none;
|
|
166
|
+
font-size: var(--main-font-size);
|
|
167
|
+
user-select: none;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* leave room for panel titles */
|
|
171
|
+
.container[data-titled="true"] {
|
|
172
|
+
margin-top: 1.3vw;
|
|
173
|
+
|
|
174
|
+
.editor {
|
|
175
|
+
box-sizing: border-box;
|
|
176
|
+
border-top: 1vw solid var(--term-bg-color);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.terminal {
|
|
180
|
+
box-sizing: border-box;
|
|
181
|
+
padding-top: 0.7vw;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
154
185
|
%darklighted {
|
|
155
186
|
background-color: var(--term-bg-color);
|
|
156
187
|
--background: var(--term-bg-color);
|
package/lib/gui/editor.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export default class Editor {
|
|
|
7
7
|
private _kernel;
|
|
8
8
|
private _editor;
|
|
9
9
|
private _onSetContent?;
|
|
10
|
+
private _silent;
|
|
10
11
|
constructor(element: HTMLElement, kernel: KernelBase, onSetContent?: (_: string) => void, onChange?: () => void);
|
|
11
12
|
/**
|
|
12
13
|
* Set Ace mode from language.
|
package/lib/gui/editor.js
CHANGED
|
@@ -4,6 +4,8 @@ import ace from "./ace";
|
|
|
4
4
|
*/
|
|
5
5
|
export default class Editor {
|
|
6
6
|
constructor(element, kernel, onSetContent, onChange) {
|
|
7
|
+
// true while content is set programmatically (no onChange call)
|
|
8
|
+
this._silent = false;
|
|
7
9
|
this._kernel = kernel;
|
|
8
10
|
this._onSetContent = onSetContent;
|
|
9
11
|
this._editor = ace.edit(element);
|
|
@@ -14,9 +16,12 @@ export default class Editor {
|
|
|
14
16
|
highlightSelectedWord: true,
|
|
15
17
|
fontSize: "12pt",
|
|
16
18
|
});
|
|
17
|
-
// connect onchange
|
|
19
|
+
// connect onchange (only for user changes)
|
|
18
20
|
if (onChange != null)
|
|
19
|
-
this._editor.session.on("change", () =>
|
|
21
|
+
this._editor.session.on("change", () => {
|
|
22
|
+
if (!this._silent)
|
|
23
|
+
onChange();
|
|
24
|
+
});
|
|
20
25
|
/* editor's completion */
|
|
21
26
|
// removing all completers and keeping track of local completer
|
|
22
27
|
const localCompleter = this._editor.completers[1]; // any hint to detect this dynamically?
|
|
@@ -85,14 +90,24 @@ export default class Editor {
|
|
|
85
90
|
* Dispatch action to set content.
|
|
86
91
|
*/
|
|
87
92
|
setContent(content) {
|
|
93
|
+
// set it directly since state may not change
|
|
94
|
+
// (user modifications are not dispatched)
|
|
95
|
+
this.setContentNoDispatch(content);
|
|
88
96
|
this._onSetContent?.(content);
|
|
89
97
|
}
|
|
90
98
|
/**
|
|
91
99
|
* Set Ace editor content.
|
|
92
100
|
*/
|
|
93
101
|
setContentNoDispatch(content) {
|
|
94
|
-
if (content
|
|
102
|
+
if (content == null)
|
|
103
|
+
return;
|
|
104
|
+
this._silent = true;
|
|
105
|
+
try {
|
|
95
106
|
this._editor.setValue(content, -1);
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
this._silent = false;
|
|
110
|
+
}
|
|
96
111
|
}
|
|
97
112
|
/**
|
|
98
113
|
* Get editor's content
|
package/lib/gui/gui.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export default class GUI extends GUIBase {
|
|
|
8
8
|
private _editor?;
|
|
9
9
|
private _term?;
|
|
10
10
|
private _graphics?;
|
|
11
|
+
private _pendingContent?;
|
|
12
|
+
private _theme?;
|
|
11
13
|
constructor(options: GUIOptions);
|
|
12
14
|
/**
|
|
13
15
|
* Set editor.
|
|
@@ -41,6 +43,14 @@ export default class GUI extends GUIBase {
|
|
|
41
43
|
* Set editor's content.
|
|
42
44
|
*/
|
|
43
45
|
setContent(content: string): void;
|
|
46
|
+
/**
|
|
47
|
+
* Set the current theme (called by the app on theme change).
|
|
48
|
+
*/
|
|
49
|
+
setTheme(theme: "dark" | "light"): void;
|
|
50
|
+
/**
|
|
51
|
+
* Get the current theme.
|
|
52
|
+
*/
|
|
53
|
+
theme(): Promise<"dark" | "light" | undefined>;
|
|
44
54
|
/**
|
|
45
55
|
* Notify the user with an error.
|
|
46
56
|
*/
|
|
@@ -65,6 +75,19 @@ export default class GUI extends GUIBase {
|
|
|
65
75
|
*/
|
|
66
76
|
rollback(): Promise<void>;
|
|
67
77
|
protected setupUI(options: any): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Called when the editor's content is modified by the user.
|
|
80
|
+
*/
|
|
81
|
+
editorChanged(): void;
|
|
82
|
+
/**
|
|
83
|
+
* Called on ctrl+s (download the script by default).
|
|
84
|
+
*/
|
|
85
|
+
protected onSaveShortcut(): void;
|
|
86
|
+
/**
|
|
87
|
+
* Called before closing the page (backup by default).
|
|
88
|
+
* Return a string to ask the user for confirmation.
|
|
89
|
+
*/
|
|
90
|
+
protected onBeforeUnload(): string | undefined;
|
|
68
91
|
/**
|
|
69
92
|
* Run the editor script in the terminal.
|
|
70
93
|
*/
|
package/lib/gui/gui.js
CHANGED
|
@@ -22,6 +22,10 @@ export default class GUI extends GUIBase {
|
|
|
22
22
|
*/
|
|
23
23
|
setEditor(editor) {
|
|
24
24
|
this._editor = editor;
|
|
25
|
+
if (this._pendingContent != null) {
|
|
26
|
+
editor.setContent(this._pendingContent);
|
|
27
|
+
this._pendingContent = undefined;
|
|
28
|
+
}
|
|
25
29
|
}
|
|
26
30
|
/**
|
|
27
31
|
* Set terminal.
|
|
@@ -58,13 +62,31 @@ export default class GUI extends GUIBase {
|
|
|
58
62
|
* Get editor's content.
|
|
59
63
|
*/
|
|
60
64
|
content() {
|
|
61
|
-
|
|
65
|
+
if (this._editor == null)
|
|
66
|
+
return this._pendingContent;
|
|
67
|
+
return this._editor.getContent();
|
|
62
68
|
}
|
|
63
69
|
/**
|
|
64
70
|
* Set editor's content.
|
|
65
71
|
*/
|
|
66
72
|
setContent(content) {
|
|
67
|
-
|
|
73
|
+
// editor not yet available, delay
|
|
74
|
+
if (this._editor == null)
|
|
75
|
+
this._pendingContent = content;
|
|
76
|
+
else
|
|
77
|
+
this._editor.setContent(content);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Set the current theme (called by the app on theme change).
|
|
81
|
+
*/
|
|
82
|
+
setTheme(theme) {
|
|
83
|
+
this._theme = theme;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Get the current theme.
|
|
87
|
+
*/
|
|
88
|
+
async theme() {
|
|
89
|
+
return this._theme;
|
|
68
90
|
}
|
|
69
91
|
/**
|
|
70
92
|
* Notify the user with an error.
|
|
@@ -135,13 +157,29 @@ export default class GUI extends GUIBase {
|
|
|
135
157
|
if ((event.ctrlKey || event.metaKey) &&
|
|
136
158
|
String.fromCharCode(event.which).toLowerCase() === "s") {
|
|
137
159
|
event.preventDefault();
|
|
138
|
-
this.
|
|
160
|
+
this.onSaveShortcut();
|
|
139
161
|
}
|
|
140
162
|
});
|
|
141
163
|
/* backup before closing */
|
|
142
|
-
window.onbeforeunload = () =>
|
|
143
|
-
|
|
144
|
-
|
|
164
|
+
window.onbeforeunload = () => this.onBeforeUnload();
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Called when the editor's content is modified by the user.
|
|
168
|
+
*/
|
|
169
|
+
editorChanged() { }
|
|
170
|
+
/**
|
|
171
|
+
* Called on ctrl+s (download the script by default).
|
|
172
|
+
*/
|
|
173
|
+
onSaveShortcut() {
|
|
174
|
+
this.download();
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Called before closing the page (backup by default).
|
|
178
|
+
* Return a string to ask the user for confirmation.
|
|
179
|
+
*/
|
|
180
|
+
onBeforeUnload() {
|
|
181
|
+
this.backup().catch(this.notifyError.bind(this));
|
|
182
|
+
return undefined;
|
|
145
183
|
}
|
|
146
184
|
/**
|
|
147
185
|
* Run the editor script in the terminal.
|
package/lib/gui/terminal.d.ts
CHANGED
|
@@ -27,14 +27,20 @@ export default class Terminal {
|
|
|
27
27
|
* Force waiting for input to end.
|
|
28
28
|
*/
|
|
29
29
|
private _stopWaitInput;
|
|
30
|
+
/**
|
|
31
|
+
* Mask user input and keep it out of the history
|
|
32
|
+
* (password input).
|
|
33
|
+
*/
|
|
34
|
+
private _setPasswordMode;
|
|
30
35
|
/**
|
|
31
36
|
* Print a string in the terminal.
|
|
37
|
+
* If `className` is provided, the text is wrapped in a span with this class.
|
|
32
38
|
*/
|
|
33
|
-
echo(message: string): void;
|
|
39
|
+
echo(message: string, className?: string): void;
|
|
34
40
|
/**
|
|
35
41
|
* Print error in terminal (in red).
|
|
36
42
|
*/
|
|
37
|
-
error(message: string):
|
|
43
|
+
error(message: string): void;
|
|
38
44
|
/**
|
|
39
45
|
* Pause the terminal execution (usefull to wait for event or promise).
|
|
40
46
|
*/
|
package/lib/gui/terminal.js
CHANGED
|
@@ -25,6 +25,21 @@ export default class Terminal {
|
|
|
25
25
|
await kernel.ready();
|
|
26
26
|
return (await kernel.complete(command))?.[0] ?? [];
|
|
27
27
|
},
|
|
28
|
+
keymap: {
|
|
29
|
+
// JQuery.terminal's CTRL+C copies echoed commands twice (they share
|
|
30
|
+
// role="presentation" with the command line) so we copy the
|
|
31
|
+
// selected text ourselves (like right click > copy)
|
|
32
|
+
"CTRL+C": (e, original) => {
|
|
33
|
+
const text = window.getSelection()?.toString();
|
|
34
|
+
if (!text)
|
|
35
|
+
return original(e);
|
|
36
|
+
// without clipboard API, fallback to browser's native copy
|
|
37
|
+
if (navigator.clipboard == null)
|
|
38
|
+
return true;
|
|
39
|
+
navigator.clipboard.writeText(text.replace(/\xA0/g, " "));
|
|
40
|
+
return false;
|
|
41
|
+
},
|
|
42
|
+
},
|
|
28
43
|
});
|
|
29
44
|
// this prevents JQuery Terminal to redraw terminal and remove
|
|
30
45
|
// script wrapping feature
|
|
@@ -55,6 +70,7 @@ export default class Terminal {
|
|
|
55
70
|
},
|
|
56
71
|
"eval.input": (data) => {
|
|
57
72
|
this._inputMode = true;
|
|
73
|
+
this._setPasswordMode(data?.content?.password === true);
|
|
58
74
|
const prompt = data?.content?.prompt;
|
|
59
75
|
if (prompt != null)
|
|
60
76
|
this.echo(prompt);
|
|
@@ -90,6 +106,7 @@ export default class Terminal {
|
|
|
90
106
|
if (this._inputMode) {
|
|
91
107
|
this._term.set_prompt("");
|
|
92
108
|
this._inputMode = false;
|
|
109
|
+
this._setPasswordMode(false);
|
|
93
110
|
this.pause();
|
|
94
111
|
this._inputResolve?.(command);
|
|
95
112
|
return;
|
|
@@ -114,14 +131,29 @@ export default class Terminal {
|
|
|
114
131
|
return;
|
|
115
132
|
this._inputResolve?.();
|
|
116
133
|
this._inputMode = false;
|
|
134
|
+
this._setPasswordMode(false);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Mask user input and keep it out of the history
|
|
138
|
+
* (password input).
|
|
139
|
+
*/
|
|
140
|
+
_setPasswordMode(on) {
|
|
141
|
+
this._term.set_mask(on);
|
|
142
|
+
if (on)
|
|
143
|
+
this._term.history().disable();
|
|
144
|
+
else
|
|
145
|
+
this._term.history().enable();
|
|
117
146
|
}
|
|
118
147
|
/**
|
|
119
148
|
* Print a string in the terminal.
|
|
149
|
+
* If `className` is provided, the text is wrapped in a span with this class.
|
|
120
150
|
*/
|
|
121
|
-
echo(message) {
|
|
151
|
+
echo(message, className) {
|
|
122
152
|
// JQuery.terminal adds a trailing newline that is not easy
|
|
123
153
|
// to remove with the current API. Doing our best here.
|
|
124
|
-
|
|
154
|
+
// escape brackets to prevent JQuery.terminal from interpreting
|
|
155
|
+
// [[...]] (e.g. Python's nested lists) as formatting or commands
|
|
156
|
+
message = $.terminal.escape_brackets(message.toString());
|
|
125
157
|
// prompt can be polluted with previous echo
|
|
126
158
|
// _prompt_tail is the line part just before the prompt
|
|
127
159
|
const term = this._term;
|
|
@@ -132,14 +164,17 @@ export default class Terminal {
|
|
|
132
164
|
else
|
|
133
165
|
prompt_tail = "";
|
|
134
166
|
// here, prompt and _prompt_tail should be correctly isolated
|
|
135
|
-
|
|
167
|
+
let lines = message.split("\n");
|
|
168
|
+
// formatting is applied line by line (empty formatting is avoided)
|
|
169
|
+
if (className != null)
|
|
170
|
+
lines = lines.map((l) => (l ? `[[;;;${className}]${l}]` : l));
|
|
136
171
|
lines[0] = prompt_tail + lines[0];
|
|
137
172
|
prompt_tail = lines.pop() ?? "";
|
|
138
173
|
term.set_prompt(prompt_tail + prompt);
|
|
139
174
|
if (lines.length) {
|
|
140
175
|
// term.echo("") does not display blank line
|
|
141
176
|
// so we replace this call with term.echo('\b')
|
|
142
|
-
term.echo(lines.join("\n") || "\b");
|
|
177
|
+
term.echo(lines.join("\n") || "\b", { exec: false });
|
|
143
178
|
}
|
|
144
179
|
this._prompt_tail = prompt_tail;
|
|
145
180
|
}
|
|
@@ -147,10 +182,9 @@ export default class Terminal {
|
|
|
147
182
|
* Print error in terminal (in red).
|
|
148
183
|
*/
|
|
149
184
|
error(message) {
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
message
|
|
153
|
-
return this._term.error(message.replace(/\n+$/, ""));
|
|
185
|
+
// stderr can be written in several chunks (not always ending with
|
|
186
|
+
// a newline) so we rely on echo to handle partial lines
|
|
187
|
+
this.echo(message, "terminal-error");
|
|
154
188
|
}
|
|
155
189
|
/**
|
|
156
190
|
* Pause the terminal execution (usefull to wait for event or promise).
|
package/lib/index.d.ts
CHANGED
|
@@ -1,2 +1,9 @@
|
|
|
1
|
-
import ConsoleApp from "./components/app";
|
|
2
|
-
|
|
1
|
+
import ConsoleApp, { type AppProps } from "./components/app";
|
|
2
|
+
import GUI from "./gui/gui";
|
|
3
|
+
import { useGUI } from "./hooks/gui";
|
|
4
|
+
import { useDispatch } from "./hooks/dispatch";
|
|
5
|
+
import { useConsoleState } from "./hooks/state";
|
|
6
|
+
import { Button } from "./components/button";
|
|
7
|
+
import { type PanelTitles } from "./components/main-container";
|
|
8
|
+
import { defaultStoredStates, type State, type Action, type Language, type Theme, type ViewMode, type RightPanel } from "./state-manager";
|
|
9
|
+
export { ConsoleApp, GUI, useGUI, useDispatch, useConsoleState, Button, defaultStoredStates, type AppProps, type PanelTitles, type State, type Action, type Language, type Theme, type ViewMode, type RightPanel, };
|
package/lib/index.js
CHANGED
|
@@ -1,2 +1,8 @@
|
|
|
1
1
|
import ConsoleApp from "./components/app";
|
|
2
|
-
|
|
2
|
+
import GUI from "./gui/gui";
|
|
3
|
+
import { useGUI } from "./hooks/gui";
|
|
4
|
+
import { useDispatch } from "./hooks/dispatch";
|
|
5
|
+
import { useConsoleState } from "./hooks/state";
|
|
6
|
+
import { Button } from "./components/button";
|
|
7
|
+
import { defaultStoredStates, } from "./state-manager";
|
|
8
|
+
export { ConsoleApp, GUI, useGUI, useDispatch, useConsoleState, Button, defaultStoredStates, };
|
package/lib/state-manager.d.ts
CHANGED
|
@@ -11,14 +11,17 @@ interface State {
|
|
|
11
11
|
rightPanel: RightPanel;
|
|
12
12
|
graphicsDirty: boolean;
|
|
13
13
|
editorContent: string;
|
|
14
|
+
storedStates: string[];
|
|
14
15
|
}
|
|
15
16
|
interface Action {
|
|
16
17
|
type: "init" | "set-theme" | "switch-theme" | "set-view-mode" | "switch-view" | "set-right-panel" | "graphics-dirty" | "set-editor-content";
|
|
17
18
|
payload?: Theme | ViewMode | RightPanel | State | string;
|
|
18
19
|
}
|
|
20
|
+
declare const defaultStoredStates: string[];
|
|
19
21
|
declare const stateManager: {
|
|
20
|
-
useReducer: (language: Language, initFromStorage: boolean) => readonly [{
|
|
22
|
+
useReducer: (language: Language, initFromStorage: boolean, storedStates?: string[]) => readonly [{
|
|
21
23
|
language: Language;
|
|
24
|
+
storedStates: string[];
|
|
22
25
|
ready: boolean;
|
|
23
26
|
theme: Theme;
|
|
24
27
|
viewMode: ViewMode;
|
|
@@ -34,5 +37,6 @@ declare const stateManager: {
|
|
|
34
37
|
graphicsDirty: (state: State) => boolean;
|
|
35
38
|
editorContent: (state: State) => string;
|
|
36
39
|
};
|
|
37
|
-
export { type Dispatch, type Action, type Language, type Theme, type ViewMode, type RightPanel, };
|
|
40
|
+
export { type State, type Dispatch, type Action, type Language, type Theme, type ViewMode, type RightPanel, };
|
|
41
|
+
export { defaultStoredStates };
|
|
38
42
|
export default stateManager;
|
package/lib/state-manager.js
CHANGED
|
@@ -2,6 +2,13 @@ import { useEffect } from "react";
|
|
|
2
2
|
/* we use immer to fake mutable state */
|
|
3
3
|
import { useImmerReducer } from "use-immer";
|
|
4
4
|
import { Storage } from "@basthon/checkpoints";
|
|
5
|
+
/* states to store (by default) */
|
|
6
|
+
const defaultStoredStates = [
|
|
7
|
+
"theme",
|
|
8
|
+
"viewMode",
|
|
9
|
+
"rightPanel",
|
|
10
|
+
"editorContent",
|
|
11
|
+
];
|
|
5
12
|
/* this initial state */
|
|
6
13
|
const initialState = {
|
|
7
14
|
ready: false,
|
|
@@ -11,18 +18,17 @@ const initialState = {
|
|
|
11
18
|
rightPanel: "terminal",
|
|
12
19
|
graphicsDirty: false,
|
|
13
20
|
editorContent: "",
|
|
21
|
+
storedStates: defaultStoredStates,
|
|
14
22
|
};
|
|
15
23
|
/* copy throught JSON, should be sufficient for what we do */
|
|
16
24
|
function copy(obj) {
|
|
17
25
|
return JSON.parse(JSON.stringify(obj));
|
|
18
26
|
}
|
|
19
|
-
/* states to store */
|
|
20
|
-
const storedStates = ["theme", "viewMode", "rightPanel", "editorContent"];
|
|
21
27
|
/* get state from storage */
|
|
22
|
-
const stateFromStorage = async (language) => {
|
|
28
|
+
const stateFromStorage = async (language, storedStates) => {
|
|
23
29
|
const storage = new Storage(`console.${language}.states`);
|
|
24
30
|
await storage.ready();
|
|
25
|
-
const state = { ...initialState, language };
|
|
31
|
+
const state = { ...initialState, language, storedStates };
|
|
26
32
|
for (let key of storedStates) {
|
|
27
33
|
const storageValue = await storage.get(key);
|
|
28
34
|
if (storageValue != null)
|
|
@@ -34,7 +40,7 @@ const stateFromStorage = async (language) => {
|
|
|
34
40
|
const stateToStorage = async (state) => {
|
|
35
41
|
const storage = new Storage(`console.${state.language}.states`);
|
|
36
42
|
await storage.ready();
|
|
37
|
-
for (let key of storedStates)
|
|
43
|
+
for (let key of state.storedStates)
|
|
38
44
|
await storage.set(key, state[key]);
|
|
39
45
|
};
|
|
40
46
|
/* where our global logic takes place */
|
|
@@ -78,17 +84,18 @@ const reducer = (state, action) => {
|
|
|
78
84
|
};
|
|
79
85
|
/* stateManager exposed as API */
|
|
80
86
|
const stateManager = {
|
|
81
|
-
useReducer: (language, initFromStorage) => {
|
|
87
|
+
useReducer: (language, initFromStorage, storedStates = defaultStoredStates) => {
|
|
82
88
|
const [state, dispatch] = useImmerReducer(reducer, {
|
|
83
89
|
...initialState,
|
|
84
90
|
language,
|
|
91
|
+
storedStates,
|
|
85
92
|
});
|
|
86
93
|
// dispatch init from storage
|
|
87
94
|
useEffect(() => {
|
|
88
95
|
(async () => {
|
|
89
96
|
let payload = undefined;
|
|
90
97
|
if (initFromStorage)
|
|
91
|
-
payload = await stateFromStorage(language);
|
|
98
|
+
payload = await stateFromStorage(language, storedStates);
|
|
92
99
|
dispatch({ type: "init", payload });
|
|
93
100
|
})();
|
|
94
101
|
}, []);
|
|
@@ -102,4 +109,5 @@ const stateManager = {
|
|
|
102
109
|
graphicsDirty: (state) => state.graphicsDirty,
|
|
103
110
|
editorContent: (state) => state.editorContent,
|
|
104
111
|
};
|
|
112
|
+
export { defaultStoredStates };
|
|
105
113
|
export default stateManager;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@basthon/console-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.8",
|
|
4
4
|
"description": "Basthon - Console React Component",
|
|
5
5
|
"homepage": "https://basthon.fr",
|
|
6
6
|
"bugs": {
|
|
@@ -15,12 +15,10 @@
|
|
|
15
15
|
"main": "lib/index.js",
|
|
16
16
|
"types": "lib/index.d.ts",
|
|
17
17
|
"directories": {
|
|
18
|
-
"lib": "lib/"
|
|
19
|
-
"patches": "patches/"
|
|
18
|
+
"lib": "lib/"
|
|
20
19
|
},
|
|
21
20
|
"files": [
|
|
22
|
-
"lib"
|
|
23
|
-
"patches"
|
|
21
|
+
"lib"
|
|
24
22
|
],
|
|
25
23
|
"exports": {
|
|
26
24
|
".": {
|
|
@@ -65,7 +63,7 @@
|
|
|
65
63
|
"constants-browserify": "^1.0.0",
|
|
66
64
|
"immer": "10.2.0",
|
|
67
65
|
"jquery": "^3.6.4",
|
|
68
|
-
"jquery.terminal": "
|
|
66
|
+
"jquery.terminal": "2.45.2",
|
|
69
67
|
"notie": "^4.3.1",
|
|
70
68
|
"react-ace": "^13.0.0",
|
|
71
69
|
"tty-browserify": "^0.0.1",
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
diff --git a/js/jquery.terminal.js b/js/jquery.terminal.js
|
|
2
|
-
index ed30f14fac229448c2b5b4ce8ed5437567723805..4aef6feaac0ba5a8ffc0dc34ac4e92e1ea725db5 100644
|
|
3
|
-
--- a/js/jquery.terminal.js
|
|
4
|
-
+++ b/js/jquery.terminal.js
|
|
5
|
-
@@ -1286,7 +1286,7 @@
|
|
6
|
-
var command_re = /((?:"[^"\\]*(?:\\[\S\s][^"\\]*)*"|'[^'\\]*(?:\\[\S\s][^'\\]*)*'|`[^`\\]*(?:\\[\S\s][^`\\]*)*`|\/[^\/\\]*(?:\\[\S\s][^\/\\]*)*\/[gimsuy]*(?=\s|$)|(?:\\\s|\S))+)(?=\s|$)/gi;
|
|
7
|
-
var extended_command_re = /^\s*((terminal|cmd)::([a-z_]+)\(([\s\S]*)\))\s*$/;
|
|
8
|
-
var format_exec_split_re = /(\[\[(?:-?[@!gbiusor])*;[^\]]+\](?:\\[[\]]|[^\]])*\]|\[\[[\s\S]+?\]\])/;
|
|
9
|
-
- var format_exec_re = /(\[\[[\s\S]+?\]\])/;
|
|
10
|
-
+ var format_exec_re = /(\[\[(?:[^\][]|\\\])+\]\])/;
|
|
11
|
-
var float_re = /^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?$/;
|
|
12
|
-
var re_re = /^\/((?:\\\/|[^/]|\[[^\]]*\/[^\]]*\])+)\/([gimsuy]*)$/;
|
|
13
|
-
var string_re = /("(?:[^"\\]|\\(?:\\\\)*"|\\\\)*"|'(?:[^'\\]|\\(?:\\\\)*'|\\\\)*'|`(?:[^`\\]|\\(?:\\\\)*`|\\\\)*`)/;
|