this definitely needed to be over 1k lines of code for our initial push

This commit is contained in:
2025-05-04 00:34:24 -05:00
parent d7af2d8aef
commit 41daa9b0d3
5 changed files with 1301 additions and 2 deletions
+18
View File
@@ -4,3 +4,21 @@ version = "0.1.0"
edition = "2021"
[dependencies]
iced = { version = "0.13.1", default-features = true, features = ["image", "advanced"] } # GUI, bundles wgpu + winit
tray-icon = "0.20.1" # cross‑platform system tray
image = "0.24.7" # Image processing
walkdir = "2.4.0" # Directory traversal
global-hotkey = "0.4.2" # Global hotkey registration
lazy_static = "1.4.0" # Thread-safe static initialization
# Platform-specific dependencies
[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3.9", features = ["winuser", "wingdi", "consoleapi"] } # Windows API
ctor = "0.2.4" # Constructor/destructor attributes
[target.'cfg(target_os = "macos")'.dependencies]
objc = "0.2.7" # Objective-C bindings
cocoa = "0.24.1" # macOS Cocoa bindings
[target.'cfg(target_os = "linux")'.dependencies]
x11 = { version = "2.21.0", features = ["xlib"] } # X11 bindings
Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 407 B

+524 -2
View File
@@ -1,3 +1,525 @@
fn main() {
println!("Hello, Cursor Rush!");
use iced::{
widget::{button, text, Column, Row, container, image as iced_image},
Element, Length, Subscription, Task, Theme,
};
use std::{
path::{Path, PathBuf},
sync::{mpsc, Arc, Mutex},
thread,
time::Duration,
};
use ::image::{GenericImageView, RgbaImage, open as image_open};
use walkdir::WalkDir;
use tray_icon::{
TrayIconBuilder,
menu::{Menu, MenuEvent},
};
use global_hotkey::{
GlobalHotKeyManager,
hotkey::HotKey as GlobalHotKey,
HotKeyState,
};
// Import our platform-specific module
mod platform;
// Define a struct to hold cursor information
#[derive(Clone)]
struct CursorInfo {
name: String,
path: PathBuf,
}
// We'll use a simpler approach for the cursor overlay
// Define menu item IDs
const NEXT_CURSOR_ID: &str = "next_cursor";
const QUIT_ID: &str = "quit";
// Shared state between the UI and the tray icon
struct SharedState {
current_index: usize,
cursors: Vec<CursorInfo>,
current_cursor_image: Option<RgbaImage>,
running: bool,
}
// Our application state
#[derive(Default)]
struct CursorRush {
cursors: Vec<CursorInfo>,
current_index: usize,
shared_state: Option<Arc<Mutex<SharedState>>>,
cursor_position: (f32, f32), // Track the mouse position
}
// Messages that can be sent in our application
#[derive(Debug, Clone)]
enum Message {
NextCursor,
SelectCursor(usize),
Quit,
TrayEvent,
CursorMoved((f32, f32)), // New message for cursor movement
}
// Function to load all PNG files from the assets directory
fn load_cursor_files() -> Vec<CursorInfo> {
let mut cursors = Vec::new();
println!("Looking for cursor PNG files in assets directory...");
// Check if assets directory exists
if !std::path::Path::new("assets").exists() {
println!("Assets directory not found!");
return cursors;
}
for entry in WalkDir::new("assets").into_iter().filter_map(|e| e.ok()) {
let path = entry.path();
println!("Found file: {}", path.display());
if path.is_file() && path.extension().map_or(false, |ext| ext == "png") {
let name = path.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("Unknown")
.to_string();
println!("Adding cursor: {} at path {}", name, path.display());
cursors.push(CursorInfo {
name,
path: path.to_path_buf(),
});
}
}
println!("Found {} cursor PNG files", cursors.len());
// Sort cursors by name for consistent ordering
cursors.sort_by(|a, b| a.name.cmp(&b.name));
cursors
}
// Function to validate a cursor image
fn validate_cursor_image(path: &Path) -> bool {
// Load the image to validate it
match image_open(path) {
Ok(img) => {
let (width, height) = img.dimensions();
// Ensure reasonable dimensions
if width > 128 || height > 128 || width == 0 || height == 0 {
eprintln!("Image dimensions not suitable for cursor: {}x{}", width, height);
return false;
}
true
},
Err(e) => {
eprintln!("Failed to open image {}: {}", path.display(), e);
false
},
}
}
// Apply the cursor at the given path
fn apply_cursor(path: &Path) -> bool {
// Validate the cursor image
if validate_cursor_image(path) {
// Use platform-specific cursor setting
if platform::set_cursor(path) {
println!("Successfully set cursor");
return true;
} else {
eprintln!("Failed to set cursor");
}
}
false
}
// Top-level subscription function to listen for events
fn subscription(_state: &CursorRush) -> Subscription<Message> {
iced::event::listen().map(|event| {
if let iced::Event::Mouse(iced::mouse::Event::CursorMoved { position }) = event {
// Track cursor position for UI display purposes
Message::CursorMoved((position.x, position.y))
} else {
// We need to return some message, so we'll use TrayEvent as a no-op
Message::TrayEvent
}
})
}
// Boot function to initialize the application
fn boot() -> (CursorRush, Task<Message>) {
// Load cursor files from assets directory
let cursors = load_cursor_files();
if cursors.is_empty() {
eprintln!("No cursor PNG files found in assets directory!");
return (CursorRush::default(), Task::none());
}
// Load the first cursor image
let current_cursor_image = if let Some(cursor) = cursors.first() {
match image_open(&cursor.path) {
Ok(img) => Some(img.to_rgba8()),
Err(_) => None,
}
} else {
None
};
// Apply the first cursor by default (this will hide the system cursor and start the render thread)
if let Some(cursor) = cursors.first() {
apply_cursor(&cursor.path);
// Explicitly call render_cursor to ensure the render thread starts
platform::render_cursor();
}
// Create shared state
let shared_state = Arc::new(Mutex::new(SharedState {
current_index: 0,
cursors: cursors.clone(),
current_cursor_image,
running: true,
}));
// Create a channel to communicate with the UI
let (tx, rx) = mpsc::channel();
// Set up the tray icon
setup_tray_icon(&cursors, shared_state.clone(), tx.clone());
// Initialize the application state with loaded cursors
let mut state = CursorRush::default();
state.cursors = cursors;
state.shared_state = Some(shared_state.clone());
// Set up global hotkey manager
let hotkey_manager = GlobalHotKeyManager::new().expect("Failed to create hotkey manager");
// Register Ctrl+Shift+F12 as the exit hotkey (uncommon but easy to remember)
use global_hotkey::hotkey::{Code, Modifiers};
let hotkey = GlobalHotKey::new(
Some(Modifiers::CONTROL | Modifiers::SHIFT),
Code::F12,
);
if let Err(e) = hotkey_manager.register(hotkey) {
eprintln!("Failed to register hotkey: {}", e);
} else {
println!("Press Ctrl+Shift+F12 to exit the application");
}
// We're using the event-driven approach with the subscription function for mouse tracking
// Spawn a thread to listen for tray events
let tx_clone = tx.clone();
thread::spawn(move || {
loop {
if let Ok(_) = rx.recv() {
// Send a message to the UI
let _ = tx_clone.send(Message::TrayEvent);
}
}
});
// Spawn a thread to listen for hotkeys
let state_clone = shared_state.clone();
thread::spawn(move || {
let event_receiver = global_hotkey::GlobalHotKeyEvent::receiver();
loop {
if let Ok(event) = event_receiver.try_recv() {
if event.state == HotKeyState::Pressed {
println!("Exit hotkey pressed, shutting down...");
// Set running to false
if let Ok(mut state) = state_clone.lock() {
state.running = false;
}
// Restore the system cursor
platform::restore_cursor();
// Exit the application
std::process::exit(0);
}
}
// Small sleep to prevent high CPU usage
std::thread::sleep(Duration::from_millis(10));
}
});
// Using the event-driven approach with the subscription function for cursor tracking
(state, Task::none())
}
fn main() -> iced::Result {
iced::application("Cursor Rush", update, view)
.theme(|_| Theme::Dark)
.subscription(subscription)
.centered()
.run_with(boot)
}
// Set up the system tray icon
fn setup_tray_icon(
cursors: &[CursorInfo],
shared_state: Arc<Mutex<SharedState>>,
tx: mpsc::Sender<Message>,
) {
if cursors.is_empty() {
return;
}
// Create a menu
let mut menu = Menu::new();
// Add menu items with IDs
let next_item = tray_icon::menu::MenuItem::with_id(
NEXT_CURSOR_ID,
"Next cursor",
true,
None, // No accelerator
);
let quit_item = tray_icon::menu::MenuItem::with_id(
QUIT_ID,
"Quit",
true,
None, // No accelerator
);
// Add items to menu
if let Ok(menu_with_items) = Menu::with_items(&[&next_item, &quit_item]) {
menu = menu_with_items;
}
// Use the first PNG as the tray icon
let icon_path = &cursors[0].path;
// Load the image
let img = match image_open(icon_path) {
Ok(img) => img,
Err(e) => {
eprintln!("Failed to open image {}: {}", icon_path.display(), e);
return;
}
};
// Convert to RGBA
let rgba_img = img.to_rgba8();
let (width, height) = rgba_img.dimensions();
// Create icon from RGBA data
let icon = match tray_icon::Icon::from_rgba(rgba_img.into_raw(), width, height) {
Ok(icon) => icon,
Err(e) => {
eprintln!("Failed to create icon: {}", e);
return;
}
};
// Build the tray icon
let _tray = TrayIconBuilder::new()
.with_tooltip("Cursor Rush")
.with_icon(icon)
.with_menu(Box::new(menu))
.build()
.unwrap();
// Listen for tray menu events in a separate thread
let state_clone = shared_state.clone();
thread::spawn(move || {
let menu_channel = MenuEvent::receiver();
while let Ok(event) = menu_channel.recv() {
match event.id.as_ref() {
NEXT_CURSOR_ID => {
// Handle next cursor event
let mut state = state_clone.lock().unwrap();
if !state.cursors.is_empty() {
state.current_index = (state.current_index + 1) % state.cursors.len();
if let Some(cursor) = state.cursors.get(state.current_index) {
apply_cursor(&cursor.path);
// Ensure the render thread is running with the new cursor
platform::render_cursor();
// Load the new cursor image
let current_cursor_image = match image_open(&cursor.path) {
Ok(img) => Some(img.to_rgba8()),
Err(_) => None,
};
// Update the cursor image in shared state
state.current_cursor_image = current_cursor_image;
}
}
// Notify the UI
let _ = tx.send(Message::TrayEvent);
},
QUIT_ID => {
// Restore the system cursor before exiting
platform::restore_cursor();
// Exit the application
std::process::exit(0);
},
_ => {} // any other menu items you add later
}
}
});
}
// Update function to handle messages
fn update(state: &mut CursorRush, message: Message) -> Task<Message> {
match message {
Message::NextCursor => {
if !state.cursors.is_empty() {
state.current_index = (state.current_index + 1) % state.cursors.len();
// Apply the new cursor
if let Some(cursor) = state.cursors.get(state.current_index) {
apply_cursor(&cursor.path);
// Ensure the render thread is running with the new cursor
platform::render_cursor();
// Load the new cursor image
let current_cursor_image = match image::open(&cursor.path) {
Ok(img) => Some(img.to_rgba8()),
Err(_) => None,
};
// Update shared state
if let Some(shared) = &state.shared_state {
if let Ok(mut shared_state) = shared.lock() {
shared_state.current_index = state.current_index;
shared_state.current_cursor_image = current_cursor_image;
}
}
}
}
Task::none()
},
Message::SelectCursor(idx) => {
if idx < state.cursors.len() {
state.current_index = idx;
// Apply the selected cursor
if let Some(cursor) = state.cursors.get(state.current_index) {
apply_cursor(&cursor.path);
// Ensure the render thread is running with the new cursor
platform::render_cursor();
// Load the new cursor image
let current_cursor_image = match image_open(&cursor.path) {
Ok(img) => Some(img.to_rgba8()),
Err(_) => None,
};
// Update shared state
if let Some(shared) = &state.shared_state {
if let Ok(mut shared_state) = shared.lock() {
shared_state.current_index = state.current_index;
shared_state.current_cursor_image = current_cursor_image;
}
}
}
}
Task::none()
},
Message::Quit => {
// Restore the system cursor before exiting
platform::restore_cursor();
// Exit the application
std::process::exit(0);
},
Message::TrayEvent => {
// Sync with shared state
if let Some(shared) = &state.shared_state {
if let Ok(shared_state) = shared.lock() {
state.current_index = shared_state.current_index;
}
}
Task::none()
},
Message::CursorMoved(position) => {
// Update the cursor position
state.cursor_position = position;
Task::none()
},
}
}
// View function to render the UI
fn view(state: &CursorRush) -> Element<Message> {
let mut content = Column::new()
.spacing(16)
.push(text("Cursor Rush").size(24));
// Add a button for each cursor
if !state.cursors.is_empty() {
let current_cursor = &state.cursors[state.current_index];
content = content.push(text(format!("Current cursor: {}", current_cursor.name)));
// Display the current cursor image as a preview
let img_handle = iced_image::Handle::from_path(&current_cursor.path);
let cursor_image = iced_image::Image::new(img_handle)
.width(Length::Fixed(64.0))
.height(Length::Fixed(64.0));
content = content.push(cursor_image);
// Create a row of cursor buttons
let mut cursor_row = Row::new().spacing(10);
for (i, cursor) in state.cursors.iter().enumerate() {
// Create a small preview image for each cursor
let preview_handle = iced_image::Handle::from_path(&cursor.path);
let preview_image = iced_image::Image::new(preview_handle)
.width(Length::Fixed(32.0))
.height(Length::Fixed(32.0));
let btn = button(
Row::new()
.spacing(5)
.push(preview_image)
.push(text(&cursor.name).size(16))
)
.on_press(Message::SelectCursor(i));
cursor_row = cursor_row.push(btn);
}
content = content
.push(cursor_row)
.push(button("Next cursor").on_press(Message::NextCursor))
.push(button("Quit").on_press(Message::Quit));
// Add instructions for the hotkey
content = content.push(
text("Press Ctrl+Shift+F12 to exit the application").size(14)
);
} else {
content = content.push(text("No cursors found in assets directory"));
}
// Add cursor position information for debugging
if !state.cursors.is_empty() {
let (x, y) = state.cursor_position;
content = content.push(
text(format!("Cursor position: ({:.1}, {:.1})", x, y)).size(12)
);
}
// Create the main container
container(content)
.width(Length::Fill)
.height(Length::Fill)
.center_x(Length::Fill)
.center_y(Length::Fill)
.padding(20)
.into()
}
+759
View File
@@ -0,0 +1,759 @@
// Platform-specific cursor handling
// Windows implementation
#[cfg(windows)]
mod windows {
use std::path::Path;
use std::sync::{Arc, Mutex, atomic::{AtomicBool, Ordering}};
use std::thread;
use std::time::Duration;
use std::sync::Once;
use std::fs::File;
use std::io::Write;
use std::process::Command;
use winapi::{
um::winuser::{
GetDC, ReleaseDC, GetCursorPos, LoadCursorW,
SetSystemCursor, CopyIcon, IDC_ARROW,
CreateCursor, SystemParametersInfoW, SPI_SETCURSORS,
CreateWindowExW, DestroyWindow, UpdateLayeredWindow,
SetLayeredWindowAttributes, ShowWindow, SetWindowPos,
RegisterClassExW, DefWindowProcW,
WS_EX_LAYERED, WS_EX_TRANSPARENT, WS_EX_TOPMOST, WS_EX_TOOLWINDOW,
WS_POPUP, SW_SHOW, HWND_TOPMOST, SWP_NOSIZE, SWP_NOMOVE,
LWA_ALPHA, LWA_COLORKEY, WM_DESTROY, WM_CLOSE,
CS_HREDRAW, CS_VREDRAW,
},
um::wingdi::{
SetPixel, CreateCompatibleDC, DeleteDC, SelectObject, DeleteObject,
CreateCompatibleBitmap, RGB, BLENDFUNCTION, AC_SRC_OVER, AC_SRC_ALPHA,
},
um::consoleapi::SetConsoleCtrlHandler,
shared::windef::{HWND, POINT, HCURSOR, SIZE},
shared::minwindef::{DWORD, BOOL, TRUE, FALSE, UINT, WPARAM, LPARAM, LRESULT},
};
// Import the Windows API function to reset system cursors
#[link(name = "user32")]
extern "system" {
fn SystemParametersInfoA(uiAction: u32, uiParam: u32, pvParam: *mut std::ffi::c_void, fWinIni: u32) -> BOOL;
}
// Constants for SystemParametersInfo
const SPI_SETCURSORS_A: u32 = 0x0057;
const SPIF_UPDATEINIFILE: u32 = 0x01;
const SPIF_SENDCHANGE: u32 = 0x02;
use std::ptr::null_mut;
use ::image::{RgbaImage, open as image_open};
use lazy_static::lazy_static;
// Static variables for cursor state
static mut CURSOR_HIDDEN: bool = false;
// Flag to control the rendering thread
static RENDERING_ACTIVE: AtomicBool = AtomicBool::new(false);
// Define all system cursor IDs we'll replace
static SYSTEM_CURSORS: [DWORD; 11] = [
32512, // IDC_ARROW
32513, // IDC_IBEAM
32515, // IDC_CROSS
32514, // IDC_WAIT
32649, // IDC_HAND
32650, // IDC_APPSTARTING
32648, // IDC_NO
32646, // IDC_SIZEALL
32645, // IDC_SIZENS
32644, // IDC_SIZEWE
32642, // IDC_SIZENWSE
];
// Use lazy_static for thread-safe access to the cursor image
lazy_static! {
static ref CURRENT_CURSOR_IMAGE: Arc<Mutex<Option<RgbaImage>>> = Arc::new(Mutex::new(None));
static ref RENDER_THREAD: Mutex<Option<thread::JoinHandle<()>>> = Mutex::new(None);
}
// We need to use a static variable for the window handle since HWND isn't Send
static mut CURSOR_WINDOW: Option<HWND> = None;
// Window class name for our cursor window
const CURSOR_WINDOW_CLASS: &str = "CursorRushOverlay";
// Window procedure for our cursor window
unsafe extern "system" fn window_proc(hwnd: HWND, msg: UINT, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
match msg {
WM_CLOSE | WM_DESTROY => {
DestroyWindow(hwnd);
0
},
_ => DefWindowProcW(hwnd, msg, wparam, lparam),
}
}
// Create a layered, topmost window for our cursor
unsafe fn create_cursor_window() -> Option<HWND> {
use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
// Convert class name to wide string
let class_name: Vec<u16> = OsStr::new(CURSOR_WINDOW_CLASS)
.encode_wide()
.chain(std::iter::once(0))
.collect();
// Register window class
let mut wc = std::mem::zeroed::<winapi::um::winuser::WNDCLASSEXW>();
wc.cbSize = std::mem::size_of::<winapi::um::winuser::WNDCLASSEXW>() as u32;
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = Some(window_proc);
wc.hInstance = std::ptr::null_mut();
wc.lpszClassName = class_name.as_ptr();
wc.hCursor = LoadCursorW(std::ptr::null_mut(), IDC_ARROW as _);
let atom = RegisterClassExW(&wc);
if atom == 0 {
eprintln!("Failed to register window class");
return None;
}
// Create window with layered, transparent, and topmost attributes
// WS_EX_TRANSPARENT: Makes the window transparent to mouse clicks
// WS_EX_LAYERED: Required for transparency and UpdateLayeredWindow
// WS_EX_TOPMOST: Keeps the window above all other windows
// WS_EX_TOOLWINDOW: Prevents the window from showing in the taskbar
// WS_EX_NOACTIVATE: Prevents the window from becoming active when clicked
let hwnd = CreateWindowExW(
WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOPMOST | WS_EX_TOOLWINDOW | 0x08000000, // WS_EX_NOACTIVATE
class_name.as_ptr(),
std::ptr::null(), // Window title (none)
WS_POPUP, // Popup window with no border
0, 0, // Position (will be updated based on cursor position)
128, 128, // Size (will be updated based on cursor image)
std::ptr::null_mut(), // Parent window
std::ptr::null_mut(), // Menu
std::ptr::null_mut(), // Instance
std::ptr::null_mut(), // Additional data
);
if hwnd.is_null() {
eprintln!("Failed to create cursor window");
return None;
}
// Make the window transparent with click-through capability
SetLayeredWindowAttributes(hwnd, 0, 255, LWA_ALPHA | LWA_COLORKEY);
// Show the window
ShowWindow(hwnd, SW_SHOW);
// Ensure it stays on top
SetWindowPos(
hwnd,
HWND_TOPMOST,
0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE
);
Some(hwnd)
}
// We'll use a static variable for original cursors since they're not thread-safe
static mut ORIGINAL_CURSORS: Option<Vec<(DWORD, HCURSOR)>> = None;
// Flag to track if we've registered the control handler
static mut CTRL_HANDLER_REGISTERED: bool = false;
// Use Once to ensure initialization happens only once
static INIT: Once = Once::new();
// Control handler function for console events (Ctrl+C, close, etc.)
extern "system" fn ctrl_handler(_ctrl_type: DWORD) -> BOOL {
println!("Control event received, restoring cursors...");
// Call our cleanup function to restore cursors
cleanup();
// Return FALSE to allow other handlers to process the event
FALSE
}
// Cleanup function to restore cursors and clean up resources
fn cleanup() {
unsafe {
// Only restore if we've hidden the cursor
if CURSOR_HIDDEN {
println!("Restoring system cursors...");
// First, force Windows to reload the cursors
SystemParametersInfoW(SPI_SETCURSORS, 0, null_mut(), 0);
// Try multiple restoration methods and track success
let mut restoration_success = false;
// Method 1: Try to restore the original system cursors if we have them
if let Some(ref original_cursors) = ORIGINAL_CURSORS {
if !original_cursors.is_empty() {
let mut method1_success = true;
println!("Attempting to restore {} saved original cursors", original_cursors.len());
for &(cursor_id, cursor) in original_cursors.iter() {
// Make a copy of the original cursor
let cursor_copy = CopyIcon(cursor as _) as HCURSOR;
if !cursor_copy.is_null() {
// Restore the system cursor
if SetSystemCursor(cursor_copy, cursor_id) == 0 {
eprintln!("Failed to restore system cursor {}", cursor_id);
method1_success = false;
}
} else {
method1_success = false;
}
}
if method1_success {
println!("Successfully restored original cursors");
restoration_success = true;
}
}
}
// Method 2: If method 1 failed, try with the default arrow cursor
if !restoration_success {
println!("Using arrow cursor restoration method");
let mut method2_success = true;
// Load the default arrow cursor
let arrow_cursor = LoadCursorW(null_mut(), 32512 as _); // IDC_ARROW = 32512
if !arrow_cursor.is_null() {
// Make a copy for each system cursor
for &cursor_id in SYSTEM_CURSORS.iter() {
let cursor_copy = CopyIcon(arrow_cursor as _) as HCURSOR;
if !cursor_copy.is_null() {
if SetSystemCursor(cursor_copy, cursor_id) == 0 {
eprintln!("Failed to set system cursor {} to arrow", cursor_id);
method2_success = false;
}
} else {
method2_success = false;
}
}
} else {
method2_success = false;
}
if method2_success {
println!("Successfully restored cursors using arrow cursor");
restoration_success = true;
}
}
// Method 3: Reset all system cursors to their defaults using Windows API
if !restoration_success {
println!("Using system reset method");
// Call our reset function which uses both W and A versions of SystemParametersInfo
reset_system_cursors();
// We'll assume this worked unless we have evidence it didn't
restoration_success = true;
// Try to verify by checking if we can load a cursor
let test_cursor = LoadCursorW(null_mut(), 32512 as _); // IDC_ARROW
if test_cursor.is_null() {
restoration_success = false;
}
}
// Method 4: Only create and run a batch file as a last resort if all other methods failed
if !restoration_success {
println!("All direct restoration methods failed, using batch file fallback");
create_cursor_reset_file();
}
CURSOR_HIDDEN = false;
println!("System cursors restoration complete");
}
}
// Stop the rendering thread
RENDERING_ACTIVE.store(false, Ordering::SeqCst);
// Destroy the cursor window if it exists
unsafe {
if let Some(hwnd) = CURSOR_WINDOW.take() {
DestroyWindow(hwnd);
println!("Cursor window destroyed during cleanup");
}
}
}
// Register the cleanup function with control handler
fn register_cleanup() {
// Use Once to ensure this only happens once
INIT.call_once(|| {
unsafe {
// Register our control handler
if SetConsoleCtrlHandler(Some(ctrl_handler), TRUE) == 0 {
eprintln!("Failed to register control handler");
} else {
println!("Control handler registered for cursor restoration");
CTRL_HANDLER_REGISTERED = true;
}
}
});
}
// Initialize module
fn initialize() {
// Register cleanup handler on module load
register_cleanup();
// Reset system cursors on startup in case they were left in a bad state
reset_system_cursors();
}
// Call initialize when module is loaded
#[ctor::ctor]
fn module_init() {
initialize();
println!("Windows cursor module initialized with cleanup handlers");
}
// Call cleanup when module is unloaded
#[ctor::dtor]
fn module_cleanup() {
println!("Module unloading, restoring cursors...");
// Just call cleanup which now handles all restoration methods
cleanup();
// No need to create a reset file here as cleanup() will do it if needed
}
// Function to reset all system cursors to their defaults
fn reset_system_cursors() {
unsafe {
// Try multiple approaches to reset cursors
// First try with the W version
let result1 = SystemParametersInfoW(SPI_SETCURSORS, 0, null_mut(), 0);
// Then try with the A version with flags to update the system settings
let result2 = SystemParametersInfoA(
SPI_SETCURSORS_A,
0,
null_mut(),
SPIF_UPDATEINIFILE | SPIF_SENDCHANGE
);
// Try direct call to rundll32 as a more forceful approach
if result1 == 0 && result2 == 0 {
// Both API calls failed, try using Command
match Command::new("rundll32.exe")
.args(&["user32.dll,SystemParametersInfo", "0x0057", "0", "0", "0x01"])
.status()
{
Ok(status) => {
if status.success() {
println!("Reset system cursors using rundll32");
} else {
eprintln!("rundll32 cursor reset failed with status: {}", status);
}
},
Err(e) => eprintln!("Failed to execute rundll32: {}", e),
}
} else {
println!("Reset system cursors to defaults via API");
}
}
}
// Create a batch file that will reset the cursors and run it
fn create_cursor_reset_file() {
// Create a temporary batch file to reset cursors
let batch_content = r#"@echo off
echo Resetting system cursors...
rundll32.exe user32.dll,SystemParametersInfo 0x0057 0 0 0x01 | 0x02
echo Cursors reset.
timeout /t 2
del "%~f0"
"#;
let file_path = "reset_cursors.bat";
// Write the batch file
match File::create(file_path) {
Ok(mut file) => {
if let Err(e) = file.write_all(batch_content.as_bytes()) {
eprintln!("Failed to write to batch file: {}", e);
return;
}
println!("Created cursor reset batch file");
// Run the batch file in a separate process
match Command::new("cmd")
.args(&["/C", "start", "/min", file_path])
.spawn()
{
Ok(_) => println!("Launched cursor reset batch file"),
Err(e) => eprintln!("Failed to launch batch file: {}", e),
}
},
Err(e) => eprintln!("Failed to create batch file: {}", e),
}
}
// Function to save the original system cursors
fn save_original_cursors() {
unsafe {
// Only save if we haven't already
if ORIGINAL_CURSORS.is_none() {
let mut cursors = Vec::new();
for &cursor_id in SYSTEM_CURSORS.iter() {
let cursor = LoadCursorW(null_mut(), cursor_id as _);
if !cursor.is_null() {
// Make a copy of the cursor
let cursor_copy = CopyIcon(cursor as _) as HCURSOR;
if !cursor_copy.is_null() {
cursors.push((cursor_id, cursor_copy));
}
}
}
ORIGINAL_CURSORS = Some(cursors);
println!("Saved {} original system cursors", ORIGINAL_CURSORS.as_ref().unwrap().len());
}
}
}
pub fn set_cursor(path: &Path) -> bool {
// Save the original system cursors before hiding them
save_original_cursors();
// Register cleanup handler to ensure cursor is restored on exit
register_cleanup();
// Hide the system cursor
unsafe {
if !CURSOR_HIDDEN {
// Create an invisible cursor (1x1 transparent cursor)
// Create bitmap data for a 1x1 cursor
let and_plane: [u8; 1] = [0xFF]; // All bits 1 = transparent
let xor_plane: [u8; 1] = [0x00]; // All bits 0 = black
let empty_cursor = CreateCursor(
null_mut(), // hInst
0, // xHotSpot
0, // yHotSpot
1, // nWidth
1, // nHeight
and_plane.as_ptr() as *const _, // pvANDPlane
xor_plane.as_ptr() as *const _, // pvXORPlane
);
if empty_cursor.is_null() {
eprintln!("Failed to create empty cursor");
return false;
}
// Replace all system cursors with our empty cursor
for &cursor_id in SYSTEM_CURSORS.iter() {
// Make a copy of our empty cursor for each system cursor
let cursor_copy = CopyIcon(empty_cursor as _) as HCURSOR;
if !cursor_copy.is_null() {
// Set the system cursor
if SetSystemCursor(cursor_copy, cursor_id) == 0 {
eprintln!("Failed to set system cursor {}", cursor_id);
}
}
}
CURSOR_HIDDEN = true;
println!("System cursors hidden");
}
}
// Load the cursor image
match image_open(path) {
Ok(img) => {
let rgba_img = img.to_rgba8();
*CURRENT_CURSOR_IMAGE.lock().unwrap() = Some(rgba_img);
println!("Using PNG cursor: {:?}", path);
// Start the rendering thread if it's not already running
start_render_thread_if_needed();
true
},
Err(e) => {
eprintln!("Failed to load cursor image: {}", e);
false
}
}
}
// Function to restore the cursor when the program exits
pub fn restore_cursor() {
println!("Explicit restore_cursor called");
// Call our cleanup function which handles cursor restoration
cleanup();
// Join the thread if it exists
if let Some(handle) = RENDER_THREAD.lock().unwrap().take() {
// Use a timeout to avoid hanging if the thread is stuck
let thread_handle = handle;
match thread_handle.join() {
Ok(_) => println!("Render thread joined successfully"),
Err(_) => eprintln!("Error joining render thread"),
}
}
// Destroy the cursor window if it exists
unsafe {
if let Some(hwnd) = CURSOR_WINDOW.take() {
if DestroyWindow(hwnd) == 0 {
eprintln!("Failed to destroy cursor window");
} else {
println!("Cursor window destroyed");
}
}
}
// Clear the cursor image
*CURRENT_CURSOR_IMAGE.lock().unwrap() = None;
// Unregister our control handler to avoid double cleanup
unsafe {
if CTRL_HANDLER_REGISTERED {
if SetConsoleCtrlHandler(Some(ctrl_handler), FALSE) == 0 {
eprintln!("Failed to unregister control handler");
} else {
CTRL_HANDLER_REGISTERED = false;
println!("Control handler unregistered");
}
}
}
// Final verification that cursors are restored
unsafe {
if CURSOR_HIDDEN {
println!("Cursor still marked as hidden after cleanup, forcing reset");
reset_system_cursors();
CURSOR_HIDDEN = false;
}
}
}
// Function to start the rendering thread if it's not already running
fn start_render_thread_if_needed() {
let mut render_thread_guard = RENDER_THREAD.lock().unwrap();
// Only start a new thread if one isn't already running
if render_thread_guard.is_none() {
// Set the rendering active flag
RENDERING_ACTIVE.store(true, Ordering::SeqCst);
// Clone the Arc for the thread
let cursor_image_arc = CURRENT_CURSOR_IMAGE.clone();
// Create a new thread for continuous rendering
let handle = thread::spawn(move || {
println!("Starting cursor render thread");
// Render loop
while RENDERING_ACTIVE.load(Ordering::SeqCst) {
render_cursor_internal(&cursor_image_arc);
// Sleep to control frame rate (60 FPS)
thread::sleep(Duration::from_millis(16));
}
println!("Cursor render thread stopped");
});
// Store the thread handle
*render_thread_guard = Some(handle);
}
}
// Internal function to render the cursor (called from the render thread)
fn render_cursor_internal(cursor_image_arc: &Arc<Mutex<Option<RgbaImage>>>) {
// Get the cursor image
let cursor_image_lock = cursor_image_arc.lock().unwrap();
if let Some(ref cursor_image) = *cursor_image_lock {
unsafe {
// Get the current mouse position
let mut point = POINT { x: 0, y: 0 };
if GetCursorPos(&mut point) == 0 {
return;
}
// Get image dimensions
let (width, height) = cursor_image.dimensions();
// Get or create the cursor window
let hwnd = unsafe {
if CURSOR_WINDOW.is_none() {
// Create a new window if we don't have one
if let Some(window) = create_cursor_window() {
CURSOR_WINDOW = Some(window);
} else {
return;
}
}
// Unwrap is safe because we just checked it's Some
CURSOR_WINDOW.unwrap()
};
// Move the window to the cursor position
// Adjust position to center the cursor image on the actual cursor point
SetWindowPos(
hwnd,
HWND_TOPMOST,
point.x - (width as i32 / 2),
point.y - (height as i32 / 2),
width as i32,
height as i32,
0
);
// Get the window DC
let hwnd_dc = GetDC(hwnd);
if hwnd_dc.is_null() {
return;
}
// Create a compatible DC for double-buffering
let mem_dc = CreateCompatibleDC(hwnd_dc);
if mem_dc.is_null() {
ReleaseDC(hwnd, hwnd_dc);
return;
}
// Create a compatible bitmap
let bitmap = CreateCompatibleBitmap(hwnd_dc, width as i32, height as i32);
if bitmap.is_null() {
DeleteDC(mem_dc);
ReleaseDC(hwnd, hwnd_dc);
return;
}
// Select the bitmap into the memory DC
let old_bitmap = SelectObject(mem_dc, bitmap as _);
// Draw the cursor image to the memory DC
for y in 0..height {
for x in 0..width {
let pixel = cursor_image.get_pixel(x, y);
// Only draw non-transparent pixels
if pixel[3] > 128 { // Alpha channel > 50%
let color = RGB(pixel[0], pixel[1], pixel[2]);
SetPixel(mem_dc, x as i32, y as i32, color);
}
}
}
// Copy the memory DC to the window DC using BitBlt
use winapi::um::wingdi::BitBlt;
use winapi::um::wingdi::SRCCOPY;
BitBlt(
hwnd_dc,
0, 0,
width as i32, height as i32,
mem_dc,
0, 0,
SRCCOPY
);
// Clean up
SelectObject(mem_dc, old_bitmap as _);
DeleteObject(bitmap as _);
DeleteDC(mem_dc);
ReleaseDC(hwnd, hwnd_dc);
}
}
}
// Function to render the custom cursor (called from the application)
pub fn render_cursor() {
// This is now just a trigger to ensure the render thread is running
start_render_thread_if_needed();
}
}
// macOS implementation
#[cfg(target_os = "macos")]
mod macos {
use std::path::Path;
pub fn set_cursor(_path: &Path) -> bool {
// macOS cursor implementation would go here
// This is a placeholder - actual implementation would require
// Objective-C/Cocoa bindings to NSCursor
println!("macOS cursor swapping not yet implemented");
false
}
pub fn restore_cursor() {
// Placeholder for restoring cursor on macOS
println!("macOS cursor restoration not yet implemented");
}
pub fn render_cursor() {
// On macOS, we would update the cursor on mouse movement
// This is a placeholder
}
}
// Linux implementation
#[cfg(target_os = "linux")]
mod linux {
use std::path::Path;
pub fn set_cursor(_path: &Path) -> bool {
// Linux cursor implementation would go here
// This is a placeholder - actual implementation would require
// X11 or Wayland bindings
println!("Linux cursor swapping not yet implemented");
false
}
pub fn restore_cursor() {
// Placeholder for restoring cursor on Linux
println!("Linux cursor restoration not yet implemented");
}
pub fn render_cursor() {
// On Linux, we would update the cursor on mouse movement
// This is a placeholder
}
}
// Export the appropriate implementation based on platform
#[cfg(windows)]
pub use windows::{set_cursor, restore_cursor, render_cursor};
#[cfg(target_os = "macos")]
pub use macos::{set_cursor, restore_cursor, render_cursor};
#[cfg(target_os = "linux")]
pub use linux::{set_cursor, restore_cursor, render_cursor};
// Default implementation for other platforms
#[cfg(not(any(windows, target_os = "macos", target_os = "linux")))]
pub fn set_cursor(_path: &Path) -> bool {
println!("Cursor swapping not supported on this platform");
false
}
#[cfg(not(any(windows, target_os = "macos", target_os = "linux")))]
pub fn restore_cursor() {
println!("Cursor restoration not supported on this platform");
}
#[cfg(not(any(windows, target_os = "macos", target_os = "linux")))]
pub fn render_cursor() {
// No-op for unsupported platforms
}