Skip to content

Development

weijianz edited this page Oct 27, 2026 · 15 revisions

中文

Code structure

The root is the macOS app; Core/ is the core every platform shares, a Swift package of its own (it builds and tests on its own, on macOS, Linux and Windows); Windows/ is the Windows package, the start of the Windows app.

Core/                       the core every platform shares, a Swift package of its own
  Sources/
    AllInOneIMECore/          Bedrock client, SigV4, prompts, the two-level input state machine (including commands
                              and the voice gesture, off for now: Features.voiceInput); no AppKit dependency
    AllInOneIMEText/          the interface text and its language (UIString, InterfaceText, InterfaceLanguage, and
                              StringCatalog, which reads the catalog where there are no .lproj bundles); all the
                              settings launcher links (AllInOneIMECore re-exports it)
    AllInOneIMEQuickJS/       QuickJS-ng (its C sources in CQuickJS/), the JavaScript of plugins and @js where there
                              is no JavaScriptCore (Windows, Linux, Android)
    AllInOneIMERime/          librime wrapper
    CRime/                    module map for the librime C API
    AllInOneIMECLI/           the command line, allinoneime-cli, run by each platform's own main
  Tests/                    the core's unit tests, AllInOneIMECoreTests (see Tests below)
  Scripts/                  the core package's own scripts
  Resources/                Localizable.xcstrings: the core's interface text
  LanguagePacks/            the Korean and Hindi language packs (ko-hangul, hi-phonetic), for make packs
  Examples/extensions/      an example of every script extension feature
  Plugins/                  @x, @threads, @bsky, @weibo and the script plugin @xe, which the plugin library publishes
Sources/                    the macOS app
  AllInOneIME/              the InputMethodKit input method, candidate panel, settings window, speech recognition
                            (voice input, off for now)
  AllInOneIMEMac/           what only the Mac has (key layouts, code signing, Spotlight, AppleScript, the updater's
                            install), and Platform.mac
  AllInOneIMESettings/      the "AllInOneIME Settings" launcher: opens the input method's settings window with --settings
  AllInOneIMEInstaller/     "Install AllInOneIME" on the disk image: installs, updates and uninstalls
  allinoneime-cli/          the Mac's main for AllInOneIMECLI (make cli builds .build/release/allinoneime-cli)
Tests/                      the macOS app's tests, AllInOneIMEMacTests, and the Rime tests, AllInOneIMERimeTests (see
                            Tests below)
Windows/                    the Windows package, the start of the Windows app
Resources/                  the three Info.plist files (input method, settings, installer), Localizable.xcstrings
                            (the app's own interface text), icons (AppIcon.png), entitlements, Signing/ (the
                            Developer ID provisioning profile), rime/default.custom.yaml
Scripts/                    screenshots, icons, the disk image, language packs and their index, importing
                            translations, wiki translation, real-app test, fixtures
ThirdParty/                 librime and rime-ice, downloaded by make deps (not in the repo)

For the input method's own development options, see ~/Library/Input Methods/AllInOneIME.app/Contents/MacOS/AllInOneIME --help.

make targets

