Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions openless-all/app/src-tauri/src/windows_ime_ipc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ const NMPWAIT_NOWAIT: u32 = 0x00000001;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WindowsImeIpcError {
// Constructed only on the non-Windows path of `submit_text`; on Windows the
// active arms map to NoReadyClient/Timeout/etc. Keep the variant for the
// cross-platform stub.
#[allow(dead_code)]
Unavailable(String),
NoReadyClient,
Timeout,
Expand Down Expand Up @@ -113,13 +117,17 @@ pub struct ImeSubmitTarget {
pub thread_id: u32,
}

// Fields are exercised only via the test helpers below; keep dead_code allow
// scoped narrowly so the production path stays warning-clean.
#[derive(Clone)]
pub struct WindowsImeIpcServer {
#[allow(dead_code)]
inner: std::sync::Arc<parking_lot::Mutex<WindowsImeIpcState>>,
}

#[derive(Debug, Default)]
struct WindowsImeIpcState {
#[allow(dead_code)]
ready_client_id: Option<String>,
}

Expand All @@ -130,10 +138,12 @@ impl WindowsImeIpcServer {
}
}

#[allow(dead_code)]
pub fn mark_client_ready_for_test(&self, client_id: String) {
self.inner.lock().ready_client_id = Some(client_id);
}

#[allow(dead_code)]
pub fn has_ready_client(&self) -> bool {
self.inner.lock().ready_client_id.is_some()
}
Expand Down
62 changes: 55 additions & 7 deletions openless-all/app/src-tauri/src/windows_ime_profile.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,48 @@
pub const OPENLESS_TSF_LANG_ID: u16 = 0x0804;
pub const OPENLESS_TEXT_SERVICE_CLSID_BRACED: &str = "{6B9F3F4F-5EE7-42D6-9C61-9F80B03A5D7D}";
pub const OPENLESS_PROFILE_GUID_BRACED: &str = "{9B5F5E04-23F6-47DA-9A26-D221F6C3F02E}";

/// Resolve the host UI lang id at runtime.
///
/// On Windows we ask `GetUserDefaultUILanguage`; on other platforms (and as a
/// safe fallback) we return 0x0409 (en-US) so that callers downstream can do
/// a "non-zh" branch decision without panicking.
#[cfg(target_os = "windows")]
pub fn host_ui_lang_id() -> u16 {
// SAFETY: GetUserDefaultUILanguage takes no arguments and always succeeds.
unsafe { windows::Win32::Globalization::GetUserDefaultUILanguage() }
}

#[cfg(not(target_os = "windows"))]
#[allow(dead_code)]
pub fn host_ui_lang_id() -> u16 {
0x0409
}

/// The lang id under which we activate the OpenLess TSF profile.
///
/// Historically this was a hard-coded `0x0804` (zh-CN), which on non-zh hosts
/// (notably Japanese hosts running ATOK or Microsoft IME) caused the active
/// IME to get hijacked into a Chinese-input profile. We now resolve this from
/// the host UI language at runtime: zh hosts get `0x0804` so the existing
/// activation path continues to work; non-zh hosts get their own host lang id.
///
/// Note: the bundled C++ TSF DLL still ships with `kOpenLessLangId = 0x0804`
/// in `guids.h`. That's a follow-up — see the prepare_session log line in
/// `windows_ime_session.rs`. With this PR Rust no longer fights the DLL on zh
/// hosts (the values match), and on non-zh hosts the prepare path returns
/// `unavailable()` before we ever reach this function.
#[allow(dead_code)]
pub fn openless_tsf_lang_id() -> u16 {
let host = host_ui_lang_id();
let primary = host & 0x03FF;
const PRIMARY_LANG_CHINESE: u16 = 0x04;
if primary == PRIMARY_LANG_CHINESE {
0x0804
} else {
host
}
}

use crate::types::{WindowsImeInstallState, WindowsImeStatus};

