Skip to content

Commit

Permalink
Allow running shell scripts from the FileManager/Favorites (koreader#…
Browse files Browse the repository at this point in the history
…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.
  • Loading branch information
NiLuJe authored and mwoz123 committed Mar 29, 2020
1 parent 1b4995b commit bb4273a
Show file tree
Hide file tree
Showing 9 changed files with 144 additions and 38 deletions.
93 changes: 65 additions & 28 deletions frontend/apps/filemanager/filemanager.lua
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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"),
Expand All @@ -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, {
{
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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)
Expand All @@ -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()
Expand Down Expand Up @@ -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,
Expand All @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -880,20 +917,20 @@ 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)),
})
end
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
Expand Down Expand Up @@ -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]
)
Expand Down Expand Up @@ -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]
)
Expand Down Expand Up @@ -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()
Expand Down
39 changes: 39 additions & 0 deletions 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"),
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion frontend/device/kindle/device.lua
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion frontend/device/sysfs_light.lua
Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions frontend/document/credocument.lua
Expand Up @@ -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,
Expand Down
12 changes: 6 additions & 6 deletions frontend/ui/uimanager.lua
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion frontend/ui/widget/imageviewer.lua
Expand Up @@ -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,
Expand Down

0 comments on commit bb4273a

Please sign in to comment.