Target What it does
make / make app Builds and packages build/AllInOneIME.app. With PROFILE=<profile> SIGN_IDENTITY=<a certificate it lists>, a build with the keychain entitlements: it can sync keys through iCloud Keychain. Without the provisioning profile (a development build), keys stay in the login keychain and can't sync
make deps Downloads and verifies librime 1.16.1 and rime-ice 2026.06.30, precompiles the dictionaries (into ThirdParty/)
make build Builds the input method only (swift build)
make settings-app Packages build/AllInOneIME Settings.app
make installer-app Packages build/Install AllInOneIME.app, the installer on the disk image
make install Packages both apps, installs them in ~/Library/Input Methods and ~/Applications, registers the input method and adds it to the input source list
make uninstall Disables it, removes it from the input source list, deletes both apps
make pack The Chinese language pack: build/packs/zh-pinyin-<version>.tar.xz, its SHA-256 and its packs.json entry
make packs Every language pack: Chinese, and Korean and Hindi from Core/LanguagePacks/ (ko-hangul, hi-phonetic)
make install-pack Installs the Chinese pack from this checkout's dictionaries into the installed input method, without a download
make status Status of the installed input method; see Installation and Troubleshooting
make test Unit tests and Rime tests (swift test), and make strings-check
make strings Adds new interface text to the String Catalog (Resources/Localizable.xcstrings), marks what's unused stale; see Interface text
make selftest Drives the input method with a simulated text field, with real Rime and real Bedrock
make screenshots Runs make selftest, then generates the screenshots in docs/ and docs/en/
make panel-images Captures the menu bar panel on screen (frosted glass, a row swiped open): docs/panel-*.png, docs/en/panel-*.png
make dmg The release installer, build/AllInOneIME-<version>.dmg (universal) and its .sha256. It needs the Developer ID Application certificate it's signed with, in the keychain, and DIST_PROFILE, the provisioning profile that grants the input method its keychain entitlements (see Release checklist); with NOTARY_PROFILE (a notarytool keychain profile) also notarized and stapled. Then make update-zip
make update-zip The in-app update, build/AllInOneIME-<version>-update.zip: the same two apps as the disk image's installer
make update-manifest site/public/update.json and update.json.sig, signed with the releases' key from the login keychain; UPDATE_SUMMARY_EN="…" UPDATE_SUMMARY_ZH="…" give its summary
make update-test The in-app updater end to end: a temporary install, a test server on 127.0.0.1, a test key (never the installed app)
make realtest Typing test through the installed input method, in a real text field of another app
make realtest-build / make realtest-run Only builds / only runs the real-app test program
make realtest-when-unlocked Runs realtest-run once, automatically, after the screen is unlocked (a temporary launchd job)
make realtest-cancel Cancels that job
make cli Builds allinoneime-cli
make icon Regenerates Resources/AppIcon.icns and the menu bar icon Resources/icon.tiff from Resources/AppIcon.png
make clean Deletes .build and build
make distclean Also deletes ThirdParty