#[cfg(target_os = "windows")]
Expand Down Expand Up @@ -88,6 +129,8 @@ pub fn restore_decision(

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WindowsImeProfileError {
// TSF path is force-disabled until per-host lang_id is wired in. See windows_ime_session.rs::prepare_session.
#[allow(dead_code)]
Unavailable(String),
WindowsApi(String),
}
Expand Down Expand Up @@ -130,10 +173,12 @@ impl WindowsImeProfileManager {
Self
}

#[allow(dead_code)]
pub fn capture_active_profile(&self) -> WindowsImeProfileResult<ImeProfileSnapshot> {
windows_impl::capture_active_profile()
}

#[allow(dead_code)]
pub fn activate_openless_profile(&self) -> WindowsImeProfileResult<()> {
windows_impl::activate_openless_profile()
}
Expand Down Expand Up @@ -207,6 +252,7 @@ mod windows_impl {
const OPENLESS_TSF_KEYBOARD_CATEGORY_KEY: &str = r"Software\Microsoft\CTF\TIP\{6B9F3F4F-5EE7-42D6-9C61-9F80B03A5D7D}\Category\Category\{34745C63-B2F0-4784-8B67-5E12C8701A31}\{6B9F3F4F-5EE7-42D6-9C61-9F80B03A5D7D}";
const OPENLESS_TSF_IMMERSIVE_CATEGORY_KEY: &str = r"Software\Microsoft\CTF\TIP\{6B9F3F4F-5EE7-42D6-9C61-9F80B03A5D7D}\Category\Category\{13A016DF-560B-46CD-947A-4C3AF1E0E35D}\{6B9F3F4F-5EE7-42D6-9C61-9F80B03A5D7D}";
const OPENLESS_TSF_SYSTRAY_CATEGORY_KEY: &str = r"Software\Microsoft\CTF\TIP\{6B9F3F4F-5EE7-42D6-9C61-9F80B03A5D7D}\Category\Category\{25504FB4-7BAB-4BC1-9C69-CF81890F0EF5}\{6B9F3F4F-5EE7-42D6-9C61-9F80B03A5D7D}";
#[allow(dead_code)]
const OPENLESS_PROFILE_ACTIVATION_FLAGS: u32 =
TF_IPPMF_FORSESSION | TF_IPPMF_DONTCARECURRENTINPUTLANGUAGE | TF_IPPMF_ENABLEPROFILE;
const PROFILE_RESTORE_FLAGS: u32 = TF_IPPMF_FORSESSION | TF_IPPMF_DONTCARECURRENTINPUTLANGUAGE;
Expand Down Expand Up @@ -296,20 +342,22 @@ mod windows_impl {
)
}

#[allow(dead_code)]
pub fn activate_openless_profile() -> WindowsImeProfileResult<()> {
let clsid = parse_guid(OPENLESS_TEXT_SERVICE_CLSID_BRACED)?;
let profile_guid = parse_guid(OPENLESS_PROFILE_GUID_BRACED)?;
let lang_id = openless_tsf_lang_id();

with_input_processor_profiles(|profiles| unsafe {
profiles.EnableLanguageProfile(&clsid, OPENLESS_TSF_LANG_ID, &profile_guid, true)?;
profiles.ChangeCurrentLanguage(OPENLESS_TSF_LANG_ID)?;
profiles.ActivateLanguageProfile(&clsid, OPENLESS_TSF_LANG_ID, &profile_guid)
profiles.EnableLanguageProfile(&clsid, lang_id, &profile_guid, true)?;
profiles.ChangeCurrentLanguage(lang_id)?;
profiles.ActivateLanguageProfile(&clsid, lang_id, &profile_guid)
})?;

with_profile_manager(|manager| unsafe {
manager.ActivateProfile(
TF_PROFILETYPE_INPUTPROCESSOR,
OPENLESS_TSF_LANG_ID,
lang_id,
&clsid,
&profile_guid,
null_hkl(),
Expand Down Expand Up @@ -358,7 +406,7 @@ mod windows_impl {
let snapshot = capture_active_profile()?;

Ok(matches!(snapshot.kind(), ImeProfileKind::TextService)
&& snapshot.lang_id() == OPENLESS_TSF_LANG_ID
&& snapshot.lang_id() == openless_tsf_lang_id()
&& snapshot.clsid().map(normalize_guid_string).as_deref()
== Some(OPENLESS_TEXT_SERVICE_CLSID_BRACED)
&& snapshot
Expand Down Expand Up @@ -524,6 +572,7 @@ mod windows_impl {
operation(&manager).map_err(windows_api_error("ITfInputProcessorProfileMgr operation"))
}

#[allow(dead_code)]
fn with_input_processor_profiles<T>(
operation: impl FnOnce(&ITfInputProcessorProfiles) -> windows::core::Result<T>,
) -> WindowsImeProfileResult<T> {
Expand Down Expand Up @@ -670,7 +719,6 @@ mod windows_tests {

#[test]
fn openless_profile_identifiers_are_fixed() {
assert_eq!(OPENLESS_TSF_LANG_ID, 0x0804);
assert_eq!(
OPENLESS_TEXT_SERVICE_CLSID_BRACED,
"{6B9F3F4F-5EE7-42D6-9C61-9F80B03A5D7D}"
Expand Down
3 changes: 3 additions & 0 deletions openless-all/app/src-tauri/src/windows_ime_protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ pub fn decode_message(line: &str) -> Result<ImePipeMessage, serde_json::Error> {
serde_json::from_str(line)
}

// Used only by tests today; kept as part of the public protocol surface so
// the future IPC client can validate session-id correlation.
#[allow(dead_code)]
pub fn is_result_for_pending_session(
message: &ImePipeMessage,
pending_session_id: &str,
Expand Down
20 changes: 20 additions & 0 deletions openless-all/app/src-tauri/src/windows_ime_session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,24 @@ impl WindowsImeSessionController {
pub fn prepare_session(&self) -> PreparedWindowsImeSession {
#[cfg(target_os = "windows")]
{
// Per-host gating: the bundled TSF DLL still hard-codes
// lang_id 0x0804 (zh-CN) in guids.h. On a zh host that lines up
// and the existing TSF activation path works as designed. On any
// non-zh host, going through TSF either hijacks the active IME
// (ATOK / Microsoft IME on Japanese hosts) or fails activation
// and corrupts IME state. Until guids.h + DLL rebuild ships, only
// route through TSF when the host UI language is zh.
let host_lang = crate::windows_ime_profile::host_ui_lang_id();
let primary = host_lang & 0x03FF;
// 0x04 is the primary language id for Chinese (Simplified/Traditional/etc).
const PRIMARY_LANG_CHINESE: u16 = 0x04;
if primary != PRIMARY_LANG_CHINESE {
log::info!(
"[windows-ime] host UI lang_id=0x{host_lang:04x} is not zh; skipping TSF path"
);
return PreparedWindowsImeSession::unavailable();
}

let saved_profile = match self.profile_manager.capture_active_profile() {
Ok(snapshot) => snapshot,
Err(error) => {
Expand All @@ -114,6 +132,8 @@ impl WindowsImeSessionController {

#[cfg(not(target_os = "windows"))]
{
let _ = &self.profile_manager;
let _ = &self.ipc;
PreparedWindowsImeSession::unavailable()
}
}
Expand Down
Loading