diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index b2ae445..8ef9b1d 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4987,6 +4987,7 @@ dependencies = [ "tracing-appender", "tracing-subscriber", "wana_kana", + "winapi", "windows 0.56.0", "windows-core 0.56.0", "zip", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 4f28d27..bdea8f6 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -36,6 +36,7 @@ ipc-channel = "0.19.0" zip = "2.6.1" wana_kana = "4.0.0" itertools = "0.14.0" +winapi = { version = "0.3", features = ["winreg", "winnt"] } [dependencies.tracing-subscriber] version = "0.3.16" diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 5860c42..b6c0a3d 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -14,6 +14,7 @@ mod transform_rule; mod tsf; mod tsf_availability; mod tsf_conversion; +mod vr; use std::{ io::{BufRead, BufReader}, @@ -54,6 +55,7 @@ struct AppState { static STATE: Lazy> = Lazy::new(|| Mutex::new(Config::load().unwrap())); static DICTIONARY: Lazy> = Lazy::new(|| Mutex::new(Dictionary::load().unwrap())); static APP_HANDLE: OnceLock = OnceLock::new(); +static SERVER_PROCESS: Lazy>> = Lazy::new(|| Mutex::new(None)); #[tauri::command] fn load_settings(state: State) -> Result { @@ -135,18 +137,21 @@ async fn check_update() -> Result { } } +#[tauri::command] +async fn register_manifest() -> Result<(), String> { + vr::create_vrmanifest().unwrap(); + vr::register_manifest_with_openvr().unwrap(); + Ok(()) +} + fn start_server_process() -> String { let exe_path = std::env::current_exe().unwrap(); let server_path = exe_path.to_str().unwrap(); let mut command = std::process::Command::new(server_path); - command.arg("server"); - let mut child = command - .arg("server") - .stdout(Stdio::piped()) - .spawn() - .unwrap(); + + let mut child = command.stdout(Stdio::piped()).spawn().unwrap(); let stdout = child.stdout.take().unwrap(); let reader = BufReader::new(stdout); @@ -160,11 +165,31 @@ fn start_server_process() -> String { } } + *SERVER_PROCESS.lock().unwrap() = Some(child); + server_name } +fn cleanup_server_process() { + if let Some(mut child) = SERVER_PROCESS.lock().unwrap().take() { + debug!("Terminating server process"); + let _ = child.kill(); + let _ = child.wait(); + } +} + static SERVER_NAME: Lazy>> = Lazy::new(|| Mutex::new(None)); +struct ServerProcessGuard; + +impl Drop for ServerProcessGuard { + fn drop(&mut self) { + cleanup_server_process(); + } +} + +static _SERVER_GUARD: Lazy = Lazy::new(|| ServerProcessGuard); + fn extract_dictionary() { let self_exe_path = PathBuf::from(SELF_EXE_PATH.read().unwrap().as_str()); let zip_path = self_exe_path @@ -199,6 +224,8 @@ async fn main() { let server_name = start_server_process(); SERVER_NAME.lock().unwrap().replace(server_name); + Lazy::force(&_SERVER_GUARD); + extract_dictionary(); tauri::Builder::default() @@ -222,7 +249,8 @@ async fn main() { open_ms_settings_regionlanguage_jpnime, load_dictionary, save_dictionary, - check_update + check_update, + register_manifest, ]) .setup(|app| { APP_HANDLE.set(app.app_handle().to_owned()).unwrap(); @@ -259,6 +287,17 @@ async fn main() { Ok(()) }) + .on_window_event(|_window, event| match event { + tauri::WindowEvent::CloseRequested { .. } => { + cleanup_server_process(); + } + tauri::WindowEvent::Destroyed => { + cleanup_server_process(); + } + _ => {} + }) .run(tauri::generate_context!()) .expect("error while running tauri application"); + + cleanup_server_process(); } diff --git a/src-tauri/src/vr.rs b/src-tauri/src/vr.rs new file mode 100644 index 0000000..44029fa --- /dev/null +++ b/src-tauri/src/vr.rs @@ -0,0 +1,220 @@ +use std::{fs, path::PathBuf, process::Command}; + +use serde_json::json; +use tracing::{debug, error, info, warn}; + +use crate::SELF_EXE_PATH; + +const APP_KEY: &str = "dev.mii.vrclipboardime"; + +pub fn create_vrmanifest() -> Result<(), Box> { + info!("Starting VR manifest file creation"); + + let self_exe_path = PathBuf::from(SELF_EXE_PATH.read().unwrap().as_str()); + debug!("Executable path: {}", self_exe_path.display()); + + let manifest = json!({ + "source": "builtin", + "applications": [{ + "app_key": APP_KEY, + "launch_type": "binary", + "binary_path_windows": self_exe_path.to_string_lossy(), + "is_dashboard_overlay": true, + "strings": { + "en_us": { + "name": "VRClipboard-IME", + "description": "VRClipboard-IME" + }, + "ja_jp": { + "name": "VRClipboard-IME", + "description": "VRClipboard-IME" + } + } + }] + }); + + debug!( + "Manifest content to be created:\n{}", + serde_json::to_string_pretty(&manifest)? + ); + + let exe_dir = self_exe_path + .parent() + .ok_or("Failed to get executable directory")? + .to_path_buf(); + + let manifest_path = exe_dir.join("vrclipboard-ime.vrmanifest"); + debug!("Manifest file path: {}", manifest_path.display()); + + let manifest_content = serde_json::to_string_pretty(&manifest)?; + fs::write(&manifest_path, &manifest_content)?; + info!( + "Successfully created VR manifest file: {}", + manifest_path.display() + ); + debug!( + "Write completed - File size: {} bytes", + manifest_content.len() + ); + + Ok(()) +} + +pub fn register_manifest_with_openvr() -> Result<(), Box> { + info!("Starting OpenVR manifest registration using proper method"); + + let self_exe_path = PathBuf::from(SELF_EXE_PATH.read().unwrap().as_str()); + let exe_dir = self_exe_path + .parent() + .ok_or("Failed to get executable directory")?; + let manifest_path = exe_dir.join("vrclipboard-ime.vrmanifest"); + + info!("Registering manifest file: {}", manifest_path.display()); + + let steam_config_dir = get_steam_config_directory()?; + let steam_dir = steam_config_dir + .parent() + .ok_or("Failed to get Steam directory")?; + + let vrcmd_path = steam_dir + .join("steamapps") + .join("common") + .join("SteamVR") + .join("bin") + .join("win64") + .join("vrcmd.exe"); + + if vrcmd_path.exists() { + info!("Using vrcmd.exe to register manifest"); + debug!("vrcmd.exe path: {}", vrcmd_path.display()); + + let output = Command::new(&vrcmd_path) + .args(&["--appmanifest", manifest_path.to_string_lossy().as_ref()]) + .output()?; + + debug!("vrcmd exit status: {}", output.status); + + if output.status.success() { + info!("Successfully registered manifest with vrcmd"); + } + } else { + return Err("vrcmd.exe not found".into()); + } + + info!("OpenVR manifest registration completed"); + Ok(()) +} + +fn get_steam_config_directory() -> Result> { + info!("Searching for Steam configuration directory"); + + let possible_paths = vec![PathBuf::from(r"C:\Program Files (x86)\Steam\config")]; + + debug!("Attempting to get Steam path from registry"); + if let Ok(steam_path) = get_steam_path_from_registry() { + info!("Found Steam path in registry: {}", steam_path.display()); + let config_path = steam_path.join("config"); + if config_path.exists() { + info!( + "Found Steam configuration directory: {}", + config_path.display() + ); + return Ok(config_path); + } else { + warn!( + "Configuration directory from registry does not exist: {}", + config_path.display() + ); + } + } else { + debug!("Failed to get Steam path from registry"); + } + + debug!("Searching in standard paths"); + for path in &possible_paths { + debug!("Checking path: {}", path.display()); + if path.exists() { + info!("Found Steam configuration directory: {}", path.display()); + return Ok(path.clone()); + } + } + + error!( + "Steam configuration directory not found. Checked paths: {:?}", + possible_paths + ); + Err("Steam configuration directory not found".into()) +} + +fn get_steam_path_from_registry() -> Result> { + debug!("Reading Steam path from registry"); + + use std::ffi::OsString; + use std::os::windows::ffi::OsStringExt; + + unsafe { + use std::ptr; + use winapi::um::winnt::*; + use winapi::um::winreg::*; + + let mut hkey = ptr::null_mut(); + let registry_path = "SOFTWARE\\WOW6432Node\\Valve\\Steam"; + debug!("Opening registry key: {}", registry_path); + + let result = RegOpenKeyExW( + HKEY_LOCAL_MACHINE, + "SOFTWARE\\WOW6432Node\\Valve\\Steam\0" + .encode_utf16() + .collect::>() + .as_ptr(), + 0, + KEY_READ, + &mut hkey, + ); + + if result == 0 { + debug!("Successfully opened registry key"); + let mut buffer = vec![0u16; 512]; + let mut buffer_size = (buffer.len() * 2) as u32; + + let result = RegQueryValueExW( + hkey, + "InstallPath\0" + .encode_utf16() + .collect::>() + .as_ptr(), + ptr::null_mut(), + ptr::null_mut(), + buffer.as_mut_ptr() as *mut u8, + &mut buffer_size, + ); + + RegCloseKey(hkey); + + if result == 0 { + let len = (buffer_size / 2) as usize; + if len > 0 && buffer[len - 1] == 0 { + buffer.truncate(len - 1); + } + let path_string = OsString::from_wide(&buffer) + .into_string() + .map_err(|_| "Failed to convert registry path to UTF-8")?; + let steam_path = PathBuf::from(path_string); + debug!( + "Retrieved Steam path from registry: {}", + steam_path.display() + ); + return Ok(steam_path); + } else { + warn!( + "Failed to read InstallPath value. Registry error code: {}", + result + ); + } + } else { + warn!("Failed to open registry key. Error code: {}", result); + } + } + + Err("Failed to get Steam path from registry".into()) +} diff --git a/src/SettingsComponent.tsx b/src/SettingsComponent.tsx index fa1276b..2ed2320 100644 --- a/src/SettingsComponent.tsx +++ b/src/SettingsComponent.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useRef } from 'react'; import { invoke } from '@tauri-apps/api/core'; -import { ChevronDown, Settings, Save, AlertCircle, Check } from 'lucide-react'; +import { ChevronDown, Settings, Save, AlertCircle, Check, Download } from 'lucide-react'; export interface Config { prefix: string; @@ -110,6 +110,7 @@ const SettingsComponent: React.FC = ({ }); const [isOpen, setIsOpen] = useState(false); const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'success' | 'error'>('idle'); + const [manifestStatus, setManifestStatus] = useState<'idle' | 'registering' | 'success' | 'error'>('idle'); const dropdownRef = useRef(null); useEffect(() => { @@ -249,11 +250,36 @@ const SettingsComponent: React.FC = ({ } }; - // 入力フィールドが無効化されるべきかを判定する関数 const isInputDisabled = () => { return settings.use_tsf_reconvert || settings.use_azookey_conversion; }; + const handleRegisterManifest = async () => { + setManifestStatus('registering'); + try { + await invoke('register_manifest'); + setManifestStatus('success'); + setTimeout(() => setManifestStatus('idle'), 3000); + } catch (error) { + console.error('Failed to register manifest:', error); + setManifestStatus('error'); + setTimeout(() => setManifestStatus('idle'), 3000); + } + }; + + const ManifestStatusIndicator = () => { + switch (manifestStatus) { + case 'registering': + return 登録中; + case 'success': + return 登録完了; + case 'error': + return 登録失敗; + default: + return null; + } + }; + return (
@@ -395,6 +421,32 @@ const SettingsComponent: React.FC = ({

+ +
+

SteamVR連携

+
+
+

+ オーバーレイアプリケーションとして登録します +

+
+
+ +
+ +
+
+
+