@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.
@@ -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,6 @@
1
+ local _ = require("gettext")
2
+ return {
3
+ name = "helloworld",
4
+ fullname = _("Hello World"),
5
+ description = _([[A minimal plugin that adds a menu entry showing an InfoMessage.]]),
6
+ }
@@ -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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kurokeita/add-skill",
3
- "version": "1.20.0",
3
+ "version": "1.21.0",
4
4
  "description": "CLI to install AI agent skills to various platforms",
5
5
  "type": "module",
6
6
  "bin": {