@@ -38,6 +38,7 @@ use eframe::egui::Frame;
3838use eframe:: egui:: Id ;
3939use eframe:: egui:: Layout ;
4040use eframe:: egui:: Margin ;
41+ use eframe:: egui:: PointerButton ;
4142use eframe:: egui:: Rgba ;
4243use eframe:: egui:: Style ;
4344use eframe:: egui:: TextStyle ;
@@ -57,13 +58,95 @@ use komorebi_themes::Base16Value;
5758use komorebi_themes:: Base16Wrapper ;
5859use komorebi_themes:: Catppuccin ;
5960use komorebi_themes:: CatppuccinValue ;
61+ use lazy_static:: lazy_static;
62+ use parking_lot:: Mutex ;
6063use std:: cell:: RefCell ;
6164use std:: collections:: HashMap ;
65+ use std:: io:: Error ;
66+ use std:: io:: ErrorKind ;
67+ use std:: io:: Result ;
68+ use std:: io:: Write ;
69+ use std:: os:: windows:: process:: CommandExt ;
6270use std:: path:: PathBuf ;
71+ use std:: process:: ChildStdin ;
72+ use std:: process:: Command ;
73+ use std:: process:: Stdio ;
6374use std:: rc:: Rc ;
6475use std:: sync:: atomic:: Ordering ;
6576use std:: sync:: Arc ;
6677
78+ const CREATE_NO_WINDOW : u32 = 0x0800_0000 ;
79+
80+ lazy_static ! {
81+ static ref SESSION_STDIN : Mutex <Option <ChildStdin >> = Mutex :: new( None ) ;
82+ }
83+
84+ fn start_powershell ( ) -> Result < ( ) > {
85+ // found running session, do nothing
86+ if SESSION_STDIN . lock ( ) . as_mut ( ) . is_some ( ) {
87+ tracing:: debug!( "PowerShell session already started" ) ;
88+ return Ok ( ( ) ) ;
89+ }
90+
91+ tracing:: debug!( "Starting PowerShell session" ) ;
92+
93+ let mut child = Command :: new ( "powershell.exe" )
94+ . args ( [ "-NoLogo" , "-NoProfile" , "-Command" , "-" ] )
95+ . stdin ( Stdio :: piped ( ) )
96+ . creation_flags ( CREATE_NO_WINDOW )
97+ . spawn ( ) ?;
98+
99+ let stdin = child. stdin . take ( ) . expect ( "stdin piped" ) ;
100+
101+ // Store stdin for later commands
102+ let mut session_stdin = SESSION_STDIN . lock ( ) ;
103+ * session_stdin = Option :: from ( stdin) ;
104+
105+ Ok ( ( ) )
106+ }
107+
108+ fn stop_powershell ( ) -> Result < ( ) > {
109+ tracing:: debug!( "Stopping PowerShell session" ) ;
110+
111+ if let Some ( mut session_stdin) = SESSION_STDIN . lock ( ) . take ( ) {
112+ if let Err ( e) = session_stdin. write_all ( b"exit\n " ) {
113+ tracing:: error!( error = %e, "failed to write exit command to PowerShell stdin" ) ;
114+ return Err ( e) ;
115+ }
116+ if let Err ( e) = session_stdin. flush ( ) {
117+ tracing:: error!( error = %e, "failed to flush PowerShell stdin" ) ;
118+ return Err ( e) ;
119+ }
120+
121+ tracing:: debug!( "PowerShell session stopped" ) ;
122+ } else {
123+ tracing:: debug!( "PowerShell session already stopped" ) ;
124+ }
125+
126+ Ok ( ( ) )
127+ }
128+
129+ pub fn exec_powershell ( cmd : & str ) -> Result < ( ) > {
130+ if let Some ( session_stdin) = SESSION_STDIN . lock ( ) . as_mut ( ) {
131+ if let Err ( e) = writeln ! ( session_stdin, "{}" , cmd) {
132+ tracing:: error!( error = %e, cmd = cmd, "failed to write command to PowerShell stdin" ) ;
133+ return Err ( e) ;
134+ }
135+
136+ if let Err ( e) = session_stdin. flush ( ) {
137+ tracing:: error!( error = %e, "failed to flush PowerShell stdin" ) ;
138+ return Err ( e) ;
139+ }
140+
141+ return Ok ( ( ) ) ;
142+ }
143+
144+ Err ( Error :: new (
145+ ErrorKind :: NotFound ,
146+ "PowerShell session not started" ,
147+ ) )
148+ }
149+
67150pub struct Komobar {
68151 pub hwnd : Option < isize > ,
69152 pub monitor_index : Option < usize > ,
@@ -82,6 +165,18 @@ pub struct Komobar {
82165 pub size_rect : komorebi_client:: Rect ,
83166 pub work_area_offset : komorebi_client:: Rect ,
84167 applied_theme_on_first_frame : bool ,
168+ mouse_follows_focus : bool ,
169+ input_config : InputConfig ,
170+ }
171+
172+ struct InputConfig {
173+ accumulated_scroll_delta : Vec2 ,
174+ act_on_vertical_scroll : bool ,
175+ act_on_horizontal_scroll : bool ,
176+ vertical_scroll_threshold : f32 ,
177+ horizontal_scroll_threshold : f32 ,
178+ vertical_scroll_max_threshold : f32 ,
179+ horizontal_scroll_max_threshold : f32 ,
85180}
86181
87182pub fn apply_theme (
@@ -368,15 +463,19 @@ impl Komobar {
368463 }
369464 MonitorConfigOrIndex :: Index ( idx) => ( * idx, None ) ,
370465 } ;
371- let monitor_index = self . komorebi_notification_state . as_ref ( ) . and_then ( |state| {
372- state
373- . borrow ( )
374- . monitor_usr_idx_map
375- . get ( & usr_monitor_index)
376- . copied ( )
466+
467+ let mapped_state = self . komorebi_notification_state . as_ref ( ) . map ( |state| {
468+ let state = state. borrow ( ) ;
469+ (
470+ state. monitor_usr_idx_map . get ( & usr_monitor_index) . copied ( ) ,
471+ state. mouse_follows_focus ,
472+ )
377473 } ) ;
378474
379- self . monitor_index = monitor_index;
475+ if let Some ( state) = mapped_state {
476+ self . monitor_index = state. 0 ;
477+ self . mouse_follows_focus = state. 1 ;
478+ }
380479
381480 if let Some ( monitor_index) = self . monitor_index {
382481 if let ( prev_rect, Some ( new_rect) ) = ( & self . work_area_offset , & config_work_area_offset)
@@ -435,6 +534,36 @@ impl Komobar {
435534 self . disabled = true ;
436535 }
437536
537+ if let Some ( mouse) = & self . config . mouse {
538+ self . input_config . act_on_vertical_scroll =
539+ mouse. on_scroll_up . is_some ( ) || mouse. on_scroll_down . is_some ( ) ;
540+ self . input_config . act_on_horizontal_scroll =
541+ mouse. on_scroll_left . is_some ( ) || mouse. on_scroll_right . is_some ( ) ;
542+ self . input_config . vertical_scroll_threshold = mouse
543+ . vertical_scroll_threshold
544+ . unwrap_or ( 30.0 )
545+ . clamp ( 10.0 , 300.0 ) ;
546+ self . input_config . horizontal_scroll_threshold = mouse
547+ . horizontal_scroll_threshold
548+ . unwrap_or ( 30.0 )
549+ . clamp ( 10.0 , 300.0 ) ;
550+ // limit how many "ticks" can be accumulated
551+ self . input_config . vertical_scroll_max_threshold =
552+ self . input_config . vertical_scroll_threshold * 3.0 ;
553+ self . input_config . horizontal_scroll_max_threshold =
554+ self . input_config . horizontal_scroll_threshold * 3.0 ;
555+
556+ if mouse. has_command ( ) {
557+ start_powershell ( ) . unwrap_or_else ( |_| {
558+ tracing:: error!( "failed to start powershell session" ) ;
559+ } ) ;
560+ } else {
561+ stop_powershell ( ) . unwrap_or_else ( |_| {
562+ tracing:: error!( "failed to stop powershell session" ) ;
563+ } ) ;
564+ }
565+ }
566+
438567 tracing:: info!( "widget configuration options applied" ) ;
439568
440569 self . komorebi_notification_state = komorebi_notification_state;
@@ -608,6 +737,16 @@ impl Komobar {
608737 size_rect : komorebi_client:: Rect :: default ( ) ,
609738 work_area_offset : komorebi_client:: Rect :: default ( ) ,
610739 applied_theme_on_first_frame : false ,
740+ mouse_follows_focus : false ,
741+ input_config : InputConfig {
742+ accumulated_scroll_delta : Vec2 :: new ( 0.0 , 0.0 ) ,
743+ act_on_vertical_scroll : false ,
744+ act_on_horizontal_scroll : false ,
745+ vertical_scroll_threshold : 0.0 ,
746+ horizontal_scroll_threshold : 0.0 ,
747+ vertical_scroll_max_threshold : 0.0 ,
748+ horizontal_scroll_max_threshold : 0.0 ,
749+ } ,
611750 } ;
612751
613752 komobar. apply_config ( & cc. egui_ctx , None ) ;
@@ -961,6 +1100,111 @@ impl eframe::App for Komobar {
9611100 let frame = render_config. change_frame_on_bar ( frame, & ctx. style ( ) ) ;
9621101
9631102 CentralPanel :: default ( ) . frame ( frame) . show ( ctx, |ui| {
1103+ if let Some ( mouse_config) = & self . config . mouse {
1104+ let command = if ui
1105+ . input ( |i| i. pointer . button_double_clicked ( PointerButton :: Primary ) )
1106+ {
1107+ tracing:: debug!( "Input: primary button double clicked" ) ;
1108+ & mouse_config. on_primary_double_click
1109+ } else if ui. input ( |i| i. pointer . button_clicked ( PointerButton :: Secondary ) ) {
1110+ tracing:: debug!( "Input: secondary button clicked" ) ;
1111+ & mouse_config. on_secondary_click
1112+ } else if ui. input ( |i| i. pointer . button_clicked ( PointerButton :: Middle ) ) {
1113+ tracing:: debug!( "Input: middle button clicked" ) ;
1114+ & mouse_config. on_middle_click
1115+ } else if ui. input ( |i| i. pointer . button_clicked ( PointerButton :: Extra1 ) ) {
1116+ tracing:: debug!( "Input: extra1 button clicked" ) ;
1117+ & mouse_config. on_extra1_click
1118+ } else if ui. input ( |i| i. pointer . button_clicked ( PointerButton :: Extra2 ) ) {
1119+ tracing:: debug!( "Input: extra2 button clicked" ) ;
1120+ & mouse_config. on_extra2_click
1121+ } else if self . input_config . act_on_vertical_scroll
1122+ || self . input_config . act_on_horizontal_scroll
1123+ {
1124+ let scroll_delta = ui. input ( |input| input. smooth_scroll_delta ) ;
1125+
1126+ self . input_config . accumulated_scroll_delta += scroll_delta;
1127+
1128+ if scroll_delta. y != 0.0 && self . input_config . act_on_vertical_scroll {
1129+ // Do not store more than the max threshold
1130+ self . input_config . accumulated_scroll_delta . y =
1131+ self . input_config . accumulated_scroll_delta . y . clamp (
1132+ -self . input_config . vertical_scroll_max_threshold ,
1133+ self . input_config . vertical_scroll_max_threshold ,
1134+ ) ;
1135+
1136+ // When the accumulated scroll passes the threshold, trigger a tick.
1137+ if self . input_config . accumulated_scroll_delta . y . abs ( )
1138+ >= self . input_config . vertical_scroll_threshold
1139+ {
1140+ let direction_command =
1141+ if self . input_config . accumulated_scroll_delta . y > 0.0 {
1142+ & mouse_config. on_scroll_up
1143+ } else {
1144+ & mouse_config. on_scroll_down
1145+ } ;
1146+
1147+ // Remove one tick's worth of scroll from the accumulator, preserving any excess.
1148+ self . input_config . accumulated_scroll_delta . y -=
1149+ self . input_config . vertical_scroll_threshold
1150+ * self . input_config . accumulated_scroll_delta . y . signum ( ) ;
1151+
1152+ tracing:: debug!(
1153+ "Input: vertical scroll ticked. excess: {} | threshold: {}" ,
1154+ self . input_config. accumulated_scroll_delta. y,
1155+ self . input_config. vertical_scroll_threshold
1156+ ) ;
1157+
1158+ direction_command
1159+ } else {
1160+ & None
1161+ }
1162+ } else if scroll_delta. x != 0.0 && self . input_config . act_on_horizontal_scroll {
1163+ // Do not store more than the max threshold
1164+ self . input_config . accumulated_scroll_delta . x =
1165+ self . input_config . accumulated_scroll_delta . x . clamp (
1166+ -self . input_config . horizontal_scroll_max_threshold ,
1167+ self . input_config . horizontal_scroll_max_threshold ,
1168+ ) ;
1169+
1170+ // When the accumulated scroll passes the threshold, trigger a tick.
1171+ if self . input_config . accumulated_scroll_delta . x . abs ( )
1172+ >= self . input_config . horizontal_scroll_threshold
1173+ {
1174+ let direction_command =
1175+ if self . input_config . accumulated_scroll_delta . x > 0.0 {
1176+ & mouse_config. on_scroll_left
1177+ } else {
1178+ & mouse_config. on_scroll_right
1179+ } ;
1180+
1181+ // Remove one tick's worth of scroll from the accumulator, preserving any excess.
1182+ self . input_config . accumulated_scroll_delta . x -=
1183+ self . input_config . horizontal_scroll_threshold
1184+ * self . input_config . accumulated_scroll_delta . x . signum ( ) ;
1185+
1186+ tracing:: debug!(
1187+ "Input: horizontal scroll ticked. excess: {} | threshold: {}" ,
1188+ self . input_config. accumulated_scroll_delta. x,
1189+ self . input_config. horizontal_scroll_threshold
1190+ ) ;
1191+
1192+ direction_command
1193+ } else {
1194+ & None
1195+ }
1196+ } else {
1197+ & None
1198+ }
1199+ } else {
1200+ & None
1201+ } ;
1202+
1203+ if let Some ( command) = command {
1204+ command. execute ( self . mouse_follows_focus ) ;
1205+ }
1206+ }
1207+
9641208 // Apply grouping logic for the bar as a whole
9651209 let area_frame = if let Some ( frame) = & self . config . frame {
9661210 Frame :: NONE
0 commit comments