blob: 9906aa5718b879d60e0b92747a8f1b3e8173bd94 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. 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:io' as io;
/// Wrapper around iteratively build command line output.
///
/// Uses control/escape sequences to reset output when necessary, but otherwise
/// simply appends new output to the end of previously written messages.
///
/// Usage:
///
/// ```dart
/// OutputBuffer outputBuffer = OutputBuffer(standardOut: stdout);
///
/// // Prints "this will appear on its own line\n"
/// outputBuffer.addLine('this will appear on its own line');
///
/// // Prints "this will be added to an in-progress line" (no newline!)
/// outputBuffer.addSubstring('this will be added to');
/// outputBuffer.addSubstring(' an in-progress line');
///
/// // Prints "\nbut now back to whole lines\n" (leading newline bc it followed
/// // a partial line, plus trailing)
/// outputBuffer.addLine('but now back to whole lines')
/// ```
class OutputBuffer {
/// The actual content of our output. Can be built and flushed iteratively.
final List<String> _buffer;
/// The ultimate destination for our output. If not provided, defaults to
/// the global [Stdout]
final io.Stdout stdout;
/// Set to `true` if we previously entered a trailing newline for cosmetic
/// purposes. Useful when we hope to emit whole lines after emitting
/// substrings.
bool _isCursorOnNewline;
final _ansiEscape = String.fromCharCode(27);
OutputBuffer([stdout])
: _buffer = [],
_isCursorOnNewline = false,
stdout = stdout ?? io.stdout;
void _clearLines([int lines]) {
lines ??= 1;
if (_isCursorOnNewline) {
_cursorUp();
_isCursorOnNewline = false;
}
int counter = 0;
while (counter < lines) {
_clearLine();
if (counter < lines - 1) {
_cursorUp();
}
counter++;
}
}
void _clearLine() {
stdout
..write('$_ansiEscape[2K') // clear line
..write('$_ansiEscape[0G'); // cursor to 0-index on current line
}
void _cursorUp() {
stdout.write('$_ansiEscape[1A'); // cursor up
}
/// Appends additional characters to the last line of the buffer
void addSubstring(String msg, {bool shouldFlush = true}) {
if (_buffer.isEmpty) {
_buffer.add('');
}
_buffer.last += msg;
if (shouldFlush) {
stdout.write(msg);
_isCursorOnNewline = false;
}
}
/// Adds an additional line to the end of the buffer
void addLine(String msg, {bool shouldFlush = true}) {
addLines([msg], shouldFlush: shouldFlush);
}
/// Adds N additional lines to the end of the buffer
void addLines(List<String> msgs, {bool shouldFlush = true}) {
if (!_isCursorOnNewline) {
stdout.writeln('');
_isCursorOnNewline = true;
}
msgs.forEach(_registerLine);
if (shouldFlush) {
_flushLines(msgs);
}
}
/// Gracefully handles adding lines that themselves contain newlines, since
/// otherwise that breaks some assumptions.
void _registerLine(String line) {
_buffer.addAll(line.split('\n'));
}
/// Replaces the content of individual lines in the buffer and, optionally,
/// reflushes them to the [stdout].
///
/// Assumes the passed list of strings should overlay the end of [buffer].
/// Thus, if [buffer] is a list of 10 strings, and this function is invoked
/// with a list of 3 strings, indices 7, 8, and 9 (the last three elements)
/// will be replaced.
void updateLines(List<String> msgs, {bool shouldFlush = true}) {
while (msgs.length > _buffer.length) {
_buffer.add('');
}
for (int count in Iterable<int>.generate(msgs.length)) {
int indexToReplace = _buffer.length - msgs.length + count;
_buffer[indexToReplace] = msgs[count];
}
if (shouldFlush) {
_clearLines(msgs.length);
_flushLines(msgs);
}
}
/// Eliminates lines off the end of the buffer and moves the cursor up the
/// same number of lines.
void stripLines(int numLines) {
_buffer.removeRange(_buffer.length - numLines, _buffer.length);
_clearLines(numLines);
}
/// Sends a list of strings to the [stdout]. Is completely unaware of the
/// [buffer], so calling this directly can desync the internal state from what
/// is rendered in the terminal.
void _flushLines(List<String> lines) {
stdout.writeln(lines.join('\n'));
_isCursorOnNewline = true;
}
/// Writes the entire [buffer] to the [stdout].
void flush({int start, int end}) {
_flushLines(_buffer.sublist(start ?? 0, end ?? _buffer.length));
}
/// Clears the [stdout], without touching the [buffer]. If you are not
/// planning to re-flush, you should consider also resetting [buffer].
void clear() {
_clearLines(_isCursorOnNewline ? _buffer.length : _buffer.length - 1);
}
}