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.
pull/5811/head
NiLuJe 4 years ago committed by GitHub
parent d64e143297
commit 5499d85cbc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -40,7 +40,8 @@ local UIManager = require("ui/uimanager")
local filemanagerutil = require("apps/filemanager/filemanagerutil") local filemanagerutil = require("apps/filemanager/filemanagerutil")
local lfs = require("libs/libkoreader-lfs") local lfs = require("libs/libkoreader-lfs")
local logger = require("logger") local logger = require("logger")
local util = require("ffi/util") local BaseUtil = require("ffi/util")
local util = require("util")
local _ = require("gettext") local _ = require("gettext")
local C_ = _.pgettext local C_ = _.pgettext
local Screen = Device.screen local Screen = Device.screen
@ -226,10 +227,10 @@ function FileManager:init()
}, },
{ {
text = _("Purge .sdr"), text = _("Purge .sdr"),
enabled = DocSettings:hasSidecarFile(util.realpath(file)), enabled = DocSettings:hasSidecarFile(BaseUtil.realpath(file)),
callback = function() callback = function()
UIManager:show(ConfirmBox:new{ 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_text = _("Purge"),
ok_callback = function() ok_callback = function()
filemanagerutil.purgeSettings(file) filemanagerutil.purgeSettings(file)
@ -271,7 +272,7 @@ function FileManager:init()
UIManager:close(self.file_dialog) UIManager:close(self.file_dialog)
fileManager.rename_dialog = InputDialog:new{ fileManager.rename_dialog = InputDialog:new{
title = _("Rename file"), title = _("Rename file"),
input = util.basename(file), input = BaseUtil.basename(file),
buttons = {{ buttons = {{
{ {
text = _("Cancel"), text = _("Cancel"),
@ -297,8 +298,44 @@ function FileManager:init()
} }
}, },
-- a little hack to get visual functionality grouping -- 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 if lfs.attributes(file, "mode") == "file" then
table.insert(buttons, { table.insert(buttons, {
{ {
@ -352,7 +389,7 @@ function FileManager:init()
end end
end end
if lfs.attributes(file, "mode") == "directory" then if lfs.attributes(file, "mode") == "directory" then
local realpath = util.realpath(file) local realpath = BaseUtil.realpath(file)
table.insert(buttons, { table.insert(buttons, {
{ {
text = _("Set as HOME directory"), text = _("Set as HOME directory"),
@ -679,7 +716,7 @@ end
function FileManager:setHome(path) function FileManager:setHome(path)
path = path or self.file_chooser.path path = path or self.file_chooser.path
UIManager:show(ConfirmBox:new{ 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_text = _("Set as HOME"),
ok_callback = function() ok_callback = function()
G_reader_settings:saveSetting("home_dir", path) G_reader_settings:saveSetting("home_dir", path)
@ -692,7 +729,7 @@ function FileManager:openRandomFile(dir)
local random_file = DocumentRegistry:getRandomFile(dir, false) local random_file = DocumentRegistry:getRandomFile(dir, false)
if random_file then if random_file then
UIManager:show(MultiConfirmBox:new { 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_text = _("Open"),
choice1_callback = function() choice1_callback = function()
FileManager.instance:onClose() FileManager.instance:onClose()
@ -725,17 +762,17 @@ end
function FileManager:pasteHere(file) function FileManager:pasteHere(file)
if self.clipboard then if self.clipboard then
file = util.realpath(file) file = BaseUtil.realpath(file)
local orig = util.realpath(self.clipboard) local orig = BaseUtil.realpath(self.clipboard)
local dest = lfs.attributes(file, "mode") == "directory" and local dest = lfs.attributes(file, "mode") == "directory" and
file or file:match("(.*/)") file or file:match("(.*/)")
local function infoCopyFile() local function infoCopyFile()
-- if we copy a file, also copy its sidecar directory -- if we copy a file, also copy its sidecar directory
if DocSettings:hasSidecarFile(orig) then 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 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 { UIManager:show(InfoMessage:new {
text = T(_("Copied to: %1"), BD.dirpath(dest)), text = T(_("Copied to: %1"), BD.dirpath(dest)),
timeout = 2, timeout = 2,
@ -755,7 +792,7 @@ function FileManager:pasteHere(file)
end end
if self:moveFile(orig, dest) then if self:moveFile(orig, dest) then
-- Update history and collections. -- 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) require("readhistory"):updateItemByPath(orig, dest_file)
ReadCollection:updateItemByPath(orig, dest_file) ReadCollection:updateItemByPath(orig, dest_file)
-- Update last open file. -- Update last open file.
@ -780,7 +817,7 @@ function FileManager:pasteHere(file)
else else
info_file = infoCopyFile info_file = infoCopyFile
end end
local basename = util.basename(self.clipboard) local basename = BaseUtil.basename(self.clipboard)
local mode = lfs.attributes(string.format("%s/%s", dest, basename), "mode") local mode = lfs.attributes(string.format("%s/%s", dest, basename), "mode")
if mode == "file" or mode == "directory" then if mode == "file" or mode == "directory" then
local text local text
@ -809,7 +846,7 @@ end
function FileManager:createFolder(curr_folder, new_folder) function FileManager:createFolder(curr_folder, new_folder)
local folder = string.format("%s/%s", 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 local text
if code == 0 then if code == 0 then
self:onRefresh() self:onRefresh()
@ -825,10 +862,10 @@ end
function FileManager:deleteFile(file) function FileManager:deleteFile(file)
local ok, err, is_dir 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 if file_abs_path == nil then
UIManager:show(InfoMessage:new{ UIManager:show(InfoMessage:new{
text = util.template(_("File %1 not found"), BD.filepath(file)), text = T(_("File %1 not found"), BD.filepath(file)),
}) })
return return
end end
@ -837,7 +874,7 @@ function FileManager:deleteFile(file)
if lfs.attributes(file_abs_path, "mode") == "file" then if lfs.attributes(file_abs_path, "mode") == "file" then
ok, err = os.remove(file_abs_path) ok, err = os.remove(file_abs_path)
else else
ok, err = util.purgeDir(file_abs_path) ok, err = BaseUtil.purgeDir(file_abs_path)
is_dir = true is_dir = true
end end
if ok and not err then if ok and not err then
@ -852,19 +889,19 @@ function FileManager:deleteFile(file)
end end
ReadCollection:removeItemByPath(file, is_dir) ReadCollection:removeItemByPath(file, is_dir)
UIManager:show(InfoMessage:new{ UIManager:show(InfoMessage:new{
text = util.template(_("Deleted %1"), BD.filepath(file)), text = T(_("Deleted %1"), BD.filepath(file)),
timeout = 2, timeout = 2,
}) })
else else
UIManager:show(InfoMessage:new{ 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
end end
function FileManager:renameFile(file) function FileManager:renameFile(file)
if util.basename(file) ~= self.rename_dialog:getInputText() then if BaseUtil.basename(file) ~= self.rename_dialog:getInputText() then
local dest = util.joinPath(util.dirname(file), self.rename_dialog:getInputText()) local dest = BaseUtil.joinPath(BaseUtil.dirname(file), self.rename_dialog:getInputText())
if self:moveFile(file, dest) then if self:moveFile(file, dest) then
ReadCollection:updateItemByPath(file, dest) ReadCollection:updateItemByPath(file, dest)
if lfs.attributes(dest, "mode") == "file" then if lfs.attributes(dest, "mode") == "file" then
@ -880,12 +917,12 @@ function FileManager:renameFile(file)
end end
if move_history then if move_history then
UIManager:show(InfoMessage:new{ 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, timeout = 2,
}) })
else else
UIManager:show(InfoMessage:new{ UIManager:show(InfoMessage:new{
text = util.template( text = T(
_("Failed to move history data of %1 to %2.\nThe reading history may be lost."), _("Failed to move history data of %1 to %2.\nThe reading history may be lost."),
BD.filepath(file), BD.filepath(dest)), BD.filepath(file), BD.filepath(dest)),
}) })
@ -893,7 +930,7 @@ function FileManager:renameFile(file)
end end
else else
UIManager:show(InfoMessage:new{ UIManager:show(InfoMessage:new{
text = util.template( text = T(
_("Failed to rename from %1 to %2"), BD.filepath(file), BD.filepath(dest)), _("Failed to rename from %1 to %2"), BD.filepath(file), BD.filepath(dest)),
}) })
end end
@ -932,7 +969,7 @@ function FileManager:getSortingMenuTable()
end end
return { return {
text_func = function() text_func = function()
return util.template( return T(
_("Sort by: %1"), _("Sort by: %1"),
collates[fm.file_chooser.collate][1] collates[fm.file_chooser.collate][1]
) )
@ -982,7 +1019,7 @@ function FileManager:getStartWithMenuTable()
end end
return { return {
text_func = function() text_func = function()
return util.template( return T(
_("Start with: %1"), _("Start with: %1"),
start_withs[start_with_setting][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. Returns a boolean value to indicate the result of mv command.
--]] --]]
function FileManager:moveFile(from, to) function FileManager:moveFile(from, to)
return util.execute(self.mv_bin, from, to) == 0 return BaseUtil.execute(self.mv_bin, from, to) == 0
end end
function FileManager:onHome() function FileManager:onHome()

@ -1,11 +1,17 @@
local BD = require("ui/bidi")
local ButtonDialogTitle = require("ui/widget/buttondialogtitle") local ButtonDialogTitle = require("ui/widget/buttondialogtitle")
local Device = require("device")
local FileManagerBookInfo = require("apps/filemanager/filemanagerbookinfo") local FileManagerBookInfo = require("apps/filemanager/filemanagerbookinfo")
local InfoMessage = require("ui/widget/infomessage")
local InputContainer = require("ui/widget/container/inputcontainer") local InputContainer = require("ui/widget/container/inputcontainer")
local Menu = require("ui/widget/menu") local Menu = require("ui/widget/menu")
local ReadCollection = require("readcollection") local ReadCollection = require("readcollection")
local UIManager = require("ui/uimanager") local UIManager = require("ui/uimanager")
local Screen = require("device").screen local Screen = require("device").screen
local BaseUtil = require("ffi/util")
local util = require("util")
local _ = require("gettext") local _ = require("gettext")
local T = require("ffi/util").template
local FileManagerCollection = InputContainer:extend{ local FileManagerCollection = InputContainer:extend{
coll_menu_title = _("Favorites"), 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{ self.collfile_dialog = ButtonDialogTitle:new{
title = item.text:match("([^/]+)$"), title = item.text:match("([^/]+)$"),
title_align = "center", title_align = "center",

@ -142,7 +142,7 @@ function Kindle:usbPlugIn()
-- NOTE: We cannot support running in USBMS mode (we cannot, we live on the partition being exported!). -- 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... -- 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. -- 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 -- 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)! -- 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. -- On the upside, we don't have to bother waking up the WM to show us the USBMS screen :D.

@ -67,7 +67,7 @@ function SysfsLight:setNaturalBrightness(brightness, warmth)
-- Newer devices use a mixer instead of writting values per color. -- Newer devices use a mixer instead of writting values per color.
if self.frontlight_mixer then 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) warmth = math.floor(warmth / self.nl_max)
if set_brightness then if set_brightness then
-- Prefer the ioctl, as it's much lower latency. -- Prefer the ioctl, as it's much lower latency.

@ -882,6 +882,9 @@ function CreDocument:register(registry)
registry:addProvider("rtf", "application/rtf", self, 90) registry:addProvider("rtf", "application/rtf", self, 90)
registry:addProvider("xhtml", "application/xhtml+xml", self, 90) registry:addProvider("xhtml", "application/xhtml+xml", self, 90)
registry:addProvider("zip", "application/zip", self, 10) 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 end
-- Optimise usage of some of the above methods by caching their results, -- Optimise usage of some of the above methods by caching their results,

@ -491,15 +491,15 @@ the second parameter (refreshtype) can either specify a refreshtype
or a function that returns refreshtype AND refreshregion and is called or a function that returns refreshtype AND refreshregion and is called
after painting the widget. after painting the widget.
Here's a quick rundown of what each refreshtype should be used for: 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. Highest quality, but highest latency.
Don't abuse if you only want a flash (in this case, prefer flashpartial or flashui). 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. Can be promoted to flashing after FULL_REFRESH_COUNT refreshes.
Don't abuse to avoid spurious flashes. 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. 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. Should apply to most highlighting effects achieved through inversion.
Note that if your highlighted element contains text, Note that if your highlighted element contains text,
you might want to keep the unhighlight refresh as "ui" instead, for crisper 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, -- 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 -- 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 -- 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 for i = 1, #self._refresh_stack do
-- check for collision with refreshes that are already enqueued -- check for collision with refreshes that are already enqueued
if region:intersectWith(self._refresh_stack[i].region) then if region:intersectWith(self._refresh_stack[i].region) then

@ -363,7 +363,7 @@ function ImageViewer:update()
file = self.file, file = self.file,
image = self.image, image = self.image,
image_disposable = false, -- we may re-use 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, width = max_image_w,
height = max_image_h, height = max_image_h,
rotation_angle = rotation_angle, rotation_angle = rotation_angle,

@ -661,6 +661,33 @@ function util.getFileNameSuffix(file)
return suffix return suffix
end 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 --- Gets human friendly size as string
---- @int size (bytes) ---- @int size (bytes)
---- @bool right_align (by padding with spaces on the left) ---- @bool right_align (by padding with spaces on the left)

@ -41,7 +41,7 @@ describe("FileManager module", function()
root_path = "../../test", 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) util.copyFile("../../test/2col.pdf", tmp_fn)
local tmp_sidecar = docsettings:getSidecarDir(util.realpath(tmp_fn)) local tmp_sidecar = docsettings:getSidecarDir(util.realpath(tmp_fn))

Loading…
Cancel
Save