blob: bece14bd6ca13c25d30f83631fbe699aa70d3970 [file] [log] [blame]
// 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;
}
}