KOReader 플러그인 개발
KOReader의 프론트엔드는 작은 위젯 툴킷 위에서 동작하는 일반 Lua(LuaJIT)입니다. 플러그인은 <name>.koplugin/ 디렉터리에 main.lua(코드)와 _meta.lua(표시 정보)로 구성됩니다. 빌드 단계가 없으므로, 올바른 방법은 아래의 검증된 규칙을 따르고 번들 스크립트로 확인하는 것이 API를 추측하는 것보다 빠릅니다.
공식 문서: https://koreader.rocks/doc/ (API 참조, 소스에서 생성됨) 및 소스 https://github.com/koreader/koreader (plugins/에 약 40개의 실제 플러그인; plugins/hello.koplugin이 최소 예제). 시그니처가 확실하지 않으면 모듈 문서 페이지를 확인하거나 소스를 읽으세요 — KOReader API는 릴리스마다 변경되며, 문서 사이트가 기억보다 우선합니다.
워크플로우
- 스캐폴딩 (새 플러그인):
python3 <skill>/scripts/new_plugin.py <id> "<전체 이름>" [--dir <경로>]명령어로 템플릿에서<id>.koplugin/디렉터리를 생성합니다. 메뉴 항목, Dispatcher 액션, 설정 지속성, 생명주기 훅이 미리 연결되어 있습니다. 처음부터 작성하지 말고 이를 편집하세요. - 구현 은 이 파일의 패턴을 따르고, 필요한 경우에만 참조 파일을 열어보세요:
references/plugin-anatomy.md— 파일 레이아웃, 로더 규칙,is_doc_only, 초기화 순서, 메뉴sorting_hint값references/ui-widgets.md— InfoMessage, ConfirmBox, InputDialog, MultiInputDialog, Menu, ButtonDialog, SpinWidget, Notification 및 작동 코드 조각references/events-and-lifecycle.md—onXxx핸들러, 브로드캐스팅, 스케줄링, Dispatcher, 리더 vs 파일 관리자 컨텍스트references/settings-and-storage.md— LuaSettings, G_reader_settings, 책별 문서 설정, DataStorage 경로references/testing-and-verification.md— 에뮬레이터 (./kodev run), luacheck, busted 스펙, 기기 설치 및 로그 읽기references/pitfalls.md— KOReader를 충돌시키거나 아무것도 하지 않는 실수; 마무리 전에 훑어보세요.
- 검증 (항상, 작동한다고 말하기 전에):
python3 <skill>/scripts/validate_plugin.py <path/to/x.koplugin>명령어를 실행합니다. 구조,_meta.lua, 메뉴 연결, gettext 사용, LuaJIT 문법(luajit -bl), luacheck(도구가 있을 때)를 확인합니다. 모든 ERROR를 수정하고 모든 WARN을 읽으세요. 그런 다음 검증된 내용과 검증되지 않은 내용을 솔직히 말하세요 — 검증기는 런타임 동작을 증명할 수 없으며, 오직 에뮬레이터나 기기만이 이를 증명할 수 있습니다(테스트 참조 참고).
최소 실행 가능한 플러그인
-- _meta.lua
local _ = require("gettext")
return {
fullname = _("My Plugin"),
description = _([[Plugin management에 표시될 한 줄 설명.]]),
}
-- main.lua
local Dispatcher = require("dispatcher")
local InfoMessage = require("ui/widget/infomessage")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local _ = require("gettext")
local MyPlugin = WidgetContainer:extend{
name = "myplugin", -- 고유 ID, 디렉터리 접두사와 일치
is_doc_only = false, -- true = 문서가 열려 있을 때만 로드됨
}
function MyPlugin:onDispatcherRegisterActions()
Dispatcher:registerAction("myplugin_hello", {
category = "none", event = "MyPluginHello",
title = _("My Plugin: hello"), general = true,
})
end
function MyPlugin:init()
self:onDispatcherRegisterActions()
self.ui.menu:registerToMainMenu(self)
end
function MyPlugin:addToMainMenu(menu_items)
menu_items.myplugin = {
text = _("My Plugin"),
sorting_hint = "more_tools",
callback = function() self:onMyPluginHello() end,
}
end
function MyPlugin:onMyPluginHello()
UIManager:show(InfoMessage:new{ text = _("Hello from my plugin") })
return true -- 이벤트 소비됨
end
return MyPlugin
중요한 규칙 (그리고 그 이유)
- 사용자에게 보이는 모든 문자열을
_()로 감싸세요 — KOReader는 gettext를 통해 번역됩니다. 감싸지 않은 문자열은 절대 지역화될 수 없습니다. 문자열 보간에는T = require("ffi/util").template을 사용하세요(T(_("페이지 %1"), n))...연결은 언어에 따라 단어 순서가 다르므로 사용하지 마세요. - 이벤트는
on<이벤트 이름>형식의 메서드입니다; 이벤트를 소비한 경우에만true를 반환하고, 그렇지 않으면 다른 위젯(코어 KOReader 포함)이 이벤트를 받지 못합니다. - UI 스레드를 절대 차단하지 마세요. 네트워크 호출, 큰 파일 검색,
os.execute는 단일 스레드 e-잉크 기기를 멈추게 합니다. 긴 작업에는NetworkMgr:runWhenOnline,UIManager:scheduleIn,Trapper:wrap(하위 프로세스 + 진행률)을 사용하세요. LuaSettings로 지속성을 유지하고 의도적으로 플러시하세요. 설정 파일은DataStorage:getSettingsDir()에 저장됩니다. 변경 후(onFlushSettings에서도):flush()를 호출하세요 — 그렇지 않으면 충돌이나 배터리 방전 시 설정이 손실됩니다.- 타이머(
UIManager:unschedule(fn))와onCloseWidget/onCloseDocument에서 연 위젯을 정리하세요. 누수된 스케줄된 콜백은 기기를 계속 깨우고 배터리를 소모합니다. - E-잉크 인식 UI:
InfoMessage에timeout을 사용하고, 애니메이션을 피하며, 변경된 위젯에만 최소한으로 새로 고침하세요(UIManager:setDirty(widget, "ui")). logger를 사용하세요 (require("logger");logger.dbg/info/warn/err).print대신 사용하세요. 출력은 에뮬레이터의 stdout과 기기의crash.log로 전달됩니다.- 선택적 종속성을 보호하세요 (
self.ui.statistics,self.ui.document,Device:hasFrontlight()): 동일한 플러그인이 파일 관리자나 해당 기능이 없는 기기에서 로드될 수 있습니다.
결과 보고
생성하거나 변경한 내용, 정확한 검증 명령어와 그 결과, 그리고 검증되지 않은 부분(예: "에뮬레이터에서 실행되지 않음")을 명시하세요. 검증기나 luacheck를 사용할 수 없었다면, 깨끗하게 통과했다고 암시하지 말고 그렇게 말하세요.