Commit | Line | Data |
---|---|---|
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 | ||
17 | var 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 %> <%- cmd %></div><% }); %></div>"), | |
90 | help: _.template("<div><div><strong>Commands:</strong></div><% _.each(commands, function(cmd) { %><div> <%- cmd %></div><% }); %></div>"), | |
91 | bad_command: _.template('<div><strong>Unrecognized command: </strong><%=cmd%></div>'), | |
92 | input_cmd: _.template('<div id="<%- id %>"><span class="prompt"></span> <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>\': <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, ' '); | |
152 | var cursor = text.substr(cursorIdx, 1); | |
153 | var right = _.escape(text.substr(cursorIdx + 1)).replace(/ /g, ' '); | |
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(' ').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 | ||
404 | var readline = {}; | |
405 | readline.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 | } |