diff --git a/lib/src/backend/win32_ansi_stdin.dart b/lib/src/backend/win32_ansi_stdin.dart index 7757d320..607536a5 100644 --- a/lib/src/backend/win32_ansi_stdin.dart +++ b/lib/src/backend/win32_ansi_stdin.dart @@ -54,21 +54,39 @@ class Win32AnsiStdin extends Stream> implements Stdin { void startEventLoop() { if (_running) return; _running = true; + // Dart's stdin.lineMode = false clears ENABLE_PROCESSED_INPUT, which is + // what makes Windows generate SIGINT for Ctrl+C. + _ensureProcessedInput(); _eventLoop(); } + void _ensureProcessedInput() { + final modePtr = calloc(); + try { + if (_getConsoleMode(_inputHandle, modePtr) != 0) { + _setConsoleMode(_inputHandle, modePtr.value | _enableProcessedInput); + } + } finally { + calloc.free(modePtr); + } + } + Future _eventLoop() async { final pInputRecord = calloc<_InputRecord>(); final pEventsRead = calloc(); try { while (_running) { - // Yield to Dart event loop await Future.delayed(Duration.zero); + if (!_running) break; + // Bounded wait keeps the Dart event loop responsive. Without this, + // ReadConsoleInputW parks the isolate until the next keystroke, + // starving timers and signal handlers. + final waitResult = _waitForSingleObject(_inputHandle, _pollIntervalMs); if (!_running) break; + if (waitResult != _waitObject0) continue; - // Read one input event final result = _readConsoleInputW(_inputHandle, pInputRecord, 1, pEventsRead); if (result != 0 && pEventsRead.value > 0) { @@ -377,10 +395,17 @@ class Win32AnsiStdin extends Stream> implements Stdin { // Windows API Constants const int _stdInputHandle = -10; +const int _enableProcessedInput = 0x0001; const int _enableMouseInput = 0x0010; const int _enableExtendedFlags = 0x0080; const int _enableQuickEditMode = 0x0040; +// WaitForSingleObject return value: object is signaled. +const int _waitObject0 = 0x00000000; + +// 16ms poll keeps the input loop responsive. +const int _pollIntervalMs = 16; + // Event types const int _keyEvent = 0x0001; const int _mouseEvent = 0x0002; @@ -494,6 +519,11 @@ typedef _ReadConsoleInputDart = int Function( int nLength, Pointer lpNumberOfEventsRead); +typedef _WaitForSingleObjectNative = Uint32 Function( + IntPtr hHandle, Uint32 dwMilliseconds); +typedef _WaitForSingleObjectDart = int Function( + int hHandle, int dwMilliseconds); + final _kernel32 = DynamicLibrary.open('kernel32.dll'); final _getStdHandle = _kernel32 @@ -510,3 +540,7 @@ final _setConsoleMode = final _readConsoleInputW = _kernel32.lookupFunction<_ReadConsoleInputNative, _ReadConsoleInputDart>( 'ReadConsoleInputW'); + +final _waitForSingleObject = _kernel32.lookupFunction< + _WaitForSingleObjectNative, + _WaitForSingleObjectDart>('WaitForSingleObject'); diff --git a/lib/src/utils/nocterm_paths.dart b/lib/src/utils/nocterm_paths.dart index f87e7594..1d224975 100644 --- a/lib/src/utils/nocterm_paths.dart +++ b/lib/src/utils/nocterm_paths.dart @@ -28,22 +28,16 @@ String getNoctermDirectory() { } String getProjectDirectory() { + final start = Directory.current.path; var parent = Directory.current; while (true) { - final newParent = parent.parent; - - if (newParent == parent) { - throw StateError('Could not determine project directory'); - } - final pubspec = File(p.join(parent.path, 'pubspec.yaml')); - if (pubspec.existsSync()) { - return parent.path; - } - if (newParent.path == '/') { - return '/'; - } + if (pubspec.existsSync()) return parent.path; + final newParent = parent.parent; + // Compare paths, not Directory identity — terminates at Windows drive + // roots where parent.parent returns the same path. + if (newParent.path == parent.path) return start; parent = newParent; } } diff --git a/test/utils/nocterm_paths_test.dart b/test/utils/nocterm_paths_test.dart new file mode 100644 index 00000000..ace8afef --- /dev/null +++ b/test/utils/nocterm_paths_test.dart @@ -0,0 +1,48 @@ +import 'dart:io'; + +import 'package:nocterm/src/utils/nocterm_paths.dart'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +void main() { + group('getProjectDirectory', () { + late Directory tempRoot; + late Directory previousCwd; + + setUp(() { + previousCwd = Directory.current; + tempRoot = Directory.systemTemp.createTempSync('nocterm_paths_test_'); + }); + + tearDown(() { + Directory.current = previousCwd; + if (tempRoot.existsSync()) tempRoot.deleteSync(recursive: true); + }); + + test('returns the closest ancestor containing pubspec.yaml', () { + final project = Directory(p.join(tempRoot.path, 'project'))..createSync(); + File(p.join(project.path, 'pubspec.yaml')).writeAsStringSync('name: x'); + final nested = Directory(p.join(project.path, 'a', 'b')) + ..createSync(recursive: true); + Directory.current = nested; + + expect(getProjectDirectory(), equals(project.path)); + }); + + test('returns cwd when no pubspec.yaml ancestor exists', () { + final dir = Directory(p.join(tempRoot.path, 'a', 'b', 'c')) + ..createSync(recursive: true); + Directory.current = dir; + + expect(getProjectDirectory(), equals(dir.path)); + }); + + test('terminates when walking past the filesystem root', () { + final dir = Directory(p.join(tempRoot.path, 'deep', 'no', 'project')) + ..createSync(recursive: true); + Directory.current = dir; + + expect(getProjectDirectory(), equals(dir.path)); + }); + }); +}