Skip to content

Commit 8a045bb

Browse files
cuzicclaude
andcommitted
refactor(transport): suppress_physical を PhysicalKeyDisposition に分離
Decision(意味論)を不可侵にし、物理 KANJI キーを OS に届けるかの配送判断を 新設 PhysicalKeyDisposition(Allow/Suppress)に移す。kp_stage_execute は Decision を Consume に書き換えるのをやめ、配送判断を executor に渡す。 - runtime/transport.rs 新規: PhysicalKeyDisposition::plan / to_callback - execute_filter: 各 Decision アームで physical.to_callback を使う - execute_relay: PassThrough Suppress は handle_passthrough を呼ばず Consumed、 PassThroughWith は Allow のときだけ ReinjectKey を積む Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 0350fb1 commit 8a045bb

4 files changed

Lines changed: 109 additions & 61 deletions

File tree

crates/awase-windows/src/runtime/executor.rs

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use awase::types::{RawKeyEvent, VkCode};
1717

1818
use crate::hook::CallbackResult;
1919
use crate::platform::WindowsPlatform;
20+
use crate::runtime::PhysicalKeyDisposition;
2021
use crate::state::platform_state::ImeStateHub;
2122
use crate::vk::VkCodeExt;
2223
use crate::RawKeyEventExt as _;
@@ -93,11 +94,12 @@ impl DecisionExecutor {
9394
ime: &ImeStateHub,
9495
decision: Decision,
9596
raw_event: &RawKeyEvent,
97+
physical: PhysicalKeyDisposition,
9698
) -> BatchResult {
9799
self.applied_snapshot = ime.model().applied;
98100
match self.hook_mode {
99-
HookMode::Filter => self.execute_filter(platform, decision),
100-
HookMode::Relay => self.execute_relay(platform, decision, raw_event),
101+
HookMode::Filter => self.execute_filter(platform, decision, physical),
102+
HookMode::Relay => self.execute_relay(platform, decision, raw_event, physical),
101103
}
102104
}
103105

