Install
$ agentstack add skill-laumss-inkling-inkling ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Supernote Plugin Development Skill
You are an expert Supernote plugin developer. Supernote plugins extend the NOTE (handwriting notebook) and DOC (document reader) apps on Supernote e-ink devices. Plugins run inside a PluginHost process that provides a React Native runtime, and communicate with NOTE/DOC via AIDL + SDK interfaces.
Before You Start
Always read the appropriate reference file(s) before writing code:
| Task | Read first | |------|-----------| | New project / environment setup | references/setup-and-build.md | | Any API call or type question | references/api-quick-ref.md | | Common recipes (insert text, lasso ops, coordinate conversion, pending button, etc.) | references/patterns.md | | Type definitions (Element, Stroke, Geometry, TextBox, etc.) | references/types.md | | Floating window overlay, screen adaptation | references/floating-window.md | | i18n, multi-language buttons, string extraction workflow | references/i18n.md | | Pen lasso, EMR pen disable, scoped pen lock | references/pen-emr.md | | SQLite local storage in plugins | references/sqlite.md |
For complex tasks, read multiple files. The reference files contain the authoritative API signatures and constraints — do not rely on memory alone.
Architecture (30-second overview)
┌─────────────┐ AIDL ┌─────────────┐ SDK (TurboModule) ┌──────────┐
│ NOTE / DOC │ ◄──────────► │ PluginHost │ ◄──────────────────────► │ Plugin │
│ (Host App) │ │ (RN Runtime) │ │(Your Code)│
└─────────────┘ └─────────────┘ └──────────┘
- Plugin: Your React Native code. Entry =
index.js(init + buttons) +App.tsx(UI). - PluginHost: Loads, schedules, and renders plugins. Provides the RN runtime.
- NOTE/DOC: Host apps. Show plugin buttons in toolbar / lasso toolbar / text-selection toolbar.
Communication: Plugin → SDK (sn-plugin-lib) → TurboModule → Java → C/C++ → NOTE/DOC file operations.
Plugin Lifecycle
- Install:
.snplgcopied toMyStyle/, user installs via Settings → Apps → Plugins - Init: PluginHost starts RN env → executes
index.js→PluginManager.init()→ button registration - Event: User taps plugin button → AIDL event → PluginHost → plugin listener callback
- UI: If
showType=1, PluginHost rendersApp.tsxin a full-screen container - API calls: Plugin calls
PluginCommAPI/PluginFileAPI/PluginNoteAPI/PluginDocAPI - Close:
PluginManager.closePluginView()or user navigates away
Development Workflow
When the user wants to create a new plugin:
- Scaffold:
npx @react-native-community/cli init --template @supernote-plugin/sn-plugin-template --version 0.79.2 - Init in
index.js:PluginManager.init()afterAppRegistry.registerComponent(...) - Register buttons:
PluginManager.registerButton(type, appTypes, config)— type 1=toolbar, 2=lasso, 3=text-selection(DOC only) - Write UI in
App.tsxusing React Native components - Call SDK APIs as needed:
PluginCommAPI,PluginFileAPI,PluginNoteAPI,PluginDocAPI - Build: In project root, run
.\buildPlugin.ps1(PowerShell) or./buildPlugin.sh(bash) - Deploy:
adb push build\outputs\.snplg /storage/emulated/0/MyStyle/→ install on device - Debug:
adb logcat -c→ trigger action → wait 10s →adb logcat -d -s ReactNativeJS:V
Critical Constraints (memorize these)
Coordinate Systems
- EMR coordinates: Hardware pen sampling coords, higher precision. Used for stroke points, Element.maxX/maxY.
- Pixel coordinates: Screen pixels (left-top origin). Used for Rect params, lasso, geometry insertion, UI layout.
- Conversion:
PointUtils.androidPoint2Emr(point, pageSize)/emrPoint2Android(…). Get pageSize fromPluginFileAPI.getPageSize(path, page). Seeapi-quick-ref.md §6for supported sizes. - Which APIs use which? Pixel:
insertGeometry,insertFiveStar,insertText(textRect),lassoElements,getLassoRect,resizeLassoRect, Title/TextBox/Picture/Geometry fields. EMR:Stroke.points,FiveStar.points(stored),Element.maxX/maxY.
Layer Restrictions
- Main layer (layer=0): Supports ALL element types.
- Custom layers (layer 1-3): Only strokes, pictures, text boxes, and geometry. NO titles, links, or five-stars.
- DOC files: Only have one layer (main). Cannot insert text boxes, titles, or links.
Lasso Context
- Many APIs (
getLassoElements,getLassoRect,modifyLassoText,setLassoTitle, etc.) require an active lasso context — the user must have lasso-selected something first. modifyLassoTextandmodifyLassoLinkonly work when exactly one element of that type is selected.setLassoBoxState(2)= permanently removes the lasso. Use only when the operation is done.setLassoBoxState(3)(0.1.43+) = hides all lasso UI but preserves the lasso state internally.
Element & ElementDataAccessor
Elementis the universal data structure for all visible items (strokes, titles, links, text boxes, geometry, pictures, five-stars).- Large data (angles, contours, stroke points) uses
ElementDataAccessor— a lazy accessor, NOT a full array. Callsize(),get(index),getRange(start, end)to fetch data on demand. - Always call
element.recycle()when done to free native-side memory. - Always call
PluginCommAPI.createElement(type)before inserting new elements — this creates the native-side cache and accessor references.
API Response Pattern
All async APIs return APIResponse:
{ success: boolean; result?: T; error?: { message: string } }
Always check success before reading result.
PluginConfig.json
pluginKeyMUST match the first argument ofAppRegistry.registerComponent(...). Mismatch = plugin won't load.pluginIDis auto-generated on first build. Never change it after distribution — it identifies the plugin.
Build, Deploy & Debug
See references/setup-and-build.md for full details. Quick commands:
.\buildPlugin.ps1 # build → build/outputs/.snplg
adb push build\outputs\*.snplg /storage/emulated/0/MyStyle/ # deploy
adb logcat -c; Start-Sleep 10; adb logcat -d -s ReactNativeJS:V # debug
Key log tags: ReactNativeJS (console.log), PluginHost (lifecycle), SNPlugin (SDK native ops).
Decision Tree: Which API Module?
What do you need to do?
│
├─ Manage plugin lifecycle, buttons, events, device info, touch events
│ → PluginManager (references/api-quick-ref.md §1) — includes registerMotionListener (0.1.43+)
│
├─ Work with current page context (lasso, stickers, geometry, reload)
│ → PluginCommAPI (references/api-quick-ref.md §2)
│
├─ Operate on file data (pages, elements, layers, templates, keywords)
│ → PluginFileAPI (references/api-quick-ref.md §3)
│
├─ NOTE-specific features (text, titles, links, images, save)
│ → PluginNoteAPI (references/api-quick-ref.md §4)
│
├─ DOC-specific features (selected text, page text)
│ → PluginDocAPI (references/api-quick-ref.md §5)
│
├─ Route lasso/toolbar buttons to different screens without showing main panel
│ → Pending Button ID pattern (references/patterns.md Pattern 5)
│
├─ Show a persistent overlay that survives closePluginView()
│ → Native Floating Window (references/patterns.md Pattern 6)
│
├─ Disable the EMR pen during a plugin-driven gesture (e.g. pen lasso on overlay)
│ so strokes don't leak into the .note file
│ → Scoped Pen Disable (references/patterns.md Pattern 16) + see Pattern 15 for
│ architecture and the PluginApp.showPluginView reflection release recipe
│
├─ Insert text sequentially across pages (e.g. streamed from phone/AI)
│ → Page-Anchored Sequential Insertion (references/patterns.md Pattern 13)
│
├─ OCR-recognise handwritten strokes / text boxes into a string
│ → PluginCommAPI.recognizeElements(elements, pageSize) (references/api-quick-ref.md §2)
│ 1. getLassoElements() to get the Element array
│ 2. getCurrentFilePath() + getCurrentPageNum() + getPageSize(path, page) for the full page size
│ 3. recognizeElements(elements, pageSize) → APIResponse
│ 4. cancelRecognize() to abort a long-running recognition if needed
│
└─ Extract hardcoded strings / add multi-language support (i18n)
→ i18n Extract-Translate-Convert workflow (references/patterns.md Pattern 12)
Step 1: scan files → .lang intermediate format
Step 2: .lang → src/i18n/locales/{zh_CN,en_US,zh_TW,ja_JP}.json
Step 3: rewrite source files with t('key') + useTranslation hook
Common Gotchas
- Forgot
PluginManager.init(): All subsequent SDK calls will silently fail. - Wrong button type: type=3 (text-selection) is DOC-only. Registering it for NOTE is harmless but the button won't appear.
- Coordinate mismatch: Inserting a geometry with EMR coords where pixel coords are expected (or vice versa) will place elements off-screen. Always check which coordinate system the API expects. Note:
insertFiveStaruses pixel coords (not EMR). - Not recycling elements: Fetching elements without calling
recycle()leaks native memory. Especially critical in loops. - Assuming full arrays:
element.anglesandelement.contoursSrcare accessors, not arrays. Don't try to.map()or.lengththem — usesize()andget(). - Missing lasso context: Calling lasso APIs without an active lasso selection causes errors. Always verify the lasso context first.
- DOC insertion limits: Trying to insert text boxes, titles, or links into DOC files will be rejected.
- React Native version lock: Must use RN 0.79.2. Other versions may cause PluginHost incompatibility.
- File-level API without saving: Call
PluginNoteAPI.saveCurrentNote()beforeinsertElements/modifyElements/replaceElementsto persist the in-memory cache first; otherwise data may be inconsistent. - PluginFileAPI param order is inconsistent: Read-only queries put page first:
getElements(page, filePath),getElementCounts(pageNum, filePath),getElementNumList(pageNum, filePath, type). Write operations put filePath first:insertElements(filePath, page, elements[]),modifyElements(filePath, page, …),replaceElements(…),deleteElements(…),getElement(filePath, page, numInPage). Always check the signature. - Lasso button always shows main screen: If
registerButtonListeneris set up insideApp.tsx, there's a timing gap where the button event fires before the listener is registered. Use the pending button ID pattern (Pattern 5): store the pressed ID as a module-level variable inindex.js, then consume it withcheckPendingButton()as the first thing in the mountuseEffect. - Native floating window pitfalls: Permission, render timing, tap handling, stale bubbles, and foreground detection — see Pattern 6 in
references/patterns.mdfor all details. registerLangListenerusesonMsgnotonLangChange: The callback isonMsg: (msg) => {}and language code is atmsg.lang. The lang value uses underscores (zh_CN) — convert withmsg.lang.replace('_', '-')before passing to i18next.registerButtonname must be a JSON string for localization: Passing a plain string means the button always shows that literal text regardless of device language. For multi-language support, serialize an object:name: JSON.stringify({en: 'Sticker', zh_CN: '贴纸', ...}).onButtonPressevent has apressEventfield: For lasso toolbar buttons,event.pressEvent === 3. Don't rely solely onid— checkpressEventto confirm the event type before routing.NativePluginManagervsPluginManager: Two different modules.NativePluginManager.getPluginDirPath()returns the plugin's private data directory (use for databases, sticker files). Cache this value — it's a slow async native call.- Rotation needs three listeners: Use
NativePluginManager.getOrientation()for initial value on mount,DeviceEventEmitter.addListener('plugin_event_rotation', ...)for rotation events, andDimensions.addEventListener('change', ...)for updated pixel dimensions. All three are needed for correct layout. generateStickerThumbnailtakes a Size object: The third argument is{width, height}, not two separate numbers. CallPluginCommAPI.getStickerSize(path)first.saveStickerByLassotakes a full file path: The argument is the destination file path (e.g.pluginDir + '/sticker/my.sticker'), not just a name.PluginNoteAPI.insertTextalways targets the current displayed page: There is no page parameter — text is inserted into whichever page the user is currently viewing. If your plugin tracks atargetPagefor sequential insertion, you must callPluginCommAPI.getCurrentPageNum()before eachinsertTextand verify the user is on the expected page. Inserting without this check will silently place text on the wrong page.getLastElement()takes no parameters: The official signature isgetLastElement() → APIResponse. It returns the last element of the currently displayed page. Do not pass(page, filePath)— those parameters are not part of the API.- Sequential text insertion across pages needs page-wait: After
insertNotePage()+reloadFile(), do NOT immediately resume inserting. The user must flip to the new page first (sinceinsertTexttargets the displayed page). Use a polling loop (getCurrentPageNum) to detect when the user arrives on the target page, then resume. A naïve timeout fallback that blindly resumes will insert text onto the wrong page. - Note file switch detection: If your plugin does background work (text insertion, etc.), periodically call
getCurrentFilePath()to verify the user hasn't switched to a different note. The SDK does not emit a "file changed" event — you must poll. - External page count changes: If the user manually adds or removes pages while your plugin tracks a
targetPage, page indices shift and your target becomes stale. Periodically callgetNoteTotalPageNum(path)and compare against your expected count to detect external changes. recognizeElementsneeds full page size, not lasso rect: Pass the result ofgetPageSize(filePath, pageNum)as thesizeargument — NOT the lasso bounding rect. Passing the lasso rect causes the firmware to throwIllegalArgumentException: getRealMaxX, unknown pageSizeand recognition fails entirely.recognizeElementsonly supports strokes and text boxes: Other element types (geometry, pictures, five-stars, links) are silently ignored. Filter your element list or checkgetLassoElementTypeCounts()before calling to avoid confusing empty results.PluginManager.closePluginView()does NOT firenotifyClientPluginState(0): The SDK skips the state-0 notification when transitioning the PluginApp tostop. Anything the note app does in response toonPluginState(state=1)(most importantlysendFullScreenDisableAreafor the EMR pen lock) will not be reversed byclosePluginViewalone. To release such state, first callPluginApp.showPluginView(0)by reflection (see Pattern 15), thenclosePluginViewfor cleanup. Note (0.1.43):closePluginViewalso requires aPromiseparameter in the native module — calling it via reflection withnulltriggers a non-fatal NPE atpromise.resolve(…)after the close logic has already executed.PluginManager.showPluginView()(0.1.43) /NativePluginManager.showPluginView()— both no-arg only: Calling either always opens the plugin view and triggersnotifyPluginState(1). SDK change in 0.1.43: ThePluginAppAPIabstract class removed theshowPluginView(int showType)overload — the abstract signature is nowshowPluginView()(no-arg). However, the device-side PluginHost firmware still has the int-arg method on the concrete `Plug
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: Laumss
- Source: Laumss/Inkling
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.