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
- Global hotkey support
- Steam integration for item drops
- Tauri-based frontend with React
## Project Structure
```
src/
├── app/ # Application core logic
│ ├── mod.rs # Main app module exports
│ ├── state.rs # Application state management
│ ├── messages.rs # Message definitions
│ └── cursor.rs # Cursor-related functionality
├── ui/ # User interface components
│ ├── mod.rs # UI module exports
│ ├── view.rs # View rendering logic
│ └── tray.rs # System tray functionality
├── platform/ # Platform-specific implementations
│ ├── mod.rs # Platform module exports
│ ├── windows/ # Windows-specific code
│ │ ├── mod.rs # Windows module exports
│ │ ├── types.rs # Windows type definitions
│ │ ├── state.rs # Windows state management
│ │ └── cursor.rs # Windows cursor handling
│ ├── macos/ # macOS-specific code (in development)
│ │ ├── mod.rs # macOS module exports
│ │ ├── state.rs # macOS state management
│ │ └── 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
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
│ │ ├── integrations/ # External integrations
│ │ │ ├── mod.rs # Integrations module exports
│ │ │ └── steam.rs # Steam integration
│ │ └── platform/ # Platform-specific code
│ │ ├── mod.rs # Platform module exports
│ │ ├── types.rs # Common type definitions
│ │ ├── windows/ # Windows-specific code
│ │ ├── macos/ # macOS-specific code
│ │ └── linux/ # Linux-specific code
│ └── Cargo.toml # Rust dependencies
└── package.json # npm dependencies
```
## Building
@@ -60,6 +44,8 @@ src/
- Rust 1.70.0 or later
- 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
@@ -68,41 +54,49 @@ src/
### Dependencies
The application uses the following major dependencies:
- iced 0.13.1 - GUI framework
- tray-icon 0.20.1 - System tray functionality
- Tauri 1.6.0 - Cross-platform framework
- React - Frontend framework
- image 0.24.7 - Image processing
- global-hotkey 0.4.2 - Global hotkey registration
- steamworks 0.11.0 - Steam integration
- winapi 0.3.9 - Windows API bindings
- once_cell 1.19.0 - Lazy initialization
### Build Commands
```bash
# Debug build
cargo build
# Navigate to the frontend directory
cd frontend
# Release build
cargo build --release
# Install npm dependencies
npm install
# Run in development mode
npm run tauri dev
# Build for production
npm run tauri build
```
## Usage
1. Place your cursor PNG files in the `assets` directory (PNG format only)
2. Run the application using `cargo run` or the compiled executable
3. The application will load all PNG files from the assets directory
4. Select a cursor from the list in the UI
5. The application will replace your system cursor with the selected image
6. When closing the window, the application minimizes to the system tray
7. To fully exit, use the system tray menu or press the exit hotkey
1. Make sure you have Node.js and npm installed
2. Navigate to the frontend directory: `cd frontend`
3. Install dependencies if you haven't already: `npm install`
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 two sample cursors in the `assets` directory:
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 `assets` directory
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
@@ -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
- Help with documentation
- Contribute to macOS and Linux platform support
- Improve the Tauri integration
## License
+2 -1
View File
@@ -14,13 +14,14 @@ rust-version = "1.56"
tauri-build = { version = "1.5.2", features = [] }
[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 = { 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"
steamworks = "0.10.0"
[features]
# this feature is used for production builds or when `devPath` points to the filesystem
# 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);
}
}
+140 -2
View File
@@ -1,10 +1,11 @@
use std::sync::Mutex;
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::env;
mod platform;
mod integrations;
// Define cursor structure
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -168,6 +169,24 @@ fn quit_app(app_handle: AppHandle) {
});
}
// 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
fn load_cursor_files() -> Vec<Cursor> {
println!("=== RESOURCE DEBUG [load_cursor_files] ===");
@@ -273,19 +292,138 @@ pub fn run() {
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
tauri::Builder::default()
.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![
get_cursors,
get_current_cursor_index,
select_cursor,
next_cursor,
quit_app
quit_app,
minimize_to_tray
])
.setup(|_app| {
// Register for events or perform additional setup
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(())
})
// This matches exactly how it was before our changes
+14 -2
View File
@@ -14,7 +14,11 @@
"all": false,
"window": {
"all": false,
"startDragging": true
"startDragging": true,
"minimize": true,
"hide": true,
"show": true,
"close": true
},
"shell": {
"all": false,
@@ -27,6 +31,12 @@
]
}
},
"systemTray": {
"iconPath": "icons/icon.png",
"iconAsTemplate": false,
"menuOnLeftClick": false,
"title": "Cursor Rush"
},
"bundle": {
"active": true,
"identifier": "com.cursor-rush.dev",
@@ -47,13 +57,15 @@
},
"windows": [
{
"label": "main",
"title": "Cursor Rush",
"width": 310,
"height": 600,
"resizable": true,
"fullscreen": false,
"decorations": false,
"transparent": true
"transparent": true,
"visible": true
}
]
}
+81 -12
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 { appWindow } from "@tauri-apps/api/window";
import "./App.css";
@@ -17,8 +17,37 @@ function App() {
const [activePage, setActivePage] = useState("cursors");
const [cursors, setCursors] = useState([]);
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(() => {
const loadCursors = async () => {
try {
@@ -47,6 +76,53 @@ function App() {
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
const selectCursor = async (index) => {
try {
@@ -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
const quitApp = async () => {
try {
@@ -101,6 +167,7 @@ function App() {
setActivePage={setActivePage}
minimizeToTray={minimizeToTray}
quitApp={quitApp}
settings={settings}
/>
<Content
@@ -109,6 +176,8 @@ function App() {
currentCursorIndex={currentCursorIndex}
selectCursor={selectCursor}
nextCursor={nextCursor}
settings={settings}
setSettings={setSettings}
/>
<Footer
+2 -2
View File
@@ -7,7 +7,7 @@ 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 }) {
function Content({ activePage, cursors, currentCursorIndex, selectCursor, nextCursor, settings, setSettings }) {
// Render the appropriate content based on active page
const renderContent = () => {
switch (activePage) {
@@ -29,7 +29,7 @@ function Content({ activePage, cursors, currentCursorIndex, selectCursor, nextCu
case 'crates':
return <Crates />;
case 'settings':
return <Settings />;
return <Settings settings={settings} setSettings={setSettings} />;
default:
return <Cursors
cursors={cursors}
+17 -2
View File
@@ -16,7 +16,7 @@ import {
faBagShopping
} 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
const navItems = [
{ id: 'inventory', icon: faBagShopping, tooltip: 'Inventory' },
@@ -25,6 +25,17 @@ function Header({ activePage, setActivePage, minimizeToTray, quitApp }) {
{ 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 (
<header className="app-header" data-tauri-drag-region>
<div className="tabs-container">
@@ -42,7 +53,11 @@ function Header({ activePage, setActivePage, minimizeToTray, quitApp }) {
<button className="minimize-button" onClick={minimizeToTray} title="Minimize to Tray">
_
</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>
</div>
@@ -1,7 +1,15 @@
import React from 'react';
import './Settings.css';
function Settings() {
function Settings({ settings, setSettings }) {
// Handler for checkbox changes
const handleSettingChange = (settingName) => {
setSettings({
...settings,
[settingName]: !settings[settingName]
});
};
return (
<div className="settings-container">
<h2 className="content-title">Settings</h2>
@@ -10,13 +18,21 @@ function Settings() {
<h3>Application Settings</h3>
<div className="setting-item">
<label>
<input type="checkbox" />
<input
type="checkbox"
checked={settings.startWithWindows}
onChange={() => handleSettingChange('startWithWindows')}
/>
Start with Windows
</label>
</div>
<div className="setting-item">
<label>
<input type="checkbox" checked />
<input
type="checkbox"
checked={settings.minimizeToTrayOnClose}
onChange={() => handleSettingChange('minimizeToTrayOnClose')}
/>
Minimize to tray on close
</label>
</div>
@@ -26,7 +42,11 @@ function Settings() {
<h3>Cursor Settings</h3>
<div className="setting-item">
<label>
<input type="checkbox" checked />
<input
type="checkbox"
checked={settings.autoApplyCursorOnSelection}
onChange={() => handleSettingChange('autoApplyCursorOnSelection')}
/>
Auto-apply cursor on selection
</label>
</div>