dart_console icon indicating copy to clipboard operation
dart_console copied to clipboard

If stdin is closed cursorPosition throws a RangeError

Open bsutton opened this issue 3 years ago • 4 comments

if stdin is closed (seems to be within docker) hen the call to Console.cursorPosition will fail. RangeError: Invalid value: Not in inclusive range 0..1114111: -1 new String.fromCharCode (dart:core-patch/string_patch.dart:45) Console.cursorPosition (package:dart_console/src/console.dart:304)

looks like the results from the read need to be checked for -1 (end of file) before trying to decode as a char code.

bsutton avatar Jun 17 '21 01:06 bsutton

Thanks! Fixed by exclusion. Finding cursor position requires writing an ANSI character code to stdout and then reading the response to stdin, so if the latter isn't available, the call will fail. That seems to be a limitation of GitHub Actions, so I've edited the tests to require a working stdin/stdout. Was there something else you were looking at other than the tests?

timsneath avatar Aug 10 '21 02:08 timsneath

This is a broader problem than just the unit tests. I'm using the cursor position in production code. In some instances the production code runs without a head (e.g. docker). I would be nice if the code behaved in a more consistent manner when running headless.

I'm a little unsure just what the correct action is. I think there are two components.

  1. discoverability How can a user check/know that the function will work.
  2. what should the function do if called without the pre-conditions being met.

There is probably a reasonable argument that this can be done by documenting the limitations on the method call and then throwing an appropriate exception such as:

throw IllegalState('cursor position not supported when not attached to a console');

Perhaps also a method such as 'isAttached' so that the user can easily discover the state without having to work out stdin and stdout.

bsutton avatar Aug 10 '21 03:08 bsutton

Yeah, there's certainly the opportunity to detect whether the console is running headless or not. I'm wondering how other tools do it.

Help me understand the scenario: is it something like a progress bar, where you want to keep rewriting the same line? Looking at our own tools (e.g. dart test), I see we just spew out multiple lines in that scenario. There may be a variant of the method that resets to the home column even if it's not a terminal that supports ANSI escape codes.

timsneath avatar Aug 10 '21 14:08 timsneath

So a use case on this one is difficult.

I've exposed components of dart_console in my DCli library. DCli is a library which allows users to build cli applications.

As such there is no single use case. My objective is to design a library that is easy to use and intuitive.

DCli does implement a progress bar.

final term = Terminal();
final percentage = Format.percentage(progress.progress, 1);
if (term.isAnsi) {
term
..clearLine()
..startOfLine();
echo(
'${EnumHelper.getName(progress.status).padRight(15)}${Format.bytesAsReadable
(progress.downloaded)}/${Format.bytesAsReadable(progress.length)} $
percentage');
} else {
if (_progressSuppressor % 1000 == 0 ||
progress.status == FetchStatus.complete) {
print(
'${EnumHelper.getName(progress.status).padRight(15)}${Format.bytesAsReadable
(progress.downloaded)}/${Format.bytesAsReadable(progress.length)} $
percentage');
}
_progressSuppressor++;
if (_progressSuppressor > 1000) {
_progressSuppressor = 0;
}
}

I also return default values for column and row: int get columns { if (hasTerminal) { return _console.windowWidth; } else { return 80; } }

/// Returns the row location of the cursor. /// The first row is row 0. int get row => _cursor?.row ?? 24;

If someone attempts to retrieve the cursor position I return null:

Coordinate? get _cursor { /// attempting to get the cursor position if /// we don't have a terminal will hang the app /// as to obtain the cursor we must read from the /// terminal. if (hasTerminal && Ansi.isSupported) { try { return console.cursorPosition; // ignore: avoid_catching_errors } on RangeError catch () { // if stdin is closed (seems to be within docker) // then the call to cursorPosition will fail. // RangeError: Invalid value: Not in inclusive range 0..1114111: -1 // new String.fromCharCode (dart:core-patch/string_patch.dart:45) // Console.cursorPosition (package:dart_console/src/console.dart:304) return null; } } else { return null; } }

I have used dart_console in a couple of other apps I've built on to DCli that use the dart. critical_test uses the library to output progress messages on a single line (e.g. each time it runs a test it updates the same line with the name of the test, no. of failures and successes so far). I also have a classic progress bar in one of my apps.

Here is the main class I expose: import 'dart:io';

import 'package:dart_console/dart_console.dart';

import 'ansi.dart';

/// /// Modes available when clearing a screen or line. /// /// When used with clearScreen: /// [all] - clears the entire screen /// [fromCursor] - clears from the cursor until the end of the screen /// [toCursor] - clears from the start of the screen to the cursor. /// /// When used with clearLine: /// [all] - clears the entire line /// [fromCursor] - clears from the cursor until the end of the line. /// [toCursor] - clears from the start of the line to the cursor. /// enum TerminalClearMode { // scrollback, /// clear whole screen all,

/// clear screen from the cursor to the bottom of the screen. fromCursor,

/// clear screen from the top of the screen to the cursor toCursor }

/// Provides access to the Ansi Terminal. class Terminal { /// Factory ctor to get a Termainl factory Terminal() => _self;

// ignore: flutter_style_todos /// TODO(bsutton): if we don't have a terminal or ansi isn't support /// we need to suppress any ansi codes being output.

Terminal._internal();

static final _self = Terminal._internal();

final _console = Console();

/// Returns true if ansi escape characters are supported. bool get isAnsi => Ansi.isSupported;

/// Clears the screen. /// If ansi escape sequences are not supported this is a no op. /// This call does not update the cursor position so in most /// cases you will want to call [home] after calling [clearScreen]. /// dart /// Terminal() /// ..clearScreen() /// ..home(); /// void clearScreen({TerminalClearMode mode = TerminalClearMode.all}) { switch (mode) { // case AnsiClearMode.scrollback: // write('${esc}3J', newline: false); // break;

case TerminalClearMode.all: _console.clearScreen(); break; case TerminalClearMode.fromCursor: write('${Ansi.esc}0Jm'); break; case TerminalClearMode.toCursor: write('${Ansi.esc}1Jm'); break; } }

