Commit Graph
30 Commits
Author SHA1 Message Date
fanlusky 88a086a75b perf(build): Release 编译跳过重复 configure,只构建打包用的目标
本地打包每次都要等很久,其中编译这一段有两处纯粹的重复劳动。

Visual Studio 生成器把 ZERO_CHECK 接进了每个目标,CMakeLists.txt 变化时
构建会自己重新生成,所以已有 CMake 缓存时前置的 cmake --preset 不改变任何
结果,只是每个构建目录白等约 10 秒,三个目录就是 30 秒。改成缓存存在就跳过,
需要强制重跑(改了 preset、换了工具链)时传 -Reconfigure。

构建目标也从 ALL_BUILD 收窄到 MetasequoiaImeServer / MetasequoiaImeTsf。
原先会一并构建 MetasequoiaImeServerTests、test_webview_contract、
test_windows_ipc_contract,而 Prepare-PackageFiles.ps1 随后又把它们从包里
删掉。server 目标的依赖闭包已经覆盖 Watchdog、三个面板、Settings、
DictionaryReplay 和 OpenCC 词典,产物一个不少。要连测试一起构建(比如为了
跑 ctest)传 -Target ALL_BUILD,server/AGENTS.md 里的 ctest 段落已注明。

顺带给 cmake --build 加上 --parallel,真正有代码改动时也能并行。

无改动时的增量编译:TSF 18.0s -> 3.7s,Server 21.9s -> 10.9s。
CI 不受影响,它直接调 cmake,不经过这些脚本。
2026-09-13 03:56:40 +08:00
fanlusky a7779d2b05 refactor(engine): 将 engine 从 submodule 内嵌为仓内一等组件
`vendor/MetasequoiaImeEngine` submodule 改为 `engine/` 目录,导入的是
product-lock.json 原本钉住的提交 c810d201,逐文件核对过 blob,因此这是一次
搬迁而不是引擎升级。引擎自己的嵌套 submodule(googlepinyinime-rev、utfcpp、
miniaudio)按各自 gitlink 一并展开,来源与提交见 engine/UPSTREAM.md。

Windows 专门化裁掉了不属于本产品的部分:whisper.cpp 与本地推理 provider、
macOS 示例宿主、整个 dictionary/ 建库流水线、上游 CI 与 release-please 配置。
词库仍按 product-lock.json 在构建时取发布产物。

引擎不再进 product-lock.json:没有 gitlink 就没有可比对的东西,锁里写一个上游
commit 等于宣称这棵树仍等于上游,而第一个 Windows 专门化改动落地时这句话就假了。
verify-contracts / verify-checkout 随之移除,verify-published 保留。

CI 与工具链:引擎源码并入根 scripts/format.sh(删掉引擎自己的那份,避免两套排除
列表分叉);引擎 voice.yml 里的 silero_vad.onnx 摘要校验移到本仓 lock job;
engine-bump-triage 工作流与其测试删除;gitsubmodule 的 dependabot 频率由 daily
改为 monthly。

README、README.en、AGENTS.md、CONTRIBUTING、docs/product-release.md 及各子目录
文档同步说明新的位置与边界。
2026-09-12 03:40:48 +08:00
fanlusky 0febd575ac refactor(windows): clear the TSF /W4 warnings
The bulk of them were left behind when the keystroke latency logging was
removed: PerfTimer objects and *ElapsedMs doubles that nothing reads any more
(C4189). Dropping them also takes the QueryPerformanceCounter calls back out of
the keystroke path. The timers that still feed DebugTsfKeyLatency (Ipc.cpp,
KeyEventSink.cpp, KeyHandlerEditSession.cpp) are untouched.

The rest, all behaviour-preserving:

- C4463: the four CompositionProcessorEngine configuration flags were one-bit
  BOOL fields, so every `= TRUE` stored -1. They are plain BOOLs now.
- C4456: renamed the shadowing inner locals (pLI -> pItem, focusToken ->
  expectedFocusToken).
- C4100: commented out the parameter names that are part of a TSF interface but
  unused in the implementation.
