11 changed files with 421 additions and 139 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"git.ignoreLimitWarning": true
}
+49 -54
View File
@@ -11,47 +11,31 @@ A cross-platform application for customizing your cursor with gameified logic.
- System tray integration with context menu - System tray integration with context menu
- Global hotkey support - Global hotkey support
- Steam integration for item drops - Steam integration for item drops
- Tauri-based frontend with React
## Project Structure ## Project Structure
``` ```
src/ frontend/
├── app/ # Application core logic ├── src/ # React frontend code
│ ├── mod.rs # Main app module exports │ ├── App.jsx # Main React component
│ ├── state.rs # Application state management │ ├── components/ # UI components
│ ├── messages.rs # Message definitions │ └── cursors/ # Cursor image files
│ └── cursor.rs # Cursor-related functionality ├── src-tauri/ # Rust backend code
├── ui/ # User interface components │ ├── src/ # Rust source code
│ ├── mod.rs # UI module exports │ │ ├── lib.rs # Main library code
│ ├── view.rs # View rendering logic │ │ ├── main.rs # Application entry point
│ └── tray.rs # System tray functionality │ │ ├── integrations/ # External integrations
├── platform/ # Platform-specific implementations │ │ │ ├── mod.rs # Integrations module exports
│ ├── mod.rs # Platform module exports │ │ │ └── steam.rs # Steam integration
│ ├── windows/ # Windows-specific code │ │ └── platform/ # Platform-specific code
│ │ ├── mod.rs # Windows module exports │ │ ├── mod.rs # Platform module exports
│ │ ├── types.rs # Windows type definitions │ │ ├── types.rs # Common type definitions
│ │ ├── state.rs # Windows state management │ │ ├── windows/ # Windows-specific code
│ │ └── cursor.rs # Windows cursor handling │ │ ├── macos/ # macOS-specific code
│ ├── macos/ # macOS-specific code (in development) │ │ └── linux/ # Linux-specific code
│ │ ├── mod.rs # macOS module exports │ └── Cargo.toml # Rust dependencies
│ │ ├── state.rs # macOS state management └── package.json # npm dependencies
│ │ └── cursor.rs # macOS cursor handling
│ ├── 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
│ ├── mod.rs # Integrations module exports
│ └── steam.rs # Steam integration
├── utils/ # Utility functions and helpers
│ ├── mod.rs # Utils module exports
│ ├── config.rs # Configuration management
│ └── logging.rs # Logging utilities
└── main.rs # Application entry point
``` ```
## Building ## Building
@@ -60,6 +44,8 @@ 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: - Platform-specific dependencies:
- Windows: None (fully supported) - Windows: None (fully supported)
- macOS: Development in progress - macOS: Development in progress
@@ -68,41 +54,49 @@ src/
### Dependencies ### Dependencies
The application uses the following major dependencies: The application uses the following major dependencies:
- iced 0.13.1 - GUI framework - Tauri 1.6.0 - Cross-platform framework
- tray-icon 0.20.1 - System tray functionality - React - Frontend framework
- image 0.24.7 - Image processing - image 0.24.7 - Image processing
- global-hotkey 0.4.2 - Global hotkey registration - winapi 0.3.9 - Windows API bindings
- steamworks 0.11.0 - Steam integration - 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 (PNG format only) 1. Make sure you have Node.js and npm installed
2. Run the application using `cargo run` or the compiled executable 2. Navigate to the frontend directory: `cd frontend`
3. The application will load all PNG files from the assets directory 3. Install dependencies if you haven't already: `npm install`
4. Select a cursor from the list in the UI 4. Run the application in development mode: `npm run tauri dev`
5. The application will replace your system cursor with the selected image 5. The application will start with a React frontend and Rust backend
6. When closing the window, the application minimizes to the system tray 6. Use the UI to browse and select cursors
7. To fully exit, use the system tray menu or press the exit hotkey 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 ### Custom Cursors
The application comes with two sample cursors in the `assets` directory: The application comes with sample cursors:
- `default.png` - A simple default cursor - `default.png` - A simple default cursor
- `icon.png` - An icon cursor
- `red.png` - A red-colored cursor - `red.png` - A red-colored cursor
To add your own cursors: To add your own cursors:
1. Create PNG images with transparent backgrounds 1. Create PNG images with transparent backgrounds
2. Place them in the `assets` directory 2. Place them in the `frontend/src/cursors` directory
3. Restart the application to load the new cursors 3. Restart the application to load the new cursors
4. The cursor name in the UI will be derived from the filename 4. The cursor name in the UI will be derived from the filename
@@ -147,6 +141,7 @@ Contributions to Cursor Rush are welcome! Here are some ways you can contribute:
- Submit pull requests with bug fixes or new features - Submit pull requests with bug fixes or new features
- Help with documentation - Help with documentation
- Contribute to macOS and Linux platform support - Contribute to macOS and Linux platform support
- Improve the Tauri integration
## License ## License
+3 -2
View File
@@ -14,13 +14,14 @@ rust-version = "1.56"
tauri-build = { version = "1.5.2", features = [] } tauri-build = { version = "1.5.2", features = [] }
[dependencies] [dependencies]
tauri = { version = "1.6.0", features = [ "window-start-dragging", "shell-open"] } tauri = { version = "1.6.0", features = [ "window-start-dragging", "shell-open", "system-tray", "window-minimize", "window-hide", "window-show", "window-close"] }
serde_json = "1.0" serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
image = "0.24" image = "0.24"
# Ensure "consoleapi" is in this features list # Ensure "consoleapi" is in this features list
winapi = { version = "0.3.9", features = ["winuser", "libloaderapi", "wingdi", "errhandlingapi", "consoleapi"] } winapi = { version = "0.3.9", features = ["winuser", "libloaderapi", "wingdi", "errhandlingapi", "consoleapi"] }
once_cell = "1.19.0" once_cell = "1.19.0"
steamworks = "0.10.0"
[features] [features]
# this feature is used for production builds or when `devPath` points to the filesystem # this feature is used for production builds or when `devPath` points to the filesystem
# DO NOT REMOVE!! # DO NOT REMOVE!!
@@ -0,0 +1,3 @@
// External integrations module
pub mod steam;
@@ -0,0 +1,26 @@
// Steam integration
use steamworks::{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 }
}
#[allow(dead_code)]
pub fn drop_item(&self) {
// Placeholder for item drop functionality
// The actual implementation would use the Steam API to trigger an item drop
println!("Steam item drop triggered (placeholder)");
// In a real implementation with the correct API version, we would use:
// self.client.get_inventory().trigger_item_drop(item_def_id);
}
}
+178 -40
View File
@@ -1,10 +1,11 @@
use std::sync::Mutex; use std::sync::Mutex;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use tauri::{Manager, AppHandle}; use tauri::{Manager, AppHandle, CustomMenuItem, SystemTray, SystemTrayMenu, SystemTrayMenuItem, SystemTrayEvent, WindowEvent};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::env; use std::env;
mod platform; mod platform;
mod integrations;
// Define cursor structure // Define cursor structure
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -40,26 +41,26 @@ fn get_current_cursor_index(state: tauri::State<AppStateWrapper>) -> usize {
#[tauri::command] #[tauri::command]
fn select_cursor(index: usize, state: tauri::State<AppStateWrapper>, app_handle: AppHandle) -> Result<(), String> { fn select_cursor(index: usize, state: tauri::State<AppStateWrapper>, app_handle: AppHandle) -> Result<(), String> {
println!("=== RESOURCE DEBUG [select_cursor] ==="); println!("=== RESOURCE DEBUG [select_cursor] ===");
let mut app_state = state.0.lock().unwrap(); let mut app_state = state.0.lock().unwrap();
if index < app_state.cursors.len() { if index < app_state.cursors.len() {
app_state.current_index = index; app_state.current_index = index;
let cursor = &app_state.cursors[index]; let cursor = &app_state.cursors[index];
println!("Cursor selected: {}", cursor.name); println!("Cursor selected: {}", cursor.name);
println!("Original path: {}", cursor.path); println!("Original path: {}", cursor.path);
// Get the resource directory // Get the resource directory
let resource_dir = app_handle.path_resolver().resource_dir(); let resource_dir = app_handle.path_resolver().resource_dir();
println!("Resource dir: {:?}", resource_dir); println!("Resource dir: {:?}", resource_dir);
// List contents of resource dir // List contents of resource dir
if let Some(res_dir) = &resource_dir { if let Some(res_dir) = &resource_dir {
println!("Listing contents of resource dir: {}", res_dir.display()); println!("Listing contents of resource dir: {}", res_dir.display());
if let Ok(entries) = std::fs::read_dir(res_dir) { if let Ok(entries) = std::fs::read_dir(res_dir) {
for (i, entry) in entries.filter_map(Result::ok).enumerate() { for (i, entry) in entries.filter_map(Result::ok).enumerate() {
println!(" {}: {}", i, entry.path().display()); println!(" {}: {}", i, entry.path().display());
// List subdirectories // List subdirectories
if entry.path().is_dir() { if entry.path().is_dir() {
if let Ok(subentries) = std::fs::read_dir(entry.path()) { if let Ok(subentries) = std::fs::read_dir(entry.path()) {
@@ -73,29 +74,29 @@ fn select_cursor(index: usize, state: tauri::State<AppStateWrapper>, app_handle:
println!("Failed to read resource directory"); println!("Failed to read resource directory");
} }
} }
let filename = Path::new(&cursor.path) let filename = Path::new(&cursor.path)
.file_name() .file_name()
.and_then(|name| name.to_str()) .and_then(|name| name.to_str())
.ok_or_else(|| format!("Invalid cursor path: {}", cursor.path))?; .ok_or_else(|| format!("Invalid cursor path: {}", cursor.path))?;
println!("Extracted filename: {}", filename); println!("Extracted filename: {}", filename);
// Try using the actual file path directly // Try using the actual file path directly
println!("Using direct file path: {}", cursor.path); println!("Using direct file path: {}", cursor.path);
// Verify the file exists // Verify the file exists
if Path::new(&cursor.path).exists() { if Path::new(&cursor.path).exists() {
println!("File exists, applying cursor..."); println!("File exists, applying cursor...");
if let Err(e) = platform::set_system_cursor(&cursor.path) { if let Err(e) = platform::set_system_cursor(&cursor.path) {
println!("Error applying cursor: {}", e); println!("Error applying cursor: {}", e);
return Err(format!("Failed to apply cursor '{}': {}", cursor.name, e)); return Err(format!("Failed to apply cursor '{}': {}", cursor.name, e));
} }
// Emit an event to notify the frontend // Emit an event to notify the frontend
let _ = app_handle.emit_all("cursor_changed", index); // Ignore error let _ = app_handle.emit_all("cursor_changed", index); // Ignore error
println!("Successfully applied cursor using actual file path"); println!("Successfully applied cursor using actual file path");
return Ok(()); return Ok(());
} else { } else {
@@ -111,11 +112,11 @@ fn select_cursor(index: usize, state: tauri::State<AppStateWrapper>, app_handle:
#[tauri::command] #[tauri::command]
fn next_cursor(state: tauri::State<AppStateWrapper>, app_handle: AppHandle) -> Result<usize, String> { fn next_cursor(state: tauri::State<AppStateWrapper>, app_handle: AppHandle) -> Result<usize, String> {
println!("=== RESOURCE DEBUG [next_cursor] ==="); println!("=== RESOURCE DEBUG [next_cursor] ===");
let mut app_state = state.0.lock().unwrap(); let mut app_state = state.0.lock().unwrap();
if !app_state.cursors.is_empty() { if !app_state.cursors.is_empty() {
app_state.current_index = (app_state.current_index + 1) % app_state.cursors.len(); app_state.current_index = (app_state.current_index + 1) % app_state.cursors.len();
let cursor = &app_state.cursors[app_state.current_index]; let cursor = &app_state.cursors[app_state.current_index];
println!("Next cursor: {} ({})", cursor.name, cursor.path); println!("Next cursor: {} ({})", cursor.name, cursor.path);
@@ -126,22 +127,22 @@ fn next_cursor(state: tauri::State<AppStateWrapper>, app_handle: AppHandle) -> R
// Try both resource paths like in select_cursor // Try both resource paths like in select_cursor
println!("Extracted filename: {}", filename); println!("Extracted filename: {}", filename);
// Try using the actual file path directly // Try using the actual file path directly
println!("Using direct file path: {}", cursor.path); println!("Using direct file path: {}", cursor.path);
// Verify the file exists // Verify the file exists
if Path::new(&cursor.path).exists() { if Path::new(&cursor.path).exists() {
println!("File exists, applying cursor..."); println!("File exists, applying cursor...");
if let Err(e) = platform::set_system_cursor(&cursor.path) { if let Err(e) = platform::set_system_cursor(&cursor.path) {
println!("Error applying cursor: {}", e); println!("Error applying cursor: {}", e);
return Err(format!("Failed to apply cursor '{}': {}", cursor.name, e)); return Err(format!("Failed to apply cursor '{}': {}", cursor.name, e));
} }
// Emit an event to notify the frontend // Emit an event to notify the frontend
let _ = app_handle.emit_all("cursor_changed", app_state.current_index); // Ignore error let _ = app_handle.emit_all("cursor_changed", app_state.current_index); // Ignore error
println!("Successfully applied cursor using actual file path"); println!("Successfully applied cursor using actual file path");
return Ok(app_state.current_index); return Ok(app_state.current_index);
} else { } else {
@@ -161,22 +162,40 @@ fn quit_app(app_handle: AppHandle) {
Ok(_) => println!("Default cursor restored"), Ok(_) => println!("Default cursor restored"),
Err(e) => eprintln!("Failed to restore default cursor: {}", e), Err(e) => eprintln!("Failed to restore default cursor: {}", e),
} }
// Exit the application // Exit the application
std::thread::spawn(move || { std::thread::spawn(move || {
app_handle.exit(0); app_handle.exit(0);
}); });
} }
// Command to minimize the window to tray
#[tauri::command]
fn minimize_to_tray(app_handle: AppHandle) -> Result<(), String> {
println!("Minimizing to tray");
// Get the main window
let window = app_handle.get_window("main")
.ok_or_else(|| "Failed to get main window".to_string())?;
// Hide the window
window.hide().map_err(|e| format!("Failed to hide window: {}", e))?;
// The tray should already be visible as configured in tauri.conf.json
println!("Window hidden, application should now be accessible from the system tray");
Ok(())
}
// Function to load cursor files from src/cursors directory // Function to load cursor files from src/cursors directory
fn load_cursor_files() -> Vec<Cursor> { fn load_cursor_files() -> Vec<Cursor> {
println!("=== RESOURCE DEBUG [load_cursor_files] ==="); println!("=== RESOURCE DEBUG [load_cursor_files] ===");
let mut cursors = Vec::new(); let mut cursors = Vec::new();
// Determine the path to src/cursors relative to src-tauri (CARGO_MANIFEST_DIR) // Determine the path to src/cursors relative to src-tauri (CARGO_MANIFEST_DIR)
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
println!("CARGO_MANIFEST_DIR: {}", manifest_dir.display()); println!("CARGO_MANIFEST_DIR: {}", manifest_dir.display());
// Check multiple possible locations // Check multiple possible locations
let possible_cursor_dirs = vec![ let possible_cursor_dirs = vec![
manifest_dir.join("../src/cursors"), manifest_dir.join("../src/cursors"),
@@ -184,26 +203,26 @@ fn load_cursor_files() -> Vec<Cursor> {
manifest_dir.join("cursors"), manifest_dir.join("cursors"),
manifest_dir.clone(), // Check manifest dir itself manifest_dir.clone(), // Check manifest dir itself
]; ];
println!("CHECKING FOR PNG/CUR FILES IN MULTIPLE DIRECTORIES:"); println!("CHECKING FOR PNG/CUR FILES IN MULTIPLE DIRECTORIES:");
for (i, cursors_dir) in possible_cursor_dirs.iter().enumerate() { for (i, cursors_dir) in possible_cursor_dirs.iter().enumerate() {
println!("Checking dir {}: {}", i+1, cursors_dir.display()); println!("Checking dir {}: {}", i+1, cursors_dir.display());
println!(" Directory exists: {}", cursors_dir.exists()); println!(" Directory exists: {}", cursors_dir.exists());
if cursors_dir.exists() { if cursors_dir.exists() {
println!(" Looking for cursor files in this directory..."); println!(" Looking for cursor files in this directory...");
if let Ok(entries) = std::fs::read_dir(cursors_dir) { if let Ok(entries) = std::fs::read_dir(cursors_dir) {
for entry in entries.filter_map(Result::ok) { for entry in entries.filter_map(Result::ok) {
let path = entry.path(); let path = entry.path();
println!(" Found: {}", path.display()); println!(" Found: {}", path.display());
if path.is_file() && path.extension().map_or(false, |ext| ext == "png" || ext == "cur") { if path.is_file() && path.extension().map_or(false, |ext| ext == "png" || ext == "cur") {
let name = path.file_stem() let name = path.file_stem()
.and_then(|s| s.to_str()) .and_then(|s| s.to_str())
.unwrap_or("Unknown") .unwrap_or("Unknown")
.to_string(); .to_string();
println!(" ADDING CURSOR: {} at path {}", name, path.display()); println!(" ADDING CURSOR: {} at path {}", name, path.display());
cursors.push(Cursor { cursors.push(Cursor {
name, name,
@@ -212,14 +231,14 @@ fn load_cursor_files() -> Vec<Cursor> {
} }
} }
} }
if !cursors.is_empty() { if !cursors.is_empty() {
println!(" Found {} cursor files in this directory.", cursors.len()); println!(" Found {} cursor files in this directory.", cursors.len());
break; break;
} }
} }
} }
// If we found no cursors, use mock data with paths relative to what resolve_resource expects // If we found no cursors, use mock data with paths relative to what resolve_resource expects
if cursors.is_empty() { if cursors.is_empty() {
println!("No cursors found in any directory. Using mock data instead."); println!("No cursors found in any directory. Using mock data instead.");
@@ -228,28 +247,28 @@ fn load_cursor_files() -> Vec<Cursor> {
for dir in &possible_cursor_dirs { for dir in &possible_cursor_dirs {
println!(" {}", dir.display()); println!(" {}", dir.display());
} }
// Mock data paths should reflect their eventual resource path structure // Mock data paths should reflect their eventual resource path structure
cursors.push(Cursor { cursors.push(Cursor {
name: "Default".to_string(), name: "Default".to_string(),
path: "src/cursors/default.png".to_string(), // Filename will be 'default.png' path: "src/cursors/default.png".to_string(), // Filename will be 'default.png'
}); });
cursors.push(Cursor { cursors.push(Cursor {
name: "Icon".to_string(), name: "Icon".to_string(),
path: "src/cursors/icon.png".to_string(), // Filename will be 'icon.png' path: "src/cursors/icon.png".to_string(), // Filename will be 'icon.png'
}); });
cursors.push(Cursor { cursors.push(Cursor {
name: "Red".to_string(), name: "Red".to_string(),
path: "src/cursors/red.png".to_string(), // Filename will be 'red.png' path: "src/cursors/red.png".to_string(), // Filename will be 'red.png'
}); });
println!("Created {} mock cursor entries", cursors.len()); println!("Created {} mock cursor entries", cursors.len());
} }
println!("Loaded {} cursor files", cursors.len()); println!("Loaded {} cursor files", cursors.len());
// Sort cursors by name for consistent ordering // Sort cursors by name for consistent ordering
cursors.sort_by(|a, b| a.name.cmp(&b.name)); cursors.sort_by(|a, b| a.name.cmp(&b.name));
cursors cursors
@@ -258,34 +277,153 @@ fn load_cursor_files() -> Vec<Cursor> {
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
println!("Starting Cursor Rush application"); println!("Starting Cursor Rush application");
// Initialize platform-specific cursor handling // Initialize platform-specific cursor handling
if let Err(e) = platform::initialize() { if let Err(e) = platform::initialize() {
eprintln!("Failed to initialize platform: {}", e); eprintln!("Failed to initialize platform: {}", e);
} }
// Load cursors from assets directory // Load cursors from assets directory
let cursors = load_cursor_files(); let cursors = load_cursor_files();
// Create app state // Create app state
let app_state = AppState { let app_state = AppState {
cursors, cursors,
current_index: 0, current_index: 0,
}; };
// Create system tray menu
let next_cursor_item = CustomMenuItem::new("next_cursor".to_string(), "Next cursor");
let show_window_item = CustomMenuItem::new("show_window".to_string(), "Show window");
let quit_item = CustomMenuItem::new("quit".to_string(), "Quit");
let tray_menu = SystemTrayMenu::new()
.add_item(next_cursor_item)
.add_item(show_window_item)
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(quit_item);
let system_tray = SystemTray::new().with_menu(tray_menu);
// This is one of the simplest ways to initialize Tauri // This is one of the simplest ways to initialize Tauri
tauri::Builder::default() tauri::Builder::default()
.manage(AppStateWrapper(Mutex::new(app_state))) .manage(AppStateWrapper(Mutex::new(app_state)))
.system_tray(system_tray)
.on_system_tray_event(|app, event| match event {
SystemTrayEvent::LeftClick { .. } => {
println!("System tray received a left click");
if let Some(window) = app.get_window("main") {
match window.is_visible() {
Ok(visible) => {
if visible {
println!("Window is already visible, focusing it");
let _ = window.set_focus();
} else {
println!("Window is hidden, showing it");
if let Err(e) = window.show() {
eprintln!("Failed to show window: {}", e);
}
if let Err(e) = window.set_focus() {
eprintln!("Failed to focus window: {}", e);
}
}
},
Err(e) => {
eprintln!("Failed to check window visibility: {}", e);
// Try to show it anyway
if let Err(e) = window.show() {
eprintln!("Failed to show window: {}", e);
}
}
}
} else {
eprintln!("Main window not found");
}
}
SystemTrayEvent::MenuItemClick { id, .. } => {
println!("Menu item clicked: {}", id);
match id.as_str() {
"next_cursor" => {
println!("Next cursor requested from tray");
let state = app.state::<AppStateWrapper>();
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];
if Path::new(&cursor.path).exists() {
if let Err(e) = platform::set_system_cursor(&cursor.path) {
eprintln!("Error applying cursor from tray: {}", e);
} else {
// Emit an event to notify the frontend
let _ = app.emit_all("cursor_changed", app_state.current_index);
}
}
}
}
"show_window" => {
println!("Show window requested from tray");
if let Some(window) = app.get_window("main") {
if let Err(e) = window.show() {
eprintln!("Failed to show window: {}", e);
}
if let Err(e) = window.set_focus() {
eprintln!("Failed to focus window: {}", e);
}
} else {
eprintln!("Main window not found");
}
}
"quit" => {
println!("Quit requested from tray");
// 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),
}
app.exit(0);
}
_ => {}
}
}
_ => {}
})
.on_window_event(move |event| {
match event.event() {
WindowEvent::CloseRequested { api, .. } => {
// Prevent the default close action
api.prevent_close();
// Get the window and hide it
let window = event.window();
println!("Window close requested in backend handler, hiding window");
// Hide the window instead of closing it
if let Err(e) = window.hide() {
eprintln!("Failed to hide window: {}", e);
} else {
println!("Window hidden successfully, app is now in system tray");
}
}
_ => {}
}
})
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
get_cursors, get_cursors,
get_current_cursor_index, get_current_cursor_index,
select_cursor, select_cursor,
next_cursor, next_cursor,
quit_app quit_app,
minimize_to_tray
]) ])
.setup(|_app| { .setup(|_app| {
// Register for events or perform additional setup // Register for events or perform additional setup
println!("Tauri application setup complete"); println!("Tauri application setup complete");
// We're not setting up a window close handler here anymore
// The frontend will handle this based on the minimize-to-tray setting
Ok(()) Ok(())
}) })
// This matches exactly how it was before our changes // This matches exactly how it was before our changes
+14 -2
View File
@@ -14,7 +14,11 @@
"all": false, "all": false,
"window": { "window": {
"all": false, "all": false,
"startDragging": true "startDragging": true,
"minimize": true,
"hide": true,
"show": true,
"close": true
}, },
"shell": { "shell": {
"all": false, "all": false,
@@ -27,6 +31,12 @@
] ]
} }
}, },
"systemTray": {
"iconPath": "icons/icon.png",
"iconAsTemplate": false,
"menuOnLeftClick": false,
"title": "Cursor Rush"
},
"bundle": { "bundle": {
"active": true, "active": true,
"identifier": "com.cursor-rush.dev", "identifier": "com.cursor-rush.dev",
@@ -47,13 +57,15 @@
}, },
"windows": [ "windows": [
{ {
"label": "main",
"title": "Cursor Rush", "title": "Cursor Rush",
"width": 310, "width": 310,
"height": 600, "height": 600,
"resizable": true, "resizable": true,
"fullscreen": false, "fullscreen": false,
"decorations": false, "decorations": false,
"transparent": true "transparent": true,
"visible": true
} }
] ]
} }
+89 -20
View File
@@ -1,4 +1,4 @@
import { useState, useEffect } from "react"; import { useState, useEffect, useCallback } from "react";
import { invoke } from "@tauri-apps/api/tauri"; import { invoke } from "@tauri-apps/api/tauri";
import { appWindow } from "@tauri-apps/api/window"; import { appWindow } from "@tauri-apps/api/window";
import "./App.css"; import "./App.css";
@@ -17,8 +17,37 @@ function App() {
const [activePage, setActivePage] = useState("cursors"); const [activePage, setActivePage] = useState("cursors");
const [cursors, setCursors] = useState([]); const [cursors, setCursors] = useState([]);
const [currentCursorIndex, setCurrentCursorIndex] = useState(0); const [currentCursorIndex, setCurrentCursorIndex] = useState(0);
const [settings, setSettings] = useState({
minimizeToTrayOnClose: true,
startWithWindows: false,
autoApplyCursorOnSelection: true
});
// Load cursors on mount // Function to minimize app to tray
const minimizeToTray = useCallback(async () => {
try {
console.log('Minimizing app to tray');
// First hide the window to ensure it disappears immediately
await appWindow.hide();
console.log('Window hidden, now ensuring tray is visible');
// Then call the backend to ensure the tray is visible
await invoke("minimize_to_tray");
console.log('minimize_to_tray command completed successfully');
} catch (error) {
console.error("Failed to minimize to tray:", error);
// Fallback to window API if the invoke fails
try {
console.log('Trying fallback hide');
await appWindow.hide();
console.log('Fallback hide succeeded');
} catch (fallbackError) {
console.error("Fallback hide also failed:", fallbackError);
}
}
}, []);
// Load cursors on mount and set up event listeners
useEffect(() => { useEffect(() => {
const loadCursors = async () => { const loadCursors = async () => {
try { try {
@@ -26,7 +55,7 @@ function App() {
const loadedCursors = await invoke("get_cursors"); const loadedCursors = await invoke("get_cursors");
console.log("Loaded cursors:", loadedCursors); console.log("Loaded cursors:", loadedCursors);
setCursors(loadedCursors || []); setCursors(loadedCursors || []);
// Get current cursor index // Get current cursor index
const currentIndex = await invoke("get_current_cursor_index"); const currentIndex = await invoke("get_current_cursor_index");
setCurrentCursorIndex(currentIndex); setCurrentCursorIndex(currentIndex);
@@ -47,6 +76,53 @@ function App() {
loadCursors(); loadCursors();
}, []); }, []);
// Set up window close event listener
useEffect(() => {
const setupCloseHandler = async () => {
const unlisten = await appWindow.onCloseRequested(async (event) => {
console.log('Window close requested');
if (settings.minimizeToTrayOnClose) {
console.log('Minimize to tray setting is enabled, preventing close and hiding instead');
// Prevent the window from closing
event.preventDefault();
// Hide the window instead
try {
console.log('Calling minimizeToTray function');
await minimizeToTray();
console.log('Window should now be hidden and app should be in system tray');
} catch (error) {
console.error('Failed to minimize to tray on close:', error);
}
} else {
console.log('Minimize to tray setting is disabled, allowing window to close');
// Let the window close normally, but we need to call quit_app to ensure proper cleanup
try {
await invoke("quit_app");
} catch (error) {
console.error("Failed to quit app:", error);
}
}
});
return unlisten;
};
let unlistenFn;
setupCloseHandler().then(unlisten => {
unlistenFn = unlisten;
console.log('Close handler set up successfully');
}).catch(err => console.error('Failed to set up close handler:', err));
// Clean up event listener on unmount
return () => {
if (unlistenFn) {
unlistenFn();
}
};
}, [settings.minimizeToTrayOnClose, minimizeToTray]);
// Functions to handle cursor operations using Tauri backend // Functions to handle cursor operations using Tauri backend
const selectCursor = async (index) => { const selectCursor = async (index) => {
try { try {
@@ -60,7 +136,7 @@ function App() {
const nextCursor = async () => { const nextCursor = async () => {
if (cursors.length === 0) return; if (cursors.length === 0) return;
try { try {
console.log('Switching to next cursor'); console.log('Switching to next cursor');
const newIndex = await invoke("next_cursor"); const newIndex = await invoke("next_cursor");
@@ -73,16 +149,6 @@ function App() {
} }
}; };
// 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 // Function to exit app
const quitApp = async () => { const quitApp = async () => {
try { try {
@@ -96,22 +162,25 @@ function App() {
return ( return (
<div className="modal-container"> <div className="modal-container">
<div className="modal"> <div className="modal">
<Header <Header
activePage={activePage} activePage={activePage}
setActivePage={setActivePage} setActivePage={setActivePage}
minimizeToTray={minimizeToTray} minimizeToTray={minimizeToTray}
quitApp={quitApp} quitApp={quitApp}
settings={settings}
/> />
<Content <Content
activePage={activePage} activePage={activePage}
cursors={cursors} cursors={cursors}
currentCursorIndex={currentCursorIndex} currentCursorIndex={currentCursorIndex}
selectCursor={selectCursor} selectCursor={selectCursor}
nextCursor={nextCursor} nextCursor={nextCursor}
settings={settings}
setSettings={setSettings}
/> />
<Footer <Footer
version="0.1.0" version="0.1.0"
status={cursors.length > 0 ? `${cursors.length} cursors loaded` : "No cursors found"} status={cursors.length > 0 ? `${cursors.length} cursors loaded` : "No cursors found"}
/> />
+6 -6
View File
@@ -7,19 +7,19 @@ import Cursors from './content_modules/Cursors';
import Crates from './content_modules/Crates'; import Crates from './content_modules/Crates';
import Settings from './content_modules/Settings'; import Settings from './content_modules/Settings';
function Content({ activePage, cursors, currentCursorIndex, selectCursor, nextCursor }) { function Content({ activePage, cursors, currentCursorIndex, selectCursor, nextCursor, settings, setSettings }) {
// Render the appropriate content based on active page // Render the appropriate content based on active page
const renderContent = () => { const renderContent = () => {
switch (activePage) { switch (activePage) {
case 'inventory': case 'inventory':
return <Inventory return <Inventory
cursors={cursors} cursors={cursors}
currentCursorIndex={currentCursorIndex} currentCursorIndex={currentCursorIndex}
selectCursor={selectCursor} selectCursor={selectCursor}
/>; />;
case 'cursors': case 'cursors':
return ( return (
<Cursors <Cursors
cursors={cursors} cursors={cursors}
currentCursorIndex={currentCursorIndex} currentCursorIndex={currentCursorIndex}
selectCursor={selectCursor} selectCursor={selectCursor}
@@ -29,9 +29,9 @@ function Content({ activePage, cursors, currentCursorIndex, selectCursor, nextCu
case 'crates': case 'crates':
return <Crates />; return <Crates />;
case 'settings': case 'settings':
return <Settings />; return <Settings settings={settings} setSettings={setSettings} />;
default: default:
return <Cursors return <Cursors
cursors={cursors} cursors={cursors}
currentCursorIndex={currentCursorIndex} currentCursorIndex={currentCursorIndex}
selectCursor={selectCursor} selectCursor={selectCursor}
@@ -39,7 +39,7 @@ function Content({ activePage, cursors, currentCursorIndex, selectCursor, nextCu
/>; />;
} }
}; };
return ( return (
<div className="app-content"> <div className="app-content">
{renderContent()} {renderContent()}
+25 -10
View File
@@ -1,12 +1,12 @@
import React from 'react'; import React from 'react';
import './Header.css'; import './Header.css';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { import {
faGripVertical, faGripVertical,
faBoxOpen, faBoxOpen,
faMousePointer, faMousePointer,
faBoxes, faBoxes,
faCog, faCog,
faBoxArchive, faBoxArchive,
faBox, faBox,
faWarehouse, faWarehouse,
@@ -16,7 +16,7 @@ import {
faBagShopping faBagShopping
} from '@fortawesome/free-solid-svg-icons'; } from '@fortawesome/free-solid-svg-icons';
function Header({ activePage, setActivePage, minimizeToTray, quitApp }) { function Header({ activePage, setActivePage, minimizeToTray, quitApp, settings }) {
// Navigation buttons for each page with icons // Navigation buttons for each page with icons
const navItems = [ const navItems = [
{ id: 'inventory', icon: faBagShopping, tooltip: 'Inventory' }, { id: 'inventory', icon: faBagShopping, tooltip: 'Inventory' },
@@ -24,12 +24,23 @@ function Header({ activePage, setActivePage, minimizeToTray, quitApp }) {
{ id: 'crates', icon: faBoxOpen, tooltip: 'Crates' }, { id: 'crates', icon: faBoxOpen, tooltip: 'Crates' },
{ id: 'settings', icon: faCog, tooltip: 'Settings' } { id: 'settings', icon: faCog, tooltip: 'Settings' }
]; ];
// Handle close button click based on settings
const handleCloseClick = () => {
if (settings && settings.minimizeToTrayOnClose) {
console.log('Close button clicked, minimizing to tray based on settings');
minimizeToTray();
} else {
console.log('Close button clicked, quitting application based on settings');
quitApp();
}
};
return ( return (
<header className="app-header" data-tauri-drag-region> <header className="app-header" data-tauri-drag-region>
<div className="tabs-container"> <div className="tabs-container">
{navItems.map((item) => ( {navItems.map((item) => (
<div <div
key={item.id} key={item.id}
className={`tab ${activePage === item.id ? 'active' : ''}`} className={`tab ${activePage === item.id ? 'active' : ''}`}
onClick={() => setActivePage(item.id)} onClick={() => setActivePage(item.id)}
@@ -42,7 +53,11 @@ function Header({ activePage, setActivePage, minimizeToTray, quitApp }) {
<button className="minimize-button" onClick={minimizeToTray} title="Minimize to Tray"> <button className="minimize-button" onClick={minimizeToTray} title="Minimize to Tray">
_ _
</button> </button>
<button className="close-button" onClick={quitApp} title="Quit Application"> <button
className="close-button"
onClick={handleCloseClick}
title={settings && settings.minimizeToTrayOnClose ? "Minimize to Tray" : "Quit Application"}
>
× ×
</button> </button>
</div> </div>
@@ -1,7 +1,15 @@
import React from 'react'; import React from 'react';
import './Settings.css'; import './Settings.css';
function Settings() { function Settings({ settings, setSettings }) {
// Handler for checkbox changes
const handleSettingChange = (settingName) => {
setSettings({
...settings,
[settingName]: !settings[settingName]
});
};
return ( return (
<div className="settings-container"> <div className="settings-container">
<h2 className="content-title">Settings</h2> <h2 className="content-title">Settings</h2>
@@ -10,23 +18,35 @@ function Settings() {
<h3>Application Settings</h3> <h3>Application Settings</h3>
<div className="setting-item"> <div className="setting-item">
<label> <label>
<input type="checkbox" /> <input
type="checkbox"
checked={settings.startWithWindows}
onChange={() => handleSettingChange('startWithWindows')}
/>
Start with Windows Start with Windows
</label> </label>
</div> </div>
<div className="setting-item"> <div className="setting-item">
<label> <label>
<input type="checkbox" checked /> <input
type="checkbox"
checked={settings.minimizeToTrayOnClose}
onChange={() => handleSettingChange('minimizeToTrayOnClose')}
/>
Minimize to tray on close Minimize to tray on close
</label> </label>
</div> </div>
</div> </div>
<div className="settings-group"> <div className="settings-group">
<h3>Cursor Settings</h3> <h3>Cursor Settings</h3>
<div className="setting-item"> <div className="setting-item">
<label> <label>
<input type="checkbox" checked /> <input
type="checkbox"
checked={settings.autoApplyCursorOnSelection}
onChange={() => handleSettingChange('autoApplyCursorOnSelection')}
/>
Auto-apply cursor on selection Auto-apply cursor on selection
</label> </label>
</div> </div>