Skip to content

Commit 080e96d

Browse files
cuzicclaude
andcommitted
refactor(output): ImeApplyPlanner を Output から抽出し Plan/Result を型化
IME ON/OFF 適用の「計画」(ImeApplyPlan) と「結果」(ImeApplyResult) を 副作用なしの data-model として型化。ImeApplyPlanner::plan が観測 コンテキストから ime_controller 準拠の戦略優先順で適用機構を導出し、 ImeApplyResult::from_outcome が ImeOpenOutcome を正規化する。 production 配線(plan→execute 2 段化)は将来の TODO。 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 4782e3e commit 080e96d

2 files changed

Lines changed: 191 additions & 0 deletions

File tree

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
//! IME ON/OFF 適用の「計画」と「結果」を型化する data-model 層。
2+
//!
3+
//! 実行は既存の [`crate::ime_controller`] Strategy 群が担うが、このモジュールは
4+
//! 「どの機構で適用するか([`ImeApplyPlan`])」を副作用なしで決定し、
5+
//! 実行後の [`awase::platform::ImeOpenOutcome`] を意味のある [`ImeApplyResult`] に
6+
//! 正規化する。plan と execute を分離することでテスト可能性を高める。
7+
//!
8+
//! TODO(H-4/H-5): 現状 planner の決定は ime_controller の Strategy 選択と並走する
9+
//! 純粋関数。将来的に `ImeController::apply` を `plan()` → `execute(plan)` の
10+
//! 2 段構成へ移行し、この planner を SSOT にする。
11+
12+
use awase::platform::ImeOpenOutcome;
13+
use crate::tsf::observer::ActiveImeKind;
14+
15+
/// IME を目標状態へ移すために選択された適用機構。
16+
///
17+
/// [`crate::ime_controller`] の戦略優先順(ImmCross → GjiDirect → MsImeDirect →
18+
/// KanjiToggle)に対応する。
19+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20+
pub(crate) enum ImeApplyPlan {
21+
/// 既に目標状態のため何も送らない(`AlreadyMatched` 相当)。
22+
Noop,
23+
/// MS-IME 環境で冪等な VK_DBE_HIRAGANA / ALPHANUMERIC を送る。
24+
SendVkDbeHiragana,
25+
/// IME 種別不明環境のフォールバック。非冪等な VK_KANJI トグルを送る。
26+
SendKanjiToggle,
27+
/// IMM クロスプロセス / GJI direct で TSF compartment を開閉する。
28+
UseTsfCompartment,
29+
/// GJI プローブ実行中のため適用を保留する。
30+
DeferUntilProbe,
31+
}
32+
33+
/// IME 適用の正規化された結果。
34+
///
35+
/// [`ImeOpenOutcome`] を「確定 / 未確定送信済み / 失敗 / 保留 / 陳腐化」に写像する。
36+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37+
pub(crate) enum ImeApplyResult {
38+
/// IMM 経由等で目標状態が確定した(`Applied` / `AlreadyMatched`)。
39+
Confirmed,
40+
/// フォールバックを送信したが OS 処理完了まで不確定(`FallbackSent`)。
41+
SentUnverified,
42+
/// 適用に失敗した(`Failed`、非日本語環境等)。
43+
Failed,
44+
/// プローブ保留等で適用しなかった。
45+
Deferred,
46+
/// shadow が stale でトグルが unsafe のため送信を見送った(`UnsafeToToggle`)。
47+
Stale,
48+
}
49+
50+
impl ImeApplyResult {
51+
/// 実行後の [`ImeOpenOutcome`] を正規化する。
52+
#[must_use]
53+
pub(crate) fn from_outcome(outcome: ImeOpenOutcome) -> Self {
54+
match outcome {
55+
ImeOpenOutcome::Applied | ImeOpenOutcome::AlreadyMatched => Self::Confirmed,
56+
ImeOpenOutcome::FallbackSent => Self::SentUnverified,
57+
ImeOpenOutcome::Failed => Self::Failed,
58+
ImeOpenOutcome::UnsafeToToggle => Self::Stale,
59+
}
60+
}
61+
62+
/// shadow model / applied_snapshot を更新してよい結果か。
63+
///
64+
/// `Deferred` / `Stale` は適用が実行されていないため状態を進めてはいけない。
65+
#[must_use]
66+
pub(crate) fn should_commit_state(self) -> bool {
67+
matches!(self, Self::Confirmed | Self::SentUnverified)
68+
}
69+
}
70+
71+
/// [`ImeApplyPlanner::plan`] の入力。観測値はここに集約して渡す(planner は自ら読まない)。
72+
#[derive(Debug, Clone, Copy)]
73+
pub(crate) struct ImeApplyContext {
74+
/// shadow が既に目標 open 状態か。
75+
pub already_matched: bool,
76+
/// GJI プローブ実行中か。
77+
pub probe_in_flight: bool,
78+
/// IMM クロスプロセス bridge が生きているか(Imm32Unavailable では false)。
79+
pub imm_cross_available: bool,
80+
/// 検出済み IME 種別。
81+
pub active_ime_kind: ActiveImeKind,
82+
/// アプリが VK_KANJI トグル制御を要するか(Chrome/Edge 等)。
83+
pub uses_kanji_toggle: bool,
84+
}
85+
86+
/// IME 適用機構を副作用なしで決定する planner。
87+
pub(crate) struct ImeApplyPlanner;
88+
89+
impl ImeApplyPlanner {
90+
/// 観測コンテキストから適用計画を導出する。
91+
///
92+
/// 戦略優先順(ime_controller 準拠):
93+
/// 1. 既に一致 → `Noop`
94+
/// 2. プローブ実行中 → `DeferUntilProbe`
95+
/// 3. IMM クロス可 or GJI 検出 → `UseTsfCompartment`
96+
/// 4. MS-IME 検出 → `SendVkDbeHiragana`
97+
/// 5. それ以外(VK_KANJI トグルアプリ / 種別不明)→ `SendKanjiToggle`
98+
#[must_use]
99+
pub(crate) fn plan(ctx: &ImeApplyContext) -> ImeApplyPlan {
100+
if ctx.already_matched {
101+
return ImeApplyPlan::Noop;
102+
}
103+
if ctx.probe_in_flight {
104+
return ImeApplyPlan::DeferUntilProbe;
105+
}
106+
match ctx.active_ime_kind {
107+
ActiveImeKind::GoogleJapaneseInput => ImeApplyPlan::UseTsfCompartment,
108+
ActiveImeKind::MicrosoftIme if !ctx.uses_kanji_toggle => {
109+
ImeApplyPlan::SendVkDbeHiragana
110+
}
111+
_ if ctx.imm_cross_available && !ctx.uses_kanji_toggle => {
112+
ImeApplyPlan::UseTsfCompartment
113+
}
114+
_ => ImeApplyPlan::SendKanjiToggle,
115+
}
116+
}
117+
}
118+
119+
#[cfg(test)]
120+
mod tests {
121+
use super::*;
122+
123+
fn ctx() -> ImeApplyContext {
124+
ImeApplyContext {
125+
already_matched: false,
126+
probe_in_flight: false,
127+
imm_cross_available: false,
128+
active_ime_kind: ActiveImeKind::GoogleJapaneseInput,
129+
uses_kanji_toggle: false,
130+
}
131+
}
132+
133+
#[test]
134+
fn already_matched_is_noop() {
135+
let c = ImeApplyContext { already_matched: true, ..ctx() };
136+
assert_eq!(ImeApplyPlanner::plan(&c), ImeApplyPlan::Noop);
137+
}
138+
139+
#[test]
140+
fn probe_in_flight_defers_before_strategy() {
141+
let c = ImeApplyContext { probe_in_flight: true, ..ctx() };
142+
assert_eq!(ImeApplyPlanner::plan(&c), ImeApplyPlan::DeferUntilProbe);
143+
}
144+
145+
#[test]
146+
fn gji_uses_compartment() {
147+
assert_eq!(ImeApplyPlanner::plan(&ctx()), ImeApplyPlan::UseTsfCompartment);
148+
}
149+
150+
#[test]
151+
fn msime_without_kanji_toggle_sends_dbe() {
152+
let c = ImeApplyContext {
153+
active_ime_kind: ActiveImeKind::MicrosoftIme,
154+
..ctx()
155+
};
156+
assert_eq!(ImeApplyPlanner::plan(&c), ImeApplyPlan::SendVkDbeHiragana);
157+
}
158+
159+
#[test]
160+
fn kanji_toggle_app_falls_back() {
161+
let c = ImeApplyContext {
162+
active_ime_kind: ActiveImeKind::MicrosoftIme,
163+
uses_kanji_toggle: true,
164+
..ctx()
165+
};
166+
assert_eq!(ImeApplyPlanner::plan(&c), ImeApplyPlan::SendKanjiToggle);
167+
}
168+
169+
#[test]
170+
fn outcome_normalization() {
171+
assert_eq!(ImeApplyResult::from_outcome(ImeOpenOutcome::Applied), ImeApplyResult::Confirmed);
172+
assert_eq!(
173+
ImeApplyResult::from_outcome(ImeOpenOutcome::AlreadyMatched),
174+
ImeApplyResult::Confirmed
175+
);
176+
assert_eq!(
177+
ImeApplyResult::from_outcome(ImeOpenOutcome::FallbackSent),
178+
ImeApplyResult::SentUnverified
179+
);
180+
assert_eq!(ImeApplyResult::from_outcome(ImeOpenOutcome::Failed), ImeApplyResult::Failed);
181+
assert_eq!(
182+
ImeApplyResult::from_outcome(ImeOpenOutcome::UnsafeToToggle),
183+
ImeApplyResult::Stale
184+
);
185+
assert!(ImeApplyResult::Confirmed.should_commit_state());
186+
assert!(!ImeApplyResult::Deferred.should_commit_state());
187+
assert!(!ImeApplyResult::Stale.should_commit_state());
188+
}
189+
}

crates/awase-windows/src/output/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ mod resolve;
1515
mod vk_send;
1616
mod key_injector;
1717
mod tsf_warmup_coord;
18+
#[allow(dead_code)] // production 配線は将来(plan→execute 2 段化)。現状は純粋 planner + テスト
19+
mod ime_apply_planner;
1820
use resolve::special_key_to_vk;
1921
pub(crate) use tsf_warmup_coord::TsfWarmupCoordinator;
2022

0 commit comments

Comments
 (0)