From 5499d85cbc047d0c738eb8129f54bddcd9936088 Mon Sep 17 00:00:00 2001 From: NiLuJe Date: Mon, 3 Feb 2020 20:08:18 +0100 Subject: [PATCH] Allow running shell scripts from the FileManager/Favorites (#5804) * Allow running Shell/Python scripts from the FM * Show an InfoMessage before/after running the script Since we're blocking the UI ;). * Allow running scripts from the favorites menu, too. --- frontend/apps/filemanager/filemanager.lua | 93 +++++++++++++------ .../filemanager/filemanagercollection.lua | 39 ++++++++ frontend/device/kindle/device.lua | 2 +- frontend/device/sysfs_light.lua | 2 +- frontend/document/credocument.lua | 3 + frontend/ui/uimanager.lua | 12 +-- frontend/ui/widget/imageviewer.lua | 2 +- frontend/util.lua | 27 ++++++ spec/unit/filemanager_spec.lua | 2 +- 9 files changed, 144 insertions(+), 38 deletions(-) diff --git a/frontend/apps/filemanager/filemanager.lua b/frontend/apps/filemanager/filemanager.lua index c93e25c77..285bc2839 100644 --- a/frontend/apps/filemanager/filemanager.lua +++ b/frontend/apps/filemanager/filemanager.lua @@ -40,7 +40,8 @@ local UIManager = require("ui/uimanager") local filemanagerutil = require("apps/filemanager/filemanagerutil") local lfs = require("libs/libkoreader-lfs") local logger = require("logger") -local util = require("ffi/util") +local BaseUtil = require("ffi/util") +local util = require("util") local _ = require("gettext") local C_ = _.pgettext local Screen = Device.screen @@ -226,10 +227,10 @@ function FileManager:init() }, { text = _("Purge .sdr"), - enabled = DocSettings:hasSidecarFile(util.realpath(file)), + enabled = DocSettings:hasSidecarFile(BaseUtil.realpath(file)), callback = function() UIManager:show(ConfirmBox:new{ - text = util.template(_("Purge .sdr to reset settings for this document?\n\n%1"), BD.filename(self.file_dialog.title)), + text = T(_("Purge .sdr to reset settings for this document?\n\n%1"), BD.filename(self.file_dialog.title)), ok_text = _("Purge"), ok_callback = function() filemanagerutil.purgeSettings(file) @@ -271,7 +272,7 @@ function FileManager:init() UIManager:close(self.file_dialog) fileManager.rename_dialog = InputDialog:new{ title = _("Rename file"), - input = util.basename(file), + input = BaseUtil.basename(file), buttons = {{ { text = _("Cancel"), @@ -297,8 +298,44 @@ function FileManager:init() } }, -- a little hack to get visual functionality grouping - {}, + { + }, } + + if not Device:isAndroid() and lfs.attributes(file, "mode") == "file" and util.isAllowedScript(file) then + -- NOTE: We populate the empty separator, in order not to mess with the button reordering code in CoverMenu + table.insert(buttons[3], + { + -- @translators This is the script's programming language (e.g., shell or python) + text = T(_("Execute %1 script"), util.getScriptType(file)), + enabled = true, + callback = function() + UIManager:close(self.file_dialog) + local script_is_running_msg = InfoMessage:new{ + -- @translators %1 is the script's programming language (e.g., shell or python), %2 is the filename + text = T(_("Running %1 script %2 ..."), util.getScriptType(file), BD.filename(BaseUtil.basename(file))), + } + UIManager:show(script_is_running_msg) + UIManager:scheduleIn(0.5, function() + local rv = os.execute(BaseUtil.realpath(file)) + UIManager:close(script_is_running_msg) + if rv == 0 then + UIManager:show(InfoMessage:new{ + text = _("The script exited successfully."), + }) + else + --- @note: Lua 5.1 returns the raw return value from the os's system call. Counteract this madness. + UIManager:show(InfoMessage:new{ + text = T(_("The script returned a non-zero status code: %1!"), bit.rshift(rv, 8)), + icon_file = "resources/info-warn.png", + }) + end + end) + end, + } + ) + end + if lfs.attributes(file, "mode") == "file" then table.insert(buttons, { { @@ -352,7 +389,7 @@ function FileManager:init() end end if lfs.attributes(file, "mode") == "directory" then - local realpath = util.realpath(file) + local realpath = BaseUtil.realpath(file) table.insert(buttons, { { text = _("Set as HOME directory"), @@ -679,7 +716,7 @@ end function FileManager:setHome(path) path = path or self.file_chooser.path UIManager:show(ConfirmBox:new{ - text = util.template(_("Set '%1' as HOME directory?"), BD.dirpath(path)), + text = T(_("Set '%1' as HOME directory?"), BD.dirpath(path)), ok_text = _("Set as HOME"), ok_callback = function() G_reader_settings:saveSetting("home_dir", path) @@ -692,7 +729,7 @@ function FileManager:openRandomFile(dir) local random_file = DocumentRegistry:getRandomFile(dir, false) if random_file then UIManager:show(MultiConfirmBox:new { - text = T(_("Do you want to open %1?"), BD.filename(util.basename(random_file))), + text = T(_("Do you want to open %1?"), BD.filename(BaseUtil.basename(random_file))), choice1_text = _("Open"), choice1_callback = function() FileManager.instance:onClose() @@ -725,17 +762,17 @@ end function FileManager:pasteHere(file) if self.clipboard then - file = util.realpath(file) - local orig = util.realpath(self.clipboard) + file = BaseUtil.realpath(file) + local orig = BaseUtil.realpath(self.clipboard) local dest = lfs.attributes(file, "mode") == "directory" and file or file:match("(.*/)") local function infoCopyFile() -- if we copy a file, also copy its sidecar directory if DocSettings:hasSidecarFile(orig) then - util.execute(self.cp_bin, "-r", DocSettings:getSidecarDir(orig), dest) + BaseUtil.execute(self.cp_bin, "-r", DocSettings:getSidecarDir(orig), dest) end - if util.execute(self.cp_bin, "-r", orig, dest) == 0 then + if BaseUtil.execute(self.cp_bin, "-r", orig, dest) == 0 then UIManager:show(InfoMessage:new { text = T(_("Copied to: %1"), BD.dirpath(dest)), timeout = 2, @@ -755,7 +792,7 @@ function FileManager:pasteHere(file) end if self:moveFile(orig, dest) then -- Update history and collections. - local dest_file = string.format("%s/%s", dest, util.basename(orig)) + local dest_file = string.format("%s/%s", dest, BaseUtil.basename(orig)) require("readhistory"):updateItemByPath(orig, dest_file) ReadCollection:updateItemByPath(orig, dest_file) -- Update last open file. @@ -780,7 +817,7 @@ function FileManager:pasteHere(file) else info_file = infoCopyFile end - local basename = util.basename(self.clipboard) + local basename = BaseUtil.basename(self.clipboard) local mode = lfs.attributes(string.format("%s/%s", dest, basename), "mode") if mode == "file" or mode == "directory" then local text @@ -809,7 +846,7 @@ end function FileManager:createFolder(curr_folder, new_folder) local folder = string.format("%s/%s", curr_folder, new_folder) - local code = util.execute(self.mkdir_bin, folder) + local code = BaseUtil.execute(self.mkdir_bin, folder) local text if code == 0 then self:onRefresh() @@ -825,10 +862,10 @@ end function FileManager:deleteFile(file) local ok, err, is_dir - local file_abs_path = util.realpath(file) + local file_abs_path = BaseUtil.realpath(file) if file_abs_path == nil then UIManager:show(InfoMessage:new{ - text = util.template(_("File %1 not found"), BD.filepath(file)), + text = T(_("File %1 not found"), BD.filepath(file)), }) return end @@ -837,7 +874,7 @@ function FileManager:deleteFile(file) if lfs.attributes(file_abs_path, "mode") == "file" then ok, err = os.remove(file_abs_path) else - ok, err = util.purgeDir(file_abs_path) + ok, err = BaseUtil.purgeDir(file_abs_path) is_dir = true end if ok and not err then @@ -852,19 +889,19 @@ function FileManager:deleteFile(file) end ReadCollection:removeItemByPath(file, is_dir) UIManager:show(InfoMessage:new{ - text = util.template(_("Deleted %1"), BD.filepath(file)), + text = T(_("Deleted %1"), BD.filepath(file)), timeout = 2, }) else UIManager:show(InfoMessage:new{ - text = util.template(_("An error occurred while trying to delete %1"), BD.filepath(file)), + text = T(_("An error occurred while trying to delete %1"), BD.filepath(file)), }) end end function FileManager:renameFile(file) - if util.basename(file) ~= self.rename_dialog:getInputText() then - local dest = util.joinPath(util.dirname(file), self.rename_dialog:getInputText()) + if BaseUtil.basename(file) ~= self.rename_dialog:getInputText() then + local dest = BaseUtil.joinPath(BaseUtil.dirname(file), self.rename_dialog:getInputText()) if self:moveFile(file, dest) then ReadCollection:updateItemByPath(file, dest) if lfs.attributes(dest, "mode") == "file" then @@ -880,12 +917,12 @@ function FileManager:renameFile(file) end if move_history then UIManager:show(InfoMessage:new{ - text = util.template(_("Renamed from %1 to %2"), BD.filepath(file), BD.filepath(dest)), + text = T(_("Renamed from %1 to %2"), BD.filepath(file), BD.filepath(dest)), timeout = 2, }) else UIManager:show(InfoMessage:new{ - text = util.template( + text = T( _("Failed to move history data of %1 to %2.\nThe reading history may be lost."), BD.filepath(file), BD.filepath(dest)), }) @@ -893,7 +930,7 @@ function FileManager:renameFile(file) end else UIManager:show(InfoMessage:new{ - text = util.template( + text = T( _("Failed to rename from %1 to %2"), BD.filepath(file), BD.filepath(dest)), }) end @@ -932,7 +969,7 @@ function FileManager:getSortingMenuTable() end return { text_func = function() - return util.template( + return T( _("Sort by: %1"), collates[fm.file_chooser.collate][1] ) @@ -982,7 +1019,7 @@ function FileManager:getStartWithMenuTable() end return { text_func = function() - return util.template( + return T( _("Start with: %1"), start_withs[start_with_setting][1] ) @@ -1018,7 +1055,7 @@ A shortcut to execute mv command (self.mv_bin) with from and to as parameters. Returns a boolean value to indicate the result of mv command. --]] function FileManager:moveFile(from, to) - return util.execute(self.mv_bin, from, to) == 0 + return BaseUtil.execute(self.mv_bin, from, to) == 0 end function FileManager:onHome() diff --git a/frontend/apps/filemanager/filemanagercollection.lua b/frontend/apps/filemanager/filemanagercollection.lua index 36387cbe3..c8bcc1d83 100644 --- a/frontend/apps/filemanager/filemanagercollection.lua +++ b/frontend/apps/filemanager/filemanagercollection.lua @@ -1,11 +1,17 @@ +local BD = require("ui/bidi") local ButtonDialogTitle = require("ui/widget/buttondialogtitle") +local Device = require("device") local FileManagerBookInfo = require("apps/filemanager/filemanagerbookinfo") +local InfoMessage = require("ui/widget/infomessage") local InputContainer = require("ui/widget/container/inputcontainer") local Menu = require("ui/widget/menu") local ReadCollection = require("readcollection") local UIManager = require("ui/uimanager") local Screen = require("device").screen +local BaseUtil = require("ffi/util") +local util = require("util") local _ = require("gettext") +local T = require("ffi/util").template local FileManagerCollection = InputContainer:extend{ coll_menu_title = _("Favorites"), @@ -87,6 +93,39 @@ function FileManagerCollection:onMenuHold(item) }, }, } + -- NOTE: Duplicated from frontend/apps/filemanager/filemanager.lua + if not Device:isAndroid() and util.isAllowedScript(item.file) then + table.insert(buttons, { + { + -- @translators This is the script's programming language (e.g., shell or python) + text = T(_("Execute %1 script"), util.getScriptType(item.file)), + enabled = true, + callback = function() + UIManager:close(self.collfile_dialog) + local script_is_running_msg = InfoMessage:new{ + -- @translators %1 is the script's programming language (e.g., shell or python), %2 is the filename + text = T(_("Running %1 script %2 ..."), util.getScriptType(item.file), BD.filename(BaseUtil.basename(item.file))), + } + UIManager:show(script_is_running_msg) + UIManager:scheduleIn(0.5, function() + local rv = os.execute(BaseUtil.realpath(item.file)) + UIManager:close(script_is_running_msg) + if rv == 0 then + UIManager:show(InfoMessage:new{ + text = _("The script exited successfully."), + }) + else + UIManager:show(InfoMessage:new{ + text = T(_("The script returned a non-zero status code: %1!"), bit.rshift(rv, 8)), + icon_file = "resources/info-warn.png", + }) + end + end) + end, + } + }) + end + self.collfile_dialog = ButtonDialogTitle:new{ title = item.text:match("([^/]+)$"), title_align = "center", diff --git a/frontend/device/kindle/device.lua b/frontend/device/kindle/device.lua index 731f73413..09af86a69 100644 --- a/frontend/device/kindle/device.lua +++ b/frontend/device/kindle/device.lua @@ -142,7 +142,7 @@ function Kindle:usbPlugIn() -- NOTE: We cannot support running in USBMS mode (we cannot, we live on the partition being exported!). -- But since that's the default state of the Kindle system, we have to try to make nice... -- To that end, we're currently SIGSTOPping volumd to inhibit the system's USBMS mode handling. - -- It's not perfect (f.g., if the system is setup for USBMS and not USBNet, + -- It's not perfect (e.g., if the system is setup for USBMS and not USBNet, -- the frontlight will be turned off when plugged in), but it at least prevents users from completely -- shooting themselves in the foot (c.f., https://github.com/koreader/koreader/issues/3220)! -- On the upside, we don't have to bother waking up the WM to show us the USBMS screen :D. diff --git a/frontend/device/sysfs_light.lua b/frontend/device/sysfs_light.lua index 6c6dc96c4..00e509b5b 100644 --- a/frontend/device/sysfs_light.lua +++ b/frontend/device/sysfs_light.lua @@ -67,7 +67,7 @@ function SysfsLight:setNaturalBrightness(brightness, warmth) -- Newer devices use a mixer instead of writting values per color. if self.frontlight_mixer then - -- Honor the device's scale, which may not be [0...100] (f.g., it's [0...10] on the Forma) ;). + -- Honor the device's scale, which may not be [0...100] (e.g., it's [0...10] on the Forma) ;). warmth = math.floor(warmth / self.nl_max) if set_brightness then -- Prefer the ioctl, as it's much lower latency. diff --git a/frontend/document/credocument.lua b/frontend/document/credocument.lua index c27b8fcf2..7dfa7c3c6 100644 --- a/frontend/document/credocument.lua +++ b/frontend/document/credocument.lua @@ -882,6 +882,9 @@ function CreDocument:register(registry) registry:addProvider("rtf", "application/rtf", self, 90) registry:addProvider("xhtml", "application/xhtml+xml", self, 90) registry:addProvider("zip", "application/zip", self, 10) + -- Scripts that we allow running in the FM (c.f., util.isAllowedScript) + registry:addProvider("sh", "application/x-shellscript", self, 90) + registry:addProvider("py", "text/x-python", self, 90) end -- Optimise usage of some of the above methods by caching their results, diff --git a/frontend/ui/uimanager.lua b/frontend/ui/uimanager.lua index fdf4e4632..672771e89 100644 --- a/frontend/ui/uimanager.lua +++ b/frontend/ui/uimanager.lua @@ -491,15 +491,15 @@ the second parameter (refreshtype) can either specify a refreshtype or a function that returns refreshtype AND refreshregion and is called after painting the widget. Here's a quick rundown of what each refreshtype should be used for: -full: high-fidelity flashing refresh (f.g., large images). +full: high-fidelity flashing refresh (e.g., large images). Highest quality, but highest latency. Don't abuse if you only want a flash (in this case, prefer flashpartial or flashui). -partial: medium fidelity refresh (f.g., text on a white background). +partial: medium fidelity refresh (e.g., text on a white background). Can be promoted to flashing after FULL_REFRESH_COUNT refreshes. Don't abuse to avoid spurious flashes. -ui: medium fidelity refresh (f.g., mixed content). +ui: medium fidelity refresh (e.g., mixed content). Should apply to most UI elements. -fast: low fidelity refresh (f.g., monochrome content). +fast: low fidelity refresh (e.g., monochrome content). Should apply to most highlighting effects achieved through inversion. Note that if your highlighted element contains text, you might want to keep the unhighlight refresh as "ui" instead, for crisper text. @@ -877,9 +877,9 @@ function UIManager:_refresh(mode, region, dither) -- NOTE: While, ideally, we shouldn't merge refreshes w/ different waveform modes, -- this allows us to optimize away a number of quirks of our rendering stack - -- (f.g., multiple setDirty calls queued when showing/closing a widget because of update mechanisms), + -- (e.g., multiple setDirty calls queued when showing/closing a widget because of update mechanisms), -- as well as a few actually effective merges - -- (f.g., the disappearance of a selection HL with the following menu update). + -- (e.g., the disappearance of a selection HL with the following menu update). for i = 1, #self._refresh_stack do -- check for collision with refreshes that are already enqueued if region:intersectWith(self._refresh_stack[i].region) then diff --git a/frontend/ui/widget/imageviewer.lua b/frontend/ui/widget/imageviewer.lua index c41ff7bf5..d0d825adc 100644 --- a/frontend/ui/widget/imageviewer.lua +++ b/frontend/ui/widget/imageviewer.lua @@ -363,7 +363,7 @@ function ImageViewer:update() file = self.file, image = self.image, image_disposable = false, -- we may re-use self.image - alpha = true, -- we might be showing images with an alpha channel (f.g., from Wikipedia) + alpha = true, -- we might be showing images with an alpha channel (e.g., from Wikipedia) width = max_image_w, height = max_image_h, rotation_angle = rotation_angle, diff --git a/frontend/util.lua b/frontend/util.lua index 4a6bf3ab8..b90fa7972 100644 --- a/frontend/util.lua +++ b/frontend/util.lua @@ -661,6 +661,33 @@ function util.getFileNameSuffix(file) return suffix end +--- Returns true if the file is a script we allow running +--- Basically a helper method to check a specific list of file extensions. +---- @string filename +---- @treturn boolean +function util.isAllowedScript(file) + local file_ext = string.lower(util.getFileNameSuffix(file)) + if file_ext == "sh" + or file_ext == "py" then + return true + else + return false + end +end + +--- Companion helper function that returns the script's language, +--- based on the filme extension. +---- @string filename +---- @treturn string (lowercase) (or nil if !isAllowedScript) +function util.getScriptType(file) + local file_ext = string.lower(util.getFileNameSuffix(file)) + if file_ext == "sh" then + return "shell" + elseif file_ext == "py" then + return "python" + end +end + --- Gets human friendly size as string ---- @int size (bytes) ---- @bool right_align (by padding with spaces on the left) diff --git a/spec/unit/filemanager_spec.lua b/spec/unit/filemanager_spec.lua index e9e962dea..55f728325 100644 --- a/spec/unit/filemanager_spec.lua +++ b/spec/unit/filemanager_spec.lua @@ -41,7 +41,7 @@ describe("FileManager module", function() root_path = "../../test", } - local tmp_fn = "../../test/2col.test.tmp.sh" + local tmp_fn = "../../test/2col.test.tmp.foo" util.copyFile("../../test/2col.pdf", tmp_fn) local tmp_sidecar = docsettings:getSidecarDir(util.realpath(tmp_fn))