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
+2 -1
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);
}
}
+140 -2
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)]
@@ -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 // 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] ===");
@@ -273,19 +292,138 @@ pub fn run() {
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
} }
] ]
} }
+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 { 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 {
@@ -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 {
@@ -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 {
@@ -101,6 +167,7 @@ function App() {
setActivePage={setActivePage} setActivePage={setActivePage}
minimizeToTray={minimizeToTray} minimizeToTray={minimizeToTray}
quitApp={quitApp} quitApp={quitApp}
settings={settings}
/> />
<Content <Content
@@ -109,6 +176,8 @@ function App() {
currentCursorIndex={currentCursorIndex} currentCursorIndex={currentCursorIndex}
selectCursor={selectCursor} selectCursor={selectCursor}
nextCursor={nextCursor} nextCursor={nextCursor}
settings={settings}
setSettings={setSettings}
/> />
<Footer <Footer
+2 -2
View File
@@ -7,7 +7,7 @@ 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) {
@@ -29,7 +29,7 @@ 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}
+17 -2
View File
@@ -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' },
@@ -25,6 +25,17 @@ function Header({ activePage, setActivePage, minimizeToTray, quitApp }) {
{ 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">
@@ -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,13 +18,21 @@ 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>
@@ -26,7 +42,11 @@ function Settings() {
<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>