fix(stage-tamagotchi): deadlock of window-state due to wrapped window-state call

Great thanks to @sumimakito for the fix!

Co-authored-by: Makito <5277268+sumimakito@users.noreply.github.com>
This commit is contained in:
Neko Ayaka
2025-07-01 16:33:21 +08:00
co-authored by Makito
parent 206e5e580c
commit d662c20d1d
11 changed files with 207 additions and 270 deletions
@@ -15,6 +15,10 @@
"core:window:allow-set-focus",
"core:window:allow-set-minimizable",
"core:window:allow-set-visible-on-all-workspaces",
"core:window:allow-set-position",
"core:window:allow-available-monitors",
"core:window:allow-current-monitor",
"core:window:allow-primary-monitor",
"mcp:default",
"global-shortcut:allow-is-registered",
"global-shortcut:allow-register",
@@ -27,8 +27,6 @@ pub fn run() {
.plugin(tauri_plugin_window_state::Builder::default().build())
.plugin(tauri_plugin_positioner::init())
// Internal plugins
.plugin(plugins::window::init())
.plugin(plugins::window_persistence::init())
.plugin(plugins::window_pass_through_on_hover::init())
.plugin(plugins::window_router_link::init())
.plugin(plugins::audio_transcription::init())
@@ -1,6 +1,4 @@
pub mod audio_transcription;
pub mod audio_vad;
pub mod window;
pub mod window_pass_through_on_hover;
pub mod window_persistence;
pub mod window_router_link;
@@ -1,70 +0,0 @@
use tauri::{
Monitor,
Runtime,
plugin::{Builder, TauriPlugin},
};
#[tauri::command]
pub async fn get_display_info<R: Runtime>(
window: tauri::Window<R>
) -> Result<(Vec<Monitor>, Monitor), String> {
let monitors = match window.available_monitors() {
std::result::Result::Ok(monitors) => monitors,
_ => vec![],
};
let primary_monitor = match window.primary_monitor() {
std::result::Result::Ok(monitor) => match monitor {
Some(monitor) => monitor,
None => {
return Err("Primary monitor not found".to_string());
},
},
_ => return Err("Failed to get primary monitor".to_string()),
};
Ok((monitors, primary_monitor))
}
#[tauri::command]
pub async fn get_current_window_info<R: Runtime>(
window: tauri::Window<R>
) -> Result<((u32, u32), (i32, i32)), String> {
match window.current_monitor() {
std::result::Result::Ok(optional_monitor) => match optional_monitor {
Some(monitor) => {
let monitor_size = monitor.size();
let position = monitor.position();
Ok((
(monitor_size.width, monitor_size.height),
(position.x, position.y),
))
},
_ => Ok(((0, 0), (0, 0))),
},
_ => Ok(((0, 0), (0, 0))),
}
}
#[tauri::command]
pub async fn set_position<R: Runtime>(
window: tauri::Window<R>,
x: i32,
y: i32,
) -> Result<(), String> {
use tauri::Position;
window
.set_position(Position::Physical(tauri::PhysicalPosition { x, y }))
.map_err(|e| format!("Failed to set window position: {}", e))
}
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("proj-airi-tauri-plugin-window")
.invoke_handler(tauri::generate_handler![
get_display_info,
get_current_window_info,
set_position
])
.build()
}
@@ -1,79 +0,0 @@
use log::info;
use tauri::{
Manager,
Result,
RunEvent,
Runtime,
Window,
plugin::{Builder, TauriPlugin},
};
use tauri_plugin_window_state::{AppHandleExt, WindowExt};
#[tauri::command]
pub async fn save<R: Runtime>(window: Window<R>) -> Result<()> {
info!("Saving window state...");
window
.app_handle()
.save_window_state(tauri_plugin_window_state::StateFlags::all())
.map_err(|e| format!("Failed to save window state: {}", e))
.unwrap_or_else(|err| {
info!("Failed to restore window state: {}", err);
});
Ok(())
}
#[tauri::command]
pub async fn restore<R: Runtime>(window: Window<R>) -> Result<()> {
info!("Restoring window state...");
window
.restore_state(tauri_plugin_window_state::StateFlags::all())
.map_err(|e| format!("Failed to restore window state: {}", e))
.unwrap_or_else(|err| {
info!("Failed to restore window state: {}", err);
});
Ok(())
}
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("proj-airi-tauri-plugin-window-persistence")
.invoke_handler(tauri::generate_handler![save, restore,])
.on_window_ready(|window| {
window
.restore_state(tauri_plugin_window_state::StateFlags::all())
.map_err(|e| format!("Failed to restore window state: {}", e))
.unwrap_or_else(|err| {
info!("Failed to restore window state: {}", err);
});
})
.on_event(|app, event| match event {
RunEvent::Exit { .. } => {
info!("Exiting app");
info!("Exited app");
// Save window state on exit
app
.save_window_state(tauri_plugin_window_state::StateFlags::all())
.map_err(|e| format!("Failed to save window state: {}", e))
.unwrap_or_else(|err| {
info!("Failed to save window state on exit: {}", err);
});
},
RunEvent::ExitRequested { .. } => {
info!("Requested Exiting app");
info!("Requested Exited app");
app
.save_window_state(tauri_plugin_window_state::StateFlags::all())
.map_err(|e| format!("Failed to save window state: {}", e))
.unwrap_or_else(|err| {
info!("Failed to save window state on exit: {}", err);
});
},
_ => {},
})
.build()
}
@@ -0,0 +1,42 @@
import type { Position } from '@tauri-apps/plugin-positioner'
import { computedAsync, until } from '@vueuse/core'
import { useAppRuntime } from './runtime'
import { untilImported } from './tauri'
export function useTauriPositioner() {
const { platform, isInitialized } = useAppRuntime()
const tauriPositionerApi = computedAsync(async () => {
await until(isInitialized).toBeTruthy()
if (platform.value !== 'web') {
return untilImported(() => import('@tauri-apps/plugin-positioner'), console.warn)
}
})
async function ensureImported() {
await until(isInitialized).toBeTruthy()
if (platform.value === 'web') {
console.warn('Attempted to use Tauri positioner in web platform')
return
}
await until(tauriPositionerApi).toBeTruthy()
const imported = await tauriPositionerApi.value
if (!imported) {
throw new Error('Tauri positioner API not available')
}
}
async function moveWindow(to: Position) {
await ensureImported()
return tauriPositionerApi.value?.moveWindow(to)
}
return {
moveWindow,
}
}
@@ -1,16 +1,14 @@
import type {
DisplayInfo,
Monitor,
Point,
Size,
} from './tauri'
import type { Monitor } from '@tauri-apps/api/window'
import type { DisplayInfo, Point, Size } from './tauri'
import { useThrottleFn, watchThrottled } from '@vueuse/core'
import { computed, readonly, ref } from 'vue'
import { useAppRuntime } from './runtime'
import { useTauriCore } from './tauri'
import { useTauriWindow } from './tauri'
import { useTauriPointAndWindowFrame } from './tauri-window-pass-through-on-hover'
import { useTauriWindowState } from './tauri-window-state'
export interface WindowPersistenceConfig {
autoSave?: boolean
@@ -42,8 +40,9 @@ export function useWindowPersistence(config: WindowPersistenceConfig = {}) {
} = config
const { platform } = useAppRuntime()
const { invoke } = useTauriCore()
const { windowFrame } = useTauriPointAndWindowFrame()
const { saveWindowState, restoreStateCurrent } = useTauriWindowState()
const { setPosition, getAvailableMonitors, getPrimaryMonitor } = useTauriWindow()
// Debounced save function (declare early to avoid usage before definition)
const throttledSave = useThrottleFn(() => savePosition(), savePeriod)
@@ -144,7 +143,7 @@ export function useWindowPersistence(config: WindowPersistenceConfig = {}) {
}
try {
// invoke('plugin:proj-airi-tauri-plugin-window-persistence|save')
saveWindowState()
return true
}
@@ -163,7 +162,7 @@ export function useWindowPersistence(config: WindowPersistenceConfig = {}) {
isRestoring.value = true
// Restore window state using Tauri plugin
// await invoke('plugin:proj-airi-tauri-plugin-window-persistence|restore')
await restoreStateCurrent()
// Ensure the restored position is within bounds
if (constrainToDisplays && currentWindowPosition.value) {
@@ -228,7 +227,7 @@ export function useWindowPersistence(config: WindowPersistenceConfig = {}) {
try {
isPositioning.value = true
await invoke('plugin:proj-airi-tauri-plugin-window|set_position', { x: pos.x, y: pos.y })
await setPosition(pos.x, pos.y)
if (autoSave) {
throttledSave()
@@ -418,7 +417,18 @@ export function useWindowPersistence(config: WindowPersistenceConfig = {}) {
return
try {
const [monitors, primaryMonitor] = (await invoke('plugin:proj-airi-tauri-plugin-window|get_display_info'))!
const monitors = await getAvailableMonitors()
if (!monitors || monitors.length === 0) {
console.warn('[WindowPersistence] Failed to get available monitors')
return
}
const primaryMonitor = await getPrimaryMonitor()
if (!primaryMonitor) {
console.warn('[WindowPersistence] Failed to get primary monitor')
return
}
displayInfo.value = {
monitors,
primaryMonitor,
@@ -0,0 +1,77 @@
import { computedAsync, until } from '@vueuse/core'
import { useAppRuntime } from './runtime'
import { untilImported } from './tauri'
export enum StateFlags {
SIZE = 1,
POSITION = 2,
MAXIMIZED = 4,
VISIBLE = 8,
DECORATIONS = 16,
FULLSCREEN = 32,
ALL = 63,
}
export function useTauriWindowState() {
const { platform, isInitialized } = useAppRuntime()
const tauriWindowStateApi = computedAsync(async () => {
await until(isInitialized).toBeTruthy()
if (platform.value !== 'web') {
return untilImported(() => import('@tauri-apps/plugin-window-state'), console.warn)
}
})
async function ensureImported() {
await until(isInitialized).toBeTruthy()
if (platform.value === 'web') {
console.warn('Attempted to save window state in web platform')
return
}
await until(tauriWindowStateApi).toBeTruthy()
const imported = await tauriWindowStateApi.value
if (!imported) {
throw new Error('Tauri window state API not available')
}
}
async function saveWindowState(stateFlag?: StateFlags) {
await ensureImported()
if (stateFlag != null) {
return tauriWindowStateApi.value?.saveWindowState(stateFlag)
}
else {
return tauriWindowStateApi.value?.saveWindowState(tauriWindowStateApi.value.StateFlags.ALL)
}
}
async function restoreState(stateFlag?: StateFlags, windowLabel = 'main') {
await ensureImported()
if (stateFlag != null) {
return tauriWindowStateApi.value?.restoreState(windowLabel, stateFlag)
}
else {
return tauriWindowStateApi.value?.restoreState(windowLabel, tauriWindowStateApi.value.StateFlags.ALL)
}
}
async function restoreStateCurrent(stateFlag?: StateFlags) {
await ensureImported()
if (stateFlag != null) {
return tauriWindowStateApi.value?.restoreStateCurrent(stateFlag)
}
else {
return tauriWindowStateApi.value?.restoreStateCurrent(tauriWindowStateApi.value.StateFlags.ALL)
}
}
return {
saveWindowState,
restoreState,
restoreStateCurrent,
}
}
+62 -85
View File
@@ -1,19 +1,18 @@
import type { InvokeArgs, InvokeOptions } from '@tauri-apps/api/core'
import type { EventCallback, EventName, UnlistenFn } from '@tauri-apps/api/event'
import type { Position } from '@tauri-apps/plugin-positioner'
import type { StateFlags } from '@tauri-apps/plugin-window-state'
import type { Monitor } from '@tauri-apps/api/window'
import { withRetry } from '@moeru/std'
import { computedAsync, until } from '@vueuse/core'
import { useAppRuntime } from './runtime'
async function untilNoError<T>(fn: () => Promise<T>, onError?: (err?: unknown | null) => void): Promise<T> {
export async function untilNoError<T>(fn: () => Promise<T>, onError?: (err?: unknown | null) => void): Promise<T> {
const fnRetry = withRetry(fn, { retryDelay: 5000, retry: 5, onError })
return await fnRetry()
}
async function untilImported<T>(fn: () => Promise<T>, onError?: (err?: unknown | null) => void): Promise<T> {
export async function untilImported<T>(fn: () => Promise<T>, onError?: (err?: unknown | null) => void): Promise<T> {
return await untilNoError(fn, onError)
}
@@ -57,17 +56,6 @@ export interface AiriTamagotchiEvents extends Events {
'mcp_plugin_destroyed': undefined
}
export interface Monitor {
name: string
size: { width: number, height: number }
position: { x: number, y: number }
workArea: {
position: { x: number, y: number }
size: { width: number, height: number }
}
scale_factor: number
}
export interface DisplayInfo {
monitors: Monitor[]
primaryMonitor: Monitor
@@ -157,41 +145,12 @@ export interface InvokeMethods {
'plugin:proj-airi-tauri-plugin-window-pass-through-on-hover|start_pass_through': { args: undefined, options: undefined, returns: void }
'plugin:proj-airi-tauri-plugin-window-pass-through-on-hover|stop_pass_through': { args: undefined, options: undefined, returns: void }
// Plugin - Window
'plugin:proj-airi-tauri-plugin-window|get_display_info': {
args: undefined
options: undefined
returns: [Monitor[], Monitor]
}
'plugin:proj-airi-tauri-plugin-window|get_current_window_info': {
args: undefined
options: undefined
returns: [[number, number], [number, number]]
}
'plugin:proj-airi-tauri-plugin-window|set_position': {
args: { x: number, y: number }
options: undefined
returns: void
}
// Plugin - WindowRouterLink
'plugin:proj-airi-tauri-plugin-window-router-link|go': {
args: { route: string, windowLabel?: string } | undefined
options: undefined
returns: void
}
// // Plugin - Window Persistence
// 'plugin:proj-airi-tauri-plugin-window-persistence|save': {
// args: undefined
// options: undefined
// returns: void
// }
// 'plugin:proj-airi-tauri-plugin-window-persistence|restore': {
// args: undefined
// options: undefined
// returns: void
// }
}
interface InvokeMethodShape {
@@ -234,89 +193,107 @@ export function useTauriCore<IM extends Record<keyof IM, InvokeMethodShape> = In
return {
invoke,
core: tauriCoreApi,
}
}
export function useTauriPositioner() {
export function useTauriDpi() {
const { platform, isInitialized } = useAppRuntime()
const tauriPositionerApi = computedAsync(async () => {
const tauriDpiApi = computedAsync(async () => {
await until(isInitialized).toBeTruthy()
if (platform.value !== 'web') {
return untilImported(() => import('@tauri-apps/plugin-positioner'), console.warn)
return untilImported(() => import('@tauri-apps/api/window'), console.warn)
}
})
async function ensureImported() {
await until(isInitialized).toBeTruthy()
if (platform.value === 'web') {
console.warn('Attempted to use Tauri positioner in web platform')
return
}
await until(tauriPositionerApi).toBeTruthy()
const imported = await tauriPositionerApi.value
async function createLogicalPosition(x: number, y: number) {
const imported = await tauriDpiApi.value
if (!imported) {
throw new Error('Tauri positioner API not available')
throw new Error('Tauri DPI API not available')
}
}
async function moveWindow(to: Position) {
await ensureImported()
return tauriPositionerApi.value?.moveWindow(to)
return new imported.LogicalPosition(x, y)
}
return {
moveWindow,
createLogicalPosition,
}
}
export function useTauriWindowState() {
export function useTauriWindow() {
const { platform, isInitialized } = useAppRuntime()
const { createLogicalPosition } = useTauriDpi()
const tauriWindowStateApi = computedAsync(async () => {
const tauriWindowApi = computedAsync(async () => {
await until(isInitialized).toBeTruthy()
if (platform.value !== 'web') {
return untilImported(() => import('@tauri-apps/plugin-window-state'), console.warn)
return untilImported(() => import('@tauri-apps/api/window'), console.warn)
}
})
async function ensureImported() {
async function _ensureImported() {
await until(isInitialized).toBeTruthy()
if (platform.value === 'web') {
console.warn('Attempted to save window state in web platform')
return
throw new Error('Tauri Window API is not available in web platform')
}
await until(tauriWindowStateApi).toBeTruthy()
const imported = await tauriWindowStateApi.value
await until(tauriWindowApi).toBeTruthy()
const imported = await tauriWindowApi.value
if (!imported) {
throw new Error('Tauri window state API not available')
throw new Error('Tauri Window API not available')
}
return imported
}
async function getCurrentMonitor() {
try {
return await _ensureImported().then(imported => imported.currentMonitor())
}
catch (error) {
console.error('Failed to get current monitor:', error)
return undefined
}
}
async function saveWindowState(stateFlag: StateFlags) {
await ensureImported()
return tauriWindowStateApi.value?.saveWindowState(stateFlag)
async function getAvailableMonitors() {
try {
return await _ensureImported().then(imported => imported.availableMonitors())
}
catch (error) {
console.error('Failed to get available monitors:', error)
return []
}
}
async function restoreState(stateFlag: StateFlags, windowLabel = 'main') {
await ensureImported()
return tauriWindowStateApi.value?.restoreState(windowLabel, stateFlag)
async function getPrimaryMonitor() {
try {
return await _ensureImported().then(imported => imported.primaryMonitor())
}
catch (error) {
console.error('Failed to get primary monitor:', error)
return undefined
}
}
async function restoreStateCurrent(stateFlag: StateFlags) {
await ensureImported()
return tauriWindowStateApi.value?.restoreStateCurrent(stateFlag)
async function setPosition(x: number, y: number) {
try {
const imported = await _ensureImported()
const window = imported.getCurrentWindow()
return await window.setPosition(await createLogicalPosition(x, y))
}
catch (error) {
console.error('Failed to set window position:', error)
}
}
return {
saveWindowState,
restoreState,
restoreStateCurrent,
getAvailableMonitors,
getCurrentMonitor,
getPrimaryMonitor,
setPosition,
}
}
-6
View File
@@ -1,6 +0,0 @@
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<object, object, any>
export default component
}
-14
View File
@@ -1,14 +0,0 @@
declare module '*.js?url' {
const content: string
export default content
}
declare module '*.wasm?url' {
const content: string
export default content
}
declare module '*.cjs?url' {
const content: string
export default content
}