blob: d758c929ee6b1747116019b76b8b38b7b6f7c2d1 [file] [log] [blame]
// Copyright (c) 2020, 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.
// @dart=2.9
import "dart:async" show StreamSubscription, Timer;
import "dart:io" show Platform, ProcessSignal, exit, stdin, stdout;
import "dart:isolate" show RawReceivePort;
import "dart:typed_data" show Uint16List;
class Application {
int _latestKnownTerminalColumns;
int _latestKnownTerminalRows;
RawReceivePort _preventClose;
StreamSubscription<List<int>> _stdinListen;
StreamSubscription<ProcessSignal> _sigintListen;
StreamSubscription<ProcessSignal> _sigwinchListen;
Timer _timer;
final Widget _widget;
_Output _output;
bool _started = false;
Application(this._widget) {
_latestKnownTerminalColumns = stdout.terminalColumns;
_latestKnownTerminalRows = stdout.terminalLines;
_preventClose = new RawReceivePort();
stdin.echoMode = false;
stdin.lineMode = false;
_stdinListen = stdin.listen(_stdinListener);
_sigintListen = ProcessSignal.sigint.watch().listen((ProcessSignal signal) {
quit();
exit(0);
});
if (!Platform.isWindows) {
_sigwinchListen =
ProcessSignal.sigwinch.watch().listen((ProcessSignal signal) {
_latestKnownTerminalColumns = stdout.terminalColumns;
_latestKnownTerminalRows = stdout.terminalLines;
_repaint();
});
}
}
void _repaint() {
_output =
new _Output(_latestKnownTerminalRows, _latestKnownTerminalColumns);
_widget.print(new WriteOnlyPartialOutput(
_output, 0, 0, _output.rows, _output.columns));
_printOutput();
}
void _stdinListener(List<int> data) {
try {
if (_widget.input(this, data)) {
_repaint();
}
} catch (e) {
quit();
rethrow;
}
}
void quit() {
_gotoMainScreenBuffer();
_showCursor();
_timer.cancel();
_preventClose.close();
_stdinListen.cancel();
_sigintListen.cancel();
_sigwinchListen?.cancel();
}
void start() {
if (_started) throw "Already started!";
_started = true;
_gotoAlternativeScreenBuffer();
_hideCursor();
_repaint();
_timer = new Timer.periodic(new Duration(milliseconds: 100), (t) {
int value = stdout.terminalColumns;
bool changed = false;
if (value != _latestKnownTerminalColumns) {
_latestKnownTerminalColumns = value;
changed = true;
}
value = stdout.terminalLines;
if (value != _latestKnownTerminalRows) {
_latestKnownTerminalRows = value;
changed = true;
}
if (changed) {
_repaint();
}
});
}
_Output _prevOutput;
_printOutput() {
int currentPosRow = -1;
int currentPosColumn = -1;
StringBuffer buffer = new StringBuffer();
if (_prevOutput == null ||
_prevOutput.columns != _output.columns ||
_prevOutput.rows != _output.rows) {
_clearScreenAlt();
_prevOutput = null;
}
for (int row = 0; row < _output.rows; row++) {
for (int column = 0; column < _output.columns; column++) {
String char = _output.getChar(row, column);
int rawModifier = _output.getRawModifiers(row, column);
if (_prevOutput != null) {
String prevChar = _prevOutput.getChar(row, column);
int prevRawModifier = _prevOutput.getRawModifiers(row, column);
if (prevChar == char && prevRawModifier == rawModifier) continue;
}
Modifier modifier = _output.getModifier(row, column);
switch (modifier) {
case Modifier.Undefined:
// Do nothing.
break;
case Modifier.Bold:
buffer.write("${CSI}1m");
break;
case Modifier.Italic:
buffer.write("${CSI}3m");
break;
case Modifier.Underline:
buffer.write("${CSI}4m");
break;
}
ForegroundColor foregroundColor =
_output.getForegroundColor(row, column);
switch (foregroundColor) {
case ForegroundColor.Undefined:
// Do nothing.
break;
case ForegroundColor.Black:
buffer.write("${CSI}30m");
break;
case ForegroundColor.Red:
buffer.write("${CSI}31m");
break;
case ForegroundColor.Green:
buffer.write("${CSI}32m");
break;
case ForegroundColor.Yellow:
buffer.write("${CSI}33m");
break;
case ForegroundColor.Blue:
buffer.write("${CSI}34m");
break;
case ForegroundColor.Magenta:
buffer.write("${CSI}35m");
break;
case ForegroundColor.Cyan:
buffer.write("${CSI}36m");
break;
case ForegroundColor.White:
buffer.write("${CSI}37m");
break;
}
BackgroundColor backgroundColor =
_output.getBackgroundColor(row, column);
switch (backgroundColor) {
case BackgroundColor.Undefined:
// Do nothing.
break;
case BackgroundColor.Black:
buffer.write("${CSI}40m");
break;
case BackgroundColor.Red:
buffer.write("${CSI}41m");
break;
case BackgroundColor.Green:
buffer.write("${CSI}42m");
break;
case BackgroundColor.Yellow:
buffer.write("${CSI}43m");
break;
case BackgroundColor.Blue:
buffer.write("${CSI}44m");
break;
case BackgroundColor.Magenta:
buffer.write("${CSI}45m");
break;
case BackgroundColor.Cyan:
buffer.write("${CSI}46m");
break;
case BackgroundColor.White:
buffer.write("${CSI}47m");
break;
}
if (char != null) {
buffer.write(char);
} else {
buffer.write(" ");
}
// End old modifier.
buffer.write("${CSI}0m");
if (currentPosRow != row || currentPosColumn != column) {
// 1-indexed.
_setCursorPosition(row + 1, column + 1);
}
stdout.write(buffer.toString());
buffer.clear();
currentPosRow = row;
currentPosColumn = column + 1;
}
}
_prevOutput = _output;
}
// "ESC [" is "Control Sequence Introducer" (CSI) according to Wikipedia
// (https://en.wikipedia.org/wiki/ANSI_escape_code#Escape_sequences).
static const String CSI = "\x1b[";
void _setCursorPosition(int row, int column) {
// "CSI n ; m H": Cursor Position.
stdout.write("${CSI}${row};${column}H");
}
void _gotoAlternativeScreenBuffer() {
// "CSI ? 1049 h": Enable alternative screen buffer.
stdout.write("${CSI}?1049h");
}
void _gotoMainScreenBuffer() {
// "CSI ? 1049 l": Disable alternative screen buffer.
stdout.write("${CSI}?1049l");
}
void _clearScreenAlt() {
_setCursorPosition(0, 0);
// "CSI n J": Erase in Display. Clears part of the screen.
// If n is 0 (or missing), clear from cursor to end of screen.
stdout.write("${CSI}0J");
_setCursorPosition(0, 0);
}
void _hideCursor() {
// "CSI ? 25 l": DECTCEM Hides the cursor.
stdout.write("${CSI}?25l");
}
void _showCursor() {
// "CSI ? 25 h": DECTCEM Shows the cursor, from the VT320.
stdout.write("${CSI}?25h");
}
}
abstract class Widget {
void print(WriteOnlyOutput output);
bool input(Application app, List<int> data);
}
class BoxedWidget extends Widget {
final Widget _content;
BoxedWidget(this._content);
@override
bool input(Application app, List<int> data) {
return _content?.input(app, data) ?? false;
}
@override
void print(WriteOnlyOutput output) {
// Corners.
output.setCell(0, 0, char: /*"\u250c"*/ "\u250c");
output.setCell(0, output.columns - 1, char: "\u2510");
output.setCell(output.rows - 1, 0, char: "\u2514");
output.setCell(output.rows - 1, output.columns - 1, char: "\u2518");
// Top and bottom line.
for (int i = 1; i < output.columns - 1; i++) {
output.setCell(0, i, char: "\u2500");
output.setCell(output.rows - 1, i, char: "\u2500");
}
// Left and right line
for (int i = 1; i < output.rows - 1; i++) {
output.setCell(i, 0, char: "\u2502");
output.setCell(i, output.columns - 1, char: "\u2502");
}
// Reduce all sides by one.
_content?.print(new WriteOnlyPartialOutput(
output, 1, 1, output.rows - 2, output.columns - 2));
}
}
class QuitOnQWidget extends Widget {
Widget _contentWidget;
QuitOnQWidget(this._contentWidget);
@override
void print(WriteOnlyOutput output) {
_contentWidget?.print(output);
}
@override
bool input(Application app, List<int> data) {
if (data.length == 1 && String.fromCharCode(data[0]) == 'q') {
app.quit();
return false;
}
return _contentWidget?.input(app, data) ?? false;
}
}
class WithSingleLineBottomWidget extends Widget {
Widget _contentWidget;
Widget _bottomWidget;
WithSingleLineBottomWidget(this._contentWidget, this._bottomWidget);
@override
void print(WriteOnlyOutput output) {
// All but the last row.
_contentWidget?.print(new WriteOnlyPartialOutput(
output, 0, 0, output.rows - 1, output.columns));
// Only that last row.
_bottomWidget?.print(new WriteOnlyPartialOutput(
output, output.rows - 1, 0, 1, output.columns));
}
@override
bool input(Application app, List<int> data) {
bool result = _contentWidget?.input(app, data) ?? false;
result |= _bottomWidget?.input(app, data) ?? false;
return result;
}
}
enum Modifier {
Undefined,
Bold,
Italic,
Underline,
}
enum ForegroundColor {
Undefined,
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
White,
}
enum BackgroundColor {
Undefined,
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
White,
}
class _Output implements WriteOnlyOutput {
final int rows;
final int columns;
final Uint16List _text;
final Uint16List _modifiers;
_Output(this.rows, this.columns)
: _text = new Uint16List(rows * columns),
_modifiers = new Uint16List(rows * columns);
int getPosition(int row, int column) {
return row * columns + column;
}
void setCell(int row, int column,
{String char,
Modifier modifier,
ForegroundColor foregroundColor,
BackgroundColor backgroundColor}) {
int position = getPosition(row, column);
if (char != null) {
List<int> codeUnits = char.codeUnits;
assert(codeUnits.length == 1);
_text[position] = codeUnits.single;
}
int outModifier = _modifiers[position];
if (modifier != null) {
int mask = 0x03 << 8;
int value = (modifier.index & 0x03) << 8;
outModifier &= ~mask;
outModifier |= value;
}
if (foregroundColor != null) {
int mask = 0xF << 4;
int value = (foregroundColor.index & 0xF) << 4;
outModifier &= ~mask;
outModifier |= value;
}
if (backgroundColor != null) {
int mask = 0xF;
int value = (backgroundColor.index & 0xF);
outModifier &= ~mask;
outModifier |= value;
}
_modifiers[position] = outModifier;
}
String getChar(int row, int column) {
int position = getPosition(row, column);
int char = _text[position];
if (char > 0) return new String.fromCharCode(char);
return null;
}
int getRawModifiers(int row, int column) {
int position = getPosition(row, column);
return _modifiers[position];
}
Modifier getModifier(int row, int column) {
int position = getPosition(row, column);
int modifier = _modifiers[position];
int value = (modifier >> 8) & 0x3;
return Modifier.values[value];
}
ForegroundColor getForegroundColor(int row, int column) {
int position = getPosition(row, column);
int modifier = _modifiers[position];
int value = (modifier >> 4) & 0xF;
return ForegroundColor.values[value];
}
BackgroundColor getBackgroundColor(int row, int column) {
int position = getPosition(row, column);
int modifier = _modifiers[position];
int value = modifier & 0xF;
return BackgroundColor.values[value];
}
}
abstract class WriteOnlyOutput {
int get rows;
int get columns;
void setCell(int row, int column,
{String char,
Modifier modifier,
ForegroundColor foregroundColor,
BackgroundColor backgroundColor});
}
class WriteOnlyPartialOutput implements WriteOnlyOutput {
final WriteOnlyOutput _output;
final int offsetRow;
final int offsetColumn;
final int rows;
final int columns;
WriteOnlyPartialOutput(this._output, this.offsetRow, this.offsetColumn,
this.rows, this.columns) {
if (offsetRow + rows > _output.rows ||
offsetColumn + columns > _output.columns) {
throw "Out of bounds";
}
}
@override
void setCell(int row, int column,
{String char,
Modifier modifier,
ForegroundColor foregroundColor,
BackgroundColor backgroundColor}) {
if (row >= rows || column >= columns) return;
_output.setCell(row + offsetRow, column + offsetColumn,
char: char,
foregroundColor: foregroundColor,
backgroundColor: backgroundColor);
}
}