Terminal emulator: full rewrite, real vt52 emulator (#8636)

New real terminal emulator, replacing the old plugin.
The emulator is basically a vt52 terminal (enriched with
some ANSI-sequences, as ash, vi and mksh don't behave well
on a vt52 term).
So far working: ash, mksh, bash, nano, vi, busybox, watch...
The input supports: tab-completion; cursor movement;
backspace; start of line, end of line (long press);
page up, page down (long press).
User scripts may be placed in the koterm.koplugin/scripts/
folder, aliases can be put in the file aliases and startup
command in the file profile.user in that folder.
reviewable/pr8742/r1
zwim 2 years ago committed by GitHub
parent 943dc99645
commit f2557a7aa6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -5,7 +5,7 @@ CI_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${CI_DIR}/common.sh"
# shellcheck disable=2016
mapfile -t shellscript_locations < <({ git grep -lE '^#!(/usr)?/bin/(env )?(bash|sh)' && git submodule --quiet foreach '[ "$path" = "base" -o "$path" = "platform/android/luajit-launcher" ] || git grep -lE "^#!(/usr)?/bin/(env )?(bash|sh)" | sed "s|^|$path/|"' && git ls-files ./*.sh; } | sort | uniq)
mapfile -t shellscript_locations < <({ git grep -lE '^#!(/usr)?/bin/(env )?(bash|sh)' | sed "/^plugins\/terminal.koplugin\/shfm$/d" && git submodule --quiet foreach '[ "$path" = "base" -o "$path" = "platform/android/luajit-launcher" ] || git grep -lE "^#!(/usr)?/bin/(env )?(bash|sh)" | sed "s|^|$path/|"' && git ls-files ./*.sh; } | sort | uniq)
SHELLSCRIPT_ERROR=0
SHFMT_OPTIONS="-i 4 -ci"

@ -128,6 +128,8 @@ local InputDialog = InputContainer:new{
input_type = nil,
deny_keyboard_hiding = false, -- don't hide keyboard on tap outside
enter_callback = nil,
strike_callback = nil, -- call this on every keystroke (used by Terminal plugin's TermInputText)
inputtext_class = InputText, -- (Terminal plugin provides TermInputText)
readonly = false, -- don't allow editing, will not show keyboard
allow_newline = false, -- allow entering new lines (this disables any enter_callback)
cursor_at_end = true, -- starts with cursor at end of text, ready for appending
@ -279,7 +281,7 @@ function InputDialog:init()
if not self.text_height or self.fullscreen then
-- We need to find the best height to avoid screen overflow
-- Create a dummy input widget to get some metrics
local input_widget = InputText:new{
local input_widget = self.inputtext_class:new{
text = self.fullscreen and "-" or self.input,
input_type = self.input_type,
face = self.input_face,
@ -330,7 +332,7 @@ function InputDialog:init()
-- (will work in case of re-init as these are saved by onClose()
self._top_line_num, self._charpos = self.view_pos_callback()
end
self._input_widget = InputText:new{
self._input_widget = self.inputtext_class:new{
text = self.input,
hint = self.input_hint,
face = self.input_face,
@ -356,6 +358,7 @@ function InputDialog:init()
end
end
end,
strike_callback = self.strike_callback,
edit_callback = self._buttons_edit_callback, -- nil if no Save/Close buttons
scroll_callback = self._buttons_scroll_callback, -- nil if no Nav or Scroll buttons
scroll = true,

@ -61,6 +61,7 @@ local KeyValueItem = InputContainer:new{
-- "right": only align right if value overflow 1/2 width
-- "right_always": align value right even when small and
-- only key overflows 1/2 width
close_callback = nil,
}
function KeyValueItem:init()
@ -650,6 +651,9 @@ end
function KeyValuePage:onClose()
UIManager:close(self)
if self.close_callback then
self.close_callback()
end
return true
end

@ -131,8 +131,16 @@ function VirtualKey:init()
end
elseif self.label == "" then
self.callback = function() self.keyboard:upLine() end
self.hold_callback = function()
self.ignore_key_release = true
self.keyboard:scrollUp()
end
elseif self.label == "" then
self.callback = function() self.keyboard:downLine() end
self.hold_callback = function()
self.ignore_key_release = true
self.keyboard:scrollDown()
end
else
self.callback = function () self.keyboard:addChar(self.key) end
self.hold_callback = function()
@ -1033,10 +1041,18 @@ function VirtualKeyboard:upLine()
self.inputbox:upLine()
end
function VirtualKeyboard:scrollUp()
self.inputbox:scrollUp()
end
function VirtualKeyboard:downLine()
self.inputbox:downLine()
end
function VirtualKeyboard:scrollDown()
self.inputbox:scrollDown()
end
function VirtualKeyboard:clear()
logger.dbg("clear input")
self.inputbox:clear()

@ -1,6 +1,6 @@
local _ = require("gettext")
return {
name = "terminal",
fullname = _("Terminal"),
description = _([[Executes simple commands and shows their output.]]),
fullname = _("Terminal emulator"),
description = _([[KOReader's terminal emulator]]),
}

@ -0,0 +1,169 @@
local KeyValuePage = require("ui/widget/keyvaluepage")
local InfoMessage = require("ui/widget/infomessage")
local MultiInputDialog = require("ui/widget/multiinputdialog")
local UIManager = require("ui/uimanager")
local _ = require("gettext")
local util = require("util")
local T = require("ffi/util").template
local Aliases = {
filename = "",
close_callback = nil,
parent = nil,
alias_kv = nil,
kv_pairs = {},
}
function Aliases:show(filename, close_callback, parent)
self.filename = filename
self.close_callback = close_callback
self.parent = parent
self:load()
self.alias_kv = KeyValuePage:new{
title = "Aliases (Shortcuts)",
kv_pairs = self.kv_pairs,
close_callback = self.close_callback,
}
UIManager:show(self.alias_kv)
end
function Aliases:updateKeyValues()
self.alias_kv.kv_pairs = self.kv_pairs
UIManager:close(self.alias_kv)
self.alias_kv = KeyValuePage:new{
title = "Aliases (Shortcuts)",
kv_pairs = self.kv_pairs,
close_callback = self.close_callback,
}
UIManager:show(self.alias_kv)
end
function Aliases:load()
local file = io.open(self.filename, "r")
self.kv_pairs = {}
if file then
for line in file:lines() do
line = line:gsub("^ *alias *", "") -- drop alias
local dummy, separator = line:find("^[%a%d][%a%d-_]*%=") -- find separator
if line ~= "" and line:sub(1, 1) ~= "#" and separator then
local alias_name = line:sub(1, separator - 1)
local alias_command = line:sub(separator + 1):gsub("\"", "")
table.insert(self.kv_pairs, {alias_name, alias_command,
callback = function() self.editAlias(self, alias_name, alias_command) end
})
end
end
file:close()
table.sort(self.kv_pairs, function(a, b) return a[1] < b[1] end)
end
table.insert(self.kv_pairs, 1,
{ _("Create a new alias"), "", callback = function() self.editAlias(self, "", "") end})
table.insert(self.kv_pairs, 2, "---")
end
function Aliases:save()
local file = io.open(self.filename .. ".new", "w")
if not file then
UIManager:show(InfoMessage:new{
text = T(_("Terminal emulator: error saving: %1"), self.filename)
})
end
file:write("# Aliases generated by terminal emulator\n\n")
for i = 3, #self.kv_pairs do
file:write("alias " .. self.kv_pairs[i][1] .. "=\"" .. self.kv_pairs[i][2] .. "\"\n")
end
file:close()
os.remove(self.filename)
os.rename(self.filename .. ".new", self.filename)
end
function Aliases:editAlias(alias_name, alias_command)
local alias_input
alias_input = MultiInputDialog:new{
title = _("Edit alias"),
fields = {
{
description = _("Alias name:"),
text = alias_name,
},
{
description = _("Alias command:"),
text = alias_command,
},
},
buttons = {
{
{
text = _("Cancel"),
callback = function()
UIManager:close(alias_input)
end
},
{
text = _("Delete"),
callback = function()
UIManager:close(alias_input)
for i, v in pairs(self.kv_pairs) do
if v[1] == alias_name then
table.remove(self.kv_pairs, i)
self.parent:transmit("unalias " .. alias_name .."\n")
end
end
self:save()
self:updateKeyValues()
end
},
{
text = _("Save"),
callback = function()
local fields = MultiInputDialog:getFields()
local name = fields[1] and util.trim(fields[1])
local value = fields[2] and util.trim(fields[2])
if name ~= "" and value ~= "" then
UIManager:close(alias_input)
for i, v in pairs(self.kv_pairs) do
if v[1] == alias_name then
table.remove(self.kv_pairs, i)
self.parent:transmit("unalias " .. alias_name .."\n")
end
end
self.parent:transmit("alias " .. name .. "='" .. value .. "'\n")
table.insert(self.kv_pairs, {name, value,
callback = function()
self.editAlias(self, name, value)
end
})
table.remove(self.kv_pairs, 2)
table.remove(self.kv_pairs, 1)
table.sort(self.kv_pairs, function(a, b) return a[1] < b[1] end)
table.insert(self.kv_pairs, 1, { _("Create a new alias"), "",
callback = function() self:editAlias(self, "", "") end })
table.insert(self.kv_pairs, 2, "---")
self:save()
self:updateKeyValues()
end
end
},
{
text = _("Execute"),
callback = function()
local fields = MultiInputDialog:getFields()
local value = fields[2] and util.trim(fields[2])
if value ~= "" then
UIManager:close(alias_input)
self.alias_kv:onClose()
self.parent:transmit(value .. "\n")
end
end
},
},
},
}
UIManager:show(alias_input)
alias_input:onShowKeyboard()
end
return Aliases

@ -1,501 +1,567 @@
local ButtonDialog = require("ui/widget/buttondialog")
local CenterContainer = require("ui/widget/container/centercontainer")
local DataStorage = require("datastorage")
--[[
This plugin provides a terminal emulator (VT52 (+some ANSI))
]]
local Device = require("device")
-- grantpt and friends are necessary (introduced on Android in API 21).
-- So sorry for the Tolinos with (Android 4.4.x).
-- Maybe https://f-droid.org/de/packages/jackpal.androidterm/ could be an alternative then.
if Device:isAndroid() and Device.firmware_rev < 21 then
return
end
local Aliases = require("aliases")
local Dispatcher = require("dispatcher")
local DataStorage = require("datastorage")
local Font = require("ui/font")
local InfoMessage = require("ui/widget/infomessage")
local InputDialog = require("ui/widget/inputdialog")
local LuaSettings = require("luasettings")
local Menu = require("ui/widget/menu")
local TextViewer = require("ui/widget/textviewer")
local Trapper = require("ui/trapper")
local MultiConfirmBox = require("ui/widget/multiconfirmbox")
local ScrollTextWidget = require("ui/widget/scrolltextwidget")
local SpinWidget = require("ui/widget/spinwidget")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local TermInputText = require("terminputtext")
local TextWidget = require("ui/widget/textwidget")
local bit = require("bit")
local lfs = require("libs/libkoreader-lfs")
local logger = require("logger")
local util = require("ffi/util")
local _ = require("gettext")
local N_ = _.ngettext
local Screen = require("device").screen
local T = util.template
local T = require("ffi/util").template
local ffi = require("ffi")
local C = ffi.C
-- for terminal emulator
ffi.cdef[[
static const int SIGTERM = 15;
int grantpt(int fd) __attribute__((nothrow, leaf));
int unlockpt(int fd) __attribute__((nothrow, leaf));
char *ptsname(int fd) __attribute__((nothrow, leaf));
pid_t setsid(void) __attribute__((nothrow, leaf));
static const int TCIFLUSH = 0;
int tcdrain(int fd) __attribute__((nothrow, leaf));
int tcflush(int fd, int queue_selector) __attribute__((nothrow, leaf));
]]
local CHUNK_SIZE = 80 * 40 -- max. nb of read bytes (reduce this, if taps are not detected)
local Terminal = WidgetContainer:new{
name = "terminal",
command = "",
dump_file = util.realpath(DataStorage:getDataDir()) .. "/terminal_output.txt",
items_per_page = 16,
settings = LuaSettings:open(DataStorage:getSettingsDir() .. "/terminal_shortcuts.lua"),
shortcuts_dialog = nil,
shortcuts_menu = nil,
-- shortcuts_file = DataStorage:getSettingsDir() .. "/terminal_shortcuts.lua",
shortcuts = {},
source = "terminal",
history = "",
is_shell_open = false,
buffer_size = 1024 * G_reader_settings:readSetting("terminal_buffer_size", 16), -- size in kB
refresh_time = 0.2,
terminal_data = ".",
}
function Terminal:onDispatcherRegisterActions()
Dispatcher:registerAction("show_terminal", { category = "none", event = "TerminalStart", title = _("Show terminal"), general=true, })
end
function Terminal:init()
self:onDispatcherRegisterActions()
self.ui.menu:registerToMainMenu(self)
self.items_per_page = G_reader_settings:readSetting("items_per_page") or 16
self.shortcuts = self.settings:readSetting("shortcuts", {})
end
function Terminal:saveShortcuts()
self.settings:flush()
UIManager:show(InfoMessage:new{
text = _("Shortcuts saved"),
timeout = 2
})
self.chunk_size = CHUNK_SIZE
self.chunk = ffi.new('uint8_t[?]', self.chunk_size)
self.terminal_data = DataStorage:getDataDir()
lfs.mkdir(self.terminal_data .. "/scripts")
os.remove("terminal.pid") -- clean leftover from last run
end
function Terminal:manageShortcuts()
self.shortcuts_dialog = CenterContainer:new {
dimen = Screen:getSize(),
}
self.shortcuts_menu = Menu:new{
show_parent = self.ui,
width = Screen:getWidth(),
height = Screen:getHeight(),
covers_fullscreen = true, -- hint for UIManager:_repaint()
is_borderless = true,
is_popout = false,
perpage = self.items_per_page,
onMenuHold = self.onMenuHoldShortcuts,
_manager = self,
}
table.insert(self.shortcuts_dialog, self.shortcuts_menu)
self.shortcuts_menu.close_callback = function()
UIManager:close(self.shortcuts_dialog)
function Terminal:spawnShell(cols, rows)
if self.is_shell_open then
self.input_widget:resize(rows, cols)
self.input_widget:interpretAnsiSeq(self:receive())
return
end
-- sort the shortcuts:
if #self.shortcuts > 0 then
table.sort(self.shortcuts, function(v1, v2)
return v1.text < v2.text
end)
local shell = G_reader_settings:readSetting("terminal_shell", "sh")
local ptmx_name = "/dev/ptmx"
self.ptmx = C.open(ptmx_name, bit.bor(C.O_RDWR, C.O_NONBLOCK, C.O_CLOEXEC))
if C.grantpt(self.ptmx) ~= 0 then
logger.err("Terminal: can not grantpt")
end
self:updateItemTable()
if C.unlockpt(self.ptmx) ~= 0 then
logger.err("Terminal: can not unockpt")
end
function Terminal:updateItemTable()
local item_table = {}
if #self.shortcuts > 0 then
local actions_count = 3 -- separator + actions
for nr, f in ipairs(self.shortcuts) do
local item = {
nr = nr,
text = f.text,
commands = f.commands,
editable = true,
deletable = true,
callback = function()
-- so we know which middle button to display in the results:
self.source = "shortcut"
-- execute immediately, skip terminal dialog:
self.command = self:ensureWhitelineAfterCommands(f.commands)
Trapper:wrap(function()
self:execute()
end)
self.slave_pty = ffi.string(C.ptsname(self.ptmx))
logger.info("Terminal: slave_pty", self.slave_pty)
local pid = C.fork()
if pid < 0 then
logger.err("Terminal: fork failed")
return
elseif pid == 0 then
C.close(self.ptmx)
C.setsid()
pid = C.getpid()
local pid_file = io.open("terminal.pid", "w")
if pid_file then
pid_file:write(pid)
pid_file:close()
end
}
table.insert(item_table, item)
-- add page actions at end of each page with shortcuts:
local factor = self.items_per_page - actions_count
if nr % factor == 0 or nr == #self.shortcuts then
-- insert "separator":
table.insert(item_table, {
text = " ",
deletable = false,
editable = false,
callback = function()
self:manageShortcuts()
end,
})
-- actions:
self:insertPageActions(item_table)
local pts = C.open(self.slave_pty, C.O_RDWR)
if pts == -1 then
logger.err("Terminal: cannot open slave pty: ", pts)
return
end
C.dup2(pts, 0);
C.dup2(pts, 1);
C.dup2(pts, 2);
C.close(pts);
if cols and rows then
if not Device:isAndroid() then
os.execute("stty cols " .. cols .. " rows " .. rows)
end
-- no shortcuts defined yet:
else
self:insertPageActions(item_table)
end
local title = N_("Terminal shortcut", "Terminal shortcuts", #self.shortcuts)
self.shortcuts_menu:switchItemTable(tostring(#self.shortcuts) .. " " .. title, item_table)
UIManager:show(self.shortcuts_dialog)
C.setenv("TERM", "vt52", 1)
C.setenv("ENV", "./plugins/terminal.koplugin/profile", 1)
C.setenv("BASH_ENV", "./plugins/terminal.koplugin/profile", 1)
C.setenv("TERMINAL_DATA", self.terminal_data, 1)
if Device:isAndroid() then
C.setenv("ANDROID", "ANDROID", 1)
end
if C.execlp(shell, shell) ~= 0 then
-- the following two prints are shown in the terminal emulator.
print("Terminal: something has gone really wrong in spawning the shell\n\n:-(\n")
print("Maybe an incorrect shell: '" .. shell .. "'\n")
os.exit()
end
os.exit()
return
end
function Terminal:insertPageActions(item_table)
table.insert(item_table, {
text = " " .. _("to terminal…"),
deletable = false,
editable = false,
callback = function()
self:terminal()
end,
})
table.insert(item_table, {
text = " " .. _("close…"),
deletable = false,
editable = false,
callback = function()
return false
end,
})
self.is_shell_open = true
if Device:isAndroid() then
-- feed the following commands to the running shell
self:transmit("export TERM=vt52\n")
self:transmit("stty cols " .. cols .. " rows " .. rows .."\n")
end
function Terminal:onMenuHoldShortcuts(item)
if item.deletable or item.editable then
local shortcut_shortcuts_dialog
shortcut_shortcuts_dialog = ButtonDialog:new{
buttons = {{
{
text = _("Edit name"),
enabled = item.editable,
callback = function()
UIManager:close(shortcut_shortcuts_dialog)
if self._manager.shortcuts_dialog ~= nil then
UIManager:close(self._manager.shortcuts_dialog)
self._manager.shortcuts_dialog = nil
self.input_widget:resize(rows, cols)
self.input_widget:interpretAnsiSeq(self:receive())
logger.info("Terminal: spawn done")
end
self._manager:editName(item)
function Terminal:receive()
local last_result = ""
repeat
C.tcdrain(self.ptmx)
local count = tonumber(C.read(self.ptmx, self.chunk, self.chunk_size))
if count > 0 then
last_result = last_result .. string.sub(ffi.string(self.chunk), 1, count)
end
},
{
text = _("Edit commands"),
enabled = item.editable,
callback = function()
UIManager:close(shortcut_shortcuts_dialog)
if self._manager.shortcuts_dialog ~= nil then
UIManager:close(self._manager.shortcuts_dialog)
self._manager.shortcuts_dialog = nil
until count <= 0 or #last_result >= self.chunk_size - 1
return last_result
end
self._manager:editCommands(item)
function Terminal:refresh(reset)
if reset then
self.refresh_time = 1/32
UIManager:unschedule(Terminal.refresh)
end
},
},
{
{
text = _("Copy"),
enabled = item.editable,
callback = function()
UIManager:close(shortcut_shortcuts_dialog)
if self._manager.shortcuts_dialog ~= nil then
UIManager:close(self._manager.shortcuts_dialog)
self._manager.shortcuts_dialog = nil
local next_text = self:receive()
if next_text ~= "" then
self.input_widget:interpretAnsiSeq(next_text)
self.input_widget:trimBuffer(self.buffer_size)
if self.is_shell_open then
UIManager:tickAfterNext(function()
UIManager:scheduleIn(self.refresh_time, Terminal.refresh, self)
end)
end
self._manager:copyCommands(item)
else
if self.is_shell_open then
if self.refresh_time > 5 then
self.refresh_time = self.refresh_time
elseif self.refresh_time > 1 then
self.refresh_time = self.refresh_time * 1.1
else
self.refresh_time = self.refresh_time * 2
end
},
{
text = _("Delete"),
enabled = item.deletable,
callback = function()
UIManager:close(shortcut_shortcuts_dialog)
if self._manager.shortcuts_dialog ~= nil then
UIManager:close(self._manager.shortcuts_dialog)
self._manager.shortcuts_dialog = nil
UIManager:scheduleIn(self.refresh_time, Terminal.refresh, self)
end
self._manager:deleteShortcut(item)
end
}
}}
}
UIManager:show(shortcut_shortcuts_dialog)
return true
end
function Terminal:transmit(chars)
C.write(self.ptmx, chars, #chars)
self:refresh(true)
end
function Terminal:copyCommands(item)
local new_item = {
text = item.text .. " (copy)",
commands = item.commands
}
table.insert(self.shortcuts, new_item)
UIManager:show(InfoMessage:new{
text = _("Shortcut copied"),
timeout = 2
})
self:saveShortcuts()
self:manageShortcuts()
--- kills a running shell
-- @param ask if true ask if a shell is running, don't kill
-- @return pid if shell is running, -1 otherwise
function Terminal:killShell(ask)
UIManager:unschedule(Terminal.refresh)
local pid_file = io.open("terminal.pid", "r")
if not pid_file then
return -1
end
function Terminal:editCommands(item)
local edit_dialog
edit_dialog = InputDialog:new{
title = T(_('Edit commands for "%1"'), item.text),
input = item.commands,
para_direction_rtl = false, -- force LTR
input_type = "string",
allow_newline = true,
cursor_at_end = true,
fullscreen = true,
buttons = {{{
text = _("Cancel"),
callback = function()
UIManager:close(edit_dialog)
edit_dialog = nil
self:manageShortcuts()
end,
}, {
text = _("Save"),
callback = function()
local input = edit_dialog:getInputText()
UIManager:close(edit_dialog)
edit_dialog = nil
if input:match("[A-Za-z]") then
self.shortcuts[item.nr]["commands"] = input
self:saveShortcuts()
self:manageShortcuts()
local pid = tonumber(pid_file:read("*a"))
pid_file:close()
if ask then
return pid
else
local terminate = "\03\n\nexit\n"
self:transmit(terminate)
-- do other things before killing first
self.is_shell_open = false
self.history = ""
os.remove("terminal.pid")
C.close(self.ptmx)
C.kill(pid, C.SIGTERM)
local status = ffi.new('int[1]')
-- status = tonumber(status[0])
-- If still running: ret = 0 , status = 0
-- If exited: ret = pid , status = 0 or 9 if killed
-- If no more running: ret = -1 , status = 0
C.waitpid(pid, status, 0) -- wait until shell is terminated
return -1
end
end,
}}},
end
function Terminal:getCharSize()
local tmp = TextWidget:new{
text = " ",
face = self.input_face,
}
UIManager:show(edit_dialog)
edit_dialog:onShowKeyboard()
return tmp:getSize().w
end
function Terminal:editName(item)
local edit_dialog
edit_dialog = InputDialog:new{
title = _("Edit name"),
input = item.text,
para_direction_rtl = false, -- force LTR
function Terminal:generateInputDialog()
return InputDialog:new{
title = _("Terminal Emulator"),
input = self.history,
input_face = self.input_face,
para_direction_rtl = false,
input_type = "string",
allow_newline = false,
cursor_at_end = true,
fullscreen = true,
buttons = {{{
text = _("Cancel"),
inputtext_class = TermInputText,
buttons = {{
{
text = "", -- tabulator "⇤" and "⇥"
callback = function()
UIManager:close(edit_dialog)
edit_dialog = nil
self:manageShortcuts()
self:transmit("\009")
end,
}, {
text = _("Save"),
},
{
text = "/", -- slash
callback = function()
local input = edit_dialog:getInputText()
UIManager:close(edit_dialog)
edit_dialog = nil
if input:match("[A-Za-z]") then
self.shortcuts[item.nr]["text"] = input
self:saveShortcuts()
self:manageShortcuts()
end
self:transmit("/")
end,
}}},
}
UIManager:show(edit_dialog)
edit_dialog:onShowKeyboard()
end
function Terminal:deleteShortcut(item)
for i = #self.shortcuts, 1, -1 do
local element = self.shortcuts[i]
if element.text == item.text and element.commands == item.commands then
table.remove(self.shortcuts, i)
end
end
self:saveShortcuts()
self:manageShortcuts()
end
function Terminal:onTerminalStart()
-- if shortcut commands are defined, go directly to the the shortcuts manager (so we can execute scripts more quickly):
if #self.shortcuts == 0 then
self:terminal()
else
self:manageShortcuts()
end
end
function Terminal:terminal()
self.input = InputDialog:new{
title = _("Enter a command and press \"Execute\""),
input = self.command:gsub("\n+$", ""),
para_direction_rtl = false, -- force LTR
input_type = "string",
allow_newline = true,
cursor_at_end = true,
fullscreen = true,
buttons = {{{
text = _("Cancel"),
},
{
text = _("Esc"), -- @translators This is the ESC-key on the keyboard.
callback = function()
UIManager:close(self.input)
self:transmit("\027")
end,
}, {
text = _("Shortcuts"),
},
{
text = _("Ctrl"), -- @translators This is the CTRL-key on the keyboard.
callback = function()
UIManager:close(self.input)
self:manageShortcuts()
self.ctrl = true
end,
}, {
text = _("Save"),
},
{
text = _("Ctrl-C"), -- @translators This is the CTRL-C key combination.
callback = function()
local input = self.input:getInputText()
if input:match("[A-Za-z]") then
local function callback(name)
local new_shortcut = {
text = name,
commands = input,
}
table.insert(self.shortcuts, new_shortcut)
self:saveShortcuts()
self:transmit("\003")
-- consume and drop everything
C.tcflush(self.ptmx, C.TCIFLUSH)
while self:receive() ~= "" do
C.tcflush(self.ptmx, C.TCIFLUSH)
end
local prompt
prompt = InputDialog:new{
title = _("Name"),
input = "",
input_type = "text",
fullscreen = true,
condensed = true,
allow_newline = false,
cursor_at_end = true,
buttons = {{{
text = _("Cancel"),
self.input_widget:addChars("\003", true) -- as we flush the queue
end,
},
{
text = "", --clear
callback = function()
UIManager:close(prompt)
self.history = ""
self.input = {}
self.input_dialog:setInputText("$ ")
end,
},
{
text = _("Save"),
is_enter_default = true,
text = "",
callback = function()
local newval = prompt:getInputText()
UIManager:close(prompt)
callback(newval)
self.input_widget:upLine()
end,
}}}
}
UIManager:show(prompt)
prompt:onShowKeyboard()
end
hold_callback = function()
self.input_widget:scrollUp()
end,
}, {
text = _("Execute"),
},
{
text = "",
callback = function()
UIManager:close(self.input)
-- so we know which middle button to display in the results:
self.source = "terminal"
self.command = self:ensureWhitelineAfterCommands(self.input:getInputText())
Trapper:wrap(function()
self:execute()
end)
self.input_widget:downLine()
end,
}}},
}
UIManager:show(self.input)
self.input:onShowKeyboard()
hold_callback = function()
self.input_widget:scrollDown()
end,
},
{
text = "", -- settings menu
callback = function ()
UIManager:close(self.input_widget.keyboard)
Aliases:show(self.terminal_data .. "/scripts/aliases",
function()
UIManager:show(self.input_widget.keyboard)
UIManager:setDirty(self.input_dialog, "fast") -- is there a better solution
end,
self)
end,
},
{
text = "", --cancel
callback = function()
UIManager:show(MultiConfirmBox:new{
text = _("You can close the terminal, but leave the shell open for further commands or quit it now."),
choice1_text = _("Close"),
choice1_callback = function()
self.history = self.input_dialog:getInputText()
-- trim trialing spaces and newlines
while self.history:sub(#self.history, #self.history) == "\n"
or self.history:sub(#self.history, #self.history) == " " do
self.history = self.history:sub(1, #self.history - 1)
end
-- for prettier formatting of output by separating commands and result thereof with a whiteline:
function Terminal:ensureWhitelineAfterCommands(commands)
if string.sub(commands, -1) ~= "\n" then
commands = commands .. "\n"
UIManager:close(self.input_dialog)
if self.touchmenu_instance then
self.touchmenu_instance:updateItems()
end
return commands
end,
choice2_text = _("Quit"),
choice2_callback = function()
self.history = ""
self:killShell()
UIManager:close(self.input_dialog)
if self.touchmenu_instance then
self.touchmenu_instance:updateItems()
end
function Terminal:execute()
local wait_msg = InfoMessage:new{
text = _("Executing…"),
}
UIManager:show(wait_msg)
local entries = { self.command }
local command = self.command .. " 2>&1 ; echo" -- ensure we get stderr and output something
local completed, result_str = Trapper:dismissablePopen(command, wait_msg)
if completed then
table.insert(entries, result_str)
self:dump(entries)
table.insert(entries, _("Output was also written to"))
table.insert(entries, self.dump_file)
else
table.insert(entries, _("Execution canceled."))
end,
})
end,
},
}},
enter_callback = function()
self:transmit("\r")
end,
strike_callback = function(chars)
if self.ctrl and #chars == 1 then
chars = string.char(chars:upper():byte() - ("A"):byte()+1)
self.ctrl = false
end
UIManager:close(wait_msg)
local viewer
local buttons_table
local back_button = {
text = _("Back"),
callback = function()
UIManager:close(viewer)
if self.source == "terminal" then
self:terminal()
else
self:manageShortcuts()
if chars == "\n" then
chars = "\r\n"
end
self:transmit(chars)
end,
}
local close_button = {
text = _("Close"),
end
function Terminal:onClose()
self:killShell()
end
function Terminal:onTerminalStart(touchmenu_instance)
self.touchmenu_instance = touchmenu_instance
self.input_face = Font:getFace("smallinfont",
G_reader_settings:readSetting("terminal_font_size", 14))
self.ctrl = false
self.input_dialog = self:generateInputDialog()
self.input_widget = self.input_dialog._input_widget
local scroll_bar_width = ScrollTextWidget.scroll_bar_width
+ ScrollTextWidget.text_scroll_span
self.maxc = math.floor((self.input_widget.width - scroll_bar_width) / self:getCharSize())
self.maxr = math.floor(self.input_widget.height
/ self.input_widget:getLineHeight())
self.store_position = 1
logger.dbg("Terminal: resolution= " .. self.maxc .. "x" .. self.maxr)
self:spawnShell(self.maxc, self.maxr)
UIManager:show(self.input_dialog)
UIManager:scheduleIn(0.25, Terminal.refresh, self, true)
self.input_dialog:onShowKeyboard(true)
end
function Terminal:addToMainMenu(menu_items)
menu_items.terminal = {
text = _("Terminal emulator"),
-- sorting_hint = "more_tools",
keep_menu_open = true,
sub_item_table = {
{
text = _("About terminal emulator"),
callback = function()
UIManager:close(viewer)
local about_text = _([[Terminal emulator can start a shell (command prompt).
There are two environment variables TERMINAL_HOME and TERMINAL_DATA containing the path of the install and the data folders.
Commands to be executed on start can be placed in:
'$TERMINAL_DATA/scripts/profile.user'.
Aliases (shortcuts) to frequently used commands can be placed in:
'$TERMINAL_DATA/scripts/aliases'.]])
if not Device:isAndroid() then
about_text = about_text .. _("\n\nYou can use 'shfm' as a file manager, '?' shows shfms help message.")
end
UIManager:show(InfoMessage:new{
text = about_text,
})
end,
}
if self.source == "terminal" then
buttons_table = {
keep_menu_open = true,
separator = true,
},
{
back_button,
text_func = function()
local state = self.is_shell_open and "running" or "not running"
return T(_("Open terminal session (%1)"), state)
end,
callback = function(touchmenu_instance)
self:onTerminalStart(touchmenu_instance)
end,
keep_menu_open = true,
},
{
text = _("Shortcuts"),
-- switch to shortcuts:
callback = function()
UIManager:close(viewer)
self:manageShortcuts()
text = _("End terminal session"),
enabled_func = function()
return self:killShell(true) >= 0
end,
callback = function(touchmenu_instance)
self:killShell()
if touchmenu_instance then touchmenu_instance:updateItems() end
end,
keep_menu_open = true,
separator = true,
},
close_button,
{
text_func = function()
return T(_("Font size: %1"),
G_reader_settings:readSetting("terminal_font_size", 14))
end,
callback = function(touchmenu_instance)
local cur_size = G_reader_settings:readSetting("terminal_font_size")
local size_spin = SpinWidget:new{
value = cur_size,
value_min = 10,
value_max = 30,
value_hold_step = 2,
default_value = 14,
title_text = _("Terminal emulator font size "),
callback = function(spin)
G_reader_settings:saveSetting("terminal_font_size", spin.value)
if touchmenu_instance then touchmenu_instance:updateItems() end
end,
}
UIManager:show(size_spin)
end,
keep_menu_open = true,
},
{
text_func = function()
return T(_("Buffer size: %1 kB"),
G_reader_settings:readSetting("terminal_buffer_size", 16))
end,
callback = function(touchmenu_instance)
local cur_buffer = G_reader_settings:readSetting("terminal_buffer_size")
local buffer_spin = SpinWidget:new{
value = cur_buffer,
value_min = 10,
value_max = 30,
value_hold_step = 2,
default_value = 16,
title_text = _("Terminal emulator buffer size (kB)"),
callback = function(spin)
G_reader_settings:saveSetting("terminal_buffer_size", spin.value)
if touchmenu_instance then touchmenu_instance:updateItems() end
end,
}
else
buttons_table = {
UIManager:show(buffer_spin)
end,
keep_menu_open = true,
},
{
back_button,
text_func = function()
return T(_("Shell executable: %1"),
G_reader_settings:readSetting("terminal_shell", "sh"))
end,
callback = function(touchmenu_instance)
self.shell_dialog = InputDialog:new{
title = _("Shell to use"),
description = _("Here you can select the startup shell.\nDefault: sh"),
input = G_reader_settings:readSetting("terminal_shell", "sh"),
buttons = {{
{
text = _("Terminal"),
-- switch to terminal:
text = _("Cancel"),
callback = function()
UIManager:close(viewer)
self:terminal()
UIManager:close(self.shell_dialog)
end,
},
close_button,
},
}
{
text = _("Default"),
callback = function()
G_reader_settings:saveSetting("terminal_shell", "sh")
UIManager:close(self.shell_dialog)
if touchmenu_instance then
touchmenu_instance:updateItems()
end
viewer = TextViewer:new{
title = _("Command output"),
text = table.concat(entries, "\n"),
justified = false,
text_face = Font:getFace("smallinfont"),
buttons_table = buttons_table,
}
UIManager:show(viewer)
end,
},
{
text = _("Save"),
is_enter_default = true,
callback = function()
local new_shell = self.shell_dialog:getInputText()
if new_shell == "" then
new_shell = "sh"
end
function Terminal:dump(entries)
local content = table.concat(entries, "\n")
local file = io.open(self.dump_file, "w")
if file then
file:write(content)
file:close()
else
logger.warn("Failed to dump terminal output " .. content .. " to " .. self.dump_file)
G_reader_settings:saveSetting("terminal_shell", new_shell)
UIManager:close(self.shell_dialog)
if touchmenu_instance then
touchmenu_instance:updateItems()
end
end
function Terminal:addToMainMenu(menu_items)
menu_items.terminal = {
text = _("Terminal emulator"),
keep_menu_open = true,
callback = function()
self:onTerminalStart()
},
}}}
UIManager:show(self.shell_dialog)
self.shell_dialog:onShowKeyboard()
end,
keep_menu_open = true,
},
}
}
end
function Terminal:onDispatcherRegisterActions()
Dispatcher:registerAction("terminal",
{category = "none", event = "TerminalStart", title = _("Terminal Emulator"), device = true})
end
return Terminal

@ -0,0 +1,53 @@
#!/bin/sh
# This file gets executed on every start of the terminal shell.
# Do not edit this file!
# You may change the contents of 'aliases' and 'profile.user'
#
# shellcheck source=/dev/null
TERMINAL_HOME="$(pwd)"
# make TERMINAL_DATA an absolute path
cd "${TERMINAL_DATA}" || exit
TERMINAL_DATA="$(pwd)"
HOME="${TERMINAL_DATA}"
PATH="${PATH}:${TERMINAL_DATA}/scripts:${TERMINAL_HOME}/plugins/terminal.koplugin/"
TERM="vt52"
PS1="$ "
if [ "$(id -u)" -eq "0" ]; then
PS1="# "
fi
EDITOR="nano"
SHFM_OPENER="${TERMINAL_HOME}/plugins/terminal.koplugin/shfm_opener.sh"
export EDITOR
export HOME
export TERMINAL_HOME
export TERMINAL_DATA
export PATH
export PS1
export SHFM_OPENER
export TERM
# Source a user profile if it exists.
USER_PROFILE="${TERMINAL_DATA}/scripts/profile.user"
if [ -f "${USER_PROFILE}" ]; then
. "${USER_PROFILE}"
fi
# Source command aliases if they exist.
# These will be managed by terminal.koplugin so you don't need
# to change it manually.
ALIASES="${TERMINAL_DATA}/scripts/aliases"
if [ -f "${ALIASES}" ]; then
. "${ALIASES}"
fi
if [ -z "${ANDROID}" ]; then
echo "You can use shfm as a filemanager, ? shows help in shfm."
fi

File diff suppressed because it is too large Load Diff

@ -0,0 +1,35 @@
#!/bin/sh -e
#
# open file in application based on file extension
mime_type=$(file -bi "$1")
case "${mime_type}" in
application/x*)
./"$1"
echo "Application done, hit enter to return"
read -r
exit
;;
text/x-shellscript*)
./"$1"
echo "Shellscript done, hit enter to return"
read -r
exit
;;
esac
case "$1" in
*.sh)
sh "$1"
echo "Shellscript done, enter to return."
read -r
exit
;;
# all other files
*)
"${EDITOR:=vi}" "$1"
;;
esac

@ -0,0 +1,857 @@
--[[
module used for terminal emulator to override InputText
]]
local InputText = require("ui/widget/inputtext")
local UIManager = require("ui/uimanager")
local dbg = require("dbg")
local logger = require("logger")
local util = require("util")
local esc = "\027"
local esc_seq = {
backspace = "\008",
cursor_left = "\027[D",
cursor_right = "\027[C",
cursor_up = "\027[A",
cursor_down = "\027[B",
cursor_pos1 = "\027[7~",
cursor_end = "\027[8~",
page_up = "\027[5~",
page_down = "\027[6~",
}
local function isNum(char)
if #char ~= 1 then return end
if char:byte() >= ("0"):byte() and char:byte() <= ("9"):byte() then
return true
end
end
local function isPrintable(ch)
return ch:byte() >= 32 or ch == "\010" or ch == "\013"
end
local TermInputText = InputText:extend{
maxr = 40,
maxc = 80,
min_buffer_size = 2 * 40 * 80, -- minimal size of scrollback buffer
strike_callback = nil,
sequence_state = "",
store_pos_dec = nil,
store_pos_sco = nil,
store_position = nil, -- when entered alternate keypad
scroll_region_bottom = nil,
scroll_region_top = nil,
scroll_region_line = nil,
wrap = true,
alternate_buffer = {},
save_buffer = {},
}
-- disable positioning cursor by tap in emulator mode
function TermInputText:onTapTextBox(arg, ges)
return true
end
function TermInputText:resize(maxr, maxc)
self.maxr = maxr
self.maxc = maxc
self.min_buffer_size = 2 * self.maxr * self.maxc
end
-- reduce the size of the buffer,
function TermInputText:trimBuffer(new_size)
if not new_size or new_size < self.min_buffer_size then
new_size = self.min_buffer_size
end
if #self.charlist > new_size then
local old_pos = self.charpos -- for adjusting saved positions
-- remove char from beginning
while #self.charlist > new_size do
table.remove(self.charlist, 1)
self.charpos = self.charpos - 1
end
-- remove the (rest) of the first line
while self.charlist[1] and self.charlist[1] ~= "\n" do
table.remove(self.charlist, 1)
self.charpos = self.charpos - 1
end
-- remove newline at the first line
if self.charlist[1] and self.charlist[1] == "\n" then
table.remove(self.charlist, 1)
self.charpos = self.charpos - 1
end
-- IMPORTANT: update stored positions if the buffer has to be trimmed
local shift_pos = old_pos - self.charpos
if self.store_position then
self.store_position = self.store_position - shift_pos
if self.store_position < 1 then
self.store_position = 1
end
end
if self.store_pos_dec then
self.store_pos_dec = self.store_pos_dec - shift_pos
if self.store_pos_dec < 1 then
self.store_pos_dec = 1
end
end
if self.store_pos_sco then
self.store_pos_sco = self.store_pos_sco - shift_pos
if self.store_pos_sco < 1 then
self.store_pos_sco = 1
end
end
-- unlikely but this could happen if the cursor is at the beginning
-- and the buffer has to be trimmed
if self.charpos < 1 then
self.charpos = 1
end
self:initTextBox(table.concat(self.charlist), true)
end
end
function TermInputText:saveBuffer(buffer)
table.insert(self[buffer],
{
self.charlist,
self.charpos,
self.store_pos_sco,
self.store_position,
self.scroll_region_bottom,
self.scroll_region_top,
self.scroll_region_line,
self.wrap,
})
self.charlist = {}
self.charpos = 1
self.store_pos_dec = nil
self.store_pos_sco = nil
self.store_position = nil
self.scroll_region_bottom = nil
self.scroll_region_top = nil
self.scroll_region_line = nil
self.wrap = true
end
function TermInputText:restoreBuffer(buffer)
local former_buffer = table.remove(self[buffer])
if type(former_buffer[1]) == "table" then
self.charlist,
self.charpos,
self.store_pos_sco,
self.store_position,
self.scroll_region_bottom,
self.scroll_region_top,
self.scroll_region_line,
self.wrap = unpack(former_buffer)
end
end
function TermInputText:_helperVT52VT100(cmd, mode, param1, param2, param3)
if cmd == "A" then -- cursor up
param1 = param1 == 0 and 1 or param1
for i = 1, param1 do
if self.scroll_region_line then
self:scrollRegionDown()
end
self:moveCursorUp(true)
end
return true
elseif cmd == "B" then -- cursor down
param1 = param1 == 0 and 1 or param1
for i = 1, param1 do
self:moveCursorDown(true)
end
return true
elseif cmd == "C" then -- cursor right
param1 = param1 == 0 and 1 or param1
for i = 1, param1 do
self:rightChar(true)
end
return true
elseif cmd == "D" then -- cursor left
param1 = param1 == 0 and 1 or param1
for i = 1, param1 do
self:leftChar(true)
end
return true
elseif cmd == "H" then -- cursor home
param1 = param1 == 0 and 1 or param1
param2 = param2 == 0 and 1 or param2
self:moveCursorToRowCol(param1, param2)
if self.scroll_region_line and param1 <= self.scroll_region_bottom
and param1 >= self.scroll_region_top then
self.scroll_region_line = param1
end
return true
elseif cmd == "J" then -- clear to end of screen
if param1 == 0 then
self:clearToEndOfScreen()
elseif param1 == 1 then
return false --- @todo not implemented
elseif param1 == 2 then
local saved_pos = self.charpos
self:moveCursorToRowCol(1, 1)
self:clearToEndOfScreen()
self.charpos = saved_pos
end
return true
elseif cmd == "K" then -- clear to end of line
self:delToEndOfLine()
return true
elseif cmd == "L" then
if self.scroll_region_line then
self:scrollRegionDown()
end
return true
elseif cmd == "h" and mode == "?" then --
--- if param2 == 25 then set cursor visible
if param2 == 7 then -- enable wrap around
self.wrap = true
elseif param2 == 47 then -- save screen
self:saveBuffer("save_buffer")
print("xxxxxxxxxxxx save screen")
elseif param2 == 1049 then -- enable alternate buffer
self:saveBuffer("alternate_buffer")
print("xxxxxxxxxxxx enable alternate buffer")
end
return true
elseif cmd == "l" and mode == "?" then --
--- if param2 == 25 then set cursor invisible
if param2 == 7 then -- enable wrap around
self.wrap = false
elseif param2 == 47 then -- restore screen
self:restoreBuffer("save_buffer")
print("xxxxxxxxxxxx restore screen")
elseif param2 == 1049 then -- disable alternate buffer
self:restoreBuffer("alternate_buffer")
print("xxxxxxxxxxxx disable alternate buffer")
end
return true
elseif cmd == "m" then
-- graphics mode not supported yet(?)
return true
elseif cmd == "n" then
--- @todo
return true
elseif cmd == "r" then
if param2 > 0 and param2 < self.maxr then
self.scroll_region_bottom = param2
else
self.scroll_region_bottom = nil
end
if self.scroll_region_bottom and param1 < self.maxr and param1 <= param2 then
self.scroll_region_top = param1
self.scroll_region_line = 1
else
self.scroll_region_bottom = nil
self.scroll_region_top = nil
self.scroll_region_line = nil
end
logger.dbg("Terminal: set scroll region", param1, param2, self.scroll_region_top, self.scroll_region_bottom, self.scroll_region_line)
return true
end
return false
end
function TermInputText:interpretAnsiSeq(text)
local pos = 1
local param1, param2, param3 = 0, 0, 0
while pos <= #text do
local next_byte = text:sub(pos, pos)
if self.sequence_state == "" then
if next_byte == esc then
self.sequence_state = "esc"
elseif isPrintable(next_byte) then
local part = next_byte
-- all bytes up to the next control sequence
while pos < #text and isPrintable(next_byte) do
next_byte = text:sub(pos+1, pos+1)
if next_byte ~= "" and pos < #text and isPrintable(next_byte) then
part = part .. next_byte
pos = pos + 1
end
end
self:addChars(part, true, true)
elseif next_byte == "\008" then
self.charpos = self.charpos - 1
end
elseif self.sequence_state == "esc" then
self.sequence_state = ""
if next_byte == "A" then -- cursor up
self:moveCursorUp(true)
elseif next_byte == "B" then -- cursor down
self:moveCursorDown(true)
elseif next_byte == "C" then -- cursor right
self:rightChar(true)
elseif next_byte == "D" then -- cursor left
self:leftChar(true)
elseif next_byte == "F" then -- enter graphics mode
logger.dbg("Terminal: enter graphics mode not supported")
elseif next_byte == "G" then -- exit graphics mod
logger.dbg("Terminal: leave graphics mode not supported")
elseif next_byte == "H" then -- cursor home
self:moveCursorToRowCol(1, 1)
elseif next_byte == "I" then -- reverse line feed (cursor up and insert line)
self:reverseLineFeed(true)
elseif next_byte == "J" then -- clear to end of screen
self:clearToEndOfScreen()
elseif next_byte == "K" then -- clear to end of line
self:delToEndOfLine()
elseif next_byte == "L" then -- insert line
logger.dbg("Terminal: insert not supported")
elseif next_byte == "M" then -- remove line
logger.dbg("Terminal: remove line not supported")
elseif next_byte == "Y" then -- set cursor pos (row, col)
self.sequence_state = "escY"
elseif next_byte == "Z" then -- ident(ify)
self.strike_callback("\027/K") -- identify as VT52 without printer
elseif next_byte == "=" then -- alternate keypad
self:enterAlternateKeypad()
elseif next_byte == ">" then -- exit alternate keypad
self:exitAlternateKeypad()
elseif next_byte == "[" then
self.sequence_state = "CSI1"
elseif next_byte == "7" then
self.store_pos_dec = self.charpos
elseif next_byte == "8" then
self.charpos = self.store_pos_dec
end
elseif self.sequence_state == "escY" then
param1 = next_byte
self.sequence_state = "escYrow"
elseif self.sequence_state == "escYrow" then
param2 = next_byte
-- row and column are offsetted with 32 (' ')
if param1 ~= 0 and param2 ~= 0 then
local row = param1 and (param1:byte() - (" "):byte() + 1) or 1
local col = param2 and (param2:byte() - (" "):byte() + 1) or 1
self:moveCursorToRowCol(row, col)
param1, param2, param3 = 0, 0, 0
end
self.sequence_state = ""
elseif self.sequence_state == "CSI1" then
if next_byte == "s" then -- save cursor pos
self.store_pos_sco = self.charpos
elseif next_byte == "u" then -- restore cursor pos
self.charpos = self.store_pos_sco
elseif next_byte == "?" then
self.sequence_mode = "?"
self.sequence_state = "escParam2"
elseif isNum(next_byte) then
param1 = param1 * 10 + next_byte:byte() - ("0"):byte()
else
if next_byte == ";" then
self.sequence_state = "escParam2"
else
pos = pos - 1
self.sequence_state = "escOtherCmd"
end
end
elseif self.sequence_state == "escParam2" then
if isNum(next_byte) then
param2 = param2 * 10 + next_byte:byte() - ("0"):byte()
else
if next_byte == ";" then
self.sequence_state = "escParam3"
else
pos = pos - 1
self.sequence_state = "escOtherCmd"
end
end
elseif self.sequence_state == "escParam3" then
if isNum(next_byte) then
param3 = param3 * 10 + next_byte:byte() - ("0"):byte()
else
pos = pos - 1
self.sequence_state = "escOtherCmd"
end
elseif self.sequence_state == "escOtherCmd" then
if not self:_helperVT52VT100(next_byte, self.sequence_mode, param1, param2, param3) then
-- drop other VT100 sequences
logger.info("Terminal: ANSI-final: not supported", next_byte,
next_byte:byte(), next_byte, param1, param2, param3)
end
param1, param2, param3 = 0, 0, 0
self.sequence_state = ""
self.sequence_mode = ""
else
logger.dbg("Terminal: detected error in esc sequence, not my fault.")
self.sequence_state = ""
end -- self.sequence_state
pos = pos + 1
end
self:initTextBox(table.concat(self.charlist), true)
end
function TermInputText:scrollRegionDown(column)
column = column or 1
if self.scroll_region_line > self.scroll_region_top then
self.scroll_region_line = self.scroll_region_line - 1
else -- scroll down
local pos = self.charpos
for i = self.scroll_region_line, self.scroll_region_bottom do
while pos > 1 and self.charlist[pos] ~= "\n" do
pos = pos + 1
end
if pos < #self.charlist then
pos = pos + 1
end
end
pos = pos - 1
table.remove(self.charlist, pos)
while self.charlist[pos] ~= "\n" do
table.remove(self.charlist, pos)
end
pos = self.charpos
for i = column, self.maxc - column + 1 do
table.insert(self.charlist, pos, ".")
pos = pos + 1
end
table.insert(self.charlist, pos, "\n")
end
end
function TermInputText:scrollRegionUp(column)
column = column or 1
if self.scroll_region_line < self.scroll_region_bottom then
self.scroll_region_line = self.scroll_region_line + 1
else -- scroll up
local pos = self.charpos
for i = self.scroll_region_line, self.scroll_region_top + 1, -1 do
while pos > 1 and self.charlist[pos] ~= "\n" do
pos = pos - 1
end
if pos > 1 then
pos = pos - 1
end
end
pos = pos + 1
table.remove(self.charlist, pos)
self.charpos = self.charpos - 1
pos = pos - 1
while pos > 0 and self.charlist[pos] ~= "\n" do
table.remove(self.charlist, pos)
pos = pos - 1
end
pos = self.charpos + 1
for i = column, self.maxc - column do
table.insert(self.charlist, pos, " ")
pos = pos + 1
end
table.insert(self.charlist, pos, "\n")
for i = 1, column - 1 do
table.insert(self.charlist, pos, " ")
pos = pos + 1
end
end
end
function TermInputText:addChars(chars, skip_callback, skip_table_concat)
-- the same as in inputtext.lua
if not chars then
-- VirtualKeyboard:addChar(key) gave us 'nil' once (?!)
-- which would crash table.concat()
return
end
if self.enter_callback and chars == "\n" and not skip_callback then
UIManager:scheduleIn(0.3, function() self.enter_callback() end)
return
end
-- this is an addon to inputtext.lua
if self.strike_callback and not skip_callback then
self.strike_callback(chars)
return
end
-- the same as in inputtext.lua
if self.readonly or not self:isTextEditable(true) then
return
end
self.is_text_edited = true
if #self.charlist == 0 then -- widget text is empty or a hint text is displayed
self.charpos = 1 -- move cursor to the first position
end
-- this is a modification of inputtext.lua
local chars_list = util.splitToChars(chars) -- for UTF8
for i = 1, #chars_list do
if chars_list[i] == "\n" then
-- detect current column
local pos = self.charpos
while pos > 0 and self.charlist[pos] ~= "\n" do
pos = pos - 1
end
local column = self.charpos - pos
if self.scroll_region_line then
self:scrollRegionUp(column)
end
-- go to EOL
while self.charlist[self.charpos] and self.charlist[self.charpos] ~= "\n" do
self.charpos = self.charpos + 1
end
if not self.charlist[self.charpos] then -- add new line if necessary
table.insert(self.charlist, self.charpos, "\n")
self.charpos = self.charpos + 1
end
-- go to column in next line
for j = 1, column-1 do
if not self.charlist[self.charpos] then
table.insert(self.charlist, self.charpos, " ")
self.charpos = self.charpos + 1
else
break
end
end
if self.charlist[self.charpos] then
self.charpos = self.charpos + 1
end
-- fill line
pos = self.charpos
for j = column, self.maxc do
if not self.charlist[pos] then
table.insert(self.charlist, pos, " ")
end
pos = pos + 1
end
if not self.charlist[pos] then
table.insert(self.charlist, pos, "\n")
end
elseif chars_list[i] == "\r" then
if self.charlist[self.charpos] == "\n" then
self.charpos = self.charpos - 1
end
while self.charpos >=1 and self.charlist[self.charpos] ~= "\n" do
self.charpos = self.charpos - 1
end
self.charpos = self.charpos + 1
elseif chars_list[i] == "\b" then
self.charpos = self.charpos - 1
else
if self.wrap then
if self.charlist[self.charpos] == "\n" then
self.charpos = self.charpos + 1
for j = 0, self.maxc-1 do
if not self.charlist[self.charpos + j] then
table.insert(self.charlist, self.charpos + j, " ")
end
end
end
else
local column = 1
local pos = self.charpos
while pos > 0 and self.charlist[pos] ~= "\n" do
pos = pos - 1
column = column + 1
end
if self.charlist[self.charpos] == "\n" or column > self.maxc then
self.charpos = self.charpos - 1
end
end
table.remove(self.charlist, self.charpos)
table.insert(self.charlist, self.charpos, chars_list[i])
self.charpos = self.charpos + 1
end
end
-- the same as in inputtext.lua
if not skip_table_concat then
self:initTextBox(table.concat(self.charlist), true)
end
end
dbg:guard(TermInputText, "addChars",
function(self, chars)
assert(type(chars) == "string",
"TermInputText: Wrong chars value type (expected string)!")
end)
function TermInputText:enterAlternateKeypad()
self.store_position = self.charpos
self:formatTerminal(true)
end
function TermInputText:exitAlternateKeypad()
if self.store_position then
self.charpos = self.store_position
self.store_position = nil
-- clear the alternate keypad buffer
while self.charlist[self.charpos] do
table.remove(self.charlist, self.charpos)
end
end
end
--- generates a "tty-matrix"
-- @param maxr number of rows
-- @param maxc number of columns
-- @param clear if true, fill the matrix ' '
function TermInputText:formatTerminal(clear)
local i = self.store_position or 1
-- so we end up in a maxr x maxc array for positioning
for r = 1, self.maxr do
for c = 1, self.maxc do
if not self.charlist[i] then -- end of text
table.insert(self.charlist, i, "\n")
end
if self.charlist[i] ~= "\n" then
if clear then
self.charlist[i] = ' '
end
else
table.insert(self.charlist, i, ' ')
end
i = i + 1
end
if self.charlist[i] ~= "\n" then
table.insert(self.charlist, i, "\n")
end
i = i + 1 -- skip newline
end
-- table.remove(self.charlist, i - 1)
end
function TermInputText:moveCursorToRowCol(r, c)
self:formatTerminal()
local cur_r, cur_c = 1, 0
local i = self.store_position or 1
while i < #self.charlist do
if self.charlist[i] ~= "\n" then
cur_c = cur_c + 1
else
cur_c = 0 -- as we are at the last NL
cur_r = cur_r + 1
end
self.charpos = i
if cur_r == r and cur_c == c then
break
end
i = i + 1
end
self:moveCursorToCharPos(self.charpos)
end
function TermInputText:clearToEndOfScreen()
local pos = self.charpos
while pos <= #self.charlist do
if self.charlist[pos] ~= "\n" then
self.charlist[pos] = " "
end
pos = pos + 1
end
self.is_text_edited = true
self:initTextBox(table.concat(self.charlist))
-- self:moveCursorToCharPos(self.charpos)
end
function TermInputText:delToEndOfLine()
if self.readonly or not self:isTextEditable(true) then
return
end
local cur_pos = self.charpos
-- self.charlist[self.charpos] is the char after the cursor
while self.charlist[cur_pos] and self.charlist[cur_pos] ~= "\n" do
self.charlist[cur_pos] = " "
cur_pos = cur_pos + 1
end
self:initTextBox(table.concat(self.charlist))
end
function TermInputText:reverseLineFeed(skip_callback)
if self.strike_callback and not skip_callback then
self.strike_callback(esc_seq.page_down)
return
end
if self.charpos > 1 and self.charlist[self.charpos] == "\n" then
self.charpos = self.charpos - 1
end
local cur_col = 0
while self.charpos > 1 and self.charlist[self.charpos] ~= "\n" do
self.charpos = self.charpos - 1
cur_col = cur_col + 1
end
if self.charpos > 1 then
self.charpos = self.charpos + 1
end
for i = 1, 80 do
table.insert(self.charlist, self.charpos, " ")
end
end
------------------------------------------------------------------
-- overridden InputText methods --
------------------------------------------------------------------
function TermInputText:leftChar(skip_callback)
if self.charpos == 1 then return end
if self.strike_callback and not skip_callback then
self.strike_callback(esc_seq.cursor_left)
return
end
local left_char = self.charlist[self.charpos - 1]
if not left_char or left_char == "\n" then
return
end
--InputText.leftChar(self)
self.charpos = self.charpos - 1
end
function TermInputText:rightChar(skip_callback)
if self.strike_callback and not skip_callback then
self.strike_callback(esc_seq.cursor_right)
return
end
if self.charpos > #self.charlist then return end
local right_char = self.charlist[self.charpos + 1]
if not right_char and right_char == "\n" then
return
end
InputText.rightChar(self)
end
function TermInputText:moveCursorUp()
local pos = self.charpos
while self.charlist[pos] and self.charlist[pos] ~= "\n" do
pos = pos - 1
end
local column = self.charpos - pos
pos = pos - 1
while self.charlist[pos] and self.charlist[pos] ~= "\n" do
pos = pos - 1
end
self.charpos = pos + column
self:moveCursorToCharPos(self.charpos)
end
function TermInputText:moveCursorDown()
local pos = self.charpos
-- detect current column
while pos > 0 and self.charlist[pos] ~= "\n" do
pos = pos - 1
end
local column = self.charpos - pos
while self.charlist[self.charpos] and self.charlist[self.charpos] ~= "\n" do
self.charpos = self.charpos + 1
end
self.charpos = self.charpos + 1
for i = 1, column-1 do
if self.charlist[pos+i] or self.charlist[pos+i] ~= "\n" then
self.charpos = self.charpos + 1
else
break
end
end
self:moveCursorToCharPos(self.charpos)
end
function TermInputText:delChar()
if self.readonly or not self:isTextEditable(true) then
return
end
if self.charpos == 1 then return end
if self.strike_callback then
self.strike_callback(esc_seq.backspace)
return
end
InputText.delChar(self)
end
function TermInputText:delToStartOfLine()
return
end
function TermInputText:scrollDown(skip_callback)
if self.strike_callback and not skip_callback then
self.strike_callback(esc_seq.page_down)
return
end
InputText.scrollDown(self)
end
function TermInputText:scrollUp(skip_callback)
if self.strike_callback and not skip_callback then
self.strike_callback(esc_seq.page_up)
return
end
InputText.scrollUp(self)
end
function TermInputText:goToStartOfLine(skip_callback)
if self.strike_callback then
if not skip_callback then
self.strike_callback(esc_seq.cursor_pos1)
else
if self.charlist[self.charpos] == "\n" then
self.charpos = self.charpos - 1
end
while self.charpos >=1 and self.charlist[self.charpos] ~= "\n" do
self.charpos = self.charpos - 1
end
self.charpos = self.charpos + 1
self.text_widget:moveCursorToCharPos(self.charpos)
end
return
end
InputText.goToStartOfLine(self)
end
function TermInputText:goToEndOfLine(skip_callback)
if self.strike_callback then
if not skip_callback then
self.strike_callback(esc_seq.cursor_end)
else
while self.charpos <= #self.charlist and self.charlist[self.charpos] ~= "\n" do
self.charpos = self.charpos + 1
end
end
self.text_widget:moveCursorToCharPos(self.charpos)
return
end
InputText.goToEndOfLine(self)
end
function TermInputText:upLine(skip_callback)
if self.strike_callback and not skip_callback then
self.strike_callback(esc_seq.cursor_up)
return
end
InputText.upLine(self)
end
function TermInputText:downLine(skip_callback)
if #self.charlist == 0 then return end -- Avoid cursor moving within a hint.
if self.strike_callback and not skip_callback then
self.strike_callback(esc_seq.cursor_down)
return
end
InputText.downLine(self)
end
return TermInputText
Loading…
Cancel
Save