/// Clears the current line, moves the cursor to column 0 /// and then prints [text] effectively overwriting the current /// console line. /// If the current console doesn't support ansi escape /// sequences ([isAnsi] == false) then this call /// will simply revert to calling [print]. void overwriteLine(String text) { clearLine(); column = 0; _console.write(text); }

/// Writes [text] to the terminal at the current /// cursor location without appending a newline character. void write(String text) { _console.write(text); }

/// Writes [text] to the console followed by a newline. /// You can control the alignment of [text] by passing the optional /// [alignment] argment which defaults to left alignment. /// The alignment is based on the current terminals width with /// spaces inserted to the left of the string to facilitate the alignment. /// Make certain the current line is clear and the cursor is at column 0 /// before calling this method otherwise the alignment will not work /// as expected. void writeLine(String text, {TextAlignment alignment = TextAlignment.left}) => _console.writeLine(text, alignment);

/// Clears the current console line without moving the cursor. /// If you want to write over the current line then /// call [clearLine] followed by [startOfLine] and then /// use [write] rather than print as it will leave /// the cursor on the current line. /// Alternatively use [overwriteLine]; void clearLine({TerminalClearMode mode = TerminalClearMode.all}) { switch (mode) { // case AnsiClearMode.scrollback: case TerminalClearMode.all: _console.eraseLine(); break; case TerminalClearMode.fromCursor: _console.eraseCursorToEnd(); break; case TerminalClearMode.toCursor: write('${Ansi.esc}1K'); break; } }

/// show/hide the cursor void showCursor({required bool show}) { if (show) { _console.showCursor(); } else { _console.hideCursor(); } }

/// Moves the cursor to the start of previous line. @Deprecated('Use [cursorUp]') static void previousLine() { Terminal().cursorUp(); }

/// Moves the cursor up one row void cursorUp() => _console.cursorUp();

/// Moves the cursor down one row void cursorDown() => _console.cursorDown();

/// Moves the cursor to the left one column void cursorLeft() => _console.cursorUp();

/// Moves the cursor to the right one column void cursorRight() => _console.cursorRight();

/// Returns the column location of the cursor int get column => _cursor?.col ?? 0;

/// moves the cursor to the given column /// 0 is the first column // ignore: avoid_setters_without_getters set column(int column) { _console.cursorPosition = Coordinate(row, 0); }

/// Moves the cursor to the start of line. void startOfLine() { column = 0; }

/// The width of the terminal in columns. /// Where a column is one character wide. /// If no terminal is attached to a value of 80 is returned. /// This value can change if the users resizes the console window. int get columns { if (hasTerminal) { return _console.windowWidth; } else { return 80; } }

/// Returns the row location of the cursor. /// The first row is row 0. int get row => _cursor?.row ?? 24;

/// moves the cursor to the given row /// 0 is the first row set row(int row) { _console.cursorPosition = Coordinate(row, column); }

Coordinate? get _cursor { /// attempting to get the cursor position if /// we don't have a terminal will hang the app /// as to obtain the cursor we must read from the /// terminal. if (hasTerminal && Ansi.isSupported) { try { return console.cursorPosition; // ignore: avoid_catching_errors } on RangeError catch () { // if stdin is closed (seems to be within docker) // then the call to cursorPosition will fail. // RangeError: Invalid value: Not in inclusive range 0..1114111: -1 // new String.fromCharCode (dart:core-patch/string_patch.dart:45) // Console.cursorPosition (package:dart_console/src/console.dart:304) return null; } } else { return null; } }

/// Whether a terminal is attached to stdin. bool get hasTerminal => stdin.hasTerminal;

/// The height of the terminal in rows. /// Where a row is one character high. /// If no terminal is attached to stdout, a [StdoutException] is thrown. /// This value can change if the users resizes the console window. int get rows { if (hasTerminal) { return _console.windowHeight; } else { return 24; } }

/// Sets the cursor to the top left corner /// of the screen (0,0) void home() => _console.resetCursorPosition();

/// Returns the current console height in rows. @Deprecated('Use rows') int get lines => rows; }

S. Brett Sutton Noojee Contact Solutions 03 8320 8100

On Wed, 11 Aug 2021 at 00:34, Tim Sneath @.***> wrote:

Yeah, there's certainly the opportunity to detect whether the console is running headless or not. I'm wondering how other tools do it.

Help me understand the scenario: is it something like a progress bar, where you want to keep rewriting the same line? Looking at our own tools (e.g. dart test), I see we just spew out multiple lines in that scenario. There may be a variant of the method that resets to the home column even if it's not a terminal that supports ANSI escape codes.

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/timsneath/dart_console/issues/34#issuecomment-896082891, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAG32OC6BAX33P5VYYJP7ATT4E2HVANCNFSM462TOABQ . Triage notifications on the go with GitHub Mobile for iOS https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Android https://play.google.com/store/apps/details?id=com.github.android&utm_campaign=notification-email .

bsutton avatar Aug 10 '21 22:08 bsutton