@kurokeita/add-skill 1.20.0 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/skills/koreader-plugin-development/SKILL.md +141 -0
- package/dist/skills/koreader-plugin-development/examples/2-example-patch.lua +17 -0
- package/dist/skills/koreader-plugin-development/examples/_meta.lua +6 -0
- package/dist/skills/koreader-plugin-development/examples/helloworld.lua +36 -0
- package/dist/skills/koreader-plugin-development/references/debugging.md +92 -0
- package/dist/skills/koreader-plugin-development/references/events-and-widgets.md +102 -0
- package/dist/skills/koreader-plugin-development/references/patches.md +118 -0
- package/dist/skills/koreader-plugin-development/references/plugin-anatomy.md +97 -0
- package/package.json +1 -1
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: koreader-plugin-development
|
|
3
|
+
description: This skill should be used when the user asks to "create a KOReader plugin", "write a koplugin", "build a KOReader patch", "userpatch", "modify KOReader behavior", "add a menu item to KOReader", "develop for KOReader", or mentions ".koplugin", "koreader/patches/", `WidgetContainer`, `UIManager`, or KOReader event handlers. Provides plugin and patch development guidance for the KOReader e-reader application (Lua, LuaJIT).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# KOReader Plugin and Patch Development
|
|
7
|
+
|
|
8
|
+
KOReader is a document viewer for e-ink readers, written in Lua on LuaJIT. It exposes two main extension mechanisms:
|
|
9
|
+
|
|
10
|
+
- **Plugins** — self-contained `.koplugin/` directories loaded by `pluginloader` from `DEFAULT_PLUGIN_PATH` or `extra_plugin_paths`. Use for new features, menu entries, or background tasks.
|
|
11
|
+
- **Patches** — Lua files in `koreader/patches/` applied at startup by the `userpatch` module. Use for runtime monkey-patching of KOReader internals when a plugin is too heavy or the change must run before plugin load.
|
|
12
|
+
|
|
13
|
+
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.
|
|
14
|
+
|
|
15
|
+
## Mental Model
|
|
16
|
+
|
|
17
|
+
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.
|
|
18
|
+
|
|
19
|
+
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.
|
|
20
|
+
|
|
21
|
+
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.
|
|
22
|
+
|
|
23
|
+
## Workflow
|
|
24
|
+
|
|
25
|
+
### To create a plugin
|
|
26
|
+
|
|
27
|
+
1. Create a directory `MyPlugin.koplugin/` containing at least:
|
|
28
|
+
- `main.lua` — module returning a `WidgetContainer`-derived class
|
|
29
|
+
- `_meta.lua` — table with `name`, `fullname`, `description`, optional `version`
|
|
30
|
+
2. Place it under KOReader's `plugins/` directory (or set `extra_plugin_paths`).
|
|
31
|
+
3. Implement the plugin class. Required pieces:
|
|
32
|
+
- `local MyPlugin = WidgetContainer:extend{ name = "myplugin" }` at the top of `main.lua`, with `return MyPlugin` at the bottom (return the class, not an instance — the loader instantiates it per host)
|
|
33
|
+
- `function MyPlugin:init() ... end` to wire dispatcher actions and event handlers
|
|
34
|
+
- `function MyPlugin:addToMainMenu(menu_items) ... end` if the plugin needs a menu entry
|
|
35
|
+
4. Surface UI via `UIManager:show(InfoMessage:new{ text = "..." })` or custom widgets.
|
|
36
|
+
5. React to events via `onEventName(self, ...)` methods. Return `true` to consume.
|
|
37
|
+
6. Restart KOReader (or re-enter file manager) to pick up the plugin.
|
|
38
|
+
|
|
39
|
+
See `examples/helloworld.lua` for a minimal plugin and `references/plugin-anatomy.md` for full structure rules and lifecycle.
|
|
40
|
+
|
|
41
|
+
### To create a patch
|
|
42
|
+
|
|
43
|
+
1. Create `koreader/patches/2-my-fix.lua` (the leading number sets priority; see priorities below).
|
|
44
|
+
2. `require` the target module and replace or wrap methods on it.
|
|
45
|
+
3. Keep the patch small and version-pin it via a header comment that names the KOReader version it targets — patches break across releases.
|
|
46
|
+
4. Restart KOReader. Errors during patch application surface in `crash.log`.
|
|
47
|
+
|
|
48
|
+
Priority is encoded in the filename prefix passed by `userpatch.applyPatches(priority)`. Common values:
|
|
49
|
+
|
|
50
|
+
| Prefix | Phase | Use for |
|
|
51
|
+
|--------|-------|---------|
|
|
52
|
+
| `1-` | early-once (very early) | Startup-only configuration, env tweaks |
|
|
53
|
+
| `2-` | early (before UI) | Patch core modules before UI builds |
|
|
54
|
+
| `3-` | late (after UI) | Override running widgets, add menu items |
|
|
55
|
+
|
|
56
|
+
See `references/patches.md` for monkey-patch idioms, version-guards, and pitfalls.
|
|
57
|
+
|
|
58
|
+
## Common Tasks
|
|
59
|
+
|
|
60
|
+
### Add a menu item
|
|
61
|
+
|
|
62
|
+
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.
|
|
63
|
+
|
|
64
|
+
### Listen for an event
|
|
65
|
+
|
|
66
|
+
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.
|
|
67
|
+
|
|
68
|
+
### Show something to the user
|
|
69
|
+
|
|
70
|
+
Quick info: `UIManager:show(InfoMessage:new{ text = _("Done") })`.
|
|
71
|
+
Transient toast: `Notification:notify("Saved")`.
|
|
72
|
+
Custom UI: subclass `WidgetContainer` (or use `ButtonDialog`, `InputDialog`), then `UIManager:show(self)`. Always pair with `UIManager:close(self)` when dismissing.
|
|
73
|
+
|
|
74
|
+
### Run code on a schedule
|
|
75
|
+
|
|
76
|
+
`UIManager:scheduleIn(seconds, function() ... end)` and `UIManager:unschedule(callback)`. For repeating background work, prefer subclassing `BackgroundTaskPlugin` (`ui.plugin.background_task_plugin`).
|
|
77
|
+
|
|
78
|
+
### Persist plugin state
|
|
79
|
+
|
|
80
|
+
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.
|
|
81
|
+
|
|
82
|
+
## Debugging
|
|
83
|
+
|
|
84
|
+
- Enable debug mode (`./kodev run --debug` in dev, or `Settings -> Developer options -> Enable debug logging` on device) to get stack traces for event handlers and `logger.dbg(...)` output.
|
|
85
|
+
- `logger.dbg("label", value)` prints to stdout and to `koreader/crash.log`. Lua arguments evaluate eagerly — guard heavy expressions with `if dbg.is_on then ... end`.
|
|
86
|
+
- Read `crash.log` first when something fails silently; missing menu entries usually trace back to a load-time error in the plugin.
|
|
87
|
+
- Iterate on widget code without booting the reader: `./kodev wbuilder` spins up a minimal UI host. Add `UIManager:show(MyWidget:new{...})` lines to `tools/wbuilder.lua` to preview.
|
|
88
|
+
|
|
89
|
+
See `references/debugging.md` for emulator setup, asserts, and breakpoint strategies.
|
|
90
|
+
|
|
91
|
+
## Important Gotchas
|
|
92
|
+
|
|
93
|
+
- **Module identity:** patches must target the exact module path KOReader uses (`require("apps.reader.readerui")`, not `require("readerui")` from outside the source root). Mismatched paths silently no-op.
|
|
94
|
+
- **Event return contract:** forgetting to `return true` causes events to keep propagating, often manifesting as duplicated actions.
|
|
95
|
+
- **String translation:** wrap user-visible strings in `_(...)` from `gettext` so they participate in translations.
|
|
96
|
+
- **Reader vs. FileManager host:** plugins can run under either. Check `self.ui.name == "ReaderUI"` or use `if self.ui.document then` to branch.
|
|
97
|
+
- **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.
|
|
98
|
+
- **No pcall around plugin init:** an error during `init` disables the plugin without obvious user feedback. Keep `init` defensive and short; defer heavy work to lazy methods or first-event.
|
|
99
|
+
|
|
100
|
+
## File Layout Reference
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
koreader/
|
|
104
|
+
├── plugins/
|
|
105
|
+
│ └── MyPlugin.koplugin/
|
|
106
|
+
│ ├── _meta.lua
|
|
107
|
+
│ ├── main.lua
|
|
108
|
+
│ └── (assets, sub-modules, README.md)
|
|
109
|
+
├── patches/
|
|
110
|
+
│ ├── 2-fix-something.lua
|
|
111
|
+
│ └── 3-override-menu.lua
|
|
112
|
+
└── crash.log
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Source-of-Truth Links
|
|
116
|
+
|
|
117
|
+
These pages back the rules above. Verify against them when behavior diverges.
|
|
118
|
+
|
|
119
|
+
- Development guide (frontend layout): <https://koreader.rocks/doc/topics/Development_guide.md.html>
|
|
120
|
+
- Events guide (propagation, builtin events): <https://koreader.rocks/doc/topics/Events.md.html>
|
|
121
|
+
- Hacking guide (debugging, wbuilder): <https://koreader.rocks/doc/topics/Hacking.md.html>
|
|
122
|
+
- `pluginloader` module: <https://koreader.rocks/doc/modules/pluginloader.html>
|
|
123
|
+
- `userpatch` module: <https://koreader.rocks/doc/modules/userpatch.html>
|
|
124
|
+
- HelloWorld example plugin: <https://github.com/koreader/koreader/tree/master/plugins/helloworld.koplugin>
|
|
125
|
+
|
|
126
|
+
## Additional Resources
|
|
127
|
+
|
|
128
|
+
### Reference Files
|
|
129
|
+
|
|
130
|
+
- `references/plugin-anatomy.md` — `_meta.lua`, `main.lua`, lifecycle, `addToMainMenu`, dispatcher integration, plugin disable/enable settings.
|
|
131
|
+
- `references/events-and-widgets.md` — `WidgetContainer` propagation rules, common reader/filemanager events, `UIManager` lifecycle.
|
|
132
|
+
- `references/patches.md` — patch priorities, monkey-patch idioms, version-guard patterns, removal/cleanup.
|
|
133
|
+
- `references/debugging.md` — `logger`, `dbg`, `crash.log`, `wbuilder`, emulator workflow.
|
|
134
|
+
|
|
135
|
+
### Examples
|
|
136
|
+
|
|
137
|
+
- `examples/helloworld.lua` — minimal plugin `main.lua` showing menu registration and `InfoMessage`.
|
|
138
|
+
- `examples/_meta.lua` — minimal metadata file.
|
|
139
|
+
- `examples/2-example-patch.lua` — early-phase patch that wraps an existing method.
|
|
140
|
+
|
|
141
|
+
When information conflicts, the source-of-truth links above take precedence over this skill — verify against them before changing production code.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
-- koreader/patches/2-example-patch.lua
|
|
2
|
+
-- Early-phase patch: wrap InfoMessage:init to prefix every message with "[patched]".
|
|
3
|
+
-- Targets KOReader v2025.05+. Remove if upstream changes the InfoMessage API.
|
|
4
|
+
|
|
5
|
+
local Version = require("version")
|
|
6
|
+
if Version:getCurrentRevision() < "v2025.05" then return end
|
|
7
|
+
|
|
8
|
+
local InfoMessage = require("ui.widget.infomessage")
|
|
9
|
+
local original_init = InfoMessage.init
|
|
10
|
+
|
|
11
|
+
function InfoMessage:init()
|
|
12
|
+
if self.text and not self._patched then
|
|
13
|
+
self.text = "[patched] " .. self.text
|
|
14
|
+
self._patched = true
|
|
15
|
+
end
|
|
16
|
+
return original_init(self)
|
|
17
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
-- Minimal KOReader plugin: HelloWorld.koplugin/main.lua
|
|
2
|
+
-- Add to plugins/HelloWorld.koplugin/ alongside _meta.lua, then restart KOReader.
|
|
3
|
+
|
|
4
|
+
local InfoMessage = require("ui.widget.infomessage")
|
|
5
|
+
local UIManager = require("ui.uimanager")
|
|
6
|
+
local WidgetContainer = require("ui.widget.container.widgetcontainer")
|
|
7
|
+
local logger = require("logger")
|
|
8
|
+
local _ = require("gettext")
|
|
9
|
+
|
|
10
|
+
local HelloWorld = WidgetContainer:extend{
|
|
11
|
+
name = "helloworld",
|
|
12
|
+
is_doc_only = false,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function HelloWorld:init()
|
|
16
|
+
self.ui.menu:registerToMainMenu(self)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
function HelloWorld:addToMainMenu(menu_items)
|
|
20
|
+
menu_items.helloworld = {
|
|
21
|
+
text = _("Hello World"),
|
|
22
|
+
sorting_hint = "tools",
|
|
23
|
+
callback = function()
|
|
24
|
+
UIManager:show(InfoMessage:new{
|
|
25
|
+
text = _("Hello, plugin world."),
|
|
26
|
+
})
|
|
27
|
+
end,
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
function HelloWorld:onPageUpdate(new_pageno)
|
|
32
|
+
logger.dbg("HelloWorld saw PageUpdate", new_pageno)
|
|
33
|
+
-- Do not return true; let other widgets handle the event too.
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
return HelloWorld
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Debugging KOReader Plugins and Patches
|
|
2
|
+
|
|
3
|
+
## Quick logger
|
|
4
|
+
|
|
5
|
+
```lua
|
|
6
|
+
local logger = require("logger")
|
|
7
|
+
logger.dbg("foo", { a = 1 }) -- prefixed with DEBUG
|
|
8
|
+
logger.info("startup ok")
|
|
9
|
+
logger.warn("recoverable issue")
|
|
10
|
+
logger.err("failure", err)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`logger.dbg` only prints when debug logging is enabled. Lua evaluates arguments eagerly, so wrap heavy work:
|
|
14
|
+
|
|
15
|
+
```lua
|
|
16
|
+
local dbg = require("dbg")
|
|
17
|
+
if dbg.is_on then
|
|
18
|
+
logger.dbg("expensive view", buildSnapshot())
|
|
19
|
+
end
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Where logs go
|
|
23
|
+
|
|
24
|
+
- Desktop emulator: stdout of the `./kodev run` process.
|
|
25
|
+
- On device: `koreader/crash.log`. Lines tagged `04/06/17-21:44:53 DEBUG …`.
|
|
26
|
+
|
|
27
|
+
`crash.log` is the first place to look when a plugin "doesn't load" or a patch "didn't run". Plugin-load errors land here with the plugin path.
|
|
28
|
+
|
|
29
|
+
## Enable debug mode
|
|
30
|
+
|
|
31
|
+
- Dev: `./kodev run --debug`
|
|
32
|
+
- On device: `Settings -> Developer options -> Enable debug logging`. Optionally `Enable verbose debug logging` for finer detail. Restart KOReader for the change to take effect.
|
|
33
|
+
|
|
34
|
+
In debug mode the loader logs stack traces from event handlers — this is how you find which plugin or patch raised an exception during event dispatch.
|
|
35
|
+
|
|
36
|
+
## Emulator workflow
|
|
37
|
+
|
|
38
|
+
`./kodev` is the dev driver. Useful targets:
|
|
39
|
+
|
|
40
|
+
- `./kodev run` — start the SDL emulator with the bundled plugins.
|
|
41
|
+
- `./kodev run --debug` — same with debug logging.
|
|
42
|
+
- `./kodev wbuilder` — minimal UI host for prototyping a single widget without booting reader/file-manager. Append `UIManager:show(MyWidget:new{...})` lines to `tools/wbuilder.lua`.
|
|
43
|
+
- `./kodev test` — run the unit suite (`busted`).
|
|
44
|
+
- `./kodev clean` / `./kodev fetch-thirdparty` — when builds drift.
|
|
45
|
+
|
|
46
|
+
For out-of-tree plugin development, point KOReader at your folder via the `extra_plugin_paths` setting (UI: Plugin manager) so you do not have to copy files on each iteration.
|
|
47
|
+
|
|
48
|
+
## Inspecting state
|
|
49
|
+
|
|
50
|
+
- `dbg.dump(any_value)` — pretty-prints a Lua table to the log.
|
|
51
|
+
- `require("dump")(any_value)` — alternative dumper used elsewhere in the codebase.
|
|
52
|
+
- `UIManager._window_stack` — the live widget stack, top of stack is the topmost visible widget.
|
|
53
|
+
- `self.ui` from inside a plugin gives access to the host (`ReaderUI` or `FileManager`) and all sibling modules (`self.ui.menu`, `self.ui.document`, `self.ui.dictionary`, etc.).
|
|
54
|
+
|
|
55
|
+
## Asserts and fail-fast
|
|
56
|
+
|
|
57
|
+
Plugin `init` errors disable the plugin silently in production; let them propagate during development:
|
|
58
|
+
|
|
59
|
+
```lua
|
|
60
|
+
function MyPlugin:init()
|
|
61
|
+
assert(self.ui, "no host UI")
|
|
62
|
+
-- ...
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Wrap the assert in `if dbg.is_on then` if you need a forgiving build.
|
|
67
|
+
|
|
68
|
+
## Common diagnostics
|
|
69
|
+
|
|
70
|
+
| Symptom | First check |
|
|
71
|
+
|---------|-------------|
|
|
72
|
+
| Plugin not in plugin manager | `crash.log` for load error; verify `_meta.lua` returns a table |
|
|
73
|
+
| Menu entry missing | `addToMainMenu` actually called? `self.ui.menu:registerToMainMenu(self)` in `init`? |
|
|
74
|
+
| Event handler not firing | Event name typo; `is_doc_only` true while in FileManager; child widget consuming first |
|
|
75
|
+
| Patch silently no-op | Wrong module path passed to `require`; another patch already replaced the same method |
|
|
76
|
+
| UI not redrawing | Missing `UIManager:setDirty(widget, "ui")` after mutating widget state |
|
|
77
|
+
|
|
78
|
+
## Reproducing on device vs emulator
|
|
79
|
+
|
|
80
|
+
Emulator runs the same Lua but uses SDL for framebuffer and lacks real Wi-Fi, real e-ink refresh quirks, and some device-specific paths. Behaviors that depend on the framebuffer driver (waveform modes, partial refresh) only repro on hardware. Path-sensitive logic (case-sensitive filesystems, mount points) usually shows up on Linux but not on macOS.
|
|
81
|
+
|
|
82
|
+
## Reading the source effectively
|
|
83
|
+
|
|
84
|
+
When the docs are silent (most plugin internals), grep:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
git grep "Event:new(\"PageUpdate\""
|
|
88
|
+
git grep "registerToMainMenu"
|
|
89
|
+
git grep "applyPatches"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The KOReader codebase is the authoritative reference; treat upstream code as documentation.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Events and Widgets
|
|
2
|
+
|
|
3
|
+
## Class hierarchy
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
EventListener -- ui.widget.eventlistener (handleEvent)
|
|
7
|
+
└── Widget -- ui.widget.widget
|
|
8
|
+
└── WidgetContainer -- ui.widget.container.widgetcontainer
|
|
9
|
+
├── FrameContainer, CenterContainer, BottomContainer, ...
|
|
10
|
+
├── InputContainer (gesture handling)
|
|
11
|
+
├── ReaderUI / FileManager (top-level hosts)
|
|
12
|
+
└── any plugin extending WidgetContainer
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
All widgets inherit `handleEvent`. `WidgetContainer` overrides it to first propagate to children; if no child returns `true`, the container's own `on<EventName>` runs.
|
|
16
|
+
|
|
17
|
+
## Sending an event
|
|
18
|
+
|
|
19
|
+
```lua
|
|
20
|
+
widget:handleEvent(Event:new("Timeout")) -- direct, may be unsafe if widget destroys itself
|
|
21
|
+
UIManager:sendEvent(Event:new("PageUpdate", 5)) -- from topmost widget down
|
|
22
|
+
UIManager:broadcastEvent(Event:new("CloseDocument")) -- to every widget on the stack
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`Event:new(name, arg1, arg2, ...)` packs args; the receiver implements `on<Name>(self, arg1, arg2, ...)`.
|
|
26
|
+
|
|
27
|
+
## The event consumption contract
|
|
28
|
+
|
|
29
|
+
```lua
|
|
30
|
+
function MyPlugin:onPageUpdate(new_pageno)
|
|
31
|
+
if not self.enabled then return end -- do not consume
|
|
32
|
+
self:doWork(new_pageno)
|
|
33
|
+
return true -- stop propagation
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- `return true` — consume; sibling widgets and the container's own handler are skipped.
|
|
38
|
+
- `return nil`/`false` — let propagation continue.
|
|
39
|
+
|
|
40
|
+
Consumption is the most common source of "my plugin runs but the reader stops responding to taps" bugs. Consume only events whose semantics you fully replace.
|
|
41
|
+
|
|
42
|
+
## Common reader events
|
|
43
|
+
|
|
44
|
+
| Event | When | Args |
|
|
45
|
+
|-------|------|------|
|
|
46
|
+
| `ReaderReady` | After document opens and modules initialized | `doc_settings` |
|
|
47
|
+
| `CloseDocument` | Before tearing down ReaderUI | — |
|
|
48
|
+
| `PosUpdate` | Reader rolling module signals scroll position change | `pos` |
|
|
49
|
+
| `UpdatePos` | Typesetting changed; recompute layout | — |
|
|
50
|
+
| `PageUpdate` | Page number changed | `new_pageno` |
|
|
51
|
+
| `PageChangeAnimation` | Animated page turn | `forward` |
|
|
52
|
+
| `BookMetadataChanged` | Metadata edited | `prop_updated` |
|
|
53
|
+
| `Suspend` / `Resume` | Device sleep cycle | — |
|
|
54
|
+
| `NetworkConnected` / `NetworkDisconnected` | Network state | — |
|
|
55
|
+
|
|
56
|
+
## Common file-manager events
|
|
57
|
+
|
|
58
|
+
| Event | When |
|
|
59
|
+
|-------|------|
|
|
60
|
+
| `PathChanged` | Directory navigated |
|
|
61
|
+
| `BookmarkAdded` / `BookmarkRemoved` | Bookmark mutated |
|
|
62
|
+
| `SetupShowReader` | About to launch Reader for a file |
|
|
63
|
+
|
|
64
|
+
For exhaustive coverage, search KOReader source for `Event:new("` — every emit is grep-able.
|
|
65
|
+
|
|
66
|
+
## UIManager lifecycle for widgets
|
|
67
|
+
|
|
68
|
+
```lua
|
|
69
|
+
UIManager:show(widget) -- push onto _window_stack, schedule paint
|
|
70
|
+
UIManager:setDirty(widget, "ui") -- request redraw
|
|
71
|
+
UIManager:close(widget) -- pop, schedule repaint of stack below
|
|
72
|
+
UIManager:scheduleIn(0.5, fn) -- run after 500ms
|
|
73
|
+
UIManager:unschedule(fn)
|
|
74
|
+
UIManager:nextTick(fn) -- defer to next event loop tick
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Always pair `show` with `close`. Widgets registered with `is_always_active = true` keep receiving events even when not at the top of the stack.
|
|
78
|
+
|
|
79
|
+
## Useful built-in widgets
|
|
80
|
+
|
|
81
|
+
| Widget | Purpose |
|
|
82
|
+
|--------|---------|
|
|
83
|
+
| `InfoMessage` | Modal info text with auto-close |
|
|
84
|
+
| `ConfirmBox` | Yes/no prompt with callbacks |
|
|
85
|
+
| `ButtonDialog` | Multi-button dialog from a 2D grid |
|
|
86
|
+
| `InputDialog` | Text input with buttons |
|
|
87
|
+
| `Notification` | Transient toast (no user dismissal) |
|
|
88
|
+
| `Menu` | Scrollable list of items |
|
|
89
|
+
| `KeyValuePage` | Two-column property display |
|
|
90
|
+
| `TrapWidget` | Block input while a long task runs |
|
|
91
|
+
|
|
92
|
+
All are under `ui.widget.*` and accept a table of properties to `:new{...}`.
|
|
93
|
+
|
|
94
|
+
## Draw-page code path (for performance work)
|
|
95
|
+
|
|
96
|
+
1. `ReaderView:recalculate` flags itself dirty.
|
|
97
|
+
2. UIManager main loop calls `ReaderView:paintTo`.
|
|
98
|
+
3. `ReaderView:paintTo` calls `document:drawPage`.
|
|
99
|
+
4. `document:drawPage` checks the cache; if hit, returns the cached buffer.
|
|
100
|
+
5. On miss, `document:renderPage` calls `_document:openPage`, `page:draw`, then caches the buffer.
|
|
101
|
+
|
|
102
|
+
Hooking `ReaderView:paintTo` from a plugin can layer custom overlays per page; do it sparingly and never block.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Patches (`koreader/patches/`)
|
|
2
|
+
|
|
3
|
+
User patches are arbitrary Lua files in `koreader/patches/` that the `userpatch` module applies during startup. Use them when:
|
|
4
|
+
|
|
5
|
+
- A plugin would be overkill for a one-line behavior change.
|
|
6
|
+
- The change must run before plugins or before the UI builds.
|
|
7
|
+
- Distributing a hotfix to users who can drop a single file into a directory.
|
|
8
|
+
|
|
9
|
+
## Priority ordering
|
|
10
|
+
|
|
11
|
+
`userpatch.applyPatches(priority)` is called multiple times during boot. Each call processes files whose name starts with the matching numeric prefix, sorted lexicographically.
|
|
12
|
+
|
|
13
|
+
| Filename prefix | Phase | Run point | Typical use |
|
|
14
|
+
|-----------------|-------|-----------|-------------|
|
|
15
|
+
| `1-` | early-once | Very early, before most modules load | Set globals, env vars, monkey-patch core utilities |
|
|
16
|
+
| `2-` | early | After core modules load, before UI | Override module methods that the UI then uses |
|
|
17
|
+
| `3-` | late | After UI is built | Replace running widgets, mutate menus |
|
|
18
|
+
|
|
19
|
+
A patch named `2-fix-readerfooter.lua` runs in the early phase; `3-add-menu.lua` runs late.
|
|
20
|
+
|
|
21
|
+
## Patch skeleton
|
|
22
|
+
|
|
23
|
+
```lua
|
|
24
|
+
-- 2-stay-on-cover.lua
|
|
25
|
+
-- Targets KOReader v2025.05+; remove if behavior is fixed upstream.
|
|
26
|
+
|
|
27
|
+
local ReaderRolling = require("apps.reader.modules.readerrolling")
|
|
28
|
+
local original_onReadSettings = ReaderRolling.onReadSettings
|
|
29
|
+
|
|
30
|
+
function ReaderRolling:onReadSettings(config)
|
|
31
|
+
local res = original_onReadSettings(self, config)
|
|
32
|
+
if config:nilOrTrue("stay_on_cover") then
|
|
33
|
+
self:gotoPos(0)
|
|
34
|
+
end
|
|
35
|
+
return res
|
|
36
|
+
end
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Always:
|
|
40
|
+
|
|
41
|
+
1. Capture the original function before replacement.
|
|
42
|
+
2. Call the original from inside your override unless the goal is total replacement.
|
|
43
|
+
3. Pass `self` and all args through unchanged.
|
|
44
|
+
|
|
45
|
+
## Patch idioms
|
|
46
|
+
|
|
47
|
+
### Add a method
|
|
48
|
+
|
|
49
|
+
```lua
|
|
50
|
+
local M = require("apps.reader.modules.readerbookmark")
|
|
51
|
+
function M:exportAll()
|
|
52
|
+
-- ...
|
|
53
|
+
end
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Wrap an existing method
|
|
57
|
+
|
|
58
|
+
```lua
|
|
59
|
+
local M = require("ui.widget.infomessage")
|
|
60
|
+
local orig = M.init
|
|
61
|
+
function M:init()
|
|
62
|
+
orig(self)
|
|
63
|
+
self.text = "[patched] " .. (self.text or "")
|
|
64
|
+
end
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Replace a method
|
|
68
|
+
|
|
69
|
+
```lua
|
|
70
|
+
local M = require("apps.reader.modules.readerfooter")
|
|
71
|
+
function M:onTapFooter() return false end
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Add a menu entry from a patch
|
|
75
|
+
|
|
76
|
+
A patch can mutate the menu builder used by the UI — the safer option is to attach to an existing module's `addToMainMenu`:
|
|
77
|
+
|
|
78
|
+
```lua
|
|
79
|
+
local M = require("apps.reader.modules.readermenu")
|
|
80
|
+
local orig = M.setUpdateItemTable
|
|
81
|
+
function M:setUpdateItemTable()
|
|
82
|
+
orig(self)
|
|
83
|
+
self.menu_items.tools.sub_item_table[#self.menu_items.tools.sub_item_table + 1] = {
|
|
84
|
+
text = "Patched entry",
|
|
85
|
+
callback = function() end,
|
|
86
|
+
}
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
For complex menu work, prefer a plugin.
|
|
91
|
+
|
|
92
|
+
## Version guarding
|
|
93
|
+
|
|
94
|
+
KOReader internals shift between releases. A patch that worked on v2025.04 may crash on v2025.07.
|
|
95
|
+
|
|
96
|
+
```lua
|
|
97
|
+
local Version = require("version")
|
|
98
|
+
local current = Version:getCurrentRevision()
|
|
99
|
+
if current < "v2025.05" then return end -- skip if older
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Always include a target-version comment header so users (and you, six months later) can audit at a glance.
|
|
103
|
+
|
|
104
|
+
## Removal
|
|
105
|
+
|
|
106
|
+
To disable a patch, delete it or rename it so the prefix no longer matches a known phase (e.g. `_off-2-foo.lua`). Patches do not auto-clean their effects on next boot — KOReader simply does not reapply them.
|
|
107
|
+
|
|
108
|
+
Patches that allocate scheduled tasks or register listeners must clean up on `CloseDocument` / `Exit` if the user might disable them mid-session. Easier path: design patches to be idempotent and applied only at startup.
|
|
109
|
+
|
|
110
|
+
## Failure mode
|
|
111
|
+
|
|
112
|
+
A patch that errors during application aborts only that patch; remaining patches and the rest of boot continue. The traceback lands in `crash.log` with the patch filename. Always test with debug logging on (`./kodev run --debug`) before shipping.
|
|
113
|
+
|
|
114
|
+
## When NOT to use a patch
|
|
115
|
+
|
|
116
|
+
- Adding substantial new UI → write a plugin.
|
|
117
|
+
- Anything users should be able to toggle → write a plugin.
|
|
118
|
+
- Patching something that has an official extension hook → use the hook (events, dispatcher, `addToMainMenu`).
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Plugin Anatomy
|
|
2
|
+
|
|
3
|
+
## Directory layout
|
|
4
|
+
|
|
5
|
+
A plugin is a directory whose name matches `.+%.koplugin`. KOReader scans `DEFAULT_PLUGIN_PATH` (the bundled `plugins/` directory) plus any path listed in the `extra_plugin_paths` setting. The user setting `plugins_disabled` is a table of plugin names to skip.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
MyPlugin.koplugin/
|
|
9
|
+
├── _meta.lua # plugin metadata (required for menu integration)
|
|
10
|
+
├── main.lua # returns the plugin class
|
|
11
|
+
├── README.md # optional, surfaced in the plugin manager UI
|
|
12
|
+
├── i18n/ # optional Crowdin-style translations
|
|
13
|
+
└── *.lua # additional sub-modules
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## `_meta.lua`
|
|
17
|
+
|
|
18
|
+
Returns a plain table that pluginloader reads before instantiating `main.lua`. Minimum fields:
|
|
19
|
+
|
|
20
|
+
```lua
|
|
21
|
+
return {
|
|
22
|
+
name = "myplugin", -- internal id, must match WidgetContainer name
|
|
23
|
+
fullname = _("My Plugin"), -- shown in plugin manager
|
|
24
|
+
description = _([[One-paragraph description.]]),
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Optional fields recognized by the loader: `version`, `author`. The translation function `_` comes from `gettext` and is available in `_meta.lua` evaluation scope.
|
|
29
|
+
|
|
30
|
+
## `main.lua` skeleton
|
|
31
|
+
|
|
32
|
+
```lua
|
|
33
|
+
local WidgetContainer = require("ui.widget.container.widgetcontainer")
|
|
34
|
+
local InfoMessage = require("ui.widget.infomessage")
|
|
35
|
+
local UIManager = require("ui.uimanager")
|
|
36
|
+
local _ = require("gettext")
|
|
37
|
+
|
|
38
|
+
local MyPlugin = WidgetContainer:extend{
|
|
39
|
+
name = "myplugin",
|
|
40
|
+
is_doc_only = false, -- true: only load inside ReaderUI
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function MyPlugin:init()
|
|
44
|
+
self.ui.menu:registerToMainMenu(self)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
function MyPlugin:addToMainMenu(menu_items)
|
|
48
|
+
menu_items.myplugin = {
|
|
49
|
+
text = _("My plugin"),
|
|
50
|
+
sorting_hint = "tools",
|
|
51
|
+
callback = function()
|
|
52
|
+
UIManager:show(InfoMessage:new{ text = _("Hello from MyPlugin") })
|
|
53
|
+
end,
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
return MyPlugin
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Key conventions:
|
|
61
|
+
|
|
62
|
+
- `name` must be unique and match the directory stem.
|
|
63
|
+
- `is_doc_only = true` restricts the plugin to ReaderUI; omit for both hosts.
|
|
64
|
+
- `init` runs after the host UI built `self.ui`. Treat it as the plugin's constructor — register menus, dispatcher actions, and event listeners here.
|
|
65
|
+
- Returning the class (not an instance) lets pluginloader instantiate per host.
|
|
66
|
+
|
|
67
|
+
## Lifecycle and host wiring
|
|
68
|
+
|
|
69
|
+
`pluginloader` instantiates plugins for the active UI host (`FileManager` or `ReaderUI`). Each instance is appended as a child of that host's `WidgetContainer`, so it receives every event the host receives via standard propagation.
|
|
70
|
+
|
|
71
|
+
To register hooks beyond the menu:
|
|
72
|
+
|
|
73
|
+
- Dispatcher actions (gestures, profiles): `Dispatcher:registerAction("my_action", { category = "none", event = "MyAction", title = _("My action"), general = true })` then handle `onMyAction`.
|
|
74
|
+
- Background work: extend `ui.plugin.background_task_plugin`.
|
|
75
|
+
- Toggle behavior: extend `ui.plugin.switch_plugin`.
|
|
76
|
+
|
|
77
|
+
## Event handlers in plugins
|
|
78
|
+
|
|
79
|
+
Implement `onFoo(self, ...)` to handle event `Foo`. The host UI broadcasts events down the tree; plugins receive them after host modules. Returning `true` stops propagation. Avoid swallowing events the reader needs (`PageUpdate`, `PosUpdate`) unless that is the goal.
|
|
80
|
+
|
|
81
|
+
## Persistence
|
|
82
|
+
|
|
83
|
+
- App-wide settings: `local LuaSettings = require("luasettings"); local s = LuaSettings:open(DataStorage:getSettingsDir() .. "/myplugin.lua")`.
|
|
84
|
+
- Per-document state: `self.ui.doc_settings:saveSetting("myplugin_state", value)`. Saved with the document's metadata.
|
|
85
|
+
- Always handle the case where the file does not yet exist; LuaSettings degrades to an empty table.
|
|
86
|
+
|
|
87
|
+
## Internationalization
|
|
88
|
+
|
|
89
|
+
Wrap user-visible strings: `local _ = require("gettext"); _("Save")`. For plurals, `gettext.ngettext("%1 page", "%1 pages", n)`. Translations live under KOReader's main `l10n/` tree; for in-tree plugins, use the project's translation pipeline. For out-of-tree plugins, you may ship strings English-only or vendor your own gettext catalog.
|
|
90
|
+
|
|
91
|
+
## Loading errors
|
|
92
|
+
|
|
93
|
+
Errors during plugin load disable the plugin silently for the user but log to `crash.log`. Defensive techniques:
|
|
94
|
+
|
|
95
|
+
- Wrap optional dependencies (e.g. plugins that integrate with another plugin) in `pcall(require, "...")`.
|
|
96
|
+
- Keep `_meta.lua` side-effect free — it loads in a plain `dofile` context.
|
|
97
|
+
- Avoid top-level `require` of modules that only exist in newer KOReader versions; gate with `pcall` and fall back gracefully.
|