@@ -15,14 +15,21 @@ public sealed class DesktopCommandExecutor
1515
1616 private readonly IDesktopInputAdapter adapter ;
1717 private readonly ICursorOverlayNotifier ? cursorOverlay ;
18+ private readonly IModifierKeyOverlayNotifier ? modifierOverlay ;
1819 private readonly Func < double > now ;
1920 private readonly Dictionary < string , TextInputStreamState > textInputStreams = new ( StringComparer . Ordinal ) ;
21+ private readonly HashSet < string > activeModifierKeys = new ( StringComparer . Ordinal ) ;
2022 private string ? activeDragButton ;
2123
22- public DesktopCommandExecutor ( IDesktopInputAdapter adapter , ICursorOverlayNotifier ? cursorOverlay = null , Func < double > ? now = null )
24+ public DesktopCommandExecutor (
25+ IDesktopInputAdapter adapter ,
26+ ICursorOverlayNotifier ? cursorOverlay = null ,
27+ Func < double > ? now = null ,
28+ IModifierKeyOverlayNotifier ? modifierOverlay = null )
2329 {
2430 this . adapter = adapter ;
2531 this . cursorOverlay = cursorOverlay ;
32+ this . modifierOverlay = modifierOverlay ;
2633 this . now = now ?? ( ( ) => DateTimeOffset . UtcNow . ToUnixTimeMilliseconds ( ) ) ;
2734 }
2835
@@ -52,6 +59,8 @@ public async Task<CommandExecutionResult> ExecuteAsync(JsonElement command, Canc
5259 "mouse.rightClick" => await RightClickMouseAsync ( cancellationToken ) ,
5360 "mouse.scroll" => await ScrollMouseAsync ( payload , cancellationToken ) ,
5461 "keyboard.key" => await PressKeyAsync ( payload . GetProperty ( "key" ) . GetString ( ) ?? "" , cancellationToken ) ,
62+ "keyboard.modifierDown" => await SetModifierAsync ( payload . GetProperty ( "key" ) . GetString ( ) ?? "" , down : true , cancellationToken ) ,
63+ "keyboard.modifierUp" => await SetModifierAsync ( payload . GetProperty ( "key" ) . GetString ( ) ?? "" , down : false , cancellationToken ) ,
5564 "keyboard.shortcut" => await PressShortcutAsync ( payload . GetProperty ( "keys" ) , cancellationToken ) ,
5665 "keyboard.typeText" => await TypeTextAsync ( payload . GetProperty ( "text" ) . GetString ( ) ?? "" , cancellationToken ) ,
5766 "keyboard.textStream.open" => OpenTextInputStream ( command . GetProperty ( "deviceId" ) . GetString ( ) ?? "" , payload . GetProperty ( "streamId" ) . GetString ( ) ?? "" ) ,
@@ -79,18 +88,21 @@ public async Task<CommandExecutionResult> ExecuteAsync(JsonElement command, Canc
7988
8089 public async Task ReleaseHeldMouseButtonsAsync ( CancellationToken cancellationToken = default )
8190 {
82- if ( activeDragButton is null ) return ;
83- string button = activeDragButton ;
84- await adapter . SetMouseButtonDownAsync ( button , false , cancellationToken ) ;
85- activeDragButton = null ;
86- cursorOverlay ? . SetDragActive ( false ) ;
87- cursorOverlay ? . Hide ( ) ;
91+ await ReleaseHeldInputsAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
92+ }
93+
94+ public async Task ReleaseHeldInputsAsync ( CancellationToken cancellationToken = default )
95+ {
96+ await ReleaseHeldMouseButtonAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
97+ await ReleaseHeldModifiersAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
8898 }
8999
90100 public void EndControlSession ( )
91101 {
92102 activeDragButton = null ;
103+ activeModifierKeys . Clear ( ) ;
93104 cursorOverlay ? . EndControlSession ( ) ;
105+ modifierOverlay ? . EndControlSession ( ) ;
94106 }
95107
96108 private async Task < CommandExecutionResult > MoveMouseAsync ( JsonElement payload , CancellationToken cancellationToken )
@@ -171,6 +183,37 @@ private async Task<CommandExecutionResult> PressKeyAsync(string key, Cancellatio
171183 return CommandExecutionResult . Success ;
172184 }
173185
186+ private async Task < CommandExecutionResult > SetModifierAsync ( string key , bool down , CancellationToken cancellationToken )
187+ {
188+ if ( down )
189+ {
190+ if ( activeModifierKeys . Contains ( key ) )
191+ {
192+ return CommandExecutionResult . Success ;
193+ }
194+
195+ await adapter . SetKeyDownAsync ( key , true , cancellationToken ) ;
196+ activeModifierKeys . Add ( key ) ;
197+ UpdateModifierOverlay ( ) ;
198+ return CommandExecutionResult . Success ;
199+ }
200+
201+ if ( ! activeModifierKeys . Remove ( key ) )
202+ {
203+ return CommandExecutionResult . Success ;
204+ }
205+
206+ try
207+ {
208+ await adapter . SetKeyDownAsync ( key , false , cancellationToken ) ;
209+ return CommandExecutionResult . Success ;
210+ }
211+ finally
212+ {
213+ UpdateModifierOverlay ( ) ;
214+ }
215+ }
216+
174217 private async Task < CommandExecutionResult > PressShortcutAsync ( JsonElement keysElement , CancellationToken cancellationToken )
175218 {
176219 string [ ] keys = keysElement . EnumerateArray ( ) . Select ( key => key . GetString ( ) ?? "" ) . ToArray ( ) ;
@@ -310,6 +353,56 @@ private static bool IsMouseCommand(string type)
310353 return type is "mouse.move" or "mouse.click" or "mouse.doubleClick" or "mouse.rightClick" or "mouse.scroll" or "mouse.dragStart" or "mouse.dragEnd" ;
311354 }
312355
356+ private async Task ReleaseHeldMouseButtonAsync ( CancellationToken cancellationToken )
357+ {
358+ if ( activeDragButton is null ) return ;
359+ string button = activeDragButton ;
360+ activeDragButton = null ;
361+ await adapter . SetMouseButtonDownAsync ( button , false , cancellationToken ) ;
362+ cursorOverlay ? . SetDragActive ( false ) ;
363+ cursorOverlay ? . Hide ( ) ;
364+ }
365+
366+ private async Task ReleaseHeldModifiersAsync ( CancellationToken cancellationToken )
367+ {
368+ foreach ( string key in new [ ] { "Meta" , "Shift" , "Alt" , "Ctrl" } )
369+ {
370+ if ( ! activeModifierKeys . Remove ( key ) ) continue ;
371+ try
372+ {
373+ await adapter . SetKeyDownAsync ( key , false , cancellationToken ) ;
374+ }
375+ finally
376+ {
377+ UpdateModifierOverlay ( ) ;
378+ }
379+ }
380+ }
381+
382+ private void UpdateModifierOverlay ( )
383+ {
384+ modifierOverlay ? . SetActiveModifiers ( ActiveModifierLabels ( ) ) ;
385+ }
386+
387+ private IReadOnlyList < string > ActiveModifierLabels ( )
388+ {
389+ List < string > labels = [ ] ;
390+ foreach ( string key in new [ ] { "Ctrl" , "Alt" , "Shift" , "Meta" } )
391+ {
392+ if ( activeModifierKeys . Contains ( key ) )
393+ {
394+ labels . Add ( DisplayModifierLabel ( key ) ) ;
395+ }
396+ }
397+
398+ return labels ;
399+ }
400+
401+ private static string DisplayModifierLabel ( string key )
402+ {
403+ return key == "Meta" ? "Start" : key ;
404+ }
405+
313406 private static string TextInputStreamKey ( string deviceId , string streamId )
314407 {
315408 return $ "{ deviceId } :{ streamId } ";
0 commit comments