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