local module = {} function module.check() return true -- Usually always loaded, but maybe it would be worth it to check if the computer has a GPU or not? I'm not sure. end function module.init() local serialize = require("serialize") local unicode = require("unicode") local event = require("event") --local ocelot = component.proxy(component.list("ocelot")()) local component = require("component") local computer = require("computer") local gpu = component.gpu _G._PUBLIC.terminal = {} local cursorPosX = 1 local cursorPosY = 1 local width, height = gpu.getResolution() function _PUBLIC.terminal.getCursorPos() return cursorPosX,cursorPosY end function _PUBLIC.terminal.setCursorPos(x,y) checkArg(1,x,"number","nil") checkArg(2,y,"number","nil") if type(x)~=nil then cursorPosX=math.min(math.max(x,1),width) end if type(y)~=nil then cursorPosY=math.min(math.max(y,1),height) end end local readHistory = {} function _PUBLIC.terminal.getHistory(id) checkArg(1,id,"string") return table.copy(readHistory[id]) end function _PUBLIC.terminal.setHistory(id,hist) checkArg(1,id,"string") checkArg(2,hist,"table") for i=1,#hist do hist[i]=tostring(hist[i]) end readHistory[id]=hist end function _PUBLIC.terminal.addToHistory(id,hist) checkArg(1,id,"string") checkArg(2,hist,"string") table.insert(readHistory[id],hist) end local ANSIColorPalette = { ["dark"] = { [0] = 0x000000, [1] = 0x800000, [2] = 0x008000, [3] = 0x808000, [4] = 0x000080, [5] = 0x800080, [6] = 0x008080, [7] = 0xC0C0C0 }, ["bright"] = { [0] = 0x808080, [1] = 0xFF0000, [2] = 0x00FF00, [3] = 0xFFFF00, [4] = 0x0000FF, [5] = 0xFF00FF, [6] = 0x00FFFF, [7] = 0xFFFFFF } } defaultForegroundColor = ANSIColorPalette["bright"][7] defaultBackgroundColor = ANSIColorPalette["dark"][0] gpu.setForeground(defaultForegroundColor) gpu.setBackground(defaultBackgroundColor) local function scrollDown() if gpu.copy(1,1,width,height,0,-1) then local prevForeground = gpu.getForeground() local prevBackground = gpu.getBackground() gpu.setForeground(defaultForegroundColor) gpu.setBackground(defaultBackgroundColor) gpu.fill(1, height, width, 1, " ") gpu.setForeground(prevForeground) gpu.setBackground(prevBackground) cursorPosY=height end end local function newLine() cursorPosX=1 cursorPosY = cursorPosY + 1 if cursorPosY>height then scrollDown() end end local function parseCodeNumbers(code) local o = {} for num in code:sub(3,-2):gmatch("[^;]+") do table.insert(o,tonumber(num)) end return o end local function from8BitColor(num) num=math.floor(num)&255 if num<16 then return 0x444444*((num>>3)&1)+(0xBB0000*((num>>2)&1)|0x00BB00*((num>>1)&1)|0x0000BB*(num&1)) end if num>=232 then return 0x10101*(8+(num-232)*10) end num=num-16 local palette = {0,95,135,175,215,255} return (palette[(num//36)%6+1]<<16)|(palette[(num//6)%6+1]<<8)|palette[num%6+1] end local function from24BitColor(r,g,b) r,g,b=math.floor(r)&255,math.floor(g)&255,math.floor(b)&255 return (r<<16)|(g<<8)|b end local function findCodeEnd(text,i) local function inRange(v,min,max) return v>=min and v<=max end i=i+2 while i<=#text and not inRange(text:byte(i),0x40,0x7F) do i=i+1 end return i end function _PUBLIC.terminal.write(text, textWrap) -- you don't know how tiring this was just for ANSI escape code support if textWrap == nil then textWrap = true end if not text or not tostring(text) then return end if text:find("\a") then computer.beep() end text = tostring(text) text = "\27[0m" .. text:gsub("\t", " ") local readBreak = 0 -- readBreak is for when, inside the for loop, there normally would have been an increase in the "i" variable because it has read more than one character. -- unfortunately, changing the "i" variable would have unpredictable effects, so to not risk anything, this workaround was done. local section = "" local function printSection() if #section==0 then return end while true do gpu.set(cursorPosX,cursorPosY,section) if unicode.wlen(section) > width - cursorPosX + 1 and textWrap then section = section:sub(width - cursorPosX + 2) newLine() else cursorPosX = cursorPosX+unicode.wlen(section) break end end section = "" end for i=1,#text do if readBreak>0 then readBreak = readBreak - 1 goto continue end if string.byte(text,i)==10 then printSection() newLine() elseif string.byte(text,i)==13 then printSection() cursorPosX=1 elseif string.byte(text,i)==0x1b and i<=#text-2 then printSection() --ocelot.log("0x1b char detected") local codeType = string.sub(text,i+1,i+1) if codeType=="[" then -- Control Sequence Introducer --ocelot.log("Control Sequence Introducer") local codeEndIdx = findCodeEnd(text,i) -- codeEndIdx = string.find(text,"m",i) local code = string.sub(text,i,codeEndIdx) --ocelot.log("Code: "..code.." ("..i..", "..codeEndIdx..")") readBreak = readBreak + #code - 1 local nums = parseCodeNumbers(code) local codeEnd = code:sub(-1) --ocelot.log("Code end: "..codeEnd..", "..#codeEnd) if codeEnd == "m" then -- Select Graphic Rendition --ocelot.log("Select Graphic Rendition, ID "..nums[1]) if nums[1]>=30 and nums[1]<=37 then gpu.setForeground(ANSIColorPalette["dark"][nums[1]%10]) end if nums[1]==38 and nums[2]==5 then gpu.setForeground(from8BitColor(nums[3])) end if nums[1]==38 and nums[2]==2 then gpu.setForeground(from24BitColor(nums[3],nums[4],nums[5])) end if nums[1]==39 or nums[1]==0 then gpu.setForeground(defaultForegroundColor) end if nums[1]>=40 and nums[1]<=47 then gpu.setBackground(ANSIColorPalette["dark"][nums[1]%10]) end if nums[1]==48 and nums[2]==5 then gpu.setBackground(from8BitColor(nums[3])) end if nums[1]==48 and nums[2]==2 then gpu.setBackground(from24BitColor(nums[3],nums[4],nums[5])) end if nums[1]==49 or nums[1]==0 then gpu.setBackground(defaultBackgroundColor) end if nums[1]>=90 and nums[1]<=97 then gpu.setForeground(ANSIColorPalette["bright"][nums[1]%10]) end if nums[1]>=100 and nums[1]<=107 then gpu.setBackground(ANSIColorPalette["bright"][nums[1]%10]) end end end else --gpu.set(cursorPosX,cursorPosY,string.sub(text,i,i)) section = section..string.sub(text,i,i) end ::continue:: end printSection() end function _G.print(...) local args = {...} local stringArgs = {} for _, arg in pairs(args) do if type(arg)=="table" then table.insert(stringArgs, serialize.table(arg,true)) elseif tostring(arg) then table.insert(stringArgs, tostring(arg)) end end _PUBLIC.terminal.write(table.concat(stringArgs, " ") .. "\n") end function _G._PUBLIC.terminal.clear() gpu.setForeground(defaultForegroundColor) gpu.setBackground(defaultBackgroundColor) gpu.fill(1,1,width,height," ") cursorPosX, cursorPosY = 1, 1 end function _G._PUBLIC.terminal.read(options) checkArg(1, options, "table", "nil") local function checkOption(name, value, neededType) assert(not value or type(value) == neededType, ("%s option must be %s, %s provided"):format(name, neededType, type(value))) end if not options then options = {} end checkOption("readHistoryType", options.readHistoryType, "string") checkOption("prefix", options.prefix, "string") checkOption("maxChars", options.maxChars, "number") checkOption("defaultText", options.defaultText, "string") checkOption("censor", options.censor, "string") options.maxChars = options.maxChars or math.huge local text = options.defaultText or "" local historyIdx if options.readHistoryType then if not readHistory[options.readHistoryType] then readHistory[options.readHistoryType] = {text} elseif readHistory[options.readHistoryType][#readHistory[options.readHistoryType] ] ~= text then table.insert(readHistory[options.readHistoryType], text) end historyIdx = #readHistory[options.readHistoryType] end local function updateHistory() if not options.readHistoryType then return end if historyIdx ~= #readHistory[options.readHistoryType] then return end readHistory[options.readHistoryType][historyIdx]=text end local cur = unicode.len(text)+1 if options.prefix then _PUBLIC.terminal.write(options.prefix) end local startX, startY = cursorPosX, cursorPosY local fg, bg = gpu.getForeground(), gpu.getBackground() local cursorBlink = true local function checkScroll(y) for i=1,y-height do scrollDown() startY=startY-1 end return math.min(y,height) end local function set(index, character, invertedColors) -- HACK: Currently, this will uncensor all spaces in the inputted text. if character==nil or character=="" then return end if options.censor then character = character:gsub("[^ ]", options.censor) end if invertedColors then gpu.setForeground(bg) gpu.setBackground(fg) else gpu.setForeground(fg) gpu.setBackground(bg) end index=startX+index-1 local setX, setY = (index-1)%width+1, startY+((index-1)//width+1)-1 setY = checkScroll(setY) gpu.set(setX,setY,unicode.sub(character,1,width-setX+1)) for i=1,math.ceil((#character+setX-1)/width)+1 do gpu.set(1,setY+i,unicode.sub(character,2-setX+i*width,width+i*width-setX)) setY = checkScroll(setY) end end local function strDef(a,b) if #a==0 then return b end return a end local function curPos(cur) return unicode.wlen(unicode.sub(text,1,cur-1))+1 end local function add(chr) if type(chr)~="string" or #chr==0 then return end if unicode.len(text)>=options.maxChars then return end if options.maxChars1 then text=unicode.sub(text,1,cur-2)..unicode.sub(text,cur) cur=cur-1 set(curPos(cur),strDef(unicode.sub(text,cur,cur)," "),true) cursorBlink = true set(curPos(cur)+1,unicode.sub(text,cur+1).." ",false) updateHistory() elseif key=="delete" then text = unicode.sub(text,1,cur-1)..unicode.sub(text,cur+1) set(curPos(cur),strDef(unicode.sub(text,cur,cur)," "),true) cursorBlink = true if cur<=unicode.len(text) then set(curPos(cur+1),unicode.sub(text,cur+1).." ",false) end updateHistory() elseif key=="enter" then set(curPos(cur),strDef(unicode.sub(text,cur,cur)," "),false) break elseif not (args[3]<32 or (args[3]>0x7F and args[3]<=0x9F)) then add(unicode.char(args[3]) or " ") updateHistory() end elseif args and args[1]=="clipboard" then local clip = args[3] if not args[3] then goto continue end while isLine(unicode.sub(clip,1,1)) do clip=unicode.sub(clip,2) end while isLine(unicode.sub(clip,-1)) do clip=unicode.sub(clip,1,-2) end add(clip) updateHistory() else cursorBlink=not cursorBlink set(curPos(cur),strDef(unicode.sub(text,cur,cur)," "),cursorBlink) end ::continue:: end if options.readHistoryType then if readHistory[options.readHistoryType][#readHistory[options.readHistoryType]]=="" then table.remove(readHistory[options.readHistoryType],#readHistory[options.readHistoryType]) end if historyIdx<#readHistory[options.readHistoryType] then -- table.remove(readHistory[options.readHistoryType],historyIdx) table.insert(readHistory[options.readHistoryType],text) end while #readHistory[options.readHistoryType] > 50 do table.remove(readHistory[options.readHistoryType], 1) end end cursorPosX=1 cursorPosY=cursorPosY+math.ceil((unicode.wlen(text)+startX-1)/width) if cursorPosY>height then scrollDown() end return text end end function module.exit() _G._PUBLIC.terminal = nil end return module