@@ -56,6 +56,13 @@ public class ConsoleWindowSystem
5656 // Managed thread id of the main-loop (UI) thread; set at Run() start. -1 until then.
5757 private int _uiThreadId = - 1 ;
5858
59+ // Software cursor: opt-in caret drawn by the app (blinking) with hardware cursor hidden.
60+ // Resolved once at startup from the driver's options.
61+ private bool _useSoftwareCursor ;
62+ private int _cursorBlinkRateMs = 500 ;
63+ private readonly Core . CursorBlinkClock _blinkClock = new ( ) ;
64+ private System . Drawing . Point _lastSoftwareCursorPos = new ( - 1 , - 1 ) ;
65+
5966 // Signal to wake the main loop when input or UI actions arrive
6067 private readonly ManualResetEventSlim _wakeSignal = new ( false ) ;
6168
@@ -407,6 +414,14 @@ public ConsoleWindowSystem(IConsoleDriver driver, ITheme theme, PluginConfigurat
407414
408415 // Initialize panels from config or legacy StatusBarOptions
409416 _panelStateService . InitializePanels ( _options ) ;
417+
418+ // Resolve software-cursor settings once from the driver's options (if it exposes them).
419+ // Headless/other drivers leave the defaults (false / 500ms).
420+ if ( _consoleDriver is Drivers . NetConsoleDriver netDriver )
421+ {
422+ _useSoftwareCursor = netDriver . Options . UseSoftwareCursor ;
423+ _cursorBlinkRateMs = netDriver . Options . CursorBlinkRate ;
424+ }
410425 }
411426
412427 #endregion
@@ -460,6 +475,10 @@ public IConsoleDriver ConsoleDriver
460475 /// </summary>
461476 internal CursorStateService CursorStateService => _cursorStateService ;
462477
478+ /// <summary>True when a software caret should currently be drawn.</summary>
479+ internal bool SoftwareCursorOn =>
480+ _useSoftwareCursor && _cursorStateService . CurrentState . IsVisible && _blinkClock . IsOn ( _cursorBlinkRateMs ) ;
481+
463482 /// <summary>
464483 /// Gets the window state service for managing window lifecycle and state.
465484 /// </summary>
@@ -920,6 +939,8 @@ public int Run()
920939 var now = DateTime . UtcNow ;
921940 var elapsed = ( now - _lastRenderTime ) . TotalMilliseconds ;
922941
942+ if ( _useSoftwareCursor ) _blinkClock . Advance ( elapsed ) ;
943+
923944 // Track performance metrics on EVERY iteration (independent of rendering)
924945 bool metricsNeedUpdate = false ;
925946 if ( Performance . IsPerformanceMetricsEnabled )
@@ -943,7 +964,8 @@ public int Run()
943964 }
944965
945966 // Frame pacing: render if windows are dirty OR metrics need update OR desktop needs render OR animations active
946- bool shouldRender = AnyWindowDirty ( ) || metricsNeedUpdate || Render . DesktopNeedsRender || Animations . HasActiveAnimations || Parsing . MarkupSpinnerClock . ShouldKeepRendering ( Animations . IsEnabled ) || Render . IsStatusBarDirty ( ) || _desktopPortalService . AnyPortalDirty ( ) ;
967+ bool shouldRender = AnyWindowDirty ( ) || metricsNeedUpdate || Render . DesktopNeedsRender || Animations . HasActiveAnimations || Parsing . MarkupSpinnerClock . ShouldKeepRendering ( Animations . IsEnabled ) || Render . IsStatusBarDirty ( ) || _desktopPortalService . AnyPortalDirty ( )
968+ || ( _useSoftwareCursor && _cursorStateService . CurrentState . IsVisible ) ;
947969
948970 // Calculate recommended sleep duration once (used in both branches)
949971 var recommendedSleep = _inputStateService . GetRecommendedSleepDuration (
@@ -986,6 +1008,9 @@ public int Run()
9861008 UpdateCursor ( ) ;
9871009 if ( _uiActionsPending && _idleTime > Configuration . SystemDefaults . MinSleepDurationMs )
9881010 _idleTime = Configuration . SystemDefaults . MinSleepDurationMs ;
1011+ // Keep the loop ticking fast enough for smooth caret blinking.
1012+ if ( _useSoftwareCursor && _cursorStateService . CurrentState . IsVisible )
1013+ _idleTime = Math . Min ( _idleTime , _cursorBlinkRateMs ) ;
9891014 _currentPhase = Core . MainLoopPhase . Idle ;
9901015 try
9911016 {
@@ -1335,6 +1360,18 @@ private void UpdateCursor()
13351360 _cursorStateService . HideCursor ( ) ;
13361361 }
13371362
1363+ // Software cursor: reset the blink clock whenever the caret moves so the new
1364+ // position starts in the "on" phase. CurrentState holds the real (visible) caret.
1365+ if ( _useSoftwareCursor )
1366+ {
1367+ var caretPos = _cursorStateService . CurrentState . AbsolutePosition ;
1368+ if ( caretPos != _lastSoftwareCursorPos )
1369+ {
1370+ _blinkClock . Reset ( ) ;
1371+ _lastSoftwareCursorPos = caretPos ;
1372+ }
1373+ }
1374+
13381375 // Apply cursor state to the actual console
13391376 // CRITICAL: Protect Console I/O with lock to prevent concurrent writes
13401377 // from corrupting ANSI sequences during InputLoop's mouse parsing
@@ -1343,6 +1380,15 @@ private void UpdateCursor()
13431380 _cursorStateService . ApplyCursorToConsole (
13441381 _consoleDriver . ScreenSize . Width ,
13451382 _consoleDriver . ScreenSize . Height ) ;
1383+
1384+ // Software cursor: keep CurrentState as the real (visible) caret for the overlay,
1385+ // but force the HARDWARE cursor hidden so only the app-drawn caret is seen.
1386+ // Only emit when the caret is logically visible (avoids churn; the overlay
1387+ // redraws every frame while focused — see the keep-alive in the main loop).
1388+ if ( _useSoftwareCursor && _cursorStateService . CurrentState . IsVisible )
1389+ {
1390+ _consoleDriver . SetCursorVisible ( false ) ;
1391+ }
13461392 }
13471393 }
13481394
0 commit comments