10 Commits
Author SHA1 Message Date
Joby 0133b61944 updated readme with run directions 2025-05-17 17:25:50 -05:00
Hemeleh 20592954f7 tauri integration 2025-05-16 17:44:04 +02:00
Hemeleh 06b2f0ffaf tauri integration 2025-05-16 17:38:33 +02:00
Joby 2e71cd8b9e Updated README 2025-05-11 18:52:52 -05:00
Joby a92ef1e07c Fix cursor swapping issues and minimize cursor flashing 2025-05-11 18:48:04 -05:00
Joby b3d85106a8 Implement cursor asset caching for improved performance
Key improvements:
1. Add image caching to CursorInfo struct to avoid repeated disk reads and decoding
2. Implement Windows cursor handle caching to minimize GDI resource allocation
3. Update all code paths to use cached resources for better performance
4. Remove deprecated/unused functions for cleaner codebase

This change significantly reduces CPU usage and improves cursor switching performance.
2025-05-11 18:37:23 -05:00
Joby 34b0d700a6 Replace busy-wait hotkey listener with efficient event-driven approach 2025-05-11 18:21:50 -05:00
Joby 0e6841bd8a Clean up dead code and fix warnings while preserving Steam drop functionality and cross-platform skeleton code 2025-05-11 18:11:58 -05:00
Joby 9f6f3442dc unclean implimentation of windows cursor set as image 2025-05-10 23:42:25 -05:00
Joby a8d8d2ea0b Fixed close hotkey not working so it works now 2025-05-09 22:10:03 -05:00
96 changed files with 7310 additions and 1976 deletions
-27
View File
@@ -1,27 +0,0 @@
[package]
name = "cursor_rush"
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
steamworks = { version = "0.11.0", features = ["raw-bindings"] }
anyhow = "1.0.98"
once_cell = "1.18.0" # Thread-safe lazy initialized global variables
# 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
+123 -20
View File
@@ -6,8 +6,12 @@ A cross-platform application for customizing your cursor with gameified logic.
- Replace the system cursor with custom PNG images - Replace the system cursor with custom PNG images
- Minimize to system tray for background operation - Minimize to system tray for background operation
- Cross-platform support (Windows, macOS, Linux) - Windows support (macOS and Linux support in development)
- Steam integration - Optimized rendering with low CPU usage
- System tray integration with context menu
- Global hotkey support
- Steam integration for item drops
- Tauri-based frontend with React
## Project Structure ## Project Structure
@@ -28,12 +32,19 @@ src/
│ │ ├── mod.rs # Windows module exports │ │ ├── mod.rs # Windows module exports
│ │ ├── types.rs # Windows type definitions │ │ ├── types.rs # Windows type definitions
│ │ ├── state.rs # Windows state management │ │ ├── state.rs # Windows state management
│ │ ├── cursor.rs # Windows cursor handling │ │ └── cursor.rs # Windows cursor handling
│ │ ├── window.rs # Windows window management │ ├── macos/ # macOS-specific code (in development)
│ │ └── hook.rs # Windows mouse hook handling │ │ ├── mod.rs # macOS module exports
│ ├── macos.rs # macOS-specific code │ │ ├── state.rs # macOS state management
│ ├── linux.rs # Linux-specific code │ │ └── cursor.rs # macOS cursor handling
│ └── unsupported.rs # Fallback for unsupported platforms │ ├── linux/ # Linux-specific code (in development)
│ │ ├── mod.rs # Linux module exports
│ │ ├── state.rs # Linux state management
│ │ └── cursor.rs # Linux cursor handling
│ └── unsupported/ # Fallback for unsupported platforms
│ ├── mod.rs # Unsupported module exports
│ ├── state.rs # Unsupported state management
│ └── cursor.rs # Unsupported cursor handling
├── integrations/ # External integrations ├── integrations/ # External integrations
│ ├── mod.rs # Integrations module exports │ ├── mod.rs # Integrations module exports
│ └── steam.rs # Steam integration │ └── steam.rs # Steam integration
@@ -50,34 +61,126 @@ src/
- Rust 1.70.0 or later - Rust 1.70.0 or later
- Cargo - Cargo
- Node.js 16.0.0 or later
- npm 7.0.0 or later
- Platform-specific dependencies:
- Windows: None (fully supported)
- macOS: Development in progress
- Linux: Development in progress
### Dependencies
The application uses the following major dependencies:
- Tauri 1.6.0 - Cross-platform framework
- React - Frontend framework
- image 0.24.7 - Image processing
- winapi 0.3.9 - Windows API bindings
- once_cell 1.19.0 - Lazy initialization
### Build Commands ### Build Commands
```bash ```bash
# Debug build # Navigate to the frontend directory
cargo build cd frontend
# Release build # Install npm dependencies
cargo build --release npm install
# Run in development mode
npm run tauri dev
# Build for production
npm run tauri build
``` ```
## Usage ## Usage
1. Place your cursor PNG files in the `assets` directory 1. Make sure you have Node.js and npm installed
2. Run the application 2. Navigate to the frontend directory: `cd frontend`
3. Select a cursor from the list 3. Install dependencies if you haven't already: `npm install`
4. Press Ctrl+Shift+F12 to exit the application 4. Run the application in development mode: `npm run tauri dev`
5. The application will start with a React frontend and Rust backend
6. Use the UI to browse and select cursors
7. The application will replace your system cursor with the selected image
8. To exit, use the "Quit" button in the application or from the system tray
### Custom Cursors
The application comes with sample cursors:
- `default.png` - A simple default cursor
- `icon.png` - An icon cursor
- `red.png` - A red-colored cursor
To add your own cursors:
1. Create PNG images with transparent backgrounds
2. Place them in the `frontend/src/cursors` directory
3. Restart the application to load the new cursors
4. The cursor name in the UI will be derived from the filename
## Hotkeys ## Hotkeys
- Ctrl+Shift+F12: Exit the application - Ctrl+Shift+F12: Exit the application (restores original system cursor)
- If this hotkey fails, the application will try to use Ctrl+Alt+F12 or F12 as fallbacks
## System Tray ## System Tray
- Left-click: Show the application window - Left-click: Show the application window
- Right-click: Open the context menu - Right-click: Open the context menu with the following options:
- Next cursor: Switch to the next cursor - Next cursor: Switch to the next cursor in the list
- Show window: Show the application window - Show window: Show the application window
- Quit: Exit the application - Quit: Exit the application and restore the original cursor
## Performance
- The application uses optimized rendering techniques to minimize CPU usage
- On Windows, it uses the Windows API to efficiently replace system cursors
- The application uses event-driven cursor updates instead of polling when possible
- Cursor images are cached to improve performance when switching between cursors
## Previously Fixed Issues
The following issues have been addressed in recent updates:
- Task Manager compatibility: The application now properly handles elevated applications
- System tray menu stability: Fixed issues with the tray menu disappearing when mousing over it
## Steam Integration
The application includes integration with the Steam API for future gameified features:
- Item drop functionality is implemented but not yet active
- Steam must be running for this functionality to work
## Project Structure (Tauri Version)
```
frontend/
├── src/ # React frontend code
│ ├── App.jsx # Main React component
│ ├── components/ # UI components
│ └── cursors/ # Cursor image files
├── src-tauri/ # Rust backend code
│ ├── src/ # Rust source code
│ │ ├── lib.rs # Main library code
│ │ ├── main.rs # Application entry point
│ │ └── platform/ # Platform-specific code
│ │ ├── mod.rs # Platform module exports
│ │ ├── windows/ # Windows-specific code
│ │ ├── macos/ # macOS-specific code
│ │ └── linux/ # Linux-specific code
│ └── Cargo.toml # Rust dependencies
└── package.json # npm dependencies
```
## Contributing
Contributions to Cursor Rush are welcome! Here are some ways you can contribute:
- Report bugs and issues
- Suggest new features or improvements
- Submit pull requests with bug fixes or new features
- Help with documentation
- Contribute to macOS and Linux platform support
- Improve the Tauri integration
## License
This project is currently in development and does not yet have a specified license. A license will be added in a future update.
+24
View File
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+12
View File
@@ -0,0 +1,12 @@
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
+33
View File
@@ -0,0 +1,33 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
export default [
{ ignores: ['dist'] },
{
files: ['**/*.{js,jsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...js.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
]
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
+3758
View File
File diff suppressed because it is too large Load Diff
+33
View File
@@ -0,0 +1,33 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview",
"tauri": "tauri"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@tauri-apps/api": "^1.6.0",
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
"devDependencies": {
"@eslint/js": "^9.25.0",
"@tauri-apps/cli": "^1.6.3",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.4.1",
"eslint": "^9.25.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"vite": "^6.3.5"
}
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+3
View File
@@ -0,0 +1,3 @@
# Generated by Cargo
# will have compiled files and executables
/target/
+27
View File
@@ -0,0 +1,27 @@
[package]
name = "cursor-rush"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
license = ""
repository = ""
edition = "2021"
rust-version = "1.56"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "1.5.2", features = [] }
[dependencies]
tauri = { version = "1.6.0", features = [ "window-start-dragging", "shell-open"] }
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
image = "0.24"
# Ensure "consoleapi" is in this features list
winapi = { version = "0.3.9", features = ["winuser", "libloaderapi", "wingdi", "errhandlingapi", "consoleapi"] }
once_cell = "1.19.0"
[features]
# this feature is used for production builds or when `devPath` points to the filesystem
# DO NOT REMOVE!!
custom-protocol = ["tauri/custom-protocol"]
+3
View File
@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 B

+294
View File
@@ -0,0 +1,294 @@
use std::sync::Mutex;
use serde::{Serialize, Deserialize};
use tauri::{Manager, AppHandle};
use std::path::{Path, PathBuf};
use std::env;
mod platform;
// Define cursor structure
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Cursor {
name: String,
path: String,
}
// App state to track cursors
pub struct AppState {
cursors: Vec<Cursor>,
current_index: usize,
}
// Thread-safe app state wrapper
pub struct AppStateWrapper(Mutex<AppState>);
// Command to get all cursors
#[tauri::command]
fn get_cursors(state: tauri::State<AppStateWrapper>) -> Vec<Cursor> {
let app_state = state.0.lock().unwrap();
app_state.cursors.clone()
}
// Command to get current cursor index
#[tauri::command]
fn get_current_cursor_index(state: tauri::State<AppStateWrapper>) -> usize {
let app_state = state.0.lock().unwrap();
app_state.current_index
}
// Command to select a cursor
#[tauri::command]
fn select_cursor(index: usize, state: tauri::State<AppStateWrapper>, app_handle: AppHandle) -> Result<(), String> {
println!("=== RESOURCE DEBUG [select_cursor] ===");
let mut app_state = state.0.lock().unwrap();
if index < app_state.cursors.len() {
app_state.current_index = index;
let cursor = &app_state.cursors[index];
println!("Cursor selected: {}", cursor.name);
println!("Original path: {}", cursor.path);
// Get the resource directory
let resource_dir = app_handle.path_resolver().resource_dir();
println!("Resource dir: {:?}", resource_dir);
// List contents of resource dir
if let Some(res_dir) = &resource_dir {
println!("Listing contents of resource dir: {}", res_dir.display());
if let Ok(entries) = std::fs::read_dir(res_dir) {
for (i, entry) in entries.filter_map(Result::ok).enumerate() {
println!(" {}: {}", i, entry.path().display());
// List subdirectories
if entry.path().is_dir() {
if let Ok(subentries) = std::fs::read_dir(entry.path()) {
for (j, subentry) in subentries.filter_map(Result::ok).enumerate() {
println!(" {}.{}: {}", i, j, subentry.path().display());
}
}
}
}
} else {
println!("Failed to read resource directory");
}
}
let filename = Path::new(&cursor.path)
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| format!("Invalid cursor path: {}", cursor.path))?;
println!("Extracted filename: {}", filename);
// Try using the actual file path directly
println!("Using direct file path: {}", cursor.path);
// Verify the file exists
if Path::new(&cursor.path).exists() {
println!("File exists, applying cursor...");
if let Err(e) = platform::set_system_cursor(&cursor.path) {
println!("Error applying cursor: {}", e);
return Err(format!("Failed to apply cursor '{}': {}", cursor.name, e));
}
// Emit an event to notify the frontend
let _ = app_handle.emit_all("cursor_changed", index); // Ignore error
println!("Successfully applied cursor using actual file path");
return Ok(());
} else {
println!("File doesn't exist: {}", cursor.path);
return Err(format!("Cursor file does not exist: {}", cursor.path));
}
} else {
Err("Index out of bounds".to_string())
}
}
// Command to go to the next cursor
#[tauri::command]
fn next_cursor(state: tauri::State<AppStateWrapper>, app_handle: AppHandle) -> Result<usize, String> {
println!("=== RESOURCE DEBUG [next_cursor] ===");
let mut app_state = state.0.lock().unwrap();
if !app_state.cursors.is_empty() {
app_state.current_index = (app_state.current_index + 1) % app_state.cursors.len();
let cursor = &app_state.cursors[app_state.current_index];
println!("Next cursor: {} ({})", cursor.name, cursor.path);
let filename = Path::new(&cursor.path)
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| format!("Invalid cursor path: {}", cursor.path))?;
// Try both resource paths like in select_cursor
println!("Extracted filename: {}", filename);
// Try using the actual file path directly
println!("Using direct file path: {}", cursor.path);
// Verify the file exists
if Path::new(&cursor.path).exists() {
println!("File exists, applying cursor...");
if let Err(e) = platform::set_system_cursor(&cursor.path) {
println!("Error applying cursor: {}", e);
return Err(format!("Failed to apply cursor '{}': {}", cursor.name, e));
}
// Emit an event to notify the frontend
let _ = app_handle.emit_all("cursor_changed", app_state.current_index); // Ignore error
println!("Successfully applied cursor using actual file path");
return Ok(app_state.current_index);
} else {
println!("File doesn't exist: {}", cursor.path);
return Err(format!("Cursor file does not exist: {}", cursor.path));
}
} else {
Err("No cursors available".to_string())
}
}
// Command to quit the application
#[tauri::command]
fn quit_app(app_handle: AppHandle) {
// Clean up and restore default cursor before exit
match platform::restore_system_cursor() {
Ok(_) => println!("Default cursor restored"),
Err(e) => eprintln!("Failed to restore default cursor: {}", e),
}
// Exit the application
std::thread::spawn(move || {
app_handle.exit(0);
});
}
// Function to load cursor files from src/cursors directory
fn load_cursor_files() -> Vec<Cursor> {
println!("=== RESOURCE DEBUG [load_cursor_files] ===");
let mut cursors = Vec::new();
// Determine the path to src/cursors relative to src-tauri (CARGO_MANIFEST_DIR)
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
println!("CARGO_MANIFEST_DIR: {}", manifest_dir.display());
// Check multiple possible locations
let possible_cursor_dirs = vec![
manifest_dir.join("../src/cursors"),
manifest_dir.join("src/cursors"),
manifest_dir.join("cursors"),
manifest_dir.clone(), // Check manifest dir itself
];
println!("CHECKING FOR PNG/CUR FILES IN MULTIPLE DIRECTORIES:");
for (i, cursors_dir) in possible_cursor_dirs.iter().enumerate() {
println!("Checking dir {}: {}", i+1, cursors_dir.display());
println!(" Directory exists: {}", cursors_dir.exists());
if cursors_dir.exists() {
println!(" Looking for cursor files in this directory...");
if let Ok(entries) = std::fs::read_dir(cursors_dir) {
for entry in entries.filter_map(Result::ok) {
let path = entry.path();
println!(" Found: {}", path.display());
if path.is_file() && path.extension().map_or(false, |ext| ext == "png" || ext == "cur") {
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(Cursor {
name,
path: path.to_string_lossy().to_string(),
});
}
}
}
if !cursors.is_empty() {
println!(" Found {} cursor files in this directory.", cursors.len());
break;
}
}
}
// If we found no cursors, use mock data with paths relative to what resolve_resource expects
if cursors.is_empty() {
println!("No cursors found in any directory. Using mock data instead.");
println!("IMPORTANT: This is probably the source of your problem!");
println!("The app could not find your cursor PNG files. Check that they exist in one of these directories:");
for dir in &possible_cursor_dirs {
println!(" {}", dir.display());
}
// Mock data paths should reflect their eventual resource path structure
cursors.push(Cursor {
name: "Default".to_string(),
path: "src/cursors/default.png".to_string(), // Filename will be 'default.png'
});
cursors.push(Cursor {
name: "Icon".to_string(),
path: "src/cursors/icon.png".to_string(), // Filename will be 'icon.png'
});
cursors.push(Cursor {
name: "Red".to_string(),
path: "src/cursors/red.png".to_string(), // Filename will be 'red.png'
});
println!("Created {} mock cursor entries", cursors.len());
}
println!("Loaded {} cursor files", cursors.len());
// Sort cursors by name for consistent ordering
cursors.sort_by(|a, b| a.name.cmp(&b.name));
cursors
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
println!("Starting Cursor Rush application");
// Initialize platform-specific cursor handling
if let Err(e) = platform::initialize() {
eprintln!("Failed to initialize platform: {}", e);
}
// Load cursors from assets directory
let cursors = load_cursor_files();
// Create app state
let app_state = AppState {
cursors,
current_index: 0,
};
// This is one of the simplest ways to initialize Tauri
tauri::Builder::default()
.manage(AppStateWrapper(Mutex::new(app_state)))
.invoke_handler(tauri::generate_handler![
get_cursors,
get_current_cursor_index,
select_cursor,
next_cursor,
quit_app
])
.setup(|_app| {
// Register for events or perform additional setup
println!("Tauri application setup complete");
Ok(())
})
// This matches exactly how it was before our changes
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
+6
View File
@@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
cursor_rush::run() // Corrected line
}
+20
View File
@@ -0,0 +1,20 @@
// Linux platform implementation for cursor handling
use super::Result;
// Initialize cursor handling
pub fn init() -> Result<()> {
println!("Linux cursor initialization not yet implemented");
Ok(()) // Return OK for now to not block platform-agnostic code
}
pub fn set_cursor(path: &str) -> Result<()> {
// Linux cursor implementation would go here
println!("Linux cursor setting not yet implemented for path: {}", path);
Ok(()) // Return OK for now to not block platform-agnostic code
}
pub fn restore_default_cursor() -> Result<()> {
// Linux cursor restoration would go here
println!("Linux cursor restoration not yet implemented");
Ok(()) // Return OK for now to not block platform-agnostic code
}
+20
View File
@@ -0,0 +1,20 @@
// macOS platform implementation for cursor handling
use super::Result;
// Initialize cursor handling
pub fn init() -> Result<()> {
println!("macOS cursor initialization not yet implemented");
Ok(()) // Return OK for now to not block platform-agnostic code
}
pub fn set_cursor(path: &str) -> Result<()> {
// macOS cursor implementation would go here
println!("macOS cursor setting not yet implemented for path: {}", path);
Ok(()) // Return OK for now to not block platform-agnostic code
}
pub fn restore_default_cursor() -> Result<()> {
// macOS cursor restoration would go here
println!("macOS cursor restoration not yet implemented");
Ok(()) // Return OK for now to not block platform-agnostic code
}
+64
View File
@@ -0,0 +1,64 @@
// Platform-specific cursor handling
// Common types for cursor handling
pub mod types;
// Windows implementation
#[cfg(target_os = "windows")]
mod windows;
// macOS implementation
#[cfg(target_os = "macos")]
mod macos;
// Linux implementation
#[cfg(target_os = "linux")]
mod linux;
// Error type for platform operations
pub type Result<T> = std::result::Result<T, String>;
// Platform-agnostic interface for cursor handling
#[cfg(target_os = "windows")]
pub use self::windows::{set_cursor, restore_default_cursor, init};
#[cfg(target_os = "macos")]
pub use self::macos::{set_cursor, restore_default_cursor, init};
#[cfg(target_os = "linux")]
pub use self::linux::{set_cursor, restore_default_cursor, init};
// Initialize platform-specific cursor handling
pub fn initialize() -> Result<()> {
#[cfg(target_os = "windows")]
{
init()
}
#[cfg(not(target_os = "windows"))]
{
Ok(())
}
}
// Cross-platform implementation for setting cursor
pub fn set_system_cursor(path: &str) -> Result<()> {
println!("Setting system cursor: {}", path);
set_cursor(path)
}
// Cross-platform implementation for restoring default cursor
pub fn restore_system_cursor() -> Result<()> {
println!("Restoring system cursor");
restore_default_cursor()
}
// Default implementations for non-supported platforms
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
pub fn set_cursor(_path: &str) -> Result<()> {
Err("Cursor customization not supported on this platform".to_string())
}
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
pub fn restore_default_cursor() -> Result<()> {
Err("Cursor restoration not supported on this platform".to_string())
}
+26
View File
@@ -0,0 +1,26 @@
use std::path::PathBuf;
use image::RgbaImage;
// Windows-specific cursor handle wrapper
#[cfg(target_os = "windows")]
#[derive(Debug, Clone, Copy)]
pub struct WinCursorWrapper {
pub handle: winapi::shared::windef::HCURSOR,
}
#[cfg(target_os = "windows")]
unsafe impl Send for WinCursorWrapper {}
// Common type for cursor information
#[derive(Debug, Clone)]
pub struct CursorInfo {
pub name: String,
pub path: PathBuf,
pub image: Option<RgbaImage>,
pub hotspot_x: i32,
pub hotspot_y: i32,
// Platform-specific fields
#[cfg(target_os = "windows")]
pub win_cursor: Option<WinCursorWrapper>,
}
@@ -0,0 +1,512 @@
// Windows platform cursor management
use std::ptr::null_mut;
use std::sync::atomic::Ordering;
use winapi::{
um::winuser::{
GetDC, ReleaseDC, LoadCursorW, SetSystemCursor, CopyIcon,
SystemParametersInfoW, SPI_SETCURSORS, SetCursor,
MAKEINTRESOURCEW, IDC_ARROW, CreateIconIndirect, ICONINFO,
DestroyIcon,
},
um::wingdi::{
CreateCompatibleDC, DeleteDC, SelectObject, DeleteObject,
BITMAPINFO, BITMAPINFOHEADER, DIB_RGB_COLORS, BI_RGB, CreateDIBSection,
},
um::consoleapi::SetConsoleCtrlHandler,
shared::minwindef::{TRUE, FALSE, DWORD},
};
use ::image::{RgbaImage, open as image_open};
use super::types::*;
use super::state::*;
// Control handler function for console events (Ctrl+C, close, etc.)
extern "system" fn ctrl_handler(_ctrl_type: u32) -> i32 {
println!("Control event received, attempting cleanup...");
cleanup(); // Call cleanup logic
FALSE // Allow other handlers (like default exit) to run
}
// Create a cursor from an RGBA image
unsafe fn create_cursor_from_image(image: &RgbaImage) -> Option<SyncHCURSOR> {
let (width, height) = image.dimensions();
let width_i32 = width as i32;
let height_i32 = height as i32;
// Create device context and bitmap for the color data
let screen_dc = GetDC(null_mut());
if screen_dc.is_null() {
eprintln!("Failed to get screen DC");
return None;
}
// Create a compatible DC for our bitmap
let color_dc = CreateCompatibleDC(screen_dc);
if color_dc.is_null() {
eprintln!("Failed to create compatible DC for color");
ReleaseDC(null_mut(), screen_dc);
return None;
}
// Create a compatible DC for our mask bitmap
let mask_dc = CreateCompatibleDC(screen_dc);
if mask_dc.is_null() {
eprintln!("Failed to create compatible DC for mask");
DeleteDC(color_dc);
ReleaseDC(null_mut(), screen_dc);
return None;
}
// Set up BITMAPINFO structure for DIB
let mut bitmap_info: BITMAPINFO = std::mem::zeroed();
bitmap_info.bmiHeader.biSize = std::mem::size_of::<BITMAPINFOHEADER>() as u32;
bitmap_info.bmiHeader.biWidth = width_i32;
bitmap_info.bmiHeader.biHeight = -height_i32; // Negative for top-down DIB
bitmap_info.bmiHeader.biPlanes = 1;
bitmap_info.bmiHeader.biBitCount = 32; // 32 bits per pixel (BGRA)
bitmap_info.bmiHeader.biCompression = BI_RGB;
// Create the color DIB section
let mut color_bits_ptr: *mut winapi::ctypes::c_void = null_mut();
let mut ppv_bits_color: *mut winapi::ctypes::c_void = color_bits_ptr;
let color_bitmap = CreateDIBSection(
color_dc,
&bitmap_info,
DIB_RGB_COLORS,
&mut ppv_bits_color,
null_mut(),
0,
);
color_bits_ptr = ppv_bits_color;
if color_bitmap.is_null() || color_bits_ptr.is_null() {
eprintln!("Failed to create color DIB section");
DeleteDC(mask_dc);
DeleteDC(color_dc);
ReleaseDC(null_mut(), screen_dc);
return None;
}
// Create the mask DIB section (1-bit monochrome)
bitmap_info.bmiHeader.biBitCount = 1; // 1 bit per pixel for mask
let mut mask_bits_ptr: *mut winapi::ctypes::c_void = null_mut();
let mut ppv_bits: *mut winapi::ctypes::c_void = mask_bits_ptr;
let mask_bitmap = CreateDIBSection(
mask_dc,
&bitmap_info,
DIB_RGB_COLORS,
&mut ppv_bits,
null_mut(),
0,
);
mask_bits_ptr = ppv_bits;
if mask_bitmap.is_null() || mask_bits_ptr.is_null() {
eprintln!("Failed to create mask DIB section");
DeleteObject(color_bitmap as _);
DeleteDC(mask_dc);
DeleteDC(color_dc);
ReleaseDC(null_mut(), screen_dc);
return None;
}
// Select the bitmaps into the DCs
let old_color_bitmap = SelectObject(color_dc, color_bitmap as _);
let old_mask_bitmap = SelectObject(mask_dc, mask_bitmap as _);
if old_color_bitmap.is_null() || old_mask_bitmap.is_null() {
eprintln!("Failed to select bitmaps into DCs");
DeleteObject(mask_bitmap as _);
DeleteObject(color_bitmap as _);
DeleteDC(mask_dc);
DeleteDC(color_dc);
ReleaseDC(null_mut(), screen_dc);
return None;
}
// Copy the image data to the color DIB, converting RGBA to BGRA
let color_bits = std::slice::from_raw_parts_mut(color_bits_ptr as *mut u8, (width * height * 4) as usize);
// Calculate mask stride in bytes (1 bit per pixel, padded to DWORD)
let mask_stride = ((width + 31) / 32) * 4;
let mask_bits = std::slice::from_raw_parts_mut(mask_bits_ptr as *mut u8, (mask_stride * height) as usize);
// Clear mask bits (all transparent initially)
for i in 0..(mask_stride * height) as usize {
mask_bits[i] = 0xFF; // All bits set = transparent
}
// Process image pixels
for y in 0..height {
for x in 0..width {
let pixel = image.get_pixel(x, y);
let color_idx = ((y * width + x) * 4) as usize;
// Get RGBA components
let r = pixel[0];
let g = pixel[1];
let b = pixel[2];
let a = pixel[3];
// Set color data (BGR order)
color_bits[color_idx + 0] = b;
color_bits[color_idx + 1] = g;
color_bits[color_idx + 2] = r;
color_bits[color_idx + 3] = a;
// Update mask bit if pixel is not transparent
if a > 0 {
// Calculate bit position in mask
let byte_idx = (y * mask_stride + (x / 8)) as usize;
let bit_idx = 7 - (x % 8); // Bits are stored MSB first
// Clear the bit (0 = opaque in AND mask)
mask_bits[byte_idx] &= !(1 << bit_idx);
}
}
}
// Create ICONINFO structure
let mut icon_info: ICONINFO = std::mem::zeroed();
icon_info.fIcon = FALSE; // This is a cursor, not an icon
icon_info.xHotspot = width as DWORD / 2; // Center hotspot
icon_info.yHotspot = height as DWORD / 2;
icon_info.hbmMask = mask_bitmap;
icon_info.hbmColor = color_bitmap;
// Create the cursor
let cursor = CreateIconIndirect(&mut icon_info as *mut _);
// Clean up resources
SelectObject(color_dc, old_color_bitmap);
SelectObject(mask_dc, old_mask_bitmap);
DeleteDC(color_dc);
DeleteDC(mask_dc);
ReleaseDC(null_mut(), screen_dc);
// Windows makes a copy of the bitmaps for the cursor, so we can delete them
DeleteObject(color_bitmap as _);
DeleteObject(mask_bitmap as _);
if cursor.is_null() {
eprintln!("Failed to create cursor");
return None;
}
// Return the cursor wrapped in our thread-safe type
Some(SyncHCURSOR(cursor))
}
// Cleanup function to restore cursors and clean up resources
pub fn cleanup() {
println!("Running cursor cleanup...");
// Restore system cursors if they were hidden
// Use swap to ensure it runs only once
if CURSOR_HIDDEN.swap(false, Ordering::SeqCst) {
println!("Restoring system cursors...");
unsafe {
// Try multiple approaches to ensure cursors are restored
// 1. First try to restore original cursors if we saved them
let original_cursors_opt = ORIGINAL_CURSORS.lock().unwrap().take();
if let Some(original_cursors) = original_cursors_opt {
println!("Restoring {} saved original cursors", original_cursors.len());
for (cursor_id, cursor) in original_cursors {
if !cursor.0.is_null() {
// Make a copy of the original cursor
let cursor_copy = CopyIcon(cursor.0);
if !cursor_copy.is_null() {
if SetSystemCursor(cursor_copy, cursor_id) == 0 {
eprintln!("Failed to restore original cursor for ID: {}", cursor_id);
DestroyIcon(cursor_copy);
}
} else {
eprintln!("Failed to copy original cursor for ID: {}", cursor_id);
}
// Destroy the original cursor we saved
DestroyIcon(cursor.0);
}
}
} else {
println!("No saved original cursors found");
}
// 2. Force Windows to reload default cursor settings
println!("Forcing Windows to reload default cursor settings");
SystemParametersInfoW(SPI_SETCURSORS, 0, null_mut(), 0);
// 3. Also try the A version for maximum compatibility
super::state::reset_system_cursors();
// 4. Reset to default arrow cursor
let arrow_cursor = LoadCursorW(null_mut(), MAKEINTRESOURCEW(IDC_ARROW as u16));
if !arrow_cursor.is_null() {
SetCursor(arrow_cursor);
println!("Reset to default arrow cursor.");
}
}
println!("System cursors restoration complete.");
}
// Destroy the custom cursor if it exists
let custom_cursor_to_destroy = CUSTOM_CURSOR.lock().unwrap().take();
if let Some(cursor_wrapper) = custom_cursor_to_destroy {
unsafe {
if !cursor_wrapper.0.is_null() {
DestroyIcon(cursor_wrapper.0);
println!("Custom cursor destroyed during cleanup.");
}
}
}
// Reset the last known cursor position
*LAST_CURSOR_POS.lock().unwrap() = None;
// Clear the current cursor image
*CURRENT_CURSOR_IMAGE.lock().unwrap() = None;
// Unregister control handler
if CTRL_HANDLER_REGISTERED.load(Ordering::SeqCst) {
unsafe {
if SetConsoleCtrlHandler(Some(ctrl_handler), FALSE) != 0 {
CTRL_HANDLER_REGISTERED.store(false, Ordering::SeqCst);
println!("Control handler unregistered.");
} else {
eprintln!("Failed to unregister control handler.");
}
}
}
println!("Cursor cleanup complete.");
}
// --- Public API Functions ---
// Hide the system cursor - now just initializes the control handler
pub fn hide_system_cursor() -> bool {
// Initialize once
INIT.get_or_init(|| {
// Register control handler for clean exit
unsafe {
if SetConsoleCtrlHandler(Some(ctrl_handler), TRUE) != 0 {
CTRL_HANDLER_REGISTERED.store(true, Ordering::SeqCst);
println!("Control handler registered for clean exit.");
} else {
eprintln!("Failed to register control handler: {}", std::io::Error::last_os_error());
}
}
});
// Check if already hidden
if CURSOR_HIDDEN.load(Ordering::SeqCst) {
return true; // Already hidden
}
// Save original cursors for restoration
let mut original_cursors = Vec::new();
unsafe {
for &cursor_id in SYSTEM_CURSORS.iter() {
// Get the original cursor
let original_cursor = CopyIcon(LoadCursorW(null_mut(), MAKEINTRESOURCEW(cursor_id as u16)));
if !original_cursor.is_null() {
// Save the original cursor for restoration
original_cursors.push((cursor_id, SyncHCURSOR(original_cursor)));
}
}
}
// Store original cursors for restoration
if !original_cursors.is_empty() {
*ORIGINAL_CURSORS.lock().unwrap() = Some(original_cursors);
}
// Mark as hidden - this is now just a flag to indicate we've saved the original cursors
CURSOR_HIDDEN.store(true, Ordering::SeqCst);
true
}
// Update the cursor image using CursorInfo (with caching)
pub fn update_cursor_info(cursor_info: &mut crate::platform::types::CursorInfo) -> bool {
println!("Updating cursor to: {}", cursor_info.name);
// Use cached image if available, otherwise load from disk
let image: image::RgbaImage = if let Some(ref cached_image) = cursor_info.image {
println!("Using cached image for cursor: {}", cursor_info.name);
let img_clone: image::RgbaImage = cached_image.clone();
img_clone
} else {
println!("Loading image from disk for cursor: {}", cursor_info.name);
match image_open(&cursor_info.path) {
Ok(img) => {
let rgba = img.to_rgba8();
// Cache the image for future use
// Make sure to specify the type to avoid type inference issues
let image_copy: image::RgbaImage = rgba.clone();
cursor_info.image = Some(image_copy);
rgba
},
Err(e) => {
eprintln!("Failed to load cursor image: {}", e);
return false;
}
}
};
// Store the image in the global state for reference
*CURRENT_CURSOR_IMAGE.lock().unwrap() = Some(image.clone());
unsafe {
// Create a new cursor first before destroying the old one
// This minimizes the time when no custom cursor is active
let new_cursor = if let Some(ref cached_image) = cursor_info.image {
// Always create a fresh cursor from the image to avoid invalid handle issues
create_cursor_from_image(cached_image)
} else {
create_cursor_from_image(&image)
};
if let Some(new_cursor_wrapper) = new_cursor {
// Apply the new cursor to all system cursors first
let success = apply_cursor_to_all_system_cursors(new_cursor_wrapper.0);
// Now that the new cursor is applied, we can clean up the old one
let mut cursor_guard = CUSTOM_CURSOR.lock().unwrap();
if let Some(old_cursor) = cursor_guard.take() {
// Only destroy the old cursor after the new one is applied
DestroyIcon(old_cursor.0);
}
// Store the new cursor
*cursor_guard = Some(new_cursor_wrapper);
// Update the cached cursor in CursorInfo
#[cfg(windows)]
{
// Convert SyncHCURSOR to WinCursorWrapper for storage in cursor_info
cursor_info.win_cursor = Some(crate::platform::types::WinCursorWrapper {
handle: new_cursor_wrapper.0
});
}
if !success {
eprintln!("Failed to apply cursor to all system cursors");
// Even if we failed to set all system cursors, try to at least set the current cursor
SetCursor(new_cursor_wrapper.0);
}
return true;
} else {
eprintln!("Failed to create cursor from image");
// If we failed to create a new cursor, try to use the cached one as a fallback
#[cfg(windows)]
if let Some(cached_cursor) = &cursor_info.win_cursor {
if !cached_cursor.handle.is_null() {
println!("Falling back to cached cursor as a last resort");
let mut cursor_guard = CUSTOM_CURSOR.lock().unwrap();
// Convert WinCursorWrapper to SyncHCURSOR
let sync_cursor = super::types::SyncHCURSOR(cached_cursor.handle);
*cursor_guard = Some(sync_cursor);
SetCursor(cached_cursor.handle);
return true;
}
}
return false;
}
}
}
// Apply a cursor to all system cursors
unsafe fn apply_cursor_to_all_system_cursors(cursor: winapi::shared::windef::HCURSOR) -> bool {
let mut success = true;
// First, ensure the cursor is valid
if cursor.is_null() {
eprintln!("Cannot apply null cursor to system cursors");
return false;
}
// Immediately set the current cursor to minimize flashing
SetCursor(cursor);
// Apply the cursor to all system cursor types
// Start with the most commonly used cursors first
let priority_cursors = [
32512, // IDC_ARROW (most important)
32513, // IDC_IBEAM (text cursor)
32649, // IDC_HAND (links)
];
// Apply to priority cursors first
for &cursor_id in priority_cursors.iter() {
// Make a copy of the cursor
let cursor_copy = CopyIcon(cursor);
if cursor_copy.is_null() {
let error = std::io::Error::last_os_error();
eprintln!("Failed to copy cursor for priority ID: {} - Error: {}", cursor_id, error);
success = false;
continue;
}
// Set the system cursor
if SetSystemCursor(cursor_copy, cursor_id) == 0 {
let error = std::io::Error::last_os_error();
eprintln!("Failed to set cursor for priority ID: {} - Error: {}", cursor_id, error);
DestroyIcon(cursor_copy); // Clean up the copy if we failed to set it
success = false;
}
}
// Now apply to the remaining cursors
for &cursor_id in SYSTEM_CURSORS.iter() {
// Skip the priority cursors we already processed
if priority_cursors.contains(&cursor_id) {
continue;
}
// Make a copy of the cursor
let cursor_copy = CopyIcon(cursor);
if cursor_copy.is_null() {
// Don't log errors for non-priority cursors to reduce console spam
success = false;
continue;
}
// Set the system cursor
if SetSystemCursor(cursor_copy, cursor_id) == 0 {
// Don't log errors for non-priority cursors to reduce console spam
DestroyIcon(cursor_copy); // Clean up the copy if we failed to set it
success = false;
}
}
// If we had any failures, set the current cursor again as a fallback
if !success {
SetCursor(cursor);
}
success
}
// Restore the system cursor
pub fn restore_cursor() {
cleanup();
}
// Function to handle mouse movement and update the cursor
// Kept for potential future use but currently not needed with SetSystemCursor approach
#[allow(dead_code)]
pub fn handle_mouse_move() {
// With SetSystemCursor, we don't need to constantly update the cursor on mouse move
// This function is kept for API compatibility, but it's now a no-op
// System cursors are already set to our custom cursor
}
@@ -0,0 +1,45 @@
// Windows platform implementation module
pub mod types;
mod cursor;
mod state;
use std::path::PathBuf;
use crate::platform::Result;
// Functions expected by the platform-agnostic interface
pub fn set_cursor(path: &str) -> Result<()> {
println!("Windows platform: setting cursor to {}", path);
// Create a cursor info struct to pass to update_cursor_info
let mut cursor_info = crate::platform::types::CursorInfo {
name: path.split('\\').last().unwrap_or("unknown").to_string(),
path: PathBuf::from(path),
image: None,
hotspot_x: 0, // Default hotspot at top-left
hotspot_y: 0,
#[cfg(target_os = "windows")]
win_cursor: None,
};
// Use the existing update_cursor_info function to handle loading and applying the cursor
if !cursor::update_cursor_info(&mut cursor_info) {
return Err(format!("Failed to update cursor from {}", path));
}
Ok(())
}
pub fn restore_default_cursor() -> Result<()> {
println!("Windows platform: restoring default cursor");
cursor::restore_cursor();
Ok(())
}
pub fn init() -> Result<()> {
println!("Windows platform: initializing cursor system");
if !cursor::hide_system_cursor() {
return Err("Failed to initialize cursor system".to_string());
}
Ok(())
}
@@ -1,10 +1,9 @@
// Windows platform state management // Windows platform state management
use std::sync::{Mutex, atomic::{AtomicBool, AtomicPtr}}; use std::sync::{Mutex, atomic::AtomicBool};
use std::time::Instant;
use winapi::{ use winapi::{
shared::windef::POINT,
shared::minwindef::DWORD, shared::minwindef::DWORD,
shared::windef::POINT,
}; };
use ::image::RgbaImage; use ::image::RgbaImage;
use once_cell::sync::{Lazy, OnceCell}; use once_cell::sync::{Lazy, OnceCell};
@@ -23,16 +22,10 @@ pub static CURRENT_CURSOR_IMAGE: Lazy<Mutex<Option<RgbaImage>>> = Lazy::new(|| M
pub static LAST_CURSOR_POS: Lazy<Mutex<Option<POINT>>> = Lazy::new(|| Mutex::new(None)); pub static LAST_CURSOR_POS: Lazy<Mutex<Option<POINT>>> = Lazy::new(|| Mutex::new(None));
// Use Lazy<Mutex<Option<Handle>>> for WinAPI handles with our thread-safe wrappers // Use Lazy<Mutex<Option<Handle>>> for WinAPI handles with our thread-safe wrappers
pub static CURSOR_WINDOW: Lazy<Mutex<Option<SyncHWND>>> = Lazy::new(|| Mutex::new(None)); pub static CUSTOM_CURSOR: Lazy<Mutex<Option<SyncHCURSOR>>> = Lazy::new(|| Mutex::new(None));
pub static CURSOR_BITMAP: Lazy<Mutex<Option<SyncHBITMAP>>> = Lazy::new(|| Mutex::new(None));
pub static CURSOR_DC: Lazy<Mutex<Option<SyncHDC>>> = Lazy::new(|| Mutex::new(None));
// Mouse hook related variables // Note: Legacy variables for window-based cursor approach and mouse hook tracking
pub static MOUSE_HOOK_HANDLE: Lazy<Mutex<Option<SyncHHOOK>>> = Lazy::new(|| Mutex::new(None)); // have been removed as part of code cleanup
// This one is for CallNextHookEx to avoid locking inside the hook callback
pub static CURRENT_HOOK_FOR_CALLBACK: AtomicPtr<std::os::raw::c_void> =
AtomicPtr::new(std::ptr::null_mut());
pub static LAST_MOUSE_MOVE_TIME: Lazy<Mutex<Option<Instant>>> = Lazy::new(|| Mutex::new(None));
// Use OnceCell for INIT to avoid `static mut` // Use OnceCell for INIT to avoid `static mut`
pub static INIT: OnceCell<()> = OnceCell::new(); pub static INIT: OnceCell<()> = OnceCell::new();
@@ -0,0 +1,40 @@
// Windows platform types and constants
use winapi::{
shared::windef::HCURSOR,
shared::minwindef::DWORD,
};
// --- Thread-Safe Handle Wrappers ---
// These wrappers make raw Windows handles safe to share between threads
// Wrapper for HCURSOR (cursor handle)
#[derive(Copy, Clone, Debug)]
pub struct SyncHCURSOR(pub HCURSOR);
unsafe impl Send for SyncHCURSOR {}
unsafe impl Sync for SyncHCURSOR {}
// --- Constants ---
// Constants for SystemParametersInfo
pub const SPI_SETCURSORS_A: u32 = 0x0057;
pub const SPIF_UPDATEINIFILE: u32 = 0x01;
pub const SPIF_SENDCHANGE: u32 = 0x02;
// Note: Constants for window-based cursor approach have been removed as part of code cleanup
// Define all system cursor IDs we'll replace
pub 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
];
+60
View File
@@ -0,0 +1,60 @@
{
"build": {
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build",
"devPath": "http://localhost:5173",
"distDir": "../dist"
},
"package": {
"productName": "cursor-rush",
"version": "0.1.0"
},
"tauri": {
"allowlist": {
"all": false,
"window": {
"all": false,
"startDragging": true
},
"shell": {
"all": false,
"open": true
},
"fs": {
"all": false,
"scope": [
"$APP/cursors/*"
]
}
},
"bundle": {
"active": true,
"identifier": "com.cursor-rush.dev",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"targets": "all"
},
"security": {
"csp": null
},
"updater": {
"active": false
},
"windows": [
{
"title": "Cursor Rush",
"width": 310,
"height": 600,
"resizable": true,
"fullscreen": false,
"decorations": false,
"transparent": true
}
]
}
}
+102
View File
@@ -0,0 +1,102 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--bg-color: rgba(24, 24, 37, 0.85);
--nav-bg: rgba(40, 40, 56, 0.4);
--nav-active: rgba(65, 66, 88, 0.7);
--border-color: rgba(49, 50, 68, 0.6);
--text-color: #cdd6f4;
--accent-color: #89b4fa;
--button-bg: rgba(65, 66, 88, 0.6);
--button-hover: rgba(70, 71, 90, 0.8);
--button-active: rgba(137, 180, 250, 0.8);
--tab-bg: rgba(40, 40, 56, 0.4);
--tab-active: rgba(65, 66, 88, 0.7);
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: transparent;
color: var(--text-color);
overflow: hidden;
margin: 0;
padding: 0;
height: 100vh;
width: 100vw;
}
/* Modal Container - Draggable Area */
.modal-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
width: 100vw;
background-color: transparent;
}
/* Modal - Main App Window */
.modal {
width: 100%;
height: 100%;
background-color: var(--bg-color);
backdrop-filter: blur(10px);
border-radius: 8px;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
border: 1px solid var(--border-color);
}
/* Scrollbars */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--nav-bg);
}
::-webkit-scrollbar-thumb {
background: var(--nav-bg);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--nav-active);
}
/* Buttons */
button {
background-color: var(--button-bg);
color: var(--text-color);
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
}
button:hover {
background-color: var(--button-hover);
}
button:active {
background-color: var(--button-active);
color: var(--bg-color);
}
/* Images */
img {
max-width: 100%;
height: auto;
}
+123
View File
@@ -0,0 +1,123 @@
import { useState, useEffect } from "react";
import { invoke } from "@tauri-apps/api/tauri";
import { appWindow } from "@tauri-apps/api/window";
import "./App.css";
// Import cursor images directly
import defaultCursor from "./cursors/default.png";
import iconCursor from "./cursors/icon.png";
import redCursor from "./cursors/red.png";
// Components
import Header from "./components/Header";
import Content from "./components/Content";
import Footer from "./components/Footer";
function App() {
const [activePage, setActivePage] = useState("cursors");
const [cursors, setCursors] = useState([]);
const [currentCursorIndex, setCurrentCursorIndex] = useState(0);
// Load cursors on mount
useEffect(() => {
const loadCursors = async () => {
try {
// Get cursors from the Tauri backend
const loadedCursors = await invoke("get_cursors");
console.log("Loaded cursors:", loadedCursors);
setCursors(loadedCursors || []);
// Get current cursor index
const currentIndex = await invoke("get_current_cursor_index");
setCurrentCursorIndex(currentIndex);
} catch (error) {
console.error("Failed to load cursors:", error);
// Use the imported cursor images directly
setCursors([
{ name: "default", path: defaultCursor },
{ name: "icon", path: iconCursor },
{ name: "red", path: redCursor },
// We don't have blue and gold cursors imported since they don't exist yet
// These will be excluded for now
]);
setCurrentCursorIndex(0);
}
};
loadCursors();
}, []);
// Functions to handle cursor operations using Tauri backend
const selectCursor = async (index) => {
try {
console.log(`Selecting cursor at index ${index}`);
await invoke("select_cursor", { index });
setCurrentCursorIndex(index);
} catch (error) {
console.error("Failed to select cursor:", error);
}
};
const nextCursor = async () => {
if (cursors.length === 0) return;
try {
console.log('Switching to next cursor');
const newIndex = await invoke("next_cursor");
setCurrentCursorIndex(newIndex);
} catch (error) {
console.error("Failed to switch to next cursor:", error);
// Fallback if backend fails
const newIndex = (currentCursorIndex + 1) % cursors.length;
setCurrentCursorIndex(newIndex);
}
};
// Function to minimize app to tray
const minimizeToTray = async () => {
try {
console.log('Minimizing app to tray');
await appWindow.hide();
} catch (error) {
console.error("Failed to minimize to tray:", error);
}
};
// Function to exit app
const quitApp = async () => {
try {
console.log('Quitting app');
await invoke("quit_app");
} catch (error) {
console.error("Failed to quit app:", error);
}
};
return (
<div className="modal-container">
<div className="modal">
<Header
activePage={activePage}
setActivePage={setActivePage}
minimizeToTray={minimizeToTray}
quitApp={quitApp}
/>
<Content
activePage={activePage}
cursors={cursors}
currentCursorIndex={currentCursorIndex}
selectCursor={selectCursor}
nextCursor={nextCursor}
/>
<Footer
version="0.1.0"
status={cursors.length > 0 ? `${cursors.length} cursors loaded` : "No cursors found"}
/>
</div>
</div>
);
}
export default App;
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

