add support for steamvr manifest

This commit is contained in:
mii443
2025-05-23 21:58:17 +09:00
parent 9f8c133d70
commit c86984ade5
5 changed files with 322 additions and 9 deletions

1
src-tauri/Cargo.lock generated
View File

@ -4987,6 +4987,7 @@ dependencies = [
"tracing-appender",
"tracing-subscriber",
"wana_kana",
"winapi",
"windows 0.56.0",
"windows-core 0.56.0",
"zip",

View File

@ -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"

View File

@ -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
View 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())
}

View File

@ -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>