- C4702: dropped the unreachable `return 0;` after FreeLibraryAndExitThread,
  which is __declspec(noreturn).
- C4102: removed the two vestigial NoPresenter labels and the empty branches
  that used to jump to them.
- C4267/C4244: made the narrowing conversions in the pipe write and the offline
  key handler explicit.
- C4996: SharedConfigPath now uses GetEnvironmentVariableW instead of the
  deprecated _wgetenv.

x64 and x86 Release both build clean; ctest passes in both.
2026-09-12 00:55:56 +08:00
Ouchihao 1d2431add6 fix(windows): commit highlighted candidates with numpad punctuation 2026-09-11 19:32:17 +08:00
fanlusky 580ac98d0a fix(windows): match bare-Shift release on the arming latch, not GetKeyState
Word does deliver the bare-Shift release to the key-event sink; what it
does not deliver is a fresh GetKeyState() snapshot alongside it. Hosts
that call into ITfKeystrokeMgr outside the message dispatch see the
modifier state as of the last message the thread pulled, so
Global::PureShiftKeyUp/IsShiftKeyDownOnly -- both derived from
GetKeyState() -- can still describe the previous keystroke. The release
match then silently fails even though OnTestKeyUp and OnKeyUp both ran.

Match on the released virtual key plus _shiftHotkeyArmed instead.
The latch already means "this Shift went down alone and nothing else has
been pressed since" (_TrackModifierHotkeyArming disarms on any other
key-down) and it self-clears, so TestKeyUp/KeyUp cannot double-toggle.
Arming reads GetAsyncKeyState(), which reports the real keyboard rather
than the host thread's queue. Same treatment for bare Ctrl.

This is how Weasel and WindInput handle it -- neither installs a
keyboard hook for the toggle -- so revert the hook back to mintty-only.
mintty is a genuinely different failure: it offers no bare modifier
key-up to the sink at all, so there is no event to match against.
2026-09-11 03:07:14 +08:00
fanlusky 0838681465 fix(windows): restore bare-Shift CN/EN toggle in Word
70930df stopped eating the bare-Shift release so JetBrains double-Shift
keeps working, and moved the toggle into OnTestKeyUp because TSF does not
call OnKeyUp after a FALSE test result. Word never completes a bare-Shift
release through ITfKeyEventSink at all, so neither callback runs and the
CN/EN toggle was lost there.

The mintty workaround already solves exactly this shape: a thread-local
WH_KEYBOARD hook watches the real bare-Shift press/release and feeds it
back into the deferred hotkey path, with _MarkBareShiftHandled() latching
presses the key-event sink already toggled so hosts that do deliver the
release never toggle twice. Install that hook for every host instead of
only mintty.exe, and rename its members off the mintty-specific naming.

Also log the three bare-Shift release paths under [issue47] -- key-up was
previously uninstrumented, which is why the Word diagnostic log could not
distinguish "release never arrived" from "toggle queued but dropped".
2026-09-11 01:34:00 +08:00
fanlusky 093b485124 Merge branch 'develop' into fix/issue-242-settings-cannot-save
冲突解决:

- windows/src/Utils/FanyUtils.h:develop 通过新增的 .gitattributes 把整个文件
  从 CRLF 规范化成 LF,与本分支删除 GetIMEDataDirPath() 声明的改动整文件冲突。
  取 develop 的 LF 版本,再删掉该声明。
- server/tests/src/test_config_non_ascii_path.cpp:仅 include 块冲突,取两侧并集
  (algorithm/filesystem/fstream/iterator/string/system_error)。另修正一处错误的
  自动合并——develop 新增的候选回退字体断言本属于
  config_round_trips_under_non_ascii_profile_path,git 却按上下文并进了本分支新增的
  config_migrates_legacy_acp_mangled_path,已移回原测试。
