diff --git a/.editorconfig b/.editorconfig index 9d08a1a82..897763fbb 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,9 +1,37 @@ root = true +[*.toml] +indent_size = 4 +indent_style = space +max_line_length = 100 +trim_trailing_whitespace = true + +[*.rs] +indent_size = 2 +indent_style = space +max_line_length = 100 +trim_trailing_whitespace = true + +[*.md] +# double whitespace at end of line +# denotes a line break in Markdown +trim_trailing_whitespace = false + +[*.{js,ts,vue,tsx,jsx,html,css,json,yaml,yml}] +indent_size = 2 +indent_style = space +trim_trailing_whitespace = true + +[*.go] +indent_size = 4 +indent_style = tab +trim_trailing_whitespace = true + +[*.proto] +indent_size = 2 +indent_style = space + [*] charset = utf-8 -indent_style = space -indent_size = 2 end_of_line = lf insert_final_newline = true -trim_trailing_whitespace = true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 60c6ba645..3e20326fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,13 @@ on: branches: - main +env: + # Rust + CARGO_TERM_COLOR: always + # https://github.com/Mozilla-Actions/sccache-action#rust-code + RUSTC_WRAPPER: sccache + SCCACHE_GHA_ENABLED: 'true' + jobs: lint: name: Lint @@ -16,15 +23,16 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - with: - components: rustfmt,clippy + # # Rust + # - uses: dtolnay/rust-toolchain@nightly + # - uses: mozilla-actions/sccache-action@v0.0.3 - name: Install system dependencies run: | sudo apt update sudo apt install -y libwebkit2gtk-4.1-dev + # Node.js - uses: pnpm/action-setup@v3 - uses: actions/setup-node@v4 with: @@ -39,12 +47,20 @@ jobs: pnpm run lint pnpm run lint:rust + # - name: Lint Rust + # run: | + # cargo +nightly fmt --all --check + # cargo clippy -- -W clippy::pedantic -W clippy::nursery -A clippy::missing-errors-doc -A clippy::module_name_repetitions + build-test: name: Build Test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable + + # # Rust + # - uses: dtolnay/rust-toolchain@nightly + # - uses: mozilla-actions/sccache-action@v0.0.3 - name: Install system dependencies run: | @@ -64,13 +80,18 @@ jobs: run: | pnpm run build:packages pnpm run build:apps - pnpm run build:crates + + # - name: Build Rust + # run: | + # pnpm run build:crates typecheck: name: Type Check runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + + # Node.js - uses: pnpm/action-setup@v3 - uses: actions/setup-node@v4 with: diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 000000000..9f36e3288 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +nodejs 24.1.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 89af0ca5b..c27169c44 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,6 +12,10 @@ Hello! Thank you for your interest in contributing to this project. This guide w
Windows setup +0. Download [Visual Studio](https://visualstudio.microsoft.com/downloads/), and follow the instructions here: https://rust-lang.github.io/rustup/installation/windows-msvc.html#walkthrough-installing-visual-studio-2022 + + > Make sure to install Windows SDK and C++ build tools when installing Visual Studio. + 1. Open PowerShell 2. Install [`scoop`](https://scoop.sh/) @@ -20,12 +24,21 @@ Hello! Thank you for your interest in contributing to this project. This guide w Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression ``` -3. Install `git`, `node` through `scoop` +3. Install `git`, Node.js, `rustup`, `msvc` through `scoop` ```powershell - scoop install git nodejs + scoop install git nodejs rustup + + # For Rust dependencies + # Not required if you are not going to develop on either crates or apps/tamagotchi + scoop install main/rust-msvc + # Rust & Windows specific + rustup toolchain install stable-x86_64-pc-windows-msvc + rustup default stable-x86_64-pc-windows-msvc ``` + > https://stackoverflow.com/a/64121601 + 4. Install `pnpm` through `corepack` ```powershell @@ -112,6 +125,10 @@ git checkout -b ```shell corepack enable pnpm install + +# For Rust dependencies +# Not required if you are not going to develop on either crates or apps/tamagotchi +cargo fetch ``` > [!NOTE] diff --git a/apps/stage-tamagotchi/src-tauri/build.rs b/apps/stage-tamagotchi/src-tauri/build.rs index 795b9b7c8..311900de5 100644 --- a/apps/stage-tamagotchi/src-tauri/build.rs +++ b/apps/stage-tamagotchi/src-tauri/build.rs @@ -1,3 +1,3 @@ fn main() { - tauri_build::build() + tauri_build::build(); } diff --git a/apps/stage-tamagotchi/src-tauri/src/app_click_through/mod.rs b/apps/stage-tamagotchi/src-tauri/src/app_click_through/mod.rs new file mode 100644 index 000000000..b3362a193 --- /dev/null +++ b/apps/stage-tamagotchi/src-tauri/src/app_click_through/mod.rs @@ -0,0 +1,3 @@ +pub mod native_macos; +pub mod native_windows; +pub mod state; diff --git a/apps/stage-tamagotchi/src-tauri/src/app_click_through/native_macos.rs b/apps/stage-tamagotchi/src-tauri/src/app_click_through/native_macos.rs new file mode 100644 index 000000000..8299311de --- /dev/null +++ b/apps/stage-tamagotchi/src-tauri/src/app_click_through/native_macos.rs @@ -0,0 +1,72 @@ +#[cfg(target_os = "macos")] +use objc2::{class, msg_send}; + +/// Get cursor position relative to the window +#[cfg(target_os = "macos")] +pub async fn is_cursor_in_window(window: &tauri::Window) -> bool { + use objc2_foundation::{NSPoint, NSRect}; + + unsafe { + // Get cursor position in screen coordinates (macOS coordinates - origin at bottom left) + let mouse_location: NSPoint = msg_send![class!(NSEvent), mouseLocation]; + + // Get all screens + // + // We need screens count because for multiple-display users, + // in macOS, the Native API returns the mouse coordinates relative to the primary + // display, for example, if we say the primary display is 1920x1080, another two lies + // on both sides of the primary display with the size of 1920x1080 too, mouse coordinates + // will be 0 at the left edge of p-display, and 1920 at the right edge of the p-display, + // -1080 at the left edge of the left-side display, and 2160 at the right edge of the right-side + // display. + let screens: *const objc2::runtime::AnyObject = msg_send![class!(NSScreen), screens]; + let screens_count: usize = msg_send![screens, count]; + + // Get window position and size from Tauri + // Get the NSWindow from Tauri window to access native properties + // + // While Tauri does provide us window.outer_position() and window.outer_size() + // it's not enough to determine if the cursor is inside the window. + // The value of those two functions returns the in-compatible coordinates (top-left origin) + // with OBJ-C Native API values. This produces a lot of issues when calculating the collision + // of the cursor relative to the window. + // + // We need to get the window's frame in macOS coordinates (bottom-left origin) + // and check if the cursor is inside that frame. + let ns_window: *mut objc2::runtime::AnyObject = window.ns_window().unwrap().cast(); + let window_frame: NSRect = msg_send![ns_window, frame]; + + // Log all screens information + for _ in 0..screens_count { + // For debugging purpose, screen object, frame size of the screen, visible frame size of the screen, + // and the scale factor of the screen can be obtained as follows: + // + // let screen: *const objc2::runtime::AnyObject = msg_send![screens, objectAtIndex: i]; + // let frame: NSRect = msg_send![screen, frame]; + // let visible_frame: NSRect = msg_send![screen, visibleFrame]; + // let scale_factor: f64 = msg_send![screen, backingScaleFactor]; + + // Check if mouse is inside our window's frame + let is_inside = mouse_location.x >= window_frame.origin.x && mouse_location.x <= (window_frame.origin.x + window_frame.size.width) && mouse_location.y >= window_frame.origin.y && mouse_location.y <= (window_frame.origin.y + window_frame.size.height); + + if is_inside { + return true; + } + } + } + false +} + +/// Check if modifier key is pressed (Option/Alt or Control) +#[cfg(target_os = "macos")] +pub fn is_modifier_pressed() -> bool { + unsafe { + let event_class: &'static objc2::runtime::AnyClass = class!(NSEvent); + let flags: u64 = msg_send![event_class, modifierFlags]; + + // Check for Alt/Option (1 << 19) or Control (1 << 18) + // let ctrl_pressed = (flags & (1 << 18)) != 0; + // alt_pressed || ctrl_pressed + (flags & (1 << 19)) != 0 + } +} diff --git a/apps/stage-tamagotchi/src-tauri/src/app_click_through/native_windows.rs b/apps/stage-tamagotchi/src-tauri/src/app_click_through/native_windows.rs new file mode 100644 index 000000000..737ada8d0 --- /dev/null +++ b/apps/stage-tamagotchi/src-tauri/src/app_click_through/native_windows.rs @@ -0,0 +1,35 @@ +/// Get cursor position relative to the window +#[cfg(target_os = "windows")] +pub async fn is_cursor_in_window(window: &tauri::Window) -> bool { + use windows::Win32::{ + Foundation::{POINT, RECT}, + UI::WindowsAndMessaging::{GetCursorPos, GetWindowRect}, + }; + + unsafe { + let hwnd = window.hwnd().unwrap(); + let mut cursor_pos = POINT::default(); + let mut window_rect = RECT::default(); + + // Get cursor position in screen coordinates + if GetCursorPos(&mut cursor_pos).is_ok() { + // Get window rectangle + if GetWindowRect(hwnd, &mut window_rect).is_ok() { + // Check if cursor is inside window bounds + return cursor_pos.x >= window_rect.left && cursor_pos.x <= window_rect.right && cursor_pos.y >= window_rect.top && cursor_pos.y <= window_rect.bottom; + } + } + + false + } +} + +/// Check if modifier key is pressed (Alt key) +#[cfg(target_os = "windows")] +pub fn is_modifier_pressed() -> bool { + use windows::Win32::UI::Input::KeyboardAndMouse::{GetAsyncKeyState, VK_MENU}; + unsafe { + // Check if Alt key is pressed (VK_MENU is the virtual key code for Alt) + GetAsyncKeyState(VK_MENU.0 as i32) < 0 + } +} diff --git a/apps/stage-tamagotchi/src-tauri/src/app_click_through/state.rs b/apps/stage-tamagotchi/src-tauri/src/app_click_through/state.rs new file mode 100644 index 000000000..4683d7a9f --- /dev/null +++ b/apps/stage-tamagotchi/src-tauri/src/app_click_through/state.rs @@ -0,0 +1,37 @@ +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; + +use tauri::{Emitter, Manager}; + +#[derive(Default)] +pub struct WindowClickThroughState { + pub monitoring_enabled: Arc, + pub enabled: Arc, + pub cursor_inside: Arc, +} + +pub fn set_cursor_inside(window: &tauri::Window, is_inside: bool) -> Result<(), String> { + let state = window.state::(); + + state.cursor_inside.store(is_inside, Ordering::Relaxed); + + window.set_ignore_cursor_events(is_inside).map_err(|e| format!("Failed to set click-through state: {e}"))?; + + let _ = window.emit("tauri-app:window-click-through:is-inside", is_inside); + + Ok(()) +} + +pub fn set_click_through_enabled(window: &tauri::Window, enabled: bool) -> Result<(), String> { + let state = window.state::(); + + state.enabled.store(enabled, Ordering::Relaxed); + + window.set_ignore_cursor_events(enabled).map_err(|e| format!("Failed to set click-through state: {e}"))?; + + let _ = window.emit("tauri-app:window-click-through:enabled", enabled); + + Ok(()) +} diff --git a/apps/stage-tamagotchi/src-tauri/src/windows/chat.rs b/apps/stage-tamagotchi/src-tauri/src/app_windows/chat.rs similarity index 74% rename from apps/stage-tamagotchi/src-tauri/src/windows/chat.rs rename to apps/stage-tamagotchi/src-tauri/src/app_windows/chat.rs index 92770ffc4..61f16c0b0 100644 --- a/apps/stage-tamagotchi/src-tauri/src/windows/chat.rs +++ b/apps/stage-tamagotchi/src-tauri/src/app_windows/chat.rs @@ -1,20 +1,16 @@ use std::path::Path; -use tauri::{WebviewUrl, WebviewWindowBuilder}; #[cfg(target_os = "macos")] use tauri::TitleBarStyle; +use tauri::{WebviewUrl, WebviewWindowBuilder}; pub fn new_chat_window(app: &tauri::AppHandle) -> Result<(), tauri::Error> { - let mut builder = WebviewWindowBuilder::new( - app, - "chat", - WebviewUrl::App(Path::new("#/chat").to_path_buf()), - ) - .title("Chat") - .inner_size(600.0, 800.0) - .shadow(true) - .transparent(false) - .accept_first_mouse(true); + let mut builder = WebviewWindowBuilder::new(app, "chat", WebviewUrl::App(Path::new("#/chat").to_path_buf())) + .title("Chat") + .inner_size(600.0, 800.0) + .shadow(true) + .transparent(false) + .accept_first_mouse(true); #[cfg(target_os = "macos")] { @@ -29,5 +25,5 @@ pub fn new_chat_window(app: &tauri::AppHandle) -> Result<(), tauri::Error> { } builder.build()?; - return Ok(()); + Ok(()) } diff --git a/apps/stage-tamagotchi/src-tauri/src/windows/mod.rs b/apps/stage-tamagotchi/src-tauri/src/app_windows/mod.rs similarity index 100% rename from apps/stage-tamagotchi/src-tauri/src/windows/mod.rs rename to apps/stage-tamagotchi/src-tauri/src/app_windows/mod.rs diff --git a/apps/stage-tamagotchi/src-tauri/src/windows/settings.rs b/apps/stage-tamagotchi/src-tauri/src/app_windows/settings.rs similarity index 73% rename from apps/stage-tamagotchi/src-tauri/src/windows/settings.rs rename to apps/stage-tamagotchi/src-tauri/src/app_windows/settings.rs index 511548fc5..c63891f5f 100644 --- a/apps/stage-tamagotchi/src-tauri/src/windows/settings.rs +++ b/apps/stage-tamagotchi/src-tauri/src/app_windows/settings.rs @@ -1,20 +1,16 @@ use std::path::Path; -use tauri::{WebviewUrl, WebviewWindowBuilder}; #[cfg(target_os = "macos")] use tauri::TitleBarStyle; +use tauri::{WebviewUrl, WebviewWindowBuilder}; pub fn new_settings_window(app: &tauri::AppHandle) -> Result<(), tauri::Error> { - let mut builder = WebviewWindowBuilder::new( - app, - "settings", - WebviewUrl::App(Path::new("#/settings").to_path_buf()), - ) - .title("Settings") - .inner_size(450.0, 800.0) - .shadow(true) - .transparent(false) - .accept_first_mouse(true); + let mut builder = WebviewWindowBuilder::new(app, "settings", WebviewUrl::App(Path::new("#/settings").to_path_buf())) + .title("Settings") + .inner_size(450.0, 800.0) + .shadow(true) + .transparent(false) + .accept_first_mouse(true); #[cfg(target_os = "macos")] { @@ -29,5 +25,5 @@ pub fn new_settings_window(app: &tauri::AppHandle) -> Result<(), tauri::Error> { } builder.build()?; - return Ok(()); + Ok(()) } diff --git a/apps/stage-tamagotchi/src-tauri/src/commands.rs b/apps/stage-tamagotchi/src-tauri/src/commands.rs index 18dc618ec..a094b7b28 100644 --- a/apps/stage-tamagotchi/src-tauri/src/commands.rs +++ b/apps/stage-tamagotchi/src-tauri/src/commands.rs @@ -1,6 +1,6 @@ use tauri::Manager; -use crate::windows; +use crate::app_windows; #[tauri::command] pub async fn open_settings_window(app: tauri::AppHandle) -> Result<(), tauri::Error> { @@ -10,7 +10,7 @@ pub async fn open_settings_window(app: tauri::AppHandle) -> Result<(), tauri::Er return Ok(()); } - windows::settings::new_settings_window(&app)?; + app_windows::settings::new_settings_window(&app)?; Ok(()) } @@ -22,6 +22,6 @@ pub async fn open_chat_window(app: tauri::AppHandle) -> Result<(), tauri::Error> return Ok(()); } - windows::chat::new_chat_window(&app)?; + app_windows::chat::new_chat_window(&app)?; Ok(()) } diff --git a/apps/stage-tamagotchi/src-tauri/src/lib.rs b/apps/stage-tamagotchi/src-tauri/src/lib.rs index 821cd299d..0ef0adee9 100644 --- a/apps/stage-tamagotchi/src-tauri/src/lib.rs +++ b/apps/stage-tamagotchi/src-tauri/src/lib.rs @@ -1,177 +1,54 @@ -use tauri::menu::{Menu, MenuItem}; -use tauri::tray::TrayIconBuilder; -use tauri::Emitter; -use tauri::RunEvent; +use std::{sync::atomic::Ordering, time::Duration}; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; -use std::time::Duration; +use tauri::{ + menu::{Menu, MenuItem}, + tray::TrayIconBuilder, + Emitter, + Manager, + RunEvent, + WebviewUrl, + WebviewWindowBuilder, +}; +use tauri_plugin_prevent_default::Flags; use tokio::time::sleep; -#[cfg(target_os = "macos")] -use objc2::{class, msg_send}; - -#[cfg(target_os = "macos")] -use objc2_foundation::{NSPoint, NSRect}; - -#[cfg(target_os = "macos")] -use tauri::{ActivationPolicy, TitleBarStyle}; -use tauri::{Manager, WebviewUrl, WebviewWindowBuilder}; -use tauri_plugin_prevent_default::Flags; - -#[cfg(target_os = "windows")] -use windows::Win32::UI::WindowsAndMessaging::{ - GetWindowLongW, SetWindowLongW, GWL_EXSTYLE, WS_EX_TRANSPARENT, -}; - +mod app_click_through; +mod app_windows; mod commands; -mod windows; -#[derive(Default)] -struct WindowClickThroughState { - is_click_through_monitoring_enabled: Arc, - is_click_through_enabled: Arc, - is_click_through_cursor_inside: Arc, -} - -fn set_cursor_inside(window: &tauri::Window, is_inside: bool) -> Result<(), String> { - let state = window.state::(); - - state - .is_click_through_cursor_inside - .store(is_inside, Ordering::Relaxed); - - window - .set_ignore_cursor_events(is_inside) - .map_err(|e| format!("Failed to set click-through state: {}", e))?; - - let _ = window.emit("tauri-app:window-click-through:is-inside", is_inside); - - Ok(()) -} - -fn set_click_through_enabled(window: &tauri::Window, enabled: bool) -> Result<(), String> { - let state = window.state::(); - - state - .is_click_through_enabled - .store(enabled, Ordering::Relaxed); - - window - .set_ignore_cursor_events(enabled) - .map_err(|e| format!("Failed to set click-through state: {}", e))?; - - let _ = window.emit("tauri-app:window-click-through:enabled", enabled); - - Ok(()) -} - -/// Get cursor position relative to the window #[cfg(target_os = "macos")] -async fn is_cursor_in_window(window: &tauri::Window) -> bool { - unsafe { - // Get cursor position in screen coordinates (macOS coordinates - origin at bottom left) - let mouse_location: NSPoint = msg_send![class!(NSEvent), mouseLocation]; - - // Get all screens - // - // We need screens count because for multiple-display users, - // in macOS, the Native API returns the mouse coordinates relative to the primary - // display, for example, if we say the primary display is 1920x1080, another two lies - // on both sides of the primary display with the size of 1920x1080 too, mouse coordinates - // will be 0 at the left edge of p-display, and 1920 at the right edge of the p-display, - // -1080 at the left edge of the left-side display, and 2160 at the right edge of the right-side - // display. - let screens: *const objc2::runtime::AnyObject = msg_send![class!(NSScreen), screens]; - let screens_count: usize = msg_send![screens, count]; - - // Get window position and size from Tauri - // Get the NSWindow from Tauri window to access native properties - // - // While Tauri does provide us window.outer_position() and window.outer_size() - // it's not enough to determine if the cursor is inside the window. - // The value of those two functions returns the in-compatible coordinates (top-left origin) - // with OBJ-C Native API values. This produces a lot of issues when calculating the collision - // of the cursor relative to the window. - // - // We need to get the window's frame in macOS coordinates (bottom-left origin) - // and check if the cursor is inside that frame. - let ns_window: *mut objc2::runtime::AnyObject = window.ns_window().unwrap() as *mut _; - let window_frame: NSRect = msg_send![ns_window, frame]; - - // Log all screens information - for _ in 0..screens_count { - // For debugging purpose, screen object, frame size of the screen, visible frame size of the screen, - // and the scale factor of the screen can be obtained as follows: - // - // let screen: *const objc2::runtime::AnyObject = msg_send![screens, objectAtIndex: i]; - // let frame: NSRect = msg_send![screen, frame]; - // let visible_frame: NSRect = msg_send![screen, visibleFrame]; - // let scale_factor: f64 = msg_send![screen, backingScaleFactor]; - - // Check if mouse is inside our window's frame - let is_inside = mouse_location.x >= window_frame.origin.x - && mouse_location.x <= (window_frame.origin.x + window_frame.size.width) - && mouse_location.y >= window_frame.origin.y - && mouse_location.y <= (window_frame.origin.y + window_frame.size.height); - - if is_inside { - return true; - } - } - } - false -} - -/// Check if modifier key is pressed (Option/Alt or Control) -#[cfg(target_os = "macos")] -fn is_modifier_pressed() -> bool { - unsafe { - let event_class: &'static objc2::runtime::AnyClass = class!(NSEvent); - let flags: u64 = msg_send![event_class, modifierFlags]; - - // Check for Alt/Option (1 << 19) or Control (1 << 18) - let alt_pressed = (flags & (1 << 19)) != 0; - // let ctrl_pressed = (flags & (1 << 18)) != 0; - - // alt_pressed || ctrl_pressed - alt_pressed - } -} +use app_click_through::native_macos::{is_cursor_in_window, is_modifier_pressed}; +#[cfg(target_os = "windows")] +use app_click_through::native_windows::{is_cursor_in_window, is_modifier_pressed}; +use app_click_through::state::{set_click_through_enabled, set_cursor_inside, WindowClickThroughState}; #[tauri::command] async fn start_monitor_for_clicking_through(window: tauri::Window) -> Result<(), String> { - log::info!("Starting monitor for clicking through"); - - let window = window.clone(); + let window = window; let state = window.state::(); - let is_click_through_enabled = state.is_click_through_enabled.clone(); - let is_click_through_monitoring_enabled = state.is_click_through_monitoring_enabled.clone(); + let enabled = state.enabled.clone(); + let monitoring_enabled = state.monitoring_enabled.clone(); // Already monitoring? - if is_click_through_monitoring_enabled.load(Ordering::Relaxed) { + if monitoring_enabled.load(Ordering::Relaxed) { return Ok(()); - } else { - // Set to true - state - .is_click_through_monitoring_enabled - .store(true, Ordering::Relaxed); } + // Set to true + state.monitoring_enabled.store(true, Ordering::Relaxed); + // Then start interval timer for monitoring tauri::async_runtime::spawn(async move { loop { sleep(Duration::from_millis(32)).await; // ~30FPS check rate // If monitoring is already stopped, break the loop - if !is_click_through_monitoring_enabled.load(Ordering::Relaxed) { - log::info!("Monitoring is stopped, breaking loop"); + if !monitoring_enabled.load(Ordering::Relaxed) { break; } // If is disabled already, skip until next check - if !is_click_through_enabled.load(Ordering::Relaxed) { - log::info!("Click-through is disabled, skipping check"); + if !enabled.load(Ordering::Relaxed) { continue; } @@ -180,41 +57,36 @@ async fn start_monitor_for_clicking_through(window: tauri::Window) -> Result<(), let cursor_inside = is_cursor_in_window(&window).await; let modifier_pressed = is_modifier_pressed(); - log::info!( - "Cursor inside: {}, Modifier pressed: {}", - cursor_inside, - modifier_pressed - ); + // Only allow disabling click-through when: + // 1. Cursor is OUTSIDE the window AND + // 2. Modifier key is pressed + let _ = set_cursor_inside(&window, cursor_inside && !modifier_pressed); + } + + #[cfg(target_os = "windows")] + { + let cursor_inside = is_cursor_in_window(&window).await; + let modifier_pressed = is_modifier_pressed(); // Only allow disabling click-through when: // 1. Cursor is OUTSIDE the window AND // 2. Modifier key is pressed - let should_be_click_through = cursor_inside && !modifier_pressed; - - if should_be_click_through { - let _ = set_cursor_inside(&window, true); - } else { - let _ = set_cursor_inside(&window, false); - } + let _ = set_cursor_inside(&window, cursor_inside && !modifier_pressed); } } }); - return Ok(()); + Ok(()) } #[tauri::command] async fn stop_monitor_for_clicking_through(window: tauri::Window) -> Result<(), String> { - log::info!("Stopping monitor for clicking through"); - - let window = window.clone(); + let window = window; let state = window.state::(); // Set to false // Termination will be triggered in the next interval check (tick) - state - .is_click_through_monitoring_enabled - .store(false, Ordering::Relaxed); + state.monitoring_enabled.store(false, Ordering::Relaxed); Ok(()) } @@ -232,11 +104,11 @@ async fn stop_click_through(window: tauri::Window) -> Result<(), String> { } #[cfg_attr(mobile, tauri::mobile_entry_point)] +#[allow(clippy::missing_panics_doc)] pub fn run() { - let prevent_default_plugin = tauri_plugin_prevent_default::Builder::new() - .with_flags(Flags::RELOAD) - .build(); + let prevent_default_plugin = tauri_plugin_prevent_default::Builder::new().with_flags(Flags::RELOAD).build(); + #[allow(clippy::missing_panics_doc)] tauri::Builder::default() .plugin(prevent_default_plugin) .plugin(tauri_plugin_mcp::Builder.build()) @@ -245,30 +117,22 @@ pub fn run() { .setup(|app| { let mut builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default()); - builder = builder - .title("AIRI") - .decorations(false) - .inner_size(450.0, 600.0) - .shadow(false) - .transparent(true) - .always_on_top(true); + builder = builder.title("AIRI").decorations(false).inner_size(450.0, 600.0).shadow(false).transparent(true).always_on_top(true); #[cfg(target_os = "macos")] { - builder = builder.title_bar_style(TitleBarStyle::Transparent); + builder = builder.title_bar_style(tauri::TitleBarStyle::Transparent); } let _ = builder.build().unwrap(); #[cfg(target_os = "macos")] - app.set_activation_policy(ActivationPolicy::Accessory); // hide dock icon + { + app.set_activation_policy(tauri::ActivationPolicy::Accessory); // hide dock icon + } if cfg!(debug_assertions) { - app.handle().plugin( - tauri_plugin_log::Builder::default() - .level(log::LevelFilter::Info) - .build(), - )?; + app.handle().plugin(tauri_plugin_log::Builder::default().level(log::LevelFilter::Info).build())?; } // TODO: i18n @@ -295,7 +159,7 @@ pub fn run() { return; } - windows::settings::new_settings_window(app).unwrap(); + app_windows::settings::new_settings_window(app).unwrap(); } "hide" => { let window = app.get_webview_window("settings"); @@ -326,11 +190,10 @@ pub fn run() { ]) .build(tauri::generate_context!()) .expect("error while building tauri application") - .run(|_, event| match event { - RunEvent::ExitRequested { .. } => { + .run(|_, event| { + if let RunEvent::ExitRequested { .. } = event { println!("Exiting app"); println!("Exited app"); } - _ => {} }); } diff --git a/crates/tauri-plugin-mcp/build.rs b/crates/tauri-plugin-mcp/build.rs index ed2bb874e..98d83c405 100644 --- a/crates/tauri-plugin-mcp/build.rs +++ b/crates/tauri-plugin-mcp/build.rs @@ -1,9 +1,4 @@ -const COMMANDS: &[&str] = &[ - "connect_server", - "disconnect_server", - "list_tools", - "call_tool", -]; +const COMMANDS: &[&str] = &["connect_server", "disconnect_server", "list_tools", "call_tool"]; fn main() { tauri_plugin::Builder::new(COMMANDS).build(); diff --git a/crates/tauri-plugin-mcp/src/lib.rs b/crates/tauri-plugin-mcp/src/lib.rs index 2749c0279..22e3e6cad 100644 --- a/crates/tauri-plugin-mcp/src/lib.rs +++ b/crates/tauri-plugin-mcp/src/lib.rs @@ -1,29 +1,33 @@ use std::process::Stdio; -use rmcp::model::CallToolRequestParam; use rmcp::{ - model::{CallToolResult, Tool}, + model::{CallToolRequestParam, CallToolResult, Tool}, service::RunningService, transport::TokioChildProcess, - RoleClient, ServiceExt, + RoleClient, + ServiceExt, }; use serde_json::{Map, Value}; use tauri::{ plugin::{self, TauriPlugin}, - Manager, Runtime, + AppHandle, + Manager, + Runtime, + State, }; -use tauri::{AppHandle, State}; -use tokio::process::Command; -use tokio::sync::Mutex; +use tokio::{process::Command, sync::Mutex}; pub struct McpState { pub client: Option>, } +#[allow(clippy::missing_panics_doc)] pub fn destroy(app_handle: &AppHandle) { println!("Destroying MCP plugin"); + tokio::runtime::Runtime::new().unwrap().block_on(async { let state = app_handle.state::>(); + let mut state = state.lock().await; if state.client.is_none() { println!("MCP plugin not connected, no need to disconnect"); @@ -31,36 +35,29 @@ pub fn destroy(app_handle: &AppHandle) { } let client = state.client.take().unwrap(); + drop(state); + client.cancel().await.unwrap(); // client.waiting().await.unwrap(); - state.client = None; }); + println!("MCP plugin destroyed"); } #[tauri::command] -async fn connect_server( - state: State<'_, Mutex>, - command: String, - args: Vec, -) -> Result<(), String> { +async fn connect_server(state: State<'_, Mutex>, command: String, args: Vec) -> Result<(), String> { let mut state = state.lock().await; if state.client.is_some() { return Err("Client already connected".to_string()); } - let child_process = TokioChildProcess::new( - Command::new(command) - .args(args) - .stderr(Stdio::inherit()) - .stdout(Stdio::inherit()), - ) - .unwrap(); + let child_process = TokioChildProcess::new(Command::new(command).args(args).stderr(Stdio::inherit()).stdout(Stdio::inherit())).unwrap(); let service: RunningService = ().serve(child_process).await.unwrap(); state.client = Some(service); + drop(state); Ok(()) } @@ -73,9 +70,9 @@ async fn disconnect_server(state: State<'_, Mutex>) -> Result<(), Stri } let cancel_result = state.client.take().unwrap().cancel().await; - println!("Cancel result: {:?}", cancel_result); + println!("Cancel result: {cancel_result:?}"); // state.client.take().unwrap().waiting().await.unwrap(); - state.client = None; + drop(state); println!("Disconnected from MCP server"); @@ -90,24 +87,17 @@ async fn list_tools(state: State<'_, Mutex>) -> Result, Stri return Err("Client not connected".to_string()); } - let list_tools_result = client - .unwrap() - .list_tools(Default::default()) - .await - .unwrap(); // TODO: handle error + let list_tools_result = client.unwrap().list_tools(Option::default()).await.unwrap(); // TODO: handle error let tools = list_tools_result.tools; + drop(state); Ok(tools) } #[tauri::command] -async fn call_tool( - state: State<'_, Mutex>, - name: String, - args: Option>, -) -> Result { - println!("Calling tool: {:?}", name); - println!("Arguments: {:?}", args); +async fn call_tool(state: State<'_, Mutex>, name: String, args: Option>) -> Result { + println!("Calling tool: {name:?}"); + println!("Arguments: {args:?}"); let state = state.lock().await; let client = state.client.as_ref(); @@ -115,16 +105,10 @@ async fn call_tool( return Err("Client not connected".to_string()); } - let call_tool_result = client - .unwrap() - .call_tool(CallToolRequestParam { - name: name.into(), - arguments: args, - }) - .await - .unwrap(); + let call_tool_result = client.unwrap().call_tool(CallToolRequestParam { name: name.into(), arguments: args }).await.unwrap(); + drop(state); - println!("Tool result: {:?}", call_tool_result); + println!("Tool result: {call_tool_result:?}"); Ok(call_tool_result) } @@ -133,16 +117,12 @@ async fn call_tool( pub struct Builder; impl Builder { + #[must_use] pub fn build(self) -> TauriPlugin { println!("Building MCP plugin"); plugin::Builder::new("mcp") - .invoke_handler(tauri::generate_handler![ - connect_server, - disconnect_server, - list_tools, - call_tool - ]) + .invoke_handler(tauri::generate_handler![connect_server, disconnect_server, list_tools, call_tool]) .setup(|app_handle, _| { app_handle.manage(Mutex::new(McpState { client: None })); Ok(()) diff --git a/docs/src/content/docs/references/Contributing/guide.md b/docs/src/content/docs/references/Contributing/guide.md index 79cecc6f0..dd4ee63c7 100644 --- a/docs/src/content/docs/references/Contributing/guide.md +++ b/docs/src/content/docs/references/Contributing/guide.md @@ -15,6 +15,10 @@ Hello! Thank you for your interest in contributing to this project. This guide w
Windows setup +0. Download [Visual Studio](https://visualstudio.microsoft.com/downloads/), and follow the instructions here: https://rust-lang.github.io/rustup/installation/windows-msvc.html#walkthrough-installing-visual-studio-2022 + + > Make sure to install Windows SDK and C++ build tools when installing Visual Studio. + 1. Open PowerShell 2. Install [`scoop`](https://scoop.sh/) @@ -23,10 +27,17 @@ Hello! Thank you for your interest in contributing to this project. This guide w Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression ``` -3. Install `git`, `node` through `scoop` +3. Install `git`, Node.js, `rustup`, `msvc` through `scoop` ```powershell - scoop install git nodejs + scoop install git nodejs rustup + + # For Rust dependencies + # Not required if you are not going to develop on either crates or apps/tamagotchi + scoop install main/rust-msvc + # Rust & Windows specific + rustup toolchain install stable-x86_64-pc-windows-msvc + rustup default stable-x86_64-pc-windows-msvc ``` 4. Install `pnpm` through `corepack` @@ -115,6 +126,10 @@ git checkout -b ```shell corepack enable pnpm install + +# For Rust dependencies +# Not required if you are not going to develop on either crates or apps/tamagotchi +cargo fetch ``` :::note diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 000000000..a147c3d6e --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,13 @@ +# https://rust-lang.github.io/rustup/overrides.html#the-toolchain-file + +[toolchain] +channel = "nightly" +# https://rust-lang.github.io/rustup/concepts/components.html +components = [ + "rustc", + "cargo", + "rustfmt", + "rust-analyzer", + "clippy" +] +profile = "minimal" diff --git a/rustfmt.toml b/rustfmt.toml index b196eaa2d..20d6a3bd7 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1 +1,24 @@ +# https://github.com/rust-lang/rustfmt/blob/master/Configurations.md + +edition = "2024" tab_spaces = 2 +newline_style = "Unix" +imports_indent = "Block" # nightly +match_arm_leading_pipes = "Preserve" +# empty_item_single_line = true # nightly +group_imports = "StdExternalCrate" # nightly +imports_granularity = "Crate" # nightly +imports_layout = "HorizontalVertical" # nightly +# match_arm_blocks = false # nightly +match_block_trailing_comma = true +# normalize_comments = true # nightly +# normalize_doc_attributes = true # nightly +# overflow_delimited_expr = true # nightly +# reorder_impl_items = true # nightly +# spaces_around_ranges = true # nightly +# unstable_features = true # nightly +use_field_init_shorthand = true +use_try_shorthand = true +struct_field_align_threshold = 999 +enum_discrim_align_threshold = 999 +max_width = 256