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>'), | |
93 | 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>'), | |
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, ' '); | |
151 | var cursor = text.substr(cursorIdx, 1); | |
152 | var right = _.escape(text.substr(cursorIdx + 1)).replace(/ /g, ' '); | |
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(' ').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 | ||
390 | var readline = {}; | |
391 | readline.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 | } |