mirror of
https://github.com/mii443/vrclipboard-ime-gui.git
synced 2025-08-22 08:05:32 +00:00
add support for steamvr manifest
This commit is contained in:
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
@ -4987,6 +4987,7 @@ dependencies = [
|
||||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
"wana_kana",
|
||||
"winapi",
|
||||
"windows 0.56.0",
|
||||
"windows-core 0.56.0",
|
||||
"zip",
|
||||
|
@ -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"
|
||||
|
@ -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<Mutex<Config>> = Lazy::new(|| Mutex::new(Config::load().unwrap()));
|
||||
static DICTIONARY: Lazy<Mutex<Dictionary>> = Lazy::new(|| Mutex::new(Dictionary::load().unwrap()));
|
||||
static APP_HANDLE: OnceLock<AppHandle> = OnceLock::new();
|
||||
static SERVER_PROCESS: Lazy<Mutex<Option<std::process::Child>>> = Lazy::new(|| Mutex::new(None));
|
||||
|
||||
#[tauri::command]
|
||||
fn load_settings(state: State<AppState>) -> Result<Config, String> {
|
||||
@ -135,18 +137,21 @@ async fn check_update() -> Result<bool, String> {
|
||||
}
|
||||
}
|
||||
|
||||
#[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<Mutex<Option<String>>> = Lazy::new(|| Mutex::new(None));
|
||||
|
||||
struct ServerProcessGuard;
|
||||
|
||||
impl Drop for ServerProcessGuard {
|
||||
fn drop(&mut self) {
|
||||
cleanup_server_process();
|
||||
}
|
||||
}
|
||||
|
||||
static _SERVER_GUARD: Lazy<ServerProcessGuard> = 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();
|
||||
}
|
||||
|
220
src-tauri/src/vr.rs
Normal file
220
src-tauri/src/vr.rs
Normal file
@ -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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<PathBuf, Box<dyn std::error::Error>> {
|
||||
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<PathBuf, Box<dyn std::error::Error>> {
|
||||
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::<Vec<u16>>()
|
||||
.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::<Vec<u16>>()
|
||||
.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())
|
||||
}
|
@ -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<SettingsComponentProps> = ({
|
||||
});
|
||||
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<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@ -249,11 +250,36 @@ const SettingsComponent: React.FC<SettingsComponentProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
// 入力フィールドが無効化されるべきかを判定する関数
|
||||
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 <span className="flex items-center text-indigo-500 text-xs animate-pulse"><Download size={10} className="mr-0.5" /> 登録中</span>;
|
||||
case 'success':
|
||||
return <span className="flex items-center text-green-500 text-xs"><Check size={10} className="mr-0.5" /> 登録完了</span>;
|
||||
case 'error':
|
||||
return <span className="flex items-center text-red-500 text-xs"><AlertCircle size={10} className="mr-0.5" /> 登録失敗</span>;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
@ -395,6 +421,32 @@ const SettingsComponent: React.FC<SettingsComponentProps> = ({
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 p-2 bg-gray-50 dark:bg-gray-700 rounded border border-gray-200 dark:border-gray-600">
|
||||
<h4 className="text-xs font-medium mb-2 text-gray-700 dark:text-gray-300">SteamVR連携</h4>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
オーバーレイアプリケーションとして登録します
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-end">
|
||||
<button
|
||||
onClick={handleRegisterManifest}
|
||||
disabled={manifestStatus === 'registering'}
|
||||
className={`px-3 py-1.5 text-xs rounded transition-colors ${manifestStatus === 'registering'
|
||||
? 'bg-gray-200 dark:bg-gray-600 text-gray-500 cursor-not-allowed'
|
||||
: 'bg-indigo-500 hover:bg-indigo-600 text-white dark:bg-indigo-600 dark:hover:bg-indigo-700'
|
||||
}`}
|
||||
>
|
||||
{manifestStatus === 'registering' ? '登録中...' : '登録'}
|
||||
</button>
|
||||
<div className="mt-1">
|
||||
<ManifestStatusIndicator />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user