All: split types into types, env, printer, core.
[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>'),
93 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>'),
94 suggest: _.template("<div><% _.each(suggestions, function(suggestion) { %><div><%- suggestion %></div><% }); %></div>")
95 },
96 isActive: function() {
97 return _readline.isActive();
98 },
99 activate: function() {
100 if($(id(_shell_view_id)).length == 0) {
101 _active = false;
102 return;
103 }
104 _readline.activate();
105 },
106 deactivate: function() {
107 _console.log("deactivating");
108 _active = false;
109 _readline.deactivate();
110 },
111 setCommandHandler: function(cmd, cmdHandler) {
112 _cmdHandlers[cmd] = cmdHandler;
113 },
114 getCommandHandler: function(cmd) {
115 return _cmdHandlers[cmd];
116 },
117 setPrompt: function(prompt) {
118 _prompt = prompt;
119 if(!_active) {
120 return;
121 }
122 self.refresh();
123 },
124 onEOT: function(completionHandler) {
125 _readline.onEOT(completionHandler);
126 },
127 onCancel: function(completionHandler) {
128 _readline.onCancel(completionHandler);
129 },
130 onInitialize: function(completionHandler) {
131 _initializationHandler = completionHandler;
132 },
133 onActivate: function(completionHandler) {
134 _activationHandler = completionHandler;
135 },
136 onDeactivate: function(completionHandler) {
137 _deactivationHandler = completionHandler;
138 },
139 onNewPrompt: function(completionHandler) {
140 _promptHandler = completionHandler;
141 },
142 render: function() {
143 var text = _line.text || '';
144 var cursorIdx = _line.cursor || 0;
145 if(_searchMatch) {
146 cursorIdx = _searchMatch.cursoridx || 0;
147 text = _searchMatch.text || '';
148 $(id(_input_id) + ' .searchterm').text(_searchMatch.term);
149 }
150 var left = _.escape(text.substr(0, cursorIdx)).replace(/ /g, '&nbsp;');
151 var cursor = text.substr(cursorIdx, 1);
152 var right = _.escape(text.substr(cursorIdx + 1)).replace(/ /g, '&nbsp;');
153 $(id(_input_id) + ' .prompt').html(_prompt);
154 $(id(_input_id) + ' .input .left').html(left);
155 if(!cursor) {
156 $(id(_input_id) + ' .input .cursor').html('&nbsp;').css('textDecoration', 'underline');
157 } else {
158 $(id(_input_id) + ' .input .cursor').text(cursor).css('textDecoration', 'underline');
159 }
160 $(id(_input_id) + ' .input .right').html(right);
161 _cursor_visible = true;
162 self.scrollToBottom();
163 _console.log('rendered "' + text + '" w/ cursor at ' + cursorIdx);
164 },
165 refresh: function() {
166 $(id(_input_id)).replaceWith(self.templates.input_cmd({id:_input_id}));
167 self.render();
168 _console.log('refreshed ' + _input_id);
169
170 },
171 scrollToBottom: function() {
172 _panel.animate({scrollTop: _view.height()}, 0);
173 },
174 bestMatch: function(partial, possible) {
175 _console.log("bestMatch on partial '" + partial + "'");
176 var result = {
177 completion: null,
178 suggestions: []
179 };
180 if(!possible || possible.length == 0) {
181 return result;
182 }
183 var common = '';
184 if(!partial) {
185 if(possible.length == 1) {
186 result.completion = possible[0];
187 result.suggestions = possible;
188 return result;
189 }
190 if(!_.every(possible, function(x) {
191 return possible[0][0] == x[0]
192 })) {
193 result.suggestions = possible;
194 return result;
195 }
196 }
197 for(var i = 0; i < possible.length; i++) {
198 var option = possible[i];
199 if(option.slice(0, partial.length) == partial) {
200 result.suggestions.push(option);
201 if(!common) {
202 common = option;
203 _console.log("initial common:" + common);
204 } else if(option.slice(0, common.length) != common) {
205 _console.log("find common stem for '" + common + "' and '" + option + "'");
206 var j = partial.length;
207 while(j < common.length && j < option.length) {
208 if(common[j] != option[j]) {
209 common = common.substr(0, j);
210 break;
211 }
212 j++;
213 }
214 }
215 }
216 }
217 result.completion = common.substr(partial.length);
218 return result;
219 }
220 };
221
222 function id(id) {
223 return "#"+id;
224 }
225
226 function commands() {
227 return _.chain(_cmdHandlers).keys().filter(function(x) {
228 return x[0] != "_"
229 }).value();
230 }
231
232 function blinkCursor() {
233 if(!_active) {
234 return;
235 }
236 root.setTimeout(function() {
237 if(!_active) {
238 return;
239 }
240 _cursor_visible = !_cursor_visible;
241 if(_cursor_visible) {
242 $(id(_input_id) + ' .input .cursor').css('textDecoration', 'underline');
243 } else {
244 $(id(_input_id) + ' .input .cursor').css('textDecoration', '');
245 }
246 blinkCursor();
247 }, _blinktime);
248 }
249
250 function split(str) {
251 return _.filter(str.split(/\s+/), function(x) {
252 return x;
253 });
254 }
255
256 function getHandler(cmd) {
257 return _cmdHandlers[cmd] || _cmdHandlers._default;
258 }
259
260 function renderOutput(output, callback) {
261 if(output) {
262 $(id(_input_id)).after(output);
263 }
264 $(id(_input_id) + ' .input .cursor').css('textDecoration', '');
265 $(id(_input_id)).removeAttr('id');
266 $(id(_shell_view_id)).append(self.templates.input_cmd({id:_input_id}));
267 if(_promptHandler) {
268 return _promptHandler(function(prompt) {
269 self.setPrompt(prompt);
270 return callback();
271 });
272 }
273 return callback();
274 }
275
276 function activate() {
277 _console.log("activating shell");
278 if(!_view) {
279 _view = $(id(_shell_view_id));
280 }
281 if(!_panel) {
282 _panel = $(id(_shell_panel_id));
283 }
284 if($(id(_input_id)).length == 0) {
285 _view.append(self.templates.input_cmd({id:_input_id}));
286 }
287 self.refresh();
288 _active = true;
289 blinkCursor();
290 if(_promptHandler) {
291 _promptHandler(function(prompt) {
292 self.setPrompt(prompt);
293 })
294 }
295 if(_activationHandler) {
296 _activationHandler();
297 }
298 }
299
300 // init
301 _readline.onActivate(function() {
302 if(!_initialized) {
303 _initialized = true;
304 if(_initializationHandler) {
305 return _initializationHandler(activate);
306 }
307 }
308 return activate();
309 });
310 _readline.onDeactivate(function() {
311 if(_deactivationHandler) {
312 _deactivationHandler();
313 }
314 });
315 _readline.onChange(function(line) {
316 _line = line;
317 self.render();
318 });
319 _readline.onClear(function() {
320 _cmdHandlers.clear.exec(null, null, function() {
321 renderOutput(null, function() {
322 });
323 });
324 });
325 _readline.onSearchStart(function() {
326 $(id(_input_id)).replaceWith(self.templates.input_search({id:_input_id}));
327 _console.log('started search');
328 });
329 _readline.onSearchEnd(function() {
330 $(id(_input_id)).replaceWith(self.templates.input_cmd({id:_input_id}));
331 _searchMatch = null;
332 self.render();
333 _console.log("ended search");
334 });
335 _readline.onSearchChange(function(match) {
336 _searchMatch = match;
337 self.render();
338 });
339 _readline.onEnter(function(cmdtext, callback) {
340 _console.log("got command: " + cmdtext);
341 var result;
342 try {
343 result = "<div>" + _action(cmdtext) + "</div>";
344 } catch (e) {
345 result = "<div>" + e.stack + "</div>";
346 }
347 renderOutput(result, function() {
348 callback("");
349 });
350 });
351 _readline.onCompletion(function(line, callback) {
352 if(!line) {
353 return callback();
354 }
355 var text = line.text.substr(0, line.cursor);
356 var parts = split(text);
357
358 var cmd = parts.shift() || '';
359 var arg = parts.pop() || '';
360 _console.log("getting completion handler for " + cmd);
361 var handler = getHandler(cmd);
362 if(handler != _cmdHandlers._default && cmd && cmd == text) {
363
364 _console.log("valid cmd, no args: append space");
365 // the text to complete is just a valid command, append a space
366 return callback(' ');
367 }
368 if(!handler.completion) {
369 // handler has no completion function, so we can't complete
370 return callback();
371 }
372 _console.log("calling completion handler for " + cmd);
373 return handler.completion(cmd, arg, line, function(match) {
374 _console.log("completion: " + JSON.stringify(match));
375 if(!match) {
376 return callback();
377 }
378 if(match.suggestions && match.suggestions.length > 1) {
379 return renderOutput(self.templates.suggest({suggestions: match.suggestions}), function() {
380 callback(match.completion);
381 });
382 }
383 return callback(match.completion);
384 });
385 });
386 return self;
387 }
388})(this, $, _);
389
390var readline = {};
391readline.rlwrap = function(action) {
392 var history = new Josh.History({ key: 'josh.helloworld'});
393 var shell = Josh.Shell({history: history,
394 action: action});
395 var promptCounter = 0;
396 shell.onNewPrompt(function(callback) {
397 promptCounter++;
398 //callback("[" + promptCounter + "] $");
399 callback("user>");
400 });
401 shell.activate();
402}