OLD | NEW |
| (Empty) |
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 import 'dart:async'; | |
6 import 'dart:convert'; | |
7 import 'dart:io'; | |
8 import 'dart:math'; | |
9 | |
10 import 'terminfo.dart'; | |
11 | |
12 typedef List<String> CommandCompleter(List<String> commandParts); | |
13 | |
14 class Commando { | |
15 // Ctrl keys | |
16 static const runeCtrlA = 0x01; | |
17 static const runeCtrlB = 0x02; | |
18 static const runeCtrlD = 0x04; | |
19 static const runeCtrlE = 0x05; | |
20 static const runeCtrlF = 0x06; | |
21 static const runeTAB = 0x09; | |
22 static const runeNewline = 0x0a; | |
23 static const runeCtrlK = 0x0b; | |
24 static const runeCtrlL = 0x0c; | |
25 static const runeCtrlN = 0x0e; | |
26 static const runeCtrlP = 0x10; | |
27 static const runeCtrlU = 0x15; | |
28 static const runeCtrlY = 0x19; | |
29 static const runeESC = 0x1b; | |
30 static const runeSpace = 0x20; | |
31 static const runeDEL = 0x7F; | |
32 | |
33 StreamController<String> _commandController; | |
34 | |
35 Stream get commands => _commandController.stream; | |
36 | |
37 Commando({consoleIn, | |
38 consoleOut, | |
39 this.prompt : '> ', | |
40 this.completer : null}) { | |
41 _stdin = (consoleIn != null ? consoleIn : stdin); | |
42 _stdout = (consoleOut != null ? consoleOut : stdout); | |
43 _commandController = new StreamController<String>( | |
44 onCancel: _onCancel); | |
45 _stdin.echoMode = false; | |
46 _stdin.lineMode = false; | |
47 _screenWidth = _term.cols - 1; | |
48 _writePrompt(); | |
49 // TODO(turnidge): Handle errors in _stdin here. | |
50 _stdinSubscription = | |
51 _stdin.transform(UTF8.decoder).listen(_handleText, onDone:_done); | |
52 } | |
53 | |
54 Future _onCancel() { | |
55 _stdin.echoMode = true; | |
56 _stdin.lineMode = true; | |
57 var future = _stdinSubscription.cancel(); | |
58 if (future != null) { | |
59 return future; | |
60 } else { | |
61 return new Future.value(); | |
62 } | |
63 } | |
64 | |
65 // Before terminating, call close() to restore terminal settings. | |
66 void _done() { | |
67 _onCancel().then((_) { | |
68 _commandController.close(); | |
69 }); | |
70 } | |
71 | |
72 void _handleText(String text) { | |
73 try { | |
74 if (!_promptShown) { | |
75 _bufferedInput.write(text); | |
76 return; | |
77 } | |
78 | |
79 var runes = text.runes.toList(); | |
80 var pos = 0; | |
81 while (pos < runes.length) { | |
82 if (!_promptShown) { | |
83 // A command was processed which hid the prompt. Buffer | |
84 // the rest of the input. | |
85 // | |
86 // TODO(turnidge): Here and elsewhere in the file I pass | |
87 // runes to String.fromCharCodes. Does this work? | |
88 _bufferedInput.write( | |
89 new String.fromCharCodes(runes.skip(pos))); | |
90 return; | |
91 } | |
92 | |
93 var rune = runes[pos]; | |
94 | |
95 // Count consecutive tabs because double-tab is meaningful. | |
96 if (rune == runeTAB) { | |
97 _tabCount++; | |
98 } else { | |
99 _tabCount = 0; | |
100 } | |
101 | |
102 if (_isControlRune(rune)) { | |
103 pos += _handleControlSequence(runes, pos); | |
104 } else { | |
105 pos += _handleRegularSequence(runes, pos); | |
106 } | |
107 } | |
108 } catch(e, trace) { | |
109 _commandController.addError(e, trace); | |
110 } | |
111 } | |
112 | |
113 int _handleControlSequence(List<int> runes, int pos) { | |
114 var runesConsumed = 1; // Most common result. | |
115 var char = runes[pos]; | |
116 switch (char) { | |
117 case runeCtrlA: | |
118 _home(); | |
119 break; | |
120 | |
121 case runeCtrlB: | |
122 _leftArrow(); | |
123 break; | |
124 | |
125 case runeCtrlD: | |
126 if (_currentLine.length == 0) { | |
127 // ^D on an empty line means quit. | |
128 _stdout.writeln("^D"); | |
129 _done(); | |
130 } else { | |
131 _delete(); | |
132 } | |
133 break; | |
134 | |
135 case runeCtrlE: | |
136 _end(); | |
137 break; | |
138 | |
139 case runeCtrlF: | |
140 _rightArrow(); | |
141 break; | |
142 | |
143 case runeTAB: | |
144 if (_complete(_tabCount > 1)) { | |
145 _tabCount = 0; | |
146 } | |
147 break; | |
148 | |
149 case runeNewline: | |
150 _newline(); | |
151 break; | |
152 | |
153 case runeCtrlK: | |
154 _kill(); | |
155 break; | |
156 | |
157 case runeCtrlL: | |
158 _clearScreen(); | |
159 break; | |
160 | |
161 case runeCtrlN: | |
162 _historyNext(); | |
163 break; | |
164 | |
165 case runeCtrlP: | |
166 _historyPrevious(); | |
167 break; | |
168 | |
169 case runeCtrlU: | |
170 _clearLine(); | |
171 break; | |
172 | |
173 case runeCtrlY: | |
174 _yank(); | |
175 break; | |
176 | |
177 case runeESC: | |
178 // Check to see if this is an arrow key. | |
179 if (pos + 2 < runes.length && // must be a 3 char sequence. | |
180 runes[pos + 1] == 0x5b) { // second char must be '['. | |
181 switch (runes[pos + 2]) { | |
182 case 0x41: // ^[[A = up arrow | |
183 _historyPrevious(); | |
184 runesConsumed = 3; | |
185 break; | |
186 | |
187 case 0x42: // ^[[B = down arrow | |
188 _historyNext(); | |
189 runesConsumed = 3; | |
190 break; | |
191 | |
192 case 0x43: // ^[[C = right arrow | |
193 _rightArrow(); | |
194 runesConsumed = 3; | |
195 break; | |
196 | |
197 case 0x44: // ^[[D = left arrow | |
198 _leftArrow(); | |
199 runesConsumed = 3; | |
200 break; | |
201 | |
202 default: | |
203 // Ignore the escape character. | |
204 break; | |
205 } | |
206 } | |
207 break; | |
208 | |
209 case runeDEL: | |
210 _backspace(); | |
211 break; | |
212 | |
213 default: | |
214 // Ignore the escape character. | |
215 break; | |
216 } | |
217 return runesConsumed; | |
218 } | |
219 | |
220 int _handleRegularSequence(List<int> runes, int pos) { | |
221 var len = pos + 1; | |
222 while (len < runes.length && !_isControlRune(runes[len])) { | |
223 len++; | |
224 } | |
225 _addChars(runes.getRange(pos, len)); | |
226 return len; | |
227 } | |
228 | |
229 bool _isControlRune(int char) { | |
230 return (char >= 0x00 && char < 0x20) || (char == 0x7f); | |
231 } | |
232 | |
233 void _writePromptAndLine() { | |
234 _writePrompt(); | |
235 var pos = _writeRange(_currentLine, 0, _currentLine.length); | |
236 _cursorPos = _move(pos, _cursorPos); | |
237 } | |
238 | |
239 void _writePrompt() { | |
240 _stdout.write(prompt); | |
241 } | |
242 | |
243 void _addChars(Iterable<int> chars) { | |
244 var newLine = []; | |
245 newLine..addAll(_currentLine.take(_cursorPos)) | |
246 ..addAll(chars) | |
247 ..addAll(_currentLine.skip(_cursorPos)); | |
248 _update(newLine, (_cursorPos + chars.length)); | |
249 } | |
250 | |
251 void _backspace() { | |
252 if (_cursorPos == 0) { | |
253 return; | |
254 } | |
255 | |
256 var newLine = []; | |
257 newLine..addAll(_currentLine.take(_cursorPos - 1)) | |
258 ..addAll(_currentLine.skip(_cursorPos)); | |
259 _update(newLine, (_cursorPos - 1)); | |
260 } | |
261 | |
262 void _delete() { | |
263 if (_cursorPos == _currentLine.length) { | |
264 return; | |
265 } | |
266 | |
267 var newLine = []; | |
268 newLine..addAll(_currentLine.take(_cursorPos)) | |
269 ..addAll(_currentLine.skip(_cursorPos + 1)); | |
270 _update(newLine, _cursorPos); | |
271 } | |
272 | |
273 void _home() { | |
274 _updatePos(0); | |
275 } | |
276 | |
277 void _end() { | |
278 _updatePos(_currentLine.length); | |
279 } | |
280 | |
281 void _clearScreen() { | |
282 _stdout.write(_term.clear); | |
283 _term.resize(); | |
284 _screenWidth = _term.cols - 1; | |
285 _writePromptAndLine(); | |
286 } | |
287 | |
288 void _kill() { | |
289 var newLine = []; | |
290 newLine.addAll(_currentLine.take(_cursorPos)); | |
291 _killBuffer = _currentLine.skip(_cursorPos).toList(); | |
292 _update(newLine, _cursorPos); | |
293 } | |
294 | |
295 void _clearLine() { | |
296 _update([], 0); | |
297 } | |
298 | |
299 void _yank() { | |
300 var newLine = []; | |
301 newLine..addAll(_currentLine.take(_cursorPos)) | |
302 ..addAll(_killBuffer) | |
303 ..addAll(_currentLine.skip(_cursorPos)); | |
304 _update(newLine, (_cursorPos + _killBuffer.length)); | |
305 } | |
306 | |
307 static String _trimLeadingSpaces(String line) { | |
308 bool _isSpace(int rune) { | |
309 return rune == runeSpace; | |
310 } | |
311 return new String.fromCharCodes(line.runes.skipWhile(_isSpace)); | |
312 } | |
313 | |
314 static String _sharedPrefix(String one, String two) { | |
315 var len = min(one.length, two.length); | |
316 var runesOne = one.runes.toList(); | |
317 var runesTwo = two.runes.toList(); | |
318 var pos; | |
319 for (pos = 0; pos < len; pos++) { | |
320 if (runesOne[pos] != runesTwo[pos]) { | |
321 break; | |
322 } | |
323 } | |
324 var shared = new String.fromCharCodes(runesOne.take(pos)); | |
325 return shared; | |
326 } | |
327 | |
328 bool _complete(bool showCompletions) { | |
329 if (completer == null) { | |
330 return false; | |
331 } | |
332 | |
333 var linePrefix = _currentLine.take(_cursorPos).toList(); | |
334 List<String> commandParts = | |
335 _trimLeadingSpaces(new String.fromCharCodes(linePrefix)).split(' '); | |
336 List<String> completionList = completer(commandParts); | |
337 var completion = ''; | |
338 | |
339 if (completionList.length == 0) { | |
340 // The current line admits no possible completion. | |
341 return false; | |
342 | |
343 } else if (completionList.length == 1) { | |
344 // There is a single, non-ambiguous completion for the current line. | |
345 completion = completionList[0]; | |
346 | |
347 // If we are at the end of the line, add a space to signal that | |
348 // the completion is unambiguous. | |
349 if (_currentLine.length == _cursorPos) { | |
350 completion = completion + ' '; | |
351 } | |
352 } else { | |
353 // There are ambiguous completions. Find the longest common | |
354 // shared prefix of all of the completions. | |
355 completion = completionList.fold(completionList[0], _sharedPrefix); | |
356 } | |
357 | |
358 var lastWord = commandParts.last; | |
359 if (completion == lastWord) { | |
360 // The completion does not add anything. | |
361 if (showCompletions) { | |
362 // User hit double-TAB. Show them all possible completions. | |
363 _move(_cursorPos, _currentLine.length); | |
364 _stdout.writeln(); | |
365 _stdout.writeln(completionList); | |
366 _writePromptAndLine(); | |
367 } | |
368 return false; | |
369 } else { | |
370 // Apply the current completion. | |
371 var completionRunes = completion.runes.toList(); | |
372 | |
373 var newLine = []; | |
374 newLine..addAll(linePrefix) | |
375 ..addAll(completionRunes.skip(lastWord.length)) | |
376 ..addAll(_currentLine.skip(_cursorPos)); | |
377 _update(newLine, _cursorPos + completionRunes.length - lastWord.length); | |
378 return true; | |
379 } | |
380 } | |
381 | |
382 void _newline() { | |
383 _addLineToHistory(_currentLine); | |
384 _linePos = _lines.length; | |
385 | |
386 _end(); | |
387 _stdout.writeln(); | |
388 | |
389 // Call the user's command handler. | |
390 _commandController.add(new String.fromCharCodes(_currentLine)); | |
391 | |
392 _currentLine = []; | |
393 _cursorPos = 0; | |
394 if (_promptShown) { | |
395 _writePrompt(); | |
396 } | |
397 } | |
398 | |
399 void _leftArrow() { | |
400 _updatePos(_cursorPos - 1); | |
401 } | |
402 | |
403 void _rightArrow() { | |
404 _updatePos(_cursorPos + 1); | |
405 } | |
406 | |
407 void _addLineToHistory(List<int> line) { | |
408 if (_tempLineAdded) { | |
409 _lines.removeLast(); | |
410 _tempLineAdded = false; | |
411 } | |
412 if (line.length > 0) { | |
413 _lines.add(line); | |
414 } | |
415 } | |
416 | |
417 void _addTempLineToHistory(List<int> line) { | |
418 _lines.add(line); | |
419 _tempLineAdded = true; | |
420 } | |
421 | |
422 void _replaceHistory(List<int> line, int linePos) { | |
423 _lines[linePos] = line; | |
424 } | |
425 | |
426 void _historyPrevious() { | |
427 if (_linePos == 0) { | |
428 return; | |
429 } | |
430 | |
431 if (_linePos == _lines.length) { | |
432 // The current in-progress line gets temporarily stored in history. | |
433 _addTempLineToHistory(_currentLine); | |
434 } else { | |
435 // Any edits get committed to history. | |
436 _replaceHistory(_currentLine, _linePos); | |
437 } | |
438 | |
439 _linePos -= 1; | |
440 var line = _lines[_linePos]; | |
441 _update(line, line.length); | |
442 } | |
443 | |
444 void _historyNext() { | |
445 // For the very first command, _linePos (0) will exceed | |
446 // (_lines.length - 1) (-1) so we use a ">=" here instead of an "==". | |
447 if (_linePos >= (_lines.length - 1)) { | |
448 return; | |
449 } | |
450 | |
451 // Any edits get committed to history. | |
452 _replaceHistory(_currentLine, _linePos); | |
453 | |
454 _linePos += 1; | |
455 var line = _lines[_linePos]; | |
456 _update(line, line.length); | |
457 } | |
458 | |
459 void _updatePos(int newCursorPos) { | |
460 if (newCursorPos < 0) { | |
461 return; | |
462 } | |
463 if (newCursorPos > _currentLine.length) { | |
464 return; | |
465 } | |
466 | |
467 _cursorPos = _move(_cursorPos, newCursorPos); | |
468 } | |
469 | |
470 void _update(List<int> newLine, int newCursorPos) { | |
471 var pos = _cursorPos; | |
472 var diffPos; | |
473 var sharedLen = min(_currentLine.length, newLine.length); | |
474 | |
475 // Find first difference. | |
476 for (diffPos = 0; diffPos < sharedLen; diffPos++) { | |
477 if (_currentLine[diffPos] != newLine[diffPos]) { | |
478 break; | |
479 } | |
480 } | |
481 | |
482 // Move the cursor to where the difference begins. | |
483 pos = _move(pos, diffPos); | |
484 | |
485 // Write the new text. | |
486 pos = _writeRange(newLine, pos, newLine.length); | |
487 | |
488 // Clear any extra characters at the end. | |
489 pos = _clearRange(pos, _currentLine.length); | |
490 | |
491 // Move the cursor back to the input point. | |
492 _cursorPos = _move(pos, newCursorPos); | |
493 _currentLine = newLine; | |
494 } | |
495 | |
496 void hide() { | |
497 if (!_promptShown) { | |
498 return; | |
499 } | |
500 _promptShown = false; | |
501 // We need to erase everything, including the prompt. | |
502 var curLine = _getLine(_cursorPos); | |
503 var lastLine = _getLine(_currentLine.length); | |
504 | |
505 // Go to last line. | |
506 if (curLine < lastLine) { | |
507 for (var i = 0; i < (lastLine - curLine); i++) { | |
508 // This moves us to column 0. | |
509 _stdout.write(_term.cursorDown); | |
510 } | |
511 curLine = lastLine; | |
512 } else { | |
513 // Move to column 0. | |
514 _stdout.write('\r'); | |
515 } | |
516 | |
517 // Work our way up, clearing lines. | |
518 while (true) { | |
519 _stdout.write(_term.clrEOL); | |
520 if (curLine > 0) { | |
521 _stdout.write(_term.cursorUp); | |
522 } else { | |
523 break; | |
524 } | |
525 } | |
526 } | |
527 | |
528 void show() { | |
529 if (_promptShown) { | |
530 return; | |
531 } | |
532 _promptShown = true; | |
533 _writePromptAndLine(); | |
534 | |
535 // If input was buffered while the prompt was hidden, process it | |
536 // now. | |
537 if (!_bufferedInput.isEmpty) { | |
538 var input = _bufferedInput.toString(); | |
539 _bufferedInput.clear(); | |
540 _handleText(input); | |
541 } | |
542 } | |
543 | |
544 int _writeRange(List<int> text, int pos, int writeToPos) { | |
545 if (pos >= writeToPos) { | |
546 return pos; | |
547 } | |
548 while (pos < writeToPos) { | |
549 var margin = _nextMargin(pos); | |
550 var limit = min(writeToPos, margin); | |
551 _stdout.write(new String.fromCharCodes(text.getRange(pos, limit))); | |
552 pos = limit; | |
553 if (pos == margin) { | |
554 _stdout.write('\n'); | |
555 } | |
556 } | |
557 return pos; | |
558 } | |
559 | |
560 int _clearRange(int pos, int clearToPos) { | |
561 if (pos >= clearToPos) { | |
562 return pos; | |
563 } | |
564 while (true) { | |
565 var limit = _nextMargin(pos); | |
566 _stdout.write(_term.clrEOL); | |
567 if (limit >= clearToPos) { | |
568 return pos; | |
569 } | |
570 _stdout.write('\n'); | |
571 pos = limit; | |
572 } | |
573 } | |
574 | |
575 int _move(int pos, int newPos) { | |
576 if (pos == newPos) { | |
577 return pos; | |
578 } | |
579 | |
580 var curCol = _getCol(pos); | |
581 var curLine = _getLine(pos); | |
582 var newCol = _getCol(newPos); | |
583 var newLine = _getLine(newPos); | |
584 | |
585 if (curLine > newLine) { | |
586 for (var i = 0; i < (curLine - newLine); i++) { | |
587 _stdout.write(_term.cursorUp); | |
588 } | |
589 } | |
590 if (curLine < newLine) { | |
591 for (var i = 0; i < (newLine - curLine); i++) { | |
592 _stdout.write(_term.cursorDown); | |
593 } | |
594 | |
595 // Moving down resets column to zero, oddly. | |
596 curCol = 0; | |
597 } | |
598 if (curCol > newCol) { | |
599 for (var i = 0; i < (curCol - newCol); i++) { | |
600 _stdout.write(_term.cursorBack); | |
601 } | |
602 } | |
603 if (curCol < newCol) { | |
604 for (var i = 0; i < (newCol - curCol); i++) { | |
605 _stdout.write(_term.cursorForward); | |
606 } | |
607 } | |
608 | |
609 return newPos; | |
610 } | |
611 | |
612 int _nextMargin(int pos) { | |
613 var truePos = pos + prompt.length; | |
614 return ((truePos ~/ _screenWidth) + 1) * _screenWidth - prompt.length; | |
615 } | |
616 | |
617 int _getLine(int pos) { | |
618 var truePos = pos + prompt.length; | |
619 return truePos ~/ _screenWidth; | |
620 } | |
621 | |
622 int _getCol(int pos) { | |
623 var truePos = pos + prompt.length; | |
624 return truePos % _screenWidth; | |
625 } | |
626 | |
627 Stdin _stdin; | |
628 StreamSubscription _stdinSubscription; | |
629 IOSink _stdout; | |
630 final String prompt; | |
631 bool _promptShown = true; | |
632 final CommandCompleter completer; | |
633 TermInfo _term = new TermInfo(); | |
634 | |
635 // TODO(turnidge): See if we can get screen resize events. | |
636 int _screenWidth; | |
637 List<int> _currentLine = []; // A list of runes. | |
638 StringBuffer _bufferedInput = new StringBuffer(); | |
639 List<List<int>> _lines = []; | |
640 | |
641 // When using the command history, the current line is temporarily | |
642 // added to the history to allow the user to return to it. This | |
643 // values tracks whether the history has a temporary line at the end. | |
644 bool _tempLineAdded = false; | |
645 int _linePos = 0; | |
646 int _cursorPos = 0; | |
647 int _tabCount = 0; | |
648 List<int> _killBuffer = []; | |
649 } | |
650 | |
651 | |
652 // Demo code. | |
653 | |
654 | |
655 List<String> _myCompleter(List<String> commandTokens) { | |
656 List<String> completions = new List<String>(); | |
657 | |
658 // First word completions. | |
659 if (commandTokens.length <= 1) { | |
660 String prefix = ''; | |
661 if (commandTokens.length == 1) { | |
662 prefix = commandTokens.first; | |
663 } | |
664 if ('quit'.startsWith(prefix)) { | |
665 completions.add('quit'); | |
666 } | |
667 if ('help'.startsWith(prefix)) { | |
668 completions.add('help'); | |
669 } | |
670 if ('happyface'.startsWith(prefix)) { | |
671 completions.add('happyface'); | |
672 } | |
673 } | |
674 | |
675 // Complete 'foobar' or 'gondola' anywhere in string. | |
676 String lastWord = commandTokens.last; | |
677 if ('foobar'.startsWith(lastWord)) { | |
678 completions.add('foobar'); | |
679 } | |
680 if ('gondola'.startsWith(lastWord)) { | |
681 completions.add('gondola'); | |
682 } | |
683 | |
684 return completions; | |
685 } | |
686 | |
687 | |
688 int _helpCount = 0; | |
689 Commando cmdo; | |
690 | |
691 | |
692 void _handleCommand(String rawCommand) { | |
693 String command = rawCommand.trim(); | |
694 cmdo.hide(); | |
695 if (command == 'quit') { | |
696 cmdo.close().then((_) { | |
697 print('Exiting'); | |
698 }); | |
699 } else if (command == 'help') { | |
700 switch (_helpCount) { | |
701 case 0: | |
702 print('I will not help you.'); | |
703 break; | |
704 case 1: | |
705 print('I mean it.'); | |
706 break; | |
707 case 2: | |
708 print('Seriously.'); | |
709 break; | |
710 case 100: | |
711 print('Well now.'); | |
712 break; | |
713 default: | |
714 print("Okay. Type 'quit' to quit"); | |
715 break; | |
716 } | |
717 _helpCount++; | |
718 } else if (command == 'happyface') { | |
719 print(':-)'); | |
720 } else { | |
721 print('Received command($command)'); | |
722 } | |
723 cmdo.show(); | |
724 } | |
725 | |
726 | |
727 void main() { | |
728 print('[Commando demo]'); | |
729 cmdo = new Commando(completer:_myCompleter); | |
730 cmdo.commands.listen(_handleCommand); | |
731 } | |
OLD | NEW |