mirror of
https://github.com/mii443/vrclipboard-ime-gui.git
synced 2025-08-22 16:15: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-appender",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"wana_kana",
|
"wana_kana",
|
||||||
|
"winapi",
|
||||||
"windows 0.56.0",
|
"windows 0.56.0",
|
||||||
"windows-core 0.56.0",
|
"windows-core 0.56.0",
|
||||||
"zip",
|
"zip",
|
||||||
|
@ -36,6 +36,7 @@ ipc-channel = "0.19.0"
|
|||||||
zip = "2.6.1"
|
zip = "2.6.1"
|
||||||
wana_kana = "4.0.0"
|
wana_kana = "4.0.0"
|
||||||
itertools = "0.14.0"
|
itertools = "0.14.0"
|
||||||
|
winapi = { version = "0.3", features = ["winreg", "winnt"] }
|
||||||
|
|
||||||
[dependencies.tracing-subscriber]
|
[dependencies.tracing-subscriber]
|
||||||
version = "0.3.16"
|
version = "0.3.16"
|
||||||
|
@ -14,6 +14,7 @@ mod transform_rule;
|
|||||||
mod tsf;
|
mod tsf;
|
||||||
mod tsf_availability;
|
mod tsf_availability;
|
||||||
mod tsf_conversion;
|
mod tsf_conversion;
|
||||||
|
mod vr;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
io::{BufRead, BufReader},
|
io::{BufRead, BufReader},
|
||||||
@ -54,6 +55,7 @@ struct AppState {
|
|||||||
static STATE: Lazy<Mutex<Config>> = Lazy::new(|| Mutex::new(Config::load().unwrap()));
|
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 DICTIONARY: Lazy<Mutex<Dictionary>> = Lazy::new(|| Mutex::new(Dictionary::load().unwrap()));
|
||||||
static APP_HANDLE: OnceLock<AppHandle> = OnceLock::new();
|
static APP_HANDLE: OnceLock<AppHandle> = OnceLock::new();
|
||||||
|
static SERVER_PROCESS: Lazy<Mutex<Option<std::process::Child>>> = Lazy::new(|| Mutex::new(None));
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn load_settings(state: State<AppState>) -> Result<Config, String> {
|
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 {
|
fn start_server_process() -> String {
|
||||||
let exe_path = std::env::current_exe().unwrap();
|
let exe_path = std::env::current_exe().unwrap();
|
||||||
let server_path = exe_path.to_str().unwrap();
|
let server_path = exe_path.to_str().unwrap();
|
||||||
|
|
||||||
let mut command = std::process::Command::new(server_path);
|
let mut command = std::process::Command::new(server_path);
|
||||||
|
|
||||||
command.arg("server");
|
command.arg("server");
|
||||||
let mut child = command
|
|
||||||
.arg("server")
|
let mut child = command.stdout(Stdio::piped()).spawn().unwrap();
|
||||||
.stdout(Stdio::piped())
|
|
||||||
.spawn()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let stdout = child.stdout.take().unwrap();
|
let stdout = child.stdout.take().unwrap();
|
||||||
let reader = BufReader::new(stdout);
|
let reader = BufReader::new(stdout);
|
||||||
@ -160,11 +165,31 @@ fn start_server_process() -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*SERVER_PROCESS.lock().unwrap() = Some(child);
|
||||||
|
|
||||||
server_name
|
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));
|
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() {
|
fn extract_dictionary() {
|
||||||
let self_exe_path = PathBuf::from(SELF_EXE_PATH.read().unwrap().as_str());
|
let self_exe_path = PathBuf::from(SELF_EXE_PATH.read().unwrap().as_str());
|
||||||
let zip_path = self_exe_path
|
let zip_path = self_exe_path
|
||||||
@ -199,6 +224,8 @@ async fn main() {
|
|||||||
let server_name = start_server_process();
|
let server_name = start_server_process();
|
||||||
SERVER_NAME.lock().unwrap().replace(server_name);
|
SERVER_NAME.lock().unwrap().replace(server_name);
|
||||||
|
|
||||||
|
Lazy::force(&_SERVER_GUARD);
|
||||||
|
|
||||||
extract_dictionary();
|
extract_dictionary();
|
||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
@ -222,7 +249,8 @@ async fn main() {
|
|||||||
open_ms_settings_regionlanguage_jpnime,
|
open_ms_settings_regionlanguage_jpnime,
|
||||||
load_dictionary,
|
load_dictionary,
|
||||||
save_dictionary,
|
save_dictionary,
|
||||||
check_update
|
check_update,
|
||||||
|
register_manifest,
|
||||||
])
|
])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
APP_HANDLE.set(app.app_handle().to_owned()).unwrap();
|
APP_HANDLE.set(app.app_handle().to_owned()).unwrap();
|
||||||
@ -259,6 +287,17 @@ async fn main() {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
|
.on_window_event(|_window, event| match event {
|
||||||
|
tauri::WindowEvent::CloseRequested { .. } => {
|
||||||
|
cleanup_server_process();
|
||||||
|
}
|
||||||
|
tauri::WindowEvent::Destroyed => {
|
||||||
|
cleanup_server_process();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
})
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.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 React, { useState, useEffect, useRef } from 'react';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
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 {
|
export interface Config {
|
||||||
prefix: string;
|
prefix: string;
|
||||||
@ -110,6 +110,7 @@ const SettingsComponent: React.FC<SettingsComponentProps> = ({
|
|||||||
});
|
});
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'success' | 'error'>('idle');
|
const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'success' | 'error'>('idle');
|
||||||
|
const [manifestStatus, setManifestStatus] = useState<'idle' | 'registering' | 'success' | 'error'>('idle');
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -249,11 +250,36 @@ const SettingsComponent: React.FC<SettingsComponentProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 入力フィールドが無効化されるべきかを判定する関数
|
|
||||||
const isInputDisabled = () => {
|
const isInputDisabled = () => {
|
||||||
return settings.use_tsf_reconvert || settings.use_azookey_conversion;
|
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 (
|
return (
|
||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
@ -395,6 +421,32 @@ const SettingsComponent: React.FC<SettingsComponentProps> = ({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Reference in New Issue
Block a user