KOReader Plugin and Patch Development
KOReader is a document viewer for e-ink readers, written in Lua on LuaJIT. It exposes two main extension mechanisms:
- Plugins — self-contained
.koplugin/directories loaded bypluginloaderfromDEFAULT_PLUGIN_PATHorextra_plugin_paths. Use for new features, menu entries, or background tasks. - Patches — Lua files in
koreader/patches/applied at startup by theuserpatchmodule. Use for runtime monkey-patching of KOReader internals when a plugin is too heavy or the change must run before plugin load.
Choose plugins when the change is a feature you can ship as a folder. Choose patches for surgical fixes to existing modules, behavior overrides, or hotfixes that target a specific KOReader version.
Mental Model
KOReader's UI is a tree of WidgetContainer instances (subclasses of EventListener) managed by a top-level UIManager. Communication happens via Event objects with a handler name and args. WidgetContainer:handleEvent propagates events to children first; if no child returns true, the container's own on<EventName> method runs.
Plugins are typically classes built via WidgetContainer:extend{...} and returned from main.lua; they register themselves to the UI host (ReaderUI or FileManager) via addToMainMenu, dispatcher actions, or by listening to events.
Patches manipulate already-loaded modules (require("apps.reader.readerui"), require("ui.widget.infomessage"), etc.) to change methods, add hooks, or replace functions before or after the UI is built. Module paths must be the full namespaced form KOReader uses internally — short forms silently no-op.
Workflow
To create a plugin
- Create a directory
MyPlugin.koplugin/containing at least:main.lua— module returning aWidgetContainer-derived class_meta.lua— table withname,fullname,description, optionalversion
- Place it under KOReader's
plugins/directory (or setextra_plugin_paths). - Implement the plugin class. Required pieces:
local MyPlugin = WidgetContainer:extend{ name = "myplugin" }at the top ofmain.lua, withreturn MyPluginat the bottom (return the class, not an instance — the loader instantiates it per host)function MyPlugin:init() ... endto wire dispatcher actions and event handlersfunction MyPlugin:addToMainMenu(menu_items) ... endif the plugin needs a menu entry
- Surface UI via
UIManager:show(InfoMessage:new{ text = "..." })or custom widgets. - React to events via
onEventName(self, ...)methods. Returntrueto consume. - Restart KOReader (or re-enter file manager) to pick up the plugin.
See examples/helloworld.lua for a minimal plugin and references/plugin-anatomy.md for full structure rules and lifecycle.
To create a patch
- Create
koreader/patches/2-my-fix.lua(the leading number sets priority; see priorities below). requirethe target module and replace or wrap methods on it.- Keep the patch small and version-pin it via a header comment that names the KOReader version it targets — patches break across releases.
- Restart KOReader. Errors during patch application surface in
crash.log.
Priority is encoded in the filename prefix passed by userpatch.applyPatches(priority). Common values:
| Prefix | Phase | Use for |
|---|---|---|
1- |
early-once (very early) | Startup-only configuration, env tweaks |
2- |
early (before UI) | Patch core modules before UI builds |
3- |
late (after UI) | Override running widgets, add menu items |
See references/patches.md for monkey-patch idioms, version-guards, and pitfalls.
Common Tasks
Add a menu item
Implement addToMainMenu(self, menu_items) in a plugin and push an entry into the proper sub-table (menu_items.tools, menu_items.plugins, etc.). The host UI calls this once when the menu is built.
Listen for an event
Add onEventName(self, arg1, arg2) to the plugin class. Returning true stops further propagation; returning nil/false lets sibling widgets see the event. Common reader events include PosUpdate, UpdatePos, PageUpdate, ReaderReady, CloseDocument. See references/events-and-widgets.md for the wider catalog.
Show something to the user
Quick info: UIManager:show(InfoMessage:new{ text = _("Done") }).
Transient toast: Notification:notify("Saved").
Custom UI: subclass WidgetContainer (or use ButtonDialog, InputDialog), then UIManager:show(self). Always pair with UIManager:close(self) when dismissing.
Run code on a schedule
UIManager:scheduleIn(seconds, function() ... end) and UIManager:unschedule(callback). For repeating background work, prefer subclassing BackgroundTaskPlugin (ui.plugin.background_task_plugin).
Persist plugin state
Use LuaSettings (require("luasettings")) backed by a file under DataStorage:getSettingsDir(). For document-scoped state, write to self.ui.doc_settings so it follows the document.
Debugging
- Enable debug mode (
./kodev run --debugin dev, orSettings -> Developer options -> Enable debug loggingon device) to get stack traces for event handlers andlogger.dbg(...)output. logger.dbg("label", value)prints to stdout and tokoreader/crash.log. Lua arguments evaluate eagerly — guard heavy expressions withif dbg.is_on then ... end.- Read
crash.logfirst when something fails silently; missing menu entries usually trace back to a load-time error in the plugin. - Iterate on widget code without booting the reader:
./kodev wbuilderspins up a minimal UI host. AddUIManager:show(MyWidget:new{...})lines totools/wbuilder.luato preview.
See references/debugging.md for emulator setup, asserts, and breakpoint strategies.
Important Gotchas
- Module identity: patches must target the exact module path KOReader uses (
require("apps.reader.readerui"), notrequire("readerui")from outside the source root). Mismatched paths silently no-op. - Event return contract: forgetting to
return truecauses events to keep propagating, often manifesting as duplicated actions. - String translation: wrap user-visible strings in
_(...)fromgettextso they participate in translations. - Reader vs. FileManager host: plugins can run under either. Check
self.ui.name == "ReaderUI"or useif self.ui.document thento branch. - Version drift: KOReader internals change between releases. Both patches and plugins that touch private fields must declare a target version in the header and degrade gracefully if internals shift.
- No pcall around plugin init: an error during
initdisables the plugin without obvious user feedback. Keepinitdefensive and short; defer heavy work to lazy methods or first-event.
File Layout Reference
koreader/
├── plugins/
│ └── MyPlugin.koplugin/
│ ├── _meta.lua
│ ├── main.lua
│ └── (assets, sub-modules, README.md)
├── patches/
│ ├── 2-fix-something.lua
│ └── 3-override-menu.lua
└── crash.log
Source-of-Truth Links
These pages back the rules above. Verify against them when behavior diverges.
- Development guide (frontend layout): https://koreader.rocks/doc/topics/Development_guide.md.html
- Events guide (propagation, builtin events): https://koreader.rocks/doc/topics/Events.md.html
- Hacking guide (debugging, wbuilder): https://koreader.rocks/doc/topics/Hacking.md.html
pluginloadermodule: https://koreader.rocks/doc/modules/pluginloader.htmluserpatchmodule: https://koreader.rocks/doc/modules/userpatch.html- HelloWorld example plugin: https://github.com/koreader/koreader/tree/master/plugins/helloworld.koplugin
Additional Resources
Reference Files
references/plugin-anatomy.md—_meta.lua,main.lua, lifecycle,addToMainMenu, dispatcher integration, plugin disable/enable settings.references/events-and-widgets.md—WidgetContainerpropagation rules, common reader/filemanager events,UIManagerlifecycle.references/patches.md— patch priorities, monkey-patch idioms, version-guard patterns, removal/cleanup.references/debugging.md—logger,dbg,crash.log,wbuilder, emulator workflow.
Examples
examples/helloworld.lua— minimal pluginmain.luashowing menu registration andInfoMessage.examples/_meta.lua— minimal metadata file.examples/2-example-patch.lua— early-phase patch that wraps an existing method.
When information conflicts, the source-of-truth links above take precedence over this skill — verify against them before changing production code.