All: move some fns to core. Major cleanup.
[jackhill/mal.git] / js / josh_readline.js
CommitLineData
31690700
JM
1/* ------------------------------------------------------------------------*
2 * Copyright 2013 Arne F. Claassen
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 *-------------------------------------------------------------------------*/
16
17var Josh = Josh || {};
18(function(root, $, _) {
19 Josh.Shell = function(config) {
20 config = config || {};
21
22 // instance fields
23 var _console = config.console || (Josh.Debug && root.console ? root.console : {
24 log: function() {
25 }
26 });
27 var _prompt = config.prompt || 'jsh$';
28 var _action = config.action || function(str) {
29 return "<div>No action defined for: " + str + "</div>";
30 };
31 var _shell_view_id = config.shell_view_id || 'shell-view';
32 var _shell_panel_id = config.shell_panel_id || 'shell-panel';
33 var _input_id = config.input_id || 'shell-cli';
34 var _blinktime = config.blinktime || 500;
35 var _history = config.history || new Josh.History();
36 var _readline = config.readline || new Josh.ReadLine({history: _history, console: _console});
37 var _active = false;
38 var _cursor_visible = false;
39 var _activationHandler;
40 var _deactivationHandler;
41 var _cmdHandlers = {
42 clear: {
43 exec: function(cmd, args, callback) {
44 $(id(_input_id)).parent().empty();
45 callback();
46 }
47 },
48 help: {
49 exec: function(cmd, args, callback) {
50 callback(self.templates.help({commands: commands()}));
51 }
52 },
53 history: {
54 exec: function(cmd, args, callback) {
55 if(args[0] == "-c") {
56 _history.clear();
57 callback();
58 return;
59 }
60 callback(self.templates.history({items: _history.items()}));
61 }
62 },
63 _default: {
64 exec: function(cmd, args, callback) {
65 callback(self.templates.bad_command({cmd: cmd}));
66 },
67 completion: function(cmd, arg, line, callback) {
68 if(!arg) {
69 arg = cmd;
70 }
71 return callback(self.bestMatch(arg, self.commands()))
72 }
73 }
74 };
75 var _line = {
76 text: '',
77 cursor: 0
78 };
79 var _searchMatch = '';
80 var _view, _panel;
81 var _promptHandler;
82 var _initializationHandler;
83 var _initialized;
84
85 // public methods
86 var self = {
87 commands: commands,
88 templates: {
89 history: _.template("<div><% _.each(items, function(cmd, i) { %><div><%- i %>&nbsp;<%- cmd %></div><% }); %></div>"),
90 help: _.template("<div><div><strong>Commands:</strong></div><% _.each(commands, function(cmd) { %><div>&nbsp;<%- cmd %></div><% }); %></div>"),
91 bad_command: _.template('<div><strong>Unrecognized command:&nbsp;</strong><%=cmd%></div>'),
92 input_cmd: _.template('<div id="<%- id %>"><span class="prompt"></span>&nbsp;<span class="input"><span class="left"/><span class="cursor"/><span class="right"/></span></div>'),
31b44161 93 empty_input_cmd: _.template('<div id="<%- id %>"></div>'),
31690700
JM
94 input_search: _.template('<div id="<%- id %>">(reverse-i-search)`<span class="searchterm"></span>\':&nbsp;<span class="input"><span class="left"/><span class="cursor"/><span class="right"/></span></div>'),
95 suggest: _.template("<div><% _.each(suggestions, function(suggestion) { %><div><%- suggestion %></div><% }); %></div>")
96 },
97 isActive: function() {
98 return _readline.isActive();
99 },
100 activate: function() {
101 if($(id(_shell_view_id)).length == 0) {
102 _active = false;
103 return;
104 }
105 _readline.activate();
106 },
107 deactivate: function() {
108 _console.log("deactivating");
109 _active = false;
110 _readline.deactivate();
111 },
112 setCommandHandler: function(cmd, cmdHandler) {
113 _cmdHandlers[cmd] = cmdHandler;
114 },
115 getCommandHandler: function(cmd) {
116 return _cmdHandlers[cmd];
117 },
118 setPrompt: function(prompt) {
119 _prompt = prompt;
120 if(!_active) {
121 return;
122 }
123 self.refresh();
124 },
125 onEOT: function(completionHandler) {
126 _readline.onEOT(completionHandler);
127 },
128 onCancel: function(completionHandler) {
129 _readline.onCancel(completionHandler);
130 },
131 onInitialize: function(completionHandler) {
132 _initializationHandler = completionHandler;
133 },
134 onActivate: function(completionHandler) {
135 _activationHandler = completionHandler;
136 },
137 onDeactivate: function(completionHandler) {
138 _deactivationHandler = completionHandler;
139 },
140 onNewPrompt: function(completionHandler) {
141 _promptHandler = completionHandler;
142 },
143 render: function() {
144 var text = _line.text || '';
145 var cursorIdx = _line.cursor || 0;
146 if(_searchMatch) {
147 cursorIdx = _searchMatch.cursoridx || 0;
148 text = _searchMatch.text || '';
149 $(id(_input_id) + ' .searchterm').text(_searchMatch.term);
150 }
151 var left = _.escape(text.substr(0, cursorIdx)).replace(/ /g, '&nbsp;');
152 var cursor = text.substr(cursorIdx, 1);
153 var right = _.escape(text.substr(cursorIdx + 1)).replace(/ /g, '&nbsp;');
154 $(id(_input_id) + ' .prompt').html(_prompt);
155 $(id(_input_id) + ' .input .left').html(left);
156 if(!cursor) {
157 $(id(_input_id) + ' .input .cursor').html('&nbsp;').css('textDecoration', 'underline');
158 } else {
159 $(id(_input_id) + ' .input .cursor').text(cursor).css('textDecoration', 'underline');
160 }
161 $(id(_input_id) + ' .input .right').html(right);
162 _cursor_visible = true;
163 self.scrollToBottom();
164 _console.log('rendered "' + text + '" w/ cursor at ' + cursorIdx);
165 },
166 refresh: function() {
167 $(id(_input_id)).replaceWith(self.templates.input_cmd({id:_input_id}));
168 self.render();
169 _console.log('refreshed ' + _input_id);
170
171 },
31b44161
JM
172 println: function(text) {
173 var lines = text.split(/\n/);
174 for (var i=0; i<lines.length; i++) {
175 var line = lines[i];
176 if (line == "\\n") {
177 continue;
178 }
179 $(id(_input_id)).after(line);
180 $(id(_input_id) + ' .input .cursor').css('textDecoration', '');
181 $(id(_input_id)).removeAttr('id');
182 $(id(_shell_view_id)).append(self.templates.empty_input_cmd({id:_input_id}));
183 }
184 },
31690700
JM
185 scrollToBottom: function() {
186 _panel.animate({scrollTop: _view.height()}, 0);
187 },
188 bestMatch: function(partial, possible) {
189 _console.log("bestMatch on partial '" + partial + "'");
190 var result = {
191 completion: null,
192 suggestions: []
193 };
194 if(!possible || possible.length == 0) {
195 return result;
196 }
197 var common = '';
198 if(!partial) {
199 if(possible.length == 1) {
200 result.completion = possible[0];
201 result.suggestions = possible;
202 return result;
203 }
204 if(!_.every(possible, function(x) {
205 return possible[0][0] == x[0]
206 })) {
207 result.suggestions = possible;
208 return result;
209 }
210 }
211 for(var i = 0; i < possible.length; i++) {
212 var option = possible[i];
213 if(option.slice(0, partial.length) == partial) {
214 result.suggestions.push(option);
215 if(!common) {
216 common = option;
217 _console.log("initial common:" + common);
218 } else if(option.slice(0, common.length) != common) {
219 _console.log("find common stem for '" + common + "' and '" + option + "'");
220 var j = partial.length;
221 while(j < common.length && j < option.length) {
222 if(common[j] != option[j]) {
223 common = common.substr(0, j);
224 break;
225 }
226 j++;
227 }
228 }
229 }
230 }
231 result.completion = common.substr(partial.length);
232 return result;
233 }
234 };
235
236 function id(id) {
237 return "#"+id;
238 }
239
240 function commands() {
241 return _.chain(_cmdHandlers).keys().filter(function(x) {
242 return x[0] != "_"
243 }).value();
244 }
245
246 function blinkCursor() {
247 if(!_active) {
248 return;
249 }
250 root.setTimeout(function() {
251 if(!_active) {
252 return;
253 }
254 _cursor_visible = !_cursor_visible;
255 if(_cursor_visible) {
256 $(id(_input_id) + ' .input .cursor').css('textDecoration', 'underline');
257 } else {
258 $(id(_input_id) + ' .input .cursor').css('textDecoration', '');
259 }
260 blinkCursor();
261 }, _blinktime);
262 }
263
264 function split(str) {
265 return _.filter(str.split(/\s+/), function(x) {
266 return x;
267 });
268 }
269
270 function getHandler(cmd) {
271 return _cmdHandlers[cmd] || _cmdHandlers._default;
272 }
273
274 function renderOutput(output, callback) {
275 if(output) {
276 $(id(_input_id)).after(output);
277 }
278 $(id(_input_id) + ' .input .cursor').css('textDecoration', '');
279 $(id(_input_id)).removeAttr('id');
280 $(id(_shell_view_id)).append(self.templates.input_cmd({id:_input_id}));
281 if(_promptHandler) {
282 return _promptHandler(function(prompt) {
283 self.setPrompt(prompt);
284 return callback();
285 });
286 }
287 return callback();
288 }
289
290 function activate() {
291 _console.log("activating shell");
292 if(!_view) {
293 _view = $(id(_shell_view_id));
294 }
295 if(!_panel) {
296 _panel = $(id(_shell_panel_id));
297 }
298 if($(id(_input_id)).length == 0) {
299 _view.append(self.templates.input_cmd({id:_input_id}));
300 }
301 self.refresh();
302 _active = true;
303 blinkCursor();
304 if(_promptHandler) {
305 _promptHandler(function(prompt) {
306 self.setPrompt(prompt);
307 })
308 }
309 if(_activationHandler) {
310 _activationHandler();
311 }
312 }
313
314 // init
315 _readline.onActivate(function() {
316 if(!_initialized) {
317 _initialized = true;
318 if(_initializationHandler) {
319 return _initializationHandler(activate);
320 }
321 }
322 return activate();
323 });
324 _readline.onDeactivate(function() {
325 if(_deactivationHandler) {
326 _deactivationHandler();
327 }
328 });
329 _readline.onChange(function(line) {
330 _line = line;
331 self.render();
332 });
333 _readline.onClear(function() {
334 _cmdHandlers.clear.exec(null, null, function() {
335 renderOutput(null, function() {
336 });
337 });
338 });
339 _readline.onSearchStart(function() {
340 $(id(_input_id)).replaceWith(self.templates.input_search({id:_input_id}));
341 _console.log('started search');
342 });
343 _readline.onSearchEnd(function() {
344 $(id(_input_id)).replaceWith(self.templates.input_cmd({id:_input_id}));
345 _searchMatch = null;
346 self.render();
347 _console.log("ended search");
348 });
349 _readline.onSearchChange(function(match) {
350 _searchMatch = match;
351 self.render();
352 });
353 _readline.onEnter(function(cmdtext, callback) {
354 _console.log("got command: " + cmdtext);
355 var result;
356 try {
357 result = "<div>" + _action(cmdtext) + "</div>";
358 } catch (e) {
359 result = "<div>" + e.stack + "</div>";
360 }
361 renderOutput(result, function() {
362 callback("");
363 });
364 });
365 _readline.onCompletion(function(line, callback) {
366 if(!line) {
367 return callback();
368 }
369 var text = line.text.substr(0, line.cursor);
370 var parts = split(text);
371
372 var cmd = parts.shift() || '';
373 var arg = parts.pop() || '';
374 _console.log("getting completion handler for " + cmd);
375 var handler = getHandler(cmd);
376 if(handler != _cmdHandlers._default && cmd && cmd == text) {
377
378 _console.log("valid cmd, no args: append space");
379 // the text to complete is just a valid command, append a space
380 return callback(' ');
381 }
382 if(!handler.completion) {
383 // handler has no completion function, so we can't complete
384 return callback();
385 }
386 _console.log("calling completion handler for " + cmd);
387 return handler.completion(cmd, arg, line, function(match) {
388 _console.log("completion: " + JSON.stringify(match));
389 if(!match) {
390 return callback();
391 }
392 if(match.suggestions && match.suggestions.length > 1) {
393 return renderOutput(self.templates.suggest({suggestions: match.suggestions}), function() {
394 callback(match.completion);
395 });
396 }
397 return callback(match.completion);
398 });
399 });
400 return self;
401 }
402})(this, $, _);
403
404var readline = {};
405readline.rlwrap = function(action) {
406 var history = new Josh.History({ key: 'josh.helloworld'});
407 var shell = Josh.Shell({history: history,
408 action: action});
409 var promptCounter = 0;
410 shell.onNewPrompt(function(callback) {
31b44161
JM
411 promptCounter++;
412 callback("user>");
31690700
JM
413 });
414 shell.activate();
31b44161
JM
415
416 // map output/print to josh.js output
417 readline.println = function () {
418 shell.println(Array.prototype.slice.call(arguments).join(" "));
419 };
31690700 420}