mirror of
https://github.com/moeru-ai/airi.git
synced 2026-07-09 15:58:27 +00:00
feat(stage-tamagotchi): add click through support (previous macOS only, now both supported) (#187)
--------- Co-authored-by: DWCarrot <23579844+DWCarrot@users.noreply.github.com> Co-authored-by: Makito <5277268+sumimakito@users.noreply.github.com> Co-authored-by: Midori Kochiya <1381736+kmod-midori@users.noreply.github.com>
This commit is contained in:
parent
da4da0aeb6
commit
5c521c41a8
19 changed files with 379 additions and 284 deletions
|
|
@ -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
|
||||
|
|
|
|||
31
.github/workflows/ci.yml
vendored
31
.github/workflows/ci.yml
vendored
|
|
@ -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:
|
||||
|
|
|
|||
1
.tool-versions
Normal file
1
.tool-versions
Normal file
|
|
@ -0,0 +1 @@
|
|||
nodejs 24.1.0
|
||||
|
|
@ -12,6 +12,10 @@ Hello! Thank you for your interest in contributing to this project. This guide w
|
|||
<details>
|
||||
<summary>Windows setup</summary>
|
||||
|
||||
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 <your-branch-name>
|
|||
```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]
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
fn main() {
|
||||
tauri_build::build()
|
||||
tauri_build::build();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
pub mod native_macos;
|
||||
pub mod native_windows;
|
||||
pub mod state;
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
use tauri::{Emitter, Manager};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct WindowClickThroughState {
|
||||
pub monitoring_enabled: Arc<AtomicBool>,
|
||||
pub enabled: Arc<AtomicBool>,
|
||||
pub cursor_inside: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
pub fn set_cursor_inside(window: &tauri::Window, is_inside: bool) -> Result<(), String> {
|
||||
let state = window.state::<WindowClickThroughState>();
|
||||
|
||||
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::<WindowClickThroughState>();
|
||||
|
||||
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(())
|
||||
}
|
||||
|
|
@ -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(())
|
||||
}
|
||||
|
|
@ -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(())
|
||||
}
|
||||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<AtomicBool>,
|
||||
is_click_through_enabled: Arc<AtomicBool>,
|
||||
is_click_through_cursor_inside: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
fn set_cursor_inside(window: &tauri::Window, is_inside: bool) -> Result<(), String> {
|
||||
let state = window.state::<WindowClickThroughState>();
|
||||
|
||||
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::<WindowClickThroughState>();
|
||||
|
||||
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::<WindowClickThroughState>();
|
||||
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::<WindowClickThroughState>();
|
||||
|
||||
// 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");
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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<RunningService<RoleClient, ()>>,
|
||||
}
|
||||
|
||||
#[allow(clippy::missing_panics_doc)]
|
||||
pub fn destroy<R: Runtime>(app_handle: &AppHandle<R>) {
|
||||
println!("Destroying MCP plugin");
|
||||
|
||||
tokio::runtime::Runtime::new().unwrap().block_on(async {
|
||||
let state = app_handle.state::<Mutex<McpState>>();
|
||||
|
||||
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<R: Runtime>(app_handle: &AppHandle<R>) {
|
|||
}
|
||||
|
||||
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<McpState>>,
|
||||
command: String,
|
||||
args: Vec<String>,
|
||||
) -> Result<(), String> {
|
||||
async fn connect_server(state: State<'_, Mutex<McpState>>, command: String, args: Vec<String>) -> 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<RoleClient, ()> = ().serve(child_process).await.unwrap();
|
||||
|
||||
state.client = Some(service);
|
||||
drop(state);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -73,9 +70,9 @@ async fn disconnect_server(state: State<'_, Mutex<McpState>>) -> 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<McpState>>) -> Result<Vec<Tool>, 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<McpState>>,
|
||||
name: String,
|
||||
args: Option<Map<String, Value>>,
|
||||
) -> Result<CallToolResult, String> {
|
||||
println!("Calling tool: {:?}", name);
|
||||
println!("Arguments: {:?}", args);
|
||||
async fn call_tool(state: State<'_, Mutex<McpState>>, name: String, args: Option<Map<String, Value>>) -> Result<CallToolResult, String> {
|
||||
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<R: Runtime>(self) -> TauriPlugin<R> {
|
||||
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(())
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@ Hello! Thank you for your interest in contributing to this project. This guide w
|
|||
<details>
|
||||
<summary>Windows setup</summary>
|
||||
|
||||
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 <your-branch-name>
|
|||
```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
|
||||
|
|
|
|||
13
rust-toolchain.toml
Normal file
13
rust-toolchain.toml
Normal file
|
|
@ -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"
|
||||
23
rustfmt.toml
23
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue