goblin-laboratory 4.6.3 → 4.7.0
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/LICENSE +21 -0
- package/lib/termux.js +303 -0
- package/package.json +16 -3
- 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 +227 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017-2025 Xcraft, Epsitec SA
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/lib/termux.js
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
const {Elf} = require('xcraft-core-goblin');
|
|
3
|
+
const {string, boolean, array} = 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
|
+
|
|
47
|
+
class TermuxState extends Elf.Sculpt(TermuxShape) {}
|
|
48
|
+
|
|
49
|
+
class TermuxLogic extends Elf.Spirit {
|
|
50
|
+
state = new TermuxState({
|
|
51
|
+
id: 'termux',
|
|
52
|
+
prompt: '~ $',
|
|
53
|
+
busy: false,
|
|
54
|
+
history: [],
|
|
55
|
+
completion: '',
|
|
56
|
+
value: '',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
init(prompt) {
|
|
60
|
+
const {state} = this;
|
|
61
|
+
state.prompt = prompt;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
beginCommand(prompt, command) {
|
|
65
|
+
const {state} = this;
|
|
66
|
+
state.prompt = prompt;
|
|
67
|
+
state.busy = true;
|
|
68
|
+
state.history.push(`${prompt} ${command}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
endCommand(result) {
|
|
72
|
+
const {state} = this;
|
|
73
|
+
state.history.push(result);
|
|
74
|
+
state.busy = false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
askForCompletion(prompt, input, tools) {
|
|
78
|
+
const {state} = this;
|
|
79
|
+
state.prompt = prompt;
|
|
80
|
+
if (tools.length > 1) {
|
|
81
|
+
state.completion = '';
|
|
82
|
+
state.history.push(
|
|
83
|
+
`${prompt} ${input}\n${tools
|
|
84
|
+
.filter((tool) => tool[0] !== '$')
|
|
85
|
+
.map((tool) => {
|
|
86
|
+
const items = tool.split(' ');
|
|
87
|
+
return items[items.length - 1];
|
|
88
|
+
})
|
|
89
|
+
.join(' ')}\n`
|
|
90
|
+
);
|
|
91
|
+
} else if (tools.length === 1) {
|
|
92
|
+
state.completion = tools[0];
|
|
93
|
+
} else {
|
|
94
|
+
state.completion = '';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
setFromHistory(value) {
|
|
99
|
+
const {state} = this;
|
|
100
|
+
state.completion = value.trim();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
clearCompletion() {
|
|
104
|
+
const {state} = this;
|
|
105
|
+
state.completion = '';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// TOOLS ////////////////////////////////////////////////////////////////////
|
|
109
|
+
|
|
110
|
+
clear$tool() {
|
|
111
|
+
const {state} = this;
|
|
112
|
+
state.history = [];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function getTools(resp) {
|
|
117
|
+
const registry = resp.getCommandsRegistry();
|
|
118
|
+
return Object.fromEntries(
|
|
119
|
+
Object.entries(registry)
|
|
120
|
+
.filter(([cmd]) => cmd.endsWith('$tool'))
|
|
121
|
+
.map(([cmd, ctx]) => {
|
|
122
|
+
const tool = cmd.split('.').reverse()[0].split('$')[0];
|
|
123
|
+
return tool.length ? [tool, ctx] : [`$${cmd.split('.', 1)[0]}`, ctx];
|
|
124
|
+
})
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function getTool(tools, name) {
|
|
129
|
+
if (name[0] !== '$' && name in tools) {
|
|
130
|
+
return tools[name];
|
|
131
|
+
}
|
|
132
|
+
throw new Error(`${name}: command not found`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function getPrompt(user) {
|
|
136
|
+
return user.rank === 'admin' ? '~ #' : '~ $';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
class Termux extends Elf.Alone {
|
|
140
|
+
logic = Elf.getLogic(TermuxLogic);
|
|
141
|
+
state = new TermuxState();
|
|
142
|
+
|
|
143
|
+
_unsub;
|
|
144
|
+
_tools = {};
|
|
145
|
+
_history = new OrderedSet(); /* only the command entries */
|
|
146
|
+
|
|
147
|
+
async init() {
|
|
148
|
+
const prompt = getPrompt(this.user);
|
|
149
|
+
this.logic.init(prompt);
|
|
150
|
+
const {resp} = this.quest;
|
|
151
|
+
this._tools = getTools(resp);
|
|
152
|
+
this._unsub = resp.onCommandsRegistry(() => {
|
|
153
|
+
this._tools = getTools(resp);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async beginCommand(command) {
|
|
158
|
+
const prompt = getPrompt(this.user);
|
|
159
|
+
this.quest.doSync({prompt, command});
|
|
160
|
+
|
|
161
|
+
const entries = parseOptions(command).map(
|
|
162
|
+
(option) =>
|
|
163
|
+
option
|
|
164
|
+
.replace(/^"(.*)"$/g, '$1')
|
|
165
|
+
.replace(/^'(.*)'$/g, '$1')
|
|
166
|
+
.replace(/\\([^\\])/g, '$1') /* unescape */
|
|
167
|
+
.replace(/\\\\/g, '\\') /* keep only escaped \ */
|
|
168
|
+
);
|
|
169
|
+
const name = entries[0];
|
|
170
|
+
const params = entries.slice(1);
|
|
171
|
+
|
|
172
|
+
let result = '';
|
|
173
|
+
try {
|
|
174
|
+
if (!name) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
this._history.delete(command.trim());
|
|
179
|
+
this._history.add(command.trim());
|
|
180
|
+
|
|
181
|
+
const tool = getTool(this._tools, name);
|
|
182
|
+
const {required, optional} = tool.options.params;
|
|
183
|
+
let args = required.concat(optional).reduce((args, arg, index) => {
|
|
184
|
+
args[arg] = params[index];
|
|
185
|
+
return args;
|
|
186
|
+
}, {});
|
|
187
|
+
result = await this.quest.cmd(tool.name, args);
|
|
188
|
+
} catch (ex) {
|
|
189
|
+
result = ex.stack || ex.message || ex;
|
|
190
|
+
} finally {
|
|
191
|
+
if (result.length) {
|
|
192
|
+
result += '\n';
|
|
193
|
+
}
|
|
194
|
+
await this.endCommand(result);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async endCommand(result) {
|
|
199
|
+
this.logic.endCommand(result);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async askForCompletion(input) {
|
|
203
|
+
const prompt = getPrompt(this.user);
|
|
204
|
+
const tools = Object.keys(this._tools).filter((tool) =>
|
|
205
|
+
tool.startsWith(input)
|
|
206
|
+
);
|
|
207
|
+
this.logic.askForCompletion(prompt, input, tools);
|
|
208
|
+
|
|
209
|
+
const {completion} = this.state;
|
|
210
|
+
if (completion) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const items = input.trim().split(' ');
|
|
215
|
+
const tool = items[0];
|
|
216
|
+
if (!(tool in this._tools)) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const name = this._tools[tool].name.split('.', 1)[0];
|
|
221
|
+
if (!(`$${name}` in this._tools)) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const desc = await this.quest.cmd(`${name}.$tool`, {tool});
|
|
226
|
+
if (items.length === 1) {
|
|
227
|
+
this.logic.askForCompletion(prompt, input, Object.keys(desc));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const cmds = Object.keys(desc)
|
|
232
|
+
.filter((option) => option.startsWith(items[1]))
|
|
233
|
+
.map((option) => `${tool} ${option}`);
|
|
234
|
+
this.logic.askForCompletion(prompt, input.trim(), cmds);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async setFromHistory(up, input) {
|
|
238
|
+
let value;
|
|
239
|
+
input = input.trim();
|
|
240
|
+
if (up && !input) {
|
|
241
|
+
value = this._history.last();
|
|
242
|
+
} else if (input) {
|
|
243
|
+
value = up ? this._history.previous(input) : this._history.next(input);
|
|
244
|
+
}
|
|
245
|
+
if (!up && !value) {
|
|
246
|
+
this.logic.setFromHistory('<empty>');
|
|
247
|
+
} else if (value) {
|
|
248
|
+
this.logic.setFromHistory(value);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async clearCompletion() {
|
|
253
|
+
this.logic.clearCompletion();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/// TOOLS ////////////////////////////////////////////////////////////////////
|
|
257
|
+
|
|
258
|
+
async clear$tool() {
|
|
259
|
+
this.logic.clear$tool();
|
|
260
|
+
return '';
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async man$tool(name) {
|
|
264
|
+
const tool = getTool(this._tools, name);
|
|
265
|
+
let result = '';
|
|
266
|
+
result += ` module: ${tool.info.name} (v${tool.info.version})\n`;
|
|
267
|
+
result += `location: ${tool.location}\n`;
|
|
268
|
+
result += ` usage: ${name} ${tool.options.params.required
|
|
269
|
+
.map((arg) => arg.toUpperCase())
|
|
270
|
+
.join(' ')}\n\n`;
|
|
271
|
+
|
|
272
|
+
/* List first parameter possible values */
|
|
273
|
+
const _name = tool.name.split('.', 1)[0];
|
|
274
|
+
if (`$${_name}` in this._tools) {
|
|
275
|
+
const desc = await this.quest.cmd(`${_name}.$tool`, {tool: name});
|
|
276
|
+
const {required} = tool.options.params;
|
|
277
|
+
if (required[0]) {
|
|
278
|
+
result += `${required[0].toUpperCase()} ${Object.keys(desc)
|
|
279
|
+
.filter((option) => option[0] !== '$')
|
|
280
|
+
.join(`\n${new Array(required[0].length + 2).join(' ')}`)}`;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async $tool(tool) {
|
|
288
|
+
if (tool === 'man') {
|
|
289
|
+
return {...this._tools};
|
|
290
|
+
}
|
|
291
|
+
return {};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
295
|
+
|
|
296
|
+
dispose() {
|
|
297
|
+
if (this._unsub) {
|
|
298
|
+
this._unsub();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
module.exports = {Termux, TermuxLogic};
|
package/package.json
CHANGED
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "goblin-laboratory",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.7.0",
|
|
4
4
|
"description": "Laboratory",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
7
7
|
},
|
|
8
8
|
"main": "lib/index.js",
|
|
9
|
-
"author": "",
|
|
9
|
+
"author": "Epsitec SA",
|
|
10
|
+
"contributors": [
|
|
11
|
+
"Samuel Loup <loup@epsitec.ch>",
|
|
12
|
+
"Mathieu Schroeter <schroeter@epsitec.ch>",
|
|
13
|
+
"Yannick Vessaz <vessaz@epsitec.ch>",
|
|
14
|
+
"Gillian Fries <fries@epsitec.ch>",
|
|
15
|
+
"Jonny Quarta <quarta@epsitec.ch>",
|
|
16
|
+
"Catia Guidi <guidi@epsitec.ch>",
|
|
17
|
+
"Pierre Arnaud <arnaud@epsitec.ch>",
|
|
18
|
+
"Michael Walz <walz@epsitec.ch>",
|
|
19
|
+
"Jonathan Houmard <houmard@epsitec.ch>",
|
|
20
|
+
"Louis-Samuel Leuenberger <leuenberger@epsitec.ch>"
|
|
21
|
+
],
|
|
10
22
|
"license": "MIT",
|
|
11
23
|
"config": {
|
|
12
24
|
"xcraft": {
|
|
@@ -19,7 +31,8 @@
|
|
|
19
31
|
"xcraft-core-log": "^2.2.0",
|
|
20
32
|
"xcraft-core-probe": "^2.0.0",
|
|
21
33
|
"xcraft-core-transport": "^4.0.0",
|
|
22
|
-
"xcraft-core-stones": "^0.4.16"
|
|
34
|
+
"xcraft-core-stones": "^0.4.16",
|
|
35
|
+
"xcraft-core-utils": "^4.19.0"
|
|
23
36
|
},
|
|
24
37
|
"devDependencies": {
|
|
25
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,227 @@
|
|
|
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
|
+
sendCommand = (command) => {
|
|
68
|
+
this.doFor('termux', 'beginCommand', {command});
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
setCliFocus = () => {
|
|
72
|
+
setTimeout(() => this.inputRef.current?.focus());
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
handleKeyDown = (event) => {
|
|
76
|
+
switch (event.key) {
|
|
77
|
+
/* Valid the command line */
|
|
78
|
+
case 'Enter': {
|
|
79
|
+
const {value} = this.state;
|
|
80
|
+
this.sendCommand(value);
|
|
81
|
+
this.setState({value: ''});
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
/* Show or hide the console */
|
|
85
|
+
case 'F12': {
|
|
86
|
+
if (event.altKey) {
|
|
87
|
+
event.preventDefault();
|
|
88
|
+
this.toggleConsole();
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
/* Try to autocomplete */
|
|
93
|
+
case 'Tab': {
|
|
94
|
+
event.preventDefault();
|
|
95
|
+
this.askForCompletion();
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
/* Move into the command history */
|
|
99
|
+
case 'ArrowUp':
|
|
100
|
+
case 'ArrowDown': {
|
|
101
|
+
event.preventDefault();
|
|
102
|
+
this.setFromHistory(event.key === 'ArrowUp');
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
/* [a] move to the begining, [e] move to the end */
|
|
106
|
+
case 'a':
|
|
107
|
+
case 'e': {
|
|
108
|
+
if (event.ctrlKey) {
|
|
109
|
+
event.preventDefault();
|
|
110
|
+
if (event.key === 'a') {
|
|
111
|
+
this.inputRef.current.setSelectionRange(0, 0);
|
|
112
|
+
} else if (event.key === 'e') {
|
|
113
|
+
const {length} = this.inputRef.current.value;
|
|
114
|
+
this.inputRef.current.setSelectionRange(length, length);
|
|
115
|
+
}
|
|
116
|
+
this.inputRef.current.focus();
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
/* Erase the whole line */
|
|
121
|
+
case 'u': {
|
|
122
|
+
if (event.ctrlKey) {
|
|
123
|
+
this.setState({value: ''});
|
|
124
|
+
}
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
/* Remove the previous word */
|
|
128
|
+
case 'w': {
|
|
129
|
+
if (event.ctrlKey) {
|
|
130
|
+
const values = this.state.value.split(' ');
|
|
131
|
+
const value = values.slice(0, -1).join(' ');
|
|
132
|
+
this.setState({value});
|
|
133
|
+
}
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* Handle scroll with a timeout because (just after the send, the new state
|
|
139
|
+
* is not available)
|
|
140
|
+
*/
|
|
141
|
+
this.scrollToBottom();
|
|
142
|
+
if (this.scrollTimeout) {
|
|
143
|
+
clearTimeout(this.scrollTimeout);
|
|
144
|
+
}
|
|
145
|
+
this.scrollTimeout = setTimeout(() => {
|
|
146
|
+
this.scrollToBottom();
|
|
147
|
+
}, 100);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
renderCli() {
|
|
151
|
+
const {prompt, busy} = this.props;
|
|
152
|
+
return (
|
|
153
|
+
<div className="cli">
|
|
154
|
+
{busy ? (
|
|
155
|
+
<span> </span>
|
|
156
|
+
) : (
|
|
157
|
+
<>
|
|
158
|
+
<span>{prompt} </span>
|
|
159
|
+
<input
|
|
160
|
+
className="input"
|
|
161
|
+
type="text"
|
|
162
|
+
onKeyDown={this.handleKeyDown}
|
|
163
|
+
autoFocus
|
|
164
|
+
ref={this.inputRef}
|
|
165
|
+
value={this.state.value}
|
|
166
|
+
onChange={(e) => this.setState({value: e.target.value})}
|
|
167
|
+
></input>
|
|
168
|
+
</>
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
renderHistory() {
|
|
175
|
+
return (
|
|
176
|
+
<div className="history" onMouseDown={this.setCliFocus}>
|
|
177
|
+
{this.props.history.reverse().map((row, index) => (
|
|
178
|
+
<span key={index}>
|
|
179
|
+
{row.split('\n').map((row, index) => (
|
|
180
|
+
<span key={index}>
|
|
181
|
+
{row.replaceAll(' ', ' ')}
|
|
182
|
+
<br />
|
|
183
|
+
</span>
|
|
184
|
+
))}
|
|
185
|
+
</span>
|
|
186
|
+
))}
|
|
187
|
+
</div>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
renderConsole() {
|
|
192
|
+
return (
|
|
193
|
+
<>
|
|
194
|
+
{this.renderHistory()}
|
|
195
|
+
{this.renderCli()}
|
|
196
|
+
</>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
render() {
|
|
201
|
+
const {children} = this.props;
|
|
202
|
+
const {show} = this.state;
|
|
203
|
+
return (
|
|
204
|
+
<>
|
|
205
|
+
{children}
|
|
206
|
+
<div className={this.styles.classNames.console} data-show={show}>
|
|
207
|
+
{show ? this.renderConsole() : null}
|
|
208
|
+
</div>
|
|
209
|
+
</>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const Termux = Widget.connect((state, props) => {
|
|
215
|
+
const termux = state.get('backend').get('termux');
|
|
216
|
+
if (!termux) {
|
|
217
|
+
return {prompt: '~ $', busy: true, history: [], completion: ''};
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
prompt: termux.get('prompt', '~ $'),
|
|
221
|
+
busy: termux.get('busy', false),
|
|
222
|
+
history: termux.get('history', []),
|
|
223
|
+
completion: termux.get('completion', ''),
|
|
224
|
+
};
|
|
225
|
+
})(TermuxNC);
|
|
226
|
+
|
|
227
|
+
export default Termux;
|