goblin-laboratory 4.6.5 → 4.7.1

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/termux.js ADDED
@@ -0,0 +1,367 @@
1
+ // @ts-check
2
+ const {Elf} = require('xcraft-core-goblin');
3
+ const {string, boolean, array, option, object} = require('xcraft-core-stones');
4
+ const {parseOptions} = require('xcraft-core-utils/lib/reflect.js');
5
+
6
+ class OrderedSet {
7
+ #set = new Set();
8
+ #list = [];
9
+
10
+ add(value) {
11
+ if (!this.#set.has(value)) {
12
+ this.#set.add(value);
13
+ this.#list.push(value);
14
+ }
15
+ }
16
+
17
+ delete(value) {
18
+ if (this.#set.delete(value)) {
19
+ this.#list = this.#list.filter((v) => v !== value);
20
+ }
21
+ }
22
+
23
+ last() {
24
+ return this.#list[this.#list.length - 1];
25
+ }
26
+
27
+ previous(value) {
28
+ const i = this.#list.indexOf(value);
29
+ return i > 0 ? this.#list[i - 1] : undefined;
30
+ }
31
+
32
+ next(value) {
33
+ const i = this.#list.indexOf(value);
34
+ return i < this.#list.length - 1 && i >= 0 ? this.#list[i + 1] : undefined;
35
+ }
36
+ }
37
+
38
+ class TermuxShape {
39
+ id = string;
40
+ prompt = string;
41
+ busy = boolean;
42
+ history = array(string);
43
+ completion = string;
44
+ value = string;
45
+
46
+ inputCommand = boolean;
47
+ cmd = option(string);
48
+ args = option(object);
49
+ }
50
+
51
+ class TermuxState extends Elf.Sculpt(TermuxShape) {}
52
+
53
+ class TermuxLogic extends Elf.Spirit {
54
+ state = new TermuxState({
55
+ id: 'termux',
56
+ prompt: '~ $',
57
+ busy: false,
58
+ history: [],
59
+ completion: '',
60
+ value: '',
61
+
62
+ inputCommand: false,
63
+ cmd: null,
64
+ args: null,
65
+ });
66
+
67
+ init(prompt) {
68
+ const {state} = this;
69
+ state.prompt = prompt;
70
+ }
71
+
72
+ beginCommand(prompt, command) {
73
+ const {state} = this;
74
+ state.prompt = prompt;
75
+ state.busy = true;
76
+ state.history.push(`${prompt} ${command}`);
77
+ }
78
+
79
+ endCommand(prompt, result) {
80
+ const {state} = this;
81
+ state.prompt = prompt;
82
+ state.history.push(result);
83
+ state.busy = false;
84
+ }
85
+
86
+ inputCommand(input) {
87
+ const {state} = this;
88
+ state.history.push(input);
89
+ state.busy = true;
90
+ state.inputCommand = false;
91
+ }
92
+
93
+ forInputCommand(question, cmd, args) {
94
+ const {state} = this;
95
+ state.prompt = '-> ';
96
+ state.history.push(question);
97
+ state.busy = false;
98
+ state.inputCommand = true;
99
+ state.cmd = cmd;
100
+ state.args = args;
101
+ }
102
+
103
+ askForCompletion(prompt, input, tools) {
104
+ const {state} = this;
105
+ state.prompt = prompt;
106
+ if (tools.length > 1) {
107
+ state.completion = '';
108
+ state.history.push(
109
+ `${prompt} ${input}\n${tools
110
+ .filter((tool) => tool[0] !== '$')
111
+ .map((tool) => {
112
+ const items = tool.split(' ');
113
+ return items[items.length - 1];
114
+ })
115
+ .join(' ')}\n`
116
+ );
117
+ } else if (tools.length === 1) {
118
+ state.completion = tools[0];
119
+ } else {
120
+ state.completion = '';
121
+ }
122
+ }
123
+
124
+ setFromHistory(value) {
125
+ const {state} = this;
126
+ state.completion = value.trim();
127
+ }
128
+
129
+ clearCompletion() {
130
+ const {state} = this;
131
+ state.completion = '';
132
+ }
133
+
134
+ /// TOOLS ////////////////////////////////////////////////////////////////////
135
+
136
+ clear$tool() {
137
+ const {state} = this;
138
+ state.history = [];
139
+ }
140
+ }
141
+
142
+ function getTools(resp) {
143
+ const registry = resp.getCommandsRegistry();
144
+ return Object.fromEntries(
145
+ Object.entries(registry)
146
+ .filter(([cmd]) => cmd.endsWith('$tool'))
147
+ .map(([cmd, ctx]) => {
148
+ const tool = cmd.split('.').reverse()[0].split('$')[0];
149
+ return tool.length ? [tool, ctx] : [`$${cmd.split('.', 1)[0]}`, ctx];
150
+ })
151
+ );
152
+ }
153
+
154
+ function getTool(tools, name) {
155
+ if (name[0] !== '$' && name in tools) {
156
+ return tools[name];
157
+ }
158
+ throw new Error(`${name}: command not found`);
159
+ }
160
+
161
+ function getPrompt(user) {
162
+ return user.rank === 'admin' ? '~ #' : '~ $';
163
+ }
164
+
165
+ class Termux extends Elf.Alone {
166
+ logic = Elf.getLogic(TermuxLogic);
167
+ state = new TermuxState();
168
+
169
+ _unsub;
170
+ _tools = {};
171
+ _history = new OrderedSet(); /* only the command entries */
172
+
173
+ async init() {
174
+ const prompt = getPrompt(this.user);
175
+ this.logic.init(prompt);
176
+ const {resp} = this.quest;
177
+ this._tools = getTools(resp);
178
+ this._unsub = resp.onCommandsRegistry(() => {
179
+ this._tools = getTools(resp);
180
+ });
181
+ this.quest.goblin.defer(
182
+ this.quest.sub('*::*.<termux-input>', async (_, {msg}) => {
183
+ const {question, cmd, args} = msg.data;
184
+ await this.forInputCommand(question, cmd, args);
185
+ })
186
+ );
187
+ }
188
+
189
+ async beginCommand(command) {
190
+ const prompt = getPrompt(this.user);
191
+ this.quest.doSync({prompt, command});
192
+
193
+ const entries = parseOptions(command).map(
194
+ (option) =>
195
+ option
196
+ .replace(/^"(.*)"$/g, '$1')
197
+ .replace(/^'(.*)'$/g, '$1')
198
+ .replace(/\\([^\\])/g, '$1') /* unescape */
199
+ .replace(/\\\\/g, '\\') /* keep only escaped \ */
200
+ );
201
+ const name = entries[0];
202
+ const params = entries.slice(1);
203
+
204
+ let result = '';
205
+ try {
206
+ if (!name) {
207
+ return;
208
+ }
209
+
210
+ this._history.delete(command.trim());
211
+ this._history.add(command.trim());
212
+
213
+ const tool = getTool(this._tools, name);
214
+ const {required, optional} = tool.options.params;
215
+ let args = required.concat(optional).reduce((args, arg, index) => {
216
+ args[arg] = params[index];
217
+ return args;
218
+ }, {});
219
+ result = await this.quest.cmd(tool.name, args);
220
+ } catch (ex) {
221
+ result = ex.stack || ex.message || ex;
222
+ } finally {
223
+ /* Keep the command alive while te result is not a string.
224
+ * It uses with the <termux-input> stuff.
225
+ */
226
+ if (typeof result === 'string') {
227
+ if (result.length) {
228
+ result += '\n';
229
+ }
230
+ await this.endCommand(result);
231
+ }
232
+ }
233
+ }
234
+
235
+ async endCommand(result) {
236
+ const prompt = getPrompt(this.user);
237
+ this.logic.endCommand(prompt, result);
238
+ }
239
+
240
+ async inputCommand(input) {
241
+ const {state} = this;
242
+
243
+ this.logic.inputCommand(input);
244
+ if (!state.inputCommand || !state.cmd || !state.args) {
245
+ return;
246
+ }
247
+
248
+ let result = '';
249
+ try {
250
+ result = await this.quest.cmd(state.cmd, {input, ...state.args.toJS()});
251
+ } catch (ex) {
252
+ result = ex.stack || ex.message || ex;
253
+ }
254
+ if (typeof result === 'string') {
255
+ if (result.length) {
256
+ result += '\n';
257
+ }
258
+ await this.endCommand(result);
259
+ }
260
+ }
261
+
262
+ async forInputCommand(question, cmd, args) {
263
+ this.logic.forInputCommand(question, cmd, args);
264
+ }
265
+
266
+ async askForCompletion(input) {
267
+ const prompt = getPrompt(this.user);
268
+ const tools = Object.keys(this._tools).filter((tool) =>
269
+ tool.startsWith(input)
270
+ );
271
+ this.logic.askForCompletion(prompt, input, tools);
272
+
273
+ const {completion} = this.state;
274
+ if (completion) {
275
+ return;
276
+ }
277
+
278
+ const items = input.trim().split(' ');
279
+ const tool = items[0];
280
+ if (!(tool in this._tools)) {
281
+ return;
282
+ }
283
+
284
+ const name = this._tools[tool].name.split('.', 1)[0];
285
+ if (!(`$${name}` in this._tools)) {
286
+ return;
287
+ }
288
+
289
+ const desc = await this.quest.cmd(`${name}.$tool`, {tool});
290
+ if (items.length === 1) {
291
+ this.logic.askForCompletion(prompt, input, Object.keys(desc));
292
+ return;
293
+ }
294
+
295
+ const cmds = Object.keys(desc)
296
+ .filter((option) => option.startsWith(items[1]))
297
+ .map((option) => `${tool} ${option}`);
298
+ this.logic.askForCompletion(prompt, input.trim(), cmds);
299
+ }
300
+
301
+ async setFromHistory(up, input) {
302
+ let value;
303
+ input = input.trim();
304
+ if (up && !input) {
305
+ value = this._history.last();
306
+ } else if (input) {
307
+ value = up ? this._history.previous(input) : this._history.next(input);
308
+ }
309
+ if (!up && !value) {
310
+ this.logic.setFromHistory('<empty>');
311
+ } else if (value) {
312
+ this.logic.setFromHistory(value);
313
+ }
314
+ }
315
+
316
+ async clearCompletion() {
317
+ this.logic.clearCompletion();
318
+ }
319
+
320
+ /// TOOLS ////////////////////////////////////////////////////////////////////
321
+
322
+ async clear$tool() {
323
+ this.logic.clear$tool();
324
+ return '';
325
+ }
326
+
327
+ async man$tool(name) {
328
+ const tool = getTool(this._tools, name);
329
+ let result = '';
330
+ result += ` module: ${tool.info.name} (v${tool.info.version})\n`;
331
+ result += `location: ${tool.location}\n`;
332
+ result += ` usage: ${name} ${tool.options.params.required
333
+ .map((arg) => arg.toUpperCase())
334
+ .join(' ')}\n\n`;
335
+
336
+ /* List first parameter possible values */
337
+ const _name = tool.name.split('.', 1)[0];
338
+ if (`$${_name}` in this._tools) {
339
+ const desc = await this.quest.cmd(`${_name}.$tool`, {tool: name});
340
+ const {required} = tool.options.params;
341
+ if (required[0]) {
342
+ result += `${required[0].toUpperCase()} ${Object.keys(desc)
343
+ .filter((option) => option[0] !== '$')
344
+ .join(`\n${new Array(required[0].length + 2).join(' ')}`)}`;
345
+ }
346
+ }
347
+
348
+ return result;
349
+ }
350
+
351
+ async $tool(tool) {
352
+ if (tool === 'man') {
353
+ return {...this._tools};
354
+ }
355
+ return {};
356
+ }
357
+
358
+ //////////////////////////////////////////////////////////////////////////////
359
+
360
+ dispose() {
361
+ if (this._unsub) {
362
+ this._unsub();
363
+ }
364
+ }
365
+ }
366
+
367
+ module.exports = {Termux, TermuxLogic};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "goblin-laboratory",
3
- "version": "4.6.5",
3
+ "version": "4.7.1",
4
4
  "description": "Laboratory",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1"
