This commit is contained in:
mii
2024-08-03 15:10:22 +09:00
commit 59c1ecce7b
57 changed files with 8507 additions and 0 deletions

7
src-tauri/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas

4165
src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

40
src-tauri/Cargo.toml Normal file
View File

@@ -0,0 +1,40 @@
[package]
name = "vrclipboard-ime-gui"
version = "1.4.0"
description = "VRClipboard IME"
authors = ["mii"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "1", features = [] }
[dependencies]
tauri = { version = "1", features = [ "window-close", "window-start-dragging", "window-show", "window-unmaximize", "window-minimize", "window-maximize", "window-hide", "window-unminimize", "shell-open"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
chrono = "0.4.38"
chrono-tz = "0.9.0"
anyhow = "1.0.86"
clipboard = "0.5.0"
clipboard-master = "3.1.3"
serde_derive = "1.0.203"
serde_yaml = "0.9.34"
calc = { version = "*", default-features = false }
platform-dirs = "0.3.0"
once_cell = "1.19.0"
rosc = "~0.10"
regex = "1"
[dependencies.windows]
version = "0.52"
features = [
"Win32_System_Com",
"Win32_UI_Input_Ime"
]
[features]
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
custom-protocol = ["tauri/custom-protocol"]

3
src-tauri/build.rs Normal file
View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

4
src-tauri/config.yaml Normal file
View File

@@ -0,0 +1,4 @@
prefix: ";"
command: ";"
split: "/"
ignore_prefix: false

BIN
src-tauri/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
src-tauri/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
src-tauri/icons/icon.icns Normal file

Binary file not shown.

BIN
src-tauri/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
src-tauri/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

17
src-tauri/src/com.rs Normal file
View File

@@ -0,0 +1,17 @@
use anyhow::Result;
use windows::Win32::System::Com::{CoInitialize, CoUninitialize};
pub struct Com;
impl Drop for Com {
fn drop(&mut self) {
unsafe { CoUninitialize() };
}
}
impl Com {
pub fn new() -> Result<Self> {
unsafe { CoInitialize(None)? };
Ok(Com)
}
}

114
src-tauri/src/config.rs Normal file
View File

@@ -0,0 +1,114 @@
use std::{fs::File, io::{Read, Write}, path::{Path, PathBuf}};
use platform_dirs::AppDirs;
use serde::Serialize;
use serde_derive::Deserialize;
use anyhow::Result;
use tauri::State;
use crate::AppState;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Config {
#[serde(default = "semicolon" )]
pub prefix: String,
#[serde(default = "slash" )]
pub split: String,
#[serde(default = "semicolon" )]
pub command: String,
#[serde(default = "bool_true")]
pub ignore_prefix: bool,
#[serde(default)]
pub on_copy_mode: OnCopyMode,
#[serde(default = "bool_true")]
pub skip_url: bool
}
impl Default for Config {
fn default() -> Self {
Self {
prefix: ";".to_string(),
split: "/".to_string(),
command: ";".to_string(),
ignore_prefix: true,
on_copy_mode: OnCopyMode::ReturnToChatbox ,
skip_url: true
}
}
}
#[inline]
fn slash() -> String { String::from("/") }
#[inline]
fn semicolon() -> String { String::from(";") }
#[inline]
fn bool_true() -> bool { true }
#[derive(Debug, Clone, Deserialize, Serialize)]
pub enum OnCopyMode {
ReturnToClipboard,
ReturnToChatbox,
SendDirectly
}
impl Default for OnCopyMode {
fn default() -> Self {
Self::ReturnToChatbox
}
}
impl Config {
pub fn load() -> Result<Config> {
std::fs::create_dir_all(Self::get_path()).unwrap();
if !Path::new(&Self::get_path().join("config.yaml")).exists() {
Self::generate_default_config()?;
}
let mut file = File::open(&Self::get_path().join("config.yaml"))?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
let config: Config = serde_yaml::from_str(&contents)?;
Ok(config)
}
pub fn save(&self, state: State<AppState>) -> Result<(), String> {
std::fs::create_dir_all(Self::get_path()).unwrap();
let mut file = match File::create(&Self::get_path().join("config.yaml")) {
Ok(file) => file,
Err(e) => {
println!("Err: {:?}", e);
return Err(format!("Failed to create config file: {}", e))
},
};
match serde_yaml::to_string(&self) {
Ok(yaml) => {
if let Err(e) = file.write_all(yaml.as_bytes()) {
println!("Err: {:?}", e);
return Err(format!("Failed to write config: {}", e));
}
let mut app_config = state.config.lock().unwrap();
*app_config = self.clone();
Ok(())
},
Err(e) => {
println!("Err: {:?}", e);
Err(format!("Failed to serialize config: {}", e))
},
}
}
pub fn generate_default_config() -> Result<()> {
let mut file = File::create(&Self::get_path().join("config.yaml"))?;
file.write_all(serde_yaml::to_string(&Config::default()).unwrap().as_bytes())?;
file.flush()?;
Ok(())
}
pub fn get_path() -> PathBuf {
let app_dirs = AppDirs::new(Some("vrclipboard-ime"), false).unwrap();
let app_data = app_dirs.config_dir;
app_data
}
}

View File

@@ -0,0 +1,79 @@
use crate::{config::Config, converter::converter::{get_custom_converter, Converter}, STATE};
use anyhow::Result;
use regex::Regex;
pub struct ConversionBlock {
pub text: String,
pub converter: Box<dyn Converter>,
}
pub struct Conversion;
impl Conversion {
pub fn new() -> Self {
Self {}
}
pub fn convert_text(&self, text: &str) -> Result<String> {
println!("Processing text: {}", text);
let blocks = self.split_text(text)?;
self.convert_blocks(blocks)
}
pub fn convert_blocks(&self, blocks: Vec<ConversionBlock>) -> Result<String> {
let mut result = String::new();
for block in blocks {
let converted = self.convert_block(&block)?;
println!(" {}: {} -> {}", block.converter.name(), block.text, converted);
result.push_str(&converted);
}
println!(" {}", result);
Ok(result)
}
pub fn convert_block(&self, block: &ConversionBlock) -> Result<String> {
if block.text == "" {
return Ok(String::default());
}
block.converter.convert(&block.text)
}
pub fn split_text(&self, text: &str) -> Result<Vec<ConversionBlock>> {
let mut text = text.to_string();
let mut blocks = Vec::new();
let mut current_converter = 'r';
let config = self.get_config();
if text.starts_with(&config.command) {
text = text.split_off(1);
if text.len() != 0 {
current_converter = text.chars().next().unwrap_or('n');
text = text.split_off(1);
}
}
for (i, command_splitted) in text.split(&config.command).enumerate() {
let mut command_splitted = command_splitted.to_string();
if i != 0 {
if command_splitted.len() != 0 {
current_converter = command_splitted.chars().next().unwrap_or('n');
command_splitted = command_splitted.split_off(1);
}
}
for splitted in command_splitted.split(&config.split) {
blocks.push(ConversionBlock {
text: splitted.to_string(),
converter: get_custom_converter(current_converter).unwrap_or(get_custom_converter('n').unwrap())
});
}
}
Ok(blocks)
}
pub fn get_config(&self) -> Config {
STATE.lock().unwrap().clone()
}
}

View File

@@ -0,0 +1,21 @@
use calc::Context;
use super::converter::Converter;
pub struct CalculatorConverter;
impl Converter for CalculatorConverter {
fn convert(&self, text: &str) -> anyhow::Result<String> {
let mut ctx = Context::<f64>::default();
let result = match ctx.evaluate(text) {
Ok(result) => format!("{} = {}", text, result.to_string()),
Err(e) => e.to_string(),
};
Ok(result)
}
fn name(&self) -> String {
"calculator".to_string()
}
}

View File

@@ -0,0 +1,19 @@
use anyhow::Result;
use super::{calculator::CalculatorConverter, hiragana::HiraganaConverter, katakana::KatakanaConverter, none_converter::NoneConverter, roman_to_kanji::RomanToKanjiConverter};
pub trait Converter {
fn convert(&self, text: &str) -> Result<String>;
fn name(&self) -> String;
}
pub fn get_custom_converter(prefix: char) -> Option<Box<dyn Converter>> {
match prefix {
'r' => Some(Box::new(RomanToKanjiConverter) as Box<dyn Converter>),
'h' => Some(Box::new(HiraganaConverter) as Box<dyn Converter>),
'c' => Some(Box::new(CalculatorConverter) as Box<dyn Converter>),
'n' => Some(Box::new(NoneConverter) as Box<dyn Converter>),
'k' => Some(Box::new(KatakanaConverter) as Box<dyn Converter>),
_ => None,
}
}

View File

@@ -0,0 +1,19 @@
use windows::Win32::UI::Input::Ime::{FELANG_CMODE_HIRAGANAOUT, FELANG_CMODE_NOINVISIBLECHAR, FELANG_CMODE_PRECONV, FELANG_REQ_REV};
use crate::felanguage::FElanguage;
use super::converter::Converter;
#[derive(Clone)]
pub struct HiraganaConverter;
impl Converter for HiraganaConverter {
fn convert(&self, text: &str) -> anyhow::Result<String> {
let felanguage = FElanguage::new()?;
felanguage.j_morph_result(text, FELANG_REQ_REV, FELANG_CMODE_HIRAGANAOUT | FELANG_CMODE_PRECONV | FELANG_CMODE_NOINVISIBLECHAR)
}
fn name(&self) -> String {
"hiragana".to_string()
}
}

View File

@@ -0,0 +1,19 @@
use windows::Win32::UI::Input::Ime::{FELANG_CMODE_KATAKANAOUT, FELANG_CMODE_NOINVISIBLECHAR, FELANG_CMODE_PRECONV, FELANG_REQ_REV};
use crate::felanguage::FElanguage;
use super::converter::Converter;
#[derive(Clone)]
pub struct KatakanaConverter;
impl Converter for KatakanaConverter {
fn convert(&self, text: &str) -> anyhow::Result<String> {
let felanguage = FElanguage::new()?;
felanguage.j_morph_result(text, FELANG_REQ_REV, FELANG_CMODE_KATAKANAOUT | FELANG_CMODE_PRECONV | FELANG_CMODE_NOINVISIBLECHAR)
}
fn name(&self) -> String {
"katakana".to_string()
}
}

View File

@@ -0,0 +1,6 @@
pub mod converter;
mod hiragana;
mod katakana;
mod roman_to_kanji;
mod calculator;
mod none_converter;

View File

@@ -0,0 +1,13 @@
use super::converter::Converter;
pub struct NoneConverter;
impl Converter for NoneConverter {
fn convert(&self, text: &str) -> anyhow::Result<String> {
Ok(text.to_string())
}
fn name(&self) -> String {
"none".to_string()
}
}

View File

@@ -0,0 +1,21 @@
use windows::Win32::UI::Input::Ime::{FELANG_CMODE_HIRAGANAOUT, FELANG_CMODE_NOINVISIBLECHAR, FELANG_CMODE_PRECONV, FELANG_CMODE_ROMAN, FELANG_REQ_CONV};
use crate::felanguage::FElanguage;
use super::converter::Converter;
pub struct RomanToKanjiConverter;
impl Converter for RomanToKanjiConverter {
fn convert(&self, text: &str) -> anyhow::Result<String> {
let felanguage = FElanguage::new()?;
felanguage.j_morph_result(text, FELANG_REQ_CONV, FELANG_CMODE_HIRAGANAOUT
| FELANG_CMODE_ROMAN
| FELANG_CMODE_NOINVISIBLECHAR
| FELANG_CMODE_PRECONV)
}
fn name(&self) -> String {
"roman_to_kanji".to_string()
}
}

View File

@@ -0,0 +1,54 @@
use std::ptr;
use anyhow::Result;
use windows::{
core::{w, PCWSTR},
Win32::{
System::Com::{CLSIDFromProgID, CoCreateInstance, CLSCTX_ALL},
UI::Input::Ime::IFELanguage,
},
};
pub struct FElanguage {
ife: IFELanguage,
}
impl Drop for FElanguage {
fn drop(&mut self) {
unsafe { self.ife.Close().ok() };
}
}
impl FElanguage {
pub fn new() -> Result<Self> {
let clsid = unsafe { CLSIDFromProgID(w!("MSIME.Japan"))? };
let ife: IFELanguage = unsafe { CoCreateInstance(&clsid, None, CLSCTX_ALL)? };
unsafe { ife.Open()? };
Ok(FElanguage { ife })
}
pub fn j_morph_result(&self, input: &str, request: u32, mode: u32) -> Result<String> {
let input_utf16: Vec<u16> = input.encode_utf16().chain(Some(0)).collect();
let input_len = input_utf16.len();
let input_pcwstr = PCWSTR::from_raw(input_utf16.as_ptr());
let mut result_ptr = ptr::null_mut();
unsafe {
self.ife.GetJMorphResult(
request,
mode,
input_len as _,
input_pcwstr,
ptr::null_mut(),
&mut result_ptr,
)?;
}
let result_struct = unsafe { ptr::read_unaligned(result_ptr) };
let output_bstr_ptr = result_struct.pwchOutput;
let output_bstr = unsafe { output_bstr_ptr.to_string()? };
let output_string: String = output_bstr.chars().take(result_struct.cchOutput as usize).collect();
Ok(output_string)
}
}

108
src-tauri/src/handler.rs Normal file
View File

@@ -0,0 +1,108 @@
use std::net::UdpSocket;
use chrono::Local;
use clipboard::{ClipboardContext, ClipboardProvider};
use clipboard_master::{ClipboardHandler, CallbackResult};
use regex::Regex;
use rosc::{encoder, OscMessage, OscPacket, OscType};
use tauri::{AppHandle, Manager};
use crate::{config::{Config, OnCopyMode}, conversion::Conversion, Log, STATE};
use anyhow::Result;
pub struct ConversionHandler {
app_handle: AppHandle,
conversion: Conversion,
clipboard_ctx: ClipboardContext,
last_text: String,
}
impl ConversionHandler {
pub fn new(app_handle: AppHandle) -> Result<Self> {
let conversion = Conversion::new();
let clipboard_ctx = ClipboardProvider::new().unwrap();
Ok(Self { app_handle, conversion, clipboard_ctx, last_text: String::new() })
}
pub fn get_config(&self) -> Config {
STATE.lock().unwrap().clone()
}
}
impl ClipboardHandler for ConversionHandler {
fn on_clipboard_change(&mut self) -> CallbackResult {
let config = self.get_config();
if let Ok(mut contents) = self.clipboard_ctx.get_contents() {
if contents != self.last_text {
if contents.starts_with(&config.prefix) || config.ignore_prefix {
if config.skip_url && Regex::new(r"(http://|https://){1}[\w\.\-/:\#\?=\&;%\~\+]+").unwrap().is_match(&contents) {
return CallbackResult::Next;
}
let parsed_contents = if config.ignore_prefix { contents } else { contents.split_off(1) };
let converted = match self.conversion.convert_text(&parsed_contents) {
Ok(converted) => converted,
Err(err) => {
println!("Error: {:?}", err);
format!("Error: {:?}", err)
}
};
self.last_text = converted.clone();
match config.on_copy_mode {
OnCopyMode::ReturnToClipboard => {
let mut count = 0;
while self.clipboard_ctx.set_contents(converted.clone()).is_err() {
if count > 4 {
break;
}
count += 1;
}
},
OnCopyMode::ReturnToChatbox => {
let sock = UdpSocket::bind("127.0.0.1:0").unwrap();
let msg_buf = encoder::encode(&OscPacket::Message(OscMessage {
addr: "/chatbox/input".to_string(),
args: vec![
OscType::String(converted.clone()),
OscType::Bool(false),
OscType::Bool(true)
]
})).unwrap();
sock.send_to(&msg_buf, "127.0.0.1:9000").unwrap();
},
OnCopyMode::SendDirectly => {
let sock = UdpSocket::bind("127.0.0.1:0").unwrap();
let msg_buf = encoder::encode(&OscPacket::Message(OscMessage {
addr: "/chatbox/input".to_string(),
args: vec![
OscType::String(converted.clone()),
OscType::Bool(true),
OscType::Bool(true)
]
})).unwrap();
sock.send_to(&msg_buf, "127.0.0.1:9000").unwrap();
},
}
let datetime = Local::now();
if self.app_handle
.emit_all("addLog", Log {
time: datetime.format("%Y %m/%d %H:%M:%S").to_string(),
original: parsed_contents,
converted
}).is_err() {
println!("App handle add log failed.");
}
} else {
self.last_text = contents;
}
}
}
CallbackResult::Next
}
}

82
src-tauri/src/main.rs Normal file
View File

@@ -0,0 +1,82 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
//#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod com;
mod felanguage;
mod handler;
mod conversion;
mod config;
mod converter;
mod transform_rule;
use std::sync::Mutex;
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use tauri::{Manager, State};
use clipboard_master::Master;
use com::Com;
use config::Config;
use handler::ConversionHandler;
#[derive(Serialize, Deserialize, Debug, Clone)]
struct Log {
pub time: String,
pub original: String,
pub converted: String,
}
struct AppState {
config: Mutex<Config>,
}
static STATE: Lazy<Mutex<Config>> = Lazy::new(|| Mutex::new(Config::load().unwrap()));
#[tauri::command]
fn load_settings(state: State<AppState>) -> Result<Config, String> {
match Config::load() {
Ok(config) => {
let mut app_config = state.config.lock().unwrap();
*app_config = config.clone();
Ok(config)
},
Err(e) => Err(format!("Failed to load settings: {}", e)),
}
}
#[tauri::command]
fn save_settings(config: Config, state: State<AppState>) -> Result<(), String> {
*STATE.lock().unwrap() = config.clone();
config.save(state)
}
fn main() {
println!("VRClipboard-IME Logs\nバグがあった場合はこのログを送ってください。");
tauri::Builder::default()
.manage(AppState {
config: Mutex::new(Config::load().unwrap_or_else(|_| {
Config::generate_default_config().expect("Failed to generate default config");
Config::load().expect("Failed to load default config")
})),
})
.invoke_handler(tauri::generate_handler![load_settings, save_settings])
.setup(|app| {
app.manage(STATE.lock().unwrap().clone());
let app_handle = app.app_handle();
std::thread::spawn(move || {
let _com = Com::new().unwrap();
let conversion_handler = ConversionHandler::new(app_handle).unwrap();
let mut master = Master::new(conversion_handler);
master.run().unwrap();
});
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@@ -0,0 +1,3 @@
pub struct TransformRule {
}

56
src-tauri/tauri.conf.json Normal file
View File

@@ -0,0 +1,56 @@
{
"build": {
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build",
"devPath": "http://localhost:1420",
"distDir": "../dist"
},
"package": {
"productName": "vrclipboard-ime-gui",
"version": "1.4.0"
},
"tauri": {
"allowlist": {
"all": false,
"shell": {
"all": false,
"open": true
},
"window": {
"maximize": true,
"minimize": true,
"hide": true,
"startDragging": true,
"show": true,
"unmaximize": true,
"unminimize": true,
"close": true
}
},
"windows": [
{
"title": "VRClipboard IME",
"width": 800,
"height": 640,
"visible": true,
"decorations": false,
"transparent": true
}
],
"security": {
"csp": null
},
"bundle": {
"active": true,
"targets": "all",
"identifier": "dev.mii.vrclipboard-ime",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
}