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