forked from menubboi/HerokuShell-Rclone
-
Notifications
You must be signed in to change notification settings - Fork 13
Expand file tree
/
Copy pathcommand.js
More file actions
264 lines (223 loc) · 8.92 KB
/
Copy pathcommand.js
File metadata and controls
264 lines (223 loc) · 8.92 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
/**
* Attaches to a chat, spawns a pty, attaches it to the terminal emulator
* and the renderer and manages them. Handles incoming commands & input,
* and posts complimentary messages such as command itself and output code.
**/
var util = require("util");
var escapeHtml = require("escape-html");
var pty = require("node-pty");
var termios = require("node-termios");
var utils = require("./utils");
var terminal = require("./terminal");
var renderer = require("./renderer");
var tsyms = termios.native.ALL_SYMBOLS;
function Command(reply, context, command) {
var toUser = reply.destination > 0;
this.startTime = Date.now();
this.reply = reply;
this.command = command;
this.pty = pty.spawn(context.shell, [context.interactive ? "-ic" : "-c", command], {
cols: context.size.columns,
rows: context.size.rows,
cwd: context.cwd,
env: context.env,
});
this.termios = new termios.Termios(this.pty._fd);
this.termios.c_lflag &= ~(tsyms.ISIG | tsyms.IEXTEN);
this.termios.c_lflag &= ~tsyms.ECHO; // disable ECHO
this.termios.c_lflag |= tsyms.ICANON | tsyms.ECHONL; // we need it for /end, it needs to be active beforehand
this.termios.c_iflag = (this.termios.c_iflag & ~(tsyms.INLCR | tsyms.IGNCR)) | tsyms.ICRNL; // CR to NL
this.termios.writeTo(this.pty._fd);
this.terminal = terminal.createTerminal({
columns: context.size.columns,
rows: context.size.rows,
});
this.state = this.terminal.state;
this.renderer = new renderer.Renderer(reply, this.state, {
cursorString: "\uD83D\uDD38",
cursorBlinkString: "\uD83D\uDD38",
hidePreview: !context.linkPreviews,
unfinishedHidePreview: true,
silent: context.silent,
unfinishedSilent: true,
maxLinesWait: toUser ? 20 : 30,
maxLinesEmitted: 30,
lineTime: toUser ? 400 : 1200,
chunkTime: toUser ? 3000 : 6000,
editTime: toUser ? 300 : 2500,
unfinishedTime: toUser ? 1000 : 2000,
startFill: "· ",
});
this._initKeypad();
//FIXME: take additional steps to reduce messages sent to group. do typing actions count?
// Post initial message
this.initialMessage = new utils.EditedMessage(reply, this._renderInitial(), "HTML");
// Process command output
this.pty.on("data", this._ptyData.bind(this));
// Handle command exit
this.pty.on("exit", this._exit.bind(this));
}
util.inherits(Command, require("events").EventEmitter);
Command.prototype._renderInitial = function _renderInitial() {
var content = "", title = this.state.metas.title, badges = this.badges || "";
if (title) {
content += "<strong>" + escapeHtml(title) + "</strong>\n";
content += badges + "<strong>$</strong> " + escapeHtml(this.command);
} else {
content += badges + "<strong>$ " + escapeHtml(this.command) + "</strong>";
}
return content;
}
Command.prototype._ptyData = function _ptyData(chunk) {
//FIXME: implement some backpressure, for example, read smaller chunks, stop reading if there are >= 20 lines waiting to be pushed, set the HWM
if ((typeof chunk !== "string") && !(chunk instanceof String))
throw new Error("Expected a String, you liar.");
this.interacted = true;
this.terminal.write(chunk, "utf-8", this._update.bind(this));
};
Command.prototype._update = function _update() {
this.initialMessage.edit(this._renderInitial());
this.renderer.update();
};
Command.prototype.resize = function resize(size) {
this.interacted = true;
this.metaActive = false;
this.state.resize(size);
this._update();
this.pty.resize(size.columns, size.rows);
};
Command.prototype.redraw = function redraw() {
this.interacted = true;
this.metaActive = false;
this.pty.redraw();
};
Command.prototype.sendSignal = function sendSignal(signal, group) {
this.interacted = true;
this.metaActive = false;
var pid = this.pty.pid;
if (group) pid = -pid;
process.kill(pid, signal);
};
Command.prototype.sendEof = function sendEof() {
this.interacted = true;
this.metaActive = false;
// I don't know how to cause a 'buffer flush to the app' (the effect of Control+D)
// without actually pressing it into the console. So let's do just that.
// TTY needs to be in ICANON mode from the start, enabling it now doesn't work
// write EOF control character
this.termios.loadFrom(this.pty._fd);
this.pty.write(Buffer.from([ this.termios.c_cc[tsyms.VEOF] ]));
};
Command.prototype._exit = function _exit(code, signal) {
this._update();
this.renderer.flushUnfinished();
//FIXME: could wait until all edits are made before posting exited message
if ((Date.now() - this.startTime) < 2000 && !signal && code === 0 && !this.interacted) {
// For short lived commands that completed without output, we simply add a tick to the original message
this.badges = "\u2705 ";
this.initialMessage.edit(this._renderInitial());
} else {
if (signal)
this.reply.html("\uD83D\uDC80 <strong>Killed</strong> by %s.", utils.formatSignal(signal));
else if (code === 0)
this.reply.html("\u2705 <strong>Exited</strong> correctly.");
else
this.reply.html("\u26D4 <strong>Exited</strong> with %s.", code);
}
this._removeKeypad();
this.emit("exit");
};
Command.prototype.handleReply = function handleReply(msg) {
//FIXME: feature: if photo, file, video, voice or music, put the terminal in raw mode, hold off further input, pipe binary asset to terminal, restore
//Flags we would need to touch: -INLCR -IGNCR -ICRNL -IUCLC -ISIG -ICANON -IEXTEN, and also for convenience -ECHO -ECHONL
if (msg.type !== "text") return false;
this.sendInput(msg.text);
};
Command.prototype.sendInput = function sendInput(text, noTerminate) {
this.interacted = true;
text = text.replace(/\n/g, "\r");
if (!noTerminate) text += "\r";
if (this.metaActive) text = "\x1b" + text;
this.pty.write(text);
this.metaActive = false;
};
Command.prototype.toggleMeta = function toggleMeta(metaActive) {
if (metaActive === undefined) metaActive = !this.metaActive;
this.metaActive = metaActive;
};
Command.prototype.setSilent = function setSilent(silent) {
this.renderer.options.silent = silent;
};
Command.prototype.setLinkPreviews = function setLinkPreviews(linkPreviews) {
this.renderer.options.hidePreview = !linkPreviews;
};
Command.prototype._initKeypad = function _initKeypad() {
this.keypadToken = utils.generateToken();
var keys = {
esc: { label: "ESC", content: "\x1b" },
tab: { label: "⇥", content: "\t" },
enter: { label: "⏎", content: "\r" },
backspace: { label: "↤", content: "\x7F" },
space: { label: " ", content: " " },
up: { label: "↑", content: "\x1b[A", appKeypadContent: "\x1bOA" },
down: { label: "↓", content: "\x1b[B", appKeypadContent: "\x1bOB" },
right: { label: "→", content: "\x1b[C", appKeypadContent: "\x1bOC" },
left: { label: "←", content: "\x1b[D", appKeypadContent: "\x1bOD" },
insert: { label: "INS", content: "\x1b[2~" },
del: { label: "DEL", content: "\x1b[3~" },
home: { label: "⇱", content: "\x1bOH" },
end: { label: "⇲", content: "\x1bOF" },
prevPage: { label: "⇈", content: "\x1b[5~" },
nextPage: { label: "⇊", content: "\x1b[6~" },
};
var keypad = [
[ "esc", "up", "backspace", "del" ],
[ "left", "space", "right", "home" ],
[ "tab", "down", "enter", "end" ],
];
this.buttons = [];
this.inlineKeyboard = keypad.map(function (row) {
return row.map(function (name) {
var button = keys[name];
var data = JSON.stringify({ token: this.keypadToken, button: this.buttons.length });
var keyboardButton = { text: button.label, callback_data: data };
this.buttons.push(button);
return keyboardButton;
}.bind(this));
}.bind(this));
this.reply.bot.callback(function (query, next) {
try {
var data = JSON.parse(query.data);
} catch (e) { return next(); }
if (data.token !== this.keypadToken) return next();
this._keypadPressed(data.button, query);
}.bind(this));
};
Command.prototype.toggleKeypad = function toggleKeypad() {
if (this.keypadMessage) {
this.keypadMessage.markup = null;
this.keypadMessage.refresh();
this.keypadMessage = null;
return;
}
// FIXME: this is pretty badly implemented, we should wait until last message (or message with cursor) has known id
var messages = this.renderer.messages;
var msg = messages[messages.length - 1].ref;
msg.markup = {inline_keyboard: this.inlineKeyboard};
msg.refresh();
this.keypadMessage = msg;
};
Command.prototype._keypadPressed = function _keypadPressed(id, query) {
this.interacted = true;
if (typeof id !== "number" || !(id in this.buttons)) return;
var button = this.buttons[id];
var content = button.content;
if (button.appKeypadContent !== undefined && this.state.getMode("appKeypad"))
content = button.appKeypadContent;
this.pty.write(content);
query.answer();
};
Command.prototype._removeKeypad = function _removeKeypad() {
if (this.keypadMessage) this.toggleKeypad();
};
exports.Command = Command;