Skip to content

Commit

Permalink
fix(mason_logger): arrow keys on windows (#1061)
Browse files Browse the repository at this point in the history
  • Loading branch information
felangel authored Sep 19, 2023
1 parent c192ff4 commit 8526b2d
Show file tree
Hide file tree
Showing 11 changed files with 437 additions and 157 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/mason_logger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions packages/mason_logger/lib/src/ffi/terminal.dart
Original file line number Diff line number Diff line change
@@ -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();
}
129 changes: 129 additions & 0 deletions packages/mason_logger/lib/src/ffi/unix_terminal.dart
Original file line number Diff line number Diff line change
@@ -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<TCGetAttrNative, TCGetAttrDart>(
'tcgetattr',
);
_tcsetattr = _lib.lookupFunction<TCSetAttrNative, TCSetAttrDart>(
'tcsetattr',
);

_origTermIOSPointer = calloc<TermIOS>();
_tcgetattr(_STDIN_FILENO, _origTermIOSPointer);
}

late final DynamicLibrary _lib;
late final Pointer<TermIOS> _origTermIOSPointer;
late final TCGetAttrDart _tcgetattr;
late final TCSetAttrDart _tcsetattr;

@override
void enableRawMode() {
final origTermIOS = _origTermIOSPointer.ref;
final newTermIOSPointer = calloc<TermIOS>()
..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<cc_t> 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> termios,
);
typedef TCGetAttrDart = int Function(int fildes, Pointer<TermIOS> termios);

// int tcsetattr(int, int, const struct termios *);
typedef TCSetAttrNative = Int32 Function(
Int32 fildes,
Int32 optional_actions,
Pointer<TermIOS> termios,
);
typedef TCSetAttrDart = int Function(
int fildes,
int optional_actions,
Pointer<TermIOS> termios,
);
37 changes: 37 additions & 0 deletions packages/mason_logger/lib/src/ffi/windows_terminal.dart
Original file line number Diff line number Diff line change
@@ -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);
}
}
12 changes: 10 additions & 2 deletions packages/mason_logger/lib/src/mason_logger.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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.
Expand Down
48 changes: 0 additions & 48 deletions packages/mason_logger/lib/src/stdin_overrides.dart

This file was deleted.

59 changes: 59 additions & 0 deletions packages/mason_logger/lib/src/terminal_overrides.dart
Original file line number Diff line number Diff line change
@@ -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>(
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;
}
}
4 changes: 4 additions & 0 deletions packages/mason_logger/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 8526b2d

Please sign in to comment.