@@ -255,17 +257,18 @@ impl DecisionExecutor {
255257
&mut self,
256258
platform: &mut WindowsPlatform,
257259
decision: Decision,
260+
physical: PhysicalKeyDisposition,
258261
) -> BatchResult {
259-
let (consumed, effects) = match decision {
262+
let (callback, effects) = match decision {
260263
Decision::PassThrough => {
261264
return BatchResult {
262-
callback: CallbackResult::PassThrough,
265+
callback: physical.to_callback(false),
263266
has_pending: self.has_pending(),
264267
sync_outcomes: Vec::new(),
265268
}
266269
}
267-
Decision::PassThroughWith { effects } => (false, effects),
268-
Decision::Consume { effects } => (true, effects),
270+
Decision::PassThroughWith { effects } => (physical.to_callback(false), effects),
271+
Decision::Consume { effects } => (physical.to_callback(true), effects),
269272
};
270273

271274
let mut sync_outcomes = Vec::new();
@@ -281,11 +284,7 @@ impl DecisionExecutor {
281284
}
282285

283286
BatchResult {
284-
callback: if consumed {
285-
CallbackResult::Consumed
286-
} else {
287-
CallbackResult::PassThrough
288-
},
287+
callback,
289288
has_pending: self.has_pending(),
290289
sync_outcomes,
291290
}
@@ -306,9 +305,19 @@ impl DecisionExecutor {
306305
platform: &mut WindowsPlatform,
307306
decision: Decision,
308307
raw_event: &RawKeyEvent,
308+
physical: PhysicalKeyDisposition,
309309
) -> BatchResult {
310310
match decision {
311311
Decision::PassThrough => {
312+
// physical=Suppress(KANJI 物理キー抑止)の場合は OS に届けず Consume する。
313+
// handle_passthrough の reinject/warmup 後処理も走らせない。
314+
if physical == PhysicalKeyDisposition::Suppress {
315+
return BatchResult {
316+
has_pending: self.has_pending(),
317+
callback: CallbackResult::Consumed,
318+
sync_outcomes: Vec::new(),
319+
};
320+
}
312321
let callback = self.handle_passthrough(platform, raw_event);
313322
BatchResult {
314323
has_pending: self.has_pending(),
@@ -317,17 +326,22 @@ impl DecisionExecutor {
317326
}
318327
}
319328
Decision::PassThroughWith { mut effects } => {
320-
// flush 出力あり → Consume して flush + キー再注入を FIFO でキュー
329+
// flush 出力あり → Consume して flush + キー再注入を FIFO でキュー。
330+
// physical=Suppress(KANJI 物理キー抑止)の場合は reinject を積まない。
331+
let reinject = physical == PhysicalKeyDisposition::Allow;
321332
log::debug!(
322-
"[relay-flush] PassThroughWith: queue {} effect(s) + reinject(vk={:#04x} {})",
333+
"[relay-flush] PassThroughWith: queue {} effect(s){} (vk={:#04x} {})",
323334
effects.len(),
335+
if reinject { " + reinject" } else { " (no reinject, suppressed)" },
324336
raw_event.vk_code,
325337
match raw_event.event_type {
326338
awase::types::KeyEventType::KeyDown => "down",
327339
awase::types::KeyEventType::KeyUp => "up",
328340
},
329341
);
330-
effects.push(Effect::Input(InputEffect::ReinjectKey(*raw_event)));
342+
if reinject {
343+
effects.push(Effect::Input(InputEffect::ReinjectKey(*raw_event)));
344+
}
331345
self.queue.extend(effects);
332346
BatchResult {
333347
callback: CallbackResult::Consumed,

crates/awase-windows/src/runtime/key_pipeline.rs

Lines changed: 19 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -440,66 +440,38 @@ impl Runtime {
440440
event: &RawKeyEvent,
441441
shadow_toggled: bool,
442442
) -> CallbackResult {
443-
// 物理 IME キー(VK_KANJI / VK_F3 / VK_F4 等)の Consume 条件:
444-
// - Imm32Unavailable (Chrome/Edge): KeyDown のみ。VK_KANJI を SendInput 済みなので
445-
// 物理キーをそのまま届けると二重制御になる。
446-
// - ImmCross (LINE/Qt): KeyDown も KeyUp も Consume。set_ime_open_cross_process で
447-
// IME 制御済みのため、物理キーや IMM 注入の KeyUp をアプリに渡すと内部 IME ハンドラが
448-
// 反応して spurious VK_F3/F4 を生成し shadow_toggle が反転する
449-
// (IME ON Engine-OFF バグの根本原因)。Ctrl+無変換 と同じ「awase が完全所有」モデル。
443+
// 物理 IME キー(VK_KANJI / VK_F3 / VK_F4 等)を OS に届けるかは Decision(意味論)
444+
// とは独立した「配送機構」の判断であり、Decision を書き換えずに
445+
// PhysicalKeyDisposition で表現する。
446+
// - Imm32Unavailable (Chrome/Edge): KeyDown は shadow_toggle 発火時のみ、KeyUp は常に
447+
// Suppress。VK_KANJI を SendInput 済みなので物理キーを届けると二重制御になり、
448+
// 0xF3 KeyUp は OS が 0xF4 KeyDown を生成して shadow_toggle が反転する。
449+
// - ImmCross (LINE/Qt): Down/Up 共に Suppress。set_ime_open_cross_process で IME 制御済み。
450+
// 物理キー / IMM 注入の KeyUp をアプリに渡すと内部 IME ハンドラが spurious VK_F3/F4 を
451+
// 生成し shadow_toggle が反転する(IME ON Engine-OFF バグの根本原因)。
450452
// - TsfNative (WezTerm): TSF が KANJI を正しく処理するため物理キーを通す(従来通り)。
451453
let profile = self.platform.current_app_profile();
452-
let is_kanji_event = event.ime_relevance.shadow_action.is_some();
453-
let suppress_physical = if profile.can_use_imm32_cross_process() {
454-
// ImmCross: KANJI 関連 VK は Down/Up 共に Consume(spurious 連鎖を構造的に遮断)
455-
is_kanji_event
456-
} else {
457-
// Imm32Unavailable: KeyDown は shadow_toggle 発火時のみ Consume。
458-
// KeyUp は KANJI 関連なら常に Consume。
459-
// 理由: 0xF3 KeyUp を OS に通すと OS IME が 0xF4 KeyDown を生成し、
460-
// shadow_toggle が OFF→ON に反転する(Ctrl+無変換後の IME ON 戻り現象)。
461-
is_kanji_event
462-
&& !profile.should_pass_physical_key()
463-
&& (shadow_toggled || matches!(event.event_type, awase::types::KeyEventType::KeyUp))
464-
};
465-
let decision = if suppress_physical {
454+
let physical =
455+
crate::runtime::PhysicalKeyDisposition::plan(event, profile, shadow_toggled);
456+
if physical == crate::runtime::PhysicalKeyDisposition::Suppress {
466457
let reason = if profile.can_use_imm32_cross_process() {
467458
"imm-cross"
468459
} else {
469460
"imm32-off"
470461
};
471-
match decision {
472-
awase::engine::Decision::PassThrough => {
473-
log::debug!(
474-
"[{reason}] KANJI key consume vk={:#04x} {:?} (was PassThrough)",
475-
event.vk_code,
476-
event.event_type
477-
);
478-
awase::engine::Decision::Consume {
479-
effects: vec![].into(),
480-
}
481-
}
482-
awase::engine::Decision::PassThroughWith { effects } => {
483-
log::debug!(
484-
"[{reason}] KANJI key consume vk={:#04x} {:?} (was PassThroughWith)",
485-
event.vk_code,
486-
event.event_type
487-
);
488-
awase::engine::Decision::Consume { effects }
489-
}
490-
awase::engine::Decision::Consume { effects } => {
491-
awase::engine::Decision::Consume { effects }
492-
}
493-
}
494-
} else {
495-
decision
496-
};
462+
log::debug!(
463+
"[{reason}] KANJI key suppress vk={:#04x} {:?} (physical disposition)",
464+
event.vk_code,
465+
event.event_type
466+
);
467+
}
497468

498469
let result = self.executor.execute_from_hook(
499470
&mut self.platform,
500471
&self.platform_state.ime,
501472
decision,
502473
event,
474+
physical,
503475
);
504476
// sync path の outcome を on_ime_apply_complete(B+C+D+E)に渡す。
505477
// Filter mode では IME effects がキューへ委譲されるため通常は空。

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ mod focus_tracking;
33
mod ime_refresh;
44
mod key_pipeline;
55
pub(crate) mod message_handlers;
6+
mod transport;
7+
8+
pub(crate) use transport::PhysicalKeyDisposition;
69

710
use awase::config::ValidatedConfig;
811
use awase::engine::{Engine, EngineCommand, InputContext, SpecialKeyCombos};
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
use awase::types::{KeyEventType, RawKeyEvent};
2+
3+
use crate::focus::class_names::AppImeProfile;
4+
use crate::hook::CallbackResult;
5+
6+
/// 元の物理キーイベントを OS に届けるかどうかの配送判断。
7+
/// `Decision`(意味論)とは独立した配送機構上の判断。
8+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9+
pub(crate) enum PhysicalKeyDisposition {
10+
/// 元の物理キーイベントをそのまま OS に通す
11+
Allow,
12+
/// 元の物理キーイベントを消費(OS に届けない)
13+
Suppress,
14+
}
15+
16+
impl PhysicalKeyDisposition {
17+
/// Decision と PhysicalKeyDisposition から CallbackResult を導出する。
18+
pub(crate) const fn to_callback(self, engine_consumed: bool) -> CallbackResult {
19+
match self {
20+
Self::Suppress => CallbackResult::Consumed,
21+
Self::Allow => {
22+
if engine_consumed {
23+
CallbackResult::Consumed
24+
} else {
25+
CallbackResult::PassThrough
26+
}
27+
}
28+
}
29+
}
30+
31+
/// KANJI 関連キーの物理配送判断を計算する純粋関数。
32+
///
33+
/// - ImmCross プロファイル: Down/Up 共に Suppress(spurious 連鎖を構造的に遮断)
34+
/// - Imm32Unavailable: shadow_toggle 発火時 KeyDown と全 KeyUp を Suppress
35+
/// - TsfNative: 物理キーを通す(従来通り)
36+
pub(crate) fn plan(
37+
event: &RawKeyEvent,
38+
profile: AppImeProfile,
39+
shadow_toggled: bool,
40+
) -> Self {
41+
let is_kanji_event = event.ime_relevance.shadow_action.is_some();
42+
if !is_kanji_event {
43+
return Self::Allow;
44+
}
45+
let suppress = if profile.can_use_imm32_cross_process() {
46+
// ImmCross: KANJI 関連 VK は Down/Up 共に Suppress
47+
true
48+
} else {
49+
// Imm32Unavailable: shadow_toggle 発火時 KeyDown + 全 KeyUp を Suppress
50+
!profile.should_pass_physical_key()
51+
&& (shadow_toggled || matches!(event.event_type, KeyEventType::KeyUp))
52+
};
53+
if suppress {
54+
Self::Suppress
55+
} else {
56+
Self::Allow
57+
}
58+
}
59+
}

0 commit comments

Comments
 (0)