So I want to build a plugin (or two) for koreader. Specifically, I'd like to be able to download files using ssh / scp. I think this is the laziest way to share documents to your ereader since if you're running koreader, you probably have a server running ssh you can use to stash files. I also want to make an S3 plugin. There's lots of S3 compatible hosts (eg: min.io or backblaze) and storing your books there seems pretty easy and cheap. I thought I'd blog about the process of making a plugin as I go. Here's the most barebones basic plugin you can write ```lua local InfoMessage = require("ui/widget/infomessage") local UIManager = require("ui/uimanager") local WidgetContainer = require("ui/widget/container/widgetcontainer") local _ = require("gettext") local SCP = WidgetContainer:extend{ name = "scp", is_doc_only = false, } function SCP:init() self.ui.menu:registerToMainMenu(self) end function SCP:addToMainMenu(menu_items) menu_items.hello_world = { text = _("SCP"), callback = function() UIManager:show(InfoMessage:new{ text = _("yooo"), }) end, } end return SCP ``` ![Screenshot of KOReader with new plugin showing up](https://cdn.tahnok.ca/u/koreader_basic_plugin.png) Clicking on the item shows a model that says "yooo". I'm not sure what `is_doc_only = False` means, maybe that makes the plugin only active in documents rather than the main menu? I'm also not sure how `self.ui.menu:registerToMainMenu` ends up calling `addToMainMenu` but it makes sense that it does. The `_` is for translations I think. Basically anything a user can see should use it instead of a direct string. The other plugins I looked at call `Dispatcher::registerAction`. I think this is to register global "actions" which can be trigger by shortcuts or gestures, but I'm going to leave it out for now until I understand it better. Next up, let's make a more complex menu: ```lua function SCP:addToMainMenu(menu_items) menu_items.hello_world = { text = _("SCP"), sub_item_table = { { text = _("First"), keep_menu_open = true, callback = function() UIManager:show(InfoMessage:new{ text = _("yooo"), }) end, } } } end ``` Pretty straightforward. Next up, let's do some settings: ```lua local DataStorage = require("datastorage") local InfoMessage = require("ui/widget/infomessage") local LuaSettings = require("frontend/luasettings") local MultiInputDialog = require("ui/widget/multiinputdialog") local UIManager = require("ui/uimanager") local WidgetContainer = require("ui/widget/container/widgetcontainer") local logger = require("logger") local _ = require("gettext") local SCP = WidgetContainer:extend{ name = "scp", is_doc_only = false, } function SCP:init() self.ui.menu:registerToMainMenu(self) self.scp_settings = self.readSettings() self.host = self.scp_settings.data.scp.host self.port = self.scp_settings.data.scp.port self.user = self.scp_settings.data.scp.username self.password = self.scp_settings.data.scp.password self.path = self.scp_settings.data.scp.path end function SCP:addToMainMenu(menu_items) menu_items.hello_world = { text = _("SCP"), sub_item_table = { { text = _("Settings"), keep_menu_open = true, callback = function() self:editServerSettings() end, } } } end function SCP:readSettings() local scp_settings = LuaSettings:open(DataStorage:getSettingsDir().."/scp.lua") scp_settings:readSetting("scp", {}) return scp_settings end function SCP:saveSettings() local tempsettings = { host = self.host, port = self.port, user = self.user, password = self.password, path = self.path, } self.scp_settings:saveSetting("scp", tempsettings) self.scp_settings:flush() end function SCP:editServerSettings() local text_info = _("Some useful help text") self.settings_dialog = MultiInputDialog:new { title = _("SCP settings"), fields = { { text = self.host, hint = _("SSH Host") }, { text = self.port, input_type = "number", hint = _("SSH Port") }, { text = self.user, hint = _("User") }, { text = self.password, text_type = "password", hint = _("Password") }, { text = self.path, hint = _("Path") }, }, buttons = { { { text = _("Cancel"), id = "close", callback = function() UIManager:close(self.settings_dialog) end }, { text = _("Info"), callback = function() UIManager:show(InfoMessage:new{ text = text_info }) end }, { text = _("Apply"), callback = function() local myfields = self.settings_dialog:getFields() self.host = myfields[1] self.port = myfields[2] self.user = myfields[3] self.password = myfields[4] self.path = myfields[5] self:saveSettings() UIManager:close(self.settings_dialog) end }, }, }, } UIManager:show(self.settings_dialog) self.settings_dialog:onShowKeyboard() end return SCP ``` There's some stuff here about the settings API that seems a bit redundant