@@ -31,7 +31,8 @@
31
31
  "xcraft-core-log": "^2.2.0",
32
32
  "xcraft-core-probe": "^2.0.0",
33
33
  "xcraft-core-transport": "^4.0.0",
34
- "xcraft-core-stones": "^0.4.16"
34
+ "xcraft-core-stones": "^0.4.16",
35
+ "xcraft-core-utils": "^4.19.0"
35
36
  },
36
37
  "devDependencies": {
37
38
  "aphrodite": "^2.2.2",
package/termux.js ADDED
@@ -0,0 +1,4 @@
1
+ const {Elf} = require('xcraft-core-goblin');
2
+ const {Termux, TermuxLogic} = require('./lib/termux.js');
3
+
4
+ exports.xcraftCommands = Elf.birth(Termux, TermuxLogic);
@@ -0,0 +1,92 @@
1
+ 'use strict';
2
+
3
+ const {expect} = require('chai');
4
+ const path = require('node:path');
5
+ const {Elf} = require('xcraft-core-goblin/lib/test.js');
6
+ const {TermuxLogic, Termux} = require('../lib/termux.js');
7
+
8
+ describe('goblin.laboratory.termux.command', function () {
9
+ it('history', function () {
10
+ const termuxLogic = Elf.trial(TermuxLogic);
11
+
12
+ termuxLogic.beginCommand('$', 'cmd1', []);
13
+ termuxLogic.endCommand('done 1');
14
+ expect(termuxLogic.state.history.length).to.be.equal(2);
15
+ expect(termuxLogic.state.history[0]).to.be.equal('$ cmd1');
16
+ expect(termuxLogic.state.history[1]).to.be.equal('done 1');
17
+
18
+ termuxLogic.beginCommand('$', 'cmd2', []);
19
+ termuxLogic.endCommand('done 2');
20
+ expect(termuxLogic.state.history.length).to.be.equal(4);
21
+ expect(termuxLogic.state.history[2]).to.be.equal('$ cmd2');
22
+ expect(termuxLogic.state.history[3]).to.be.equal('done 2');
23
+ });
24
+ });
25
+
26
+ describe('goblin.laboratory.termux.completion', function () {
27
+ let runner;
28
+
29
+ this.beforeAll(function () {
30
+ runner = new Elf.Runner();
31
+ runner.init();
32
+ });
33
+
34
+ this.afterAll(function () {
35
+ runner.dispose();
36
+ });
37
+
38
+ it('history', async function () {
39
+ this.timeout(process.env.NODE_ENV === 'development' ? 1000000 : 40000);
40
+
41
+ /** @this {Elf} */
42
+ async function test() {
43
+ const xBus = require('xcraft-core-bus');
44
+ await xBus.loadModule(
45
+ this.quest.resp,
46
+ ['termux.js'],
47
+ path.join(__dirname, '..'),
48
+ {}
49
+ );
50
+
51
+ let state;
52
+ const termux = new Termux(this);
53
+
54
+ await termux.init();
55
+
56
+ await termux.beginCommand('cmd1');
57
+ await termux.endCommand('done 1');
58
+ await termux.beginCommand('cmd2');
59
+ await termux.endCommand('done 2');
60
+
61
+ //////////////////////////////////////////////////////////////////////////
62
+
63
+ await termux.setFromHistory(true, 'cmd2 '); // UP
64
+ state = await this.quest.getState('termux');
65
+ expect(state.get('completion')).to.be.equal('cmd1');
66
+
67
+ await termux.setFromHistory(true, 'cmd1 '); // UP (roof)
68
+ state = await this.quest.getState('termux');
69
+ expect(state.get('completion')).to.be.equal('cmd1');
70
+
71
+ await termux.setFromHistory(false, 'cmd1 '); // DOWN
72
+ state = await this.quest.getState('termux');
73
+ expect(state.get('completion')).to.be.equal('cmd2');
74
+
75
+ await termux.setFromHistory(false, 'cmd2 '); // DOWN (ground)
76
+ state = await this.quest.getState('termux');
77
+ expect(state.get('completion')).to.be.equal('<empty>');
78
+
79
+ //////////////////////////////////////////////////////////////////////////
80
+
81
+ await termux.setFromHistory(true, 'cmd3 '); // UP
82
+ state = await this.quest.getState('termux');
83
+ expect(state.get('completion')).to.be.equal('<empty>');
84
+
85
+ await termux.setFromHistory(false, 'cmd3 '); // DOWN
86
+ state = await this.quest.getState('termux');
87
+ expect(state.get('completion')).to.be.equal('<empty>');
88
+ }
89
+
90
+ await runner.it(test);
91
+ });
92
+ });
@@ -2,6 +2,7 @@
2
2
 
3
3
  const path = require('path');
4
4
  const Goblin = require('xcraft-core-goblin');
5
+ const {Termux} = require('../../lib/termux.js');
5
6
  const goblinName = path.basename(module.parent.filename, '.js');
6
7
 
7
8
  // Define initial logic values
@@ -102,6 +103,11 @@ Goblin.registerQuest(goblinName, 'create', function* (
102
103
 
103
104
  const themeContexts = config.themeContexts || ['theme'];
104
105
 
106
+ config.feeds.push('termux');
107
+
108
+ const termux = new Termux(quest);
109
+ yield termux.init();
110
+
105
111
  const promises = [];
106
112
  for (const ctx of themeContexts) {
107
113
  const composerId = `theme-composer@${ctx}`;
@@ -521,6 +527,8 @@ Goblin.registerQuest(goblinName, 'del', function* (quest, widgetId) {
521
527
  }
522
528
  });
523
529
 
530
+ /******************************************************************************/
531
+
524
532
  Goblin.registerQuest(goblinName, 'delete', function (quest) {
525
533
  unlisten(quest);
526
534
  });
@@ -1,10 +1,11 @@
1
1
  import React from 'react';
2
- import Widget from 'goblin-laboratory/widgets/widget';
3
- import Maintenance from 'goblin-laboratory/widgets/maintenance/widget';
2
+ import Widget from '../widget';
3
+ import Maintenance from '../maintenance/widget';
4
4
  import DisconnectOverlay from '../disconnect-overlay/widget';
5
- import ThemeContext from 'goblin-laboratory/widgets/theme-context/widget';
6
-
5
+ import ThemeContext from '../theme-context/widget';
6
+ import Termux from '../termux/widget.js';
7
7
  import importer from 'goblin_importer';
8
+
8
9
  const widgetImporter = importer('widget');
9
10
 
10
11
  class LaboratoryNC extends Widget {
@@ -13,22 +14,24 @@ class LaboratoryNC extends Widget {
13
14
  }
14
15
 
15
16
  renderContent() {
16
- const {status, root, rootId, overlay, message} = this.props;
17
+ const {id, status, root, rootId, overlay, message} = this.props;
17
18
  if (status && status !== 'off') {
18
19
  return <Maintenance id="workshop" />;
19
- } else {
20
- const widgetName = root.split('@')[0];
21
- const RootWidget = widgetImporter(widgetName);
22
- if (overlay) {
23
- return (
20
+ }
21
+
22
+ const widgetName = root.split('@')[0];
23
+ const RootWidget = widgetImporter(widgetName);
24
+ return (
25
+ <Termux labId={id}>
26
+ {overlay ? (
24
27
  <DisconnectOverlay message={message}>
25
28
  <RootWidget id={rootId} />
26
29
  </DisconnectOverlay>
27
- );
28
- } else {
29
- return <RootWidget id={rootId} />;
30
- }
31
- }
30
+ ) : (
31
+ <RootWidget id={rootId} />
32
+ )}
33
+ </Termux>
34
+ );
32
35
  }
33
36
 
34
37
  render() {
@@ -0,0 +1,75 @@
1
+ export default function styles() {
2
+ const fontColor = {
3
+ 'color': 'rgb(200, 200, 200)',
4
+ '@media (prefers-color-scheme: light)': {
5
+ color: 'rgb(50, 50, 50)',
6
+ },
7
+ };
8
+
9
+ const console = {
10
+ 'width': '100vw',
11
+ 'height': 0,
12
+ 'backgroundColor': 'rgba(0, 0, 0, 0.6)',
13
+ '@media (prefers-color-scheme: light)': {
14
+ backgroundColor: 'rgba(180, 180, 180, 0.6)',
15
+ },
16
+ 'backdropFilter': 'blur(10px)',
17
+ 'position': 'fixed',
18
+ 'top': 0,
19
+ 'left': 0,
20
+ 'transition': 'height 0.2s ease-out, opacity 0.2s',
21
+ 'opacity': 0,
22
+ 'border': '5px solid transparent',
23
+
24
+ 'display': 'flex',
25
+ 'flexDirection': 'column',
26
+ 'overflowY': 'scroll',
27
+ 'pointerEvents': 'none',
28
+
29
+ 'fontFamily': 'monospace',
30
+
31
+ '&[data-show=true]': {
32
+ pointerEvents: 'auto',
33
+ height: '50vh',
34
+ transition: 'height 0.1s ease-in, opacity 0.2s',
35
+ opacity: 1,
36
+ },
37
+
38
+ '& > .history': {
39
+ display: 'flex',
40
+ flexGrow: 1,
41
+ flexFlow: 'column-reverse',
42
+ fontSize: 'medium',
43
+ wordWrap: 'break-word',
44
+ ...fontColor,
45
+ },
46
+
47
+ '& > .cli': {
48
+ 'display': 'flex',
49
+ 'flexDirection': 'row',
50
+ 'height': '15px',
51
+
52
+ '& > span': {
53
+ whiteSpace: 'nowrap',
54
+ ...fontColor,
55
+ },
56
+ },
57
+
58
+ '& > .cli > .input': {
59
+ 'flexGrow': 1,
60
+ 'width': '100%',
61
+ ...fontColor,
62
+ 'border': 0,
63
+ 'backgroundColor': 'transparent',
64
+ 'fontSize': 'medium',
65
+
66
+ '&:focus': {
67
+ outline: 'none',
68
+ },
69
+ },
70
+ };
71
+
72
+ return {
73
+ console,
74
+ };
75
+ }
@@ -0,0 +1,243 @@
1
+ import React from 'react';
2
+ import Widget from 'goblin-laboratory/widgets/widget';
3
+ import Mousetrap from 'mousetrap';
4
+ import * as styles from './styles.js';
5
+
6
+ class TermuxNC extends Widget {
7
+ scrollTimeout;
8
+
9
+ constructor() {
10
+ super(...arguments);
11
+ this.styles = styles;
12
+ this.inputRef = React.createRef();
13
+
14
+ this.state = {
15
+ show: false,
16
+ value: '',
17
+ };
18
+ }
19
+
20
+ scrollToBottom() {
21
+ if (this.inputRef.current) {
22
+ this.inputRef.current.scrollIntoView();
23
+ }
24
+ }
25
+
26
+ componentDidMount() {
27
+ Mousetrap.bind('alt+f12', this.toggleConsole);
28
+ }
29
+
30
+ componentWillUnmount() {
31
+ super.componentWillUnmount();
32
+ Mousetrap.unbind('alt+f12');
33
+ if (this.scrollTimeout) {
34
+ clearTimeout(this.scrollTimeout);
35
+ this.scrollTimeout = null;
36
+ }
37
+ }
38
+
39
+ componentDidUpdate(prevProps) {
40
+ const {completion} = this.props;
41
+ if (completion && prevProps.completion !== completion) {
42
+ this.setState({
43
+ value: completion === '<empty>' ? '' : this.props.completion + ' ',
44
+ });
45
+ this.clearCompletion();
46
+ }
47
+ }
48
+
49
+ toggleConsole = () => {
50
+ this.setState({show: !this.state.show});
51
+ };
52
+
53
+ askForCompletion = () => {
54
+ const input = this.inputRef.current?.value;
55
+ this.doFor('termux', 'askForCompletion', {input});
56
+ };
57
+
58
+ setFromHistory = (up) => {
59
+ const input = this.inputRef.current?.value;
60
+ this.doFor('termux', 'setFromHistory', {up, input});
61
+ };
62
+
63
+ clearCompletion = () => {
64
+ this.doFor('termux', 'clearCompletion');
65
+ };
66
+
67
+ beginCommand = (command) => {
68
+ this.doFor('termux', 'beginCommand', {command});
69
+ };
70
+
71
+ inputCommand = (input) => {
72
+ this.doFor('termux', 'inputCommand', {input});
73
+ };
74
+
75
+ setCliFocus = () => {
76
+ setTimeout(() => this.inputRef.current?.focus());
77
+ };
78
+
79
+ handleKeyDown = (event) => {
80
+ switch (event.key) {
81
+ /* Valid the command line */
82
+ case 'Enter': {
83
+ const {value} = this.state;
84
+ const {inputCommand} = this.props;
85
+ if (inputCommand) {
86
+ this.inputCommand(value);
87
+ } else {
88
+ this.beginCommand(value);
89
+ }
90
+ this.setState({value: ''});
91
+ break;
92
+ }
93
+ /* Show or hide the console */
94
+ case 'F12': {
95
+ if (event.altKey) {
96
+ event.preventDefault();
97
+ this.toggleConsole();
98
+ }
99
+ break;
100
+ }
101
+ /* Try to autocomplete */
102
+ case 'Tab': {
103
+ event.preventDefault();
104
+ this.askForCompletion();
105
+ break;
106
+ }
107
+ /* Move into the command history */
108
+ case 'ArrowUp':
109
+ case 'ArrowDown': {
110
+ event.preventDefault();
111
+ this.setFromHistory(event.key === 'ArrowUp');
112
+ break;
113
+ }
114
+ /* [a] move to the begining, [e] move to the end */
115
+ case 'a':
116
+ case 'e': {
117
+ if (event.ctrlKey) {
118
+ event.preventDefault();
119
+ if (event.key === 'a') {
120
+ this.inputRef.current.setSelectionRange(0, 0);
121
+ } else if (event.key === 'e') {
122
+ const {length} = this.inputRef.current.value;
123
+ this.inputRef.current.setSelectionRange(length, length);
124
+ }
125
+ this.inputRef.current.focus();
126
+ }
127
+ break;
128
+ }
129
+ /* Erase the whole line */
130
+ case 'u': {
131
+ if (event.ctrlKey) {
132
+ this.setState({value: ''});
133
+ }
134
+ break;
135
+ }
136
+ /* Remove the previous word */
137
+ case 'w': {
138
+ if (event.ctrlKey) {
139
+ const values = this.state.value.split(' ');
140
+ const value = values.slice(0, -1).join(' ');
141
+ this.setState({value});
142
+ }
143
+ break;
144
+ }
145
+ }
146
+
147
+ /* Handle scroll with a timeout because (just after the send, the new state
148
+ * is not available)
149
+ */
150
+ this.scrollToBottom();
151
+ if (this.scrollTimeout) {
152
+ clearTimeout(this.scrollTimeout);
153
+ }
154
+ this.scrollTimeout = setTimeout(() => {
155
+ this.scrollToBottom();
156
+ }, 100);
157
+ };
158
+
159
+ renderCli() {
160
+ const {prompt, busy} = this.props;
161
+ return (
162
+ <div className="cli">
163
+ {busy ? (
164
+ <span>&nbsp;</span>
165
+ ) : (
166
+ <>
167
+ <span>{prompt}&nbsp;</span>
168
+ <input
169
+ className="input"
170
+ type="text"
171
+ onKeyDown={this.handleKeyDown}
172
+ autoFocus
173
+ ref={this.inputRef}
174
+ value={this.state.value}
175
+ onChange={(e) => this.setState({value: e.target.value})}
176
+ ></input>
177
+ </>
178
+ )}
179
+ </div>
180
+ );
181
+ }
182
+
183
+ renderHistory() {
184
+ return (
185
+ <div className="history" onMouseDown={this.setCliFocus}>
186
+ {this.props.history.reverse().map((row, index) => (
187
+ <span key={index}>
188
+ {row.split('\n').map((row, index) => (
189
+ <span key={index}>
190
+ {row.replaceAll(' ', ' ')}
191
+ <br />
192
+ </span>
193
+ ))}
194
+ </span>
195
+ ))}
196
+ </div>
197
+ );
198
+ }
199
+
200
+ renderConsole() {
201
+ return (
202
+ <>
203
+ {this.renderHistory()}
204
+ {this.renderCli()}
205
+ </>
206
+ );
207
+ }
208
+
209
+ render() {
210
+ const {children} = this.props;
211
+ const {show} = this.state;
212
+ return (
213
+ <>
214
+ {children}
215
+ <div className={this.styles.classNames.console} data-show={show}>
216
+ {show ? this.renderConsole() : null}
217
+ </div>
218
+ </>
219
+ );
220
+ }
221
+ }
222
+
223
+ const Termux = Widget.connect((state, props) => {
224
+ const termux = state.get('backend').get('termux');
225
+ if (!termux) {
226
+ return {
227
+ prompt: '~ $',
228
+ busy: true,
229
+ history: [],
230
+ completion: '',
231
+ inputCommand: false,
232
+ };
233
+ }
234
+ return {
235
+ prompt: termux.get('prompt', '~ $'),
236
+ busy: termux.get('busy', false),
237
+ history: termux.get('history', []),
238
+ completion: termux.get('completion', ''),
239
+ inputCommand: termux.get('inputCommand', false),
240
+ };
241
+ })(TermuxNC);
242
+
243
+ export default Termux;