Variables: CONFIG (default release), SIGN_IDENTITY (default Apple Development; ad-hoc signing if it isn't found), DIST_IDENTITY (make dmg's, default Developer ID Application), PROFILE (make app's provisioning profile, for the keychain entitlements), DIST_PROFILE (make dmg's provisioning profile, Resources/Signing/AllInOneIME-DeveloperID.provisionprofile; empty stops the build), NOTARY_PROFILE (empty: not notarized), OUT (the selftest's output folder, default /tmp/allinoneime-selftest), REALTEST_OUT (default ~/Library/Logs/AllInOneIME/realtest).

make, make app, make selftest and make screenshots all leave build/AllInOneIME.app behind. It has the same bundle ID as the installed copy, so macOS may launch it instead of the installed one; delete it when you're done (make install moves it away).

Tests

make test: swift test in Core/, then in the root, three sets of tests.

  • AllInOneIMECoreTests (Core/Tests/): pure logic, with a fake pinyin engine and a stubbed network (URLProtocol); no network access, no AWS needed.
  • AllInOneIMEMacTests (Tests/): the macOS app's own code. Among them, AppVersionTests checks that the versions in the three Info.plist files match AppVersion.string.
  • AllInOneIMERimeTests (Tests/): uses the real librime and ThirdParty/rime-data (make test runs make deps first).

make selftest: packages build/AllInOneIME.app, then runs AllInOneIME --selftest, which drives the real input controller with a simulated text field: real Rime, real Bedrock requests (with your config.json and AWS configuration; this costs a little), Spotlight search. It doesn't touch the clipboard or the microphone, and doesn't actually open files or Terminal; the settings window is tested on a temporary copy of the config, leaving your config.json unchanged. At the end it prints SELFTEST PASSED, SELFTEST FAILED (n), SELFTEST INCOMPLETE or SELFTEST SKIPPED (see below), and writes the screenshots to OUT.

make selftest and make realtest don't depend on your settings. The self-test uses a config of its own (Amazon Bedrock with the AWS profile and region from yours, everything else the defaults); the real-app test sets the provider to Bedrock while it runs and puts yours back. What they can't check at the moment is skipped, not failed: live Bedrock checks without credentials for that profile, anything typed while secure input is on (a password field), Apple Intelligence's checks where the on-device model isn't available, and the Electron harness's without ELECTRON_HARNESS; a locked screen ends the self-test at once. The summary counts passed, failed and skipped checks, with why each was skipped, and the exit status says how the run went: 0 (PASSED) when every check ran and passed, 1 (FAILED) when one failed, 2 when none failed but some were skipped or couldn't run (INCOMPLETE, or SKIPPED when nothing that types ran; BLOCKED: the real-app test started on a locked screen). 2 isn't a pass: make selftest and make realtest fail on it too. The self-test's --…-only subsets exit the same way, and its last line says the same for scripts: SELFTEST-RESULT {"passed":N,"failed":N,"skipped":N,"status":"pass|fail|incomplete"} (0.6.8).

make realtest: tests the installed input method, so make install first. It starts a separate app with a real NSTextView and types through the system's text input path; it switches input sources for the test and back afterwards. The screen must be unlocked; the test window holds the foreground for about a minute, so don't type meanwhile. Results are in ~/Library/Logs/AllInOneIME/realtest/output.txt, with REALTEST PASSED when it passes (INCOMPLETE, SKIPPED or BLOCKED when checks were skipped, as above). When the screen is locked, use make realtest-when-unlocked; its log is ~/Library/Logs/AllInOneIME/realtest-watch.log.

allinoneime-cli: runs @improve in Terminal with the same Core code as the input method, reading the same config.json; options override it:

make cli
.build/release/allinoneime-cli --mode improve --styles concise,jargon "this is a blocker bug your team need fix it asap"
.build/release/allinoneime-cli --output es,fr,de "running 10 minutes late, start without me"
.build/release/allinoneime-cli --bench     # built-in sentences, reports latency

Other options: --provider (bedrock, anthropic, gemini or openai), --profile, --region, --model, --jargon FILE, --raw (prints the model's raw output), --dump FILE (saves the raw event-stream response, for test data), --version.

Screenshots

make screenshots is make selftest followed by Scripts/make-readme-images.swift, once for docs/ (Chinese) and once for docs/en/ (English). The self-test renders the candidate panel and the settings window; the script puts each panel under a text line with the inline text. The English images come from their own scenes, not the Chinese ones with an English interface: English typed, @translate into Spanish, French and German, @question asked in English.

Image docs/ (Chinese) docs/en/ (English)
demo.png 2-translate-pinyin + 3-translate en-translate-draft + en-translate
improve.png 1b-sentence-pinyin + 4-final-light en-improve-draft + en-improve
english.png 7-english-light + 7b-chinese-output —
commands.png 10-palette + 11-question en-palette + en-question
open.png 12-open + 12b-open-path the same, -en
panel-dark.png 4-final-dark en-improve-dark
settings.png 6-settings 6-settings-en (English input, translating into Spanish, French, German)
dashboard.png 14-dashboard 14-dashboard-en

The menu bar panel's frosted glass can't be rendered offscreen (nothing is behind it), so make panel-images shows the real panel with sample data over a colorful window and captures it with screencapture (AllInOneIME --showcase).

The content comes from real AI answers and differs each time, so look it over before committing. Delete build/AllInOneIME.app afterwards.

Interface text

The interface comes in nine languages (from 0.6.5). Interface text is written in the code as tr(<Simplified Chinese>, <English>): Simplified Chinese and English come from the code, the other languages from the String Catalogs, Core/Resources/Localizable.xcstrings for the core's text and Resources/Localizable.xcstrings for the app's own (merged and compiled into each app by the Makefile with Xcode's xcstringstool), keyed by the English text with %@ for each interpolation.

  • After adding or changing text, run make strings (make test fails until then; the self-tests' text, in *SelfTest.swift, isn't for translators).
  • Where one English text means two things (make strings reports them, and make strings-check fails on them), give one a key of its own, a short dotted name for where or how it is used, such as key: "remove.delete" for a "Remove" that deletes; its English is the key's English value in the catalog.
  • Adding a language is adding its column to the catalog: reviewed translations, one JSON file per language ("core\t<key>" or "app\t<key>" → its translation), go in with Scripts/import-translations.py <folder>, which never touches English and refuses a translation whose placeholders aren't its key's.
  • The core's own texts take the interface language's code and look it up the same way (InterfaceText.lookup, which each app sets: its .lproj bundles on macOS; elsewhere StringCatalog reads the catalog's tables).

Conventions

  • The bundle ID and input source ID stay com.aipinyin.inputmethod.AIPinyin: macOS uses it to remember the added input method, microphone access and preferences, so changing it would make users add and authorize it again. It is also the log subsystem.
  • When you change the wording of a prompt, increment Prompt.version (@improve) or Prompt.commandVersion (@question) as the comments in Prompt.swift say; they are part of the cache key.

Release checklist

  1. Change the version in all four places together: CFBundleShortVersionString (and CFBundleVersion) in Resources/Info.plist, Resources/Settings-Info.plist and Resources/Installer-Info.plist, and AppVersion.string in Core/Sources/AllInOneIMECore/AppVersion.swift. AppVersionTests checks that they match.
  2. Update README.md and CHANGELOG.md (English) together with the Chinese README.zh-CN.md and CHANGELOG.zh-CN.md.
  3. Update the English wiki pages, then the Chinese ones from them: by hand, or by the wiki-zh workflow (Scripts/translate-wiki.py). Each Chinese page ends with a comment naming the SHA-256 of the English page it was translated from; the workflow skips a page whose comment names the current one, so after translating by hand, put the new SHA-256 there.
  4. make screenshots (and make panel-images if the panel changed), then check the images in docs/ and docs/en/: the English ones should have no Chinese in them.
  5. Delete build/AllInOneIME.app: rm -rf build/AllInOneIME.app.
  6. make dmg NOTARY_PROFILE=<profile>: the disk image notarized and stapled (from 0.6.3 on, users no longer allow it under Privacy & Security → Open Anyway), and the update archive. The input method carries the Developer ID provisioning profile (Resources/Signing/AllInOneIME-DeveloperID.provisionprofile, DIST_PROFILE), which grants its keychain entitlements. The build stops if the profile doesn't list the signing certificate, provision all devices, or have 30 days left (Scripts/profile-entitlements.sh), or if the signature lacks the keychain entitlements, and it runs the signed input method once (macOS kills one whose profile doesn't fit). An empty DIST_PROFILE stops it too: a release without the entitlements would lose sight of every key that moved into iCloud Keychain. The profile lists the two Developer ID G1 certificates only: before signing with another (G2), make a new profile in the developer portal (by hand: the App ID's signing isn't left to Xcode) that lists it, and commit it.
  7. A release v<version> (marked Latest) with both files, the changelog in English first, then Chinese, and for each change that shows, its screenshot (docs/en/ in the English half, docs/ in the Chinese half), linked at the commit so they never change.
  8. Once the release's files download: make update-manifest UPDATE_SUMMARY_EN="…" UPDATE_SUMMARY_ZH="…", commit site/public/update.json and its .sig, and deploy the site; the apps check once a day (AllInOneIME --check-update shows what they see).

Clone this wiki locally