Skip to content

Commit 3d8f68e

Browse files
CtByteLGUG2Z
authored andcommitted
feat(bar): send commands by mouse/touchpad/screen
This commit makes it possible to send commands from the bar by using the mouse/touchpad/touchscreen. Komorebi or custom commands can be sent by clicking on the mouse's primary, secondary, middle, back or forward buttons. As the primary single click is already used by widgets, only primary double clicks can send commands. This limitation is due to Egui also triggering 2 single clicks before a double click is triggered. Egui does not have an implementation for stopping event propagation out of the box and would be too much work to include. Similarly, commands can be sent on every "tick" of mouse scrolling, touchpad or touchscreen swiping in any of the 4 directions. This "tick" can be adjusted to fit user's preference. This is due to the fact, that Egui does not have an event for when a mouse "tick" occurs. It instead gives a number of points that the user scrolled/swiped on each frame. PR: #1403
1 parent 80bb728 commit 3d8f68e

File tree

9 files changed

+55050
-11
lines changed

9 files changed

+55050
-11
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ strum = { version = "0.27", features = ["derive"] }
3333
tracing = "0.1"
3434
tracing-appender = "0.2"
3535
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
36+
parking_lot = "0.12"
3637
paste = "1"
3738
sysinfo = "0.34"
3839
uds_windows = "1"

komorebi-bar/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ netdev = "0.34"
2626
num = "0.4"
2727
num-derive = "0.4"
2828
num-traits = "0.2"
29+
parking_lot = { workspace = true }
2930
random_word = { version = "0.5", features = ["en"] }
3031
reqwest = { version = "0.12", features = ["blocking"] }
3132
schemars = { workspace = true, optional = true }

komorebi-bar/src/bar.rs

Lines changed: 251 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ use eframe::egui::Frame;
3838
use eframe::egui::Id;
3939
use eframe::egui::Layout;
4040
use eframe::egui::Margin;
41+
use eframe::egui::PointerButton;
4142
use eframe::egui::Rgba;
4243
use eframe::egui::Style;
4344
use eframe::egui::TextStyle;
@@ -57,13 +58,95 @@ use komorebi_themes::Base16Value;
5758
use komorebi_themes::Base16Wrapper;
5859
use komorebi_themes::Catppuccin;
5960
use komorebi_themes::CatppuccinValue;
61+
use lazy_static::lazy_static;
62+
use parking_lot::Mutex;
6063
use std::cell::RefCell;
6164
use 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;
6270
use std::path::PathBuf;
71+
use std::process::ChildStdin;
72+
use std::process::Command;
73+
use std::process::Stdio;
6374
use std::rc::Rc;
6475
use std::sync::atomic::Ordering;
6576
use 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+
67150
pub 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

87182
pub 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

Comments
 (0)