* 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>
117 lines
5.3 KiB
C++
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
|