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
|
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
|
charset = utf-8
|
||||||
indent_style = space
|
|
||||||
indent_size = 2
|
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
insert_final_newline = true
|
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:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
|
||||||
|
env:
|
||||||
|
# Rust
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
# https://github.com/Mozilla-Actions/sccache-action#rust-code
|
||||||
|
RUSTC_WRAPPER: sccache
|
||||||
|
SCCACHE_GHA_ENABLED: 'true'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
name: Lint
|
name: Lint
|
||||||
|
|
@ -16,15 +23,16 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
# # Rust
|
||||||
with:
|
# - uses: dtolnay/rust-toolchain@nightly
|
||||||
components: rustfmt,clippy
|
# - uses: mozilla-actions/sccache-action@v0.0.3
|
||||||
|
|
||||||
- name: Install system dependencies
|
- name: Install system dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install -y libwebkit2gtk-4.1-dev
|
sudo apt install -y libwebkit2gtk-4.1-dev
|
||||||
|
|
||||||
|
# Node.js
|
||||||
- uses: pnpm/action-setup@v3
|
- uses: pnpm/action-setup@v3
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
|
|
@ -39,12 +47,20 @@ jobs:
|
||||||
pnpm run lint
|
pnpm run lint
|
||||||
pnpm run lint:rust
|
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:
|
build-test:
|
||||||
name: Build Test
|
name: Build Test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- 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
|
- name: Install system dependencies
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -64,13 +80,18 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
pnpm run build:packages
|
pnpm run build:packages
|
||||||
pnpm run build:apps
|
pnpm run build:apps
|
||||||
pnpm run build:crates
|
|
||||||
|
# - name: Build Rust
|
||||||
|
# run: |
|
||||||
|
# pnpm run build:crates
|
||||||
|
|
||||||
typecheck:
|
typecheck:
|
||||||
name: Type Check
|
name: Type Check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# Node.js
|
||||||
- uses: pnpm/action-setup@v3
|
- uses: pnpm/action-setup@v3
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
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>
|
<details>
|
||||||
<summary>Windows setup</summary>
|
<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
|
1. Open PowerShell
|
||||||
2. Install [`scoop`](https://scoop.sh/)
|
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
|
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
|
```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`
|
4. Install `pnpm` through `corepack`
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
|
|
@ -112,6 +125,10 @@ git checkout -b <your-branch-name>
|
||||||
```shell
|
```shell
|
||||||
corepack enable
|
corepack enable
|
||||||
pnpm install
|
pnpm install
|
||||||
|
|
||||||
|
# For Rust dependencies
|
||||||
|
# Not required if you are not going to develop on either crates or apps/tamagotchi
|
||||||
|
cargo fetch
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
fn main() {
|
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 std::path::Path;
|
||||||
use tauri::{WebviewUrl, WebviewWindowBuilder};
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use tauri::TitleBarStyle;
|
use tauri::TitleBarStyle;
|
||||||
|
use tauri::{WebviewUrl, WebviewWindowBuilder};
|
||||||
|
|
||||||
pub fn new_chat_window(app: &tauri::AppHandle) -> Result<(), tauri::Error> {
|
pub fn new_chat_window(app: &tauri::AppHandle) -> Result<(), tauri::Error> {
|
||||||
let mut builder = WebviewWindowBuilder::new(
|
let mut builder = WebviewWindowBuilder::new(app, "chat", WebviewUrl::App(Path::new("#/chat").to_path_buf()))
|
||||||
app,
|
.title("Chat")
|
||||||
"chat",
|
.inner_size(600.0, 800.0)
|
||||||
WebviewUrl::App(Path::new("#/chat").to_path_buf()),
|
.shadow(true)
|
||||||
)
|
.transparent(false)
|
||||||
.title("Chat")
|
.accept_first_mouse(true);
|
||||||
.inner_size(600.0, 800.0)
|
|
||||||
.shadow(true)
|
|
||||||
.transparent(false)
|
|
||||||
.accept_first_mouse(true);
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
|
|
@ -29,5 +25,5 @@ pub fn new_chat_window(app: &tauri::AppHandle) -> Result<(), tauri::Error> {
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.build()?;
|
builder.build()?;
|
||||||
return Ok(());
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -1,20 +1,16 @@
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use tauri::{WebviewUrl, WebviewWindowBuilder};
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use tauri::TitleBarStyle;
|
use tauri::TitleBarStyle;
|
||||||
|
use tauri::{WebviewUrl, WebviewWindowBuilder};
|
||||||
|
|
||||||
pub fn new_settings_window(app: &tauri::AppHandle) -> Result<(), tauri::Error> {
|
pub fn new_settings_window(app: &tauri::AppHandle) -> Result<(), tauri::Error> {
|
||||||
let mut builder = WebviewWindowBuilder::new(
|
let mut builder = WebviewWindowBuilder::new(app, "settings", WebviewUrl::App(Path::new("#/settings").to_path_buf()))
|
||||||
app,
|
.title("Settings")
|
||||||
"settings",
|
.inner_size(450.0, 800.0)
|
||||||
WebviewUrl::App(Path::new("#/settings").to_path_buf()),
|
.shadow(true)
|
||||||
)
|
.transparent(false)
|
||||||
.title("Settings")
|
.accept_first_mouse(true);
|
||||||
.inner_size(450.0, 800.0)
|
|
||||||
.shadow(true)
|
|
||||||
.transparent(false)
|
|
||||||
.accept_first_mouse(true);
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
|
|
@ -29,5 +25,5 @@ pub fn new_settings_window(app: &tauri::AppHandle) -> Result<(), tauri::Error> {
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.build()?;
|
builder.build()?;
|
||||||
return Ok(());
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
|
|
||||||
use crate::windows;
|
use crate::app_windows;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn open_settings_window(app: tauri::AppHandle) -> Result<(), tauri::Error> {
|
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(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
windows::settings::new_settings_window(&app)?;
|
app_windows::settings::new_settings_window(&app)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -22,6 +22,6 @@ pub async fn open_chat_window(app: tauri::AppHandle) -> Result<(), tauri::Error>
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
windows::chat::new_chat_window(&app)?;
|
app_windows::chat::new_chat_window(&app)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,177 +1,54 @@
|
||||||
use tauri::menu::{Menu, MenuItem};
|
use std::{sync::atomic::Ordering, time::Duration};
|
||||||
use tauri::tray::TrayIconBuilder;
|
|
||||||
use tauri::Emitter;
|
|
||||||
use tauri::RunEvent;
|
|
||||||
|
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use tauri::{
|
||||||
use std::sync::Arc;
|
menu::{Menu, MenuItem},
|
||||||
use std::time::Duration;
|
tray::TrayIconBuilder,
|
||||||
|
Emitter,
|
||||||
|
Manager,
|
||||||
|
RunEvent,
|
||||||
|
WebviewUrl,
|
||||||
|
WebviewWindowBuilder,
|
||||||
|
};
|
||||||
|
use tauri_plugin_prevent_default::Flags;
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
mod app_click_through;
|
||||||
use objc2::{class, msg_send};
|
mod app_windows;
|
||||||
|
|
||||||
#[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 commands;
|
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")]
|
#[cfg(target_os = "macos")]
|
||||||
async fn is_cursor_in_window(window: &tauri::Window) -> bool {
|
use app_click_through::native_macos::{is_cursor_in_window, is_modifier_pressed};
|
||||||
unsafe {
|
#[cfg(target_os = "windows")]
|
||||||
// Get cursor position in screen coordinates (macOS coordinates - origin at bottom left)
|
use app_click_through::native_windows::{is_cursor_in_window, is_modifier_pressed};
|
||||||
let mouse_location: NSPoint = msg_send![class!(NSEvent), mouseLocation];
|
use app_click_through::state::{set_click_through_enabled, set_cursor_inside, WindowClickThroughState};
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn start_monitor_for_clicking_through(window: tauri::Window) -> Result<(), String> {
|
async fn start_monitor_for_clicking_through(window: tauri::Window) -> Result<(), String> {
|
||||||
log::info!("Starting monitor for clicking through");
|
let window = window;
|
||||||
|
|
||||||
let window = window.clone();
|
|
||||||
let state = window.state::<WindowClickThroughState>();
|
let state = window.state::<WindowClickThroughState>();
|
||||||
let is_click_through_enabled = state.is_click_through_enabled.clone();
|
let enabled = state.enabled.clone();
|
||||||
let is_click_through_monitoring_enabled = state.is_click_through_monitoring_enabled.clone();
|
let monitoring_enabled = state.monitoring_enabled.clone();
|
||||||
|
|
||||||
// Already monitoring?
|
// Already monitoring?
|
||||||
if is_click_through_monitoring_enabled.load(Ordering::Relaxed) {
|
if monitoring_enabled.load(Ordering::Relaxed) {
|
||||||
return Ok(());
|
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
|
// Then start interval timer for monitoring
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
sleep(Duration::from_millis(32)).await; // ~30FPS check rate
|
sleep(Duration::from_millis(32)).await; // ~30FPS check rate
|
||||||
|
|
||||||
// If monitoring is already stopped, break the loop
|
// If monitoring is already stopped, break the loop
|
||||||
if !is_click_through_monitoring_enabled.load(Ordering::Relaxed) {
|
if !monitoring_enabled.load(Ordering::Relaxed) {
|
||||||
log::info!("Monitoring is stopped, breaking loop");
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If is disabled already, skip until next check
|
// If is disabled already, skip until next check
|
||||||
if !is_click_through_enabled.load(Ordering::Relaxed) {
|
if !enabled.load(Ordering::Relaxed) {
|
||||||
log::info!("Click-through is disabled, skipping check");
|
|
||||||
continue;
|
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 cursor_inside = is_cursor_in_window(&window).await;
|
||||||
let modifier_pressed = is_modifier_pressed();
|
let modifier_pressed = is_modifier_pressed();
|
||||||
|
|
||||||
log::info!(
|
// Only allow disabling click-through when:
|
||||||
"Cursor inside: {}, Modifier pressed: {}",
|
// 1. Cursor is OUTSIDE the window AND
|
||||||
cursor_inside,
|
// 2. Modifier key is pressed
|
||||||
modifier_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:
|
// Only allow disabling click-through when:
|
||||||
// 1. Cursor is OUTSIDE the window AND
|
// 1. Cursor is OUTSIDE the window AND
|
||||||
// 2. Modifier key is pressed
|
// 2. Modifier key is pressed
|
||||||
let should_be_click_through = cursor_inside && !modifier_pressed;
|
let _ = set_cursor_inside(&window, cursor_inside && !modifier_pressed);
|
||||||
|
|
||||||
if should_be_click_through {
|
|
||||||
let _ = set_cursor_inside(&window, true);
|
|
||||||
} else {
|
|
||||||
let _ = set_cursor_inside(&window, false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return Ok(());
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn stop_monitor_for_clicking_through(window: tauri::Window) -> Result<(), String> {
|
async fn stop_monitor_for_clicking_through(window: tauri::Window) -> Result<(), String> {
|
||||||
log::info!("Stopping monitor for clicking through");
|
let window = window;
|
||||||
|
|
||||||
let window = window.clone();
|
|
||||||
let state = window.state::<WindowClickThroughState>();
|
let state = window.state::<WindowClickThroughState>();
|
||||||
|
|
||||||
// Set to false
|
// Set to false
|
||||||
// Termination will be triggered in the next interval check (tick)
|
// Termination will be triggered in the next interval check (tick)
|
||||||
state
|
state.monitoring_enabled.store(false, Ordering::Relaxed);
|
||||||
.is_click_through_monitoring_enabled
|
|
||||||
.store(false, Ordering::Relaxed);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -232,11 +104,11 @@ async fn stop_click_through(window: tauri::Window) -> Result<(), String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
|
#[allow(clippy::missing_panics_doc)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
let prevent_default_plugin = tauri_plugin_prevent_default::Builder::new()
|
let prevent_default_plugin = tauri_plugin_prevent_default::Builder::new().with_flags(Flags::RELOAD).build();
|
||||||
.with_flags(Flags::RELOAD)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
|
#[allow(clippy::missing_panics_doc)]
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(prevent_default_plugin)
|
.plugin(prevent_default_plugin)
|
||||||
.plugin(tauri_plugin_mcp::Builder.build())
|
.plugin(tauri_plugin_mcp::Builder.build())
|
||||||
|
|
@ -245,30 +117,22 @@ pub fn run() {
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
let mut builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default());
|
let mut builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default());
|
||||||
|
|
||||||
builder = builder
|
builder = builder.title("AIRI").decorations(false).inner_size(450.0, 600.0).shadow(false).transparent(true).always_on_top(true);
|
||||||
.title("AIRI")
|
|
||||||
.decorations(false)
|
|
||||||
.inner_size(450.0, 600.0)
|
|
||||||
.shadow(false)
|
|
||||||
.transparent(true)
|
|
||||||
.always_on_top(true);
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
builder = builder.title_bar_style(TitleBarStyle::Transparent);
|
builder = builder.title_bar_style(tauri::TitleBarStyle::Transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = builder.build().unwrap();
|
let _ = builder.build().unwrap();
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[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) {
|
if cfg!(debug_assertions) {
|
||||||
app.handle().plugin(
|
app.handle().plugin(tauri_plugin_log::Builder::default().level(log::LevelFilter::Info).build())?;
|
||||||
tauri_plugin_log::Builder::default()
|
|
||||||
.level(log::LevelFilter::Info)
|
|
||||||
.build(),
|
|
||||||
)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: i18n
|
// TODO: i18n
|
||||||
|
|
@ -295,7 +159,7 @@ pub fn run() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
windows::settings::new_settings_window(app).unwrap();
|
app_windows::settings::new_settings_window(app).unwrap();
|
||||||
}
|
}
|
||||||
"hide" => {
|
"hide" => {
|
||||||
let window = app.get_webview_window("settings");
|
let window = app.get_webview_window("settings");
|
||||||
|
|
@ -326,11 +190,10 @@ pub fn run() {
|
||||||
])
|
])
|
||||||
.build(tauri::generate_context!())
|
.build(tauri::generate_context!())
|
||||||
.expect("error while building tauri application")
|
.expect("error while building tauri application")
|
||||||
.run(|_, event| match event {
|
.run(|_, event| {
|
||||||
RunEvent::ExitRequested { .. } => {
|
if let RunEvent::ExitRequested { .. } = event {
|
||||||
println!("Exiting app");
|
println!("Exiting app");
|
||||||
println!("Exited app");
|
println!("Exited app");
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,4 @@
|
||||||
const COMMANDS: &[&str] = &[
|
const COMMANDS: &[&str] = &["connect_server", "disconnect_server", "list_tools", "call_tool"];
|
||||||
"connect_server",
|
|
||||||
"disconnect_server",
|
|
||||||
"list_tools",
|
|
||||||
"call_tool",
|
|
||||||
];
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
tauri_plugin::Builder::new(COMMANDS).build();
|
tauri_plugin::Builder::new(COMMANDS).build();
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,33 @@
|
||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
|
|
||||||
use rmcp::model::CallToolRequestParam;
|
|
||||||
use rmcp::{
|
use rmcp::{
|
||||||
model::{CallToolResult, Tool},
|
model::{CallToolRequestParam, CallToolResult, Tool},
|
||||||
service::RunningService,
|
service::RunningService,
|
||||||
transport::TokioChildProcess,
|
transport::TokioChildProcess,
|
||||||
RoleClient, ServiceExt,
|
RoleClient,
|
||||||
|
ServiceExt,
|
||||||
};
|
};
|
||||||
use serde_json::{Map, Value};
|
use serde_json::{Map, Value};
|
||||||
use tauri::{
|
use tauri::{
|
||||||
plugin::{self, TauriPlugin},
|
plugin::{self, TauriPlugin},
|
||||||
Manager, Runtime,
|
AppHandle,
|
||||||
|
Manager,
|
||||||
|
Runtime,
|
||||||
|
State,
|
||||||
};
|
};
|
||||||
use tauri::{AppHandle, State};
|
use tokio::{process::Command, sync::Mutex};
|
||||||
use tokio::process::Command;
|
|
||||||
use tokio::sync::Mutex;
|
|
||||||
|
|
||||||
pub struct McpState {
|
pub struct McpState {
|
||||||
pub client: Option<RunningService<RoleClient, ()>>,
|
pub client: Option<RunningService<RoleClient, ()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::missing_panics_doc)]
|
||||||
pub fn destroy<R: Runtime>(app_handle: &AppHandle<R>) {
|
pub fn destroy<R: Runtime>(app_handle: &AppHandle<R>) {
|
||||||
println!("Destroying MCP plugin");
|
println!("Destroying MCP plugin");
|
||||||
|
|
||||||
tokio::runtime::Runtime::new().unwrap().block_on(async {
|
tokio::runtime::Runtime::new().unwrap().block_on(async {
|
||||||
let state = app_handle.state::<Mutex<McpState>>();
|
let state = app_handle.state::<Mutex<McpState>>();
|
||||||
|
|
||||||
let mut state = state.lock().await;
|
let mut state = state.lock().await;
|
||||||
if state.client.is_none() {
|
if state.client.is_none() {
|
||||||
println!("MCP plugin not connected, no need to disconnect");
|
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();
|
let client = state.client.take().unwrap();
|
||||||
|
drop(state);
|
||||||
|
|
||||||
client.cancel().await.unwrap();
|
client.cancel().await.unwrap();
|
||||||
// client.waiting().await.unwrap();
|
// client.waiting().await.unwrap();
|
||||||
state.client = None;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
println!("MCP plugin destroyed");
|
println!("MCP plugin destroyed");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn connect_server(
|
async fn connect_server(state: State<'_, Mutex<McpState>>, command: String, args: Vec<String>) -> Result<(), String> {
|
||||||
state: State<'_, Mutex<McpState>>,
|
|
||||||
command: String,
|
|
||||||
args: Vec<String>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let mut state = state.lock().await;
|
let mut state = state.lock().await;
|
||||||
|
|
||||||
if state.client.is_some() {
|
if state.client.is_some() {
|
||||||
return Err("Client already connected".to_string());
|
return Err("Client already connected".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let child_process = TokioChildProcess::new(
|
let child_process = TokioChildProcess::new(Command::new(command).args(args).stderr(Stdio::inherit()).stdout(Stdio::inherit())).unwrap();
|
||||||
Command::new(command)
|
|
||||||
.args(args)
|
|
||||||
.stderr(Stdio::inherit())
|
|
||||||
.stdout(Stdio::inherit()),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let service: RunningService<RoleClient, ()> = ().serve(child_process).await.unwrap();
|
let service: RunningService<RoleClient, ()> = ().serve(child_process).await.unwrap();
|
||||||
|
|
||||||
state.client = Some(service);
|
state.client = Some(service);
|
||||||
|
drop(state);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -73,9 +70,9 @@ async fn disconnect_server(state: State<'_, Mutex<McpState>>) -> Result<(), Stri
|
||||||
}
|
}
|
||||||
|
|
||||||
let cancel_result = state.client.take().unwrap().cancel().await;
|
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.take().unwrap().waiting().await.unwrap();
|
||||||
state.client = None;
|
drop(state);
|
||||||
|
|
||||||
println!("Disconnected from MCP server");
|
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());
|
return Err("Client not connected".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let list_tools_result = client
|
let list_tools_result = client.unwrap().list_tools(Option::default()).await.unwrap(); // TODO: handle error
|
||||||
.unwrap()
|
|
||||||
.list_tools(Default::default())
|
|
||||||
.await
|
|
||||||
.unwrap(); // TODO: handle error
|
|
||||||
let tools = list_tools_result.tools;
|
let tools = list_tools_result.tools;
|
||||||
|
drop(state);
|
||||||
|
|
||||||
Ok(tools)
|
Ok(tools)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn call_tool(
|
async fn call_tool(state: State<'_, Mutex<McpState>>, name: String, args: Option<Map<String, Value>>) -> Result<CallToolResult, String> {
|
||||||
state: State<'_, Mutex<McpState>>,
|
println!("Calling tool: {name:?}");
|
||||||
name: String,
|
println!("Arguments: {args:?}");
|
||||||
args: Option<Map<String, Value>>,
|
|
||||||
) -> Result<CallToolResult, String> {
|
|
||||||
println!("Calling tool: {:?}", name);
|
|
||||||
println!("Arguments: {:?}", args);
|
|
||||||
|
|
||||||
let state = state.lock().await;
|
let state = state.lock().await;
|
||||||
let client = state.client.as_ref();
|
let client = state.client.as_ref();
|
||||||
|
|
@ -115,16 +105,10 @@ async fn call_tool(
|
||||||
return Err("Client not connected".to_string());
|
return Err("Client not connected".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let call_tool_result = client
|
let call_tool_result = client.unwrap().call_tool(CallToolRequestParam { name: name.into(), arguments: args }).await.unwrap();
|
||||||
.unwrap()
|
drop(state);
|
||||||
.call_tool(CallToolRequestParam {
|
|
||||||
name: name.into(),
|
|
||||||
arguments: args,
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
println!("Tool result: {:?}", call_tool_result);
|
println!("Tool result: {call_tool_result:?}");
|
||||||
|
|
||||||
Ok(call_tool_result)
|
Ok(call_tool_result)
|
||||||
}
|
}
|
||||||
|
|
@ -133,16 +117,12 @@ async fn call_tool(
|
||||||
pub struct Builder;
|
pub struct Builder;
|
||||||
|
|
||||||
impl Builder {
|
impl Builder {
|
||||||
|
#[must_use]
|
||||||
pub fn build<R: Runtime>(self) -> TauriPlugin<R> {
|
pub fn build<R: Runtime>(self) -> TauriPlugin<R> {
|
||||||
println!("Building MCP plugin");
|
println!("Building MCP plugin");
|
||||||
|
|
||||||
plugin::Builder::new("mcp")
|
plugin::Builder::new("mcp")
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![connect_server, disconnect_server, list_tools, call_tool])
|
||||||
connect_server,
|
|
||||||
disconnect_server,
|
|
||||||
list_tools,
|
|
||||||
call_tool
|
|
||||||
])
|
|
||||||
.setup(|app_handle, _| {
|
.setup(|app_handle, _| {
|
||||||
app_handle.manage(Mutex::new(McpState { client: None }));
|
app_handle.manage(Mutex::new(McpState { client: None }));
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,10 @@ Hello! Thank you for your interest in contributing to this project. This guide w
|
||||||
<details>
|
<details>
|
||||||
<summary>Windows setup</summary>
|
<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
|
1. Open PowerShell
|
||||||
2. Install [`scoop`](https://scoop.sh/)
|
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
|
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
|
```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`
|
4. Install `pnpm` through `corepack`
|
||||||
|
|
@ -115,6 +126,10 @@ git checkout -b <your-branch-name>
|
||||||
```shell
|
```shell
|
||||||
corepack enable
|
corepack enable
|
||||||
pnpm install
|
pnpm install
|
||||||
|
|
||||||
|
# For Rust dependencies
|
||||||
|
# Not required if you are not going to develop on either crates or apps/tamagotchi
|
||||||
|
cargo fetch
|
||||||
```
|
```
|
||||||
|
|
||||||
:::note
|
:::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
|
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