Chromium Code Reviews| Index: tools/ddbg/lib/commando.dart |
| =================================================================== |
| --- tools/ddbg/lib/commando.dart (revision 0) |
| +++ tools/ddbg/lib/commando.dart (revision 0) |
| @@ -0,0 +1,700 @@ |
| +// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
|
hausner
2013/11/21 16:12:03
2013
turnidge
2013/11/21 18:25:11
Done.
|
| +// for details. All rights reserved. Use of this source code is governed by a |
| +// BSD-style license that can be found in the LICENSE file. |
| + |
| +import 'dart:async'; |
| +import 'dart:convert'; |
| +import 'dart:io'; |
| +import 'dart:math'; |
| + |
| +import 'terminfo.dart'; |
| + |
| +typedef List<String> CommandCompleter(List<String> commandParts); |
| + |
| +class Commando { |
| + // Ctrl keys |
| + static const int runeCtrlA = 0x01; |
|
hausner
2013/11/21 19:24:56
I find this to be quite verbose to read. How about
turnidge
2013/11/22 19:59:58
Done.
|
| + static const int runeCtrlB = 0x02; |
| + static const int runeCtrlD = 0x04; |
| + static const int runeCtrlE = 0x05; |
| + static const int runeCtrlF = 0x06; |
| + static const int runeTAB = 0x09; |
| + static const int runeNewline = 0x0a; |
| + static const int runeCtrlK = 0x0b; |
| + static const int runeCtrlL = 0x0c; |
| + static const int runeCtrlN = 0x0e; |
| + static const int runeCtrlP = 0x10; |
| + static const int runeCtrlU = 0x15; |
| + static const int runeCtrlY = 0x19; |
| + static const int runeESC = 0x1b; |
| + static const int runeSpace = 0x20; |
| + static const int runeDEL = 0x7F; |
| + |
| + Commando(this._stdin, |
| + this._stdout, |
| + this._handleCommand, |
| + {this.prompt:'> ', this.completer:null}) { |
|
hausner
2013/11/21 19:24:56
Should there be spaces around :
turnidge
2013/11/22 19:59:58
Seems reasonable. Done.
|
| + _stdin.echoMode = false; |
| + _stdin.lineMode = false; |
| + _screenWidth = _term.cols - 1; |
| + _writePrompt(); |
| + _stdinSubscription = |
| + _stdin.transform(UTF8.decoder).listen(_handleText, onDone:done); |
| + } |
| + |
| + void _handleText(String text) { |
| + try { |
| + if (!_promptShown) { |
| + _bufferedInput.write(text); |
| + return; |
| + } |
| + |
| + var runes = text.runes.toList(); |
|
hausner
2013/11/21 19:24:56
I'm learning as I read your code and the api docs.
turnidge
2013/11/22 19:59:58
I pass the List into handleRegularSequence/handleC
|
| + var pos = 0; |
| + while (pos < runes.length) { |
| + if (!_promptShown) { |
| + // A command was processed which hid the prompt. Buffer |
| + // the rest of the input. |
| + _bufferedInput.write( |
| + new String.fromCharCodes(runes.skip(pos))); |
| + return; |
| + } |
| + |
| + var char = runes[pos]; |
|
hausner
2013/11/21 19:24:56
This would now be the rune variable from above.
|
| + |
| + // Count consecutive tabs because double-tab is meaningful. |
| + if (char == runeTAB) { |
| + _tabCount++; |
| + } else { |
| + _tabCount = 0; |
| + } |
| + |
| + if (_isControlCharacter(char)) { |
|
hausner
2013/11/21 19:24:56
isControlRune() ? (maybe rather not, unless we ren
turnidge
2013/11/22 19:59:58
Why not? Changed.
|
| + pos += _handleControlSequence(runes, pos); |
| + } else { |
| + pos += _handleRegularSequence(runes, pos); |
| + } |
| + } |
| + } catch(e, trace) { |
| + stderr.writeln('\nUnexpected exception: $e'); |
| + stderr.writeln(trace); |
| + stderr.close().then((_) { |
| + done(); |
| + }); |
| + } |
| + } |
| + |
| + int _handleControlSequence(List<int> runes, int pos) { |
| + var runesConsumed = 1; // Most common result. |
| + var char = runes[pos]; |
| + switch (char) { |
| + case runeCtrlA: |
| + _home(); |
| + break; |
| + |
| + case runeCtrlB: |
| + _leftArrow(); |
| + break; |
| + |
| + case runeCtrlD: |
| + if (_currentLine.length == 0) { |
| + // ^D on an empty line means quit. |
| + _stdout.writeln(); |
| + done(); |
| + } else { |
| + _delete(); |
| + } |
| + break; |
| + |
| + case runeCtrlE: |
| + _end(); |
| + break; |
| + |
| + case runeCtrlF: |
| + _rightArrow(); |
| + break; |
| + |
| + case runeTAB: |
| + if (_complete(_tabCount > 1)) { |
| + _tabCount = 0; |
| + } |
| + break; |
| + |
| + case runeNewline: |
| + _newline(); |
| + break; |
| + |
| + case runeCtrlK: |
| + _kill(); |
| + break; |
| + |
| + case runeCtrlL: |
| + _clearScreen(); |
| + break; |
| + |
| + case runeCtrlN: |
| + _historyNext(); |
| + break; |
| + |
| + case runeCtrlP: |
| + _historyPrevious(); |
| + break; |
| + |
| + case runeCtrlU: |
| + _clearLine(); |
| + break; |
| + |
| + case runeCtrlY: |
| + _yank(); |
| + break; |
| + |
| + case runeESC: |
| + // Check to see if this is an arrow key. |
| + if (pos + 2 < runes.length && // must be a 3 char sequence. |
| + runes[pos + 1] == 0x5b) { // second char must be '['. |
| + switch (runes[pos + 2]) { |
| + case 0x41: // ^[[A = up arrow |
| + _historyPrevious(); |
| + runesConsumed = 3; |
| + break; |
| + |
| + case 0x42: // ^[[B = down arrow |
| + _historyNext(); |
| + runesConsumed = 3; |
| + break; |
| + |
| + case 0x43: // ^[[C = right arrow |
| + _rightArrow(); |
| + runesConsumed = 3; |
| + break; |
| + |
| + case 0x44: // ^[[D = left arrow |
| + _leftArrow(); |
| + runesConsumed = 3; |
| + break; |
| + |
| + default: |
| + // Ignore the escape character. |
| + break; |
| + } |
| + } |
| + break; |
| + |
| + case runeDEL: |
| + _backspace(); |
| + break; |
| + |
| + default: |
| + // Ignore the escape character. |
| + break; |
| + } |
| + return runesConsumed; |
| + } |
| + |
| + int _handleRegularSequence(List<int> runes, int pos) { |
| + var len = pos + 1; |
| + while (len < runes.length && !_isControlCharacter(runes[len])) { |
| + len++; |
| + } |
| + _addChars(runes.getRange(pos, len)); |
| + return len; |
| + } |
| + |
| + bool _isControlCharacter(int char) { |
| + return (char >= 0x00 && char < 0x20) || (char == 0x7f); |
| + } |
| + |
| + void done() { |
| + _stdin.echoMode = true; |
| + _stdin.lineMode = true; |
| + _stdinSubscription.cancel(); |
| + } |
| + |
| + void _writePromptAndLine() { |
| + _writePrompt(); |
| + var pos = _writeRange(_currentLine, 0, _currentLine.length); |
| + _cursorPos = _move(pos, _cursorPos); |
| + } |
| + |
| + void _writePrompt() { |
| + _stdout.write(prompt); |
| + } |
| + |
| + void _addChars(Iterable<int> chars) { |
| + var newLine = []; |
| + newLine..addAll(_currentLine.take(_cursorPos)) |
| + ..addAll(chars) |
| + ..addAll(_currentLine.skip(_cursorPos)); |
| + _update(newLine, (_cursorPos + chars.length)); |
| + } |
| + |
| + void _backspace() { |
| + if (_cursorPos == 0) { |
| + return; |
| + } |
| + |
| + var newLine = []; |
| + newLine..addAll(_currentLine.take(_cursorPos - 1)) |
| + ..addAll(_currentLine.skip(_cursorPos)); |
| + _update(newLine, (_cursorPos - 1)); |
| + } |
| + |
| + void _delete() { |
| + if (_cursorPos == _currentLine.length) { |
| + return; |
| + } |
| + |
| + var newLine = []; |
| + newLine..addAll(_currentLine.take(_cursorPos)) |
| + ..addAll(_currentLine.skip(_cursorPos + 1)); |
| + _update(newLine, _cursorPos); |
| + } |
| + |
| + void _home() { |
| + _updatePos(0); |
| + } |
| + |
| + void _end() { |
| + _updatePos(_currentLine.length); |
| + } |
| + |
| + void _clearScreen() { |
| + _stdout.write(_term.clear); |
| + _writePromptAndLine(); |
| + } |
| + |
| + void _kill() { |
| + var newLine = []; |
| + newLine.addAll(_currentLine.take(_cursorPos)); |
| + _killBuffer = _currentLine.skip(_cursorPos).toList(); |
| + _update(newLine, _cursorPos); |
| + } |
| + |
| + void _clearLine() { |
| + _update([], 0); |
| + } |
| + |
| + void _yank() { |
| + var newLine = []; |
| + newLine..addAll(_currentLine.take(_cursorPos)) |
| + ..addAll(_killBuffer) |
| + ..addAll(_currentLine.skip(_cursorPos)); |
| + _update(newLine, (_cursorPos + _killBuffer.length)); |
| + } |
| + |
| + static String _trimLeadingSpaces(String line) { |
| + bool _isSpace(int rune) { |
| + return rune == runeSpace; |
| + } |
| + return new String.fromCharCodes(line.runes.skipWhile(_isSpace)); |
|
hausner
2013/11/21 19:24:56
Is this really correct? You are feeding runes into
turnidge
2013/11/22 19:59:58
I do not know if this is correct or not. :-/
Adde
|
| + } |
| + |
| + static String _sharedPrefix(String one, String two) { |
| + var len = min(one.length, two.length); |
| + var runesOne = one.runes.toList(); |
| + var runesTwo = two.runes.toList(); |
| + var pos; |
| + for (pos = 0; pos < len; pos++) { |
| + if (runesOne[pos] != runesTwo[pos]) { |
| + break; |
| + } |
| + } |
| + var shared = new String.fromCharCodes(runesOne.take(pos)); |
| + return shared; |
| + } |
| + |
| + bool _complete(bool showCompletions) { |
| + if (completer == null) { |
| + return false; |
| + } |
| + |
| + var linePrefix = _currentLine.take(_cursorPos).toList(); |
| + List<String> commandParts = |
| + _trimLeadingSpaces(new String.fromCharCodes(linePrefix)).split(' '); |
| + List<String> completionList = completer(commandParts); |
| + var completion = ''; |
| + |
| + if (completionList.length == 0) { |
| + // The current line admits no possible completion. |
| + return false; |
| + |
| + } else if (completionList.length == 1) { |
| + // There is a single, non-ambiguous completion for the current line. |
| + completion = completionList[0]; |
| + |
| + // If we are at the end of the line, add a space to signal that |
| + // the completion is unambiguous. |
| + if (_currentLine.length == _cursorPos) { |
| + completion = completion + ' '; |
| + } |
| + } else { |
| + // There are ambiguous completions. Find the longest common |
| + // shared prefix of all of the completions. |
| + completion = completionList.fold(completionList[0], _sharedPrefix); |
| + } |
| + |
| + var lastWord = commandParts.last; |
| + if (completion == lastWord) { |
| + // The completion does not add anything. |
| + if (showCompletions) { |
| + // User hit double-TAB. Show them all possible completions. |
| + _move(_cursorPos, _currentLine.length); |
| + _stdout.writeln(); |
| + _stdout.writeln(completionList); |
| + _writePromptAndLine(); |
| + } |
| + return false; |
| + } else { |
| + // Apply the current completion. |
| + var completionRunes = completion.runes.toList(); |
| + |
| + var newLine = []; |
| + newLine..addAll(linePrefix) |
| + ..addAll(completionRunes.skip(lastWord.length)) |
| + ..addAll(_currentLine.skip(_cursorPos)); |
| + _update(newLine, _cursorPos + completionRunes.length - lastWord.length); |
| + return true; |
| + } |
| + } |
| + |
| + void _newline() { |
| + _addLineToHistory(_currentLine); |
| + _linePos = _lines.length; |
| + |
| + _end(); |
| + _stdout.writeln(); |
| + |
| + // Call the user's command handler. |
| + _handleCommand(new String.fromCharCodes(_currentLine)); |
| + |
| + _currentLine = []; |
| + _cursorPos = 0; |
| + _linePos = _lines.length; |
| + if (_promptShown) { |
| + _writePrompt(); |
| + } |
| + } |
| + |
| + void _leftArrow() { |
| + _updatePos(_cursorPos - 1); |
| + } |
| + |
| + void _rightArrow() { |
| + _updatePos(_cursorPos + 1); |
| + } |
| + |
| + void _addLineToHistory(List<int> line) { |
| + if (_tempLineAdded) { |
| + _lines.removeLast(); |
| + _tempLineAdded = false; |
| + } |
| + if (line.length > 0) { |
| + _lines.add(line); |
| + } |
| + } |
| + |
| + void _addTempLineToHistory(List<int> line) { |
| + _lines.add(line); |
| + _tempLineAdded = true; |
| + } |
| + |
| + void _replaceHistory(List<int> line, int linePos) { |
| + _lines[linePos] = line; |
| + } |
| + |
| + void _historyPrevious() { |
| + if (_linePos == 0) { |
| + return; |
| + } |
| + |
| + if (_linePos == _lines.length) { |
| + _addTempLineToHistory(_currentLine); |
| + } else { |
| + // Any edits get committed to history. |
| + _replaceHistory(_currentLine, _linePos); |
|
hausner
2013/11/21 19:24:56
So does this mean that if at the prompt I hit up a
turnidge
2013/11/22 19:59:58
Isn't that weird?! That's what gdb and bash do :-
|
| + } |
| + |
| + _linePos -= 1; |
| + var line = _lines[_linePos]; |
| + _update(line, line.length); |
| + } |
| + |
| + void _historyNext() { |
| + if (_linePos == (_lines.length - 1)) { |
| + return; |
| + } |
| + |
| + // Any edits get committed to history. |
| + _replaceHistory(_currentLine, _linePos); |
| + |
| + _linePos += 1; |
| + var line = _lines[_linePos]; |
| + _update(line, line.length); |
| + } |
| + |
| + void _updatePos(int newCursorPos) { |
| + if (newCursorPos < 0) { |
| + return; |
| + } |
| + if (newCursorPos > _currentLine.length) { |
| + return; |
| + } |
| + |
| + _cursorPos = _move(_cursorPos, newCursorPos); |
| + } |
| + |
| + void _update(List<int> newLine, int newCursorPos) { |
| + var pos = _cursorPos; |
| + var diffPos; |
| + var sharedLen = min(_currentLine.length, newLine.length); |
| + |
| + // Find first difference. |
| + for (diffPos = 0; diffPos < sharedLen; diffPos++) { |
| + if (_currentLine[diffPos] != newLine[diffPos]) { |
| + break; |
| + } |
| + } |
| + |
| + // Move the cursor to where the difference begins. |
| + pos = _move(pos, diffPos); |
| + |
| + // Write the new text. |
| + pos = _writeRange(newLine, pos, newLine.length); |
| + |
| + // Clear any extra characters at the end. |
| + pos = _clearRange(pos, _currentLine.length); |
| + |
| + // Move the cursor back to the input point. |
| + _cursorPos = _move(pos, newCursorPos); |
| + _currentLine = newLine; |
| + } |
| + |
| + void hide() { |
| + if (!_promptShown) { |
| + return; |
| + } |
| + _promptShown = false; |
| + // We need to erase everything, including the prompt. |
| + var curLine = _getLine(_cursorPos); |
| + var lastLine = _getLine(_currentLine.length); |
| + |
| + // Go to last line. |
| + if (curLine < lastLine) { |
| + for (var i = 0; i < (lastLine - curLine); i++) { |
| + // This moves us to column 0. |
| + _stdout.write(_term.cursorDown); |
| + } |
| + curLine = lastLine; |
| + } else { |
| + // Move to column 0. |
| + _stdout.write('\r'); |
| + } |
| + |
| + // Work our way up, clearing lines. |
| + while (true) { |
| + _stdout.write(_term.clrEOL); |
| + if (curLine > 0) { |
| + _stdout.write(_term.cursorUp); |
| + } else { |
| + break; |
| + } |
| + } |
| + } |
| + |
| + void show() { |
| + if (_promptShown) { |
| + return; |
| + } |
| + _promptShown = true; |
| + _writePromptAndLine(); |
| + |
| + // If input was buffered while the prompt was hidden, process it |
| + // now. |
| + if (!_bufferedInput.isEmpty) { |
| + var input = _bufferedInput.toString(); |
| + _bufferedInput.clear(); |
| + _handleText(input); |
| + } |
| + } |
| + |
| + int _writeRange(List<int> text, int pos, int writeToPos) { |
| + if (pos >= writeToPos) { |
| + return pos; |
| + } |
| + while (pos < writeToPos) { |
| + var margin = _nextMargin(pos); |
| + var limit = min(writeToPos, margin); |
| + _stdout.write(new String.fromCharCodes(text.getRange(pos, limit))); |
| + pos = limit; |
| + if (pos == margin) { |
| + _stdout.write('\n'); |
| + } |
| + } |
| + return pos; |
| + } |
| + |
| + int _clearRange(int pos, int clearToPos) { |
| + if (pos >= clearToPos) { |
| + return pos; |
| + } |
| + while (true) { |
| + var limit = _nextMargin(pos); |
| + _stdout.write(_term.clrEOL); |
| + if (limit >= clearToPos) { |
| + return pos; |
| + } |
| + _stdout.write('\n'); |
| + pos = limit; |
| + } |
| + } |
| + |
| + int _move(int pos, int newPos) { |
| + if (pos == newPos) { |
| + return pos; |
| + } |
| + |
| + var curCol = _getCol(pos); |
| + var curLine = _getLine(pos); |
| + var newCol = _getCol(newPos); |
| + var newLine = _getLine(newPos); |
| + |
| + if (curLine > newLine) { |
| + for (var i = 0; i < (curLine - newLine); i++) { |
| + _stdout.write(_term.cursorUp); |
| + } |
| + } |
| + if (curLine < newLine) { |
| + for (var i = 0; i < (newLine - curLine); i++) { |
| + _stdout.write(_term.cursorDown); |
| + } |
| + |
| + // Moving down resets column to zero, oddly. |
| + curCol = 0; |
| + } |
| + if (curCol > newCol) { |
| + for (var i = 0; i < (curCol - newCol); i++) { |
| + _stdout.write(_term.cursorBack); |
| + } |
| + } |
| + if (curCol < newCol) { |
| + for (var i = 0; i < (newCol - curCol); i++) { |
| + _stdout.write(_term.cursorForward); |
| + } |
| + } |
| + |
| + return newPos; |
| + } |
| + |
| + int _nextMargin(int pos) { |
| + var truePos = pos + prompt.length; |
| + var curLine = _getLine(pos); |
| + return ((truePos ~/ _screenWidth) + 1) * _screenWidth - prompt.length; |
| + } |
| + |
| + int _getLine(int pos) { |
| + var truePos = pos + prompt.length; |
| + return truePos ~/ _screenWidth; |
| + } |
| + |
| + int _getCol(int pos) { |
| + var truePos = pos + prompt.length; |
| + return truePos % _screenWidth; |
| + } |
| + |
| + Stdin _stdin; |
| + StreamSubscription _stdinSubscription; |
| + IOSink _stdout; |
| + final _handleCommand; |
| + final String prompt; |
| + bool _promptShown = true; |
| + final CommandCompleter completer; |
| + TermInfo _term = new TermInfo(); |
| + |
| + int _screenWidth; |
|
hausner
2013/11/21 19:24:56
This gets initialized once only in the constructor
turnidge
2013/11/22 19:59:58
Added a TODO.
My intention was to update this whe
|
| + List<int> _currentLine = []; // A list of runes. |
| + StringBuffer _bufferedInput = new StringBuffer(); |
| + List<List<int>> _lines = []; |
|
hausner
2013/11/21 19:24:56
I would find a short description helpful that expl
turnidge
2013/11/22 19:59:58
Done.
|
| + bool _tempLineAdded = false; |
| + int _linePos = 0; |
| + int _cursorPos = 0; |
| + int _tabCount = 0; |
| + List<int> _killBuffer = []; |
| +} |
| + |
| + |
| +// Demo code. |
| + |
| + |
| +List<String> _myCompleter(List<String> commandTokens) { |
| + List<String> completions = new List<String>(); |
| + |
| + // First word completions. |
| + if (commandTokens.length <= 1) { |
| + String prefix = ''; |
| + if (commandTokens.length == 1) { |
| + prefix = commandTokens.first; |
| + } |
| + if ('quit'.startsWith(prefix)) { |
| + completions.add('quit'); |
| + } |
| + if ('help'.startsWith(prefix)) { |
| + completions.add('help'); |
| + } |
| + if ('happyface'.startsWith(prefix)) { |
| + completions.add('happyface'); |
| + } |
| + } |
| + |
| + // Complete 'foobar' or 'gondola' anywhere in string. |
| + String lastWord = commandTokens.last; |
| + if ('foobar'.startsWith(lastWord)) { |
| + completions.add('foobar'); |
| + } |
| + if ('gondola'.startsWith(lastWord)) { |
| + completions.add('gondola'); |
| + } |
| + |
| + return completions; |
| +} |
| + |
| + |
| +int _helpCount = 0; |
| +Commando cmdo; |
| + |
| + |
| +void _handleCommand(String rawCommand) { |
| + String command = rawCommand.trim(); |
| + if (command == 'quit') { |
| + cmdo.done(); |
| + } else if (command == 'help') { |
| + switch (_helpCount) { |
| + case 0: |
| + print('I will not help you.'); |
| + break; |
| + case 1: |
| + print('I mean it.'); |
| + break; |
| + case 2: |
| + print('Seriously.'); |
| + break; |
| + case 100: |
| + print('Well now.'); |
| + break; |
| + default: |
| + print("Okay. Type 'quit' to quit"); |
| + break; |
| + } |
| + _helpCount++; |
| + } else if (command == 'happyface') { |
| + print(':-)'); |
| + } else { |
| + print('Received command($command)'); |
| + } |
| +} |
| + |
| + |
| +void main() { |
| + stdout.writeln('[Commando demo]'); |
| + cmd = new Commando(stdin, stdout, _handleCommand, |
| + completer:_myCompleter); |
| +} |