blob: 70caca049f8052ea46ec23599ddf4fa1206531e5 [file] [log] [blame] [raw]
local term = {}
local gpuAvailable, screenAvailable = false, false
local cursorX, cursorY = 1, 1
local cursorBlink = nil
local function toggleBlink()
if term.isAvailable() then
cursorBlink.state = not cursorBlink.state
if cursorBlink.state then
cursorBlink.alt = component.gpu.get(cursorX, cursorY)
component.gpu.set(cursorX, cursorY, unicode.char(0x2588)) -- solid block
else
component.gpu.set(cursorX, cursorY, cursorBlink.alt)
end
end
end
-------------------------------------------------------------------------------
function term.clear()
if term.isAvailable() then
local w, h = component.gpu.getResolution()
component.gpu.fill(1, 1, w, h, " ")
end
cursorX, cursorY = 1, 1
end
function term.clearLine()
if term.isAvailable() then
local w = component.gpu.getResolution()
component.gpu.fill(1, cursorY, w, 1, " ")
end
cursorX = 1
end
function term.getCursor(col, row)
return cursorX, cursorY
end
function term.setCursor(col, row)
checkArg(1, col, "number")
checkArg(2, row, "number")
if cursorBlink and cursorBlink.state then
toggleBlink()
end
cursorX = col
cursorY = row
end
function term.getCursorBlink()
return cursorBlink ~= nil
end
function term.setCursorBlink(enabled)
checkArg(1, enabled, "boolean")
if enabled then
if not cursorBlink then
cursorBlink = {}
cursorBlink.id = event.timer(0.5, toggleBlink, math.huge)
cursorBlink.state = false
elseif not cursorBlink.state then
toggleBlink()
end
elseif cursorBlink then
event.cancel(cursorBlink.id)
if cursorBlink.state then
toggleBlink()
end
cursorBlink = nil
end
end
function term.isAvailable()
return gpuAvailable and screenAvailable
end
function term.read(history)
checkArg(1, history, "table", "nil")
history = history or {}
table.insert(history, "")
local offsetX = term.getCursor() - 1
local scrollX, scrollY = 0, #history
local function getCursor()
local cx, cy = term.getCursor()
return cx - offsetX + scrollX, scrollY
end
local function setCursor(cbx, cby)
local w = component.gpu.getResolution()
local cx, cy = term.getCursor()
local ncx, ncy = cbx + offsetX - scrollX, cy
-- TODO generalize with y scrolling and make it a helper in the text lib?
scrollY = cby or scrollY
if ncx > w then
local sx = cbx - (w - offsetX)
local dx = math.abs(scrollX - sx)
scrollX = sx
component.gpu.copy(1 + offsetX + dx, cy, w - offsetX - dx, 1, -dx, 0)
local str = unicode.sub(history[scrollY], cbx - (dx - 1), cbx)
str = text.pad(str, dx)
component.gpu.set(1 + (w - dx), cy, str)
end
if ncx < 1 + offsetX then
local sx = cbx - 1
local dx = math.abs(scrollX - sx)
scrollX = sx
component.gpu.copy(1 + offsetX, cy, w - offsetX - dx, 1, dx, 0)
local str = unicode.sub(history[scrollY], cbx, cbx + dx)
str = text.pad(str, dx)
component.gpu.set(1 + offsetX, cy, str)
end
term.setCursor(cbx - scrollX + offsetX, ncy)
end
local function redraw()
local cx, cy = term.getCursor()
local bx, by = 1 + scrollX, scrollY
local w = component.gpu.getResolution()
local l = w - offsetX
local str = unicode.sub(history[by], bx, bx + l)
str = text.pad(str, l)
component.gpu.set(1 + offsetX, cy, str)
end
local function home()
local cbx, cby = getCursor()
setCursor(1, cby)
end
local function ende()
local cbx, cby = getCursor()
setCursor(unicode.len(history[cby]) + 1, cby)
end
local function enter()
local cbx, cby = getCursor()
if cby ~= #history then -- bring entry to front
history[#history] = history[cby]
table.remove(history, cby)
end
end
local function left()
local cbx, cby = getCursor()
if cbx > 1 then
setCursor(cbx - 1, cby)
return true -- for backspace
end
end
local function right(n)
n = n or 1
local cbx, cby = getCursor()
local be = unicode.len(history[cby]) + 1
if cbx < be then
setCursor(math.min(be, cbx + n), cby)
end
end
local function up()
local cbx, cby = getCursor()
if cby > 1 then
setCursor(1, cby - 1)
redraw()
ende()
end
end
local function down()
local cbx, cby = getCursor()
if cby < #history then
setCursor(1, cby + 1)
redraw()
ende()
end
end
local function copyIfNecessary()
local cbx, cby = getCursor()
if cby ~= #history then
history[#history] = history[cby]
setCursor(cbx, #history)
end
end
local function delete()
copyIfNecessary()
local cbx, cby = getCursor()
if cbx <= unicode.len(history[cby]) then
history[cby] = unicode.sub(history[cby], 1, cbx - 1) ..
unicode.sub(history[cby], cbx + 1)
local cx, cy = term.getCursor()
local w = component.gpu.getResolution()
component.gpu.copy(cx + 1, cy, w - cx, 1, -1, 0)
local br = cbx + (w - cx)
local char = unicode.sub(history[cby], br, br)
if not char or unicode.len(char) == 0 then
char = " "
end
component.gpu.set(w, cy, char)
end
end
local function insert(value)
copyIfNecessary()
local cx, cy = term.getCursor()
local cbx, cby = getCursor()
local w = component.gpu.getResolution()
history[cby] = unicode.sub(history[cby], 1, cbx - 1) ..
value ..
unicode.sub(history[cby], cbx)
local len = unicode.len(value)
local scroll = w - cx - len
if scroll > 0 then
component.gpu.copy(cx, cy, scroll, 1, len, 0)
end
component.gpu.set(cx, cy, value)
right(len)
end
local function onKeyDown(char, code)
term.setCursorBlink(false)
if code == keyboard.keys.back then
if left() then delete() end
elseif code == keyboard.keys.delete then
delete()
elseif code == keyboard.keys.left then
left()
elseif code == keyboard.keys.right then
right()
elseif code == keyboard.keys.home then
home()
elseif code == keyboard.keys["end"] then
ende()
elseif code == keyboard.keys.up then
up()
elseif code == keyboard.keys.down then
down()
elseif code == keyboard.keys.enter then
enter()
return true, history[#history] .. "\n"
elseif keyboard.isControlDown() then
local cbx, cby = getCursor()
if code == keyboard.keys.d then
if history[cby] == "" then
history[#history] = ""
return true, nil
end
elseif code == keyboard.keys.c then
history[#history] = ""
return true, nil
end
elseif not keyboard.isControl(char) then
insert(unicode.char(char))
end
term.setCursorBlink(true)
term.setCursorBlink(true) -- force toggle to caret
end
local function onClipboard(value)
copyIfNecessary()
term.setCursorBlink(false)
local cbx, cby = getCursor()
local l = value:find("\n", 1, true)
if l then
history[cby] = unicode.sub(history[cby], 1, cbx - 1)
redraw()
insert(unicode.sub(value, 1, l - 1))
return true, history[cby] .. "\n"
else
insert(value)
term.setCursorBlink(true)
term.setCursorBlink(true) -- force toggle to caret
end
end
local function cleanup()
if history[#history] == "" then
table.remove(history)
end
term.setCursorBlink(false)
print()
end
term.setCursorBlink(true)
while term.isAvailable() do
local ok, name, address, charOrValue, code = pcall(event.pull)
if not ok then
cleanup()
error("interrupted", 0)
end
if term.isAvailable() and -- may have changed since pull
type(address) == "string" and
component.isPrimary(address)
then
local done, result
if name == "key_down" then
done, result = onKeyDown(charOrValue, code)
elseif name == "clipboard" then
done, result = onClipboard(charOrValue)
end
if done then
cleanup()
return result
end
end
end
cleanup()
return nil -- fail the read if term becomes unavailable
end
function term.write(value, wrap)
if not term.isAvailable() then
return
end
value = tostring(value)
if unicode.len(value) == 0 then
return
end
value = text.detab(value)
local w, h = component.gpu.getResolution()
if not w then
return -- gpu lost its screen but the signal wasn't processed yet.
end
local blink = term.getCursorBlink()
term.setCursorBlink(false)
local function checkCursor()
if cursorX > w then
cursorX = 1
cursorY = cursorY + 1
end
if cursorY > h then
component.gpu.copy(1, 1, w, h, 0, -1)
component.gpu.fill(1, h, w, 1, " ")
cursorY = h
end
end
for line, nl in value:gmatch("([^\r\n]*)([\r\n]?)") do
while wrap and unicode.len(line) > w - (cursorX - 1) do
local partial = unicode.sub(line, 1, w - (cursorX - 1))
local wordWrapped = partial:match("(.*[^a-zA-Z0-9._])")
if wordWrapped or unicode.len(partial) > w then
partial = wordWrapped or partial
line = unicode.sub(line, unicode.len(partial) + 1)
component.gpu.set(cursorX, cursorY, partial)
end
cursorX = math.huge
checkCursor()
end
if unicode.len(line) > 0 then
component.gpu.set(cursorX, cursorY, line)
cursorX = cursorX + unicode.len(line)
end
if unicode.len(nl) == 1 then
cursorX = math.huge
checkCursor()
end
end
term.setCursorBlink(blink)
end
-------------------------------------------------------------------------------
local function onComponentAvailable(_, componentType)
local wasAvailable = term.isAvailable()
if componentType == "gpu" then
gpuAvailable = true
elseif componentType == "screen" then
screenAvailable = true
end
if not wasAvailable and term.isAvailable() then
os.pushSignal("term_available")
end
end
local function onComponentUnavailable(_, componentType)
local wasAvailable = term.isAvailable()
if componentType == "gpu" then
gpuAvailable = false
elseif componentType == "screen" then
screenAvailable = false
end
if wasAvailable and not term.isAvailable() then
os.pushSignal("term_unavailable")
end
end
_G.term = term
return function()
event.listen("component_available", onComponentAvailable)
event.listen("component_unavailable", onComponentUnavailable)
end