refactor(stage-tamagotchi): type-safe commands codegen poc (#258)

* refactor(stage-tamagotchi): type-safe commands codegen poc

* chore: remove the useless comment

* chore: [suggestion] remove comments

Co-authored-by: Neko <neko@ayaka.moe>

* chore: [suggestion] remove comments

Co-authored-by: Neko <neko@ayaka.moe>

* chore: [suggestion] remove comments

Co-authored-by: Neko <neko@ayaka.moe>

---------

Co-authored-by: Neko <neko@ayaka.moe>
This commit is contained in:
Makito
2025-07-06 00:22:31 +09:00
committed by GitHub
co-authored by Neko
parent 5ee9ddbda6
commit 24d2ce3cd9
12 changed files with 358 additions and 44 deletions
Generated
+82
View File
@@ -2,6 +2,12 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "Inflector"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
[[package]]
name = "addr2line"
version = "0.24.2"
@@ -198,6 +204,8 @@ dependencies = [
"rubato",
"serde",
"serde_json",
"specta",
"specta-typescript",
"symphonia",
"tauri",
"tauri-build",
@@ -208,6 +216,7 @@ dependencies = [
"tauri-plugin-positioner",
"tauri-plugin-prevent-default",
"tauri-plugin-window-state",
"tauri-specta",
"tokenizers",
"tokio",
"url",
@@ -5247,6 +5256,50 @@ dependencies = [
"system-deps",
]
[[package]]
name = "specta"
version = "2.0.0-rc.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab7f01e9310a820edd31c80fde3cae445295adde21a3f9416517d7d65015b971"
dependencies = [
"paste",
"specta-macros",
"thiserror 1.0.69",
]
[[package]]
name = "specta-macros"
version = "2.0.0-rc.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0074b9e30ed84c6924eb63ad8d2fe71cdc82628525d84b1fcb1f2fd40676517"
dependencies = [
"Inflector",
"proc-macro2",
"quote",
"syn 2.0.103",
]
[[package]]
name = "specta-serde"
version = "0.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77216504061374659e7245eac53d30c7b3e5fe64b88da97c753e7184b0781e63"
dependencies = [
"specta",
"thiserror 1.0.69",
]
[[package]]
name = "specta-typescript"
version = "0.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3220a0c365e51e248ac98eab5a6a32f544ff6f961906f09d3ee10903a4f52b2d"
dependencies = [
"specta",
"specta-serde",
"thiserror 1.0.69",
]
[[package]]
name = "spm_precompiled"
version = "0.1.4"
@@ -5700,6 +5753,7 @@ dependencies = [
"serde_json",
"serde_repr",
"serialize-to-javascript",
"specta",
"swift-rs",
"tauri-build",
"tauri-macros",
@@ -5960,6 +6014,34 @@ dependencies = [
"wry",
]
[[package]]
name = "tauri-specta"
version = "2.0.0-rc.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b23c0132dd3cf6064e5cd919b82b3f47780e9280e7b5910babfe139829b76655"
dependencies = [
"heck 0.5.0",
"serde",
"serde_json",
"specta",
"specta-typescript",
"tauri",
"tauri-specta-macros",
"thiserror 2.0.12",
]
[[package]]
name = "tauri-specta-macros"
version = "2.0.0-rc.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a4aa93823e07859546aa796b8a5d608190cd8037a3a5dce3eb63d491c34bda8"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.103",
]
[[package]]
name = "tauri-utils"
version = "2.5.0"
@@ -51,6 +51,9 @@ tokenizers = "0.21.2"
url = "2.5.4"
tauri-plugin-window-state = "2.3.0"
tauri-plugin-positioner = "2.3.0"
specta = "=2.0.0-rc.22"
specta-typescript = "0.0.9"
tauri-specta = { version = "=2.0.0-rc.21", features = ["derive", "typescript"] }
[target.'cfg(target_os = "macos")'.dependencies]
objc2 = "0.6.1"
@@ -9,6 +9,8 @@ use std::{sync::atomic::Ordering, time::Duration};
use native_macos::{get_mouse_location, get_window_frame};
#[cfg(target_os = "windows")]
use native_windows::{get_mouse_location, get_window_frame};
#[cfg(debug_assertions)]
use specta_typescript::Typescript;
use state::{WindowClickThroughState, set_pass_through_enabled};
use tauri::{
Emitter,
@@ -89,34 +91,65 @@ async fn stop_monitor<R: Runtime>(window: tauri::Window<R>) -> Result<()> {
}
#[tauri::command]
async fn start_pass_through<R: Runtime>(window: tauri::Window<R>) -> Result<()> {
set_pass_through_enabled(&window, true).unwrap_or_else(|e| {
#[specta::specta]
async fn start_pass_through<R: Runtime>(
window: tauri::Window<R>
) -> std::result::Result<(), String> {
set_pass_through_enabled(&window, true).map_err(|e| {
log::error!("Failed to enable click-through: {e}");
});
Ok(())
e
})
}
#[tauri::command]
async fn stop_pass_through<R: Runtime>(window: tauri::Window<R>) -> Result<()> {
set_pass_through_enabled(&window, false).unwrap_or_else(|e| {
#[specta::specta]
async fn stop_pass_through<R: Runtime>(
window: tauri::Window<R>
) -> std::result::Result<(), String> {
set_pass_through_enabled(&window, false).map_err(|e| {
log::error!("Failed to disable click-through: {e}");
});
Ok(())
e
})
}
const PLUGIN_NAME: &str = "proj-airi-tauri-plugin-window-pass-through-on-hover";
pub fn init<R: Runtime>() -> TauriPlugin<R> {
PluginBuilder::new("proj-airi-tauri-plugin-window-pass-through-on-hover")
let builder = tauri_specta::Builder::<R>::new()
.plugin_name(PLUGIN_NAME)
// ↓ HACKY WARNING
// ↓ Below is a modified version from the expansion of `tauri_specta::collect_commands!`,
// ↓ as the original macro does not accept a mix of commands with and without `#[specta::specta]`
.commands(tauri_specta::internal::command(
tauri::generate_handler![
start_monitor,
stop_monitor,
start_pass_through,
stop_pass_through
],
specta::function::collect_functions![
start_pass_through::<tauri::Wry>,
// ^^^^^^^^^^
// TODO: We have to specify the runtime type here. This is a known issue:
// - https://github.com/specta-rs/tauri-specta/issues/70
// - https://github.com/specta-rs/tauri-specta/issues/162
stop_pass_through::<tauri::Wry>,
]
));
#[cfg(debug_assertions)]
builder
.export(
Typescript::default(),
"../src/commands/bindings/window-pass-through-on-hover.ts",
)
.expect("Failed to export typescript bindings");
PluginBuilder::new(PLUGIN_NAME)
.setup(|app, _| {
app.manage(WindowClickThroughState::default());
Ok(())
})
.invoke_handler(tauri::generate_handler![
start_monitor,
stop_monitor,
start_pass_through,
stop_pass_through,
])
.invoke_handler(builder.invoke_handler())
.build()
}
@@ -1,3 +1,5 @@
#[cfg(debug_assertions)]
use specta_typescript::Typescript;
use tauri::{
Manager,
Runtime,
@@ -7,27 +9,33 @@ use tauri::{
use crate::app::windows::{chat, settings};
#[tauri::command]
#[specta::specta]
async fn go<R: Runtime>(
window: tauri::Window<R>,
route: String,
window_label: String,
// ↓ #[specta(optional)] is not available in parameters.
// ↓ This will be `string | null` in the generated TypeScript code
window_label: Option<String>,
) -> std::result::Result<(), String> {
let app = window.app_handle();
let target_window = match window_label.as_str() {
"chat" => match app.get_webview_window("chat") {
let target_window = match window_label.as_deref() {
Some("chat") => match app.get_webview_window("chat") {
Some(window) => window,
None => {
chat::new_chat_window(app).map_err(|e| format!("Failed to create chat window: {}", e))?
},
},
"settings" => match app.get_webview_window("settings") {
Some("settings") => match app.get_webview_window("settings") {
Some(window) => window,
None => settings::new_settings_window(app)
.map_err(|e| format!("Failed to create settings window: {}", e))?,
},
_ => {
return Err(format!("Unknown window label: {}", window_label));
Some(label) => {
return Err(format!("Unknown window label: {}", label));
},
None => {
return Err("Missing window label".into());
},
};
@@ -43,9 +51,23 @@ async fn go<R: Runtime>(
Ok(())
}
const PLUGIN_NAME: &str = "proj-airi-tauri-plugin-window-router-link";
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("proj-airi-tauri-plugin-window-router-link")
.invoke_handler(tauri::generate_handler![go])
let builder = tauri_specta::Builder::<R>::new()
.plugin_name(PLUGIN_NAME)
.commands(tauri_specta::collect_commands![go::<tauri::Wry>]);
#[cfg(debug_assertions)]
builder
.export(
Typescript::default(),
"../src/commands/bindings/window-router-link.ts",
)
.expect("Failed to export typescript bindings");
Builder::new(PLUGIN_NAME)
.invoke_handler(builder.invoke_handler())
.setup(|_, _| Ok(()))
.build()
}
@@ -0,0 +1,96 @@
// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually.
/** user-defined commands **/
export const commands = {
async startPassThrough() : Promise<Result<null, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("plugin:proj-airi-tauri-plugin-window-pass-through-on-hover|start_pass_through") };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async stopPassThrough() : Promise<Result<null, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("plugin:proj-airi-tauri-plugin-window-pass-through-on-hover|stop_pass_through") };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
}
}
/** user-defined events **/
/** user-defined constants **/
/** user-defined types **/
/** tauri-specta globals **/
import {
invoke as TAURI_INVOKE,
Channel as TAURI_CHANNEL,
} from "@tauri-apps/api/core";
import * as TAURI_API_EVENT from "@tauri-apps/api/event";
import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow";
type __EventObj__<T> = {
listen: (
cb: TAURI_API_EVENT.EventCallback<T>,
) => ReturnType<typeof TAURI_API_EVENT.listen<T>>;
once: (
cb: TAURI_API_EVENT.EventCallback<T>,
) => ReturnType<typeof TAURI_API_EVENT.once<T>>;
emit: null extends T
? (payload?: T) => ReturnType<typeof TAURI_API_EVENT.emit>
: (payload: T) => ReturnType<typeof TAURI_API_EVENT.emit>;
};
export type Result<T, E> =
| { status: "ok"; data: T }
| { status: "error"; error: E };
function __makeEvents__<T extends Record<string, any>>(
mappings: Record<keyof T, string>,
) {
return new Proxy(
{} as unknown as {
[K in keyof T]: __EventObj__<T[K]> & {
(handle: __WebviewWindow__): __EventObj__<T[K]>;
};
},
{
get: (_, event) => {
const name = mappings[event as keyof T];
return new Proxy((() => {}) as any, {
apply: (_, __, [window]: [__WebviewWindow__]) => ({
listen: (arg: any) => window.listen(name, arg),
once: (arg: any) => window.once(name, arg),
emit: (arg: any) => window.emit(name, arg),
}),
get: (_, command: keyof __EventObj__<any>) => {
switch (command) {
case "listen":
return (arg: any) => TAURI_API_EVENT.listen(name, arg);
case "once":
return (arg: any) => TAURI_API_EVENT.once(name, arg);
case "emit":
return (arg: any) => TAURI_API_EVENT.emit(name, arg);
}
},
});
},
},
);
}
@@ -0,0 +1,88 @@
// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually.
/** user-defined commands **/
export const commands = {
async go(route: string, windowLabel: string | null) : Promise<Result<null, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("plugin:proj-airi-tauri-plugin-window-router-link|go", { route, windowLabel }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
}
}
/** user-defined events **/
/** user-defined constants **/
/** user-defined types **/
/** tauri-specta globals **/
import {
invoke as TAURI_INVOKE,
Channel as TAURI_CHANNEL,
} from "@tauri-apps/api/core";
import * as TAURI_API_EVENT from "@tauri-apps/api/event";
import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow";
type __EventObj__<T> = {
listen: (
cb: TAURI_API_EVENT.EventCallback<T>,
) => ReturnType<typeof TAURI_API_EVENT.listen<T>>;
once: (
cb: TAURI_API_EVENT.EventCallback<T>,
) => ReturnType<typeof TAURI_API_EVENT.once<T>>;
emit: null extends T
? (payload?: T) => ReturnType<typeof TAURI_API_EVENT.emit>
: (payload: T) => ReturnType<typeof TAURI_API_EVENT.emit>;
};
export type Result<T, E> =
| { status: "ok"; data: T }
| { status: "error"; error: E };
function __makeEvents__<T extends Record<string, any>>(
mappings: Record<keyof T, string>,
) {
return new Proxy(
{} as unknown as {
[K in keyof T]: __EventObj__<T[K]> & {
(handle: __WebviewWindow__): __EventObj__<T[K]>;
};
},
{
get: (_, event) => {
const name = mappings[event as keyof T];
return new Proxy((() => {}) as any, {
apply: (_, __, [window]: [__WebviewWindow__]) => ({
listen: (arg: any) => window.listen(name, arg),
once: (arg: any) => window.once(name, arg),
emit: (arg: any) => window.emit(name, arg),
}),
get: (_, command: keyof __EventObj__<any>) => {
switch (command) {
case "listen":
return (arg: any) => TAURI_API_EVENT.listen(name, arg);
case "once":
return (arg: any) => TAURI_API_EVENT.once(name, arg);
case "emit":
return (arg: any) => TAURI_API_EVENT.emit(name, arg);
}
},
});
},
},
);
}
@@ -0,0 +1,2 @@
export * as WindowPassThroughOnHover from './bindings/window-pass-through-on-hover'
export * as WindowRouterLink from './bindings/window-router-link'
@@ -1,18 +1,13 @@
<script setup lang="ts">
import { useTauriCore } from '../../composables/tauri'
import { WindowRouterLink } from '../../commands'
const props = defineProps<{
to: string
label?: string
}>()
const { invoke } = useTauriCore()
function handleClick() {
invoke('plugin:proj-airi-tauri-plugin-window-router-link|go', {
route: props.to,
windowLabel: props.label,
})
WindowRouterLink.commands.go(props.to, props.label ?? null)
}
</script>
@@ -18,15 +18,8 @@ export interface InvokeMethods {
// Plugin - Window Pass through on hover
'plugin:proj-airi-tauri-plugin-window-pass-through-on-hover|start_monitor': { args: undefined, options: undefined, returns: void }
'plugin:proj-airi-tauri-plugin-window-pass-through-on-hover|stop_monitor': { args: undefined, options: undefined, returns: void }
'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 - WindowRouterLink
'plugin:proj-airi-tauri-plugin-window-router-link|go': {
args: { route: string, windowLabel?: string } | undefined
options: undefined
returns: void
}
}
export interface InvokeMethodShape {
+3 -5
View File
@@ -1,11 +1,9 @@
import { useTauriCore } from '../composables/tauri'
import { WindowPassThroughOnHover } from '../commands'
export async function startClickThrough() {
const { invoke } = useTauriCore()
await invoke('plugin:proj-airi-tauri-plugin-window-pass-through-on-hover|start_pass_through')
await WindowPassThroughOnHover.commands.startPassThrough()
}
export async function stopClickThrough() {
const { invoke } = useTauriCore()
await invoke('plugin:proj-airi-tauri-plugin-window-pass-through-on-hover|stop_pass_through')
await WindowPassThroughOnHover.commands.stopPassThrough()
}
+1
View File
@@ -185,6 +185,7 @@ words:
- sizecheck
- smallserial
- Sniglet
- specta
- ssml
- staticlib
- stepfun
+1
View File
@@ -8,6 +8,7 @@ export default await antfu(
'**/assets/js/**',
'**/assets/live2d/models/**',
'apps/stage-tamagotchi/out/**',
'apps/stage-tamagotchi/src/commands/bindings/**',
'apps/stage-tamagotchi/src-tauri/**',
'crates/**',
'**/drizzle/**',