| // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file | |
| // 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:math' show min; | |
| export 'package:dart_console/dart_console.dart'; | |
| import 'package:dart_console/dart_console.dart'; | |
| class SmartConsole extends Console { | |
| final ScrollbackBuffer history; | |
| late final List<KeyHandler> handlers; | |
| KeyHandler? completionHandler; | |
| SmartConsole({this.completionHandler}) | |
| : history = ScrollbackBuffer(recordBlanks: false) { | |
| handlers = [ | |
| BashNavigationKeyHandler(), | |
| BashHistoryKeyHandler(history), | |
| BashEditKeyHandler(), | |
| ]; | |
| } | |
| void moveCursorToColumn(int column) { | |
| write('\x1b[${column + 1}`'); | |
| } | |
| void hideCursor() { | |
| write('\x1b[? 5l'); | |
| } | |
| void showCursor() { | |
| write('\x1b[? 5h'); | |
| } | |
| ReadLineResult smartReadLine() { | |
| final buffer = LineEditBuffer(); | |
| drawPrompt(buffer); | |
| while (true) { | |
| final key = readKey(); | |
| bool wasHandled = false; | |
| for (final handler in handlers) { | |
| if (handler.handleKey(buffer, key)) { | |
| wasHandled = true; | |
| break; | |
| } | |
| } | |
| if (completionHandler != null && | |
| completionHandler!.handleKey(buffer, key)) { | |
| wasHandled = true; | |
| } | |
| if (!wasHandled && key.isControl) { | |
| switch (key.controlChar) { | |
| // Accept | |
| case ControlCharacter.enter: | |
| history.add(buffer.text); | |
| writeLine(); | |
| return ReadLineResult(buffer.text, false, false); | |
| // Cancel this line. | |
| case ControlCharacter.ctrlC: | |
| return ReadLineResult('', true, false); | |
| // EOF | |
| case ControlCharacter.ctrlD: | |
| if (!buffer.text.isEmpty) break; | |
| return ReadLineResult('', true, true); | |
| // Ignore. | |
| default: | |
| break; | |
| } | |
| } | |
| drawPrompt(buffer); | |
| } | |
| } | |
| void drawPrompt(LineEditBuffer buffer) { | |
| hideCursor(); | |
| const prefix = '(hsa) '; | |
| moveCursorToColumn(0); | |
| setForegroundColor(ConsoleColor.brightBlue); | |
| write(prefix); | |
| resetColorAttributes(); | |
| setForegroundColor(ConsoleColor.brightGreen); | |
| eraseCursorToEnd(); | |
| if (buffer.completionText.isNotEmpty) { | |
| write(buffer.text.substring(0, buffer.index)); | |
| setForegroundColor(ConsoleColor.brightWhite); | |
| write(buffer.completionText); | |
| setForegroundColor(ConsoleColor.brightGreen); | |
| write(buffer.text.substring(buffer.index)); | |
| } else { | |
| write(buffer.text); | |
| } | |
| moveCursorToColumn(prefix.length + buffer.index); | |
| showCursor(); | |
| } | |
| } | |
| class ReadLineResult { | |
| final String text; | |
| final bool wasCancelled; | |
| final bool shouldExit; | |
| ReadLineResult(this.text, this.wasCancelled, this.shouldExit); | |
| } | |
| /// Handler of a new key stroke when editing a line. | |
| abstract class KeyHandler { | |
| bool handleKey(LineEditBuffer buffer, Key key); | |
| } | |
| /// Handles cursor navigation. | |
| class BashNavigationKeyHandler extends KeyHandler { | |
| bool handleKey(LineEditBuffer buffer, Key key) { | |
| if (!key.isControl) return false; | |
| switch (key.controlChar) { | |
| case ControlCharacter.arrowLeft: | |
| case ControlCharacter.ctrlB: | |
| buffer.moveLeft(); | |
| return true; | |
| case ControlCharacter.arrowRight: | |
| case ControlCharacter.ctrlF: | |
| buffer.moveRight(); | |
| return true; | |
| case ControlCharacter.wordLeft: | |
| buffer.moveWordLeft(); | |
| return true; | |
| case ControlCharacter.wordRight: | |
| buffer.moveWordRight(); | |
| return true; | |
| case ControlCharacter.home: | |
| case ControlCharacter.ctrlA: | |
| buffer.moveStart(); | |
| return true; | |
| case ControlCharacter.end: | |
| case ControlCharacter.ctrlE: | |
| buffer.moveEnd(); | |
| return true; | |
| default: | |
| return false; | |
| } | |
| } | |
| } | |
| /// Handles history navigation. | |
| class BashHistoryKeyHandler extends KeyHandler { | |
| ScrollbackBuffer history; | |
| BashHistoryKeyHandler(this.history); | |
| bool handleKey(LineEditBuffer buffer, Key key) { | |
| if (!key.isControl) return false; | |
| switch (key.controlChar) { | |
| case ControlCharacter.ctrlP: | |
| case ControlCharacter.arrowUp: | |
| buffer.replaceWith(history.up(buffer.text)); | |
| return true; | |
| case ControlCharacter.ctrlN: | |
| case ControlCharacter.arrowDown: | |
| final temp = history.down(); | |
| if (temp != null) { | |
| buffer.replaceWith(temp); | |
| } | |
| return true; | |
| default: | |
| return false; | |
| } | |
| } | |
| } | |
| /// Handles text edits. | |
| class BashEditKeyHandler extends KeyHandler { | |
| bool handleKey(LineEditBuffer buffer, Key key) { | |
| if (!key.isControl) { | |
| buffer.insert(key.char); | |
| return true; | |
| } | |
| // TODO: Add support for <alt-d> (delete word). | |
| switch (key.controlChar) { | |
| case ControlCharacter.backspace: | |
| case ControlCharacter.ctrlH: | |
| buffer.backspace(); | |
| return true; | |
| case ControlCharacter.ctrlU: | |
| buffer.truncateLeft(); | |
| return true; | |
| case ControlCharacter.ctrlK: | |
| buffer.truncateRight(); | |
| return true; | |
| case ControlCharacter.delete: | |
| case ControlCharacter.ctrlD: | |
| final wasDeleted = buffer.delete(); | |
| return wasDeleted; | |
| default: | |
| return false; | |
| } | |
| } | |
| } | |
| /// Represents the state of [Console.readLine] while editing the line. | |
| class LineEditBuffer { | |
| /// The _text that was so far entered. | |
| String _text = ''; | |
| /// The _index into [_text] where the editing cursor is currently being drawn. | |
| int _index = 0; | |
| /// The _text to display as inline completion. | |
| String completionText = ''; | |
| LineEditBuffer(); | |
| String get text => _text; | |
| int get index => _index; | |
| void moveWordLeft() { | |
| if (_index > 0) { | |
| final textLeftOfCursor = _text.substring(0, _index - 1); | |
| final lastSpace = textLeftOfCursor.lastIndexOf(' '); | |
| _index = lastSpace != -1 ? lastSpace + 1 : 0; | |
| } | |
| } | |
| void moveWordRight() { | |
| if (_index < _text.length) { | |
| final textRightOfCursor = _text.substring(_index + 1); | |
| final nextSpace = textRightOfCursor.indexOf(' '); | |
| _index = nextSpace != -1 | |
| ? min(_index + nextSpace + 2, _text.length) | |
| : _text.length; | |
| } | |
| } | |
| void moveLeft() { | |
| if (_index > 0) _index--; | |
| } | |
| void moveRight() { | |
| if (_index < _text.length) _index++; | |
| } | |
| void moveStart() { | |
| _index = 0; | |
| } | |
| void moveEnd() { | |
| _index = _text.length; | |
| } | |
| void replaceWith(String newtext) { | |
| _text = newtext; | |
| _index = _text.length; | |
| } | |
| void truncateRight() { | |
| _text = _text.substring(0, _index); | |
| } | |
| void truncateLeft() { | |
| _text = _text.substring(_index, _text.length); | |
| _index = 0; | |
| } | |
| void backspace() { | |
| if (_index > 0) { | |
| _text = _text.substring(0, _index - 1) + _text.substring(_index); | |
| _index--; | |
| } | |
| } | |
| bool delete() { | |
| if (_index < _text.length) { | |
| _text = _text.substring(0, _index) + _text.substring(_index + 1); | |
| return true; | |
| } | |
| return false; | |
| } | |
| void insert(String chars) { | |
| if (_index == _text.length) { | |
| _text += chars; | |
| } else { | |
| _text = _text.substring(0, _index) + chars + _text.substring(_index); | |
| } | |
| _index += chars.length; | |
| } | |
| } |