From e0cfd7a2586bade269e4b6e9111b45cbede665a0 Mon Sep 17 00:00:00 2001 From: Paul Jobson Date: Fri, 5 Jun 2026 22:40:58 -0400 Subject: [PATCH 1/2] Added: Start Delay, Repeat Count, Manual Start (for use with Start Delay), and multiple key presses. --- src/main.rs | 307 ++++++++++++++++++++++++++++--- src/primitives.rs | 95 +++++++++- src/xkeyclicker.ui | 444 ++++++++++++++++++++++++++++++++++----------- 3 files changed, 718 insertions(+), 128 deletions(-) diff --git a/src/main.rs b/src/main.rs index f057942..5fd4fbb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,11 +11,13 @@ use std::{ use gtk::{ gio::ApplicationFlags, - prelude::{ApplicationExt, ApplicationExtManual, BuilderExtManual}, - traits::{ButtonExt, EntryExt, GtkWindowExt, WidgetExt}, - Application, ApplicationWindow, Builder, Button, EditableSignals, Entry, + glib::Type, + prelude::{ApplicationExt, ApplicationExtManual, BuilderExtManual, TreeViewExt, TreeSelectionExt, GtkListStoreExtManual, TreeViewColumnExt, TreeModelExt as _}, + traits::{ButtonExt, CellRendererToggleExt, EntryExt, GtkWindowExt, WidgetExt, GtkListStoreExt, LabelExt}, + Application, ApplicationWindow, Builder, Button, Entry, ListStore, TreeView, CellRendererText, CellRendererToggle, TreeViewColumn, DrawingArea, Label, }; -use primitives::{KeyType, NotMut, SendBox, XKeyClicker}; +use gtk::EditableSignals; +use primitives::{KeyType, NotMut, SendBox, XKeyClicker, KeyBehavior}; use rdev::{listen, simulate, Event, EventType}; mod primitives; @@ -46,19 +48,108 @@ fn main() { app.run(); } +fn on_start(xkc_handle: &ArcXKeyClicker) { + // Apply start delay + let start_delay = *xkc_handle.start_delay.lock().unwrap(); + if start_delay > 0 { + sleep(std::time::Duration::from_secs(start_delay)); + } + + let hold_keys = xkc_handle.get_hold_keys(); + let mut held_keys = xkc_handle.held_keys.lock().unwrap(); + + for key in &hold_keys { + if simulate(&EventType::KeyPress(*key)).is_ok() { + held_keys.push(*key); + } + } + + *xkc_handle.click_index.lock().unwrap() = 0; + *xkc_handle.current_count.lock().unwrap() = 0; +} + +fn on_stop(xkc_handle: &ArcXKeyClicker) { + let mut held_keys = xkc_handle.held_keys.lock().unwrap(); + + // Release in reverse order + while let Some(key) = held_keys.pop() { + let _ = simulate(&EventType::KeyRelease(key)); + } +} + +/// Returns true if we should continue clicking, false if repeat count reached +fn click_next_key(xkc_handle: &ArcXKeyClicker) -> bool { + let click_keys = xkc_handle.get_click_keys(); + if click_keys.is_empty() { + return true; + } + + let mut index = xkc_handle.click_index.lock().unwrap(); + let key = click_keys[*index % click_keys.len()]; + + let _ = simulate(&EventType::KeyPress(key)); + let _ = simulate(&EventType::KeyRelease(key)); + + *index = (*index + 1) % click_keys.len(); + + // Increment and check repeat count + let mut current = xkc_handle.current_count.lock().unwrap(); + *current += 1; + + let repeat_count = *xkc_handle.repeat_count.lock().unwrap(); + if repeat_count > 0 && *current >= repeat_count { + return false; + } + + true +} + fn auto_clicker(xkc_handle: &ArcXKeyClicker) { loop { - if *xkc_handle.state.lock().unwrap() { - if let Some(key) = *xkc_handle.repeated_key.lock().unwrap() { - let delay = &*xkc_handle.cooldown.lock().unwrap(); - simulate(&EventType::KeyPress(key)).unwrap(); - simulate(&EventType::KeyRelease(key)).unwrap(); - sleep(delay.as_duration()); + let current_state = *xkc_handle.state.lock().unwrap(); + let prev_state = *xkc_handle.prev_state.lock().unwrap(); + + // Detect state transitions + if current_state && !prev_state { + // off -> on transition + on_start(xkc_handle); + *xkc_handle.prev_state.lock().unwrap() = true; + } else if !current_state && prev_state { + // on -> off transition + on_stop(xkc_handle); + *xkc_handle.prev_state.lock().unwrap() = false; + } + + if current_state { + let delay = xkc_handle.cooldown.lock().unwrap().as_duration(); + let should_continue = click_next_key(xkc_handle); + + if !should_continue { + // Repeat count reached, stop automatically + *xkc_handle.state.lock().unwrap() = false; + } else { + sleep(delay); } + } else { + // Small sleep to avoid busy-waiting when inactive + sleep(std::time::Duration::from_millis(10)); } } } +fn refresh_list_store(list_store: &ListStore, xkc_handle: &ArcXKeyClicker) { + list_store.clear(); + let actions = xkc_handle.key_actions.lock().unwrap(); + for (i, action) in actions.iter().enumerate() { + let iter = list_store.append(); + list_store.set(&iter, &[ + (0, &(i as u32)), + (1, &format!("{:?}", action.key)), + (2, &(action.behavior == KeyBehavior::Hold)), + ]); + } +} + fn build_ui(app: &Application, entry_sender: Sender, xkc_handle: ArcXKeyClicker) { let builder = Builder::from_string(include_str!("xkeyclicker.ui")); let window: ApplicationWindow = builder.object("window").unwrap(); @@ -88,6 +179,46 @@ fn build_ui(app: &Application, entry_sender: Sender, xkc_handle: ArcXKeyC time_entry!(millis, 100); time_entry!(micros, 0); + // Manual start button + let manual_start_button: Button = builder.object("manual_start_button").unwrap(); + + // Start delay entry + let start_delay_entry: Entry = builder.object("start_delay_entry").unwrap(); + let xkc_handle_clone = xkc_handle.clone(); + let manual_start_button_clone = manual_start_button.clone(); + start_delay_entry.connect_changed(move |entry| { + if let Ok(delay) = entry.buffer().text().parse::() { + *xkc_handle_clone.start_delay.lock().unwrap() = delay; + // Enable button only if delay > 0 + manual_start_button_clone.set_sensitive(delay > 0); + } else if !entry.buffer().text().is_empty() { + entry.set_text("0"); + *xkc_handle_clone.start_delay.lock().unwrap() = 0; + manual_start_button_clone.set_sensitive(false); + } + }); + + // Manual start button click handler + let xkc_handle_for_start = xkc_handle.clone(); + manual_start_button.connect_clicked(move |_| { + let mut state = xkc_handle_for_start.state.lock().unwrap(); + if !*state { + *state = true; + } + }); + + // Repeat count entry + let repeat_count_entry: Entry = builder.object("repeat_count_entry").unwrap(); + let xkc_handle_clone = xkc_handle.clone(); + repeat_count_entry.connect_changed(move |entry| { + if let Ok(count) = entry.buffer().text().parse::() { + *xkc_handle_clone.repeat_count.lock().unwrap() = count; + } else if !entry.buffer().text().is_empty() { + entry.set_text("0"); + *xkc_handle_clone.repeat_count.lock().unwrap() = 0; + } + }); + let start_keybind_button: Button = builder.object("start_keybind").unwrap(); let keybind_entry: Entry = builder.object("keybind_entry").unwrap(); @@ -103,18 +234,144 @@ fn build_ui(app: &Application, entry_sender: Sender, xkc_handle: ArcXKeyC ); }); - let key_selector_button: Button = builder.object("key_selector").unwrap(); - let repeated_key_entry: Entry = builder.object("repeated_key_entry").unwrap(); + // Set up the key list TreeView + let key_list_store = ListStore::new(&[Type::U32, Type::STRING, Type::BOOL]); + let key_tree_view: TreeView = builder.object("key_tree_view").unwrap(); + key_tree_view.set_model(Some(&key_list_store)); + + // Column 0: Index (hidden, used for internal tracking) + // Column 1: Key name + let key_name_renderer = CellRendererText::new(); + let key_name_column = TreeViewColumn::new(); + key_name_column.set_title("Key"); + key_name_column.set_expand(true); + key_name_column.pack_start(&key_name_renderer, true); + key_name_column.add_attribute(&key_name_renderer, "text", 1); + key_tree_view.append_column(&key_name_column); + + // Column 2: Hold toggle + let hold_renderer = CellRendererToggle::new(); + hold_renderer.set_activatable(true); + + let list_store_clone = key_list_store.clone(); + let xkc_handle_clone = xkc_handle.clone(); + hold_renderer.connect_toggled(move |_, path| { + if let Some(iter) = list_store_clone.iter(&path) { + let index: u32 = list_store_clone.value(&iter, 0).get().unwrap_or(0); + xkc_handle_clone.toggle_behavior(index as usize); + refresh_list_store(&list_store_clone, &xkc_handle_clone); + } + }); + + let hold_column = TreeViewColumn::new(); + hold_column.set_title("Hold"); + hold_column.pack_start(&hold_renderer, false); + hold_column.add_attribute(&hold_renderer, "active", 2); + key_tree_view.append_column(&hold_column); + + // Add Key button + let add_key_button: Button = builder.object("add_key_button").unwrap(); + let key_status_entry: Entry = builder.object("key_status_entry").unwrap(); - key_selector_button.connect_clicked(move |_| { + let entry_sender_for_add = entry_sender_copy.clone(); + let xkc_handle_for_add = xkc_handle.clone(); + add_key_button.connect_clicked(move |_| { set_keybind( - &entry_sender_copy.clone(), - &repeated_key_entry, - &xkc_handle.clone(), - KeyType::Repeated, + &entry_sender_for_add.clone(), + &key_status_entry, + &xkc_handle_for_add.clone(), + KeyType::AddKey, ); }); + // Remove Key button + let remove_key_button: Button = builder.object("remove_key_button").unwrap(); + let tree_view_clone = key_tree_view.clone(); + let list_store_clone = key_list_store.clone(); + let xkc_handle_for_remove = xkc_handle.clone(); + remove_key_button.connect_clicked(move |_| { + let selection = tree_view_clone.selection(); + if let Some((model, iter)) = selection.selected() { + let index: u32 = model.value(&iter, 0).get().unwrap_or(0); + xkc_handle_for_remove.remove_key_action(index as usize); + refresh_list_store(&list_store_clone, &xkc_handle_for_remove); + } + }); + + // Move Up button + let move_up_button: Button = builder.object("move_up_button").unwrap(); + let tree_view_clone = key_tree_view.clone(); + let list_store_clone = key_list_store.clone(); + let xkc_handle_for_up = xkc_handle.clone(); + move_up_button.connect_clicked(move |_| { + let selection = tree_view_clone.selection(); + if let Some((model, iter)) = selection.selected() { + let index: u32 = model.value(&iter, 0).get().unwrap_or(0); + xkc_handle_for_up.move_key_up(index as usize); + refresh_list_store(&list_store_clone, &xkc_handle_for_up); + } + }); + + // Move Down button + let move_down_button: Button = builder.object("move_down_button").unwrap(); + let tree_view_clone = key_tree_view.clone(); + let list_store_clone = key_list_store.clone(); + let xkc_handle_for_down = xkc_handle.clone(); + move_down_button.connect_clicked(move |_| { + let selection = tree_view_clone.selection(); + if let Some((model, iter)) = selection.selected() { + let index: u32 = model.value(&iter, 0).get().unwrap_or(0); + xkc_handle_for_down.move_key_down(index as usize); + refresh_list_store(&list_store_clone, &xkc_handle_for_down); + } + }); + + // Status indicator setup + let status_indicator: DrawingArea = builder.object("status_indicator").unwrap(); + let status_label: Label = builder.object("status_label").unwrap(); + + // Set up drawing for the status indicator + let xkc_for_draw = xkc_handle.clone(); + status_indicator.connect_draw(move |_, cr| { + let is_active = *xkc_for_draw.state.lock().unwrap(); + + if is_active { + cr.set_source_rgb(0.0, 0.8, 0.0); // Green + } else { + cr.set_source_rgb(0.5, 0.5, 0.5); // Gray + } + + // Draw a filled circle + cr.arc(8.0, 8.0, 7.0, 0.0, 2.0 * std::f64::consts::PI); + let _ = cr.fill(); + + gtk::Inhibit(false) + }); + + // Poll for changes to refresh the list and status indicator + let list_store_poll = key_list_store.clone(); + let xkc_poll = xkc_handle.clone(); + let status_indicator_poll = status_indicator.clone(); + let status_label_poll = status_label.clone(); + let mut prev_poll_state = false; + gtk::glib::timeout_add_local(std::time::Duration::from_millis(100), move || { + let actions_len = xkc_poll.key_actions.lock().unwrap().len(); + let store_len = list_store_poll.iter_n_children(None) as usize; + if actions_len != store_len { + refresh_list_store(&list_store_poll, &xkc_poll); + } + + // Update status indicator + let current_state = *xkc_poll.state.lock().unwrap(); + if current_state != prev_poll_state { + status_indicator_poll.queue_draw(); + status_label_poll.set_text(if current_state { "Active" } else { "Inactive" }); + prev_poll_state = current_state; + } + + gtk::glib::Continue(true) + }); + window.show_all(); } @@ -125,7 +382,7 @@ fn set_keybind( key_type: KeyType, ) { *xkc_handle.should_recv.lock().unwrap() = key_type; - key_entry.set_text("Press a key to bind"); + key_entry.set_text("Press a key..."); entry_sender.send(key_entry.clone()).unwrap(); } @@ -137,18 +394,20 @@ fn keybind(event: &Event, receiver: &Arc>>, xkc_handle: } = event { let mut should_recv = xkc_handle.should_recv.lock().unwrap(); - if let KeyType::Repeated = *should_recv { - *xkc_handle.repeated_key.lock().unwrap() = Some(*key); + if let KeyType::AddKey = *should_recv { + xkc_handle.add_key_action(*key); *should_recv = KeyType::None; - let entry = receiver.0.try_recv().unwrap(); - entry.set_text(&format!("{key:?}")); + if let Ok(entry) = receiver.0.try_recv() { + entry.set_text(&format!("Added: {:?}", key)); + } } else if let KeyType::Keybind = *should_recv { *xkc_handle.keybind.lock().unwrap() = *key; *should_recv = KeyType::None; - let entry = receiver.0.try_recv().unwrap(); - entry.set_text(&format!("{key:?}")); + if let Ok(entry) = receiver.0.try_recv() { + entry.set_text(&format!("{key:?}")); + } } else if *key == *xkc_handle.keybind.lock().unwrap() { xkc_handle.state.lock().unwrap().not_mut(); } diff --git a/src/primitives.rs b/src/primitives.rs index e7688ca..4ebab5f 100644 --- a/src/primitives.rs +++ b/src/primitives.rs @@ -17,9 +17,31 @@ impl NotMut for bool { } } +#[derive(Debug, Default, Clone, Copy, PartialEq)] +pub enum KeyBehavior { + #[default] + Click, + Hold, +} + +#[derive(Debug, Clone, Copy)] +pub struct KeyAction { + pub key: Key, + pub behavior: KeyBehavior, +} + +impl KeyAction { + pub fn new(key: Key) -> Self { + Self { + key, + behavior: KeyBehavior::Click, + } + } +} + #[derive(Debug, Default, Clone, Copy)] pub enum KeyType { - Repeated, + AddKey, Keybind, #[default] None, @@ -31,7 +53,13 @@ pub struct XKeyClicker { pub should_recv: Mutex, pub state: Mutex, pub cooldown: Mutex, - pub repeated_key: Mutex>, + pub start_delay: Mutex, + pub repeat_count: Mutex, + pub current_count: Mutex, + pub key_actions: Mutex>, + pub click_index: Mutex, + pub held_keys: Mutex>, + pub prev_state: Mutex, } impl Default for XKeyClicker { @@ -41,7 +69,13 @@ impl Default for XKeyClicker { should_recv: Mutex::default(), state: Mutex::default(), cooldown: Mutex::default(), - repeated_key: Mutex::default(), + start_delay: Mutex::new(0), + repeat_count: Mutex::new(0), + current_count: Mutex::new(0), + key_actions: Mutex::new(Vec::new()), + click_index: Mutex::new(0), + held_keys: Mutex::new(Vec::new()), + prev_state: Mutex::new(false), } } } @@ -50,6 +84,61 @@ impl XKeyClicker { pub fn new() -> Arc { Arc::default() } + + pub fn add_key_action(&self, key: Key) { + self.key_actions.lock().unwrap().push(KeyAction::new(key)); + } + + pub fn remove_key_action(&self, index: usize) { + let mut actions = self.key_actions.lock().unwrap(); + if index < actions.len() { + actions.remove(index); + } + } + + pub fn move_key_up(&self, index: usize) { + let mut actions = self.key_actions.lock().unwrap(); + if index > 0 && index < actions.len() { + actions.swap(index, index - 1); + } + } + + pub fn move_key_down(&self, index: usize) { + let mut actions = self.key_actions.lock().unwrap(); + if index + 1 < actions.len() { + actions.swap(index, index + 1); + } + } + + pub fn toggle_behavior(&self, index: usize) { + let mut actions = self.key_actions.lock().unwrap(); + if index < actions.len() { + actions[index].behavior = match actions[index].behavior { + KeyBehavior::Click => KeyBehavior::Hold, + KeyBehavior::Hold => KeyBehavior::Click, + }; + } + } + + pub fn get_click_keys(&self) -> Vec { + self.key_actions + .lock() + .unwrap() + .iter() + .filter(|a| a.behavior == KeyBehavior::Click) + .map(|a| a.key) + .collect() + } + + pub fn get_hold_keys(&self) -> Vec { + self.key_actions + .lock() + .unwrap() + .iter() + .filter(|a| a.behavior == KeyBehavior::Hold) + .map(|a| a.key) + .collect() + } } #[derive(Debug)] diff --git a/src/xkeyclicker.ui b/src/xkeyclicker.ui index 4a76539..88ef50f 100644 --- a/src/xkeyclicker.ui +++ b/src/xkeyclicker.ui @@ -4,6 +4,8 @@ False + 400 + 450 True @@ -15,6 +17,46 @@ 10 vertical 3 + + + + True + False + end + 8 + 5 + + + True + False + Inactive + + + False + True + 0 + + + + + True + False + 16 + 16 + + + False + True + 1 + + + + + False + True + 0 + + True @@ -162,139 +204,186 @@ - - + True False - 15 - True - True + 3 + 0 + in - + True False - 3 - 0 - 0.40000000596046448 - in + vertical + 5 + 5 + 5 + 5 + 5 + 5 + - - + True False 5 5 5 5 - 5 - 8 - True - True + 5 + 10 + True - + + Start/Stop Key True - False - 5 - 5 - 5 - 5 - 1 - 5 - 10 - True - - - Keybind - True - True - True - - - False - True - 0 - - - - - True - True - False - F7 - False - ← Click to setup a key - - - False - True - 1 - - + True + True - 0 - 0 + False + True + 0 - + + True + True + False + F7 + False + Click button to set + + + False + True + 1 + + + + + False + True + 0 + + + + + + True + False + 5 + 5 + 5 + 5 + 10 + True + + True False - 5 - 5 - 5 - 5 - 1 - 5 - 10 - True - - - Repeated Key - True - True - True - - - False - True - 0 - - - - - True - True - False - False - ← Click to setup a key - - - False - True - 1 - - + Start Delay (seconds) + 0 - 0 - 1 + False + True + 0 + + + + + True + True + 9 + 0 + + + False + True + 1 + + False + True + 1 + - - + + + True False - Options + 5 + 5 + 5 + 5 + 10 + True + + + True + False + Repeat Count (0=infinite) + 0 + + + False + True + 0 + + + + + True + True + 9 + 0 + + + False + True + 1 + + + + False + True + 2 + + + + + + Manual Start + True + True + True + False + 5 + 5 + 5 + 5 + 5 + + + False + True + 3 + - - 0 - 0 - + + + + True + False + Options + @@ -303,6 +392,159 @@ 1 + + + + True + False + 0 + in + True + + + True + False + vertical + 5 + 5 + 5 + 5 + 5 + 5 + 5 + + + + True + False + 10 + + + + Add Key + True + True + True + + + False + True + 0 + + + + + True + True + False + True + False + Click + Add Key, then press a key + + + True + True + 1 + + + + + False + True + 0 + + + + + + True + True + True + 120 + in + + + True + True + True + False + + + + + True + True + 1 + + + + + + True + False + 5 + True + + + Remove + True + True + True + + + True + True + 0 + + + + + Move Up + True + True + True + + + True + True + 1 + + + + + Move Down + True + True + True + + + True + True + 2 + + + + + False + True + 2 + + + + + + + True + False + Keys to Repeat + + + + + True + True + 2 + + From 4275c1150412603fa590dfe9099bf7f5b8804ef7 Mon Sep 17 00:00:00 2001 From: Paul Jobson Date: Fri, 5 Jun 2026 23:47:23 -0400 Subject: [PATCH 2/2] Fixed issues brought by coderabbitai. 1. Fixed blocking sleeps (src/main.rs:48-66) Added interruptible_sleep() function that checks state every 100ms during delays. This ensures the user can stop during: - Start delay (on_start) - Click cooldown intervals (auto_clicker) 2. Empty numeric fields treated as zero (src/main.rs:230-244, 258-269) Added handling for empty strings in start_delay_entry and repeat_count_entry to explicitly set values to 0 when the field is cleared. 3. Fixed Entry channel/should_recv race condition (src/main.rs:274-337, 465-476) - Replaced the mpsc channel approach with a single shared PendingEntry (Rc>>) - Both the KeyType and Entry reference are now stored atomically, preventing mismatch when user clicks multiple buttons quickly 4. Dispatch Entry::set_text to GTK main thread (src/main.rs:187-197, 381-414, 478-502) - Replaced direct Entry updates from the rdev listener thread with a glib channel (glib::MainContext::channel) - The listener thread now sends KeyMessage::KeyCaptured to the GTK main thread - The main thread handler processes these messages and safely updates the Entry widgets Other cleanups - Removed unused SendBox type from primitives.rs - Added PartialEq derive to KeyType for comparison - Removed Debug derive from XKeyClicker (Entry doesn't implement Debug) --- src/main.rs | 195 +++++++++++++++++++++++++++++++++------------- src/primitives.rs | 7 +- 2 files changed, 142 insertions(+), 60 deletions(-) diff --git a/src/main.rs b/src/main.rs index 5fd4fbb..e863247 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,57 +2,76 @@ #![windows_subsystem = "windows"] use std::{ - sync::{ - mpsc::{channel, Receiver, Sender}, - Arc, - }, + cell::RefCell, + rc::Rc, + sync::Arc, thread::{sleep, spawn}, }; use gtk::{ gio::ApplicationFlags, - glib::Type, + glib::{self, Type}, prelude::{ApplicationExt, ApplicationExtManual, BuilderExtManual, TreeViewExt, TreeSelectionExt, GtkListStoreExtManual, TreeViewColumnExt, TreeModelExt as _}, traits::{ButtonExt, CellRendererToggleExt, EntryExt, GtkWindowExt, WidgetExt, GtkListStoreExt, LabelExt}, Application, ApplicationWindow, Builder, Button, Entry, ListStore, TreeView, CellRendererText, CellRendererToggle, TreeViewColumn, DrawingArea, Label, }; use gtk::EditableSignals; -use primitives::{KeyType, NotMut, SendBox, XKeyClicker, KeyBehavior}; -use rdev::{listen, simulate, Event, EventType}; +use primitives::{KeyType, NotMut, XKeyClicker, KeyBehavior}; +use glib::Sender; +use rdev::{listen, simulate, Event, EventType, Key}; mod primitives; type ArcXKeyClicker = Arc; -fn main() { - let xkeyclicker = XKeyClicker::new(); +/// Stores the pending Entry reference on the main thread +type PendingEntry = Rc>>; - let (entry_sender, entry_receiver) = channel(); - let entry_receiver = Arc::new(SendBox(entry_receiver)); +/// Message sent from the listener thread to the GTK main thread +#[derive(Debug, Clone)] +enum KeyMessage { + KeyCaptured { key: Key, key_type: KeyType }, +} - let xkc_handle = xkeyclicker.clone(); - // Spawn keybind listener - spawn(move || { - listen(move |e| { - keybind(&e, &entry_receiver.clone(), &xkc_handle.clone()); - }) - .unwrap(); - }); +fn main() { + let xkeyclicker = XKeyClicker::new(); let xkc_handle = xkeyclicker.clone(); // Spawn auto clicker spawn(move || auto_clicker(&xkc_handle)); let app = Application::new(Some("com.s0ra.xkeyclicker"), ApplicationFlags::default()); - app.connect_activate(move |app| build_ui(app, entry_sender.clone(), xkeyclicker.clone())); + app.connect_activate(move |app| build_ui(app, xkeyclicker.clone())); app.run(); } -fn on_start(xkc_handle: &ArcXKeyClicker) { - // Apply start delay +/// Interruptible sleep that checks state every 100ms +/// Returns false if state became inactive during the sleep +fn interruptible_sleep(xkc_handle: &ArcXKeyClicker, duration: std::time::Duration) -> bool { + let check_interval = std::time::Duration::from_millis(100); + let mut remaining = duration; + + while remaining > std::time::Duration::ZERO { + let sleep_time = remaining.min(check_interval); + sleep(sleep_time); + + // Check if we should stop + if !*xkc_handle.state.lock().unwrap() { + return false; + } + + remaining = remaining.saturating_sub(sleep_time); + } + true +} + +fn on_start(xkc_handle: &ArcXKeyClicker) -> bool { + // Apply start delay with interruptible sleep let start_delay = *xkc_handle.start_delay.lock().unwrap(); if start_delay > 0 { - sleep(std::time::Duration::from_secs(start_delay)); + if !interruptible_sleep(xkc_handle, std::time::Duration::from_secs(start_delay)) { + return false; + } } let hold_keys = xkc_handle.get_hold_keys(); @@ -66,6 +85,7 @@ fn on_start(xkc_handle: &ArcXKeyClicker) { *xkc_handle.click_index.lock().unwrap() = 0; *xkc_handle.current_count.lock().unwrap() = 0; + true } fn on_stop(xkc_handle: &ArcXKeyClicker) { @@ -112,7 +132,11 @@ fn auto_clicker(xkc_handle: &ArcXKeyClicker) { // Detect state transitions if current_state && !prev_state { // off -> on transition - on_start(xkc_handle); + let started_successfully = on_start(xkc_handle); + if !started_successfully { + // User stopped during start delay, don't mark as started + continue; + } *xkc_handle.prev_state.lock().unwrap() = true; } else if !current_state && prev_state { // on -> off transition @@ -120,6 +144,9 @@ fn auto_clicker(xkc_handle: &ArcXKeyClicker) { *xkc_handle.prev_state.lock().unwrap() = false; } + // Re-check state after potential on_start + let current_state = *xkc_handle.state.lock().unwrap(); + if current_state { let delay = xkc_handle.cooldown.lock().unwrap().as_duration(); let should_continue = click_next_key(xkc_handle); @@ -128,7 +155,8 @@ fn auto_clicker(xkc_handle: &ArcXKeyClicker) { // Repeat count reached, stop automatically *xkc_handle.state.lock().unwrap() = false; } else { - sleep(delay); + // Use interruptible sleep for cooldown + interruptible_sleep(xkc_handle, delay); } } else { // Small sleep to avoid busy-waiting when inactive @@ -150,12 +178,24 @@ fn refresh_list_store(list_store: &ListStore, xkc_handle: &ArcXKeyClicker) { } } -fn build_ui(app: &Application, entry_sender: Sender, xkc_handle: ArcXKeyClicker) { +fn build_ui(app: &Application, xkc_handle: ArcXKeyClicker) { let builder = Builder::from_string(include_str!("xkeyclicker.ui")); let window: ApplicationWindow = builder.object("window").unwrap(); window.set_application(Some(app)); + // Create glib channel for thread-safe communication from listener to GTK main thread + let (key_sender, key_receiver) = glib::MainContext::channel::(glib::PRIORITY_DEFAULT); + + // Spawn keybind listener + let xkc_handle_for_listener = xkc_handle.clone(); + spawn(move || { + listen(move |e| { + keybind(&e, &key_sender, &xkc_handle_for_listener); + }) + .unwrap(); + }); + macro_rules! time_entry { ($time_type:tt, $default_cooldown:tt) => { let $time_type: Entry = builder @@ -187,11 +227,16 @@ fn build_ui(app: &Application, entry_sender: Sender, xkc_handle: ArcXKeyC let xkc_handle_clone = xkc_handle.clone(); let manual_start_button_clone = manual_start_button.clone(); start_delay_entry.connect_changed(move |entry| { - if let Ok(delay) = entry.buffer().text().parse::() { + let text = entry.buffer().text(); + if text.is_empty() { + // Treat empty field as zero + *xkc_handle_clone.start_delay.lock().unwrap() = 0; + manual_start_button_clone.set_sensitive(false); + } else if let Ok(delay) = text.parse::() { *xkc_handle_clone.start_delay.lock().unwrap() = delay; // Enable button only if delay > 0 manual_start_button_clone.set_sensitive(delay > 0); - } else if !entry.buffer().text().is_empty() { + } else { entry.set_text("0"); *xkc_handle_clone.start_delay.lock().unwrap() = 0; manual_start_button_clone.set_sensitive(false); @@ -211,9 +256,13 @@ fn build_ui(app: &Application, entry_sender: Sender, xkc_handle: ArcXKeyC let repeat_count_entry: Entry = builder.object("repeat_count_entry").unwrap(); let xkc_handle_clone = xkc_handle.clone(); repeat_count_entry.connect_changed(move |entry| { - if let Ok(count) = entry.buffer().text().parse::() { + let text = entry.buffer().text(); + if text.is_empty() { + // Treat empty field as zero + *xkc_handle_clone.repeat_count.lock().unwrap() = 0; + } else if let Ok(count) = text.parse::() { *xkc_handle_clone.repeat_count.lock().unwrap() = count; - } else if !entry.buffer().text().is_empty() { + } else { entry.set_text("0"); *xkc_handle_clone.repeat_count.lock().unwrap() = 0; } @@ -222,14 +271,17 @@ fn build_ui(app: &Application, entry_sender: Sender, xkc_handle: ArcXKeyC let start_keybind_button: Button = builder.object("start_keybind").unwrap(); let keybind_entry: Entry = builder.object("keybind_entry").unwrap(); - let entry_sender_copy = entry_sender.clone(); + // Create shared pending entry storage (GTK main thread only) + let pending_entry: PendingEntry = Rc::new(RefCell::new(None)); + let xkc_handle_clone = xkc_handle.clone(); + let pending_entry_clone = pending_entry.clone(); start_keybind_button.connect_clicked(move |_| { set_keybind( - &entry_sender.clone(), &keybind_entry, - &xkc_handle_clone.clone(), + &pending_entry_clone, + &xkc_handle_clone, KeyType::Keybind, ); }); @@ -273,13 +325,13 @@ fn build_ui(app: &Application, entry_sender: Sender, xkc_handle: ArcXKeyC let add_key_button: Button = builder.object("add_key_button").unwrap(); let key_status_entry: Entry = builder.object("key_status_entry").unwrap(); - let entry_sender_for_add = entry_sender_copy.clone(); let xkc_handle_for_add = xkc_handle.clone(); + let pending_entry_for_add = pending_entry.clone(); add_key_button.connect_clicked(move |_| { set_keybind( - &entry_sender_for_add.clone(), &key_status_entry, - &xkc_handle_for_add.clone(), + &pending_entry_for_add, + &xkc_handle_for_add, KeyType::AddKey, ); }); @@ -326,6 +378,41 @@ fn build_ui(app: &Application, entry_sender: Sender, xkc_handle: ArcXKeyC } }); + // Handle key capture messages from the listener thread (runs on GTK main thread) + let xkc_handle_for_receiver = xkc_handle.clone(); + let pending_entry_for_receiver = pending_entry.clone(); + key_receiver.attach(None, move |msg| { + match msg { + KeyMessage::KeyCaptured { key, key_type } => { + // Check if the key type matches what we're waiting for + let mut should_recv = xkc_handle_for_receiver.should_recv.lock().unwrap(); + + if *should_recv == key_type { + // Take ownership of pending entry + let entry_opt = pending_entry_for_receiver.borrow_mut().take(); + + match key_type { + KeyType::AddKey => { + xkc_handle_for_receiver.add_key_action(key); + if let Some(entry) = entry_opt { + entry.set_text(&format!("Added: {:?}", key)); + } + } + KeyType::Keybind => { + *xkc_handle_for_receiver.keybind.lock().unwrap() = key; + if let Some(entry) = entry_opt { + entry.set_text(&format!("{:?}", key)); + } + } + KeyType::None => {} + } + *should_recv = KeyType::None; + } + } + } + glib::Continue(true) + }); + // Status indicator setup let status_indicator: DrawingArea = builder.object("status_indicator").unwrap(); let status_label: Label = builder.object("status_label").unwrap(); @@ -376,40 +463,40 @@ fn build_ui(app: &Application, entry_sender: Sender, xkc_handle: ArcXKeyC } fn set_keybind( - entry_sender: &Sender, key_entry: &Entry, + pending_entry: &PendingEntry, xkc_handle: &ArcXKeyClicker, key_type: KeyType, ) { + // Store the key type in the shared state *xkc_handle.should_recv.lock().unwrap() = key_type; + // Store the entry reference on the main thread (GTK-safe) + *pending_entry.borrow_mut() = Some(key_entry.clone()); key_entry.set_text("Press a key..."); - entry_sender.send(key_entry.clone()).unwrap(); } -fn keybind(event: &Event, receiver: &Arc>>, xkc_handle: &ArcXKeyClicker) { +fn keybind(event: &Event, key_sender: &Sender, xkc_handle: &ArcXKeyClicker) { if let Event { time: _, name: _, event_type: EventType::KeyPress(key), } = event { - let mut should_recv = xkc_handle.should_recv.lock().unwrap(); - if let KeyType::AddKey = *should_recv { - xkc_handle.add_key_action(*key); - *should_recv = KeyType::None; - - if let Ok(entry) = receiver.0.try_recv() { - entry.set_text(&format!("Added: {:?}", key)); + let should_recv = *xkc_handle.should_recv.lock().unwrap(); + match should_recv { + KeyType::AddKey | KeyType::Keybind => { + // Send key event to the GTK main thread for safe UI updates + let _ = key_sender.send(KeyMessage::KeyCaptured { + key: *key, + key_type: should_recv, + }); } - } else if let KeyType::Keybind = *should_recv { - *xkc_handle.keybind.lock().unwrap() = *key; - *should_recv = KeyType::None; - - if let Ok(entry) = receiver.0.try_recv() { - entry.set_text(&format!("{key:?}")); + KeyType::None => { + // Check if this is the toggle keybind + if *key == *xkc_handle.keybind.lock().unwrap() { + xkc_handle.state.lock().unwrap().not_mut(); + } } - } else if *key == *xkc_handle.keybind.lock().unwrap() { - xkc_handle.state.lock().unwrap().not_mut(); } } } diff --git a/src/primitives.rs b/src/primitives.rs index 4ebab5f..a34b4ea 100644 --- a/src/primitives.rs +++ b/src/primitives.rs @@ -39,7 +39,7 @@ impl KeyAction { } } -#[derive(Debug, Default, Clone, Copy)] +#[derive(Debug, Default, Clone, Copy, PartialEq)] pub enum KeyType { AddKey, Keybind, @@ -47,7 +47,6 @@ pub enum KeyType { None, } -#[derive(Debug)] pub struct XKeyClicker { pub keybind: Mutex, pub should_recv: Mutex, @@ -169,7 +168,3 @@ impl Cooldown { } } -pub struct SendBox(pub T); - -unsafe impl Send for SendBox {} -unsafe impl Sync for SendBox {}