* 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.
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.
The engine pin moved 55839c8 -> 127019f in the previous commit, and that range contains 86c5fa7 "fix(quanpin): enumerate alternative segmentations for four-syllable input". QuanpinDictionaryOnlyTriesMultipleSegmentationsForAtMostThreeSyllables asserted the behaviour that fix deliberately changed, so it failed here and, through the product lock, in MSIME-Windows' locked-Server integration job as well.
The engine is the authoritative layer: quanpin/quanpin_dictionary.cpp declares kMaxSyllablesForMultipleSegmentations = 4 and gates enumeration on segments.size() <= that. Four syllables now enumerate alternatives; five still do not. The test says exactly that, with a tbl_6_x fixture row so the five-syllable case would have something to find if the bound ever moved silently.
The engine absorbed the six local mode queries during the cross-platform work,
and MSIME-Linux has been driving them in production since. This repository kept
its own implementations of the same six features, so date-time, emoji, kaomoji,
jianpin, quick-phrase and unicode each existed twice and were free to drift.
Deletes this repository's copies and points the eight call sites in
event_listener.cpp plus the two async workers at metasequoia::local_modes.
The shuangpin profile is the part to look at. The old modules resolved the
*configured* scheme themselves:
GetShuangpinProfile(shuangpin_schema.empty() ? GetConfiguredShuangpinSchema()
: shuangpin_schema)
The engine takes a resolved ShuangpinProfile and defaults it to Xiaohe. Taking
that default would have decoded J mode, emoji and kaomoji as Xiaohe for every
user on Ziranma, Shoudao or Microsoft shuangpin, so every call site passes the
configured profile explicitly through a ConfiguredShuangpinProfile() helper.
The database path needs the same care in the other direction. The old modules
took a UTF-8 std::string and handed it straight to sqlite3_open_v2; the engine
takes a std::filesystem::path and converts back with path_to_utf8 before
opening. The two async workers therefore bridge with path_from_utf8 rather than
constructing a path from the narrow string, which on Windows would interpret a
non-ASCII data directory in the active code page.
The four existing test files are kept and re-pointed at the engine functions
with every expectation byte-identical, including the seventeen date-time format
strings and the four shuangpin schemes jianpin covers. They are the equivalence
check: if the engine's port diverged from what this repository shipped, those
assertions fail rather than the difference reaching users.
* chore: consume the engine as a library and stop prepare_env clobbering the build
The engine submodule was pinned at a197746, eight commits behind the engine's
main branch and older than the commit that first gave the engine a CMakeLists
of its own. That is why this repository listed the engine's translation units
by hand: at the pinned commit there was no target to link against. There is
one now, and Apple and Linux already consume it, so do the same here.
- advance the engine submodule from a197746 to 8cc306c
- add_subdirectory the engine and link MetasequoiaIme::Engine from the server,
the dictionary replay tool and the settings executable, dropping the two
hand-maintained source lists
- point the engine and msimeui submodule URLs at MSIME-Engine and MSIME-UI,
and update the README, which still used the pre-rename repository names
prepare_env.py, which the README tells every new contributor to run first,
overwrote four tracked build files from templates under scripts/config_files
that had fallen years behind:
- CMakeLists.txt was replaced by a 147-line copy of what is now a 400-line
build, dropping opencc, cpp-pinyin, msimeui, settings and the panels
- tests/CMakeLists.txt was replaced by an unrelated generic WinCppTemplate
- CMakePresets.json lost the vcpkg-release preset that llaunch-release.ps1
needs
Running the documented first build step therefore broke the build. The script
now only writes the two files that are genuinely generated, and rewrites
CMakePresets.json in place instead of copying a stale template over it.
The .clangd generation addressed template lines by index and the indices had
drifted: the Boost path was written over the WebView2 include and the Sciter
path over a comment, leaving clangd unable to resolve the WebView2 headers.
The template now carries named placeholders, and the script fails loudly if
any are left unsubstituted.
Also remove src/sciter. It is a third candidate window backend that no
CMakeLists has referenced for some time; the shipped backends are d2d and
webview2, and those are the only two ui_backend accepts.
* docs: keep the historical checkout directory names in the build steps
The clone URLs had to change, since the old ones only resolved through
GitHub's rename redirect, but cloning without an explicit target would also
have renamed the working directories. MSIME-Windows's AGENTS.md records that
the historical directory names are deliberate: msime-installer's
Prepare-PackageFiles.ps1 expects siblings called MetasequoiaImeServer and
MetasequoiaImeDict, and the release workflow checks the current repositories
out into exactly those paths. Clone into them explicitly instead.
* chore: re-pin the engine at the WIN32_LEAN_AND_MEAN fix
Moves the submodule from 8cc306c to f91cd20. That carries metasequoiaime/MSIME-Engine#14,
which stops the engine exporting WIN32_LEAN_AND_MEAN as a PUBLIC compile definition and so
unblocks MetasequoiaImeSettings, whose WebView2.h include failed with 101 instances of C2146
against the trimmed Windows.h the definition forced on every consumer.
The range also picks up the two dependabot merges the engine took in the meantime: #12 bumps
utfcpp and #13 bumps the pinned action SHAs.
* build(tests): link the engine target instead of listing its sources
The test executable hand-listed the same 41 engine translation units the root
CMakeLists.txt did, and compiled them without NOMINMAX. That went unnoticed
while the pinned engine predated core/key_event.h; now that the engine headers
pull in Windows.h, the min and max macros collide with std::max and the build
fails with C2589 in quanpin_query.cpp.
Linking MetasequoiaIme::Engine fixes it at the source: NOMINMAX is one of the
definitions the engine exports PUBLIC precisely because it changes the declared
API surface.
* docs(agents): correct the tests wiring and describe the new prepare_env
Two sections went stale or were inaccurate.
The tests section said the root CMakeLists.txt does not reference tests/ at
all. It does: include(CTest) at :8 defaults BUILD_TESTING to ON and :411
add_subdirectory(tests) picks it up, so the test target is configured and
compiled on every CI run. This PR proved it from the other side, by breaking
the test target's build with a NOMINMAX regression that CI caught. The real
gap is narrower and still worth the warning: ci.yml has no ctest step, so the
executable is built and never run.
The generated-files section described prepare_env.py copying templates over
CMakeLists.txt and CMakePresets.json by fixed line index. This PR removes that
behaviour, so the section now describes what the script actually does.
Drive the existing candidate HWND through CandidatePresenter and msimeui,
keep WebView2 for the menu, floating toolbar, and settings, and load
external skins from skin.toml color tokens. Restore cloud/AI badges on
D2D items, and keep click, pin, fix-position, and delete on the same
window messages as before.