From 7b5f77a264a93971b9a8172b9e87749719461e6f Mon Sep 17 00:00:00 2001 From: Preston Date: Thu, 21 May 2026 09:54:14 -0500 Subject: [PATCH 1/5] fix: terminate getProjectDirectory walk at Windows drive roots --- lib/src/utils/nocterm_paths.dart | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/lib/src/utils/nocterm_paths.dart b/lib/src/utils/nocterm_paths.dart index f87e7594..504920e7 100644 --- a/lib/src/utils/nocterm_paths.dart +++ b/lib/src/utils/nocterm_paths.dart @@ -30,20 +30,15 @@ String getNoctermDirectory() { String getProjectDirectory() { 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 Directory.current.path; + } parent = newParent; } } From 2939eed9ea6f4f8185ee366ad531f2811ecaa561 Mon Sep 17 00:00:00 2001 From: Preston Date: Thu, 21 May 2026 09:54:54 -0500 Subject: [PATCH 2/5] fix(windows): cap input loop wait so timers and signals fire --- lib/src/backend/win32_ansi_stdin.dart | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/lib/src/backend/win32_ansi_stdin.dart b/lib/src/backend/win32_ansi_stdin.dart index 7757d320..77e2a34b 100644 --- a/lib/src/backend/win32_ansi_stdin.dart +++ b/lib/src/backend/win32_ansi_stdin.dart @@ -63,12 +63,16 @@ class Win32AnsiStdin extends Stream> implements Stdin { 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) { @@ -381,6 +385,12 @@ 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 +504,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 +525,7 @@ final _setConsoleMode = final _readConsoleInputW = _kernel32.lookupFunction<_ReadConsoleInputNative, _ReadConsoleInputDart>( 'ReadConsoleInputW'); + +final _waitForSingleObject = _kernel32.lookupFunction< + _WaitForSingleObjectNative, + _WaitForSingleObjectDart>('WaitForSingleObject'); From 58f9ec59e6a433f13d165a4a497935c843f0dccc Mon Sep 17 00:00:00 2001 From: Preston Date: Thu, 21 May 2026 09:56:30 -0500 Subject: [PATCH 3/5] fix(windows): restore ENABLE_PROCESSED_INPUT so Ctrl+C generates SIGINT --- lib/src/backend/win32_ansi_stdin.dart | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/src/backend/win32_ansi_stdin.dart b/lib/src/backend/win32_ansi_stdin.dart index 77e2a34b..607536a5 100644 --- a/lib/src/backend/win32_ansi_stdin.dart +++ b/lib/src/backend/win32_ansi_stdin.dart @@ -54,9 +54,23 @@ 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(); @@ -381,6 +395,7 @@ 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; From 3edd20e602cfdd28865d31be3fb43670dd8264a9 Mon Sep 17 00:00:00 2001 From: Preston Date: Thu, 21 May 2026 10:12:19 -0500 Subject: [PATCH 4/5] test: add coverage for getProjectDirectory --- test/utils/nocterm_paths_test.dart | 48 ++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 test/utils/nocterm_paths_test.dart diff --git a/test/utils/nocterm_paths_test.dart b/test/utils/nocterm_paths_test.dart new file mode 100644 index 00000000..12c04131 --- /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(), returnsNormally); + }); + }); +} From c9ef776f9e848a5df86dfc24c48e0fe9bf088c8f Mon Sep 17 00:00:00 2001 From: Preston Date: Thu, 21 May 2026 10:40:11 -0500 Subject: [PATCH 5/5] Addressed copilot suggestions. --- lib/src/utils/nocterm_paths.dart | 5 ++--- test/utils/nocterm_paths_test.dart | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/src/utils/nocterm_paths.dart b/lib/src/utils/nocterm_paths.dart index 504920e7..1d224975 100644 --- a/lib/src/utils/nocterm_paths.dart +++ b/lib/src/utils/nocterm_paths.dart @@ -28,6 +28,7 @@ String getNoctermDirectory() { } String getProjectDirectory() { + final start = Directory.current.path; var parent = Directory.current; while (true) { final pubspec = File(p.join(parent.path, 'pubspec.yaml')); @@ -36,9 +37,7 @@ String getProjectDirectory() { 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 Directory.current.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 index 12c04131..ace8afef 100644 --- a/test/utils/nocterm_paths_test.dart +++ b/test/utils/nocterm_paths_test.dart @@ -42,7 +42,7 @@ void main() { ..createSync(recursive: true); Directory.current = dir; - expect(() => getProjectDirectory(), returnsNormally); + expect(getProjectDirectory(), equals(dir.path)); }); }); }