UNPKG

9.16 kBJavaScriptView Raw
1"use strict";
2
3/**
4 * Module dependencies.
5 */
6
7var EventEmitter = require("events").EventEmitter
8 , os = require("os")
9 , _ = require("lodash")
10 ;
11
12/**
13 * Initialize a new `Session` instance.
14 *
15 * @param {String} name
16 * @return {Session}
17 * @api public
18 */
19
20function Session(options) {
21 options = options || {};
22 this.id = options.id || this._guid();
23 this.parent = options.parent || void 0;
24 this.authenticating = options.authenticating || false;
25 this.authenticated = options.authenticated || void 0;
26 this.user = options.user || "guest";
27 this.host = options.host;
28 this.address = options.address || void 0;
29 this._isLocal = options.local || void 0;
30 this._delimiter = options.delimiter || String(os.hostname()).split(".")[0] + "~$";
31 this._modeDelimiter = void 0;
32
33 // Keeps history of how many times in a row `tab` was
34 // pressed on the keyboard.
35 this._tabCtr = 0;
36
37 // Prompt Command History
38 // Histctr moves based on number of times "up" (+= ctr)
39 // or "down" (-= ctr) was pressed in traversing
40 // command history.
41 this._hist = [];
42 this._histCtr = 0;
43
44 // When in a "mode", we reset the
45 // history and store it in a cache until
46 // exiting the "mode", at which point we
47 // resume the original history.
48 this._histCache = [];
49 this._histCtrCache = 0;
50
51 // Special command mode vantage is in at the moment,
52 // such as REPL. See mode documentation.
53 this._mode = void 0;
54
55 return this;
56}
57
58/**
59 * Session prototype.
60 */
61
62var session = Session.prototype;
63
64/**
65 * Expose `Session`.
66 */
67
68module.exports = exports = Session;
69
70/**
71 * Extend Session prototype as an event emitter.
72 */
73
74Session.prototype.__proto__ = EventEmitter.prototype;
75
76/**
77 * Routes logging for a given session.
78 * is on a local TTY, or remote.
79 *
80 * @param {String} [... arguments]
81 * @return {Session}
82 * @api public
83 */
84
85session.log = function() {
86 var self = this;
87 if (this.isLocal()) {
88 this.parent.ui.log.apply(this.parent.ui, arguments);
89 } else {
90 // If it"s an error, expose the stack. Otherwise
91 // we get a helpful "{}".
92 var args = [];
93 for (var i = 0; i < arguments.length; ++i) {
94 var str = arguments[i];
95 str = (str && str.stack) ? "Error: " + str.message : str;
96 args.push(str);
97 }
98 self.parent._send("vantage-ssn-stdout-downstream", "downstream", { sessionId: self.id, value: args });
99 }
100 return this;
101};
102
103/**
104 * Returns whether given session
105 * is on a local TTY, or remote.
106 *
107 * @return {Boolean}
108 * @api public
109 */
110
111session.isLocal = function() {
112 return this._isLocal;
113};
114
115/**
116 * Maps to vantage.prompt for a session
117 * context.
118 *
119 * @param {Object} options
120 * @param {Function} cb
121 * @api public
122 */
123
124session.prompt = function(options, cb) {
125 options = options || {};
126 options.sessionId = this.id;
127 return this.parent.prompt(options, cb);
128};
129
130/**
131 * Gets the full (normal + mode) delimiter
132 * for this session.
133 *
134 * @return {String}
135 * @api public
136 */
137
138session.fullDelimiter = function() {
139 var result = this._delimiter
140 + ((this._modeDelimiter !== undefined) ? this._modeDelimiter : "");
141 return result;
142};
143
144/**
145 * Sets the delimiter for this session.
146 *
147 * @param {String} str
148 * @return {Session}
149 * @api public
150 */
151
152session.delimiter = function(str) {
153 if (str === undefined) {
154 return this._delimiter;
155 } else {
156 this._delimiter = String(str).trim() + " ";
157 if (this.isLocal()) {
158 this.parent.ui.refresh();
159 } else {
160 this.parent._send("vantage-delimiter-downstream", "downstream", { value: str, sessionId: this.id });
161 }
162 return this;
163 }
164};
165
166/**
167 * Sets the mode delimiter for this session.
168 *
169 * @param {String} str
170 * @return {Session}
171 * @api public
172 */
173
174session.modeDelimiter = function(str) {
175 var self = this;
176 if (str === undefined) {
177 return this._modeDelimiter;
178 } else {
179 if (!this.isLocal()) {
180 self.parent._send("vantage-mode-delimiter-downstream", "downstream", { value: str, sessionId: self.id });
181 } else {
182 if (str === false || str === "false") {
183 this._modeDelimiter = void 0;
184 } else {
185 this._modeDelimiter = String(str).trim() + " ";
186 }
187 this.parent.ui.refresh();
188 }
189 return this;
190 }
191};
192
193/**
194 * Returns the result of a keypress
195 * string, depending on the type.
196 *
197 * @param {String} key
198 * @param {String} value
199 * @return {Function}
200 * @api private
201 */
202
203session.getKeypressResult = function(key, value, cb) {
204 cb = cb || function() {}
205 var keyMatch = (["up", "down", "tab"].indexOf(key) > -1);
206 if (key !== "tab") {
207 this._tabCtr = 0;
208 }
209 if (keyMatch) {
210 if (["up", "down"].indexOf(key) > -1) {
211 cb(void 0, this.getHistory(key));
212 } else if (key === "tab") {
213 this.getAutocomplete(value, function(err, data) {
214 cb(err, data);
215 });
216 }
217 } else {
218 this._histCtr = 0;
219 }
220};
221
222session.history = function(str) {
223 var exceptions = [];
224 if (str && exceptions.indexOf(String(str).toLowerCase()) === -1) {
225 this._hist.push(str);
226 }
227};
228
229
230/**
231 * Handles tab-completion. Takes a partial
232 * string as "he" and fills it in to "help", etc.
233 * Works the same as a linux terminal"s auto-complete.
234 *
235 * If the user has typed beyond than a listed command,
236 * it will look up that command and check if it has
237 * an `autocompletion` function, and will return
238 * that instead.
239 *
240 * @param {String} str
241 * @return {String}
242 * @api private
243 */
244
245session.getAutocomplete = function(str, cb) {
246 cb = cb || function() {}
247 var typed = String(str);
248 var names = _.pluck(this.parent.commands, "_name");
249
250 var match = void 0;
251 var extra = void 0;
252 names.forEach(function(name){
253 if (typed.substr(0, name.length) === name) {
254 match = name;
255 extra = typed.substr(name.length, typed.length);
256 }
257 });
258
259 var command = (match)
260 ? _.findWhere(this.parent.commands, { _name: match })
261 : void 0;
262
263 if (!command) {
264 command = _.findWhere(this.parent.commands, { _catch: true });
265 }
266
267 if (command && _.isFunction(command._autocompletion)) {
268 this._tabCtr++;
269 command._autocompletion.call(this, extra, this._tabCtr, cb);
270 } else {
271 cb(void 0, this._autocomplete(str, names));
272 }
273};
274
275/**
276 * Independent / stateless auto-complete function.
277 * Parses an array of strings for the best match.
278 *
279 * @param {String} str
280 * @param {Array} arr
281 * @return {String}
282 * @api private
283 */
284
285session._autocomplete = function(str, arr) {
286 arr.sort();
287 var arrX = _.clone(arr);
288 var strX = String(str);
289
290 var go = function() {
291 var matches = [];
292 for (var i = 0; i < arrX.length; i++) {
293 if (arrX[i].slice(0, strX.length).toLowerCase() === strX.toLowerCase()) {
294 matches.push(arrX[i]);
295 }
296 }
297 if (matches.length === 1) {
298 return matches[0] + " ";
299 } else if (matches.length === 0) {
300 return void 0;
301 } else {
302 var furthest = strX;
303 for (var k = strX.length; k < matches[0].length; ++k) {
304 var curr = String(matches[0].slice(0, k)).toLowerCase();
305 var same = 0;
306 for (var j = 0; j < matches.length; ++j) {
307 var sliced = String(matches[j].slice(0, curr.length)).toLowerCase();
308 if (sliced === curr) {
309 same++;
310 }
311 }
312 if (same === matches.length) {
313 furthest = curr;
314 continue;
315 } else {
316 break;
317 }
318 }
319 if (furthest !== strX) {
320 return furthest;
321 } else {
322 return void 0;
323 }
324 }
325 };
326
327 return go();
328};
329
330/**
331 * Public facing autocomplete helper.
332 *
333 * @param {String} str
334 * @param {Array} arr
335 * @return {String}
336 * @api public
337 */
338
339session.help = function(command) {
340 this.log(this.parent._commandHelp(command || ''))
341};
342
343/**
344 * Public facing autocomplete helper.
345 *
346 * @param {String} str
347 * @param {Array} arr
348 * @return {String}
349 * @api public
350 */
351
352session.match = function(str, arr) {
353 return this._autocomplete(str, arr);
354};
355
356/**
357 * Returns the appropriate command history
358 * string based on an "Up" or "Down" arrow
359 * key pressed by the user.
360 *
361 * @param {String} direction
362 * @return {String}
363 * @api private
364 */
365
366session.getHistory = function(direction) {
367 if (direction === "up") {
368 this._histCtr++;
369 this._histCtr = (this._histCtr > this._hist.length) ? this._hist.length : this._histCtr;
370 } else if (direction === "down") {
371 this._histCtr--;
372 this._histCtr = (this._histCtr < 1) ? 1 : this._histCtr;
373 }
374 return this._hist[this._hist.length - (this._histCtr)];
375};
376
377/**
378 * Generates random GUID for Session ID.
379 *
380 * @return {GUID}
381 * @api private
382 */
383
384session._guid = function() {
385 function s4() {
386 return Math.floor((1 + Math.random()) * 0x10000)
387 .toString(16)
388 .substring(1);
389 }
390 return s4() + s4() + "-" + s4() + "-" + s4() + "-" +
391 s4() + "-" + s4() + s4() + s4();
392};