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 +367 -0
- package/package.json +3 -2
- package/termux.js +4 -0
- package/test/termux.spec.js +92 -0
- package/widgets/laboratory/service.js +8 -0
- package/widgets/laboratory/widget.js +18 -15
- package/widgets/termux/styles.js +75 -0
- package/widgets/termux/widget.js +243 -0
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.
|
|
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,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 '
|
|
3
|
-
import Maintenance from '
|
|
2
|
+
import Widget from '../widget';
|
|
3
|
+
import Maintenance from '../maintenance/widget';
|
|
4
4
|
import DisconnectOverlay from '../disconnect-overlay/widget';
|
|
5
|
-
import ThemeContext from '
|
|
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
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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> </span>
|
|
165
|
+
) : (
|
|
166
|
+
<>
|
|
167
|
+
<span>{prompt} </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;
|