Files
6072ae7784 fix(windows): 修复临时日语 R 模式无法输入长音符 (#1000)
* fix(windows): 修复临时日语 R 模式无法输入长音符

* fix(server,windows): 临时日语策略每键只算一次,并去掉多余的帧

- Server 在 HandleImeKey 开头按本次按键所属客户端算一次策略,不再每个谓词都查活动客户端、加锁、构造 payload 再解析回来。
- 普通激活只补发 TemporaryJapanesePolicyChanged;整套输入模式状态(含语言栏刷新)仍只在 FocusRestored 时发送。
- 配置重载时输入方案变了由 ApplyConfiguredInputScheme 统一广播,不再重复广播策略。
- TSF 的 WaitForProtocolReady 不再清零策略:worker 管道先于 Main 注册,可能已收到本次连接的策略;断开时已有清零路径。

---------

Co-authored-by: ranly <ran618ly210805@qq.com>
Co-authored-by: fanlusky <1730976608@qq.com>
2026-10-16 22:30:28 +08:00

117 lines
5.3 KiB
C++

#pragma once
#include "windows_ipc.h"
// Main ClientHello reuses fields that legacy clients left zero/unused. No
// packet size, existing opcode or reverse-pipe handshake layout changes.
namespace FanyImeProtocol
{
constexpr std::uint32_t Magic = 0x4D534950; // MSIP
constexpr std::uint16_t Major = 1;
constexpr std::uint16_t Minor = 0;
constexpr std::uint32_t RequestIds = 1u << 0;
constexpr std::uint32_t FocusEpochs = 1u << 1;
constexpr std::uint32_t FramedVoice = 1u << 2;
// Optional: Ctrl+Shift+F KeyEvent toggles the configured character set without
// changing composition. A consumer must opt in only after implementing it.
constexpr std::uint32_t CharacterSetShortcut = 1u << 3;
// Optional: the Server answers the Backspace that would empty the remaining
// pinyin of an in-progress word with a CompositionRestored reply instead of
// discarding the whole composition. A client must opt in only after it can
// rebuild its keystroke buffer from that payload.
constexpr std::uint32_t CompositionRestore = 1u << 4;
// Optional: the Server renders IMESwitch / PuncSwitch / DoubleSingleByteSwitch
// as transient caret badges anchored at point[] and understands HideCaretState.
// These events are user-shortcut notifications only; authoritative mode state
// still travels in StatusSnapshot. A client must not send them unless the
// Server acknowledged this bit.
constexpr std::uint32_t CaretStateIndicator = 1u << 5;
// Optional: a matched language-switch hotkey commits raw composition, then applies
// the destination carried by a TrilingualCycle reply. Older clients retain binary switching.
constexpr std::uint32_t TrilingualCycle = 1u << 6;
constexpr std::uint32_t InputStateShortcuts = 1u << 7;
constexpr std::uint32_t TemporaryJapaneseLongVowel = 1u << 8;
constexpr std::uint32_t Capabilities = RequestIds | FocusEpochs | FramedVoice;
constexpr std::uint32_t RequiredCapabilities = RequestIds | FocusEpochs;
struct Negotiation
{
bool accepted = false;
bool legacy = false;
std::uint32_t capabilities = 0;
};
inline FanyImeNamedpipeData Hello(std::uint64_t client, std::uint64_t request,
std::uint32_t capabilities = Capabilities)
{
FanyImeNamedpipeData hello{};
hello.event_type = FanyImePipeEventType::ClientHello;
hello.client_id = client;
hello.request_id = request;
hello.keycode = Magic;
hello.wch = Major;
hello.point[0] = Minor;
hello.point[1] = static_cast<int>(RequiredCapabilities);
hello.modifiers_down = capabilities;
return hello;
}
inline Negotiation Negotiate(const FanyImeNamedpipeData &hello, std::uint32_t capabilities = Capabilities)
{
if (hello.event_type != FanyImePipeEventType::ClientHello || hello.client_id == 0)
return {};
// Existing installed DLLs use this exact unversioned hello shape. Retain
// their established protocol during an upgrade without sending a new ACK.
if (hello.keycode == 0 && hello.wch == 0 && hello.request_id == 0 && hello.modifiers_down == 0)
return {true, true, capabilities};
if (hello.keycode != Magic || hello.wch != Major || hello.request_id == 0 ||
hello.request_id == FANY_IME_NO_REQUEST_ID || hello.point[0] < 0 || hello.point[1] < 0)
return {};
const auto required = static_cast<std::uint32_t>(hello.point[1]);
const auto common = hello.modifiers_down & capabilities;
if ((required & common) != required || (common & RequiredCapabilities) != RequiredCapabilities)
return {};
return {true, false, common};
}
inline FanyImeNamedpipeDataToTsf Reply(const FanyImeNamedpipeData &hello, Negotiation result)
{
FanyImeNamedpipeDataToTsf reply{};
reply.msg_type = result.accepted ? FanyImeReplyType::ProtocolReady : FanyImeReplyType::ProtocolMismatch;
reply.request_id = hello.request_id;
reply.candidate_string[0] = Major;
reply.candidate_string[1] = Minor;
reply.candidate_string[2] = static_cast<FanyImeWireChar>(result.capabilities & 0xFFFFu);
reply.candidate_string[3] = static_cast<FanyImeWireChar>(result.capabilities >> 16);
reply.candidate_string[4] = static_cast<FanyImeWireChar>(Magic & 0xFFFFu);
reply.candidate_string[5] = static_cast<FanyImeWireChar>(Magic >> 16);
return reply;
}
inline bool IsNegotiationReply(std::uint32_t type)
{
return type == FanyImeReplyType::ProtocolReady || type == FanyImeReplyType::ProtocolMismatch;
}
inline std::uint32_t ReplyCapabilities(const FanyImeNamedpipeDataToTsf &reply)
{
return static_cast<std::uint32_t>(reply.candidate_string[2]) |
(static_cast<std::uint32_t>(reply.candidate_string[3]) << 16);
}
constexpr bool IsCharacterSetShortcut(std::uint32_t keycode, std::uint32_t modifiers)
{
return keycode == 'F' && (modifiers & 7u) == 3u;
}
inline bool AcceptReply(const FanyImeNamedpipeDataToTsf &reply, std::uint64_t expected_request)
{
const auto capabilities = ReplyCapabilities(reply);
const auto magic = static_cast<std::uint32_t>(reply.candidate_string[4]) |
(static_cast<std::uint32_t>(reply.candidate_string[5]) << 16);
return expected_request != 0 && reply.request_id == expected_request &&
reply.msg_type == FanyImeReplyType::ProtocolReady && magic == Magic && reply.candidate_string[0] == Major &&
(capabilities & RequiredCapabilities) == RequiredCapabilities;
}
} // namespace FanyImeProtocol