Rime JavaScript Plugin Development Guide
March 26, 2025 · View on GitHub
Table of Contents
- Rime JavaScript Plugin Development Guide
Introduction
The libRime-qjs plugin system allows developers to create various functional components for the Rime input method using JavaScript, such as key processors, translators, and filters. This document will guide you through developing, testing, and deploying these JavaScript plugins.
System Architecture
,---,---,---,---,---,---,---,---,---,---,---,---,---,-------,
|1/2| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 0 | + | ' | <- |
|---'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-----|
| ->| | Q | W | E | R | T | Y | U | I | O | P | ] | ^ | |
|-----',--',--',--',--',--',--',--',--',--',--',--',--'| |
| Caps | A | S | D | F | G | H | J | K | L | \ | [ | * | |
|----,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'---'----|
| | < | Z | X | C | V | B | N | M | , | . | - | |
|----'-,-',--'--,'---'---'---'---'---'---'-,-'---',--,------|
| ctrl | | alt | |altgr | | ctrl |
'------' '-----'--------------------------'------' '------'
││ ││
\ ││ ││ /
\ ││ ││ /
\ /
\ /
.
┌───────────────────────────────────────────────────────┐ ┌────────────────────────────────┐
│ │ │ │
│ │ <===== │ │
│ libRime │ │ Squirrel/Weasel/Fcitx/... │
│ │ =====> │ │
│ ┌───────────────────────────┐ │ │ │
└─────────────│ │─────────────┘ └────────────────────────────────┘
│ libRime-qjs │ ││ ││
│ │ \ ││ ││ /
└───────────────────────────┘ \ ││ ││ /
│ \ /
┌──────────────────────+─────────────────────┐ \ /
│ │ │ .
────────── ────────── ──────────
── ── ── ── ── ── .---""" """---.
│ │ │ │ │ │ : .------------------. :
│ │ │ │ │ │ | : : |
│ plugin1.js │ │ plugin2.js │ │ plugin3.js │ | | | |
│ │ │ │ │ │ | | Smart input method | |
│ │ │ │ │ │ | | understands me! | |
── ── ── ── ── ── | | | |
────────── ────────── ────────── | : : |
: '------------------' :
'---...___________...---'
/ \
'-...___________...-'
Core Components
-
libRime: The core input method engine responsible for overall scheduling and coordination. It handles basic input method functions such as text input, candidate generation, dictionary management, and provides a unified interface and runtime environment for various components.
-
libRime-qjs: A JavaScript runtime plugin that bridges libRime and JavaScript plugins. Based on the QuickJS engine, it provides the loading and execution environment for JavaScript plugins and necessary API interfaces, enabling developers to develop input method components using JavaScript.
-
Squirrel/Weasel/Fcitx/..: The front-end programs of the input method, responsible for interacting with the operating system and providing the user interface. They receive user keystrokes, pass them to libRime for processing, and display the results (such as candidates) to users. Different front-end programs are adapted for different operating system platforms.
-
plugin.js: User-developed specific plugin implementations that can include various types of input method components:
- Processor: Handles user keystroke events
- Translator: Converts input into candidates
- Filter: Filters and sorts candidates
Workflow
- User inputs text in the front-end program
- Front-end program passes the input to libRime
- libRime calls corresponding JavaScript plugins based on configuration
- libRime-qjs executes these plugins and returns results to libRime
- libRime processes and sends results back to the front-end program for display
Development Environment
Installing the Plugin
Download libRime-qjs
Download the latest libRime-qjs package from the project repository: https://github.com/HuangJian/librime-qjs/releases
Install on macOS (Squirrel)
- Backup all dynamic library files of Squirrel input method:
/Library/Input Methods/Squirrel.app/Contents/Frameworks - Copy
librime-qjs.dylibto/Library/Input Methods/Squirrel.app/Contents/Frameworks/rime-plugins - If
/Library/Input Methods/Squirrel.app/Contents/Frameworks/librime.x.xx.x.dylibversion differs from the package, replace it withlibrime.y.yy.y.dylib,librime.dylib, andlibrime.1.dylibfrom the package - Verify installation:
- Use ⌘ + Space to enable Squirrel input method
- Check logs:
cat $TMPDIR/rime.squirrel.INFO | grep qjs - If you see similar output, libRime-qjs is working:
loading plugin 'qjs' from /Library/Input Methods/Squirrel.app/Contents/Frameworks/rime-plugins/librime-qjs.dylib registering component: qjs_processor registering component: qjs_filter loaded plugin: qjs
- If issues occur, please report with detailed reproduction steps; or delete
rime-plugins/librime-qjs.dyliband restore from backup.
Install on Windows (Weasel)
-
Close Weasel:
- Right-click the Weasel icon in system tray
- Select "Exit(Q)"
-
Backup program library:
C:\Program Files\Rime\weasel-x.xx.x\rime.dll -
Install plugin:
- Copy
rime.dllfrom the package toC:\Program Files\Rime\weasel-x.xx.x, replacing the backed-uprime.dll - Note: This DLL includes components like libRime, libRime-qjs, libRime-lua, librime-octagram, librime-predict
- Copy
-
Restart Weasel:
- Right-click the Weasel icon in system tray
- Select "Deploy(E)"
-
Verify installation:
- Right-click the Weasel icon in system tray
- Select "View Log(L)"
- In the latest log file (
rime.weasel.xxxxx.INFO.xxxxxx.log), you should see:module.cc:17] [qjs] registering components from module 'qjs'. qjs_types.cc:22] [qjs] registering rime types to the quickjs engine... registry.cc:14] registering component: qjs_processor registry.cc:14] registering component: qjs_filter registry.cc:14] registering component: qjs_translator
-
If issues occur, please report with detailed reproduction steps; or delete
C:\Program Files\Rime\weasel-x.xx.x\rime.dlland restore from backup.
Install on Linux (PR Welcome)
Development Tools
- qjs: QuickJS command-line program for testing JavaScript plugins, included in the release package
- Program name is qjs for macOS and qjs.exe for Windows
- Run
./qjs --helpin command line for version info and usage instructions - Copy it to
<Rime-user-folder>/jsdirectory to run./qjs ./tests/<your-plugin>.test.jsfor plugin testing - This program has been patched to load node modules and cannot be replaced with the official QuickJS repository's program
- npm/yarn/pnpm: For dependency management
- typescript: Optional, for type hints and smart error correction
- Node.js/bun/deno: Optional, for development and testing assistance
Project Structure (Example Plugin Repository)
<Rime-user-folder> # Rime front-end program's user folder, e.g., Squirrel's default `~/Library/Rime` directory
├─── build # Rime core engine's compilation output directory, useful for checking enabled input method options and plugin information
├─── js # JavaScript plugin directory
│ ├─── qjs # QuickJS command-line program for running JavaScript plugin unit tests, copy from release package
│ ├─── package.json # Plugin package configuration file for managing dependencies
│ ├─── node_modules # Dependency libraries
│ ├─── <your-plugin>.js # JavaScript plugin source code
│ ├─── tests # Unit test directory
│ │ └─── <your-plugin>.test.js # JavaScript unit test file
│ ├─── jsconfig.json # TypeScript configuration file for loading rime type definition files, providing type hints and smart error correction
│ └─── type
│ └─── rime.d.ts # Rime type definition file for IDE type hints and smart error correction, copy from release package
├─── squirrel.custom.yaml # Squirrel personalization configuration file
├─── xxx.schema.yaml # Input method schema configuration file
├─── xxx.custom.yaml # Input method schema personalization configuration file
├─── default.yaml # Rime configuration file
└─── ...
Quick Start
Create Plugin Directory
- Create
jsdirectory in Rime user folder:cd ~/Library/Rime # macOS # or cd %APPDATA%\Rime # Windows mkdir js - Create
package.jsonfile for dependency management:{ "name": "my-rime-plugin", "version": "1.0.0", "type": "module", "private": true }
Write Plugin Main File
-
Create processor plugin file in
jsdirectory, e.g.,processor_example.js:// processor_example.js export class ExampleProcessor { process(keyEvent, env) { // Implement key processing logic console.log('Processing key event:', keyEvent.repr) if (condition1) { return 'kAccepted' // Accept key event, stop passing to next processor } if (condition2) { return 'kRejected' // Leave key event to OS, stop passing to next processor } return 'kNoop' // Don't process key event, continue passing to next processor } } -
Create translator plugin file in
jsdirectory, e.g.,translator_example.js:// translator_example.js export class ExampleTranslator { translate(input, segment, env) { // Implement translation logic console.log(`translating input: ${input}`) return [ new Candidate('example', segment.start, segment.end, 'Candidate1', 'Note1', 999), new Candidate('example', segment.start, segment.end, 'Candidate2', 'Note2', 999), new Candidate('example', segment.start, segment.end, 'Candidate3', 'Note3', 999), ] } } -
Create filter plugin file in
jsdirectory, e.g.,filter_example.js:// filter_example.js export class ExampleFilter { filter(candidates, env) { // Implement filtering logic console.log(`filtering #${candidates.length} candidates`) return candidates.filter(candidate => candidate.text.includes('example')) } }
Embrace NPM
-
libRime-qjs supports using NPM modules in plugins, but note:
- Only supports ESM format modules, not CommonJS format
- Doesn't support Node.js built-in modules (like fs, path), and hasn't enabled quickjs's
qjs:std,qjs:os, andqjs:bjsonmodules - Doesn't support modules depending on binary extensions
-
Install dependencies:
cd ~/Library/Rime/js # macOS npm install lunar-typescript # Install lunar calendar module -
Use third-party modules in plugins:
// lunar_translator.js import { Lunar } from 'lunar-typescript' export class LunarTranslator { translate(input, segment, env) { const lunar = Lunar.fromDate(new Date()) return [ new Candidate('lunar', segment.start, segment.end, lunar.getYearInGanZhi(), 'Year', 1), // Year in Chinese calendar new Candidate('lunar', segment.start, segment.end, lunar.getMonthInGanZhi(), 'Month', 2), // Month in Chinese calendar new Candidate('lunar', segment.start, segment.end, lunar.getDayInGanZhi(), 'Day', 3) // Day in Chinese calendar ] } }
Configure Input Method Schema
- Edit input method schema personalization configuration file in Rime user folder, e.g.,
rime_frost.custom.yaml:patch: 'engine/processors/@before 1': # Add custom qjs processors. "@before 1" places processor plugin before Rime default processors to ensure plugin effectiveness. qjs_processor@processor_example # Example processor, part after @ is plugin filename (without extension). Don't add - symbol when using "@before" syntax. engine/translators/+: # Add custom qjs translator - qjs_translator@translator_example # Example translator, part after @ is plugin filename (without extension). Add - symbol when using "+" syntax. - qjs_translator@lunar_translator # Example lunar calendar translator engine/filters/+: # Add custom qjs filter - qjs_filter@filter_example # Example filter, part after @ is plugin filename (without extension). Add - symbol when using "+" syntax. - Redeploy input method to apply configuration:
- macOS Squirrel: Press ⌃ + ⌥ + `, or right-click Squirrel icon in system tray and select "Deploy"
- Windows Weasel: Right-click Weasel icon in system tray and select "Deploy(E)"
API Reference
For detailed API reference, please check the JavaScript Type Definition File. This file is also included in every release package.
Basic Types
Environment
Input method environment object, provides access to input method context.
Properties and Methods
engine: Input method engine object, for accessing input method configuration and stateengine.schema: Input method schema object, contains configuration information of current schemaengine.schema.config: Configuration object, for reading input method configuration itemsengine.context: Context object, contains current input state information
Usage Example
// Access environment object in plugin
export class MyProcessor {
process(keyEvent, env) {
const option = env.engine.schema.config.getString('some_option') // Read configuration item
env.engine.commitText('text') // Commit text
env.engine.context.clear() // Clear input state
}
}
Notes
- Environment object is passed by input method engine, no need to create manually
- Each plugin has its own Environment object, modifying its configuration won't affect other plugins
- Environment objects are different in different input method sessions
Candidate
Candidate object, represents a translation candidate.
Properties
type: Candidate type, like 'simple', 'phrase', etc.start: Start position of candidate in input stringend: End position of candidate in input stringtext: Display text of candidatecomment: Comment text of candidatequality: Weight of candidate, used for sorting
Usage Example
// Create candidates in translator
export class MyTranslator {
translate(input, segment, env) {
return [
new Candidate('type', 0, input.length, 'Candidate', 'Comment', 100)
]
}
}
Notes
- Must provide all required parameters when creating Candidate object
- Higher quality value means higher position in sorting
- text and comment should have appropriate length to avoid display overflow
- Don't include newlines in text
- comment supports newline
\nand tab\tcharacters
Segment
Segment object, represents a segmentation interval of input string. For example, input shuxuguan will be segmented into multiple segments like shuxuguan, shuru, and shu.
Properties and Methods
start: Segmentation start positionend: Segmentation end positionprompt: Segmentation prompt textselectedIndex: Currently selected candidate index in candidate menu associated with segmentation. Commonly used in processor pluginscandidateSize: Number of candidates in candidate menu associated with segmentation. Commonly used in processor pluginshasTag(tag): Check if segmentation contains specified taggetCandidateAt(index): Get candidate at specified index in candidate menu associated with segmentation. Commonly used in processor plugins
Usage Example
// Use segmentation information in translator
class MyTranslator {
translate(input, segment, env) {
// Get input text of segmentation interval
const text = input.substring(segment.start, segment.end)
// Check segmentation tag
if (segment.hasTag('abc')) {
// Handle specific type of segmentation
}
}
}
Notes
- Segment object is created and managed by input method engine
- In processor plugins, if no candidate menu is expanded currently, Segment object obtained through
env.engine.context.lastSegmentis null
KeyEvent
Key event object, represents user's key input.
Properties
repr: String representation of key, like 'Return', 'space', etc. See detailed list in rime.jsshift: Whether Shift key is pressedctrl: Whether Ctrl key is pressedalt: Whether Alt key is pressedrelease: Whether it's a key release event
Usage Example
// Handle key events in processor
export class MyProcessor {
process(keyEvent, env) {
// Check specific key
if (keyEvent.repr === 'space') { // Space key
return 'kAccepted'
}
// Check key combination
if (keyEvent.ctrl && keyEvent.repr.toLowerCase() === 'b') { // Ctrl+B
return 'kAccepted'
}
return 'kNoop'
}
}
Notes
- Use KeyRepr constants defined in rime.js instead of custom strings
- Pay attention to modifier key combinations
Trie
Trie data structure, used for efficient storage and retrieval of dictionary data (key-value pairs).
Constructor
const trie = new Trie()
Methods
-
loadTextFile(path: string, entrySize?: number): Load dictionary data from text file. Slow, takes >100ms to load and parse 60k lines of English-Chinese dictionarypath: Text file pathentrySize: Number of entries in file- One entry per line, format:
<key>\t<value> - Lines starting with
#are treated as comments and ignored - Throws exception: When file doesn't exist or format is invalid
-
saveToBinaryFile(path: string): Save dictionary data to binary file for fast loading in future usepath: Target file path- Throws exception: When file can't be written
-
loadBinaryFile(path: string): Load dictionary data from binary file. Fast, takes about 10ms to load and parse 60k lines of English-Chinese dictionarypath: Binary file path- Throws exception: When file doesn't exist or format is invalid
-
find(key: string): Exact key-value pair lookupkey: Key to look up- Returns: Corresponding value if found, null otherwise
-
prefixSearch(prefix: string): Prefix searchprefix: Prefix to search- Returns: Array of key-value pairs matching prefix, each element contains
text(key) andinfo(value)
Usage Example
// Load dictionary from text file
const trie = new Trie()
trie.loadTextFile('/path/to/dict.txt', 60000) // Load 60k-line dictionary
// Lookup and search
const value = trie.find('hello') // Exact lookup
const matches = trie.prefixSearch('he') // Prefix search
// Save to binary format (for faster future loading)
trie.saveToBinaryFile('/path/to/dict.bin')
Notes
- Convert text dictionary to binary format first to significantly improve loading speed
- Ensure text dictionary format is correct and entry count matches
entrySizeparameter - Exception handling: Use try-catch to catch possible exceptions when calling methods
Plugin Lifecycle
Plugin lifecycle is divided into four phases, each with specific tasks and considerations:
1. Loading Phase
In this phase, the libRime-qjs engine will:
- Read plugin JavaScript source file
- Perform syntax check and pre-compilation
- Verify if plugin exported interfaces comply with specifications
Notes:
- Ensure plugin file path is correct and has read permission
- Avoid time-consuming operations in global scope
- Handle file encoding properly (UTF-8 recommended)
2. Initialization Phase: Call Plugin Constructor constructor()
This phase mainly completes:
- Create plugin class instance
- Load configuration information
- Initialize resources needed by plugin
Notes:
- Handle missing configuration cases properly
- Avoid time-consuming initialization operations
- Handle initialization failures properly
3. Running Phase: Call Plugin Main Methods process(), translate(), filter()
Running phase is plugin's main working phase:
- Respond to input method engine calls
- Handle user input
- Manage plugin state and context
Notes:
- Maintain good performance, avoid time-consuming operations
- For intensive string splitting operations, prefer
string.indexOf()andstring.substring()over regular expressions.
- For intensive string splitting operations, prefer
- Handle exceptions properly
- Use logging reasonably for debugging
filter()function usually handles thousands of candidates, avoidconsole.log()for each object to prevent log flooding and difficulty in locating issues.
4. Unloading Phase: Call Plugin Unload Function finalizer()
This phase handles cleanup:
- Release occupied resources
- Save data that needs persistence
- Clean up temporary files created by plugin
Notes:
- Ensure all resources are properly released
- Handle exceptions when saving user data
- Avoid time-consuming operations in this phase
Example References
processor Key Processors
- select_character.js - Select character from word
- pairs.js - Symbol pairing: Auto-complete paired symbols and move cursor inside. Cursor movement currently only supports macOS platform.
- shortcut.js - Shortcut commands: /deploy, /screenshot, ... Deployed as processor to execute commands
- slash.js - Consecutive slashes: Toggle between candidates
translator Translators
- lunar_translator.js - Lunar calendar, depends on node module
lunar-typescript - calculator.js - Calculator, implements high-precision arithmetic based on BigInt, supports
Mathlibrary functions - date_translator.js - Time, date, weekday
- custom_phrase.js - Custom phrases custom_phrase.txt
- unicode_translator.js - Unicode, input U62fc to get '拼'
- number_translator.js - Numbers, uppercase amount
- help_menu.js - Help menu, triggered by /help
- shortcut.js - Shortcut commands: /deploy, /screenshot, ... Deployed as translator to provide candidates
filter Filters
- en2cn - Simple English word definitions. Memory usage ~12MB.
- cn2en_pinyin - Chinese to English translation with pinyin. Memory usage ~16MB.
- sort_by_pinyin - Reorder candidates in place based on pinyin matching degree with input code. Eliminates fuzzy pronunciation effects.
- charset_filter - Filter out candidates containing CJK extended characters to avoid invisible characters in candidate box.
- is_in_user_dict - Add * to user dictionary words and ∞ to long sentences
- pin_cand_filter - Pin candidates to top (Order requirement: Pinned candidates > Emoji > Traditional/Simplified switch)
- long_word_filter - Long word priority (Order requirement: Long word priority > Emoji)
- reduce_english_filter - Lower the position of certain English words in candidates
- autocap_filter - Auto-capitalize English. Place at the end to handle English candidates added by en2cn.
Debugging and Testing
Logging Output
- Use
console.log()for debug output - Use
console.error()for exception logging - macOS Squirrel logs output to
$TMPDIR/rime.squirrel.INFOand$TMPDIR/rime.squirrel.ERROR - Windows Weasel logs output to
%AppData%\Local\Temp\rime.weasel\rime.weasel.xxxxx.INFO.xxxxxx.logand%AppData%\Local\Temp\rime.weasel\rime.weasel.xxxxx.ERROR.xxxxxx.log - JavaScript plugin logs are prefixed with
$qjs$for easy identification
Unit Testing
Installation and Configuration
- Copy
qjscommand-line program from release package to<Rime-user-folder>/jsdirectory - Create test file in
testsdirectory, e.g.,<your-plugin>.test.js - Run test:
./qjs ./tests/<your-plugin>.test.js
Test File Organization
// tests/example.test.js
import { ExampleProcessor } from '../example.js'
const processor = new ExampleProcessor()
const env = { /* mock environment object */ }
// Test case 1: Process space key
const spaceKey = { repr: 'space', shift: false, ctrl: false, alt: false, release: false }
const result1 = processor.process(spaceKey, env)
console.log('Test case 1:', result1 === 'kAccepted' ? 'PASS' : 'FAIL')
// Test case 2: Process Ctrl+B
const ctrlB = { repr: 'b', shift: false, ctrl: true, alt: false, release: false }
const result2 = processor.process(ctrlB, env)
console.log('Test case 2:', result2 === 'kAccepted' ? 'PASS' : 'FAIL')
Writing Test Cases
- Test each component type (processor/translator/filter) separately
- Mock necessary environment objects and input data
- Test both normal and edge cases
- Add descriptive test names and error messages
Running Tests
cd ~/Library/Rime/js # macOS
./qjs ./tests/<your-plugin>.test.js
Using Other JavaScript Engines
- Besides QuickJS, you can use other JavaScript engines like Node.js/Bun/Deno to run unit tests, helping debug issues and verify ECMAScript feature support.
- Most Node.js-specific modules can be used in Bun and Deno without additional adaptation, but none are supported in QuickJS.
- The
node:fsmodule can be replaced with QuickJS'sstdmodule. For usage examples, please refer to en2cn.benchmark.js.
Type Definition Files
Installation
- Copy
rime.d.tsfrom release package to<Rime-user-folder>/js/typedirectory - Create
jsconfig.jsoninjsdirectory:
{
"compilerOptions": {
"target": "es2022",
"module": "es2022",
"moduleResolution": "node",
"checkJs": true,
"allowJs": true,
"noEmit": true,
"typeRoots": ["./type"],
"types": ["rime"]
},
"include": ["*.js", "tests/**/*.js"],
"exclude": ["node_modules"]
}
Configuration
The type definition file provides TypeScript type information for:
- Basic types (Environment, Candidate, Segment, KeyEvent)
- Plugin interfaces (Processor, Translator, Filter)
- Utility classes (Trie)
IDE Integration
- Visual Studio Code: Automatically recognizes type definitions
- Other IDEs: Configure TypeScript support according to IDE documentation
Common Issues
-
Type definitions not working
- Check if
rime.d.tsis in the correct location - Verify
jsconfig.jsonconfiguration - Ensure IDE TypeScript support is enabled
- Check if
-
Incorrect type hints
- Update to latest
rime.d.tsfrom release package - Check if using correct import syntax
- Verify plugin class implements correct interface
- Update to latest