2
0
mirror of https://github.com/koreader/koreader synced 2024-11-02 15:40:16 +00:00
koreader/plugins/terminal.koplugin/terminputtext.lua
2023-12-14 21:16:43 +01:00

868 lines
28 KiB
Lua

--[[--
module used for terminal emulator to override InputText
@module koplugin.terminal
]]
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 = nil, -- table
save_buffer = nil, -- table
}
function TermInputText:init()
self.alternate_buffer = {}
self.save_buffer = {}
InputText.init(self)
end
-- 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_dec,
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 former_buffer and type(former_buffer[1]) == "table" then
self.charlist,
self.charpos,
self.store_pos_dec,
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 and param1 > 0 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