Skip to content

Development

weijianz edited this page Oct 16, 2026 · 7 revisions

中文

Code structure

A Swift package (Package.swift, swift-tools-version 6.0, macOS 14 or later). The tree below shows the main files; there are more (providers, plugins, the menu bar panel, the installer).

Sources/
  AllInOneIMECore/        pure logic, no AppKit
    Composer.swift          two-level input state machine: pinyin, Hangul and Hindi, draft, @ commands and the command list, ⌃V and ⌘V, voice gestures, action key
    Command.swift           @ command definitions and parsing
    ActionKey.swift         action key
    Config.swift            config.json
    Prompt.swift            prompts
    Converter.swift         one request: reads config and credentials, calls Bedrock, parses the stream, caches
    CandidateParser.swift   parses the lines of the model's output
    BedrockClient.swift     ConverseStream client
    EventStream.swift       AWS event-stream decoding
    SigV4.swift             request signing
    AWSSharedConfig.swift   reads ~/.aws/credentials and ~/.aws/config
    RewriteStyle.swift      rewrite styles and their examples
    JargonLibrary.swift     the jargon list
    InputSourceList.swift   adds to / removes from the user's input source list (AppleEnabledInputSources)
    LegacyData.swift        migrates data from AIPinyin's folders
    LanguagePack.swift      language packs: the catalog, the installed packs, checks; LanguagePackLibrary.swift: the signed index
    LatinEngine.swift       English input, with dead keys composed from the keyboard layout
    FuzzyPinyin.swift       fuzzy pinyin pairs, written as rime_ice.custom.yaml
    AppVersion.swift        version number
    Hangul.swift            Korean: the 2-set key table (from the ko-hangul pack) and composing syllables
    Hindi.swift             Hindi: Latin letters → Devanagari (from the hi-phonetic pack), its engine
    Language.swift          input languages, and which one a sentence is written in
    ScriptCommand.swift     script extensions: the protocol's keys, Raycast's mapped onto them, arguments, environment
    ScriptOutput.swift      what a script printed: text, summary/detail, Alfred Script Filter lists and their actions
    ExtensionStore.swift    extension folders, bundles (extension.json) and preferences, the file cache, Spotlight discovery
    ScriptScheduling.swift  status lines taking turns, the room in the background, the command list built only when it changes
    Refinement.swift        refining shown results: the request that gave them, each answer and the instruction after it
    QuestionHistory.swift   the recent @questions and their answers
    Redaction.swift         hiding sensitive values before a request, and putting them back (RedactionWords.swift: the words it knows)
    AppProfile.swift        per-app settings (appProfiles), by bundle ID
    AppleModel.swift        Apple Intelligence on this Mac (Foundation Models) as a provider
    IssueReport.swift       @reportissue: GitHub's new-issue page, with the details that help
    Update.swift            update.json and its signature, checking a download; AppBundles.swift: swapping the apps
    CandidateLayout.swift   the candidate panel's geometry: a row of candidates, which row a click is on
    Engine.swift, KeyMapping.swift
  AllInOneIMERime/        Swift wrapper for librime (Rime.swift)
  CRime/                  module map for the librime C API
  AllInOneIME/            the input method process (InputMethodKit)
    main.swift              entry point, plus --register / --disable / --status / --settings / --selftest / --install-pack / --check-update / --install-update / --apply-update
    InputController.swift   performs the effects Composer returns: text field, candidate panel, Bedrock, Terminal, clipboard, voice
    CandidatePanel.swift    candidate panel
    SettingsWindow.swift    settings window (SwiftUI)
    LanguagePacks.swift     downloading and installing language packs
    UIText.swift            interface text (Chinese / English)
    VoiceInput.swift        microphone and SpeechAnalyzer
    SecureInput.swift       secure input detection
    SelectedText.swift      reading the selected text (text input protocol, else Accessibility if already allowed)
    AppProfiles.swift       Settings → Apps, the input menu's "This App: <name>", the apps typed in lately
    PrivacySettings.swift   Settings → AI Provider → Privacy
    QuestionHistoryStore.swift  recent-questions.json
    TypoMemory.swift        the apps where a typo couldn't be taken back
    Updates.swift           checking, downloading and installing updates; Settings → About's Updates
    IssueReporting.swift    the details @reportissue and "Report an Issue…" add
    LiveExtensions.swift    script extensions: the index rebuilt in the background, status lines
    ScriptTasks.swift       script extensions' runs, background runs, notifications
    FileSearch.swift        search for @open
    TerminalLauncher.swift  @claude
    InputSourceRegistrar.swift  registers the input source, edits the input source list
    SelfTest.swift          make selftest
  AllInOneIMESettings/    "AllInOneIME Settings" launcher: opens the input method's settings window with --settings
  allinoneime-cli/        dev tool that runs @improve in Terminal
Tests/
  AllInOneIMECoreTests/   unit tests for Core; Fixtures/ holds event-stream test data
  AllInOneIMERimeTests/   tests with the real librime and ThirdParty/rime-data
Resources/                the three Info.plist files (input method, settings, installer), localized strings, icons, entitlements, rime/default.custom.yaml
Scripts/                  screenshots, icons, the disk image, language packs and their index, 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
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 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)
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, signed with a Developer ID Application certificate if the keychain has one (ad-hoc otherwise); 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), 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, two sets of tests.

  • AllInOneIMECoreTests: pure logic, with a fake pinyin engine and a stubbed network (URLProtocol); no network access, no AWS needed. Among them, AppVersionTests checks that the versions in the three Info.plist files match AppVersion.string.
  • AllInOneIMERimeTests: 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; the voice part tests on-device recognition with speech synthesized by say (skipped without SpeechAnalyzer). 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 or SELFTEST FAILED (n), and writes the screenshots to OUT.

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. 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 —
voice.png 8-voice 8-voice-english-en
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.

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 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.
  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