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.
reviewable/pr5811/r1
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 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()

@ -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",

@ -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.

@ -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.

@ -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,

@ -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

@ -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,

@ -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)

@ -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))

Loading…
Cancel
Save