diff --git a/.github/workflows/mason_logger.yaml b/.github/workflows/mason_logger.yaml index 37c77c539..69c19abd8 100644 --- a/.github/workflows/mason_logger.yaml +++ b/.github/workflows/mason_logger.yaml @@ -55,7 +55,7 @@ jobs: - name: Run Tests run: | dart pub global activate coverage 1.2.0 - dart test --coverage=coverage && dart pub global run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info --packages=.dart_tool/package_config.json --report-on=lib + dart test --coverage=coverage && dart pub global run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info --packages=.dart_tool/package_config.json --report-on=lib --check-ignore - name: Check Code Coverage uses: VeryGoodOpenSource/very_good_coverage@v2 diff --git a/packages/mason_logger/lib/src/ffi/terminal.dart b/packages/mason_logger/lib/src/ffi/terminal.dart new file mode 100644 index 000000000..10bc52fda --- /dev/null +++ b/packages/mason_logger/lib/src/ffi/terminal.dart @@ -0,0 +1,21 @@ +// coverage:ignore-file + +import 'dart:io'; + +import 'package:mason_logger/src/ffi/unix_terminal.dart'; +import 'package:mason_logger/src/ffi/windows_terminal.dart'; + +/// {@template terminal} +/// Interface for the underlying native terminal. +/// {@endterminal} +abstract class Terminal { + /// {@macro terminal} + factory Terminal() => Platform.isWindows ? WindowsTerminal() : UnixTerminal(); + + /// Enables raw mode which allows us to process each keypress as it comes in. + /// https://viewsourcecode.org/snaptoken/kilo/02.enteringRawMode.html + void enableRawMode(); + + /// Disables raw mode and restores the terminal’s original attributes. + void disableRawMode(); +} diff --git a/packages/mason_logger/lib/src/ffi/unix_terminal.dart b/packages/mason_logger/lib/src/ffi/unix_terminal.dart new file mode 100644 index 000000000..4d27eefac --- /dev/null +++ b/packages/mason_logger/lib/src/ffi/unix_terminal.dart @@ -0,0 +1,129 @@ +// coverage:ignore-file +// ignore_for_file: public_member_api_docs, constant_identifier_names, camel_case_types, non_constant_identifier_names, lines_longer_than_80_chars + +import 'dart:ffi'; +import 'dart:io'; + +import 'package:ffi/ffi.dart'; +import 'package:mason_logger/src/ffi/terminal.dart'; + +class UnixTerminal implements Terminal { + UnixTerminal() { + _lib = Platform.isMacOS + ? DynamicLibrary.open('/usr/lib/libSystem.dylib') + : DynamicLibrary.open('libc.so.6'); + + _tcgetattr = _lib.lookupFunction( + 'tcgetattr', + ); + _tcsetattr = _lib.lookupFunction( + 'tcsetattr', + ); + + _origTermIOSPointer = calloc(); + _tcgetattr(_STDIN_FILENO, _origTermIOSPointer); + } + + late final DynamicLibrary _lib; + late final Pointer _origTermIOSPointer; + late final TCGetAttrDart _tcgetattr; + late final TCSetAttrDart _tcsetattr; + + @override + void enableRawMode() { + final origTermIOS = _origTermIOSPointer.ref; + final newTermIOSPointer = calloc() + ..ref.c_iflag = + origTermIOS.c_iflag & ~(_BRKINT | _ICRNL | _INPCK | _ISTRIP | _IXON) + ..ref.c_oflag = origTermIOS.c_oflag & ~_OPOST + ..ref.c_cflag = (origTermIOS.c_cflag & ~_CSIZE) | _CS8 + ..ref.c_lflag = origTermIOS.c_lflag & ~(_ECHO | _ICANON | _IEXTEN | _ISIG) + ..ref.c_cc = origTermIOS.c_cc + ..ref.c_cc[_VMIN] = 0 + ..ref.c_cc[_VTIME] = 1 + ..ref.c_ispeed = origTermIOS.c_ispeed + ..ref.c_oflag = origTermIOS.c_ospeed; + + _tcsetattr(_STDIN_FILENO, _TCSANOW, newTermIOSPointer); + calloc.free(newTermIOSPointer); + } + + @override + void disableRawMode() { + if (nullptr == _origTermIOSPointer.cast()) return; + _tcsetattr(_STDIN_FILENO, _TCSANOW, _origTermIOSPointer); + } +} + +// Input Modes +// https://ftp.gnu.org/old-gnu/Manuals/glibc-2.2.3/html_node/libc_352.html +const int _BRKINT = 0x00000002; +const int _INPCK = 0x00000010; +const int _ISTRIP = 0x00000020; +const int _ICRNL = 0x00000100; +const int _IXON = 0x00000200; + +// Output Modes +// https://ftp.gnu.org/old-gnu/Manuals/glibc-2.2.3/html_node/libc_353.html#SEC362 +const int _OPOST = 0x00000001; + +// Control Modes +// https://ftp.gnu.org/old-gnu/Manuals/glibc-2.2.3/html_node/libc_354.html#SEC363 +const int _CSIZE = 0x00000300; +const int _CS8 = 0x00000300; + +// Local Modes +// https://ftp.gnu.org/old-gnu/Manuals/glibc-2.2.3/html_node/libc_355.html#SEC364 +const int _ECHO = 0x00000008; +const int _ISIG = 0x00000080; +const int _ICANON = 0x00000100; +const int _IEXTEN = 0x00000400; +const int _TCSANOW = 0; +const int _VMIN = 16; +const int _VTIME = 17; + +typedef tcflag_t = UnsignedLong; +typedef cc_t = UnsignedChar; +typedef speed_t = UnsignedLong; + +// The default standard input file descriptor number which is 0. +const _STDIN_FILENO = 0; + +// The number of elements in the control chars array. +const _NCSS = 20; + +class TermIOS extends Struct { + @tcflag_t() + external int c_iflag; // input flags + @tcflag_t() + external int c_oflag; // output flags + @tcflag_t() + external int c_cflag; // control flags + @tcflag_t() + external int c_lflag; // local flags + @Array(_NCSS) + external Array c_cc; // control chars + @speed_t() + external int c_ispeed; // input speed + @speed_t() + external int c_ospeed; // output speed +} + +// int tcgetattr(int, struct termios *); +typedef TCGetAttrNative = Int32 Function( + Int32 fildes, + Pointer termios, +); +typedef TCGetAttrDart = int Function(int fildes, Pointer termios); + +// int tcsetattr(int, int, const struct termios *); +typedef TCSetAttrNative = Int32 Function( + Int32 fildes, + Int32 optional_actions, + Pointer termios, +); +typedef TCSetAttrDart = int Function( + int fildes, + int optional_actions, + Pointer termios, +); diff --git a/packages/mason_logger/lib/src/ffi/windows_terminal.dart b/packages/mason_logger/lib/src/ffi/windows_terminal.dart new file mode 100644 index 000000000..4a272d471 --- /dev/null +++ b/packages/mason_logger/lib/src/ffi/windows_terminal.dart @@ -0,0 +1,37 @@ +// coverage:ignore-file +// ignore_for_file: public_member_api_docs + +import 'package:mason_logger/src/ffi/terminal.dart'; +import 'package:win32/win32.dart'; + +class WindowsTerminal implements Terminal { + WindowsTerminal() { + outputHandle = GetStdHandle(STD_OUTPUT_HANDLE); + inputHandle = GetStdHandle(STD_INPUT_HANDLE); + } + + late final int inputHandle; + late final int outputHandle; + + @override + void enableRawMode() { + const dwMode = (~ENABLE_ECHO_INPUT) & + (~ENABLE_PROCESSED_INPUT) & + (~ENABLE_LINE_INPUT) & + (~ENABLE_WINDOW_INPUT); + SetConsoleMode(inputHandle, dwMode); + } + + @override + void disableRawMode() { + const dwMode = ENABLE_ECHO_INPUT | + ENABLE_EXTENDED_FLAGS | + ENABLE_INSERT_MODE | + ENABLE_LINE_INPUT | + ENABLE_MOUSE_INPUT | + ENABLE_PROCESSED_INPUT | + ENABLE_QUICK_EDIT_MODE | + ENABLE_VIRTUAL_TERMINAL_INPUT; + SetConsoleMode(inputHandle, dwMode); + } +} diff --git a/packages/mason_logger/lib/src/mason_logger.dart b/packages/mason_logger/lib/src/mason_logger.dart index a32bc532a..b8e01db38 100644 --- a/packages/mason_logger/lib/src/mason_logger.dart +++ b/packages/mason_logger/lib/src/mason_logger.dart @@ -3,8 +3,9 @@ import 'dart:convert'; import 'dart:io' as io; import 'package:mason_logger/mason_logger.dart'; +import 'package:mason_logger/src/ffi/terminal.dart'; import 'package:mason_logger/src/io.dart'; -import 'package:mason_logger/src/stdin_overrides.dart'; +import 'package:mason_logger/src/terminal_overrides.dart'; part 'progress.dart'; @@ -90,8 +91,15 @@ class Logger { io.Stdout get _stdout => _overrides?.stdout ?? io.stdout; io.Stdin get _stdin => _overrides?.stdin ?? io.stdin; io.Stdout get _stderr => _overrides?.stderr ?? io.stderr; + final _terminal = TerminalOverrides.current?.createTerminal() ?? Terminal(); + KeyStroke Function() get _readKey { - return StdinOverrides.current?.readKey ?? readKey; + return () { + _terminal.enableRawMode(); + final key = TerminalOverrides.current?.readKey() ?? readKey(); + _terminal.disableRawMode(); + return key; + }; } /// Flushes internal message queue. diff --git a/packages/mason_logger/lib/src/stdin_overrides.dart b/packages/mason_logger/lib/src/stdin_overrides.dart deleted file mode 100644 index 6b575787c..000000000 --- a/packages/mason_logger/lib/src/stdin_overrides.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'dart:async'; -import 'package:mason_logger/src/io.dart' as io; - -const _asyncRunZoned = runZoned; - -/// This class facilitates overriding stdin utilities for reading key strokes. -/// It should be extended by another class in client code with overrides -/// that construct a custom implementation. -abstract class StdinOverrides { - static final _token = Object(); - - /// Returns the current [StdinOverrides] instance. - /// - /// This will return `null` if the current [Zone] does not contain - /// any [StdinOverrides]. - /// - /// See also: - /// * [StdinOverrides.runZoned] to provide [StdinOverrides] - /// in a fresh [Zone]. - /// - static StdinOverrides? get current { - return Zone.current[_token] as StdinOverrides?; - } - - /// Runs [body] in a fresh [Zone] using the provided overrides. - static R runZoned( - R Function() body, { - io.KeyStroke Function()? readKey, - }) { - final overrides = _StdinOverridesScope(readKey); - return _asyncRunZoned(body, zoneValues: {_token: overrides}); - } - - /// The function used to read key strokes from stdin. - io.KeyStroke Function() get readKey => io.readKey; -} - -class _StdinOverridesScope extends StdinOverrides { - _StdinOverridesScope(this._readKey); - - final StdinOverrides? _previous = StdinOverrides.current; - final io.KeyStroke Function()? _readKey; - - @override - io.KeyStroke Function() get readKey { - return _readKey ?? _previous?.readKey ?? super.readKey; - } -} diff --git a/packages/mason_logger/lib/src/terminal_overrides.dart b/packages/mason_logger/lib/src/terminal_overrides.dart new file mode 100644 index 000000000..233146330 --- /dev/null +++ b/packages/mason_logger/lib/src/terminal_overrides.dart @@ -0,0 +1,59 @@ +import 'dart:async'; +import 'package:mason_logger/src/ffi/terminal.dart'; +import 'package:mason_logger/src/io.dart' as io; + +const _asyncRunZoned = runZoned; + +/// This class facilitates overriding terminal utilities. +/// It should be extended by another class in client code with overrides +/// that construct a custom implementation. +abstract class TerminalOverrides { + static final _token = Object(); + + /// Returns the current [TerminalOverrides] instance. + /// + /// This will return `null` if the current [Zone] does not contain + /// any [TerminalOverrides]. + /// + /// See also: + /// * [TerminalOverrides.runZoned] to provide [TerminalOverrides] + /// in a fresh [Zone]. + /// + static TerminalOverrides? get current { + return Zone.current[_token] as TerminalOverrides?; + } + + /// Runs [body] in a fresh [Zone] using the provided overrides. + static R runZoned( + R Function() body, { + io.KeyStroke Function()? readKey, + Terminal Function()? createTerminal, + }) { + final overrides = _StdinOverridesScope(readKey, createTerminal); + return _asyncRunZoned(body, zoneValues: {_token: overrides}); + } + + /// The function used to read key strokes from stdin. + io.KeyStroke Function() get readKey => io.readKey; + + /// The function used to create a [Terminal] instance. + Terminal Function() get createTerminal => Terminal.new; +} + +class _StdinOverridesScope extends TerminalOverrides { + _StdinOverridesScope(this._readKey, this._createTerminal); + + final TerminalOverrides? _previous = TerminalOverrides.current; + final io.KeyStroke Function()? _readKey; + final Terminal Function()? _createTerminal; + + @override + io.KeyStroke Function() get readKey { + return _readKey ?? _previous?.readKey ?? super.readKey; + } + + @override + Terminal Function() get createTerminal { + return _createTerminal ?? _previous?.createTerminal ?? super.createTerminal; + } +} diff --git a/packages/mason_logger/pubspec.yaml b/packages/mason_logger/pubspec.yaml index 990c902cf..8e0637a99 100644 --- a/packages/mason_logger/pubspec.yaml +++ b/packages/mason_logger/pubspec.yaml @@ -10,6 +10,10 @@ topics: [cli, logger, mason] environment: sdk: ">=2.19.0 <4.0.0" +dependencies: + ffi: ^2.1.0 + win32: ^5.0.7 + dev_dependencies: meta: ^1.7.0 mocktail: ^1.0.0 diff --git a/packages/mason_logger/test/src/mason_logger_test.dart b/packages/mason_logger/test/src/mason_logger_test.dart index 028be78bb..5e0c9f7c2 100644 --- a/packages/mason_logger/test/src/mason_logger_test.dart +++ b/packages/mason_logger/test/src/mason_logger_test.dart @@ -2,7 +2,7 @@ import 'dart:io'; import 'package:mason_logger/mason_logger.dart'; import 'package:mason_logger/src/io.dart'; -import 'package:mason_logger/src/stdin_overrides.dart'; +import 'package:mason_logger/src/terminal_overrides.dart'; import 'package:mocktail/mocktail.dart'; import 'package:test/test.dart'; @@ -621,8 +621,8 @@ void main() { test( 'enter/return selects the nothing ' 'when defaultValues is not specified.', () { - final keyStrokes = [KeyStroke.control(ControlCharacter.ctrlJ)]; - StdinOverrides.runZoned( + final keyStrokes = [KeyStroke.control(ControlCharacter.ctrlM)]; + TerminalOverrides.runZoned( () => IOOverrides.runZoned( () { const message = 'test message'; @@ -651,8 +651,8 @@ void main() { }); test('enter/return selects the default values when specified.', () { - final keyStrokes = [KeyStroke.control(ControlCharacter.ctrlJ)]; - StdinOverrides.runZoned( + final keyStrokes = [KeyStroke.control(ControlCharacter.ctrlM)]; + TerminalOverrides.runZoned( () => IOOverrides.runZoned( () { const message = 'test message'; @@ -692,9 +692,9 @@ void main() { KeyStroke.char(' '), KeyStroke.control(ControlCharacter.arrowDown), KeyStroke.char(' '), - KeyStroke.control(ControlCharacter.ctrlJ), + KeyStroke.control(ControlCharacter.ctrlM), ]; - StdinOverrides.runZoned( + TerminalOverrides.runZoned( () => IOOverrides.runZoned( () { const message = 'test message'; @@ -786,7 +786,7 @@ void main() { KeyStroke.control(ControlCharacter.arrowDown), KeyStroke.control(ControlCharacter.ctrlM), ]; - StdinOverrides.runZoned( + TerminalOverrides.runZoned( () => IOOverrides.runZoned( () { const message = 'test message'; @@ -828,7 +828,7 @@ void main() { KeyStroke.char('j'), KeyStroke.control(ControlCharacter.ctrlM), ]; - StdinOverrides.runZoned( + TerminalOverrides.runZoned( () => IOOverrides.runZoned( () { const message = 'test message'; @@ -870,7 +870,7 @@ void main() { KeyStroke.control(ControlCharacter.arrowUp), KeyStroke.control(ControlCharacter.ctrlM), ]; - StdinOverrides.runZoned( + TerminalOverrides.runZoned( () => IOOverrides.runZoned( () { const message = 'test message'; @@ -912,7 +912,7 @@ void main() { KeyStroke.char('k'), KeyStroke.control(ControlCharacter.ctrlM), ]; - StdinOverrides.runZoned( + TerminalOverrides.runZoned( () => IOOverrides.runZoned( () { const message = 'test message'; @@ -956,7 +956,7 @@ void main() { KeyStroke.control(ControlCharacter.arrowDown), KeyStroke.control(ControlCharacter.ctrlM), ]; - StdinOverrides.runZoned( + TerminalOverrides.runZoned( () => IOOverrides.runZoned( () { const message = 'test message'; @@ -1013,7 +1013,7 @@ void main() { test('converts choices to a preferred display', () { final keyStrokes = [KeyStroke.control(ControlCharacter.ctrlM)]; - StdinOverrides.runZoned( + TerminalOverrides.runZoned( () => IOOverrides.runZoned( () { const message = 'test message'; @@ -1048,7 +1048,7 @@ void main() { test('converts results to a preferred display', () { final keyStrokes = [KeyStroke.control(ControlCharacter.ctrlM)]; - StdinOverrides.runZoned( + TerminalOverrides.runZoned( () => IOOverrides.runZoned( () { const message = 'test message'; @@ -1080,7 +1080,7 @@ void main() { 'enter selects the initial value ' 'when defaultValue is not specified.', () { final keyStrokes = [KeyStroke.control(ControlCharacter.ctrlM)]; - StdinOverrides.runZoned( + TerminalOverrides.runZoned( () => IOOverrides.runZoned( () { const message = 'test message'; @@ -1112,7 +1112,7 @@ void main() { test('enter selects the default value when specified.', () { final keyStrokes = [KeyStroke.control(ControlCharacter.ctrlM)]; - StdinOverrides.runZoned( + TerminalOverrides.runZoned( () => IOOverrides.runZoned( () { const message = 'test message'; @@ -1145,7 +1145,7 @@ void main() { test('space selects the default value when specified.', () { final keyStrokes = [KeyStroke.char(' ')]; - StdinOverrides.runZoned( + TerminalOverrides.runZoned( () => IOOverrides.runZoned( () { const message = 'test message'; @@ -1182,7 +1182,7 @@ void main() { KeyStroke.control(ControlCharacter.arrowDown), KeyStroke.control(ControlCharacter.ctrlJ), ]; - StdinOverrides.runZoned( + TerminalOverrides.runZoned( () => IOOverrides.runZoned( () { const message = 'test message'; @@ -1227,7 +1227,7 @@ void main() { KeyStroke.control(ControlCharacter.arrowUp), KeyStroke.control(ControlCharacter.ctrlJ), ]; - StdinOverrides.runZoned( + TerminalOverrides.runZoned( () => IOOverrides.runZoned( () { const message = 'test message'; @@ -1273,7 +1273,7 @@ void main() { KeyStroke.control(ControlCharacter.arrowUp), KeyStroke.control(ControlCharacter.ctrlJ), ]; - StdinOverrides.runZoned( + TerminalOverrides.runZoned( () => IOOverrides.runZoned( () { const message = 'test message'; @@ -1318,7 +1318,7 @@ void main() { KeyStroke.control(ControlCharacter.arrowDown), KeyStroke.control(ControlCharacter.ctrlJ), ]; - StdinOverrides.runZoned( + TerminalOverrides.runZoned( () => IOOverrides.runZoned( () { const message = 'test message'; @@ -1364,7 +1364,7 @@ void main() { KeyStroke.char('j'), KeyStroke.control(ControlCharacter.ctrlJ), ]; - StdinOverrides.runZoned( + TerminalOverrides.runZoned( () => IOOverrides.runZoned( () { const message = 'test message'; @@ -1409,7 +1409,7 @@ void main() { KeyStroke.char('k'), KeyStroke.control(ControlCharacter.ctrlJ), ]; - StdinOverrides.runZoned( + TerminalOverrides.runZoned( () => IOOverrides.runZoned( () { const message = 'test message'; @@ -1452,7 +1452,7 @@ void main() { test('converts choices to a preferred display', () { final keyStrokes = [KeyStroke.control(ControlCharacter.ctrlJ)]; - StdinOverrides.runZoned( + TerminalOverrides.runZoned( () => IOOverrides.runZoned( () { const message = 'test message'; @@ -1492,7 +1492,7 @@ void main() { group('promptAny', () { test('returns empty list', () { final keyStrokes = [KeyStroke.control(ControlCharacter.ctrlJ)]; - StdinOverrides.runZoned( + TerminalOverrides.runZoned( () => IOOverrides.runZoned( () { const message = 'test message'; @@ -1516,7 +1516,7 @@ void main() { KeyStroke.char('t'), KeyStroke.control(ControlCharacter.ctrlJ), ]; - StdinOverrides.runZoned( + TerminalOverrides.runZoned( () => IOOverrides.runZoned( () { const message = 'test message'; @@ -1544,7 +1544,7 @@ void main() { KeyStroke.char('s'), KeyStroke.control(ControlCharacter.ctrlJ), ]; - StdinOverrides.runZoned( + TerminalOverrides.runZoned( () => IOOverrides.runZoned( () { const message = 'test message'; @@ -1573,7 +1573,7 @@ void main() { KeyStroke.char(','), KeyStroke.control(ControlCharacter.ctrlJ), ]; - StdinOverrides.runZoned( + TerminalOverrides.runZoned( () => IOOverrides.runZoned( () { const message = 'test message'; @@ -1606,7 +1606,7 @@ void main() { KeyStroke.char(','), KeyStroke.control(ControlCharacter.ctrlJ), ]; - StdinOverrides.runZoned( + TerminalOverrides.runZoned( () => IOOverrides.runZoned( () { const message = 'test message'; @@ -1632,9 +1632,9 @@ void main() { KeyStroke.char('c'), KeyStroke.char('s'), KeyStroke.char('s'), - KeyStroke.control(ControlCharacter.ctrlJ), + KeyStroke.control(ControlCharacter.ctrlM), ]; - StdinOverrides.runZoned( + TerminalOverrides.runZoned( () => IOOverrides.runZoned( () { const message = 'test message'; @@ -1662,7 +1662,7 @@ void main() { KeyStroke.char('s'), KeyStroke.control(ControlCharacter.ctrlJ), ]; - StdinOverrides.runZoned( + TerminalOverrides.runZoned( () => IOOverrides.runZoned( () { const message = 'test message'; @@ -1694,7 +1694,7 @@ void main() { KeyStroke.char('s'), KeyStroke.control(ControlCharacter.ctrlJ), ]; - StdinOverrides.runZoned( + TerminalOverrides.runZoned( () => IOOverrides.runZoned( () { const message = 'test message'; diff --git a/packages/mason_logger/test/src/stdin_overrides_test.dart b/packages/mason_logger/test/src/stdin_overrides_test.dart deleted file mode 100644 index 926a5dcfe..000000000 --- a/packages/mason_logger/test/src/stdin_overrides_test.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:mason_logger/src/io.dart'; -import 'package:mason_logger/src/stdin_overrides.dart'; -import 'package:test/test.dart'; - -void main() { - group(StdinOverrides, () { - group('runZoned', () { - test('uses default readKey when not specified', () { - StdinOverrides.runZoned(() { - final overrides = StdinOverrides.current; - expect(overrides!.readKey, isNotNull); - }); - }); - - test('uses custom readKey when specified', () { - StdinOverrides.runZoned( - () { - final overrides = StdinOverrides.current; - expect( - overrides!.readKey(), - isA().having((s) => s.char, 'char', 'a'), - ); - }, - readKey: () => KeyStroke.char('a'), - ); - }); - - test( - 'uses current readKey when not specified ' - 'and zone already contains a readKey', () { - StdinOverrides.runZoned( - () { - StdinOverrides.runZoned(() { - final overrides = StdinOverrides.current; - expect( - overrides!.readKey(), - isA().having((s) => s.char, 'char', 'x'), - ); - }); - }, - readKey: () => KeyStroke.char('x'), - ); - }); - - test( - 'uses nested readKey when specified ' - 'and zone already contains a readKey', () { - KeyStroke rootReadKey() => KeyStroke.char('a'); - StdinOverrides.runZoned( - () { - KeyStroke nestedReadKey() => KeyStroke.char('b'); - final overrides = StdinOverrides.current; - expect( - overrides!.readKey(), - isA().having((s) => s.char, 'char', 'a'), - ); - StdinOverrides.runZoned( - () { - final overrides = StdinOverrides.current; - expect( - overrides!.readKey(), - isA().having((s) => s.char, 'char', 'b'), - ); - }, - readKey: nestedReadKey, - ); - }, - readKey: rootReadKey, - ); - }); - }); - }); -} diff --git a/packages/mason_logger/test/src/terminal_overrides_test.dart b/packages/mason_logger/test/src/terminal_overrides_test.dart new file mode 100644 index 000000000..17110f60c --- /dev/null +++ b/packages/mason_logger/test/src/terminal_overrides_test.dart @@ -0,0 +1,143 @@ +import 'package:mason_logger/src/ffi/terminal.dart'; +import 'package:mason_logger/src/io.dart'; +import 'package:mason_logger/src/terminal_overrides.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +class _MockTerminal extends Mock implements Terminal {} + +void main() { + group(TerminalOverrides, () { + group('runZoned', () { + test('uses default readKey when not specified', () { + TerminalOverrides.runZoned(() { + final overrides = TerminalOverrides.current; + expect(overrides!.readKey, isNotNull); + }); + }); + + test('uses custom readKey when specified', () { + TerminalOverrides.runZoned( + () { + final overrides = TerminalOverrides.current; + expect( + overrides!.readKey(), + isA().having((s) => s.char, 'char', 'a'), + ); + }, + readKey: () => KeyStroke.char('a'), + ); + }); + + test( + 'uses current readKey when not specified ' + 'and zone already contains a readKey', () { + TerminalOverrides.runZoned( + () { + TerminalOverrides.runZoned(() { + final overrides = TerminalOverrides.current; + expect( + overrides!.readKey(), + isA().having((s) => s.char, 'char', 'x'), + ); + }); + }, + readKey: () => KeyStroke.char('x'), + ); + }); + + test( + 'uses nested readKey when specified ' + 'and zone already contains a readKey', () { + KeyStroke rootReadKey() => KeyStroke.char('a'); + TerminalOverrides.runZoned( + () { + KeyStroke nestedReadKey() => KeyStroke.char('b'); + final overrides = TerminalOverrides.current; + expect( + overrides!.readKey(), + isA().having((s) => s.char, 'char', 'a'), + ); + TerminalOverrides.runZoned( + () { + final overrides = TerminalOverrides.current; + expect( + overrides!.readKey(), + isA().having((s) => s.char, 'char', 'b'), + ); + }, + readKey: nestedReadKey, + ); + }, + readKey: rootReadKey, + ); + }); + + test('uses default createTerminal when not specified', () { + TerminalOverrides.runZoned(() { + final overrides = TerminalOverrides.current; + expect(overrides!.createTerminal, isNotNull); + }); + }); + + test('uses custom createTerminal when specified', () { + final Terminal terminal = _MockTerminal(); + TerminalOverrides.runZoned( + () { + final overrides = TerminalOverrides.current; + expect( + overrides!.createTerminal(), + equals(terminal), + ); + }, + createTerminal: () => terminal, + ); + }); + + test( + 'uses current createTerminal when not specified ' + 'and zone already contains a createTerminal', () { + final Terminal terminal = _MockTerminal(); + TerminalOverrides.runZoned( + () { + TerminalOverrides.runZoned(() { + final overrides = TerminalOverrides.current; + expect( + overrides!.createTerminal(), + equals(terminal), + ); + }); + }, + createTerminal: () => terminal, + ); + }); + + test( + 'uses nested readKey when specified ' + 'and zone already contains a readKey', () { + final Terminal rootTerminal = _MockTerminal(); + TerminalOverrides.runZoned( + () { + final Terminal nestedTerminal = _MockTerminal(); + final overrides = TerminalOverrides.current; + expect( + overrides!.createTerminal(), + equals(rootTerminal), + ); + TerminalOverrides.runZoned( + () { + final overrides = TerminalOverrides.current; + expect( + overrides!.createTerminal(), + equals(nestedTerminal), + ); + }, + createTerminal: () => nestedTerminal, + ); + }, + createTerminal: () => rootTerminal, + ); + }); + }); + }); +}