+14
View File
@@ -0,0 +1,14 @@
/* Content Component Styles */
.app-content {
flex: 1;
overflow-y: auto;
padding: 1vh;
border-top: 2px solid rgba(49, 50, 68, 0.6);
}
.content-title {
font-size: 18px;
color: var(--accent-color, #89b4fa);
margin-bottom: 15px;
font-weight: 500;
}
+50
View File
@@ -0,0 +1,50 @@
import React from 'react';
import './Content.css';
// Import content modules
import Inventory from './content_modules/Inventory';
import Cursors from './content_modules/Cursors';
import Crates from './content_modules/Crates';
import Settings from './content_modules/Settings';
function Content({ activePage, cursors, currentCursorIndex, selectCursor, nextCursor }) {
// Render the appropriate content based on active page
const renderContent = () => {
switch (activePage) {
case 'inventory':
return <Inventory
cursors={cursors}
currentCursorIndex={currentCursorIndex}
selectCursor={selectCursor}
/>;
case 'cursors':
return (
<Cursors
cursors={cursors}
currentCursorIndex={currentCursorIndex}
selectCursor={selectCursor}
nextCursor={nextCursor}
/>
);
case 'crates':
return <Crates />;
case 'settings':
return <Settings />;
default:
return <Cursors
cursors={cursors}
currentCursorIndex={currentCursorIndex}
selectCursor={selectCursor}
nextCursor={nextCursor}
/>;
}
};
return (
<div className="app-content">
{renderContent()}
</div>
);
}
export default Content;
+21
View File
@@ -0,0 +1,21 @@
/* Footer Component Styles */
.app-footer {
width: 100%;
background-color: var(--footer-bg, #181825);
padding: 8px 15px;
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid var(--border-color, #313244);
font-size: 12px;
}
.app-status {
color: var(--text-color, #cdd6f4);
opacity: 0.8;
}
.app-version {
color: var(--text-color, #cdd6f4);
opacity: 0.6;
}
+17
View File
@@ -0,0 +1,17 @@
import React from 'react';
import './Footer.css';
function Footer({ version, status }) {
return (
<footer className="app-footer">
<div className="app-status">
{status}
</div>
<div className="app-version">
v{version}
</div>
</footer>
);
}
export default Footer;
+100
View File
@@ -0,0 +1,100 @@
/* Header Component Styles */
.app-header {
width: 100%;
user-select: none;
height: 40px;
position: relative;
z-index: 10;
background-color: transparent; /* Reverted to original */
/* cursor: move; */ /* Removed, should be handled by drag region or default */
}
.tabs-container {
display: flex;
align-items: center;
height: 100%;
position: relative;
padding-right: 10px;
background-color: transparent;
}
/* Chrome-like tabs */
.tab {
-webkit-app-region: no-drag; /* Override drag region for clickable tabs */
cursor: pointer; /* Ensure tabs have a pointer cursor */
height: 36px;
width: 42px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
font-size: 14px;
margin-right: 5px;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
color: var(--text-color);
background-color: transparent;
transition: all 0.2s ease;
cursor: pointer;
margin-top: 4px;
}
.tab:hover:not(.active):not(.drag-handle) {
background-color: var(--tab-bg);
transform: translateY(-2px);
}
.tab.active {
background-color: var(--tab-active);
color: var(--accent-color);
z-index: 1;
height: 40px;
margin-top: 0;
box-shadow: 0 0 10px rgba(137, 180, 250, 0.3);
}
/* Special drag handle tab */
.tab.drag-handle {
width: 30px;
background-color: transparent;
color: rgba(205, 214, 244, 0.5);
}
.tab.drag-handle:hover {
color: rgba(205, 214, 244, 0.8);
}
/* Window control buttons */
.window-controls {
-webkit-app-region: no-drag; /* Override drag region for buttons */
cursor: default; /* Keep default cursor for controls area */
display: flex;
gap: 8px;
margin-left: auto;
margin-right: 10px;
}
.minimize-button,
.close-button {
background: none;
border: none;
font-size: 16px;
line-height: 1;
color: var(--text-color);
cursor: pointer;
padding: 2px 8px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.minimize-button:hover,
.close-button:hover {
background-color: var(--button-hover);
}
.close-button:hover {
color: #f38ba8;
}
+54
View File
@@ -0,0 +1,54 @@
import React from 'react';
import './Header.css';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
faGripVertical,
faBoxOpen,
faMousePointer,
faBoxes,
faCog,
faBoxArchive,
faBox,
faWarehouse,
faGraduationCap,
faCubesStacked,
faBorderAll,
faBagShopping
} from '@fortawesome/free-solid-svg-icons';
function Header({ activePage, setActivePage, minimizeToTray, quitApp }) {
// Navigation buttons for each page with icons
const navItems = [
{ id: 'inventory', icon: faBagShopping, tooltip: 'Inventory' },
{ id: 'cursors', icon: faMousePointer, tooltip: 'Cursors' },
{ id: 'crates', icon: faBoxOpen, tooltip: 'Crates' },
{ id: 'settings', icon: faCog, tooltip: 'Settings' }
];
return (
<header className="app-header" data-tauri-drag-region>
<div className="tabs-container">
{navItems.map((item) => (
<div
key={item.id}
className={`tab ${activePage === item.id ? 'active' : ''}`}
onClick={() => setActivePage(item.id)}
title={item.tooltip}
>
<FontAwesomeIcon icon={item.icon} />
</div>
))}
<div className="window-controls">
<button className="minimize-button" onClick={minimizeToTray} title="Minimize to Tray">
_
</button>
<button className="close-button" onClick={quitApp} title="Quit Application">
×
</button>
</div>
</div>
</header>
);
}
export default Header;
@@ -0,0 +1,351 @@
/* Crates Module Styles */
.crates-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Current crate section (top part) */
.current-crate-section {
height: 60%;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
border-bottom: 2px solid rgba(49, 50, 68, 0.6);
padding: 10px;
}
.current-crate {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
background-color: rgba(30, 30, 46, 0.3);
border-radius: 8px;
padding: 15px;
border: 1px solid rgba(49, 50, 68, 0.4);
position: relative;
overflow: hidden;
}
/* Color accents for different crate types */
.current-crate::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 4px;
opacity: 0.8;
}
.current-crate.common::before { background-color: #a6adc8; }
.current-crate.uncommon::before { background-color: #a6e3a1; }
.current-crate.rare::before { background-color: #89b4fa; }
.current-crate.epic::before { background-color: #cba6f7; }
.current-crate.legendary::before {
background: linear-gradient(90deg, #f9e2af, #fab387, #f9e2af);
animation: shimmer 2s infinite;
}
@keyframes shimmer {
0% { background-position: -100px; }
100% { background-position: 100px; }
}
.crate-navigation {
display: flex;
align-items: center;
justify-content: space-between;
flex: 1;
}
.nav-button {
background-color: rgba(49, 50, 68, 0.6);
border: none;
border-radius: 50%;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-color);
cursor: pointer;
transition: all 0.2s;
}
.nav-button:hover {
background-color: rgba(65, 66, 88, 0.8);
transform: scale(1.1);
}
.nav-button:disabled {
opacity: 0.3;
cursor: not-allowed;
transform: none;
}
.crate-display {
display: flex;
align-items: center;
gap: 20px;
flex: 1;
padding: 0 20px;
}
.crate-image {
display: flex;
justify-content: center;
align-items: center;
width: 100px;
height: 100px;
background-color: rgba(24, 24, 37, 0.5);
border-radius: 12px;
transition: all 0.3s;
border: 1px solid rgba(49, 50, 68, 0.6);
}
.crate-icon {
font-size: 48px;
transition: all 0.3s;
}
/* Crate color styling */
.current-crate.common .crate-icon { color: #a6adc8; text-shadow: 0 0 10px rgba(166, 173, 200, 0.4); }
.current-crate.uncommon .crate-icon { color: #a6e3a1; text-shadow: 0 0 10px rgba(166, 227, 161, 0.4); }
.current-crate.rare .crate-icon { color: #89b4fa; text-shadow: 0 0 10px rgba(137, 180, 250, 0.4); }
.current-crate.epic .crate-icon { color: #cba6f7; text-shadow: 0 0 10px rgba(203, 166, 247, 0.4); }
.current-crate.legendary .crate-icon { color: #f9e2af; text-shadow: 0 0 15px rgba(249, 226, 175, 0.6); animation: pulse 2s infinite; }
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
/* Opening animation */
.crate-image.opening {
animation: shake 0.5s infinite;
}
@keyframes shake {
0% { transform: translateX(0); }
25% { transform: translateX(-5px); }
50% { transform: translateX(5px); }
75% { transform: translateX(-5px); }
100% { transform: translateX(0); }
}
.crate-info {
display: flex;
flex-direction: column;
flex: 1;
}
.crate-name {
font-size: 24px;
font-weight: 600;
margin: 0 0 5px 0;
}
.current-crate.common .crate-name { color: #a6adc8; }
.current-crate.uncommon .crate-name { color: #a6e3a1; }
.current-crate.rare .crate-name { color: #89b4fa; }
.current-crate.epic .crate-name { color: #cba6f7; }
.current-crate.legendary .crate-name { color: #f9e2af; }
.crate-description {
font-size: 14px;
opacity: 0.8;
margin: 0 0 10px 0;
}
.crate-stats {
font-size: 14px;
}
.crate-actions {
display: flex;
justify-content: center;
margin-top: 15px;
}
.open-button {
width: 100%;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 8px;
text-transform: uppercase;
letter-spacing: 0.5px;
min-width: 150px;
justify-content: center;
background-color: rgba(49, 50, 68, 0.7);
color: var(--text-color);
}
.open-button:hover:not(:disabled) {
transform: translateY(-3px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2);
}
.open-button:active:not(:disabled) {
transform: translateY(0);
}
.open-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Button colors by rarity */
.open-button.common:hover:not(:disabled) { background-color: #a6adc8; color: #1e1e2e; }
.open-button.uncommon:hover:not(:disabled) { background-color: #a6e3a1; color: #1e1e2e; }
.open-button.rare:hover:not(:disabled) { background-color: #89b4fa; color: #1e1e2e; }
.open-button.epic:hover:not(:disabled) { background-color: #cba6f7; color: #1e1e2e; }
.open-button.legendary:hover:not(:disabled) { background-color: #f9e2af; color: #1e1e2e; }
/* Crates inventory section (bottom part) */
.crates-inventory-section {
height: 40%;
display: flex;
flex-direction: column;
padding: 10px;
overflow-y: auto;
}
.inventory-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.inventory-header h3 {
font-size: 14px;
font-weight: 600;
margin: 0;
opacity: 0.9;
}
.total-crates {
font-size: 12px;
opacity: 0.7;
}
.crates-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 10px;
padding-bottom: 15px;
}
.crate-item {
background-color: rgba(49, 50, 68, 0.4);
border-radius: 8px;
padding: 10px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
border: 2px solid transparent;
position: relative;
gap: 8px;
}
.crate-item::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 3px;
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
}
/* Crate item colors */
.crate-item.common::after { background-color: #a6adc8; }
.crate-item.uncommon::after { background-color: #a6e3a1; }
.crate-item.rare::after { background-color: #89b4fa; }
.crate-item.epic::after { background-color: #cba6f7; }
.crate-item.legendary::after {
background: linear-gradient(90deg, #f9e2af, #fab387, #f9e2af);
background-size: 200% auto;
animation: shimmer 2s infinite;
}
.crate-item:hover:not(.empty) {
transform: translateY(-3px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
}
.crate-item.selected {
border-color: var(--accent-color);
background-color: rgba(49, 50, 68, 0.6);
box-shadow: 0 0 10px rgba(137, 180, 250, 0.4);
}
.crate-item.empty {
opacity: 0.4;
cursor: not-allowed;
}
.crate-item-icon {
font-size: 24px;
position: relative;
}
.crate-item.common .crate-item-icon { color: #a6adc8; }
.crate-item.uncommon .crate-item-icon { color: #a6e3a1; }
.crate-item.rare .crate-item-icon { color: #89b4fa; }
.crate-item.epic .crate-item-icon { color: #cba6f7; }
.crate-item.legendary .crate-item-icon { color: #f9e2af; }
.crate-count {
position: absolute;
top: -5px;
right: -10px;
background-color: rgba(30, 30, 46, 0.9);
border-radius: 50%;
width: 18px;
height: 18px;
font-size: 10px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid currentColor;
}
.crate-item-name {
font-size: 11px;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
/* Responsive adjustments */
@media (max-width: 500px) {
.crate-display {
flex-direction: column;
text-align: center;
gap: 10px;
}
.crate-info {
text-align: center;
}
}
@@ -0,0 +1,133 @@
import React, { useState } from 'react';
import './Crates.css';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faBox, faBoxOpen, faArrowRight, faArrowLeft, faUnlock } from '@fortawesome/free-solid-svg-icons';
function Crates() {
// Mock crate data - would come from backend
const crateTypes = [
{ id: 'common', name: 'Common Crate', count: 5, color: '#a6adc8', description: 'Contains common cursors' },
{ id: 'uncommon', name: 'Uncommon Crate', count: 3, color: '#a6e3a1', description: 'Contains uncommon and common cursors' },
{ id: 'rare', name: 'Rare Crate', count: 2, color: '#89b4fa', description: 'Contains rare and uncommon cursors' },
{ id: 'epic', name: 'Epic Crate', count: 1, color: '#cba6f7', description: 'Contains epic, rare and uncommon cursors' },
{ id: 'legendary', name: 'Legendary Crate', count: 0, color: '#f9e2af', description: 'Contains legendary and epic cursors' }
];
const [selectedCrateIndex, setSelectedCrateIndex] = useState(0);
const [isOpening, setIsOpening] = useState(false);
const selectedCrate = crateTypes[selectedCrateIndex];
// Navigate through available crates
const navigateCrate = (direction) => {
// Find next available crate with count > 0
let newIndex = selectedCrateIndex;
let attempts = 0;
while (attempts < crateTypes.length) {
newIndex = (newIndex + direction + crateTypes.length) % crateTypes.length;
if (crateTypes[newIndex].count > 0) {
break;
}
attempts++;
}
if (crateTypes[newIndex].count > 0) {
setSelectedCrateIndex(newIndex);
}
};
// Simulate opening a crate
const openCrate = () => {
if (selectedCrate.count <= 0) return;
setIsOpening(true);
// Simulate API call and animation time
setTimeout(() => {
setIsOpening(false);
}, 2000);
};
return (
<div className="crates-container">
{/* Current selected crate section */}
<div className="current-crate-section">
<div className={`current-crate ${selectedCrate.id}`}>
<div className="crate-navigation">
<button
className="nav-button"
onClick={() => navigateCrate(-1)}
disabled={crateTypes.filter(c => c.count > 0).length <= 1}
>
<FontAwesomeIcon icon={faArrowLeft} />
</button>
<div className="crate-display">
<div className={`crate-image ${isOpening ? 'opening' : ''}`}>
<FontAwesomeIcon
icon={isOpening ? faBoxOpen : faBox}
className="crate-icon"
/>
</div>
<div className="crate-info">
<h2 className="crate-name">{selectedCrate.name}</h2>
<div className="crate-stats">
<span>You have: <strong>{selectedCrate.count}</strong></span>
</div>
</div>
</div>
<button
className="nav-button"
onClick={() => navigateCrate(1)}
disabled={crateTypes.filter(c => c.count > 0).length <= 1}
>
<FontAwesomeIcon icon={faArrowRight} />
</button>
</div>
<div className="crate-actions">
<button
className={`open-button ${selectedCrate.id}`}
onClick={openCrate}
disabled={selectedCrate.count === 0 || isOpening}
>
{isOpening ? 'Opening...' : (
<>
<FontAwesomeIcon icon={faUnlock} /> Open Crate
</>
)}
</button>
</div>
</div>
</div>
{/* Crates inventory section */}
<div className="crates-inventory-section">
<div className="inventory-header">
<h3>Your Crates</h3>
<span className="total-crates">
{crateTypes.reduce((total, crate) => total + crate.count, 0)} total
</span>
</div>
<div className="crates-grid">
{crateTypes.map((crate, index) => (
<div
key={crate.id}
className={`crate-item ${crate.id} ${index === selectedCrateIndex ? 'selected' : ''} ${crate.count === 0 ? 'empty' : ''}`}
onClick={() => crate.count > 0 && setSelectedCrateIndex(index)}
>
<div className="crate-item-icon">
<FontAwesomeIcon icon={faBox} />
{crate.count > 1 && <span className="crate-count">{crate.count}</span>}
</div>
<div className="crate-item-name">{crate.name}</div>
</div>
))}
</div>
</div>
</div>
);
}
export default Crates;
@@ -0,0 +1,358 @@
/* Cursors Module Styles */
.cursors-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Equipped cursor banner */
.equipped-cursor-banner {
height: 25%;
min-height: 120px;
max-height: 150px;
display: flex;
background-color: rgba(30, 30, 46, 0.3);
border-bottom: 2px solid rgba(49, 50, 68, 0.6);
padding: 10px;
margin-bottom: 10px;
}
.current-preview {
max-width: 80%;
max-height: 80%;
object-fit: contain;
filter: drop-shadow(0 0 5px rgba(0, 0, 0, 0.3));
transition: all 0.3s ease;
}
.current-preview.legendary {
animation: float 3s ease-in-out infinite;
filter: drop-shadow(0 0 8px rgba(249, 226, 175, 0.4));
}
@keyframes float {
0% { transform: translateY(0px); }
50% { transform: translateY(-5px); }
100% { transform: translateY(0px); }
}
.equipped-cursor-details {
display: flex;
flex-direction: column;
justify-content: space-between;
flex: 1;
}
.equipped-cursor-info {
display: flex;
flex-direction: column;
}
.equipped-label {
font-size: 12px;
font-weight: 600;
color: rgba(205, 214, 244, 0.6);
letter-spacing: 1px;
margin-bottom: 4px;
}
.equipped-name {
font-size: 20px;
font-weight: 600;
margin-bottom: 5px;
}
.equipped-rarity {
font-size: 14px;
font-weight: 500;
opacity: 0.8;
}
.next-button {
align-self: flex-start;
background-color: rgba(65, 66, 88, 0.8);
border: none;
border-radius: 4px;
padding: 8px 15px;
color: var(--text-color);
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.next-button:hover {
background-color: var(--accent-color);
color: #1e1e2e;
transform: translateY(-2px);
}
/* Library controls section */
.cursors-controls {
display: flex;
flex-direction: column;
gap: 10px;
padding: 5px 10px 15px 10px;
border-bottom: 1px solid rgba(49, 50, 68, 0.4);
}
.search-box {
position: relative;
margin-bottom: 8px;
}
.search-icon {
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
color: rgba(205, 214, 244, 0.5);
pointer-events: none;
}
.search-input {
width: 100%;
background-color: rgba(49, 50, 68, 0.3);
border: 1px solid rgba(49, 50, 68, 0.6);
border-radius: 4px;
color: var(--text-color);
padding: 8px 10px 8px 32px;
font-size: 14px;
outline: none;
transition: all 0.2s;
}
.search-input:focus {
border-color: var(--accent-color);
box-shadow: 0 0 5px rgba(137, 180, 250, 0.3);
}
.filter-controls {
display: flex;
gap: 8px;
flex-wrap: wrap;
overflow-x: auto;
padding-bottom: 5px;
}
.filter-button {
background-color: rgba(49, 50, 68, 0.4);
border: none;
border-radius: 4px;
padding: 5px 10px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
border-left: 3px solid transparent;
}
.filter-button:hover {
background-color: rgba(65, 66, 88, 0.6);
}
.filter-button.active {
background-color: rgba(65, 66, 88, 0.8);
color: var(--accent-color);
}
/* Rarity-based styling for filter buttons */
.filter-button.common {
border-left-color: #a6adc8;
}
.filter-button.uncommon {
border-left-color: #a6e3a1;
}
.filter-button.rare {
border-left-color: #89b4fa;
}
.filter-button.epic {
border-left-color: #cba6f7;
}
.filter-button.legendary {
border-left-color: #f9e2af;
}
/* Main cursor library */
.cursor-library {
flex: 1;
display: flex;
flex-direction: column;
overflow-y: auto;
padding: 0 10px;
scrollbar-width: thin;
margin-top: 5px;
}
.rarity-section {
display: flex;
flex-direction: column;
margin-bottom: 20px;
}
.rarity-header {
display: flex;
align-items: center;
margin-bottom: 10px;
flex-wrap: nowrap;
}
.rarity-title {
font-size: 14px;
font-weight: 600;
margin: 0;
white-space: nowrap;
margin-right: 10px;
}
.rarity-line {
flex: 1;
height: 1px;
opacity: 0.3;
}
/* Rarity colors */
.equipped-name.common, .equipped-rarity.common, .rarity-line.common { background-color: #a6adc8; }
.equipped-name.uncommon, .equipped-rarity.uncommon, .rarity-line.uncommon { background-color: #a6e3a1; }
.equipped-name.rare, .equipped-rarity.rare, .rarity-line.rare { background-color: #89b4fa; }
.equipped-name.epic, .equipped-rarity.epic, .rarity-line.epic { background-color: #cba6f7; }
.equipped-name.legendary, .equipped-rarity.legendary, .rarity-line.legendary { background-color: #f9e2af; }
/* Horizontal scrolling cursor list */
.cursor-list {
display: flex;
gap: 12px;
overflow-x: auto;
padding: 5px 0 15px 0;
scrollbar-width: thin;
margin-bottom: 5px;
}
.cursor-card {
min-width: 150px;
max-width: 180px;
background-color: rgba(49, 50, 68, 0.4);
border-radius: 8px;
overflow: hidden;
transition: all 0.2s ease;
cursor: pointer;
border: 1px solid rgba(49, 50, 68, 0.4);
position: relative;
flex: 0 0 auto;
}
/* Circular rarity indicator in top right */
.cursor-card::before {
content: '';
position: absolute;
top: 8px;
right: 8px;
width: 12px;
height: 12px;
border-radius: 50%;
z-index: 5;
background-color: currentColor;
box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);
}
/* Rarity colors */
.cursor-card.common { color: #a6adc8; }
.cursor-card.uncommon { color: #a6e3a1; }
.cursor-card.rare { color: #89b4fa; }
.cursor-card.epic { color: #cba6f7; }
.cursor-card.legendary { color: #f9e2af; }
/* Hover and active states */
.cursor-card:hover {
transform: translateY(-3px) scale(1.02);
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.15);
border-color: currentColor;
z-index: 5;
}
.cursor-card:active {
transform: translateY(0) scale(0.98);
}
.cursor-card.active {
border-color: currentColor;
box-shadow: 0 0 12px currentColor;
}
/* Preview container */
.cursor-preview-container {
height: 100px;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(30, 30, 46, 0.5);
padding: 12px;
}
.cursor-preview-img {
max-width: 80%;
max-height: 80%;
object-fit: contain;
transition: transform 0.3s ease;
}
.cursor-card:hover .cursor-preview-img {
transform: scale(1.1);
}
/* Cursor details */
.cursor-details {
padding: 10px;
display: flex;
flex-direction: column;
gap: 3px;
}
.cursor-name {
font-weight: 500;
font-size: 13px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.cursor-rarity-badge {
display: inline-block;
font-size: 11px;
text-transform: capitalize;
opacity: 0.7;
}
/* Animation for legendary items */
.cursor-card.legendary::before {
animation: legendaryGlow 2s infinite;
}
@keyframes legendaryGlow {
0% { opacity: 0.4; }
50% { opacity: 0.9; }
100% { opacity: 0.4; }
}
/* Empty states */
.no-cursors, .no-results {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(49, 50, 68, 0.2);
border-radius: 8px;
padding: 20px;
text-align: center;
color: var(--text-color);
opacity: 0.7;
font-size: 14px;
}
@@ -0,0 +1,144 @@
import React, { useState } from 'react';
import './Cursors.css';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSort, faFilter, faSortAmountDown, faSearch } from '@fortawesome/free-solid-svg-icons';
function Cursors({ cursors, currentCursorIndex, selectCursor, nextCursor }) {
const [filter, setFilter] = useState('all');
const [searchTerm, setSearchTerm] = useState('');
const currentCursor = cursors.length > 0 && currentCursorIndex < cursors.length
? cursors[currentCursorIndex]
: null;
// This will eventually come from backend with proper data
const getRarity = (index) => {
const rarities = ['common', 'uncommon', 'rare', 'epic', 'legendary'];
return rarities[index % rarities.length];
};
// Group cursors by rarity
const cursorsByRarity = cursors.reduce((acc, cursor, index) => {
const rarity = getRarity(index);
if (!acc[rarity]) acc[rarity] = [];
acc[rarity].push({ ...cursor, index, rarity });
return acc;
}, {});
// Filter logic with search
const filteredCursors = Object.entries(cursorsByRarity)
.filter(([rarity, _]) => filter === 'all' || rarity === filter)
.map(([rarity, cursorsOfRarity]) => [
rarity,
cursorsOfRarity.filter(c =>
!searchTerm || c.name.toLowerCase().includes(searchTerm.toLowerCase())
)
])
.filter(([_, cursors]) => cursors.length > 0);
return (
<div className="cursors-container">
{/* Current cursor equipped display */}
{currentCursor && (
<div className="equipped-cursor-banner">
<div className="equipped-cursor-image">
<img
src={currentCursor.path}
alt={currentCursor.name}
className={`current-preview ${getRarity(currentCursorIndex)}`}
/>
</div>
<div className="equipped-cursor-details">
<div className="equipped-cursor-info">
<div className="equipped-label">EQUIPPED</div>
<div className={`equipped-name ${getRarity(currentCursorIndex)}`}>{currentCursor.name}</div>
<div className={`equipped-rarity ${getRarity(currentCursorIndex)}`}>
{getRarity(currentCursorIndex).toUpperCase()}
</div>
</div>
<button className="next-button" onClick={nextCursor}>Next Cursor</button>
</div>
</div>
)}
{/* Library controls */}
<div className="cursors-controls">
<div className="search-box">
<FontAwesomeIcon icon={faSearch} className="search-icon" />
<input
type="text"
placeholder="Search cursors..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="search-input"
/>
</div>
<div className="filter-controls">
<button
className={`filter-button ${filter === 'all' ? 'active' : ''}`}
onClick={() => setFilter('all')}
>
All
</button>
{Object.keys(cursorsByRarity).map(rarity => (
<button
key={rarity}
className={`filter-button ${filter === rarity ? 'active' : ''} ${rarity}`}
onClick={() => setFilter(rarity)}
>
{rarity.charAt(0).toUpperCase() + rarity.slice(1)}
</button>
))}
</div>
</div>
{/* Cursor library */}
{cursors.length > 0 ? (
<div className="cursor-library">
{filteredCursors.map(([rarity, cursorsInRarity]) => (
<div className="rarity-section" key={rarity}>
<div className={`rarity-header ${rarity}`}>
<h3 className="rarity-title">
{rarity.toUpperCase()} ({cursorsInRarity.length})
</h3>
<div className={`rarity-line ${rarity}`}></div>
</div>
<div className="cursor-list">
{cursorsInRarity.map((cursor) => (
<div
key={cursor.index}
className={`cursor-card ${cursor.index === currentCursorIndex ? 'active' : ''} ${cursor.rarity}`}
onClick={() => selectCursor(cursor.index)}
>
<div className="cursor-preview-container">
<img
src={cursor.path}
alt={cursor.name}
className="cursor-preview-img"
/>
</div>
<div className="cursor-details">
<span className="cursor-name">{cursor.name}</span>
<span className="cursor-rarity-badge">{cursor.rarity}</span>
</div>
</div>
))}
</div>
</div>
))}
{filteredCursors.length === 0 && (
<div className="no-results">No cursors match your search</div>
)}
</div>
) : (
<div className="no-cursors">
<p>No cursors found in assets directory</p>
</div>
)}
</div>
);
}
export default Cursors;
@@ -0,0 +1,359 @@
/* Inventory Module Styles */
.inventory-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
/* Search and filter controls */
.inventory-controls {
display: flex;
justify-content: space-between;
padding: 8px 10px;
background-color: rgba(49, 50, 68, 0.4);
border-radius: 8px;
margin-top: 2%;
border: 1px solid rgba(49, 50, 68, 0.8);
}
.search-bar {
flex: 1;
margin-right: 10px;
}
.search-bar input {
width: 100%;
padding: 8px 12px;
border-radius: 4px;
border: 1px solid rgba(49, 50, 68, 0.8);
background-color: rgba(30, 30, 46, 0.7);
color: #cdd6f4;
font-size: 14px;
}
.search-bar input:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 2px rgba(137, 180, 250, 0.3);
}
.search-bar input::placeholder {
color: rgba(205, 214, 244, 0.5);
}
.rarity-filter {
width: 50px;
position: relative;
display: flex;
align-items: center;
}
.rarity-filter select {
width: 100%;
padding: 8px;
border-radius: 4px;
border: 1px solid rgba(49, 50, 68, 0.8);
background-color: rgba(30, 30, 46, 0.7);
color: transparent;
text-indent: -9999px;
font-size: 14px;
appearance: none;
background-image: url("data:image/svg+xml;utf8,<svg fill='white' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'><path d='M7 10l5 5 5-5z'/><path d='M0 0h24v24H0z' fill='none'/></svg>");
background-repeat: no-repeat;
background-position: right 4px center;
cursor: pointer;
}
.rarity-filter .rarity-circle {
position: absolute;
aspect-ratio: 1/1;
width: 30%;
margin-left: 8px;
border-radius: 50%;
pointer-events: none;
z-index: 1;
}
/* Rarity colors for circle indicators */
.rarity-circle.all {
background: linear-gradient(45deg, #a6adc8, #a6e3a1, #89b4fa, #cba6f7, #f9e2af);
}
.rarity-circle.common { background-color: #a6adc8; }
.rarity-circle.uncommon { background-color: #a6e3a1; }
.rarity-circle.rare { background-color: #89b4fa; }
.rarity-circle.epic { background-color: #cba6f7; }
.rarity-circle.legendary {
background-color: #f9e2af;
box-shadow: 0 0 5px #f9e2af;
}
/* Style dropdown options to show text and circles */
.rarity-filter select option {
color: white;
position: relative;
}
.rarity-filter .rarity-option {
display: flex;
align-items: center;
gap: 8px;
color: white;
}
.rarity-filter select:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 2px rgba(137, 180, 250, 0.3);
}
/* Equipped cursor display */
.equipped-cursor {
display: flex;
background-color: rgba(49, 50, 68);
border-radius: 8px;
padding: 10px;
min-height: 25%;
max-height: 25%;
border: 1px solid rgba(49, 50, 68, 0.8);
margin-top: 10px;
}
.equipped-cursor-image {
aspect-ratio: 1/1;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(30, 30, 46, 0.5);
border-radius: 6px;
border: 1px solid rgba(49, 50, 68, 0.6);
margin-right: 10px;
}
.equipped-image {
max-width: 80%;
max-height: 80%;
object-fit: contain;
}
.equipped-cursor-info {
display: flex;
flex-direction: column;
}
.equipped-cursor-info h3 {
font-size: 14px;
margin: 0 0 3px 0;
opacity: 0.8;
}
.equipped-cursor-name {
font-size: 16px;
font-weight: 500;
}
.equipped-cursor-rarity {
font-size: 12px;
opacity: 0.8;
}
/* Minecraft-style inventory */
.cute-inventory {
min-height: 58%;
max-height: 58%;
display: flex;
overflow-y: auto;
flex-direction: column;
align-items: center;
background-color: rgba(30, 30, 46, 0.3);
border-radius: 8px;
box-sizing: border-box;
margin-top: 2%;
border: 1px solid rgba(49, 50, 68, 0.6);
}
.inventory-grid {
display: grid;
box-sizing: border-box;
width: 100%;
grid-template-columns: repeat(auto-fill, minmax(9vh, 1fr));
}
.inventory-slot {
aspect-ratio: 1;
background-color: rgba(49, 50, 68, 0.5);
border: 2px solid rgba(30, 30, 46, 0.8);
border-radius: 1vh;
display: flex;
align-items: center;
justify-content: center;
margin: 0.5vh;
position: relative;
transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
overflow: hidden;
}
/* Rarity indicator as small circle in top right */
.inventory-slot.has-item {
cursor: pointer;
position: relative;
border-color: rgba(30, 30, 46, 0.8);
}
/* Small circle rarity indicator in top right */
.inventory-slot.has-item::after {
content: '';
position: absolute;
top: 5px;
right: 5px;
width: 10px;
height: 10px;
border-radius: 50%;
z-index: 5;
}
.inventory-slot.has-item.common::after {
background-color: #a6adc8;
}
.inventory-slot.has-item.uncommon::after {
background-color: #a6e3a1;
}
.inventory-slot.has-item.rare::after {
background-color: #89b4fa;
}
.inventory-slot.has-item.epic::after {
background-color: #cba6f7;
}
.inventory-slot.has-item.legendary::after {
background-color: #f9e2af;
box-shadow: 0 0 5px rgba(249, 226, 175, 0.8);
animation: pulseDot 2s infinite;
}
@keyframes pulseDot {
0% { transform: scale(1); box-shadow: 0 0 5px rgba(249, 226, 175, 0.5); }
50% { transform: scale(1.2); box-shadow: 0 0 8px rgba(249, 226, 175, 0.8); }
100% { transform: scale(1); box-shadow: 0 0 5px rgba(249, 226, 175, 0.5); }
}
.inventory-slot.has-item:hover {
transform: scale(1.08);
z-index: 5;
}
.inventory-slot.has-item:active {
transform: scale(0.95);
}
/* Selected item overrides rarity glow */
.inventory-slot.selected {
border-color: var(--accent-color) !important;
box-shadow: 0 0 12px var(--accent-color) !important;
z-index: 10;
animation: selectedPulse 1.5s infinite;
}
@keyframes selectedPulse {
0% { box-shadow: 0 0 8px var(--accent-color) !important; }
50% { box-shadow: 0 0 15px var(--accent-color) !important; }
100% { box-shadow: 0 0 8px var(--accent-color) !important; }
}
/* Shine effect when clicked */
.inventory-slot.has-item:active::after {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0) 70%);
opacity: 0.6;
transform: scale(0);
animation: clickRipple 0.5s ease-out;
}
@keyframes clickRipple {
0% { transform: scale(0); opacity: 0.6; }
100% { transform: scale(1); opacity: 0; }
}
.inventory-item {
width: 70%;
height: 70%;
object-fit: contain;
}
/* Tooltip that appears on hover */
.inventory-tooltip {
position: absolute;
top: -40px;
left: 50%;
transform: translateX(-50%);
background-color: rgba(30, 30, 46, 0.95);
border-radius: 4px;
padding: 4px 8px;
min-width: 80px;
z-index: 10;
display: none;
flex-direction: column;
align-items: center;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
pointer-events: none;
white-space: nowrap;
}
.inventory-slot:hover .inventory-tooltip {
display: flex;
}
.tooltip-name {
font-size: 12px;
font-weight: 500;
}
.tooltip-rarity {
font-size: 10px;
opacity: 0.8;
}
/* Rarity colors */
.common, .common.equipped-cursor-name, .tooltip-name.common { color: #a6adc8; }
.uncommon, .uncommon.equipped-cursor-name, .tooltip-name.uncommon { color: #a6e3a1; }
.rare, .rare.equipped-cursor-name, .tooltip-name.rare { color: #89b4fa; }
.epic, .epic.equipped-cursor-name, .tooltip-name.epic { color: #cba6f7; }
.legendary, .legendary.equipped-cursor-name, .tooltip-name.legendary { color: #f9e2af; }
/* Legendary glow effect */
.inventory-slot.selected .inventory-item.legendary {
filter: drop-shadow(0 0 3px #f9e2af);
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { filter: drop-shadow(0 0 2px rgba(249, 226, 175, 0.5)); }
50% { filter: drop-shadow(0 0 4px rgba(249, 226, 175, 0.8)); }
100% { filter: drop-shadow(0 0 2px rgba(249, 226, 175, 0.5)); }
}
.inventory-footer {
width: 100%;
display: flex;
justify-content: center;
padding: 5px 0;
}
.cursor-count {
font-size: 12px;
color: var(--text-color);
opacity: 0.7;
}
@@ -0,0 +1,157 @@
import React, { useState, useEffect } from 'react';
import './Inventory.css';
function Inventory({ cursors, currentCursorIndex, selectCursor }) {
const [searchQuery, setSearchQuery] = useState('');
const [selectedRarity, setSelectedRarity] = useState('all');
const [filteredSlots, setFilteredSlots] = useState([]);
// Minecraft inventory typically has 27 slots (9x3)
const totalSlots = 36; // 9x4 grid
// This will eventually come from backend with proper data
const cursorRarities = {
common: 'Common',
uncommon: 'Uncommon',
rare: 'Rare',
epic: 'Epic',
legendary: 'Legendary'
};
// Assign mock rarities to cursors for demonstration
const getCursorRarity = (index) => {
const rarities = Object.keys(cursorRarities);
return rarities[index % rarities.length];
};
// Get currently selected cursor
const currentCursor = cursors.length > 0 && currentCursorIndex < cursors.length
? cursors[currentCursorIndex]
: null;
// Create array of slots for the inventory grid
const inventorySlots = Array(totalSlots).fill(null).map((_, slotIndex) => {
// Map cursor to slot if available
const cursorIndex = slotIndex < cursors.length ? slotIndex : null;
const cursor = cursorIndex !== null ? cursors[cursorIndex] : null;
const rarity = cursor ? getCursorRarity(cursorIndex) : null;
return {
slotIndex,
cursor,
cursorIndex,
rarity
};
});
// Apply filters to inventory slots
useEffect(() => {
// First, filter the slots that match our criteria
let filtered = inventorySlots.filter(slot => {
// Filter by search query
const matchesSearch = !searchQuery ||
(slot.cursor && slot.cursor.name.toLowerCase().includes(searchQuery.toLowerCase()));
// Filter by rarity
const matchesRarity = selectedRarity === 'all' ||
(slot.rarity && slot.rarity === selectedRarity);
return matchesSearch && matchesRarity;
});
// Create a new array for the filtered slots, prioritizing slots with items
const filledSlots = filtered.filter(slot => slot.cursor);
const emptySlots = Array(totalSlots - filledSlots.length).fill(null)
.map((_, index) => ({
slotIndex: filledSlots.length + index,
cursor: null,
cursorIndex: null,
rarity: null
}));
// Combine filled slots at the beginning and empty ones at the end
const result = [...filledSlots, ...emptySlots];
setFilteredSlots(result);
}, [searchQuery, selectedRarity, inventorySlots]);
return (
<div className="inventory-container">
{/* Search and Filter Controls */}
{/* Equipped cursor display */}
{currentCursor && (
<div className="equipped-cursor">
<div className="equipped-cursor-image">
<img
src={currentCursor.path}
alt={currentCursor.name}
className="equipped-image"
/>
</div>
<div className="equipped-cursor-info">
<h3>Equipped</h3>
<span className={`equipped-cursor-name ${getCursorRarity(currentCursorIndex)}`}>
{currentCursor.name}
</span>
<span className="equipped-cursor-rarity">
{cursorRarities[getCursorRarity(currentCursorIndex)]}
</span>
</div>
</div>
)}
<div className="inventory-controls">
<div className="search-bar">
<input
type="text"
placeholder="Search cursors..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<div className="rarity-filter">
<div className={`rarity-circle ${selectedRarity}`}></div>
<select
value={selectedRarity}
onChange={(e) => setSelectedRarity(e.target.value)}
aria-label="Filter by rarity"
>
<option value="all">All Rarities</option>
{Object.keys(cursorRarities).map(rarity => (
<option key={rarity} value={rarity}>{cursorRarities[rarity]}</option>
))}
</select>
</div>
</div>
{/* Minecraft-like inventory grid */}
<div className="cute-inventory">
<div className="inventory-grid">
{filteredSlots.map((slot) => (
<div
key={slot.slotIndex}
className={`inventory-slot ${slot.cursor ? 'has-item' : ''} ${slot.cursorIndex === currentCursorIndex ? 'selected' : ''} ${slot.rarity || ''}`}
onClick={() => slot.cursor && selectCursor(slot.cursorIndex)}
>
{slot.cursor && (
<>
<img
src={slot.cursor.path}
alt={slot.cursor.name}
className={`inventory-item ${slot.rarity}`}
/>
<div className="inventory-tooltip">
<span className={`tooltip-name ${slot.rarity}`}>{slot.cursor.name}</span>
<span className="tooltip-rarity">{cursorRarities[slot.rarity]}</span>
</div>
</>
)}
</div>
))}
</div>
</div>
</div>
);
}
export default Inventory;
@@ -0,0 +1,75 @@
/* Settings Module Styles */
.settings-container {
width: 100%;
display: flex;
flex-direction: column;
gap: 15px;
}
.settings-content {
display: flex;
flex-direction: column;
gap: 15px;
}
.settings-group {
background-color: var(--button-bg, #313244);
border-radius: 8px;
padding: 15px;
color: var(--text-color, #cdd6f4);
}
.settings-group h3 {
font-size: 16px;
font-weight: 500;
margin: 0 0 12px 0;
color: var(--accent-color, #89b4fa);
}
.setting-item {
padding: 8px 0;
display: flex;
align-items: center;
}
.setting-item label {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
font-size: 14px;
}
.setting-item input[type="checkbox"] {
width: 16px;
height: 16px;
margin: 0;
appearance: none;
background-color: var(--modal-bg, #181825);
border: 1px solid var(--text-color, #cdd6f4);
border-radius: 3px;
outline: none;
cursor: pointer;
position: relative;
}
.setting-item input[type="checkbox"]:checked {
background-color: var(--accent-color, #89b4fa);
border-color: var(--accent-color, #89b4fa);
}
.setting-item input[type="checkbox"]:checked::after {
content: '✓';
font-size: 12px;
color: var(--bg-color, #1e1e2e);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.settings-group p {
margin: 5px 0;
font-size: 14px;
opacity: 0.8;
}
@@ -0,0 +1,45 @@
import React from 'react';
import './Settings.css';
function Settings() {
return (
<div className="settings-container">
<h2 className="content-title">Settings</h2>
<div className="settings-content">
<div className="settings-group">
<h3>Application Settings</h3>
<div className="setting-item">
<label>
<input type="checkbox" />
Start with Windows
</label>
</div>
<div className="setting-item">
<label>
<input type="checkbox" checked />
Minimize to tray on close
</label>
</div>
</div>
<div className="settings-group">
<h3>Cursor Settings</h3>
<div className="setting-item">
<label>
<input type="checkbox" checked />
Auto-apply cursor on selection
</label>
</div>
</div>
<div className="settings-group">
<h3>About</h3>
<p>Cursor Rush v0.1.0</p>
<p>A cross-platform application for customizing your cursor.</p>
</div>
</div>
</div>
);
}
export default Settings;
Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 B

+68
View File
@@ -0,0 +1,68 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}
+9
View File
@@ -0,0 +1,9 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
+7
View File
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})
-88
View File
@@ -1,88 +0,0 @@
// Cursor management functionality
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
use ::image::open as image_open;
use ::image::GenericImageView;
use crate::platform;
// Define a struct to hold cursor information
#[derive(Clone)]
pub struct CursorInfo {
pub name: String,
pub path: PathBuf,
}
// Function to load all PNG files from the assets directory
pub 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
pub 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
pub fn apply_cursor(path: &Path) -> bool {
// Validate the cursor image
if validate_cursor_image(path) {
// Use platform-specific cursor image update (not hiding system cursor again)
if platform::update_cursor_image(path) {
println!("Successfully updated cursor image");
return true;
} else {
eprintln!("Failed to update cursor image");
}
}
false
}
-14
View File
@@ -1,14 +0,0 @@
// Application messages
// Messages that can be sent in our application
#[derive(Debug, Clone)]
pub enum Message {
NextCursor,
SelectCursor(usize),
Quit,
TrayEvent, // Used as a no-op message
CloseRequested(iced::window::Id), // Message for window close requests with the real window ID
ShowWindow, // Message to show the window from tray
WindowHidden, // Message indicating window was successfully hidden
WindowShown, // Message indicating window was successfully shown
}
-46
View File
@@ -1,46 +0,0 @@
// Application core module
// Contains the main application state and logic
mod state;
mod messages;
mod cursor;
pub use state::CursorRush;
pub use messages::Message;
pub use cursor::{CursorInfo, load_cursor_files, apply_cursor};
pub use state::SharedState;
use iced::{Subscription, Task};
use once_cell::sync::Lazy;
use iced::futures::channel::mpsc;
// Global channel for tray events
pub static TRAY_SENDER: Lazy<std::sync::Mutex<Option<mpsc::UnboundedSender<Message>>>> =
Lazy::new(|| std::sync::Mutex::new(None));
// Boot function to initialize the application
pub fn boot() -> (CursorRush, Task<Message>) {
state::boot_app()
}
// Update function to handle messages
pub fn update(state: &mut CursorRush, message: Message) -> Task<Message> {
state::update_app(state, message)
}
// Top-level subscription function to listen for events
pub fn subscription(_state: &CursorRush) -> Subscription<Message> {
state::app_subscription(_state)
}
// Helper function to hide a window to the tray
pub fn hide_window_to_tray(window_id: iced::window::Id) -> Task<Message> {
// Use ONLY the iced method for hiding. Platform specific might interfere.
let hide_task: Task<()> = iced::window::change_mode(window_id, iced::window::Mode::Hidden);
hide_task.map(|_: ()| {
println!("Window hide task completed (via iced)");
// Return a specific message to confirm the window was hidden
Message::WindowHidden
})
}
-420
View File
@@ -1,420 +0,0 @@
// Application state management
use std::{
sync::{Arc, Mutex},
thread,
time::Duration,
};
use ::image::{RgbaImage, open as image_open};
use iced::{Task, Subscription, window, event};
use iced::futures::channel::mpsc;
use global_hotkey::{
GlobalHotKeyManager,
hotkey::HotKey as GlobalHotKey,
HotKeyState,
};
// Update function to handle messages
pub fn update_app(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);
// No need to call render_cursor anymore as the mouse hook handles rendering
// 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);
// No need to call render_cursor anymore as the mouse hook handles rendering
// 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 => {
println!("Update: Handling Quit message");
// Set running flag to false to signal other threads (like hotkey listener)
if let Some(shared) = &state.shared_state {
if let Ok(mut shared_state) = shared.lock() {
shared_state.running = false;
}
}
// Restore the system cursor before exiting
platform::restore_cursor();
// Cleanly drop the tray icon *before* exiting iced
// This removes the icon from the system tray immediately
if let Some(tray) = state.tray_icon.take() {
drop(tray);
println!("Tray icon explicitly dropped.");
}
// Use iced's exit mechanism for a cleaner shutdown
println!("Requesting application exit via iced::exit...");
iced::exit()
},
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::CloseRequested(window_id) => {
println!("Handling CloseRequested: Hiding window with ID: {:?}", window_id);
// Store the real window ID for future use
// This is critical for showing the window later
state.main_window = Some(window_id);
println!("Stored window ID: {:?} for future use", window_id);
// Update shared state first
if let Some(shared) = &state.shared_state {
match shared.lock() {
Ok(mut shared_state) => {
if shared_state.window_visible { // Only hide if visible
shared_state.window_visible = false;
println!("Set window_visible to false in shared state");
println!("Hiding window with ID: {:?}, app continues running in tray", window_id);
return crate::app::hide_window_to_tray(window_id);
} else {
println!("Window already hidden, ignoring CloseRequested.");
return Task::none(); // Don't try to hide again
}
}
Err(e) => {
eprintln!("Error locking shared state for hiding: {}", e);
return Task::none();
}
}
} else {
eprintln!("Error: Shared state not available for hiding.");
return Task::none();
}
},
Message::WindowHidden => {
println!("Window successfully hidden to tray");
Task::none()
},
Message::WindowShown => {
println!("Update: Received WindowShown message. Window should now be visible.");
Task::none()
},
Message::ShowWindow => {
println!("Handling ShowWindow: Attempting to show window"); // More specific starting log
if let Some(window_id) = state.main_window {
println!("Using stored window ID: {:?}", window_id);
// It's generally safer to attempt the show operation even if the state *thinks* it's visible.
// Let's update the shared state *after* we know the task is being created.
println!("Creating show/focus tasks for window ID: {:?}", window_id);
let show_task = iced::window::change_mode(window_id, iced::window::Mode::Windowed);
let focus_task = iced::window::gain_focus(window_id);
// Update shared state *before* returning the task, but after creating it.
if let Some(shared) = &state.shared_state {
match shared.lock() {
Ok(mut shared_state) => {
if !shared_state.window_visible { // Only log if changing state
println!("Updating shared state: window_visible = true");
shared_state.window_visible = true;
} else {
println!("Shared state already indicates window is visible.");
}
}
Err(e) => {
eprintln!("Error locking shared state during show: {}", e);
// Proceed with showing the window anyway, state might be out of sync
}
}
} else {
eprintln!("Shared state unavailable during show.");
// Proceed with showing the window anyway
}
println!("Returning Task::batch to show and focus window.");
// Return the batch task
return Task::batch(vec![show_task, focus_task])
.map(|_: ()| {
// This message confirms the Iced runtime *completed* the tasks.
println!("Iced runtime reports show/focus tasks completed successfully.");
Message::WindowShown // Send confirmation message
});
} else {
eprintln!("Error: Main window ID not found when trying to show. Cannot create tasks.");
Task::none() // Cannot proceed without window_id
}
},
}
}
use crate::platform;
use crate::ui::tray::setup_tray_icon;
use crate::app::{Message, CursorInfo, load_cursor_files, apply_cursor, TRAY_SENDER};
// Shared state between the UI and the tray icon
pub struct SharedState {
pub current_index: usize,
pub cursors: Vec<CursorInfo>,
pub current_cursor_image: Option<RgbaImage>,
pub running: bool,
pub window_visible: bool, // Track if the window is visible
}
// Our application state
pub struct CursorRush {
pub cursors: Vec<CursorInfo>,
pub current_index: usize,
pub shared_state: Option<Arc<Mutex<SharedState>>>,
pub main_window: Option<iced::window::Id>, // Store the main window ID
pub tray_icon: Option<tray_icon::TrayIcon>, // Store the TrayIcon object to keep it alive
}
// Make sure the default impl initializes tray_icon to None
impl Default for CursorRush {
fn default() -> Self {
Self {
cursors: Vec::new(),
current_index: 0,
shared_state: None,
main_window: None,
tray_icon: None, // Initialize as None
}
}
}
// Top-level subscription function to listen for events
pub fn app_subscription(_state: &CursorRush) -> Subscription<Message> {
// Create a vector to hold all subscriptions
let mut subs = vec![
// Listen for events but filter out mouse movement events to avoid spamming
event::listen().map(|event| {
match event {
// Skip cursor movement events to avoid spamming
iced::Event::Mouse(iced::mouse::Event::CursorMoved { .. }) => {
// Return TrayEvent which is essentially a no-op
Message::TrayEvent
},
// Handle other events
_ => Message::TrayEvent, // Use TrayEvent as a no-op message
}
}),
// Specifically subscribe to window close requests
// This is the key to handling the X button properly
window::close_requests().map(|window_id| {
println!("Window close requested via X button for window ID: {:?}", window_id);
Message::CloseRequested(window_id)
}),
];
// Add a subscription for tray events
// We'll use a simple channel that the tray icon can send messages to
let tray_subscription = Subscription::run(
// Stream generator function
|| {
// Create a channel that the tray icon can send messages to
let (tx, rx) = mpsc::unbounded::<Message>();
// Store the sender in the global state
*TRAY_SENDER.lock().unwrap() = Some(tx);
// Return the receiver as a stream
rx
}
);
subs.push(tray_subscription);
// Combine all subscriptions
Subscription::batch(subs)
}
// Boot function implementation
pub fn boot_app() -> (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
};
// Hide the system cursor once at startup
if !platform::hide_system_cursor() {
eprintln!("Failed to hide system cursor at startup!");
}
// Apply the first cursor by default
if let Some(cursor) = cursors.first() {
apply_cursor(&cursor.path);
// No need to call render_cursor anymore as the mouse hook handles rendering
}
// Create shared state
let shared_state = Arc::new(Mutex::new(SharedState {
current_index: 0,
cursors: cursors.clone(),
current_cursor_image,
running: true,
window_visible: true, // Window is initially visible
}));
// Initialize the application state with loaded cursors
let mut state = CursorRush::default();
state.cursors = cursors.clone(); // Clone here
state.shared_state = Some(shared_state.clone());
// Set up the tray icon AND store the result in the state
state.tray_icon = setup_tray_icon(&state.cursors, shared_state.clone()); // Pass the cloned cursors
if state.tray_icon.is_none() {
eprintln!("Failed to initialize tray icon, application might not function correctly in background.");
}
setup_hotkeys(shared_state.clone());
(state, Task::none())
}
// Set up global hotkeys
fn setup_hotkeys(shared_state: Arc<Mutex<SharedState>>) {
// Set up global hotkey manager
let hotkey_manager = match GlobalHotKeyManager::new() {
Ok(manager) => manager,
Err(e) => {
eprintln!("Failed to create hotkey manager: {}", e);
// Continue without hotkey support
GlobalHotKeyManager::new().expect("Critical failure: could not 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,
);
// Register the hotkey with better error handling
match hotkey_manager.register(hotkey) {
Ok(_) => {
println!("Successfully registered Ctrl+Shift+F12 as exit hotkey");
},
Err(e) => {
eprintln!("Failed to register hotkey: {}", e);
// Try registering with a different hotkey as fallback
let fallback_hotkey = GlobalHotKey::new(
Some(Modifiers::CONTROL | Modifiers::ALT),
Code::F12,
);
match hotkey_manager.register(fallback_hotkey) {
Ok(_) => println!("Registered fallback hotkey Ctrl+Alt+F12 instead"),
Err(e) => eprintln!("Failed to register fallback hotkey: {}", e),
}
}
}
println!("Press Ctrl+Shift+F12 to exit the application");
// Spawn a thread to listen for hotkeys
let state_clone = shared_state.clone();
thread::spawn(move || {
println!("Hotkey listener thread started");
let event_receiver = global_hotkey::GlobalHotKeyEvent::receiver();
loop {
match event_receiver.try_recv() {
Ok(event) => {
println!("Received hotkey event: {:?}", event);
if event.state == HotKeyState::Pressed {
println!("Exit hotkey pressed, sending Quit message...");
// Send Quit message instead of direct exit
// Get the global sender
if let Some(tx) = TRAY_SENDER.lock().unwrap().as_ref() {
if let Err(e) = tx.unbounded_send(Message::Quit) {
eprintln!("Failed to send Quit message from hotkey thread: {}", e);
// Fallback to direct exit if channel fails
platform::restore_cursor();
std::process::exit(0);
}
} else {
eprintln!("Hotkey: Global sender not initialized yet");
// Fallback to direct exit if sender not available
platform::restore_cursor();
std::process::exit(0);
}
// Don't exit directly here, let the main thread handle it.
}
},
Err(_) => {
// No events or channel disconnected, sleep briefly
std::thread::sleep(Duration::from_millis(50));
}
}
// Check shared state running flag
if let Ok(state) = state_clone.lock() {
if !state.running {
println!("Hotkey listener detected running=false, exiting thread.");
break;
}
}
}
});
}
-3
View File
@@ -1,3 +0,0 @@
// External integrations module
pub mod steam;
-26
View File
@@ -1,26 +0,0 @@
// Steam integration
use steamworks::{
sys::{SteamAPI_ISteamInventory_TriggerItemDrop, SteamAPI_SteamInventory_v003},
AppId, Client, SingleClient,
};
#[allow(dead_code)]
pub struct Steam {
client: Client,
single: SingleClient,
}
impl Steam {
pub fn new() -> Self {
let (client, single) = Client::init_app(AppId(1430270)).unwrap();
Self { client, single }
}
pub fn drop_item(&self) {
unsafe {
let mut r = 0;
SteamAPI_ISteamInventory_TriggerItemDrop(SteamAPI_SteamInventory_v003(), &mut r, 1);
}
}
}
-21
View File
@@ -1,21 +0,0 @@
// Main entry point for the Cursor Rush application
mod app;
mod ui;
mod platform;
mod integrations;
fn main() -> iced::Result {
// Initialize Steam
let _steam = integrations::steam::Steam::new();
// Run the application
iced::application("Cursor Rush", app::update, ui::view_app)
.theme(|_| iced::Theme::Dark)
.subscription(app::subscription)
.centered()
// This is the key - we need to tell iced not to exit when the window close button is clicked
.exit_on_close_request(false)
// run_with expects a function that returns (State, Task), not the tuple directly
.run_with(app::boot)
}
-25
View File
@@ -1,25 +0,0 @@
// Linux cursor management
use std::path::Path;
// Hide the system cursor
pub fn hide_system_cursor() -> bool {
println!("Linux system cursor hiding not yet implemented");
false
}
// Update the cursor image
pub fn update_cursor_image(_path: &Path) -> bool {
println!("Linux cursor image update not yet implemented");
false
}
// Restore the original cursor
pub fn restore_cursor() {
println!("Linux cursor restoration not yet implemented");
}
// Render the cursor
pub fn render_cursor() {
// No-op
}
-7
View File
@@ -1,7 +0,0 @@
// Linux platform implementation module
mod cursor;
mod state;
// Re-export the public API
pub use cursor::{hide_system_cursor, update_cursor_image, restore_cursor, render_cursor};
-4
View File
@@ -1,4 +0,0 @@
// Linux platform state management
// This file will contain Linux-specific state management
// Currently a placeholder for future implementation
-25
View File
@@ -1,25 +0,0 @@
// macOS cursor management
use std::path::Path;
// Hide the system cursor
pub fn hide_system_cursor() -> bool {
println!("macOS system cursor hiding not yet implemented");
false
}
// Update the cursor image
pub fn update_cursor_image(_path: &Path) -> bool {
println!("macOS cursor image update not yet implemented");
false
}
// Restore the original cursor
pub fn restore_cursor() {
println!("macOS cursor restoration not yet implemented");
}
// Render the cursor
pub fn render_cursor() {
// No-op
}
-7
View File
@@ -1,7 +0,0 @@
// macOS platform implementation module
mod cursor;
mod state;
// Re-export the public API
pub use cursor::{hide_system_cursor, update_cursor_image, restore_cursor, render_cursor};
-4
View File
@@ -1,4 +0,0 @@
// macOS platform state management
// This file will contain macOS-specific state management
// Currently a placeholder for future implementation
-33
View File
@@ -1,33 +0,0 @@
// Platform-specific cursor handling
// Windows implementation
#[cfg(windows)]
mod windows;
// macOS implementation
#[cfg(target_os = "macos")]
mod macos;
// Linux implementation
#[cfg(target_os = "linux")]
mod linux;
// Unsupported platforms
#[cfg(not(any(windows, target_os = "macos", target_os = "linux")))]
mod unsupported;
// --- Public Exports ---
// Export the platform-specific functions
#[cfg(windows)]
pub use windows::{hide_system_cursor, update_cursor_image, restore_cursor};
#[cfg(target_os = "macos")]
pub use macos::{hide_system_cursor, update_cursor_image, restore_cursor, render_cursor};
#[cfg(target_os = "linux")]
pub use linux::{hide_system_cursor, update_cursor_image, restore_cursor, render_cursor};
// Default implementation for other platforms
#[cfg(not(any(windows, target_os = "macos", target_os = "linux")))]
pub use unsupported::{hide_system_cursor, update_cursor_image, restore_cursor, render_cursor};
-25
View File
@@ -1,25 +0,0 @@
// Unsupported platform cursor management
use std::path::Path;
// Hide the system cursor
pub fn hide_system_cursor() -> bool {
println!("System cursor hiding not supported");
false
}
// Update the cursor image
pub fn update_cursor_image(_path: &Path) -> bool {
println!("Cursor image update not supported");
false
}
// Restore the original cursor
pub fn restore_cursor() {
println!("Cursor restoration not supported");
}
// Render the cursor
pub fn render_cursor() {
// No-op
}
-7
View File
@@ -1,7 +0,0 @@
// Unsupported platform implementation module
mod cursor;
mod state;
// Re-export the public API
pub use cursor::{hide_system_cursor, update_cursor_image, restore_cursor, render_cursor};
-4
View File
@@ -1,4 +0,0 @@
// Unsupported platform state management
// This file will contain fallback state management for unsupported platforms
// Currently a placeholder for future implementation
-492
View File
@@ -1,492 +0,0 @@
// Windows platform cursor management
use std::path::Path;
use std::ptr::null_mut;
use std::sync::atomic::Ordering;
use winapi::{
um::winuser::{
GetDC, ReleaseDC, GetCursorPos, LoadCursorW, SetSystemCursor, CopyIcon,
CreateCursor, SystemParametersInfoW, SPI_SETCURSORS, UpdateLayeredWindow,
SetWindowPos, HWND_TOPMOST, SWP_NOSIZE, SWP_NOMOVE, SWP_NOACTIVATE,
MAKEINTRESOURCEW, IDC_ARROW, ULW_ALPHA, DestroyWindow,
},
um::wingdi::{
CreateCompatibleDC, DeleteDC, SelectObject, DeleteObject,
AC_SRC_OVER, AC_SRC_ALPHA, BITMAPINFO, BITMAPINFOHEADER,
DIB_RGB_COLORS, BI_RGB, CreateDIBSection,
},
um::consoleapi::SetConsoleCtrlHandler,
shared::minwindef::{TRUE, FALSE},
shared::windef::{POINT, SIZE},
};
use ::image::{RgbaImage, open as image_open};
use winapi::um::wingdi::BLENDFUNCTION;
use super::types::*;
use super::state::*;
use super::window::create_cursor_window;
use super::hook::{install_mouse_hook_if_needed, uninstall_mouse_hook};
// Control handler function for console events (Ctrl+C, close, etc.)
extern "system" fn ctrl_handler(_ctrl_type: u32) -> i32 {
println!("Control event received, attempting cleanup...");
cleanup(); // Call cleanup logic
FALSE // Allow other handlers (like default exit) to run
}
// Function to update the cursor position
pub fn update_cursor_position(point: &POINT) {
// Lock the statics needed
let mut last_pos_guard = LAST_CURSOR_POS.lock().unwrap();
let cursor_image_guard = CURRENT_CURSOR_IMAGE.lock().unwrap();
// Get Option<SyncWrapperType> then map to Option<Handle>
let maybe_hwnd_wrapper = *CURSOR_WINDOW.lock().unwrap();
let maybe_bitmap_wrapper = *CURSOR_BITMAP.lock().unwrap();
let maybe_dc_wrapper = *CURSOR_DC.lock().unwrap();
// Check if we need to update (if position changed)
let position_changed = last_pos_guard.map_or(true, |last| last.x != point.x || last.y != point.y);
if position_changed {
// Update last position *within the lock*
*last_pos_guard = Some(*point);
// Update the cursor window position if all resources exist
if let (Some(hwnd_wrapper), Some(ref cursor_image), Some(_bitmap_wrapper), Some(dc_wrapper)) =
(maybe_hwnd_wrapper, cursor_image_guard.as_ref(), maybe_bitmap_wrapper, maybe_dc_wrapper)
{
// Extract the raw handles using .0
let hwnd = hwnd_wrapper.0;
let dc = dc_wrapper.0;
// Get cursor image dimensions
let (width, height) = cursor_image.dimensions();
unsafe {
let screen_dc = GetDC(null_mut());
if !screen_dc.is_null() {
// Set up source and destination points and size
let src_point = POINT { x: 0, y: 0 };
// Adjust position based on image dimensions (center hotspot)
let dst_point = POINT {
x: point.x - (width as i32 / 2),
y: point.y - (height as i32 / 2)
};
let size = SIZE {
cx: width as i32,
cy: height as i32
};
// Set up blend function for alpha blending
let blend = BLENDFUNCTION {
BlendOp: AC_SRC_OVER,
BlendFlags: 0,
SourceConstantAlpha: 255, // Fully opaque
AlphaFormat: AC_SRC_ALPHA, // Use per-pixel alpha
};
// Update the layered window atomically
let result = UpdateLayeredWindow(
hwnd,
screen_dc,
&dst_point as *const POINT as *mut POINT,
&size as *const SIZE as *mut SIZE,
dc,
&src_point as *const POINT as *mut POINT,
0, // No color key
&blend as *const BLENDFUNCTION as *mut BLENDFUNCTION,
ULW_ALPHA,
);
// Re-assert TOPMOST after successful update, with SWP_NOACTIVATE to prevent stealing focus
if result != 0 {
SetWindowPos(
hwnd,
HWND_TOPMOST,
0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE,
);
}
if result == 0 {
use std::io::Error;
let error = Error::last_os_error();
eprintln!("UpdateLayeredWindow failed: {}", error);
}
ReleaseDC(null_mut(), screen_dc);
} else {
eprintln!("Failed to get screen DC for UpdateLayeredWindow");
}
}
}
}
}
// Create a bitmap from an RGBA image
unsafe fn create_bitmap_from_image(image: &RgbaImage) -> Option<(SyncHBITMAP, SyncHDC)> {
let (width, height) = image.dimensions();
let width_i32 = width as i32;
let height_i32 = height as i32;
// Create a device context compatible with the screen
let screen_dc = GetDC(null_mut());
if screen_dc.is_null() {
eprintln!("Failed to get screen DC");
return None;
}
// Create a compatible DC for our bitmap
let memory_dc = CreateCompatibleDC(screen_dc);
if memory_dc.is_null() {
eprintln!("Failed to create compatible DC");
ReleaseDC(null_mut(), screen_dc);
return None;
}
// Set up BITMAPINFO structure for DIB
let mut bitmap_info: BITMAPINFO = std::mem::zeroed();
bitmap_info.bmiHeader.biSize = std::mem::size_of::<BITMAPINFOHEADER>() as u32;
bitmap_info.bmiHeader.biWidth = width_i32;
bitmap_info.bmiHeader.biHeight = -height_i32; // Negative for top-down DIB
bitmap_info.bmiHeader.biPlanes = 1;
bitmap_info.bmiHeader.biBitCount = 32; // 32 bits per pixel (BGRA)
bitmap_info.bmiHeader.biCompression = BI_RGB;
// Create the DIB section
let mut bits_ptr: *mut std::ffi::c_void = null_mut();
let bitmap = CreateDIBSection(
memory_dc,
&bitmap_info,
DIB_RGB_COLORS,
&mut bits_ptr,
null_mut(),
0,
);
if bitmap.is_null() || bits_ptr.is_null() {
eprintln!("Failed to create DIB section");
DeleteDC(memory_dc);
ReleaseDC(null_mut(), screen_dc);
return None;
}
// Select the bitmap into the DC
let old_bitmap = SelectObject(memory_dc, bitmap as _);
if old_bitmap.is_null() {
eprintln!("Failed to select bitmap into DC");
DeleteObject(bitmap as _);
DeleteDC(memory_dc);
ReleaseDC(null_mut(), screen_dc);
return None;
}
// Copy the image data to the DIB, converting RGBA to BGRA with premultiplied alpha
let bits = std::slice::from_raw_parts_mut(bits_ptr as *mut u8, (width * height * 4) as usize);
for y in 0..height {
for x in 0..width {
let pixel = image.get_pixel(x, y);
let idx = ((y * width + x) * 4) as usize;
// Get RGBA components
let r = pixel[0] as u16;
let g = pixel[1] as u16;
let b = pixel[2] as u16;
let a = pixel[3] as u16;
// Premultiply alpha and convert to BGRA
bits[idx + 0] = ((b * a) / 255) as u8; // B
bits[idx + 1] = ((g * a) / 255) as u8; // G
bits[idx + 2] = ((r * a) / 255) as u8; // R
bits[idx + 3] = a as u8; // A
}
}
// Clean up
ReleaseDC(null_mut(), screen_dc);
// Return the bitmap and DC
Some((SyncHBITMAP(bitmap), SyncHDC(memory_dc)))
}
// Cleanup function to restore cursors and clean up resources
pub fn cleanup() {
// Uninstall the mouse hook FIRST
uninstall_mouse_hook();
// Restore system cursors if they were hidden
if CURSOR_HIDDEN.swap(false, Ordering::SeqCst) { // Use swap to ensure it runs only once
println!("Restoring system cursors...");
unsafe {
// Force Windows to reload default cursor settings
SystemParametersInfoW(SPI_SETCURSORS, 0, null_mut(), 0);
// Also try the A version just in case
super::state::reset_system_cursors();
}
let mut restoration_success = false;
// Lock ORIGINAL_CURSORS once
let original_cursors_guard = ORIGINAL_CURSORS.lock().unwrap();
if let Some(ref original_cursors) = *original_cursors_guard {
if !original_cursors.is_empty() {
println!("Attempting to restore {} saved original cursors", original_cursors.len());
let mut method1_success = true;
unsafe {
for &(cursor_id, sync_hcursor) in original_cursors.iter() {
// Extract the raw handle using .0
let original_hcursor = sync_hcursor.0;
// Make a fresh copy *each time* we set it
let cursor_copy = CopyIcon(original_hcursor);
if !cursor_copy.is_null() {
if SetSystemCursor(cursor_copy, cursor_id) == 0 {
eprintln!("Failed to restore system cursor {}", cursor_id);
// Don't destroy the copy on failure, let Windows manage SetSystemCursor's handle
method1_success = false;
}
// SetSystemCursor takes ownership if successful, CopyIcon needs no manual destroy
} else {
eprintln!("Failed to copy original cursor for ID {}", cursor_id);
method1_success = false;
}
}
}
if method1_success {
println!("Successfully restored original cursors from saved state.");
restoration_success = true;
}
}
}
// Drop the guard
drop(original_cursors_guard);
// Method 2: Fallback using standard arrow (less critical now with API reset)
if !restoration_success {
println!("Falling back to setting all cursors to standard arrow (IDC_ARROW)");
unsafe {
// Use MAKEINTRESOURCEW to convert IDC_ARROW to a resource pointer
let arrow_cursor = LoadCursorW(null_mut(), MAKEINTRESOURCEW(IDC_ARROW as u16));
if !arrow_cursor.is_null() {
for &cursor_id in SYSTEM_CURSORS.iter() {
// Make a copy for each system cursor ID
let arrow_copy = CopyIcon(arrow_cursor);
if !arrow_copy.is_null() {
if SetSystemCursor(arrow_copy, cursor_id) == 0 {
eprintln!("Failed to set system cursor {} to arrow", cursor_id);
// System takes ownership if successful
}
} else {
eprintln!("Failed to copy arrow cursor for ID {}", cursor_id);
}
}
println!("Finished setting cursors to standard arrow.");
restoration_success = true; // Consider this a success even if some minor fails occurred
} else {
eprintln!("Failed to load standard arrow cursor (IDC_ARROW).");
}
}
}
// Method 3: Reset via API (already called, but can call again for good measure)
if !restoration_success {
println!("Using system API reset method as final fallback.");
super::state::reset_system_cursors(); // Explicitly call the reset function
}
println!("System cursors restoration attempt complete.");
}
// Clean up GDI resources (Bitmap and DC)
// Take ownership from the Mutex<Option<T>>
let bitmap_wrapper_to_delete = CURSOR_BITMAP.lock().unwrap().take();
let dc_wrapper_to_delete = CURSOR_DC.lock().unwrap().take();
unsafe {
if let Some(bitmap_wrapper) = bitmap_wrapper_to_delete {
if DeleteObject(bitmap_wrapper.0 as _) != 0 {
println!("Cursor bitmap deleted during cleanup.");
} else {
eprintln!("Failed to delete cursor bitmap during cleanup.");
}
}
if let Some(dc_wrapper) = dc_wrapper_to_delete {
if DeleteDC(dc_wrapper.0) != 0 {
println!("Cursor DC deleted during cleanup.");
} else {
eprintln!("Failed to delete cursor DC during cleanup.");
}
}
}
// Destroy the cursor window
let window_wrapper_to_destroy = CURSOR_WINDOW.lock().unwrap().take();
if let Some(hwnd_wrapper) = window_wrapper_to_destroy {
unsafe {
if DestroyWindow(hwnd_wrapper.0) != 0 {
println!("Cursor window destroyed during cleanup.");
} else {
// GetLastError might provide info, e.g., if already destroyed
eprintln!("Failed to destroy cursor window during cleanup (Error {}).", winapi::um::errhandlingapi::GetLastError());
}
}
}
// Reset the last known cursor position
*LAST_CURSOR_POS.lock().unwrap() = None;
// Unregister control handler (less critical now, but good practice)
if CTRL_HANDLER_REGISTERED.load(Ordering::SeqCst) {
unsafe {
if SetConsoleCtrlHandler(Some(ctrl_handler), FALSE) != 0 {
CTRL_HANDLER_REGISTERED.store(false, Ordering::SeqCst);
println!("Control handler unregistered.");
} else {
eprintln!("Failed to unregister control handler.");
}
}
}
}
// --- Public API Functions ---
// Hide the system cursor
pub fn hide_system_cursor() -> bool {
// Initialize once
INIT.get_or_init(|| {
// Register control handler for clean exit
unsafe {
if SetConsoleCtrlHandler(Some(ctrl_handler), TRUE) != 0 {
CTRL_HANDLER_REGISTERED.store(true, Ordering::SeqCst);
println!("Control handler registered for clean exit.");
} else {
eprintln!("Failed to register control handler: {}", std::io::Error::last_os_error());
}
}
});
// Check if already hidden
if CURSOR_HIDDEN.load(Ordering::SeqCst) {
return true; // Already hidden
}
// Save original cursors before hiding
let mut original_cursors = Vec::new();
unsafe {
for &cursor_id in SYSTEM_CURSORS.iter() {
// Use MAKEINTRESOURCEW to convert IDC_ARROW to a resource pointer
let empty_cursor = CreateCursor(
null_mut(), // hInst
0, 0, // hotspot x, y
1, 1, // width, height (minimum size)
&[0xFF] as *const _ as *const std::ffi::c_void, // AND mask (all bits 1 = transparent)
&[0x00] as *const _ as *const std::ffi::c_void, // XOR mask (all bits 0 = black)
);
if !empty_cursor.is_null() {
// Get the original cursor first
let original_cursor = CopyIcon(LoadCursorW(null_mut(), MAKEINTRESOURCEW(cursor_id as u16)));
if !original_cursor.is_null() {
// Save the original cursor for restoration
original_cursors.push((cursor_id, SyncHCURSOR(original_cursor)));
}
// Set the empty cursor
if SetSystemCursor(empty_cursor, cursor_id) == 0 {
eprintln!("Failed to set system cursor {}", cursor_id);
// Don't destroy the cursor on failure, let Windows manage it
}
// SetSystemCursor takes ownership of the cursor
} else {
eprintln!("Failed to create empty cursor");
}
}
}
// Store original cursors for restoration
if !original_cursors.is_empty() {
*ORIGINAL_CURSORS.lock().unwrap() = Some(original_cursors);
}
// Mark as hidden
CURSOR_HIDDEN.store(true, Ordering::SeqCst);
// Create the cursor window
unsafe {
let hwnd_wrapper = create_cursor_window();
if let Some(hwnd) = hwnd_wrapper {
*CURSOR_WINDOW.lock().unwrap() = Some(hwnd);
} else {
eprintln!("Failed to create cursor window");
return false;
}
}
// Install the mouse hook
if !install_mouse_hook_if_needed() {
eprintln!("Failed to install mouse hook");
return false;
}
true
}
// Update the cursor image
pub fn update_cursor_image(path: &Path) -> bool {
// Load the image
let image = match image_open(path) {
Ok(img) => img.to_rgba8(),
Err(e) => {
eprintln!("Failed to load cursor image: {}", e);
return false;
}
};
// Store the image for rendering
*CURRENT_CURSOR_IMAGE.lock().unwrap() = Some(image.clone());
// Create bitmap and DC
unsafe {
// Clean up old bitmap and DC if they exist
let mut bitmap_guard = CURSOR_BITMAP.lock().unwrap();
let mut dc_guard = CURSOR_DC.lock().unwrap();
if bitmap_guard.is_some() {
if let Some(bitmap_wrapper) = bitmap_guard.take() {
DeleteObject(bitmap_wrapper.0 as _);
}
}
if dc_guard.is_some() {
if let Some(dc_wrapper) = dc_guard.take() {
DeleteDC(dc_wrapper.0);
}
}
// Create new bitmap and DC
if let Some((bitmap_wrapper, dc_wrapper)) = create_bitmap_from_image(&image) {
*bitmap_guard = Some(bitmap_wrapper);
*dc_guard = Some(dc_wrapper);
} else {
eprintln!("Failed to create bitmap from image");
return false;
}
}
// Force an update of the cursor position
unsafe {
let mut point = POINT { x: 0, y: 0 };
if GetCursorPos(&mut point) != 0 {
update_cursor_position(&point);
}
}
true
}
// Restore the system cursor
pub fn restore_cursor() {
cleanup();
}
// Render cursor functionality is now handled by the mouse hook
-125
View File
@@ -1,125 +0,0 @@
// Windows platform mouse hook management
use std::ptr::null_mut;
use std::sync::atomic::Ordering;
use std::time::Instant;
use winapi::{
um::winuser::{
SetWindowsHookExW, UnhookWindowsHookEx, CallNextHookEx, SetWindowPos,
WH_MOUSE_LL, HC_ACTION, MSLLHOOKSTRUCT,
WM_MOUSEMOVE, WM_RBUTTONDOWN, HWND_TOPMOST, SWP_NOSIZE, SWP_NOMOVE, SWP_NOACTIVATE,
},
um::libloaderapi::GetModuleHandleW,
shared::minwindef::{WPARAM, LPARAM, LRESULT, UINT},
};
use super::types::*;
use super::state::*;
use super::cursor::update_cursor_position;
// Low-level mouse hook procedure
pub unsafe extern "system" fn low_level_mouse_proc(n_code: i32, w_param: WPARAM, l_param: LPARAM) -> LRESULT {
if n_code == HC_ACTION {
// Extract mouse position from the hook struct for any mouse event
let p_msll_hook_struct = l_param as *const MSLLHOOKSTRUCT;
if !p_msll_hook_struct.is_null() {
let msll_hook_struct = *p_msll_hook_struct;
let current_pos = msll_hook_struct.pt;
match w_param as UINT {
WM_MOUSEMOVE => {
let mut last_update_time_guard = LAST_MOUSE_MOVE_TIME.lock().unwrap();
let now = Instant::now();
if last_update_time_guard.map_or(true, |last_time| now.duration_since(last_time) >= MOUSE_UPDATE_INTERVAL) {
// update_cursor_position already contains logic to only update if position actually changed via LAST_CURSOR_POS
update_cursor_position(&current_pos);
*last_update_time_guard = Some(now);
}
},
// Handle right mouse button down - immediately update cursor position and force topmost
WM_RBUTTONDOWN => {
// Force an immediate update of the cursor position when right-clicking
// This ensures the overlay is visible before the menu appears
update_cursor_position(&current_pos);
// Force the cursor window to be topmost
if let Some(hwnd_wrapper) = *CURSOR_WINDOW.lock().unwrap() {
SetWindowPos(
hwnd_wrapper.0,
HWND_TOPMOST,
0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE, // SWP_NOACTIVATE is critical
);
}
},
// Handle other mouse events if needed
_ => { /* Other mouse messages can be handled here if needed */ }
}
}
}
// Call the next hook in the chain.
// Pass the HHOOK of our own hook, loaded from CURRENT_HOOK_FOR_CALLBACK.
CallNextHookEx(
CURRENT_HOOK_FOR_CALLBACK.load(Ordering::Relaxed) as _,
n_code,
w_param,
l_param,
)
}
// Install mouse hook function
pub fn install_mouse_hook_if_needed() -> bool {
let mut hook_guard = MOUSE_HOOK_HANDLE.lock().unwrap();
if hook_guard.is_none() {
unsafe {
// GetModuleHandleW(null_mut()) gets the HMODULE for the current process (EXE)
// which is correct for a hook procedure within the same process.
let h_instance = GetModuleHandleW(null_mut());
if h_instance.is_null() {
eprintln!("Failed to get module handle for setting mouse hook: {}", std::io::Error::last_os_error());
return false;
}
let hook = SetWindowsHookExW(
WH_MOUSE_LL, // Hook type: Low-level mouse
Some(low_level_mouse_proc), // Pointer to hook procedure
h_instance, // HMODULE of the DLL containing the hook proc (or current process)
0 // Thread ID (0 for all threads on current desktop)
);
if !hook.is_null() {
// Create wrapped handle
let sync_hook = SyncHHOOK(hook);
*hook_guard = Some(sync_hook);
CURRENT_HOOK_FOR_CALLBACK.store(hook as *mut _, Ordering::SeqCst);
println!("Low-level mouse hook installed successfully.");
return true;
} else {
eprintln!("Failed to install low-level mouse hook: {}", std::io::Error::last_os_error());
return false;
}
}
}
true // Already installed
}
// Uninstall mouse hook function
pub fn uninstall_mouse_hook() {
let mut hook_guard = MOUSE_HOOK_HANDLE.lock().unwrap();
if let Some(hook_wrapper) = hook_guard.take() { // .take() removes it from Option and returns it
unsafe {
// Extract the raw handle using .0
let hook = hook_wrapper.0;
if UnhookWindowsHookEx(hook) != 0 {
CURRENT_HOOK_FOR_CALLBACK.store(std::ptr::null_mut(), Ordering::SeqCst);
println!("Low-level mouse hook uninstalled successfully.");
} else {
eprintln!("Failed to uninstall low-level mouse hook: {}", std::io::Error::last_os_error());
// Put it back if unhooking failed, though this is unlikely.
*hook_guard = Some(hook_wrapper);
}
}
}
}
-10
View File
@@ -1,10 +0,0 @@
// Windows platform implementation module
mod types;
mod cursor;
mod window;
mod hook;
mod state;
// Re-export the public API
pub use cursor::{hide_system_cursor, update_cursor_image, restore_cursor};
-68
View File
@@ -1,68 +0,0 @@
// Windows platform types and constants
use std::time::Duration;
use winapi::{
shared::windef::{HWND, HCURSOR, HBITMAP, HDC, HHOOK},
shared::minwindef::DWORD,
};
// --- Thread-Safe Handle Wrappers ---
// These wrappers make raw Windows handles safe to share between threads
// Wrapper for HWND (window handle)
#[derive(Copy, Clone, Debug)]
pub struct SyncHWND(pub HWND);
unsafe impl Send for SyncHWND {}
unsafe impl Sync for SyncHWND {}
// Wrapper for HCURSOR (cursor handle)
#[derive(Copy, Clone, Debug)]
pub struct SyncHCURSOR(pub HCURSOR);
unsafe impl Send for SyncHCURSOR {}
unsafe impl Sync for SyncHCURSOR {}
// Wrapper for HBITMAP (bitmap handle)
#[derive(Copy, Clone, Debug)]
pub struct SyncHBITMAP(pub HBITMAP);
unsafe impl Send for SyncHBITMAP {}
unsafe impl Sync for SyncHBITMAP {}
// Wrapper for HDC (device context handle)
#[derive(Copy, Clone, Debug)]
pub struct SyncHDC(pub HDC);
unsafe impl Send for SyncHDC {}
unsafe impl Sync for SyncHDC {}
// Wrapper for HHOOK (hook handle)
#[derive(Copy, Clone, Debug)]
pub struct SyncHHOOK(pub HHOOK);
unsafe impl Send for SyncHHOOK {}
unsafe impl Sync for SyncHHOOK {}
// --- Constants ---
// Constants for SystemParametersInfo
pub const SPI_SETCURSORS_A: u32 = 0x0057;
pub const SPIF_UPDATEINIFILE: u32 = 0x01;
pub const SPIF_SENDCHANGE: u32 = 0x02;
// Window class name for our cursor window
pub const CURSOR_WINDOW_CLASS: &str = "CursorRushOverlay";
// Mouse update interval (throttle to ~120Hz)
pub const MOUSE_UPDATE_INTERVAL: Duration = Duration::from_millis(8);
// Define all system cursor IDs we'll replace
pub 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
];
-89
View File
@@ -1,89 +0,0 @@
// Windows platform window management
use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
use std::ptr::null_mut;
use winapi::{
um::winuser::{
CreateWindowExW, DestroyWindow, ShowWindow, SetWindowPos,
RegisterClassExW, DefWindowProcW, LoadCursorW,
WS_EX_LAYERED, WS_EX_TRANSPARENT, WS_EX_TOPMOST, WS_EX_TOOLWINDOW, WS_EX_NOACTIVATE,
WS_POPUP, SW_SHOW, HWND_TOPMOST, SWP_NOSIZE, SWP_NOMOVE,
CS_HREDRAW, CS_VREDRAW, WNDCLASSEXW, MAKEINTRESOURCEW,
WM_DESTROY, WM_CLOSE, IDC_ARROW,
},
shared::windef::HWND,
shared::minwindef::{UINT, WPARAM, LPARAM, LRESULT},
};
use super::types::*;
use super::state::*;
// Window procedure for our cursor window
pub unsafe extern "system" fn window_proc(hwnd: HWND, msg: UINT, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
match msg {
WM_CLOSE | WM_DESTROY => {
// Clean up resources associated with the window if necessary
DestroyWindow(hwnd); // Ensure window is destroyed on close message
// Remove window handle from our static variable upon destruction
*CURSOR_WINDOW.lock().unwrap() = None;
0
},
// WM_MOUSEMOVE is handled by the low-level mouse hook
_ => DefWindowProcW(hwnd, msg, wparam, lparam),
}
}
// Create a layered, topmost window for our cursor
pub unsafe fn create_cursor_window() -> Option<SyncHWND> {
// Convert class name to wide string
let class_name_wide: Vec<u16> = OsStr::new(CURSOR_WINDOW_CLASS)
.encode_wide()
.chain(std::iter::once(0))
.collect();
// Register window class if not already registered
let mut wc = std::mem::zeroed::<WNDCLASSEXW>();
wc.cbSize = std::mem::size_of::<WNDCLASSEXW>() as u32;
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = Some(window_proc);
wc.hInstance = null_mut();
wc.lpszClassName = class_name_wide.as_ptr();
// Use MAKEINTRESOURCEW to convert IDC_ARROW to a resource pointer
wc.hCursor = LoadCursorW(null_mut(), MAKEINTRESOURCEW(IDC_ARROW as u16));
let atom = RegisterClassExW(&wc);
if atom == 0 {
// Check GetLastError() if registration fails
if winapi::um::errhandlingapi::GetLastError() != 1410 { // ERROR_CLASS_ALREADY_EXISTS
eprintln!("Failed to register window class, error: {}", winapi::um::errhandlingapi::GetLastError());
return None;
}
}
// Create window with layered, transparent, and topmost attributes
let hwnd = CreateWindowExW(
WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOPMOST | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE,
class_name_wide.as_ptr(),
null_mut(), // Window title (none)
WS_POPUP, // Popup window with no border
0, 0, // Initial Position (irrelevant, updated immediately)
1, 1, // Initial Size (irrelevant, determined by bitmap)
null_mut(), // Parent window
null_mut(), // Menu
null_mut(), // Instance
null_mut(), // Additional data
);
if hwnd.is_null() {
eprintln!("Failed to create cursor window, error: {}", winapi::um::errhandlingapi::GetLastError());
return None;
}
// Show the window and ensure it's topmost
ShowWindow(hwnd, SW_SHOW);
SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
// Return the wrapped handle
Some(SyncHWND(hwnd))
}
-7
View File
@@ -1,7 +0,0 @@
// UI module
// Contains UI components and rendering logic
pub mod view;
pub mod tray;
pub use view::view_app;
-194
View File
@@ -1,194 +0,0 @@
// System tray functionality
use std::{
sync::{Arc, Mutex},
thread,
};
use ::image::open as image_open;
use tray_icon::{
TrayIconBuilder,
TrayIcon,
TrayIconEvent,
MouseButton,
menu::{Menu, MenuEvent},
};
use crate::app::{Message, CursorInfo, TRAY_SENDER, SharedState};
// Define menu item IDs
const NEXT_CURSOR_ID: &str = "next_cursor";
const SHOW_WINDOW_ID: &str = "show_window";
const QUIT_ID: &str = "quit";
// Set up the system tray icon
pub fn setup_tray_icon(
cursors: &[CursorInfo],
shared_state: Arc<Mutex<SharedState>>,
) -> Option<TrayIcon> { // Return Option<TrayIcon>
if cursors.is_empty() {
return None; // Return None if no cursors
}
// Create a menu
let menu = Menu::with_items(&[
&tray_icon::menu::MenuItem::with_id(NEXT_CURSOR_ID, "Next cursor", true, None),
&tray_icon::menu::MenuItem::with_id(SHOW_WINDOW_ID, "Show window", true, None),
&tray_icon::menu::MenuItem::with_id(QUIT_ID, "Quit", true, None),
]).ok()?; // Use ok()? to convert Result to Option
// Use the first PNG as the tray icon
let icon_path = &cursors[0].path;
let img = image_open(icon_path).ok()?; // Use ok()?
let rgba_img = img.to_rgba8();
let (width, height) = rgba_img.dimensions();
let icon = tray_icon::Icon::from_rgba(rgba_img.into_raw(), width, height).ok()?; // Use ok()?
// Build the tray icon
println!("Building tray icon...");
let tray_result = TrayIconBuilder::new()
.with_tooltip("Cursor Rush")
.with_icon(icon)
.with_menu(Box::new(menu))
.with_menu_on_left_click(false) // Disable menu on left click - only show on right click
.build();
println!("Tray icon configuration: menu_on_left_click = false");
// Store the tray icon itself
let tray = match tray_result {
Ok(tray_instance) => {
println!("Tray icon built successfully");
tray_instance // Assign the TrayIcon object
},
Err(e) => {
eprintln!("Failed to build tray icon: {}", e);
return None; // Return None on error
}
};
// Listen for tray menu events in a separate thread
let state_clone = shared_state.clone();
println!("Spawning tray menu event listener thread...");
thread::spawn(move || {
let menu_channel = MenuEvent::receiver();
println!("Tray menu event listener waiting for events...");
while let Ok(event) = menu_channel.recv() {
println!("Tray menu event received: ID = {:?}", event.id);
let msg_to_send = match event.id.as_ref() {
NEXT_CURSOR_ID => {
println!("Tray: Next Cursor requested");
match state_clone.lock() {
Ok(mut state) => {
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) {
println!("Tray: Updating cursor image: {}", cursor.name);
crate::app::apply_cursor(&cursor.path);
// No need to call render_cursor anymore as the mouse hook handles rendering
state.current_cursor_image = image_open(&cursor.path).ok().map(|img| img.to_rgba8());
}
}
},
Err(e) => eprintln!("Tray: Failed to lock shared state for next cursor: {}", e),
}
// Send TrayEvent just to trigger UI update if needed
Some(Message::TrayEvent)
},
SHOW_WINDOW_ID => {
println!("Tray: Show Window requested");
// Send ShowWindow message to main thread
Some(Message::ShowWindow)
},
QUIT_ID => {
println!("Tray: Quit requested");
// Send Quit message to main thread
Some(Message::Quit)
},
_ => {
println!("Tray: Unknown menu event ID: {:?}", event.id);
None // Don't send a message for unknown IDs
}
};
// Send the determined message to the main UI thread using the global sender
if let Some(msg) = msg_to_send {
// Get the global sender
if let Some(tx) = TRAY_SENDER.lock().unwrap().as_ref() {
if let Err(e) = tx.unbounded_send(msg.clone()) { // Use clone if msg is needed after send
eprintln!("Tray: Failed to send {:?} message: {}", msg, e);
break; // Stop thread if channel broken
}
// If Quit message was sent, break the loop to exit the listener thread
if matches!(msg, Message::Quit) {
println!("Tray: Quit message sent, exiting listener thread");
break;
}
} else {
eprintln!("Tray: Global sender not initialized yet");
}
}
}
println!("Tray menu event listener thread finished");
});
// Set up a direct event handler for tray icon events
// This is the preferred way to handle tray icon clicks
println!("Setting up tray icon click event handler...");
// Use set_event_handler to directly handle tray icon events
TrayIconEvent::set_event_handler(Some(move |event| {
// Only process important events, filter out mouse movements
match event {
// Handle click events
TrayIconEvent::Click { button, .. } => {
println!("Tray: Click detected with button: {:?}", button);
// Left click should show the window
if button == MouseButton::Left {
println!("Tray: Left click detected, sending ShowWindow message");
// Get the global sender
if let Some(tx) = TRAY_SENDER.lock().unwrap().as_ref() {
if let Err(e) = tx.unbounded_send(Message::ShowWindow) {
eprintln!("Tray: Failed to send ShowWindow message via handler: {}", e);
}
} else {
eprintln!("Tray: Global sender not initialized yet for click handler");
}
}
},
// Handle double-click events
TrayIconEvent::DoubleClick { button, .. } => {
println!("Tray: Double click detected with button: {:?}", button);
// Double left click should also show the window
if button == MouseButton::Left {
println!("Tray: Double left click detected, sending ShowWindow message");
// Get the global sender
if let Some(tx) = TRAY_SENDER.lock().unwrap().as_ref() {
if let Err(e) = tx.unbounded_send(Message::ShowWindow) {
eprintln!("Tray: Failed to send ShowWindow message via handler on double click: {}", e);
}
} else {
eprintln!("Tray: Global sender not initialized yet for double click handler");
}
}
},
// Silently ignore mouse movement events
TrayIconEvent::Move { .. } => {
// Don't log anything for mouse movements
},
// Silently ignore mouse enter/leave events
TrayIconEvent::Enter { .. } | TrayIconEvent::Leave { .. } => {
// Don't log anything for mouse enter/leave
},
// Log other events for debugging (but not mouse-related ones)
_ => {
println!("Tray: Event received: {:?}", event);
}
}
}));
println!("Tray setup complete using set_event_handler for clicks."); // Updated log
Some(tray) // Return the TrayIcon object
}
-77
View File
@@ -1,77 +0,0 @@
// UI view components
use iced::{
widget::{button, text, Column, Row, container, image as iced_image},
Element, Length,
};
use crate::app::{CursorRush, Message};
// View function to render the UI
pub fn view_app(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 and close button behavior
content = content.push(
container(
Column::new()
.spacing(5)
.push(text("Press Ctrl+Shift+F12 to exit the application").size(16))
.push(text("Close button (X) minimizes to system tray").size(16))
)
.padding(10)
);
} else {
content = content.push(text("No cursors found in assets directory"));
}
// Create the main container
container(content)
.width(Length::Fill)
.height(Length::Fill)
.center_x(Length::Fill)
.center_y(Length::Fill)
.padding(20)
.into()
}
-34
View File
@@ -1,34 +0,0 @@
// Application configuration
use std::path::PathBuf;
// Application configuration
pub struct Config {
pub assets_dir: PathBuf,
pub default_cursor: String,
pub auto_start: bool,
pub minimize_to_tray: bool,
}
impl Default for Config {
fn default() -> Self {
Self {
assets_dir: PathBuf::from("assets"),
default_cursor: String::from("default"),
auto_start: false,
minimize_to_tray: true,
}
}
}
// Load configuration from file or use defaults
pub fn load_config() -> Config {
// TODO: Implement loading from a config file
Config::default()
}
// Save configuration to file
pub fn save_config(_config: &Config) -> Result<(), String> {
// TODO: Implement saving to a config file
Ok(())
}
-58
View File
@@ -1,58 +0,0 @@
// Logging utilities
// Log levels
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum LogLevel {
Debug,
Info,
Warning,
Error,
}
// Global log level (could be made configurable)
static mut CURRENT_LOG_LEVEL: LogLevel = LogLevel::Info;
// Set the global log level
pub fn set_log_level(level: LogLevel) {
unsafe {
CURRENT_LOG_LEVEL = level;
}
}
// Get the current log level
pub fn get_log_level() -> LogLevel {
unsafe {
CURRENT_LOG_LEVEL
}
}
// Log a message at the specified level
pub fn log(level: LogLevel, message: &str) {
if level >= unsafe { CURRENT_LOG_LEVEL } {
let prefix = match level {
LogLevel::Debug => "[DEBUG]",
LogLevel::Info => "[INFO]",
LogLevel::Warning => "[WARNING]",
LogLevel::Error => "[ERROR]",
};
println!("{} {}", prefix, message);
}
}
// Convenience functions for different log levels
pub fn debug(message: &str) {
log(LogLevel::Debug, message);
}
pub fn info(message: &str) {
log(LogLevel::Info, message);
}
pub fn warning(message: &str) {
log(LogLevel::Warning, message);
}
pub fn error(message: &str) {
log(LogLevel::Error, message);
}
-4
View File
@@ -1,4 +0,0 @@
// Utility functions and helpers
pub mod config;
pub mod logging;