| // 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); |
| } |
| } |