From 3770c1e3428448458a66bd63ba15e06fcacdb8d6 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Mon, 20 Jul 2026 15:39:32 +0200 Subject: [PATCH] fix(mason_logger): redraw chooseOne/chooseAny with relative cursor moves --- .../mason_logger/lib/src/mason_logger.dart | 38 ++++--- .../test/src/mason_logger_test.dart | 103 +++++++++++------- 2 files changed, 86 insertions(+), 55 deletions(-) diff --git a/packages/mason_logger/lib/src/mason_logger.dart b/packages/mason_logger/lib/src/mason_logger.dart index 87cd48cbb..f45672838 100644 --- a/packages/mason_logger/lib/src/mason_logger.dart +++ b/packages/mason_logger/lib/src/mason_logger.dart @@ -311,10 +311,10 @@ class Logger { defaultValue != null && resolvedDisplay(defaultValue).isNotEmpty; var index = hasDefault ? choices.indexOf(defaultValue) : 0; + final linesToClear = '$message'.split('\n').length + choices.length - 1; + void writeChoices() { _stdout - // save cursor - ..write('\x1b7') // hide cursor ..write('\x1b[?25l') ..writeln('$message'); @@ -364,11 +364,8 @@ class Logger { ..lineMode = true ..echoMode = true; + _clearChoices(linesToClear); _stdout - // restore cursor - ..write('\x1b8') - // clear to end of screen - ..write('\x1b[J') // show cursor ..write('\x1b[?25h') ..write('$message ') @@ -380,8 +377,7 @@ class Logger { break; } - // restore cursor - _stdout.write('\x1b8'); + _clearChoices(linesToClear); writeChoices(); } @@ -409,10 +405,10 @@ class Logger { : {}; var index = 0; + final linesToClear = '$message'.split('\n').length + choices.length - 1; + void writeChoices() { _stdout - // save cursor - ..write('\x1b7') // hide cursor ..write('\x1b[?25l') ..writeln('$message'); @@ -469,11 +465,8 @@ class Logger { results = selections.map((index) => choices[index]).toList(); + _clearChoices(linesToClear); _stdout - // restore cursor - ..write('\x1b8') - // clear to end of screen - ..write('\x1b[J') // show cursor ..write('\x1b[?25h') ..write('$message ') @@ -486,14 +479,27 @@ class Logger { break; } - // restore cursor - _stdout.write('\x1b8'); + _clearChoices(linesToClear); writeChoices(); } return results; } + /// Moves the cursor up [lines] and clears everything from there to the end + /// of the screen so a selection prompt can be redrawn in place. + void _clearChoices(int lines) { + if (lines > 0) { + // move cursor up to the first line of the choices + _stdout.write('\x1b[${lines}A'); + } + _stdout + // move cursor to the start of the line + ..write('\r') + // clear to end of screen + ..write('\x1b[J'); + } + String? _readLineSync() { _ensureTerminalAttached(); return _stdin.readLineSync()?.trim(); diff --git a/packages/mason_logger/test/src/mason_logger_test.dart b/packages/mason_logger/test/src/mason_logger_test.dart index d28a98a44..d0ebc16da 100644 --- a/packages/mason_logger/test/src/mason_logger_test.dart +++ b/packages/mason_logger/test/src/mason_logger_test.dart @@ -708,7 +708,6 @@ void main() { ); expect(actual, isEmpty); verifyInOrder([ - () => stdout.write('\x1b7'), () => stdout.write('\x1b[?25l'), () => stdout.writeln(message), () => stdout.write(green.wrap('❯')), @@ -740,7 +739,6 @@ void main() { ); expect(actual, equals(expected)); verifyInOrder([ - () => stdout.write('\x1b7'), () => stdout.write('\x1b[?25l'), () => stdout.writeln(message), () => stdout.write(green.wrap('❯')), @@ -781,7 +779,6 @@ void main() { ); expect(actual, equals(expected)); verifyInOrder([ - () => stdout.write('\x1b7'), () => stdout.write('\x1b[?25l'), () => stdout.writeln(message), () => stdout.write(green.wrap('❯')), @@ -790,7 +787,6 @@ void main() { () => stdout.write(' ◯ b'), () => stdout.write(' '), () => stdout.write(' ◯ c'), - () => stdout.write('\x1b7'), () => stdout.write('\x1b[?25l'), () => stdout.writeln(message), () => stdout.write(green.wrap('❯')), @@ -800,7 +796,6 @@ void main() { () => stdout.write(' ◯ b'), () => stdout.write(' '), () => stdout.write(' ◯ c'), - () => stdout.write('\x1b7'), () => stdout.write('\x1b[?25l'), () => stdout.writeln(message), () => stdout.write(green.wrap('❯')), @@ -809,7 +804,6 @@ void main() { () => stdout.write(' ◯ b'), () => stdout.write(' '), () => stdout.write(' ◯ c'), - () => stdout.write('\x1b7'), () => stdout.write('\x1b[?25l'), () => stdout.writeln(message), () => stdout.write(' ◯ a'), @@ -817,7 +811,6 @@ void main() { () => stdout.write(' ◯ b'), () => stdout.write(' '), () => stdout.write(' ◯ c'), - () => stdout.write('\x1b7'), () => stdout.write('\x1b[?25l'), () => stdout.writeln(message), () => stdout.write(' '), @@ -827,7 +820,6 @@ void main() { .write(' ${lightCyan.wrap('◉')} ${lightCyan.wrap('b')}'), () => stdout.write(' '), () => stdout.write(' ◯ c'), - () => stdout.write('\x1b7'), () => stdout.write('\x1b[?25l'), () => stdout.writeln(message), () => stdout.write(' '), @@ -837,7 +829,6 @@ void main() { .write(' ${lightCyan.wrap('◉')} ${lightCyan.wrap('b')}'), () => stdout.write(green.wrap('❯')), () => stdout.write(' ◯ c'), - () => stdout.write('\x1b7'), () => stdout.write('\x1b[?25l'), () => stdout.writeln(message), () => stdout.write(' '), @@ -872,7 +863,6 @@ void main() { ); expect(actual, equals(isEmpty)); verifyInOrder([ - () => stdout.write('\x1b7'), () => stdout.write('\x1b[?25l'), () => stdout.writeln(message), () => stdout.write(green.wrap('❯')), @@ -881,7 +871,6 @@ void main() { () => stdout.write(' ◯ b'), () => stdout.write(' '), () => stdout.write(' ◯ c'), - () => stdout.write('\x1b7'), () => stdout.write('\x1b[?25l'), () => stdout.writeln(message), () => stdout.write(' '), @@ -899,6 +888,37 @@ void main() { ); }); + test('redraws in place using relative cursor moves', () { + final keyStrokes = [ + KeyStroke.control(ControlCharacter.arrowDown), + KeyStroke.control(ControlCharacter.ctrlM), + ]; + TerminalOverrides.runZoned( + () => IOOverrides.runZoned( + () { + const message = 'test message'; + Logger().chooseAny( + message, + choices: ['a', 'b', 'c'], + ); + verifyInOrder([ + () => stdout.write(' ◯ c'), + () => stdout.write('\x1b[3A'), + () => stdout.write('\r'), + () => stdout.write('\x1b[J'), + () => stdout.write('\x1b[?25l'), + () => stdout.writeln(message), + ]); + verifyNever(() => stdout.write('\x1b7')); + verifyNever(() => stdout.write('\x1b8')); + }, + stdout: () => stdout, + stdin: () => stdin, + ), + readKey: () => keyStrokes.removeAt(0), + ); + }); + test('j selects next index', () { final keyStrokes = [ KeyStroke.char('j'), @@ -914,7 +934,6 @@ void main() { ); expect(actual, equals(isEmpty)); verifyInOrder([ - () => stdout.write('\x1b7'), () => stdout.write('\x1b[?25l'), () => stdout.writeln(message), () => stdout.write(green.wrap('❯')), @@ -923,7 +942,6 @@ void main() { () => stdout.write(' ◯ b'), () => stdout.write(' '), () => stdout.write(' ◯ c'), - () => stdout.write('\x1b7'), () => stdout.write('\x1b[?25l'), () => stdout.writeln(message), () => stdout.write(' '), @@ -956,7 +974,6 @@ void main() { ); expect(actual, isEmpty); verifyInOrder([ - () => stdout.write('\x1b7'), () => stdout.write('\x1b[?25l'), () => stdout.writeln(message), () => stdout.write(green.wrap('❯')), @@ -965,7 +982,6 @@ void main() { () => stdout.write(' ◯ b'), () => stdout.write(' '), () => stdout.write(' ◯ c'), - () => stdout.write('\x1b7'), () => stdout.write('\x1b[?25l'), () => stdout.writeln(message), () => stdout.write(' '), @@ -998,7 +1014,6 @@ void main() { ); expect(actual, isEmpty); verifyInOrder([ - () => stdout.write('\x1b7'), () => stdout.write('\x1b[?25l'), () => stdout.writeln(message), () => stdout.write(green.wrap('❯')), @@ -1007,7 +1022,6 @@ void main() { () => stdout.write(' ◯ b'), () => stdout.write(' '), () => stdout.write(' ◯ c'), - () => stdout.write('\x1b7'), () => stdout.write('\x1b[?25l'), () => stdout.writeln(message), () => stdout.write(' '), @@ -1042,7 +1056,6 @@ void main() { ); expect(actual, isEmpty); verifyInOrder([ - () => stdout.write('\x1b7'), () => stdout.write('\x1b[?25l'), () => stdout.writeln(message), () => stdout.write(green.wrap('❯')), @@ -1051,7 +1064,6 @@ void main() { () => stdout.write(' ◯ b'), () => stdout.write(' '), () => stdout.write(' ◯ c'), - () => stdout.write('\x1b7'), () => stdout.write('\x1b[?25l'), () => stdout.writeln(message), () => stdout.write(' '), @@ -1060,7 +1072,6 @@ void main() { () => stdout.write(' ◯ b'), () => stdout.write(' '), () => stdout.write(' ◯ c'), - () => stdout.write('\x1b7'), () => stdout.write('\x1b[?25l'), () => stdout.writeln(message), () => stdout.write(' '), @@ -1069,7 +1080,6 @@ void main() { () => stdout.write(' ◯ b'), () => stdout.write(green.wrap('❯')), () => stdout.write(' ◯ c'), - () => stdout.write('\x1b7'), () => stdout.write('\x1b[?25l'), () => stdout.writeln(message), () => stdout.write(green.wrap('❯')), @@ -1104,7 +1114,6 @@ void main() { ); expect(actual, isEmpty); verifyInOrder([ - () => stdout.write('\x1b7'), () => stdout.write('\x1b[?25l'), () => stdout.writeln(message), () => stdout.write(green.wrap('❯')), @@ -1137,7 +1146,8 @@ void main() { ); expect(actual, equals(expected)); verifyInOrder([ - () => stdout.write('\x1b8'), + () => stdout.write('\x1b[3A'), + () => stdout.write('\r'), () => stdout.write('\x1b[J'), () => stdout.write('$message '), () => stdout.writeln('[Key: a, Key: c]'), @@ -1206,7 +1216,6 @@ void main() { ); expect(actual, equals(expected)); verifyInOrder([ - () => stdout.write('\x1b7'), () => stdout.write('\x1b[?25l'), () => stdout.writeln(message), () => stdout.write(green.wrap('❯')), @@ -1239,7 +1248,6 @@ void main() { ); expect(actual, equals(expected)); verifyInOrder([ - () => stdout.write('\x1b7'), () => stdout.write('\x1b[?25l'), () => stdout.writeln(message), () => stdout.write(' '), @@ -1273,7 +1281,6 @@ void main() { ); expect(actual, equals(expected)); verifyInOrder([ - () => stdout.write('\x1b7'), () => stdout.write('\x1b[?25l'), () => stdout.writeln(message), () => stdout.write(' '), @@ -1308,7 +1315,6 @@ void main() { ); expect(actual, equals(expected)); verifyInOrder([ - () => stdout.write('\x1b7'), () => stdout.write('\x1b[?25l'), () => stdout.writeln(message), () => stdout.write(green.wrap('❯')), @@ -1318,7 +1324,6 @@ void main() { () => stdout.write(' ◯ b'), () => stdout.write(' '), () => stdout.write(' ◯ c'), - () => stdout.write('\x1b7'), () => stdout.write('\x1b[?25l'), () => stdout.writeln(message), () => stdout.write(' '), @@ -1337,6 +1342,37 @@ void main() { ); }); + test('redraws in place using relative cursor moves', () { + final keyStrokes = [ + KeyStroke.control(ControlCharacter.arrowDown), + KeyStroke.control(ControlCharacter.ctrlJ), + ]; + TerminalOverrides.runZoned( + () => IOOverrides.runZoned( + () { + const message = 'test message'; + Logger().chooseOne( + message, + choices: ['a', 'b', 'c'], + ); + verifyInOrder([ + () => stdout.write(' ◯ c'), + () => stdout.write('\x1b[3A'), + () => stdout.write('\r'), + () => stdout.write('\x1b[J'), + () => stdout.write('\x1b[?25l'), + () => stdout.writeln(message), + ]); + verifyNever(() => stdout.write('\x1b7')); + verifyNever(() => stdout.write('\x1b8')); + }, + stdout: () => stdout, + stdin: () => stdin, + ), + readKey: () => keyStrokes.removeAt(0), + ); + }); + test('up arrow selects previous index', () { final keyStrokes = [ KeyStroke.control(ControlCharacter.arrowUp), @@ -1354,7 +1390,6 @@ void main() { ); expect(actual, equals(expected)); verifyInOrder([ - () => stdout.write('\x1b7'), () => stdout.write('\x1b[?25l'), () => stdout.writeln(message), () => stdout.write(' '), @@ -1364,7 +1399,6 @@ void main() { .write(' ${lightCyan.wrap('◉')} ${lightCyan.wrap('b')}'), () => stdout.write(' '), () => stdout.write(' ◯ c'), - () => stdout.write('\x1b7'), () => stdout.write('\x1b[?25l'), () => stdout.writeln(message), () => stdout.write(green.wrap('❯')), @@ -1399,7 +1433,6 @@ void main() { ); expect(actual, equals(expected)); verifyInOrder([ - () => stdout.write('\x1b7'), () => stdout.write('\x1b[?25l'), () => stdout.writeln(message), () => stdout.write(green.wrap('❯')), @@ -1409,7 +1442,6 @@ void main() { () => stdout.write(' ◯ b'), () => stdout.write(' '), () => stdout.write(' ◯ c'), - () => stdout.write('\x1b7'), () => stdout.write('\x1b[?25l'), () => stdout.writeln(message), () => stdout.write(' '), @@ -1445,7 +1477,6 @@ void main() { ); expect(actual, equals(expected)); verifyInOrder([ - () => stdout.write('\x1b7'), () => stdout.write('\x1b[?25l'), () => stdout.writeln(message), () => stdout.write(' '), @@ -1455,7 +1486,6 @@ void main() { () => stdout.write(green.wrap('❯')), () => stdout .write(' ${lightCyan.wrap('◉')} ${lightCyan.wrap('c')}'), - () => stdout.write('\x1b7'), () => stdout.write('\x1b[?25l'), () => stdout.writeln(message), () => stdout.write(green.wrap('❯')), @@ -1490,7 +1520,6 @@ void main() { ); expect(actual, equals(expected)); verifyInOrder([ - () => stdout.write('\x1b7'), () => stdout.write('\x1b[?25l'), () => stdout.writeln(message), () => stdout.write(green.wrap('❯')), @@ -1500,7 +1529,6 @@ void main() { () => stdout.write(' ◯ b'), () => stdout.write(' '), () => stdout.write(' ◯ c'), - () => stdout.write('\x1b7'), () => stdout.write('\x1b[?25l'), () => stdout.writeln(message), () => stdout.write(' '), @@ -1536,7 +1564,6 @@ void main() { ); expect(actual, equals(expected)); verifyInOrder([ - () => stdout.write('\x1b7'), () => stdout.write('\x1b[?25l'), () => stdout.writeln(message), () => stdout.write(' '), @@ -1546,7 +1573,6 @@ void main() { .write(' ${lightCyan.wrap('◉')} ${lightCyan.wrap('b')}'), () => stdout.write(' '), () => stdout.write(' ◯ c'), - () => stdout.write('\x1b7'), () => stdout.write('\x1b[?25l'), () => stdout.writeln(message), () => stdout.write(green.wrap('❯')), @@ -1583,7 +1609,6 @@ void main() { ); expect(actual, equals(expected)); verifyInOrder([ - () => stdout.write('\x1b7'), () => stdout.write('\x1b[?25l'), () => stdout.writeln(message), () => stdout.write(green.wrap('❯')),