Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 90 additions & 9 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ use crate::compositor::LayoutEvent;
use crate::domain::CaptureState;
use crate::infrastructure::audio::SoundPackLoader;
use crate::input::{
InputControlCommand, KeyEvent, KeyListener, LayoutManager, ListenerConfig, ListenerHandle,
InputControlCommand, KeyEvent, KeyListener, LayoutManager, ListenerConfig, ListenerHandle, MouseEvent,
};
use crate::tray::{TrayAction, TrayHandle};
use crate::ui::{
create_bubble_window, create_launcher_window, create_settings_window, create_window,
setup_drag, show_launcher, show_settings, BubbleDisplayWidget, DisplayMode, KeyDisplayWidget,
create_bubble_window, create_launcher_window, create_mouse_cursor_window, create_settings_window, create_window,
setup_drag, show_launcher, show_settings, BubbleDisplayWidget, DisplayMode, KeyDisplayWidget, MouseCursorWidget,
};
use anyhow::Result;
use async_channel::{bounded, Receiver};
Expand Down Expand Up @@ -40,6 +40,7 @@ struct RuntimeState {
mode: Option<DisplayMode>,
routing_engine: RoutingEngine,
keystroke_window: Option<ApplicationWindow>,
mouse_cursor_window: Option<ApplicationWindow>,
bubble_window: Option<ApplicationWindow>,
launcher_window: Option<ApplicationWindow>,
settings_window: Option<ApplicationWindow>,
Expand Down Expand Up @@ -271,6 +272,9 @@ fn close_mode_windows(state: &Rc<RefCell<RuntimeState>>) {
if let Some(window) = s.keystroke_window.take() {
window.close();
}
if let Some(window) = s.mouse_cursor_window.take() {
window.close();
}
if let Some(window) = s.bubble_window.take() {
window.close();
}
Expand Down Expand Up @@ -473,6 +477,7 @@ fn start_keystroke_mode(
}

let window = create_window(app, &config)?;
let mouse_window = create_mouse_cursor_window(app, &config)?;

if config.keystroke_draggable {
let controllers = setup_drag(&window);
Expand All @@ -484,17 +489,21 @@ fn start_keystroke_mode(
config.display_timeout_ms,
)));

let mouse_display = Rc::new(RefCell::new(MouseCursorWidget::new()));

window.set_child(Some(display.borrow().widget()));
mouse_window.set_child(Some(mouse_display.borrow().widget()));

let (sender, receiver) = bounded::<KeyEvent>(1024);
let (key_sender, key_receiver) = bounded::<KeyEvent>(1024);
let (mouse_sender, mouse_receiver) = bounded::<MouseEvent>(256);

let listener_config = ListenerConfig {
all_keyboards: config.all_keyboards,
..Default::default()
};

let audio_dispatcher = state.borrow().audio_dispatcher.clone();
let listener = KeyListener::new(sender, listener_config, audio_dispatcher);
let listener = KeyListener::new(key_sender, mouse_sender, listener_config, audio_dispatcher);
let handle = listener.start()?;

let mode_active = Arc::new(AtomicBool::new(true));
Expand All @@ -503,6 +512,7 @@ fn start_keystroke_mode(
s.mode_active = Some(Arc::clone(&mode_active));
s.listener_handle = Some(handle);
s.keystroke_window = Some(window.clone());
s.mouse_cursor_window = Some(mouse_window.clone());
}

let active_key_loop = Arc::clone(&mode_active);
Expand All @@ -513,7 +523,7 @@ fn start_keystroke_mode(
let config_service_clone = config_service.clone();

glib::MainContext::default().spawn_local(async move {
while let Ok(event) = receiver.recv().await {
while let Ok(event) = key_receiver.recv().await {
if !active_key_loop.load(Ordering::SeqCst) {
break;
}
Expand Down Expand Up @@ -564,6 +574,35 @@ fn start_keystroke_mode(
debug!("Keystroke event loop terminated");
});

let active = Arc::clone(&mode_active);
let state_clone = Rc::clone(state);
let mouse_display_clone = Rc::clone(&mouse_display);

glib::MainContext::default().spawn_local(async move {
while let Ok(event) = mouse_receiver.recv().await {
if !active.load(Ordering::SeqCst) {
break;
}

let (capture, _) = state_clone.borrow().routing_engine.get_states();
if capture == CaptureState::Paused {
continue;
}

match event {
MouseEvent::Moved { .. } => {
// Cursor position tracking not possible on Wayland
// Mouse bubble window stays anchored near keystroke display
}
_ => {
let mut display = mouse_display_clone.borrow_mut();
display.handle_event(&event);
}
}
}
debug!("Mouse event loop terminated");
});

let mut rx = config_service.subscribe();
let display_c = display.clone();
let window_c = window.clone();
Expand Down Expand Up @@ -638,7 +677,12 @@ fn start_keystroke_mode(
let state_clone = Rc::clone(state);
setup_keystroke_cleanup_timer(display.clone(), window.clone(), state_clone, active);

let active = Arc::clone(&mode_active);
let state_clone = Rc::clone(state);
setup_mouse_cleanup_timer(mouse_display.clone(), mouse_window.clone(), state_clone, active);

window.present();
mouse_window.present();

Ok(())
}
Expand Down Expand Up @@ -724,15 +768,16 @@ fn start_bubble_mode(
});
}

let (sender, receiver) = bounded::<KeyEvent>(1024);
let (key_sender, key_receiver) = bounded::<KeyEvent>(1024);
let (mouse_sender, _mouse_receiver) = bounded::<MouseEvent>(256);

let listener_config = ListenerConfig {
all_keyboards: config.all_keyboards,
..Default::default()
};

let audio_dispatcher = state.borrow().audio_dispatcher.clone();
let listener = KeyListener::new(sender, listener_config, audio_dispatcher);
let listener = KeyListener::new(key_sender, mouse_sender, listener_config, audio_dispatcher);
let handle = listener.start()?;

{
Expand Down Expand Up @@ -763,7 +808,7 @@ fn start_bubble_mode(
let app = app_clone.clone();
let config_service = config_service_clone.clone();

while let Ok(event) = receiver.recv().await {
while let Ok(event) = key_receiver.recv().await {
if !active.load(Ordering::SeqCst) {
break;
}
Expand Down Expand Up @@ -975,6 +1020,42 @@ fn setup_keystroke_cleanup_timer(
});
}

fn setup_mouse_cleanup_timer(
display: Rc<RefCell<MouseCursorWidget>>,
window: ApplicationWindow,
state: Rc<RefCell<RuntimeState>>,
mode_active: Arc<AtomicBool>,
) {
glib::timeout_add_local(Duration::from_millis(100), move || {
if !mode_active.load(Ordering::SeqCst) {
return ControlFlow::Break;
}

if state.borrow().routing_engine.get_states().0 == CaptureState::Paused {
return ControlFlow::Continue;
}

let mut display = display.borrow_mut();
display.remove_expired();

if display.has_buttons() {
window.remove_css_class("fading-out");
window.set_visible(true);
} else if !window.has_css_class("fading-out") {
window.add_css_class("fading-out");

let w = window.clone();
glib::timeout_add_local_once(Duration::from_millis(200), move || {
if w.has_css_class("fading-out") {
w.set_visible(false);
}
});
}

ControlFlow::Continue
});
}

fn setup_bubble_cleanup_timer(
display: Rc<RefCell<BubbleDisplayWidget>>,
window: ApplicationWindow,
Expand Down
18 changes: 18 additions & 0 deletions src/domain/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,24 @@ impl Position {
],
}
}

/// Returns which vertical edge is anchored and whether offset should be positive or negative
/// to move the window UP (for positioning mouse bubbles above keystroke display)
#[must_use]
pub fn vertical_offset_direction(self) -> Option<(Edge, i32)> {
match self {
// Top positions: anchored to TOP, subtract to move UP
Position::TopLeft | Position::TopCenter | Position::TopRight => {
Some((Edge::Top, -1))
}
// Bottom positions: anchored to BOTTOM, add to move UP
Position::BottomLeft | Position::BottomCenter | Position::BottomRight => {
Some((Edge::Bottom, 1))
}
// Center positions: not vertically anchored, can't easily offset
_ => None,
}
}
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
Expand Down
72 changes: 72 additions & 0 deletions src/input/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@ impl KeyboardDevice {
}
}

#[derive(Debug, Clone)]
pub struct MouseDevice {
pub path: PathBuf,

pub name: String,
}

impl MouseDevice {
pub fn open(&self) -> Result<Device> {
Device::open(&self.path).with_context(|| format!("Failed to open device: {:?}", self.path))
}
}

pub fn discover_keyboards() -> Result<Vec<KeyboardDevice>> {
let mut keyboards = Vec::new();
let input_dir = PathBuf::from("/dev/input");
Expand Down Expand Up @@ -58,6 +71,47 @@ pub fn discover_keyboards() -> Result<Vec<KeyboardDevice>> {
Ok(keyboards)
}

pub fn discover_mice() -> Result<Vec<MouseDevice>> {
let mut mice = Vec::new();
let input_dir = PathBuf::from("/dev/input");

let entries = fs::read_dir(&input_dir)
.with_context(|| format!("Failed to read directory: {:?}", input_dir))?;

for entry in entries.flatten() {
let path = entry.path();
let file_name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or_default();

if !file_name.starts_with("event") {
continue;
}

match Device::open(&path) {
Ok(device) => {
if is_mouse(&device) {
let name = device.name().unwrap_or("Unknown Mouse").to_string();

info!("Found mouse: {} at {:?}", name, path);

mice.push(MouseDevice { path, name });
}
}
Err(e) => {
debug!("Could not open {:?}: {}", path, e);
}
}
}

if mice.is_empty() {
warn!("No mouse devices found. Ensure you are in the 'input' group.");
}

Ok(mice)
}

fn is_keyboard(device: &Device) -> bool {
let supported = device.supported_events();
if !supported.contains(evdev::EventType::KEY) {
Expand All @@ -75,6 +129,24 @@ fn is_keyboard(device: &Device) -> bool {
false
}

fn is_mouse(device: &Device) -> bool {
let supported = device.supported_events();

if !supported.contains(evdev::EventType::KEY) {
return false;
}

let has_mouse_buttons = device.supported_keys().map_or(false, |keys| {
keys.contains(evdev::Key::BTN_LEFT)
|| keys.contains(evdev::Key::BTN_RIGHT)
|| keys.contains(evdev::Key::BTN_MIDDLE)
});

let has_rel_events = supported.contains(evdev::EventType::RELATIVE);

has_mouse_buttons && has_rel_events
}

fn is_virtual(name: &str) -> bool {
name.as_bytes()
.windows(7)
Expand Down
Loading