2026-09-10 19:33:43 +08:00
Evan 997fdfd9cb chore: settle line endings in .gitattributes (#310)
* chore: settle line endings in .gitattributes

Nothing declared line endings, so each file kept whatever the contributor's core.autocrlf produced when it was added: 382 text files ended up LF and 50 CRLF. The CRLF ones are the hazard. Editing one on macOS or Linux writes LF lines into it, and clang-format rejects a file that mixes both -- which is how `quality / Workflow validation` went red on KeyStateCategory.h, where three lines out of a CRLF file had arrived as LF. #309 converted those three lines back and left the other 49 files loaded.

The repository now declares LF and the working trees are renormalized to match, so the failure mode is gone rather than deferred to whichever of those files is edited next.

The four binary types are declared rather than left to git's heuristic, which decides from the first 8000 bytes alone. user_dict.dat is caught by that sample today only because the 40-byte seed is almost entirely NUL.

Verified: the 56 renormalized files are byte-identical to their old contents once CR is stripped, no text file in the index still carries CRLF, and scripts/format.sh --check passes.

* chore: skip the line-ending renormalization in blame

The commit before this one rewrote every line of 56 files without changing a character of them, which is what this file exists to hide.
2026-09-10 03:04:23 +00:00
Evan 0d72ecfc64 fix(windows): normalize key header line endings for CI (#309) 2026-09-10 10:32:21 +09:00
Evan edb2074d7c test(ipc): make fallback policy independently executable (#307) 2026-09-10 08:04:47 +09:00
Evan 96eb1bb2a4 test(ipc): lock down fallback result semantics (#306)
* fix(ipc): distinguish definite local fallback

* test(ipc): lock down fallback result semantics
2026-09-10 08:00:15 +09:00
Evan 9f9cc0e4c7 fix(ipc): distinguish definite local fallback (#305) 2026-09-10 07:58:08 +09:00
Evan e2a4f3dcfb fix(tsf): remove unused client router shim (#303) 2026-09-10 07:49:31 +09:00
Evan db166187d9 feat(tsf): convert edit session keys for client router (#301)
* feat(tsf): add client key router boundary

* feat(tsf): convert edit session keys for client router
2026-09-10 07:41:53 +09:00
Evan e45b9fb7b9 feat(tsf): add client key router boundary (#300) 2026-09-10 07:40:28 +09:00
fanlusky 28689361ad fix(ci): unblock packaging on the self-hosted runners
Two independent failures kept the v0.6.5 installer from being built after
the pipe probe was fixed.

The package job died in "Verify the release commit is on main" with git
exiting 128 on dubious ownership: runner instance 2 leaves _work owned by
SYSTEM while the job runs as `runner`. actions/checkout is unaffected, so
the first plain git call is what fails, and only jobs scheduled onto that
instance see it. Mark the workspace as safe in the three self-hosted jobs.

Neither vcpkg manifest carried a builtin-baseline, so vcpkg resolved ports
against the registry at HEAD and every build depended on a live fetch of
microsoft/vcpkg. When that fetch degraded, the ports simply vanished:

    Fetching registry information from https://github.com/microsoft/vcpkg (HEAD)...
    fmt does not exist
    utfcpp does not exist

Build TSF x64 failed this way twice and passed twice on the same runner,
which is what an unpinned registry looks like. Pin both manifests to
04a9d8e5, the commit vcpkg-lock.json already records as resolved.
2026-09-09 02:18:57 +08:00
fanlusky 58fa715921 fix(tsf): stop the candidate window flickering when the host lags
Two host-timing paths could hide or displace the candidate window between
ordinary keystrokes. Both get much more likely while the machine is busy,
which is when the flicker is reported.

TS_E_NOLAYOUT from ITfContextView::GetTextExt means "not laid out yet, ask
again", but _GetTextExt reported it as a valid measurement at INVALID_Y.
Server then parks the host off-screen for that frame and the next successful
measurement pulls it back. Chromium-based hosts answer TS_E_NOLAYOUT
whenever their renderer is behind, so under load this repeats per keystroke.
Keep the anchor the session already has instead, once it has one; composition
start still falls back to the sentinel rather than reusing a stale anchor
from another document.

TF_LC_DESTROY was treated as candidate-session teardown for every process
except a hardcoded Telegram.exe. Hosts that transiently destroy and recreate
their context view mid-composition therefore got a HideCandidate between
keystrokes, which clears the live composition on the Server side: the window
blinks and the pending candidate can no longer be committed. Gate on a live
composition instead of a process name. Commit, cancel, focus loss, and
presenter cleanup all end the composition first, so they still tear the
session down through their own paths.
2026-09-08 22:53:59 +08:00
linym 075fbb2a84 fix(server): persist settings after leftover non-ASCII config paths
Elevated setup left high-IL config.toml that medium-IL Settings never
rewrote on machines hit by the ANSI path bug. Make the data directory
writable, migrate ACP-mangled leftovers, rebuild unparseable files, and
keep remaining asset paths on wchar_t.

Fixes #242
2026-09-08 22:42:26 +08:00
Aegon8719 6ff1cff42d issue-52 加入简繁切换快捷键;增加候选词翻页快捷键组合、增加以词定字快捷键组合;
issue-55 候选词与辅助码间保留一个半角空格。
2026-09-08 14:07:48 +08:00
fanlusky de4d3acf45 fix(config): handle non-ASCII (e.g. Chinese) user profile paths
std::filesystem::path(std::string) and path::string() convert through the
system ANSI code page, so a non-ASCII (e.g. Chinese) user profile path on a
non-UTF-8 (GBK 936) system was corrupted and every config read/write failed
with "设置保存失败"; on a code page that cannot represent the characters
path::string() threw, crashing the Server at startup in a watchdog restart loop.

Never route config paths through the ANSI narrow encoding:
- build g_config_path and the legacy voice path from the wide accessors;
- read config by opening the wide path + toml::parse(text) instead of
  toml::parse_file(path.string()) (LoadImeConfig, MigrateLegacyVoiceInputConfig);
- build and open the TSF shared config path as a wide std::filesystem::path.

Add a regression test that round-trips config read/write under a Chinese
profile path.
2026-09-08 02:20:37 +08:00
AaGMix e9a2c91214 feat(installer): include release PDB symbols 2026-09-07 02:10:38 +08:00
Ouchihao f90c5c39b7 fix(windows): include Chinese in IME display names 2026-09-06 17:51:48 +08:00
Evan Hu 87e842f6a3 build(windows): TSF、Server 与设置程序开启 /W4
/W4 已经是本仓的既有约定——ui/ 的五个 target 和 server/ 的四个辅助可执行文件都在用。漏掉的恰好是三个最大的:注入到 Chrome、QQ、Office 等宿主进程里的 TSF DLL,拥有全部网络调用、WebView2 宿主与皮肤加载的 Server 主进程,以及设置程序。这三个此前都跑在默认的 /W3 上。

暂不加 /WX:存量告警要先在 Windows 上读完并清掉,在那之前把告警变成错误只会逼出一堆 /wd 抑制。/permissive- 同样留到能在 Windows 上构建验证的后续改动,本次改动无法在本机编译验证这一项。
2026-09-06 18:11:37 +09:00
Evan 42238a2af6 style: 用 .clang-format 重排 server / windows / ui / log
一次性重排,无任何行为改动。四个目录各自的 .clang-format 放在仓库里已久,但全组织只有
MSIME-Linux 在 CI 里执行 clang-format 检查,这边 149 个文件里有 149 个不符合自己声明的风格。

已逐文件核对语义等价:去掉空白、注释与相邻字符串字面量拆分之后,全部 149 个文件与重排前逐字节
相同。差异只有三类——缩进与换行、clang-format 补的 } // namespace 结尾注释、以及长字符串被拆成
相邻字面量(C++ 保证自动拼接)。

这个提交会写进 .git-blame-ignore-revs。
2026-09-06 12:13:47 +09:00
Evan 4fc16952b3 wip(windows): 补齐隐私与许可交付,修好外部人的构建入口
- 新增 PRIVACY.md:逐项写明每个联网端点、发送内容、默认开关与关闭方式,明确云候选默认开启、
  token 在 config.toml 明文存储、无任何遥测
- 设置页「用户协议」「隐私政策」两个无 handler 的死按钮接上;前者改名为「开源许可协议」指向
  LICENSE,开源产品的用户条款就是许可证本身,不再承诺一份不存在的 EULA
- 安装包随包装入 GPL 正文并在安装向导显示(GPLv3 §4/§6),补上打包测试断言
- release.yml 增加 build provenance attestation,并在发布后向 MSIME-Web 发 repository_dispatch,
  让 msime.app/update.json 不再等两小时一次的 cron
- 所有 CMakePresets 的 vcpkg 路径改用 $env{VCPKG_ROOT},server 的 Boost 回退不再指向个人目录
- ui/README.md 指向本机绝对路径的 markdown 死链改成相对链接
- 新增 check-developer-paths.sh 并接入 required 的 quality 检查,防止路径回潮
- docs/installation.md 不再写死版本号
2026-09-06 12:05:06 +09:00
Evan 2562026a2a fix: 修复审计发现的候选页竞态、管道权限、TSF 空指针与皮肤 CSS 逃逸等八项缺陷 (#169)
* fix(server): publish candidate pages as immutable snapshots

Global::candidate_ui is a plain global with no lock. The IPC worker rebuilds and destroys its vectors while the UI thread paints from them, and the handoff is an asynchronous PostMessage, so the worker returns to its loop and pops the next task without waiting. A second keystroke, or an async english.db candidate arrival, could free the page_views buffer and its strings while FillItemsFromUi held a reference into it. ShowItemContextMenu reads the same vectors from a mouse callback with no message handoff at all.

The worker now copies each finished page into an immutable snapshot and publishes it at the single point where the rebuild completes; every UI-thread reader loads that snapshot once and reads only its own copy. PostMessage is deliberately left alone: the worker also serves UI-originated tasks, so a synchronous handoff would invite a cross-thread deadlock.

* fix(server): bound the aux and diagnostic pipe reads

Both listeners serve a single pipe instance inline and polled a NOWAIT handle for as long as the process ran, breaking only on a read that succeeded or failed with something other than ERROR_NO_DATA. A client that connected and never wrote held the only instance forever, so the loop never returned to ConnectNamedPipe and no later client was accepted, permanently losing the langbar right-click channel and the terminal-deactivation teardown fallback.

Give both loops the same deadline the hello reads already use and go back to accepting when it expires.

* fix(server): snapshot the voice config and replace config.toml atomically

GetConfiguredVoiceInput handed out a reference to a global that LoadImeConfig rewrites from the IPC worker thread, clearing and refilling two std::map members and around twenty strings. The voice control thread and the low-level keyboard hook, which copies the struct on every keystroke, walked those nodes as they were freed. LoadImeConfig now parses into a local and publishes it in one step under a process-local shared_mutex, and the getter returns a snapshot by value. The file lock is deliberately not reused for readers: it is a named cross-process mutex with a five second wait, and taking it in a keyboard hook would stall typing system-wide.

WriteConfiguredValue also reopened the live config.toml with ios::trunc, so a crash mid-write destroyed the user settings and could leave the file unparseable for good. It now goes through WriteFileTextAtomically, which this file already uses elsewhere. That makes the directory entry swap atomic; it does not flush, so it is not a durability guarantee.

* fix(ipc): restrict the server pipes to the owning account

The pipe DACL granted Everyone FILE_ALL_ACCESS. That bit set includes FILE_CREATE_PIPE_INSTANCE, so any local account could add an instance to a name this server already owned and answer part of the connections in its place. The names are machine-global with no per-user component, and the TSF client opened them with no SQOS, so a squatter could also impersonate the connecting thread.

Build the descriptor from the process owner SID and admit only LocalSystem, that account, and app containers with connect-only rights so UWP text input still works; fail pipe creation outright when the SID cannot be resolved rather than falling back to something permissive. Pass FILE_FLAG_FIRST_PIPE_INSTANCE on the first instance of each name so a squatted name fails loudly, and request SECURITY_IDENTIFICATION on the client side.

Authenticating the peer process after connect is a larger change to the handshake and is not attempted here.

* fix(tsf): guard the null focus paths and unadvise the compartment sink

_CandidateChangeNotification treated ITfThreadMgr::GetFocus and ITfDocumentMgr::GetTop as if they could only fail through a FAILED HRESULT. Both return S_OK with a null out-pointer when nothing has focus or the document stack is empty, and the results were dereferenced unconditionally, so an external UILess host calling Finalize while focus moved away crashed the host application. The sibling _RequestCancelComposition in the same class already checks both.

CLangBarItemButton::CleanUp only unregistered the compartment inside the SUCCEEDED branch of a CoCreateInstance, then deleted the sink regardless. This TIP registers COM-less activation support, where that call does fail, and the sink was freed while TSF still held it advised. Unadvising never needed the thread manager, so it now runs unconditionally before the deletes.

* fix(ui): drop the cached visuals when the scene is replaced

Window caches focused, hovered and captured Visual pointers into the scene tree, and SetScene destroyed that tree without clearing them. Toggling the input mode rebuilds the floating toolbar scene, so the next mouse move dispatched a virtual call through a freed object.

Clear all three before the assignment, and release the capture only when this window actually holds it: an adopted HWND may never have called SetCapture, and capturedVisual_ is set even when it did not.

* fix(settings): scope external skin CSS through the CSS parser

injectScopedSkinCss concatenated a skin toolbar.css into an @scope block as text. Block nesting is textual, so a leading close brace in the skin ended the block early and the rest styled the whole settings window. The host validates only the skin file name, never the body, and candidatePreviewCss interpolated unvalidated colour strings into rule bodies the same way.

Create the scope rule empty and re-insert every rule parsed out of the skin as a child of it, so nothing lands outside however the braces are balanced, and accept a colour only in the plain notations we actually render.

* fix(release): gate the dictionary source commit on its default branch

verify_published iterated only data["repositories"], but the dictionary block pins a source_commit exactly like they do. The manifest check merely proves the release agrees with the lock about that commit, which a branch commit satisfies just as happily, so a dictionary built from an unmerged branch shipped with the provenance the gate exists to refuse.

Run the dictionary through the same loop under its own name, resolving its repository from the lock. A retired dictionary repository that no longer answers is reported rather than failing the release: its published artifacts are immutable history, and being unable to re-check them is not the same as finding them unmerged.
2026-09-06 10:31:48 +09:00
Evan Hu b2d736cd79 fix(build): finish local tooling migration to the monorepo 2026-09-06 09:02:13 +09:00
Evan Hu 05036a763d docs: describe one repository instead of six
The product README was under windows/ after the move, but it was never a component readme: it is the download page, the feature guide and the contribution guide for the whole product, so it goes back to the root and windows/ gets a real component readme.

AGENTS.md at the root now carries the repository map, the component boundaries that used to be cross-repository rules, and the release process, since the scripts that process drives live at the root. The component files keep what is genuinely theirs and say which paths they are relative to. References that named sibling repositories now name sibling directories, and the lock documentation records that Server, the pages, the GUI framework and the installer are no longer inputs to pin.
2026-09-06 02:40:51 +09:00
Evan Hu 373f8d1473 refactor: collapse the duplicated submodules into one vendor/ directory
Windows, Server and UiHtml each carried their own gitlink to MSIME-Engine, all three pinned to the same commit, and Server carried a gitlink to MSIME-UI that is now the ui/ component. Git only reads the root .gitmodules, so the nested ones had to go regardless.

The engine and the two third-party dependencies now live once at vendor/MetasequoiaImeEngine, vendor/opencc and vendor/cpp-pinyin. Server reaches them through MSIME_VENDOR_DIR and builds ui/ as an out-of-tree subdirectory; the Windows tip's relative contract includes gained the extra level; the ui-html contract sync and the Server env/table generators follow the engine to its new location.
2026-09-06 02:27:52 +09:00
Evan Hu 253b068984 refactor: move the Windows TSF frontend under windows/
Preparation for folding Server, UI, UiHtml, Installer, ImeLog and TsfEditControl into this repository. The Windows tip's own sources, build files and local build scripts move under windows/ so every component sits at the same level. Product-level files stay at the root: the lock, the release manifest, the shared CI scripts, the integration tests, the docs and the Engine submodule.
2026-09-06 02:24:27 +09:00