diff options
Diffstat (limited to 'Programs')
22 files changed, 17048 insertions, 0 deletions
diff --git a/Programs/Enchat.bup/Contents/Info.meta b/Programs/Enchat.bup/Contents/Info.meta new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/Programs/Enchat.bup/Contents/Info.meta @@ -0,0 +1 @@ + diff --git a/Programs/Enchat.bup/Contents/Resources/resources_here.txt b/Programs/Enchat.bup/Contents/Resources/resources_here.txt new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/Programs/Enchat.bup/Contents/Resources/resources_here.txt @@ -0,0 +1 @@ + diff --git a/Programs/Enchat.bup/Contents/bits-UI/enchat3.lua b/Programs/Enchat.bup/Contents/bits-UI/enchat3.lua new file mode 100644 index 0000000..b66549e --- /dev/null +++ b/Programs/Enchat.bup/Contents/bits-UI/enchat3.lua @@ -0,0 +1,2479 @@ +--[[ + Enchat 3.0 + Get with: + wget https://github.com/LDDestroier/enchat/raw/master/enchat3.lua enchat3.lua + +This is a stable release. You fool! +--]] + +local scr_x, scr_y = term.getSize() +CHATBOX_SAFEMODE = nil + +-- non-changable settings +enchat = { + connectToSkynet = true, + version = 3.0, + isBeta = false, + port = 11000, + skynetPort = "enchat3-default", + url = "https://github.com/LDDestroier/enchat/raw/master/enchat3.lua", + betaurl = "https://github.com/LDDestroier/enchat/raw/beta/enchat3.lua", + ignoreModem = false, + dataDir = "/.enchat", + useChatbox = false, + disableChatboxWithRedstone = false, +} + +-- changable settings +local enchatSettings = { -- DEFAULT settings. + animDiv = 4, -- divisor of text animation speed (scrolling from left) + doAnimate = true, -- whether or not to animate text moving from left side of screen + reverseScroll = false, -- whether or not to make scrolling up really scroll down + redrawDelay = 0.1, -- delay between redrawing + useSetVisible = false, -- whether or not to use term.current().setVisible(), which has performance and flickering improvements + pageKeySpeed = 8, -- how far PageUP or PageDOWN should scroll + doNotif = true, -- whether or not to use oveerlay glasses for notifications, if possible + doKrazy = true, -- whether or not to add &k obfuscation + useSkynet = true, -- whether or not to use gollark's Skynet in addition to modem calls + extraNewline = true, -- adds an extra newline after every message since setting to true + acceptPictoChat = true, -- whether or not to allow tablular enchat input, which is what /picto uses + noRepeatNames = true, -- whether or not to display the username in two or more consecutive messages by the same user +} + +-- colors for various elements +palette = { + bg = colors.black, + txt = colors.white, + promptbg = colors.gray, + prompttxt = colors.white, + scrollMeter = colors.lightGray, + chevron = colors.black, + title = colors.lightGray, + titlebg = colors.gray, +} + +-- UI adjustments, used to emulate the appearance of other chat programs +UIconf = { + promptY = 1, + chevron = ">", + chatlogTop = 1, + title = "Enchat 3", + doTitle = false, + titleY = 1, + nameDecolor = false, + centerTitle = true, + prefix = "<", + suffix = "> " +} + +-- Attempt to get some slight optimization through localizing basic functions. +local mathmax, mathmin, mathrandom = math.max, math.min, math.random +local termblit, termwrite = term.blit, term.write +local termsetCursorPos, termgetCursorPos, termsetCursorBlink = term.setCursorPos, term.getCursorPos, term.setCursorBlink +local termsetTextColor, termsetBackgroundColor = term.setTextColor, term.setBackgroundColor +local termgetTextColor, termgetBackgroundColor = term.getTextColor, term.getBackgroundColor +local termclear, termclearLine = term.clear, term.clearLine +local tableinsert, tableremove, tableconcat = table.insert, table.remove, table.concat +local textutilsserialize, textutilsunserialize = textutils.serialize, textutils.unserialize +local stringsub, stringgsub, stringrep = string.sub, string.gsub, string.rep +local unpack = unpack +-- This better do something. + +local initcolors = { + bg = termgetBackgroundColor(), + txt = termgetTextColor() +} + +local tArg = {...} + +local yourName = tArg[1] +local encKey = tArg[2] + +local setEncKey = function(newKey) + encKey = newKey +end + +local saveSettings = function() + local file = fs.open(fs.combine(enchat.dataDir, "settings"), "w") + file.write( + textutilsserialize({ + enchatSettings = enchatSettings, + palette = palette, + UIconf = UIconf, + }) + ) + file.close() +end + +local loadSettings = function() + local contents + if not fs.exists(fs.combine(enchat.dataDir, "settings")) then + saveSettings() + end + local file = fs.open(fs.combine(enchat.dataDir, "settings"), "r") + contents = file.readAll() + file.close() + local newSettings = textutilsunserialize(contents) + if newSettings then + for k,v in pairs(newSettings.enchatSettings) do + enchatSettings[k] = v + end + for k,v in pairs(newSettings.palette) do + palette[k] = v + end + for k,v in pairs(newSettings.UIconf) do + UIconf[k] = v + end + else + saveSettings() + end +end + +local updateEnchat = function(doBeta) + local pPath = shell.getRunningProgram() + local h = http.get((doBeta or enchat.isBeta) and enchat.betaurl or enchat.url) + if not h then + return false, "Could not connect." + else + local content = h.readAll() + local file = fs.open(pPath, "w") + file.write(content) + file.close() + return true, "Updated!" + end +end + +-- disables chat screen updating +local pauseRendering = true + +-- primarily for use when using the pallete command, hoh hoh +local colors_strnames = { + ["white"] = colors.white, + ["pearl"] = colors.white, + ["silver"] = colors.white, + ["aryan"] = colors.white, + ["#f0f0f0"] = colors.white, + + ["orange"] = colors.orange, + ["carrot"] = colors.orange, + ["fuhrer"] = colors.orange, + ["pumpkin"] = colors.orange, + ["#f2b233"] = colors.orange, + + ["magenta"] = colors.magenta, + ["hotpink"] = colors.magenta, + ["lightpurple"] = colors.magenta, + ["light purple"] = colors.magenta, + ["#e57fd8"] = colors.magenta, + + ["lightblue"] = colors.lightBlue, + ["light blue"] = colors.lightBlue, + ["skyblue"] = colors.lightBlue, + ["#99b2f2"] = colors.lightBlue, + + ["yellow"] = colors.yellow, + ["piss"] = colors.yellow, + ["pee"] = colors.yellow, + ["lemon"] = colors.yellow, + ["spongebob"] = colors.yellow, + ["cowardice"] = colors.yellow, + ["#dede6c"] = colors.yellow, + + ["lime"] = colors.lime, + ["lightgreen"] = colors.lime, + ["light green"] = colors.lime, + ["slime"] = colors.lime, + ["radiation"] = colors.lime, + ["#7fcc19"] = colors.lime, + + ["pink"] = colors.pink, + ["lightishred"] = colors.pink, + ["lightish red"] = colors.pink, + ["communist"] = colors.pink, + ["commie"] = colors.pink, + ["patrick"] = colors.pink, + ["#f2b2cc"] = colors.pink, + + ["gray"] = colors.gray, + ["grey"] = colors.gray, + ["graey"] = colors.gray, + ["gunmetal"] = colors.gray, + ["#4c4c4c"] = colors.gray, + + ["lightgray"] = colors.lightGray, + ["lightgrey"] = colors.lightGray, + ["light gray"] = colors.lightGray, + ["light grey"] = colors.lightGray, + ["#999999"] = colors.lightGray, + + ["cyan"] = colors.cyan, + ["seawater"] = colors.cyan, + ["brine"] = colors.cyan, + ["#4c99b2"] = colors.cyan, + + ["purple"] = colors.purple, + ["purble"] = colors.purple, + ["obsidian"] = colors.purple, + ["diviner"] = colors.purple, + ["#b266e5"] = colors.purple, + + ["blue"] = colors.blue, + ["blu"] = colors.blue, + ["azure"] = colors.blue, + ["sapphire"] = colors.blue, + ["lapis"] = colors.blue, + ["volnutt"] = colors.blue, + ["blueberry"] = colors.blue, + ["x"] = colors.blue, + ["megaman"] = colors.blue, + ["#3366bb"] = colors.blue, + + ["brown"] = colors.brown, + ["shit"] = colors.brown, + ["dirt"] = colors.brown, + ["mud"] = colors.brown, + ["bricks"] = colors.brown, + ["#7f664c"] = colors.brown, + + ["green"] = colors.green, + ["grass"] = colors.green, + ["#57a64e"] = colors.green, + + ["red"] = colors.red, + ["crimson"] = colors.red, + ["vermillion"] = colors.red, + ["menstration"] = colors.red, + ["blood"] = colors.red, + ["marinara"] = colors.red, + ["zero"] = colors.red, + ["protoman"] = colors.red, + ["communism"] = colors.red, + ["#cc4c4c"] = colors.red, + + ["black"] = colors.black, + ["dark"] = colors.black, + ["darkness"] = colors.black, + ["space"] = colors.black, + ["coal"] = colors.black, + ["onyx"] = colors.black, + ["#191919"] = colors.black, +} + +local toblit = { + [0] = " ", + [1] = "0", + [2] = "1", + [4] = "2", + [8] = "3", + [16] = "4", + [32] = "5", + [64] = "6", + [128] = "7", + [256] = "8", + [512] = "9", + [1024] = "a", + [2048] = "b", + [4096] = "c", + [8192] = "d", + [16384] = "e", + [32768] = "f" +} +local tocolors = {} +for k,v in pairs(toblit) do + tocolors[v] = k +end + +local codeNames = { + ["r"] = "reset", -- Sets either the text (&) or background (~) colors to their original color. + ["{"] = "stopFormatting", -- Toggles formatting text off + ["}"] = "startFormatting", -- Toggles formatting text on + ["k"] = "krazy" -- Makes the font kuh-razy! +} + +-- indicates which character should turn into which random &k character +local kraziez = { + ["l"] = { + "!", + "l", + "1", + "|", + "i", + "I", + ":", + ";", + }, + ["m"] = { + "M", + "W", + "w", + "m", + "X", + "N", + "_", + "%", + "@", + }, + ["all"] = {} +} + +for a = 1, #kraziez["l"] do + kraziez[kraziez["l"][a]] = kraziez["l"] +end +for k,v in pairs(kraziez) do + for a = 1, #v do + kraziez[kraziez[k][a]] = v + end +end + +-- check if using older CC version, and omit special characters if it's too old to avoid crash +if tonumber(_CC_VERSION or 0) >= 1.76 then + for a = 1, 255 do + if (a ~= 32) and (a ~= 13) and (a ~= 10) then + kraziez["all"][#kraziez["all"]+1] = string.char(a) + end + end +else + for a = 33, 126 do + kraziez["all"][#kraziez["all"]+1] = string.char(a) + end +end + +local makeRandomString = function(length, begin, stop) + local output = "" + for a = 1, length do + output = output .. string.char(math.random(begin or 1, stop or 255)) + end + return output +end + +local personalID = makeRandomString(64, 32, 128) + +local explode = function(div, str, replstr, includeDiv) + if (div == '') then + return false + end + local pos, arr = 0, {} + for st, sp in function() return string.find(str, div, pos, false) end do + tableinsert(arr, string.sub(replstr or str, pos, st - 1 + (includeDiv and #div or 0))) + pos = sp + 1 + end + tableinsert(arr, string.sub(replstr or str, pos)) + return arr +end + +local parseKrazy = function(c) + if kraziez[c] then + return kraziez[c][mathrandom(1, #kraziez[c])] + else + return kraziez.all[mathrandom(1, #kraziez.all)] + end +end + +-- my main man, the function that turns unformatted strings into formatted strings +local textToBlit = function(input, onlyString, initText, initBack, checkPos, useJSONformat) + if not input then return end + checkPos = checkPos or -1 + initText, initBack = initText or toblit[term.getTextColor()], initBack or toblit[term.getBackgroundColor()] + tcode, bcode = "&", "~" + local cpos, cx = 0, 0 + local skip, ignore, ex = nil, false, nil + local text, back, nex = initText, initBack, nil + + local charOut, textOut, backOut = {}, {}, {} + local JSONoutput = {} + + local krazy = false + local bold = false + local strikethrough = false + local underline = false + local italic = false + + local codes = {} + codes["r"] = function(prev) + if not ignore then + if prev == tcode then + text = initText + bold = false + strikethrough = false + underline = false + italic = false + elseif prev == bcode then + if useJSONformat then + return 0 + else + back = initBack + end + end + krazy = false + else + return 0 + end + end + codes["k"] = function(prev) + if not ignore then + krazy = not krazy + else + return 0 + end + end + codes["{"] = function(prev) + if not ignore then + ignore = true + else + return 0 + end + end + codes["}"] = function(prev) + if ignore then + ignore = false + else + return 0 + end + end + + if useJSONformat then + codes["l"] = function(prev) + bold = true + end + codes["m"] = function(prev) + strikethrough = true + end + codes["n"] = function(prev) + underline = true + end + codes["o"] = function(prev) + italic = true + end + end + + local sx, str = 0 + input = stringgsub(input, "(\\)(%d%d?%d?)", function(cap, val) + if tonumber(val) < 256 then + cpos = cpos - #val + return string.char(val) + else + return cap..val + end + end) + + local MCcolors = { + ["0"] = "white", + ["1"] = "gold", + ["2"] = "light_purple", + ["3"] = "aqua", + ["4"] = "yellow", + ["5"] = "green", + ["6"] = "light_purple", + ["7"] = "dark_gray", + ["8"] = "gray", + ["9"] = "dark_aqua", + ["a"] = "dark_purple", + ["b"] = "dark_blue", + ["c"] = "gold", + ["d"] = "dark_green", + ["e"] = "red", + ["f"] = "black", + } + + for cx = 1, #input do + str = stringsub(input,cx,cx) + if skip then + if tocolors[str] and not ignore then + if skip == tcode then + text = str == " " and initText or str + if sx < checkPos then + cpos = cpos - 2 + end + elseif skip == bcode then + back = str == " " and initBack or str + if sx < checkPos then + cpos = cpos - 2 + end + end + elseif codes[str] and not (ignore and str == "{") then + ex = codes[str](skip) or 0 + sx = sx + ex + if sx < checkPos then + cpos = cpos - ex - 2 + end + else + sx = sx + 1 + if useJSONformat then + JSONoutput[sx] = { + text = (skip..str), + color = onlyString and "f" or MCcolors[text], + bold = (not onlyString) and bold, + italic = (not onlyString) and italic, + underline = (not onlyString) and underline, + obfuscated = (not onlyString) and krazy, + strikethrough = (not onlyString) and strikethrough + } + else + charOut[sx] = krazy and parseKrazy(prev..str) or (skip..str) + textOut[sx] = stringrep(text,2) + backOut[sx] = stringrep(back,2) + end + end + skip = nil + else + if (str == tcode or str == bcode) and (codes[stringsub(input, 1+cx, 1+cx)] or tocolors[stringsub(input,1+cx,1+cx)]) then + skip = str + else + sx = sx + 1 + if useJSONformat then + JSONoutput[sx] = { + text = str, + color = onlyString and "f" or MCcolors[text], + bold = (not onlyString) and bold, + italic = (not onlyString) and italic, + underline = (not onlyString) and underline, + obfuscated = (not onlyString) and krazy, + strikethrough = (not onlyString) and strikethrough + } + else + charOut[sx] = krazy and parseKrazy(str) or str + textOut[sx] = text + backOut[sx] = back + end + end + end + end + if useJSONformat then + return textutils.serializeJSON(JSONoutput) + else + if onlyString then + return tableconcat(charOut), (checkPos > -1) and cpos or nil + else +-- return {tableconcat(charOut), tableconcat(textOut):gsub(" ", initText), tableconcat(backOut):gsub(" ", initBack)}, (checkPos > -1) and cpos or nil + return {tableconcat(charOut), tableconcat(textOut), tableconcat(backOut)}, (checkPos > -1) and cpos or nil + end + end +end +_G.textToBlit = textToBlit + +-- convoluted read function that renders color codes as they are written. +-- struggles with \123 codes, but hey, fuck you +local colorRead = function(maxLength, _history) + local output = "" + local history, _history = {}, _history or {} + for a = 1, #_history do + history[a] = _history[a] + end + history[#history+1] = "" + local hPos = #history + local cx, cy = termgetCursorPos() + local x, xscroll = 1, 1 + local ctrlDown = false + termsetCursorBlink(true) + local evt, key, bout, xmod, timtam + while true do + termsetCursorPos(cx, cy) + bout, xmod = textToBlit(output, false, nil, nil, x) + for a = 1, #bout do + bout[a] = stringsub(bout[a], xscroll, xscroll + scr_x - cx) + end + termblit(unpack(bout)) + termwrite((" "):rep(scr_x - cx)) + termsetCursorPos(cx + x + xmod - xscroll, cy) + evt = {os.pullEvent()} + if evt[1] == "char" or evt[1] == "paste" then + output = (output:sub(1, x-1)..evt[2]..output:sub(x)):sub(1, maxLength or -1) + x = mathmin(x + #evt[2], #output+1) + elseif evt[1] == "key" then + key = evt[2] + if key == keys.leftCtrl then + ctrlDown = true + elseif key == keys.left then + x = mathmax(x - 1, 1) + elseif key == keys.right then + x = mathmin(x + 1, #output+1) + elseif key == keys.backspace then + if x > 1 then + repeat + output = output:sub(1,x-2)..output:sub(x) + x = x - 1 + until output:sub(x-1,x-1) == " " or (not ctrlDown) or (x == 1) + end + elseif key == keys.delete then + if x < #output+1 then + repeat + output = output:sub(1,x-1)..output:sub(x+1) + until output:sub(x,x) == " " or (not ctrlDown) or (x == #output+1) + end + elseif key == keys.enter then + termsetCursorBlink(false) + return output + elseif key == keys.home then + x = 1 + elseif key == keys["end"] then + x = #output+1 + elseif key == keys.up then + if history[hPos-1] then + hPos = hPos - 1 + output = history[hPos] + x = #output+1 + end + elseif key == keys.down then + if history[hPos+1] then + hPos = hPos + 1 + output = history[hPos] + x = #output+1 + end + end + elseif evt[1] == "key_up" then + if evt[2] == keys.leftCtrl then + ctrlDown = false + end + end + if hPos > 1 then + history[hPos] = output + end + if x+cx-xscroll+xmod > scr_x then + xscroll = x-(scr_x-cx)+xmod + elseif x-xscroll+xmod < 0 then + repeat + xscroll = xscroll - 1 + until x-xscroll-xmod >= 0 + end + xscroll = math.max(1, xscroll) + end +end +_G.colorRead = colorRead + +local checkValidName = function(_nayme) + local nayme = textToBlit(_nayme,true) + if type(nayme) ~= "string" then + return false + else + return (#nayme >= 2 and #nayme <= 32 and nayme:gsub(" ","") ~= "") + end +end + +if tArg[1] == "update" then + local res, message = updateEnchat(tArg[2] == "beta") + return print(message) +end + +local prettyClearScreen = function(start, stop) + termsetTextColor(colors.lightGray) + termsetBackgroundColor(colors.gray) + if _VERSION then + for y = start or 1, stop or scr_y do + termsetCursorPos(1,y) + if y == (start or 1) then + termwrite(("\135"):rep(scr_x)) + elseif y == (stop or scr_y) then + termsetTextColor(colors.gray) + termsetBackgroundColor(colors.lightGray) + termwrite(("\135"):rep(scr_x)) + else + termclearLine() + end + end + else + termclear() + end +end + +local cwrite = function(text, y) + local cx, cy = termgetCursorPos() + termsetCursorPos((scr_x/2) - math.ceil(#text/2), y or cy) + return write(text) +end + +local prettyCenterWrite = function(text, y) + local words = explode(" ", text, nil, true) + local buff = "" + local lines = 0 + for w = 1, #words do + if #buff + #words[w] > scr_x then + cwrite(buff, y + lines) + buff = "" + lines = lines + 1 + end + buff = buff..words[w] + end + cwrite(buff, y + lines) + return lines +end + +local prettyPrompt = function(prompt, y, replchar, doColor) + local cy, cx = termgetCursorPos() + termsetBackgroundColor(colors.gray) + termsetTextColor(colors.white) + local yadj = 1 + prettyCenterWrite(prompt, y or cy) + termsetCursorPos(1, y + yadj) + termsetBackgroundColor(colors.lightGray) + termclearLine() + local output + if doColor then + output = colorRead() + else + output = read(replchar) + end + return output +end + +local fwrite = function(text) + local b = textToBlit(text) + return termblit(unpack(b)) +end + +local cfwrite = function(text, y) + local cx, cy = termgetCursorPos() + termsetCursorPos((scr_x/2) - math.ceil(#textToBlit(text,true)/2), y or cy) + return fwrite(text) +end + +-- execution start! + +if not checkValidName(yourName) then -- not so fast, evildoers + yourName = nil +end + +local currentY = 2 + +if not (yourName and encKey) then + prettyClearScreen() +end + +if not yourName then + cfwrite("&8~7Text = &, Background = ~", scr_y-3) + cfwrite("&8~7&{Krazy = &k, Reset = &r", scr_y-2) + cfwrite("&7~00~11~22~33~44~55~66&8~77&7~88~99~aa~bb~cc~dd~ee~ff", scr_y-1) + yourName = prettyPrompt("Enter your name.", currentY, nil, true) + if not checkValidName(yourName) then + while true do + yourName = prettyPrompt("That name isn't valid. Enter another.", currentY, nil, true) + if checkValidName(yourName) then + break + end + end + end + currentY = currentY + 3 +end + +if not encKey then + setEncKey(prettyPrompt("Enter an encryption key.", currentY, "*")) + currentY = currentY + 3 +end + +-- prevents terminating. it is reversed upon exit. +local oldePullEvent = os.pullEvent +os.pullEvent = os.pullEventRaw + +local bottomMessage = function(text) + termsetCursorPos(1,scr_y) + termsetTextColor(colors.gray) + termclearLine() + termwrite(text) +end + +loadSettings() +saveSettings() + +termsetBackgroundColor(colors.black) +termclear() + +local getAPI = function(apiname, apipath, apiurl, doDoFile, doScroll) + apipath = fs.combine(fs.combine(enchat.dataDir,"api"), apipath) + if (not fs.exists(apipath)) then + if doScroll then term.scroll(1) end + bottomMessage(apiname .. " API not found! Downloading...") + local prog = http.get(apiurl) + if not prog then + if doScroll then term.scroll(1) end + bottomMessage("Failed to download " .. apiname .. " API. Abort.") + termsetCursorPos(1,1) + return + end + local file = fs.open(apipath,"w") + file.write(prog.readAll()) + file.close() + end + if doDoFile then + return dofile(apipath) + else + os.loadAPI(apipath) + end + if not _ENV[fs.getName(apipath)] then + if doScroll then term.scroll(1) end + bottomMessage("Failed to load " .. apiname .. " API. Abort.") + termsetCursorPos(1,1) + return + else + return _ENV[fs.getName(apipath)] + end +end + +local skynet, aes, bigfont +-- _G.skynet_CBOR_path = fs.combine(enchat.dataDir,"/api/cbor") +aes = getAPI("AES", "aes", "http://pastebin.com/raw/9E5UHiqv", false, false) +if enchat.connectToSkynet and http.websocket then + skynet = getAPI("Skynet", "skynet", "https://raw.githubusercontent.com/LDDestroier/CC/master/API/skynet.lua", true, true) +end +bigfont = getAPI("BigFont", "bigfont", "https://pastebin.com/raw/3LfWxRWh", false, true) + +if encKey and skynet and enchat.connectToSkynet then + bottomMessage("Connecting to Skynet...") + local success = parallel.waitForAny( + function() + skynet.open(enchat.skynetPort) + end, + function() + sleep(3) + end + ) + if success == 2 then + term.scroll(1) + bottomMessage("Failed to connect to skynet.") + skynet = nil + sleep(0.5) + end +end + +local log = {} -- Records all sorts of data on text. +local renderlog = {} -- Only records straight terminal output. Generated from 'log' +local IDlog = {} -- Really only used with skynet, will prevent duplicate messages. + +local scroll = 0 +local maxScroll = 0 + +local getModem = function() + if enchat.ignoreModem then + return nil + else + local modems = {peripheral.find("modem")} + return modems[1] + end +end + +local getChatbox = function() + if enchat.useChatbox then + if commands then -- oh baby, a command computer, now we're talkin' + -- mind you, you still need a chatbox to get chat input... + return { + say = function(text) + commands.tellraw("@a", textToBlit(text, false, "0", "f", nil, true)) + end, + tell = function(player, text) + commands.tellraw(player, textToBlit(text, false, "0", "f", nil, true)) + end + } + else + local cb = chatbox or peripheral.find("chat_box") + if cb then + if cb.setName then -- Computronics + cb.setName(yourName) + return { + say = cb.say, + tell = cb.say -- why is there no tell command??? + } + else -- whatever whackjob mod SwitchCraft uses I forget + return { + say = function(text, block) + if CHATBOX_SAFEMODE then +-- if CHATBOX_SAFEMODE ~= block then + cb.tell(CHATBOX_SAFEMODE, text) +-- end + else + local players = cb.getPlayerList() + for i = 1, #players do + if players[i] ~= block then + cb.tell(players[i], text) + end + end + end + end, + tell = cb.tell + } + end + else + return nil + end + end + else + return nil + end +end + +local modem = getModem() +local chatbox = getChatbox() + +if (not modem) and (not enchat.ignoreModem) then + if ccemux and (not enchat.ignoreModem) then + ccemux.attach("top", "wireless_modem") + modem = getModem() + elseif not skynet then + error("You should get a modem.") + end +end + +if modem then modem.open(enchat.port) end + +local modemTransmit = function(freq, repfreq, message) + if modem then + modem.transmit(freq, repfreq, message) + end +end + +local encrite = function(input) -- standardized encryption function + if not input then return input end + return aes.encrypt(encKey, textutilsserialize(input)) +end + +local decrite = function(input) -- redundant comments cause tuberculosis + if not input then return input end + return textutilsunserialize(aes.decrypt(encKey, input) or "") +end + +local dab = function(func, ...) -- "do and back", not...never mind + local x, y = termgetCursorPos() + local b, t = termgetBackgroundColor(), termgetTextColor() + local output = {func(...)} + termsetCursorPos(x,y) + termsetTextColor(t) + termsetBackgroundColor(b) + return unpack(output) +end + +local splitStr = function(str, maxLength) + local output = {} + for l = 1, #str, maxLength do + output[#output+1] = str:sub(l,l+maxLength+-1) + end + return output +end + +local splitStrTbl = function(tbl, maxLength) + local output, tline = {} + for w = 1, #tbl do + tline = splitStr(tbl[w], maxLength) + for t = 1, #tline do + output[#output+1] = tline[t] + end + end + return output +end + +-- same as term.blit, but wraps by-word. +local blitWrap = function(char, text, back, noWrite) -- where ALL of the onscreen wrapping is done + local cWords = splitStrTbl(explode(" ",char,nil, true), scr_x) + local tWords = splitStrTbl(explode(" ",char,text,true), scr_x) + local bWords = splitStrTbl(explode(" ",char,back,true), scr_x) + + local ox,oy = termgetCursorPos() + local cx,cy,ty = ox,oy,1 + local output = {} + local length = 0 + local maxLength = 0 + for a = 1, #cWords do + length = length + #cWords[a] + maxLength = mathmax(maxLength, length) + if ((cx + #cWords[a]) > scr_x) then + cx = 1 + length = 0 + if (cy == scr_y) then + term.scroll(1) + end + cy = mathmin(cy+1, scr_y) + ty = ty + 1 + end + if not noWrite then + termsetCursorPos(cx,cy) + termblit(cWords[a],tWords[a],bWords[a]) + end + cx = cx + #cWords[a] + output[ty] = output[ty] or {"","",""} + output[ty][1] = output[ty][1]..cWords[a] + output[ty][2] = output[ty][2]..tWords[a] + output[ty][3] = output[ty][3]..bWords[a] + end + return output, maxLength +end + +-- simple picture drawing function, for /picto +local pictochat = function(xsize, ysize) + local output = {{},{},{}} + local maxWidth, minMargin = 0, math.huge + for y = 1, ysize do + output[1][y] = {} + output[2][y] = {} + output[3][y] = {} + for x = 1, xsize do + output[1][y][x] = " " + output[2][y][x] = " " + output[3][y][x] = " " + end + end + + termsetBackgroundColor(colors.gray) + termsetTextColor(colors.black) + for y = 1, scr_y do + termsetCursorPos(1, y) + termwrite(("/"):rep(scr_x)) + end + cwrite(" [ENTER] to finish. ", scr_y) + cwrite("Push a key to change char.", scr_y-1) + + local cx, cy = math.floor((scr_x/2)-(xsize/2)), math.floor((scr_y/2)-(ysize/2)) + + local allCols = "0123456789abcdef" + local tPos, bPos = 16, 1 + local char, text, back = " ", allCols:sub(tPos,tPos), allCols:sub(bPos,bPos) + + local render = function() + termsetTextColor(colors.white) + termsetBackgroundColor(colors.black) + local mx, my + for y = 1, ysize do + for x = 1, xsize do + mx, my = x+cx+-1, y+cy+-1 + termsetCursorPos(mx,my) + termblit(output[1][y][x], output[2][y][x], output[3][y][x]) + end + end + termsetCursorPos((scr_x/2)-5,ysize+cy+1) + termwrite("Char = '") + termblit(char, text, back) + termwrite("'") + end + local evt, butt, mx, my + local isShiftDown = false + + render() + + while true do + evt = {os.pullEvent()} + if evt[1] == "mouse_click" or evt[1] == "mouse_drag" then + butt, mx, my = evt[2], evt[3]-cx+1, evt[4]-cy+1 + if mx >= 1 and mx <= xsize and my >= 1 and my <= ysize then + if butt == 1 then + output[1][my][mx] = char + output[2][my][mx] = text + output[3][my][mx] = back + elseif butt == 2 then + output[1][my][mx] = " " + output[2][my][mx] = " " + output[3][my][mx] = " " + end + render() + end + elseif evt[1] == "mouse_scroll" then + local oldTpos, oldBpos = tPos, bPos + if isShiftDown then + tPos = mathmax(1, mathmin(16, tPos + evt[2])) + else + bPos = mathmax(1, mathmin(16, bPos + evt[2])) + end + text, back = stringsub(allCols,tPos,tPos), stringsub(allCols,bPos,bPos) + if oldTpos ~= tPos or oldBpos ~= bPos then + render() + end + elseif evt[1] == "key" then + if evt[2] == keys.enter then + for y = 1, ysize do + output[1][y] = table.concat(output[1][y]) + output[2][y] = table.concat(output[2][y]) + output[3][y] = table.concat(output[3][y]) + maxWidth = math.max(maxWidth, #stringgsub(output[3][y], " +$", "")) + minMargin = math.min(minMargin, output[3][y]:find("[^ ]") or math.huge) + end + --error(minMargin) + local croppedOutput = {} + local touched = false + local crY = 0 + for a = 1, ysize do + if output[1][1] == (" "):rep(xsize) and output[3][1] == (" "):rep(xsize) then + tableremove(output[1],1) + tableremove(output[2],1) + tableremove(output[3],1) + else + for y = #output[1], 1, -1 do + if output[1][y] == (" "):rep(xsize) and output[3][y] == (" "):rep(xsize) then + tableremove(output[1],y) + tableremove(output[2],y) + tableremove(output[3],y) + else + break + end + end + break + end + end + for y = 1, #output[1] do + output[1][y] = output[1][y]:sub(minMargin, maxWidth) + output[2][y] = output[2][y]:sub(minMargin, maxWidth) + output[3][y] = output[3][y]:sub(minMargin, maxWidth) + end + return output + elseif evt[2] == keys.leftShift then + isShiftDown = true + elseif evt[2] == keys.left or evt[2] == keys.right then + local oldTpos, oldBpos = tPos, bPos + if isShiftDown then + tPos = mathmax(1, mathmin(16, tPos + (evt[2] == keys.right and 1 or -1))) + else + bPos = mathmax(1, mathmin(16, bPos + (evt[2] == keys.right and 1 or -1))) + end + text, back = allCols:sub(tPos,tPos), allCols:sub(bPos,bPos) + if oldTpos ~= tPos or oldBpos ~= bPos then + render() + end + end + elseif evt[1] == "key_up" then + if evt[2] == keys.leftShift then + isShiftDown = false + end + elseif evt[1] == "char" then + if char ~= evt[2] then + char = evt[2] + render() + end + end + end +end + +-- notifications will only appear if you have plethora's neural connector and overlay glasses on your person + +local notif = {} +notif.alpha = 248 +notif.height = 10 +notif.width = 6 +notif.time = 40 +notif.wrapX = 350 +notif.maxNotifs = 15 +local nList = {} +local colorTranslate = { + [" "] = {240, 240, 240}, + ["0"] = {240, 240, 240}, + ["1"] = {242, 178, 51 }, + ["2"] = {229, 127, 216}, + ["3"] = {153, 178, 242}, + ["4"] = {222, 222, 108}, + ["5"] = {127, 204, 25 }, + ["6"] = {242, 178, 204}, + ["7"] = {76, 76, 76 }, + ["8"] = {153, 153, 153}, + ["9"] = {76, 153, 178}, + ["a"] = {178, 102, 229}, + ["b"] = {51, 102, 204}, + ["c"] = {127, 102, 76 }, + ["d"] = {87, 166, 78 }, + ["e"] = {204, 76, 76 }, + ["f"] = {25, 25, 25 } +} +local interface, canvas = peripheral.find("neuralInterface") +if interface then + if interface.canvas then + canvas = interface.canvas() + notif.newNotification = function(char, text, back, time) + if #nList > notif.maxNotifs then + tableremove(nList, 1) + end + nList[#nList+1] = {char,text,back,time,1} -- the last one is the alpha multiplier + end + notif.displayNotifications = function(doCountDown) + local adjList = { + ["i"] = -4, + ["l"] = -3, + ["I"] = -1, + ["t"] = -2, + ["k"] = -1, + ["!"] = -4, + ["|"] = -4, + ["."] = -4, + [","] = -4, + [":"] = -4, + [";"] = -4, + ["f"] = -1, + ["'"] = -3, + ["\""] = -1, + ["<"] = -1, + [">"] = -1, + } + local drawEdgeLine = function(y,alpha) + local l = canvas.addRectangle(notif.wrapX, 1+(y-1)*notif.height, 1, notif.height) + l.setColor(unpack(colorTranslate["0"])) + l.setAlpha(alpha / 2) + end + local getWordWidth = function(str) + local output = 0 + for a = 1, #str do + output = output + notif.width + (adjList[stringsub(str,a,a)] or 0) + end + return output + end + canvas.clear() + local xadj, charadj, wordadj, t, r + local x, y, words, txtwords, bgwords = 0, 0 + for n = 1, mathmin(#nList, notif.maxNotifs) do + xadj, charadj = 0, 0 + y = y + 1 + x = 0 + words = explode(" ",nList[n][1],nil,true) + txtwords = explode(" ",nList[n][1],nList[n][2],true) + bgwords = explode(" ",nList[n][1],nList[n][3],true) + local char, text, back + local currentX = 0 + for w = 1, #words do + char = words[w] + text = txtwords[w] + back = bgwords[w] + if currentX + getWordWidth(char) > notif.wrapX then + y = y + 1 + x = 2 + xadj = 0 + currentX = x * notif.width + end + for cx = 1, #char do + x = x + 1 + charadj = (adjList[stringsub(char,cx,cx)] or 0) + r = canvas.addRectangle(xadj+1+(x-1)*notif.width, 1+(y-1)*notif.height, charadj+notif.width, notif.height) + if stringsub(back,cx,cx) ~= " " then + r.setAlpha(notif.alpha * nList[n][5]) + r.setColor(unpack(colorTranslate[stringsub(back,cx,cx)])) + else + r.setAlpha(100 * nList[n][5]) + r.setColor(unpack(colorTranslate["7"])) + end + drawEdgeLine(y,notif.alpha * nList[n][5]) + t = canvas.addText({xadj+1+(x-1)*notif.width,2+(y-1)*notif.height}, stringsub(char,cx,cx)) + t.setAlpha(notif.alpha * nList[n][5]) + t.setColor(unpack(colorTranslate[stringsub(text,cx,cx)])) + xadj = xadj + charadj + currentX = currentX + charadj+notif.width + end + end + end + for n = mathmin(#nList, notif.maxNotifs), 1, -1 do + if doCountDown then + if nList[n][4] > 1 then + nList[n][4] = nList[n][4] - 1 + else + if nList[n][5] > 0 then + while true do + nList[n][5] = mathmax(nList[n][5] - 0.2, 0) + notif.displayNotifications(false) + if nList[n][5] == 0 then break else sleep(0.05) end + end + end + tableremove(nList,n) + end + end + end + end + end +end + +local darkerCols = { + ["0"] = "8", + ["1"] = "c", + ["2"] = "a", + ["3"] = "b", + ["4"] = "1", + ["5"] = "d", + ["6"] = "2", + ["7"] = "f", + ["8"] = "7", + ["9"] = "b", + ["a"] = "7", + ["b"] = "7", + ["c"] = "f", + ["d"] = "7", + ["e"] = "7", + ["f"] = "f" +} + +-- used for regular chat. they can be disabled if you hate fun +local animations = { + slideFromLeft = function(char, text, back, frame, maxFrame, length) + return { + stringsub(char, (length or #char) - ((frame/maxFrame)*(length or #char))), + stringsub(text, (length or #text) - ((frame/maxFrame)*(length or #text))), + stringsub(back, (length or #back) - ((frame/maxFrame)*(length or #back))) + } + end, + fadeIn = function(char, text, back, frame, maxFrame, length) + -- a good example: + -- &1what &2in &3the &4world &5are &6you &7doing &8in &9my &aswamp + for i = 1, 3 - math.ceil(frame/maxFrame * 3) do + text = stringgsub(text, ".", darkerCols) + end + return { + char, + text, + back + } + end, + flash = function(char, text, back, frame, maxFrame, length) + local t = palette.txt + if frame ~= maxFrame then + t = (frame % 2 == 0) and t or palette.bg + end + return { + char, + toblit[t]:rep(#text), + (frame % 2 == 0) and back or (" "):rep(#back) + } + end, + none = function(char, text, back, frame, maxFrame, length) + return { + char, + text, + back + } + end +} + +local inAnimate = function(animType, buff, frame, maxFrame, length) + local char, text, back = buff[1], buff[2], buff[3] + if enchatSettings.doAnimate and (frame >= 0) and (maxFrame > 0) then + return animations[animType or "slideFromleft"](char, text, back, frame, maxFrame, length) + else + return {char,text,back} + end +end + +local genRenderLog = function() + local buff, prebuff, maxLength, lastUser + local scrollToBottom = scroll == maxScroll + renderlog = {} + local dcName, dcMessage + for a = 1, #log do + if not ((lastUser == log[a].personalID and log[a].personalID) and log[a].name == "" and log[a].message == " ") then + termsetCursorPos(1,1) + if UIconf.nameDecolor then + if lastUser == log[a].personalID and log[a].personalID then + dcName = "" + else + dcName = textToBlit(table.concat({log[a].prefix,log[a].name,log[a].suffix}), true, toblit[palette.txt], toblit[palette.bg]) + end + dcMessage = textToBlit(log[a].message, false, toblit[palette.txt], toblit[palette.bg]) + prebuff = { + dcName..dcMessage[1], + toblit[palette.chevron]:rep(#dcName)..dcMessage[2], + toblit[palette.bg]:rep(#dcName)..dcMessage[3] + } + else + if lastUser == log[a].personalID and log[a].personalID then + prebuff = textToBlit(" " .. log[a].message, false, toblit[palette.txt], toblit[palette.bg]) + else + prebuff = textToBlit(table.concat({ + log[a].prefix, + "&}&r~r", + log[a].name, + "&}&r~r", + log[a].suffix, + "&}&r~r", + log[a].message + }), + false, toblit[palette.txt], toblit[palette.bg]) + end + end + if log[a].message ~= " " and enchatSettings.noRepeatNames then + lastUser = log[a].personalID + end + if (log[a].frame == 0) and (canvas and enchatSettings.doNotif) then + if not (log[a].name == "" and log[a].message == " ") then + notif.newNotification(prebuff[1], prebuff[2], prebuff[3], notif.time * 4) + end + end + if log[a].maxFrame == true then + log[a].maxFrame = math.floor(mathmin(#prebuff[1], scr_x) / enchatSettings.animDiv) + end + if log[a].ignoreWrap then + buff, maxLength = {prebuff}, mathmin(#prebuff[1], scr_x) + else + buff, maxLength = blitWrap(prebuff[1], prebuff[2], prebuff[3], true) + end + -- repeat every line in multiline entries + for l = 1, #buff do + -- holy shit, two animations, lookit mr. roxas over here + if log[a].animType then + renderlog[#renderlog + 1] = inAnimate(log[a].animType, buff[l], log[a].frame, log[a].maxFrame, maxLength) + else + renderlog[#renderlog + 1] = inAnimate("fadeIn", inAnimate("slideFromLeft", buff[l], log[a].frame, log[a].maxFrame, maxLength), log[a].frame, log[a].maxFrame, maxLength) + end + end + if (log[a].frame < log[a].maxFrame) and log[a].frame >= 0 then + log[a].frame = log[a].frame + 1 + else + log[a].frame = -1 + end + end + end + maxScroll = mathmax(0, #renderlog - (scr_y - 2)) + if scrollToBottom then + scroll = maxScroll + end +end + +-- there is probably a much better way of doing this, but I don't care at the moment +local tsv = function(visible) + if term.current().setVisible and enchatSettings.useSetVisible then + return term.current().setVisible(visible) + end +end + +local renderChat = function(doScrollBackUp) + tsv(false) + termsetCursorBlink(false) + genRenderLog(log) + local ry + termsetBackgroundColor(palette.bg) + for y = UIconf.chatlogTop, (scr_y-UIconf.promptY) - 1 do + ry = (y + scroll - (UIconf.chatlogTop - 1)) + termsetCursorPos(1,y) + termclearLine() + if renderlog[ry] then + termblit(unpack(renderlog[ry])) + end + end + if UIconf.promptY ~= 0 then + termsetCursorPos(1,scr_y) + termsetTextColor(palette.scrollMeter) + termclearLine() + termwrite(scroll.." / "..maxScroll.." ") + end + + local _title = UIconf.title:gsub("YOURNAME", yourName.."&}&r~r"):gsub("ENCKEY", encKey.."&}&r~r"):gsub("PORT", tostring(enchat.port)) + if UIconf.doTitle then + termsetTextColor(palette.title) + term.setBackgroundColor(palette.titlebg) + if UIconf.nameDecolor then + if UIconf.centerTitle then + cwrite((" "):rep(scr_x)..textToBlit(_title, true)..(" "):rep(scr_x), UIconf.titleY or 1) + else + termsetCursorPos(1, UIconf.titleY or 1) + termwrite(textToBlit(_title, true)..(" "):rep(scr_x)) + end + else + local blTitle = textToBlit(_title) + termsetCursorPos(UIconf.centerTitle and ((scr_x/2) - math.ceil(#blTitle[1]/2)) or 1, UIconf.titleY or 1) + termclearLine() + termblit(unpack(blTitle)) + end + end + termsetCursorBlink(true) + tsv(true) +end + +local logadd = function(name, message, animType, maxFrame, ignoreWrap, _personalID) + log[#log + 1] = { + prefix = name and UIconf.prefix or "", + suffix = name and UIconf.suffix or "", + name = name or "", + message = message or " ", + ignoreWrap = ignoreWrap, + frame = 0, + maxFrame = maxFrame or true, + animType = animType, + personalID = _personalID + } +end + +local logaddTable = function(name, message, animType, maxFrame, ignoreWrap, _personalID) + if type(message) == "table" and type(name) == "string" then + if #message > 0 then + local isGood = true + for l = 1, #message do + if type(message[l]) ~= "string" then + isGood = false + break + end + end + if isGood then + logadd(name, message[1], animType, maxFrame, ignoreWrap, _personalID) + for l = 2, #message do + logadd(nil, message[l], animType, maxFrame, ignoreWrap, _personalID) + end + end + end + end +end + +local enchatSend = function(name, message, option, doLog, animType, maxFrame, crying, recipient, ignoreWrap, omitPersonalID) + option = option or {} + if option.doLog then + if type(message) == "string" then + logadd(name, message, option.animType, option.maxFrame, option.ignoreWrap, (not option.omitPersonalID) and personalID) + else + logaddTable(name, message, option.animType, option.maxFrame, option.ignoreWrap, (not option.omitPersonalID) and personalID) + end + end + local messageID = makeRandomString(64) + local outmsg = encrite({ + name = name, + message = message, + animType = option.animType, + maxFrame = option.maxFrame, + messageID = messageID, + recipient = option.recipient, + ignoreWrap = option.ignoreWrap, + personalID = (not option.omitPersonalID) and personalID, + cry = option.crying, + simCommand = option.simCommand, + simArgument = option.simArgument, + }) + IDlog[messageID] = true + if not enchat.ignoreModem then + modemTransmit(enchat.port, enchat.port, outmsg) + end + if skynet and enchatSettings.useSkynet then + skynet.send(enchat.skynetPort, outmsg) + end +end + +local cryOut = function(name, crying) + enchatSend(name, nil, {crying = crying}) +end + +local getPictureFile = function(path) -- ONLY NFP or NFT, fuck BLT + if not fs.exists(path) then + return false, "No such image." + else + local file = fs.open(path,"r") + local content = file.readAll() + file.close() + local output + if content:find("\31") and content:find("\30") then + output = explode("\n",content:gsub("\31","&"):gsub("\30","~"),nil,false) + else + if content:lower():gsub("[0123456789abcdef\n ]","") ~= "" then + return false, "Invalid image." + else + output = explode("\n",content:gsub("[^\n]","~%1 "),nil,false) + end + end + return output + end +end + +local getTableLength = function(tbl) + local output = 0 + for k,v in pairs(tbl) do + output = output + 1 + end + return output +end + +local userCryList = {} + +local commandInit = "/" +local commands = {} +local simmableCommands = { + big = true +} +-- Commands only have one argument, being a single string. +-- Separate arguments can be extrapolated with the explode() function. +commands.about = function() + if enchatSettings.extraNewline then + logadd(nil,nil) + end + logadd(nil,"Enchat "..enchat.version.." by LDDestroier.") + logadd(nil,"'Encrypted, decentralized, &1c&2o&3l&4o&5r&6i&7z&8e&9d&r chat program'") + logadd(nil,"Made in 2018, out of gum and procrastination.") + logadd(nil,nil) + logadd(nil,"AES Lua implementation made by SquidDev.") + logadd(nil,"'Skynet' (enables HTTP chat) belongs to gollark (osmarks).") +end +commands.exit = function() + enchatSend("*", "'"..yourName.."&}&r~r' buggered off. (disconnect)") + return "exit" +end +commands.me = function(msg) + if enchatSettings.extraNewline then + logadd(nil,nil) + end + if msg then + enchatSend("&2*", yourName.."~r&2 "..msg, {doLog = true}) + else + logadd("*",commandInit.."me [message]") + end +end +commands.tron = function() + local url = "https://raw.githubusercontent.com/LDDestroier/CC/master/tron.lua" + local prog, contents = http.get(url) + if prog then + enchatSend("*", yourName .. "&}&r~r has started a game of TRON.", {doLog = true}) + contents = prog.readAll() + pauseRendering = true + prog = load(contents, nil, nil, _ENV)(enchatSettings.useSkynet and "skynet", "quick", yourName) + else + logadd("*", "Could not download TRON.") + end + pauseRendering = false + doRender = true +end +commands.colors = function() + if enchatSettings.extraNewline then + logadd(nil,nil) + end + logadd("*", "&{Color codes: (use & or ~)&}") + logadd(nil, " &7~11~22~33~44~55~66~7&87~8&78~99~aa~bb~cc~dd~ee~ff") + logadd(nil, " &{Reset text/BG with &r and ~r.&}") + logadd(nil, " &{Use &k for krazy text.&}") +end +commands.update = function() + local res, message = updateEnchat() + if res then + enchatSend("*", yourName.."&}&r~r has updated and exited.") + termsetBackgroundColor(colors.black) + termsetTextColor(colors.white) + termclear() + termsetCursorPos(1,1) + print(message) + return "exit" + else + logadd("*", res) + end +end +commands.picto = function(filename) + local image, output, res + local isEmpty + if filename then + output, res = getPictureFile(filename) + if not output then + logadd("*",res) + logadd(nil,nil) + return + else + tableinsert(output,1,"") + end + else + isEmpty = true + output = {""} + pauseRendering = true + local image = pictochat(26,11) + pauseRendering = false + for y = 1, #image[1] do + output[#output+1] = "" + for x = 1, #image[1][1] do + output[#output] = table.concat({ + output[#output], + "&", + image[2][y]:sub(x,x), + "~", + image[3][y]:sub(x,x), + image[1][y]:sub(x,x) + }) + isEmpty = isEmpty and (image[1][y]:sub(x,x) == " " and image[3][y]:sub(x,x) == " ") + end + end + end + if not isEmpty then + enchatSend(yourName, output, {doLog = true, animType = "slideFromLeft", ignoreWrap = true}) + end +end +commands.list = function() + userCryList = {} + local tim = os.startTimer(0.5) + cryOut(yourName, true) + while true do + local evt = {os.pullEvent()} + if evt[1] == "timer" then + if evt[2] == tim then + break + end + end + end + if enchatSettings.extraNewline then + logadd(nil,nil) + end + if getTableLength(userCryList) == 0 then + logadd(nil,"Nobody's there.") + else + for k,v in pairs(userCryList) do + logadd(nil,"+'"..k.."'") + end + end +end +commands.nick = function(newName) + if enchatSettings.extraNewline then + logadd(nil,nil) + end + if newName then + if checkValidName(newName) then + if newName == yourName then + logadd("*","But you're already called that!") + else + enchatSend("*", "'"..yourName.."&}&r~r' is now known as '"..newName.."&}&r~r'.", {doLog = true}) + yourName = newName + end + else + if #newName < 2 then + logadd("*", "That name is too damned small.") + elseif #newName > 32 then + logadd("*", "Woah there, that name is too large.") + end + end + else + logadd("*",commandInit.."nick [newName]") + end +end +commands.whoami = function(now) + if enchatSettings.extraNewline then + logadd(nil,nil) + end + if now == "now" then + logadd("*","You are still '"..yourName.."&}&r~r'!") + else + logadd("*","You are '"..yourName.."&}&r~r'!") + end +end +commands.key = function(newKey) + if enchatSettings.extraNewline then + logadd(nil,nil) + end + if newKey then + if newKey ~= encKey then + enchatSend("*", "'"..yourName.."&}&r~r' buggered off. (keychange)") + setEncKey(newKey) + logadd("*", "Key changed to '"..encKey.."&}&r~r'.") + enchatSend("*", "'"..yourName.."&}&r~r' has moseyed on over.", {omitPersonalID = true}) + else + logadd("*", "That's already the key, though.") + end + else + logadd("*","Key = '"..encKey.."&}&r~r'") + logadd("*","Channel = '"..enchat.port.."'") + end +end +commands.shrug = function(face) + if enchatSettings.extraNewline then + logadd(nil,nil) + end + enchatSend(yourName, "¯\\_"..(face and ("("..face..")") or "\2").."_/¯", {doLog = true}) +end +commands.asay = function(_argument) + local sPoint = (_argument or ""):find(" ") + if enchatSettings.extraNewline then + logadd(nil,nil) + end + if not sPoint then + logadd("*","Animation types:") + for k,v in pairs(animations) do + logadd(nil," '"..k.."'") + end + else + local animType = _argument:sub(1,sPoint-1) + local message = _argument:sub(sPoint+1) + local animFrameMod = { + flash = 8, + fadeIn = 4, + } + if animations[animType] then + if textToBlit(message,true):gsub(" ","") ~= "" then + enchatSend(yourName, message, {doLog = true, animType = animType, maxFrame = animFrameMod[animType]}) + else + logadd("*","That message is no good.") + end + else + logadd("*","Invalid animation type.") + end + end +end +commands.big = function(_argument, simUser) + local sPoint = (_argument or ""):find(" ") + if enchatSettings.extraNewline then + logadd(nil,nil) + end + if not sPoint then + logadd("*",commandInit .. "big <size> <text>") + else + local fontSize = tonumber(_argument:sub(1,sPoint-1)) + local message = _argument:sub(sPoint+1) + if not fontSize then + logadd("*","Size must be number between 0 and 2.") + elseif fontSize < 0 or fontSize > 2 then + logadd("*","Size must be number between 0 and 2.") + else + fontSize = math.floor(.5+fontSize) + local tOutput + if fontSize > 0 then + message = textToBlit(message, false, "0", "f") + local output = {{},{},{}} + local x, y = 1, 1 + local char + for i = 1, #message[1] do + char = bigfont.makeBlittleText( + fontSize, + stringsub(message[1],i,i), + stringsub(message[2],i,i), + stringsub(message[3],i,i) + ) + x = x + char.width + if x >= scr_x then + y = y + char.height + x = char.width + end + for charY = 1, char.height do + output[1][y+charY-1] = (output[1][y+charY-1] or " ") .. char[1][charY] + output[2][y+charY-1] = (output[2][y+charY-1] or " ") .. char[2][charY] + output[3][y+charY-1] = (output[3][y+charY-1] or " ") .. char[3][charY] + end + end + tOutput = {""} + local yy = 1 + for y = 1, #output[1] do + tOutput[#tOutput+1] = "" + for x = 1, #output[1][y] do + tOutput[#tOutput] = table.concat({tOutput[#tOutput],"&",output[2][yy]:sub(x,x),"~",output[3][yy]:sub(x,x),output[1][yy]:sub(x,x)}) + end + yy = yy + 1 + end + else + tOutput = message + end + if simUser then + logaddTable(simUser, tOutput) + else + logaddTable(yourName, tOutput) + enchatSend(yourName, nil, {simCommand = "big", simArgument = _argument}) + end + end + end +end +commands.msg = function(_argument) + local sPoint = (_argument or ""):find(" ") + if enchatSettings.extraNewline then + logadd(nil,nil) + end + if not sPoint then + logadd("*",commandInit.."msg <recipient> <message>") + else + local recipient = _argument:sub(1,sPoint-1) + local message = _argument:sub(sPoint+1) + if not message then + logadd("*","You got half of the arguments down pat, at least.") + else + if textToBlit(message,true):gsub(" ","") == "" then + logadd("*","That message is no good.") + else + enchatSend(yourName, message, {recipient = recipient}) + logadd("*","to '"..recipient.."': "..message) + end + end + end +end +commands.palette = function(_argument) + local argument = _argument or "" + if enchatSettings.extraNewline then + logadd(nil,nil) + end + if argument:gsub("%s","") == "" then + local buff = "" + for k,v in pairs(palette) do + buff = buff..k..", " + end + buff = buff:sub(1,-3) + logadd("*",commandInit.."palette "..buff.." <colorcode>") + else + argument = explode(" ",argument) + if #argument == 1 then + if argument[1]:gsub("%s",""):lower() == "reset" or argument[1]:gsub("%s",""):lower() == "enchat3" then + palette = { + bg = colors.black, + txt = colors.white, + promptbg = colors.gray, + prompttxt = colors.white, + scrollMeter = colors.lightGray, + chevron = colors.black, + title = colors.lightGray, + titlebg = colors.gray, + } + UIconf = { + promptY = 1, + chevron = ">", + chatlogTop = 1, + title = "Enchat 3", + doTitle = false, + titleY = 1, + nameDecolor = false, + centerTitle = true, + prefix = "<", + suffix = "> " + } + termsetBackgroundColor(palette.bg) + termclear() + logadd("*","You cleansed your palette.") + saveSettings() + elseif argument[1]:gsub("%s",""):lower() == "enchat2" then + palette = { + bg = colors.gray, + txt = colors.white, + promptbg = colors.white, + prompttxt = colors.black, + scrollMeter = colors.white, + chevron = colors.lightGray, + title = colors.yellow, + titlebg = colors.gray, + } + UIconf = { + promptY = 1, + chevron = ">", + chatlogTop = 1, + title = "Enchat 2", + doTitle = false, + titleY = 1, + nameDecolor = false, + centerTitle = false, + prefix = "<", + suffix = "> " + } + termsetBackgroundColor(palette.bg) + termclear() + logadd("*","Switched to the old Enchat2 palette.") + saveSettings() + elseif argument[1]:gsub("%s",""):lower() == "enchat1" then + logadd("*","We don't talk about that one.") + elseif argument[1]:gsub("%s",""):lower() == "enchat4" then + logadd("*","Let's leave that to future LDD.") + elseif argument[1]:gsub("%s",""):lower() == "chat.lua" then + palette = { + bg = colors.black, + txt = colors.white, + promptbg = colors.black, + prompttxt = colors.white, + scrollMeter = colors.white, + chevron = colors.yellow, + title = colors.yellow, + titlebg = colors.black, + } + UIconf = { + promptY = 0, + chevron = ": ", + chatlogTop = 2, + title = "YOURNAME on ENCKEY", + doTitle = true, + titleY = 1, + nameDecolor = true, + centerTitle = true, + prefix = "<", + suffix = "> " + } + termsetBackgroundColor(palette.bg) + termclear() + logadd("*","Switched to /rom/programs/rednet/chat.lua palette.") + saveSettings() + elseif argument[1]:gsub("%s",""):lower() == "talk" then + palette = { + bg = colors.black, + txt = colors.white, + promptbg = colors.black, + prompttxt = colors.white, + scrollMeter = colors.white, + chevron = colors.white, + title = colors.black, + titlebg = colors.white, + } + UIconf = { + promptY = 0, + chevron = "", + chatlogTop = 1, + title = " enchat v3.0 channel: ENCKEY:PORT", + titleY = scr_y - 1, + doTitle = true, + nameDecolor = false, + centerTitle = false, + prefix = "<", + suffix = "> " + } + termsetBackgroundColor(palette.bg) + termclear() + logadd("*","Switched to Talk palette.") + saveSettings() + elseif argument[1]:gsub("%s",""):lower() == "darkchat" then + palette = { + bg = colors.black, + txt = colors.white, + promptbg = colors.black, + prompttxt = colors.white, + scrollMeter = colors.white, + chevron = colors.white, + title = colors.white, + titlebg = colors.blue, + } + UIconf = { + promptY = 0, + chevron = "Message: ", + chatlogTop = 1, + title = "<User: YOURNAME> <Channel: ENCKEY>", + titleY = scr_y - 1, + doTitle = true, + nameDecolor = false, + centerTitle = true, + prefix = "", + suffix = ": " + } + termsetBackgroundColor(palette.bg) + termclear() + logadd("*","Switched to DarkChat palette.") + saveSettings() + else + if not palette[argument[1]] then + logadd("*","There's no such palette option.") + else + logadd("*","'"..argument[1].."' = '"..toblit[palette[argument[1]]].."'") + end + end + else + if #argument > 2 then + argument = {argument[1], table.concat(argument," ",2)} + end + argument[1] = argument[1]:lower() + local newcol = argument[2]:lower() + if not palette[argument[1]] then + logadd("*","That's not a valid palette choice.") + else + if not (tocolors[newcol] or colors_strnames[newcol]) then + logadd("*","That isn't a valid color code. (0-f)") + else + palette[argument[1]] = (tocolors[newcol] or colors_strnames[newcol]) + logadd("*","Palette changed.",false) + saveSettings() + end + end + end + end +end +commands.clear = function() + log = {} + IDlog = {} +end +commands.ping = function(pong) + if enchatSettings.extraNewline then + logadd(nil,nil) + end + logadd(nil, pong or "Pong!") +end +commands.set = function(_argument) + if enchatSettings.extraNewline then + logadd(nil,nil) + end + argument = _argument or "" + local collist = { + ["string"] = function() return "0" end, + ["table"] = function() return "5" end, + ["number"] = function() return "0" end, + ["boolean"] = function(val) if val then return "d" else return "e" end end, + ["function"] = function() return "c" end, + ["nil"] = function() return "8" end, + ["thread"] = function() return "d" end, + ["userdata"] = function() return "c" end, -- ha + } + local custColorize = function(input) + return "&"..collist[type(input)](input) + end + local contextualQuote = function(judgetxt,txt) + if type(judgetxt) == "string" then + return table.concat({"'",txt,"'"}) + else + return txt + end + end + local arguments = explode(" ",argument) + if #argument == 0 then + for k,v in pairs(enchatSettings) do + logadd(nil,"&4'"..k.."'&r = "..contextualQuote(v,custColorize(v)..tostring(v).."&r")) + end + else + if enchatSettings[arguments[1]] ~= nil then + if #arguments >= 2 then + local newval = table.concat(arguments," ",2) + if tonumber(newval) then + newval = tonumber(newval) + elseif textutilsunserialize(newval) ~= nil then + newval = textutilsunserialize(newval) + end + if type(enchatSettings[arguments[1]]) == type(newval) then + enchatSettings[arguments[1]] = newval + logadd("*","Set '&4"..arguments[1].."&r' to &{"..contextualQuote(newval,textutilsserialize(newval).."&}").." ("..type(newval)..")") + saveSettings() + else + logadd("*","Wrong value type (it's "..type(enchatSettings[arguments[1]])..")") + end + else + logadd("*","'"..arguments[1].."' is set to "..contextualQuote(enchatSettings[arguments[1]],custColorize(enchatSettings[arguments[1]])..textutilsserialize(enchatSettings[arguments[1]]).."&r").." ("..type(enchatSettings[arguments[1]])..")") + end + else + logadd("*","No such setting.") + end + end + if enchatSettings.useSkynet and (not skynet) then + pauseRendering = true + termsetBackgroundColor(colors.black) + termclear() + pauseRendering = false + end +end +commands.help = function(cmdname) + if enchatSettings.extraNewline then + logadd(nil,nil) + end + if cmdname then + local helpList = { + exit = "Exits Enchat and returns to loader (most likely CraftOS)", + about = "Tells you a bit about this here Enchat.", + me = "Sends a message in the format of \"* yourName message\"", + colors = "Lists all the colors you can use.", + update = "Updates and overwrites Enchat, then exits if successful.", + list = "Lists all users in range using the same key.", + nick = "Give yourself a different username.", + whoami = "Tells you your current username.", + key = "Change the current encryption key. Tells you the key, if without argument.", + clear = "Clears the local chat log. Not your inventory, I swear.", + ping = "Pong. *sigh*", + shrug = "Sends out a shrugging emoticon.", + set = "Changes config options during the current session. Lists all options, if without argument.", + msg = "Sends a message that is only logged by a specific user.", + picto = "Opens an image maker and sends the result. Use the scroll wheel to change color, and hold left shift to change text color. If argument given, will look for an image at the given path and use that instead.", + tron = "Starts up a game of TRON.", + big = "Sends your message, but enlarged by a specified amount via Wojbie's BigFont API.", + help = "Shows every command, or describes a specific command.", + } + cmdname = cmdname:gsub(" ",""):gsub("/","") + if helpList[cmdname] then + logadd("*", helpList[cmdname]) + else + if commands[cmdname] then + logadd("*", "No help info for that command.") + else + logadd("*", "No such command to get help for.") + end + end + else + logadd("*","All commands:") + local output = "" + for k,v in pairs(commands) do + output = output.." "..commandInit..k.."," + end + logadd(nil, output:sub(1,-2)) + end +end +commandAliases = { + quit = commands.exit, + colours = commands.colors, + ls = commands.list, + cry = commands.list, + nickname = commands.nick, + channel = commands.key, + palate = commands.palette, + tell = commands.msg, + whisper = commands.msg, + ["?"] = commands.help, + porn = function() logadd("*","Yeah, no.") end, + whoareyou = function() logadd("*", "I'm Enchat. But surely, you know this?") end, + fuck = function() logadd("*","A mind is a terrible thing to waste.") end, + hello = function() logadd("*","Hey.") end, + hi = function() logadd("*","Hiya.") end, + hey = function() logadd("*","That's for horses.") end, + bye = function() logadd("*","You know, you can use /exit.") end, + die = function() logadd("*","You wish.") end, + nap = function() logadd("*","The time for napping has passed.") end, + sorry = function() logadd("*","That's okay.") end, + jump = function() logadd("*","Sorry. This program is in a NO JUMPING zone.") end, + enchat = function() logadd("*","At your service!") end, + win = function() logadd("*","Naturally!") end, + lose = function() logadd("*","Preposterous!") end, + xyzzy = function() logadd("*","A hollow voice says \"Fool.\"") end, + wait = function() logadd("*","Time passes...") end, + stop = function() logadd("*","Hammertime!","fadeIn") end, + shit = function() logadd("*","Man, you're telling me!") end, + eat = function() logadd("*","You're not hungry.") end, + what = function() logadd("*","What indeed.") end, + ldd = function() logadd(nil,"& that's me") end, + OrElseYouWill = function() + enchatSend("*", "'"..yourName.."&}&r~r' buggered off. (disconnect)") + error("DIE") + end +} + +local checkIfCommand = function(input) + if input:sub(1,#commandInit) == commandInit then + return true + else + return false + end +end + +local parseCommand = function(input) + local sPos1, sPos2 = input:find(" ") + local cmdName, cmdArgs + if sPos1 then + cmdName = input:sub(#commandInit+1, sPos1-1) + cmdArgs = input:sub(sPos2+1) + else + cmdName = input:sub(#commandInit+1) + cmdArgs = nil + end + + local res + local CMD = commands[cmdName] or commandAliases[cmdName] + if CMD then + res = CMD(cmdArgs) + if res == "exit" then + return "exit" + end + else + logadd("*", "No such command.") + end +end + +local main = function() + termsetBackgroundColor(palette.bg) + termclear() + os.queueEvent("render_enchat") + local mHistory = {} + + while true do + + termsetCursorPos(1, scr_y-UIconf.promptY) + termsetBackgroundColor(palette.promptbg) + termclearLine() + termsetTextColor(palette.chevron) + termwrite(UIconf.chevron) + termsetTextColor(palette.prompttxt) + + local input = colorRead(nil, mHistory) + if textToBlit(input,true):gsub(" ","") ~= "" then -- people who send blank messages in chat programs deserve to die + if checkIfCommand(input) then + local res = parseCommand(input) + if res == "exit" then + return "exit" + end + else + if enchatSettings.extraNewline then + logadd(nil,nil,nil,nil,nil,personalID) -- readability is key + end + enchatSend(yourName, input, {doLog = true}) + end + if mHistory[#mHistory] ~= input then + mHistory[#mHistory+1] = input + end + elseif input == "" then + logadd(nil,nil,nil,nil,nil,personalID) + end + os.queueEvent("render_enchat") + + end + +end + +local handleReceiveMessage = function(user, message, animType, maxFrame, _personalID) + if enchatSettings.extraNewline then + logadd(nil,nil,nil,nil,nil,_personalID) -- readability is still key + end + logadd(user, message, animations[animType] and animType or nil, (type(maxFrame) == "number") and maxFrame or nil, nil, _personalID) + os.queueEvent("render_enchat") +end + +local adjScroll = function(distance) + scroll = mathmin(maxScroll, mathmax(0, scroll + distance)) +end + +local checkRSinput = function() + return ( + rs.getInput("front") or + rs.getInput("back") or + rs.getInput("left") or + rs.getInput("right") or + rs.getInput("top") or + rs.getInput("bottom") + ) +end + +local handleEvents = function() + local oldScroll + local keysDown = {} + while true do + local evt = {os.pullEvent()} + if evt[1] == "enchat_receive" then + if type(evt[2]) == "string" and type(evt[3]) == "string" then + handleReceiveMessage(evt[2], evt[3]) + end + elseif evt[1] == "chat" and ((not checkRSinput()) or (not enchat.disableChatboxWithRedstone)) then + if enchat.useChatbox then + if enchatSettings.extraNewline then + logadd(nil,nil) -- readability is key + end + enchatSend(evt[2], evt[3], {doLog = true}) + end + elseif evt[1] == "chat_message" and ((not checkRSinput()) or (not enchat.disableChatboxWithRedstone)) then -- computronics + if enchat.useChatbox then + if enchatSettings.extraNewline then + logadd(nil,nil) -- readability is key + end + enchatSend(evt[3], evt[4], {doLog = true}) + end + elseif (evt[1] == "modem_message") or (evt[1] == "skynet_message" and enchatSettings.useSkynet) then + local side, freq, repfreq, msg, distance + if evt[1] == "modem_message" then + side, freq, repfreq, msg, distance = evt[2], evt[3], evt[4], evt[5], evt[6] + else + freq, msg = evt[2], evt[3] + end + if (freq == enchat.port) or (freq == enchat.skynetPort) then + msg = decrite(msg) + if type(msg) == "table" then + if (type(msg.name) == "string") then + if #msg.name <= 32 then + if msg.messageID and (not IDlog[msg.messageID]) then + userCryList[msg.name] = true + IDlog[msg.messageID] = true + if ((not msg.recipient) or (msg.recipient == yourName or msg.recipient == textToBlit(yourName,true))) then + if type(msg.message) == "string" then + handleReceiveMessage(msg.name, tostring(msg.message), msg.animType, msg.maxFrame, msg.personalID) + if chatbox and enchat.useChatbox and ((not checkRSinput()) or (not enchat.disableChatboxWithRedstone)) then + chatbox.say(UIconf.prefix .. msg.name .. UIconf.suffix .. msg.message, msg.name) + end + elseif type(msg.message) == "table" and enchatSettings.acceptPictoChat and #msg.message <= 64 then + logaddTable(msg.name, msg.message, msg.animType, msg.maxFrame, msg.ignoreWrap, msg.personalID) + if enchatSettings.extraNewline then + logadd(nil,nil) + end + elseif commands[msg.simCommand or false] and type(msg.simArgument) == "string" then + if simmableCommands[msg.simCommand or false] then + commands[msg.simCommand](msg.simArgument, msg.name) + end + end + end + if (msg.cry == true) then + cryOut(yourName, false) + end + end + end + end + end + end + elseif evt[1] == "mouse_scroll" and (not pauseRendering) then + local dist = evt[2] + oldScroll = scroll + adjScroll(enchatSettings.reverseScroll and -dist or dist) + if scroll ~= oldScroll then + dab(renderChat) + end + elseif evt[1] == "key" and (not pauseRendering) then + local key = evt[2] + keysDown[key] = true + oldScroll = scroll + local pageSize = (scr_y-UIconf.promptY) - UIconf.chatlogTop + if key == keys.pageUp then + adjScroll(-(keysDown[keys.leftCtrl] and pageSize or enchatSettings.pageKeySpeed)) + elseif key == keys.pageDown then + adjScroll(keysDown[keys.leftCtrl] and pageSize or enchatSettings.pageKeySpeed) + end + if scroll ~= oldScroll then + dab(renderChat) + end + elseif evt[1] == "key_up" then + local key = evt[2] + keysDown[key] = nil + elseif (evt[1] == "render_enchat") and (not pauseRendering) then + dab(renderChat) + elseif (evt[1] == "tron_complete") then + if evt[3] then + if enchatSettings.extraNewline then + logadd(nil,nil) + end + if evt[2] == "win" then + enchatSend("*", yourName .. "&}&r~r beat " .. (evt[4] or "someone") .. "&}&r~r in TRON!", {doLog = true}) + elseif evt[2] == "lose" then + enchatSend("*", (evt[4] or "Someone") .. "&}&r~r beat " .. yourName .. "&}&r~r in TRON!", {doLog = true}) + elseif evt[2] == "tie" then + enchatSend("*", yourName .. "&}&r~r tied with " .. (evt[4] or "someone") .. "&}&r~r in TRON!", {doLog = true}) + end + elseif evt[2] == "timeout" then + if enchatSettings.extraNewline then + logadd(nil,nil) + end + enchatSend("*", yourName .. "&}&r~r timed out against " .. (evt[4] or "someone") .. "&}&r~r in TRON...", {doLog = true}) + end + elseif evt[1] == "terminate" then + return "exit" + end + end +end + +local keepRedrawing = function() + while true do + sleep(enchatSettings.redrawDelay) + if not pauseRendering then + os.queueEvent("render_enchat") + end + end +end + +local handleNotifications = function() + while true do + os.pullEvent("render_enchat") + if canvas and enchatSettings.doNotif then + notif.displayNotifications(true) + end + end +end + +getModem() + +enchatSend("*", "'"..yourName.."&}&r~r' has moseyed on over.", {doLog = true, omitPersonalID = true}) + +local funky = { + main, + handleEvents, + keepRedrawing, + handleNotifications +} + +if skynet then + funky[#funky+1] = function() + while true do + if skynet then + pcall(skynet.listen) + local success, msg = pcall(skynet.open, enchat.skynetPort) + if not success then + skynet = nil + end + end + sleep(5) + end + end +end + +pauseRendering = false + +local res, outcome = pcall(function() + return parallel.waitForAny(unpack(funky)) +end) + +os.pullEvent = oldePullEvent +if skynet then + if skynet.socket then + skynet.socket.close() + end +end + +if canvas then + canvas.clear() +end + +tsv(true) -- in case it's false, y'know + +if not res then + prettyClearScreen(1,scr_y-1) + termsetTextColor(colors.white) + termsetBackgroundColor(colors.gray) + cwrite("There was an error.",2) + cfwrite("Report this to &3@LDDestroier#2901&r",3) + cwrite("on Discord,",4) + cwrite("if you feel like it.",5) + termsetCursorPos(1,7) + printError(outcome) + termsetTextColor(colors.lightGray) + cwrite("I'll probably fix it, maybe.",10) +end + +termsetCursorPos(1, scr_y) +termsetBackgroundColor(initcolors.bg) +termsetTextColor(initcolors.txt) +termclearLine() diff --git a/Programs/Firewolf.bup/Contents/Info.meta b/Programs/Firewolf.bup/Contents/Info.meta new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/Programs/Firewolf.bup/Contents/Info.meta @@ -0,0 +1 @@ + diff --git a/Programs/Firewolf.bup/Contents/Resources/resources_here.txt b/Programs/Firewolf.bup/Contents/Resources/resources_here.txt new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/Programs/Firewolf.bup/Contents/Resources/resources_here.txt @@ -0,0 +1 @@ + diff --git a/Programs/Firewolf.bup/Contents/bits-UI/firewolf.lua b/Programs/Firewolf.bup/Contents/bits-UI/firewolf.lua new file mode 100644 index 0000000..d1ba857 --- /dev/null +++ b/Programs/Firewolf.bup/Contents/bits-UI/firewolf.lua @@ -0,0 +1,3243 @@ + +-- +-- Firewolf +-- Made by GravityScore and 1lann +-- + + + +-- Variables + + +local version = "3.5" +local build = 18 + +local w, h = term.getSize() + +local isMenubarOpen = true +local menubarWindow = nil + +local allowUnencryptedConnections = true +local enableTabBar = true + +local currentWebsiteURL = "" +local builtInSites = {} + +local currentProtocol = "" +local protocols = {} + +local currentTab = 1 +local maxTabs = 5 +local maxTabNameWidth = 8 +local tabs = {} + +local languages = {} + +local history = {} + +local publicDNSChannel = 9999 +local publicResponseChannel = 9998 +local responseID = 41738 + +local httpTimeout = 10 +local searchResultTimeout = 1 +local initiationTimeout = 2 +local animationInterval = 0.125 +local fetchTimeout = 3 +local serverLimitPerComputer = 1 + +local websiteErrorEvent = "firewolf_websiteErrorEvent" +local redirectEvent = "firewolf_redirectEvent" + +local baseURL = "https://raw.githubusercontent.com/1lann/Firewolf/master/src" +local buildURL = baseURL .. "/build.txt" +local firewolfURL = baseURL .. "/client.lua" +local serverURL = baseURL .. "/server.lua" + +local originalTerminal = term.current() + +local firewolfLocation = "/" .. shell.getRunningProgram() +local downloadsLocation = "/downloads" + + +local theme = {} + +local colorTheme = { + background = colors.gray, + accent = colors.red, + subtle = colors.orange, + + lightText = colors.gray, + text = colors.white, + errorText = colors.red, +} + +local grayscaleTheme = { + background = colors.black, + accent = colors.black, + subtle = colors.black, + + lightText = colors.white, + text = colors.white, + errorText = colors.white, +} + + + +-- Utilities + + +local modifiedRead = function(properties) + local text = "" + local startX, startY = term.getCursorPos() + local pos = 0 + + local previousText = "" + local readHistory = nil + local historyPos = 0 + + if not properties then + properties = {} + end + + if properties.displayLength then + properties.displayLength = math.min(properties.displayLength, w - 2) + else + properties.displayLength = w - startX - 1 + end + + if properties.startingText then + text = properties.startingText + pos = text:len() + end + + if properties.history then + readHistory = {} + for k, v in pairs(properties.history) do + readHistory[k] = v + end + end + + if readHistory[1] == text then + table.remove(readHistory, 1) + end + + local draw = function(replaceCharacter) + local scroll = 0 + if properties.displayLength and pos > properties.displayLength then + scroll = pos - properties.displayLength + end + + local repl = replaceCharacter or properties.replaceCharacter + term.setTextColor(theme.text) + term.setCursorPos(startX, startY) + if repl then + term.write(string.rep(repl:sub(1, 1), text:len() - scroll)) + else + term.write(text:sub(scroll + 1)) + end + + term.setCursorPos(startX + pos - scroll, startY) + end + + term.setCursorBlink(true) + draw() + while true do + local event, key, x, y, param4, param5 = os.pullEvent() + + if properties.onEvent then + -- Actions: + -- - exit (bool) + -- - text + -- - nullifyText + + term.setCursorBlink(false) + local action = properties.onEvent(text, event, key, x, y, param4, param5) + if action then + if action.text then + draw(" ") + text = action.text + pos = text:len() + end if action.nullifyText then + text = nil + action.exit = true + end if action.exit then + break + end + end + draw() + end + + term.setCursorBlink(true) + if event == "char" then + local canType = true + if properties.maxLength and text:len() >= properties.maxLength then + canType = false + end + + if canType then + text = text:sub(1, pos) .. key .. text:sub(pos + 1, -1) + pos = pos + 1 + draw() + end + elseif event == "key" then + if key == keys.enter then + break + elseif key == keys.left and pos > 0 then + pos = pos - 1 + draw() + elseif key == keys.right and pos < text:len() then + pos = pos + 1 + draw() + elseif key == keys.backspace and pos > 0 then + draw(" ") + text = text:sub(1, pos - 1) .. text:sub(pos + 1, -1) + pos = pos - 1 + draw() + elseif key == keys.delete and pos < text:len() then + draw(" ") + text = text:sub(1, pos) .. text:sub(pos + 2, -1) + draw() + elseif key == keys.home then + pos = 0 + draw() + elseif key == keys["end"] then + pos = text:len() + draw() + elseif (key == keys.up or key == keys.down) and readHistory then + local shouldDraw = false + if historyPos == 0 then + previousText = text + elseif historyPos > 0 then + readHistory[historyPos] = text + end + + if key == keys.up then + if historyPos < #readHistory then + historyPos = historyPos + 1 + shouldDraw = true + end + else + if historyPos > 0 then + historyPos = historyPos - 1 + shouldDraw = true + end + end + + if shouldDraw then + draw(" ") + if historyPos > 0 then + text = readHistory[historyPos] + else + text = previousText + end + pos = text:len() + draw() + end + end + elseif event == "mouse_click" then + local scroll = 0 + if properties.displayLength and pos > properties.displayLength then + scroll = pos - properties.displayLength + end + + if y == startY and x >= startX and x <= math.min(startX + text:len(), startX + (properties.displayLength or 10000)) then + pos = x - startX + scroll + draw() + elseif y == startY then + if x < startX then + pos = scroll + draw() + elseif x > math.min(startX + text:len(), startX + (properties.displayLength or 10000)) then + pos = text:len() + draw() + end + end + end + end + + term.setCursorBlink(false) + print("") + return text +end + + +local prompt = function(items, x, y, w, h) + local selected = 1 + local scroll = 0 + + local draw = function() + for i = scroll + 1, scroll + h do + local item = items[i] + if item then + term.setCursorPos(x, y + i - 1) + term.setBackgroundColor(theme.background) + term.setTextColor(theme.lightText) + + if scroll + selected == i then + term.setTextColor(theme.text) + term.write(" > ") + else + term.write(" - ") + end + + term.write(item) + end + end + end + + draw() + while true do + local event, key, x, y = os.pullEvent() + + if event == "key" then + if key == keys.up and selected > 1 then + selected = selected - 1 + + if selected - scroll == 0 then + scroll = scroll - 1 + end + elseif key == keys.down and selected < #items then + selected = selected + 1 + end + + draw() + elseif event == "mouse_click" then + + elseif event == "mouse_scroll" then + if key > 0 then + os.queueEvent("key", keys.down) + else + os.queueEvent("key", keys.up) + end + end + end +end + + + +-- GUI + + +local clear = function(bg, fg) + term.setTextColor(fg) + term.setBackgroundColor(bg) + term.clear() + term.setCursorPos(1, 1) +end + + +local fill = function(x, y, width, height, bg) + term.setBackgroundColor(bg) + for i = y, y + height - 1 do + term.setCursorPos(x, i) + term.write(string.rep(" ", width)) + end +end + + +local center = function(text) + local x, y = term.getCursorPos() + term.setCursorPos(math.floor(w / 2 - text:len() / 2) + (text:len() % 2 == 0 and 1 or 0), y) + term.write(text) + term.setCursorPos(1, y + 1) +end + + +local centerSplit = function(text, width) + local words = {} + for word in text:gmatch("[^ \t]+") do + table.insert(words, word) + end + + local lines = {""} + while lines[#lines]:len() < width do + lines[#lines] = lines[#lines] .. words[1] .. " " + table.remove(words, 1) + + if #words == 0 then + break + end + + if lines[#lines]:len() + words[1]:len() >= width then + table.insert(lines, "") + end + end + + for _, line in pairs(lines) do + center(line) + end +end + + + +-- Updating + + +local download = function(url) + http.request(url) + local timeoutID = os.startTimer(httpTimeout) + while true do + local event, fetchedURL, response = os.pullEvent() + if (event == "timer" and fetchedURL == timeoutID) or event == "http_failure" then + return false + elseif event == "http_success" and fetchedURL == url then + local contents = response.readAll() + response.close() + return contents + end + end +end + + +local downloadAndSave = function(url, path) + local contents = download(url) + if contents and not fs.isReadOnly(path) and not fs.isDir(path) then + local f = io.open(path, "w") + f:write(contents) + f:close() + return false + end + return true +end + + +local updateAvailable = function() + local number = download(buildURL) + if not number then + return false, true + end + + if number and tonumber(number) and tonumber(number) > build then + return true, false + end + + return false, false +end + + +local redownloadBrowser = function() + return downloadAndSave(firewolfURL, firewolfLocation) +end + + + +-- Display Websites + + +builtInSites["display"] = {} + + +builtInSites["display"]["firewolf"] = function() + local logo = { + "______ _ __ ", + "| ___| | |/ _|", + "| |_ _ ____ _____ _____ | | |_ ", + "| _|| | __/ _ \\ \\ /\\ / / _ \\| | _|", + "| | | | | | __/\\ V V / <_> | | | ", + "\\_| |_|_| \\___| \\_/\\_/ \\___/|_|_| ", + } + + clear(theme.background, theme.text) + fill(1, 3, w, 9, theme.subtle) + + term.setCursorPos(1, 3) + for _, line in pairs(logo) do + center(line) + end + + term.setCursorPos(1, 10) + center(version) + + term.setBackgroundColor(theme.background) + term.setTextColor(theme.text) + term.setCursorPos(1, 14) + center("Search using the Query Box above") + center("Visit rdnt://help for help using Firewolf.") + + term.setCursorPos(1, h - 2) + center("Made by GravityScore and 1lann") +end + + +builtInSites["display"]["credits"] = function() + clear(theme.background, theme.text) + + fill(1, 6, w, 3, theme.subtle) + term.setCursorPos(1, 7) + center("Credits") + + term.setBackgroundColor(theme.background) + term.setCursorPos(1, 11) + center("Written by GravityScore and 1lann") + print("") + center("RC4 Implementation by AgentE382") +end + + +builtInSites["display"]["help"] = function() + clear(theme.background, theme.text) + + fill(1, 3, w, 3, theme.subtle) + term.setCursorPos(1, 4) + center("Help") + + term.setBackgroundColor(theme.background) + term.setCursorPos(1, 7) + center("Click on the URL bar or press control to") + center("open the query box") + print("") + center("Type in a search query or website URL") + center("into the query box.") + print("") + center("Search for nothing to see all available") + center("websites.") + print("") + center("Visit rdnt://server to setup a server.") + center("Visit rdnt://update to update Firewolf.") +end + + +builtInSites["display"]["server"] = function() + clear(theme.background, theme.text) + + fill(1, 6, w, 3, theme.subtle) + term.setCursorPos(1, 7) + center("Server Software") + + term.setBackgroundColor(theme.background) + term.setCursorPos(1, 11) + if not http then + center("HTTP is not enabled!") + print("") + center("Please enable it in your config file") + center("to download Firewolf Server.") + else + center("Press space to download") + center("Firewolf Server to:") + print("") + center("/fwserver") + + while true do + local event, key = os.pullEvent() + if event == "key" and key == 57 then + fill(1, 11, w, 4, theme.background) + term.setCursorPos(1, 11) + center("Downloading...") + + local err = downloadAndSave(serverURL, "/fwserver") + + fill(1, 11, w, 4, theme.background) + term.setCursorPos(1, 11) + center(err and "Download failed!" or "Download successful!") + end + end + end +end + + +builtInSites["display"]["update"] = function() + clear(theme.background, theme.text) + + fill(1, 3, w, 3, theme.subtle) + term.setCursorPos(1, 4) + center("Update") + + term.setBackgroundColor(theme.background) + if not http then + term.setCursorPos(1, 9) + center("HTTP is not enabled!") + print("") + center("Please enable it in your config") + center("file to download Firewolf updates.") + else + term.setCursorPos(1, 10) + center("Checking for updates...") + + local available, err = updateAvailable() + + term.setCursorPos(1, 10) + if available then + term.clearLine() + center("Update found!") + center("Press enter to download.") + + while true do + local event, key = os.pullEvent() + if event == "key" and key == keys.enter then + break + end + end + + fill(1, 10, w, 2, theme.background) + term.setCursorPos(1, 10) + center("Downloading...") + + local err = redownloadBrowser() + + term.setCursorPos(1, 10) + term.clearLine() + if err then + center("Download failed!") + else + center("Download succeeded!") + center("Please restart Firewolf...") + end + elseif err then + term.clearLine() + center("Checking failed!") + else + term.clearLine() + center("No updates found.") + end + end +end + + + +-- Built In Websites + + +builtInSites["error"] = function(err) + fill(1, 3, w, 3, theme.subtle) + term.setCursorPos(1, 4) + center("Failed to load page!") + + term.setBackgroundColor(theme.background) + term.setCursorPos(1, 9) + center(err) + print("") + center("Please try again.") +end + + +builtInSites["noresults"] = function() + fill(1, 3, w, 3, theme.subtle) + term.setCursorPos(1, 4) + center("No results!") + + term.setBackgroundColor(theme.background) + term.setCursorPos(1, 9) + center("Your search didn't return") + center("any results!") + + os.pullEvent("key") + os.queueEvent("") + os.pullEvent() +end + + +builtInSites["search advanced"] = function(results) + local startY = 6 + local height = h - startY - 1 + local scroll = 0 + + local draw = function() + fill(1, startY, w, height + 1, theme.background) + + for i = scroll + 1, scroll + height do + if results[i] then + term.setCursorPos(5, (i - scroll) + startY) + term.write(currentProtocol .. "://" .. results[i]) + end + end + end + + draw() + while true do + local event, but, x, y = os.pullEvent() + + if event == "mouse_click" and y >= startY and y <= startY + height then + local item = results[y - startY + scroll] + if item then + os.queueEvent(redirectEvent, item) + coroutine.yield() + end + elseif event == "key" then + if but == keys.up then + scroll = math.max(0, scroll - 1) + elseif but == keys.down and #results > height then + scroll = math.min(scroll + 1, #results - height) + end + + draw() + elseif event == "mouse_scroll" then + if but > 0 then + os.queueEvent("key", keys.down) + else + os.queueEvent("key", keys.up) + end + end + end +end + + +builtInSites["search basic"] = function(results) + local startY = 6 + local height = h - startY - 1 + local scroll = 0 + local selected = 1 + + local draw = function() + fill(1, startY, w, height + 1, theme.background) + + for i = scroll + 1, scroll + height do + if results[i] then + if i == selected + scroll then + term.setCursorPos(3, (i - scroll) + startY) + term.write("> " .. currentProtocol .. "://" .. results[i]) + else + term.setCursorPos(5, (i - scroll) + startY) + term.write(currentProtocol .. "://" .. results[i]) + end + end + end + end + + draw() + while true do + local event, but, x, y = os.pullEvent() + + if event == "key" then + if but == keys.up and selected + scroll > 1 then + if selected > 1 then + selected = selected - 1 + else + scroll = math.max(0, scroll - 1) + end + elseif but == keys.down and selected + scroll < #results then + if selected < height then + selected = selected + 1 + else + scroll = math.min(scroll + 1, #results - height) + end + elseif but == keys.enter then + local item = results[scroll + selected] + if item then + os.queueEvent(redirectEvent, item) + coroutine.yield() + end + end + + draw() + elseif event == "mouse_scroll" then + if but > 0 then + os.queueEvent("key", keys.down) + else + os.queueEvent("key", keys.up) + end + end + end +end + + +builtInSites["search"] = function(results) + clear(theme.background, theme.text) + + fill(1, 3, w, 3, theme.subtle) + term.setCursorPos(1, 4) + center(#results .. " Search " .. (#results == 1 and "Result" or "Results")) + + term.setBackgroundColor(theme.background) + + if term.isColor() then + builtInSites["search advanced"](results) + else + builtInSites["search basic"](results) + end +end + + +builtInSites["crash"] = function(err) + fill(1, 3, w, 3, theme.subtle) + term.setCursorPos(1, 4) + center("The website crashed!") + + term.setBackgroundColor(theme.background) + term.setCursorPos(1, 8) + centerSplit(err, w - 4) + print("\n") + center("Please report this error to") + center("the website creator.") +end + + + +-- Menubar + + +local getTabName = function(url) + local name = url:match("^[^/]+") + + if not name then + name = "Search" + end + + if name:sub(1, 3) == "www" then + name = name:sub(5):gsub("^%s*(.-)%s*$", "%1") + end + + if name:len() > maxTabNameWidth then + name = name:sub(1, maxTabNameWidth):gsub("^%s*(.-)%s*$", "%1") + end + + if name:sub(-1, -1) == "." then + name = name:sub(1, -2):gsub("^%s*(.-)%s*$", "%1") + end + + return name:gsub("^%s*(.-)%s*$", "%1") +end + + +local determineClickedTab = function(x, y) + if y == 2 then + local minx = 2 + for i, tab in pairs(tabs) do + local name = getTabName(tab.url) + + if x >= minx and x <= minx + name:len() - 1 then + return i + elseif x == minx + name:len() and i == currentTab and #tabs > 1 then + return "close" + else + minx = minx + name:len() + 2 + end + end + + if x == minx and #tabs < maxTabs then + return "new" + end + end + + return nil +end + + +local setupMenubar = function() + if enableTabBar then + menubarWindow = window.create(originalTerminal, 1, 1, w, 2, false) + else + menubarWindow = window.create(originalTerminal, 1, 1, w, 1, false) + end +end + + +local drawMenubar = function() + if isMenubarOpen then + term.redirect(menubarWindow) + menubarWindow.setVisible(true) + + fill(1, 1, w, 1, theme.accent) + term.setTextColor(theme.text) + + term.setBackgroundColor(theme.accent) + term.setCursorPos(2, 1) + if currentWebsiteURL:match("^[^%?]+") then + term.write(currentProtocol .. "://" .. currentWebsiteURL:match("^[^%?]+")) + else + term.write(currentProtocol .. "://" ..currentWebsiteURL) + end + + term.setCursorPos(w - 5, 1) + term.write("[===]") + + if enableTabBar then + fill(1, 2, w, 1, theme.subtle) + + term.setCursorPos(1, 2) + for i, tab in pairs(tabs) do + term.setBackgroundColor(theme.subtle) + term.setTextColor(theme.lightText) + if i == currentTab then + term.setTextColor(theme.text) + end + + local tabName = getTabName(tab.url) + term.write(" " .. tabName) + + if i == currentTab and #tabs > 1 then + term.setTextColor(theme.errorText) + term.write("x") + else + term.write(" ") + end + end + + if #tabs < maxTabs then + term.setTextColor(theme.lightText) + term.setBackgroundColor(theme.subtle) + term.write(" + ") + end + end + else + menubarWindow.setVisible(false) + end +end + + + +-- RC4 +-- Implementation by AgentE382 + + +local cryptWrapper = function(plaintext, salt) + local key = type(salt) == "table" and {unpack(salt)} or {string.byte(salt, 1, #salt)} + local S = {} + for i = 0, 255 do + S[i] = i + end + + local j, keylength = 0, #key + for i = 0, 255 do + j = (j + S[i] + key[i % keylength + 1]) % 256 + S[i], S[j] = S[j], S[i] + end + + local i = 0 + j = 0 + local chars, astable = type(plaintext) == "table" and {unpack(plaintext)} or {string.byte(plaintext, 1, #plaintext)}, false + + for n = 1, #chars do + i = (i + 1) % 256 + j = (j + S[i]) % 256 + S[i], S[j] = S[j], S[i] + chars[n] = bit.bxor(S[(S[i] + S[j]) % 256], chars[n]) + if chars[n] > 127 or chars[n] == 13 then + astable = true + end + end + + return astable and chars or string.char(unpack(chars)) +end + + +local crypt = function(text, key) + local resp, msg = pcall(cryptWrapper, text, key) + if resp then + return msg + else + return nil + end +end + + + +-- Base64 +-- +-- Base64 Encryption/Decryption +-- By KillaVanilla +-- http://www.computercraft.info/forums2/index.php?/topic/12450-killavanillas-various-apis/ +-- http://pastebin.com/rCYDnCxn +-- + + +local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + + +local function sixBitToBase64(input) + return string.sub(alphabet, input+1, input+1) +end + + +local function base64ToSixBit(input) + for i=1, 64 do + if input == string.sub(alphabet, i, i) then + return i-1 + end + end +end + + +local function octetToBase64(o1, o2, o3) + local shifted = bit.brshift(bit.band(o1, 0xFC), 2) + local i1 = sixBitToBase64(shifted) + local i2 = "A" + local i3 = "=" + local i4 = "=" + if o2 then + i2 = sixBitToBase64(bit.bor( bit.blshift(bit.band(o1, 3), 4), bit.brshift(bit.band(o2, 0xF0), 4) )) + if not o3 then + i3 = sixBitToBase64(bit.blshift(bit.band(o2, 0x0F), 2)) + else + i3 = sixBitToBase64(bit.bor( bit.blshift(bit.band(o2, 0x0F), 2), bit.brshift(bit.band(o3, 0xC0), 6) )) + end + else + i2 = sixBitToBase64(bit.blshift(bit.band(o1, 3), 4)) + end + if o3 then + i4 = sixBitToBase64(bit.band(o3, 0x3F)) + end + + return i1..i2..i3..i4 +end + + +local function base64ToThreeOctet(s1) + local c1 = base64ToSixBit(string.sub(s1, 1, 1)) + local c2 = base64ToSixBit(string.sub(s1, 2, 2)) + local c3 = 0 + local c4 = 0 + local o1 = 0 + local o2 = 0 + local o3 = 0 + if string.sub(s1, 3, 3) == "=" then + c3 = nil + c4 = nil + elseif string.sub(s1, 4, 4) == "=" then + c3 = base64ToSixBit(string.sub(s1, 3, 3)) + c4 = nil + else + c3 = base64ToSixBit(string.sub(s1, 3, 3)) + c4 = base64ToSixBit(string.sub(s1, 4, 4)) + end + o1 = bit.bor( bit.blshift(c1, 2), bit.brshift(bit.band( c2, 0x30 ), 4) ) + if c3 then + o2 = bit.bor( bit.blshift(bit.band(c2, 0x0F), 4), bit.brshift(bit.band( c3, 0x3C ), 2) ) + else + o2 = nil + end + if c4 then + o3 = bit.bor( bit.blshift(bit.band(c3, 3), 6), c4 ) + else + o3 = nil + end + return o1, o2, o3 +end + + +local function splitIntoBlocks(bytes) + local blockNum = 1 + local blocks = {} + for i=1, #bytes, 3 do + blocks[blockNum] = {bytes[i], bytes[i+1], bytes[i+2]} + blockNum = blockNum+1 + end + return blocks +end + + +function base64Encode(bytes) + local blocks = splitIntoBlocks(bytes) + local output = "" + for i=1, #blocks do + output = output..octetToBase64( unpack(blocks[i]) ) + end + return output +end + + +function base64Decode(str) + local bytes = {} + local blocks = {} + local blockNum = 1 + + for i=1, #str, 4 do + blocks[blockNum] = string.sub(str, i, i+3) + blockNum = blockNum+1 + end + + for i=1, #blocks do + local o1, o2, o3 = base64ToThreeOctet(blocks[i]) + table.insert(bytes, o1) + table.insert(bytes, o2) + table.insert(bytes, o3) + end + + return bytes +end + + + +-- SHA-256 +-- +-- Adaptation of the Secure Hashing Algorithm (SHA-244/256) +-- Found Here: http://lua-users.org/wiki/SecureHashAlgorithm +-- +-- Using an adapted version of the bit library +-- Found Here: https://bitbucket.org/Boolsheet/bslf/src/1ee664885805/bit.lua + + +local MOD = 2^32 +local MODM = MOD-1 + + +local function memoize(f) + local mt = {} + local t = setmetatable({}, mt) + function mt:__index(k) + local v = f(k) + t[k] = v + return v + end + return t +end + + +local function make_bitop_uncached(t, m) + local function bitop(a, b) + local res,p = 0,1 + while a ~= 0 and b ~= 0 do + local am, bm = a % m, b % m + res = res + t[am][bm] * p + a = (a - am) / m + b = (b - bm) / m + p = p * m + end + res = res + (a + b) * p + return res + end + + return bitop +end + + +local function make_bitop(t) + local op1 = make_bitop_uncached(t,2^1) + local op2 = memoize(function(a) + return memoize(function(b) + return op1(a, b) + end) + end) + return make_bitop_uncached(op2, 2 ^ (t.n or 1)) +end + + +local customBxor1 = make_bitop({[0] = {[0] = 0,[1] = 1}, [1] = {[0] = 1, [1] = 0}, n = 4}) + +local function customBxor(a, b, c, ...) + local z = nil + if b then + a = a % MOD + b = b % MOD + z = customBxor1(a, b) + if c then + z = customBxor(z, c, ...) + end + return z + elseif a then + return a % MOD + else + return 0 + end +end + + +local function customBand(a, b, c, ...) + local z + if b then + a = a % MOD + b = b % MOD + z = ((a + b) - customBxor1(a,b)) / 2 + if c then + z = customBand(z, c, ...) + end + return z + elseif a then + return a % MOD + else + return MODM + end +end + + +local function bnot(x) + return (-1 - x) % MOD +end + + +local function rshift1(a, disp) + if disp < 0 then + return lshift(a, -disp) + end + return math.floor(a % 2 ^ 32 / 2 ^ disp) +end + + +local function rshift(x, disp) + if disp > 31 or disp < -31 then + return 0 + end + return rshift1(x % MOD, disp) +end + + +local function lshift(a, disp) + if disp < 0 then + return rshift(a, -disp) + end + return (a * 2 ^ disp) % 2 ^ 32 +end + + +local function rrotate(x, disp) + x = x % MOD + disp = disp % 32 + local low = customBand(x, 2 ^ disp - 1) + return rshift(x, disp) + lshift(low, 32 - disp) +end + + +local k = { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, + 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, + 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, + 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, + 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, + 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, + 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, + 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, + 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, +} + + +local function str2hexa(s) + return (string.gsub(s, ".", function(c) + return string.format("%02x", string.byte(c)) + end)) +end + + +local function num2s(l, n) + local s = "" + for i = 1, n do + local rem = l % 256 + s = string.char(rem) .. s + l = (l - rem) / 256 + end + return s +end + + +local function s232num(s, i) + local n = 0 + for i = i, i + 3 do + n = n*256 + string.byte(s, i) + end + return n +end + + +local function preproc(msg, len) + local extra = 64 - ((len + 9) % 64) + len = num2s(8 * len, 8) + msg = msg .. "\128" .. string.rep("\0", extra) .. len + assert(#msg % 64 == 0) + return msg +end + + +local function initH256(H) + H[1] = 0x6a09e667 + H[2] = 0xbb67ae85 + H[3] = 0x3c6ef372 + H[4] = 0xa54ff53a + H[5] = 0x510e527f + H[6] = 0x9b05688c + H[7] = 0x1f83d9ab + H[8] = 0x5be0cd19 + return H +end + + +local function digestblock(msg, i, H) + local w = {} + for j = 1, 16 do + w[j] = s232num(msg, i + (j - 1)*4) + end + for j = 17, 64 do + local v = w[j - 15] + local s0 = customBxor(rrotate(v, 7), rrotate(v, 18), rshift(v, 3)) + v = w[j - 2] + w[j] = w[j - 16] + s0 + w[j - 7] + customBxor(rrotate(v, 17), rrotate(v, 19), rshift(v, 10)) + end + + local a, b, c, d, e, f, g, h = H[1], H[2], H[3], H[4], H[5], H[6], H[7], H[8] + for i = 1, 64 do + local s0 = customBxor(rrotate(a, 2), rrotate(a, 13), rrotate(a, 22)) + local maj = customBxor(customBand(a, b), customBand(a, c), customBand(b, c)) + local t2 = s0 + maj + local s1 = customBxor(rrotate(e, 6), rrotate(e, 11), rrotate(e, 25)) + local ch = customBxor (customBand(e, f), customBand(bnot(e), g)) + local t1 = h + s1 + ch + k[i] + w[i] + h, g, f, e, d, c, b, a = g, f, e, d + t1, c, b, a, t1 + t2 + end + + H[1] = customBand(H[1] + a) + H[2] = customBand(H[2] + b) + H[3] = customBand(H[3] + c) + H[4] = customBand(H[4] + d) + H[5] = customBand(H[5] + e) + H[6] = customBand(H[6] + f) + H[7] = customBand(H[7] + g) + H[8] = customBand(H[8] + h) +end + + +local function sha256(msg) + msg = preproc(msg, #msg) + local H = initH256({}) + for i = 1, #msg, 64 do + digestblock(msg, i, H) + end + return str2hexa(num2s(H[1], 4) .. num2s(H[2], 4) .. num2s(H[3], 4) .. num2s(H[4], 4) .. + num2s(H[5], 4) .. num2s(H[6], 4) .. num2s(H[7], 4) .. num2s(H[8], 4)) +end + + +local protocolName = "Firewolf" + + + +-- Cryptography + + +local Cryptography = {} +Cryptography.sha = {} +Cryptography.base64 = {} +Cryptography.aes = {} + + +function Cryptography.bytesFromMessage(msg) + local bytes = {} + + for i = 1, msg:len() do + local letter = string.byte(msg:sub(i, i)) + table.insert(bytes, letter) + end + + return bytes +end + + +function Cryptography.messageFromBytes(bytes) + local msg = "" + + for i = 1, #bytes do + local letter = string.char(bytes[i]) + msg = msg .. letter + end + + return msg +end + + +function Cryptography.bytesFromKey(key) + local bytes = {} + + for i = 1, key:len() / 2 do + local group = key:sub((i - 1) * 2 + 1, (i - 1) * 2 + 1) + local num = tonumber(group, 16) + table.insert(bytes, num) + end + + return bytes +end + + +function Cryptography.sha.sha256(msg) + return sha256(msg) +end + + +function Cryptography.aes.encrypt(msg, key) + return base64Encode(crypt(msg, key)) +end + + +function Cryptography.aes.decrypt(msg, key) + return crypt(base64Decode(msg), key) +end + + +function Cryptography.base64.encode(msg) + return base64Encode(Cryptography.bytesFromMessage(msg)) +end + + +function Cryptography.base64.decode(msg) + return Cryptography.messageFromBytes(base64Decode(msg)) +end + + +function Cryptography.channel(text) + local hashed = Cryptography.sha.sha256(text) + + local total = 0 + + for i = 1, hashed:len() do + total = total + string.byte(hashed:sub(i, i)) + end + + return (total % 55530) + 10000 +end + + +function Cryptography.sanatize(text) + local sanatizeChars = {"%", "(", ")", "[", "]", ".", "+", "-", "*", "?", "^", "$"} + + for _, char in pairs(sanatizeChars) do + text = text:gsub("%"..char, "%%%"..char) + end + return text +end + + + +-- Modem + + +local Modem = {} +Modem.modems = {} + + +function Modem.exists() + Modem.exists = false + for _, side in pairs(rs.getSides()) do + if peripheral.isPresent(side) and peripheral.getType(side) == "modem" then + Modem.exists = true + + if not Modem.modems[side] then + Modem.modems[side] = peripheral.wrap(side) + end + end + end + + return Modem.exists +end + + +function Modem.open(channel) + if not Modem.exists then + return false + end + + for side, modem in pairs(Modem.modems) do + modem.open(channel) + rednet.open(side) + end + + return true +end + + +function Modem.close(channel) + if not Modem.exists then + return false + end + + for side, modem in pairs(Modem.modems) do + modem.close(channel) + end + + return true +end + + +function Modem.closeAll() + if not Modem.exists then + return false + end + + for side, modem in pairs(Modem.modems) do + modem.closeAll() + end + + return true +end + + +function Modem.isOpen(channel) + if not Modem.exists then + return false + end + + local isOpen = false + for side, modem in pairs(Modem.modems) do + if modem.isOpen(channel) then + isOpen = true + break + end + end + + return isOpen +end + + +function Modem.transmit(channel, msg) + if not Modem.exists then + return false + end + + if not Modem.isOpen(channel) then + Modem.open(channel) + end + + for side, modem in pairs(Modem.modems) do + modem.transmit(channel, channel, msg) + end + + return true +end + + + +-- Handshake + + +local Handshake = {} + +Handshake.prime = 625210769 +Handshake.channel = 54569 +Handshake.base = -1 +Handshake.secret = -1 +Handshake.sharedSecret = -1 +Handshake.packetHeader = "["..protocolName.."-Handshake-Packet-Header]" +Handshake.packetMatch = "%["..protocolName.."%-Handshake%-Packet%-Header%](.+)" + + +function Handshake.exponentWithModulo(base, exponent, modulo) + local remainder = base + + for i = 1, exponent-1 do + remainder = remainder * remainder + if remainder >= modulo then + remainder = remainder % modulo + end + end + + return remainder +end + + +function Handshake.clear() + Handshake.base = -1 + Handshake.secret = -1 + Handshake.sharedSecret = -1 +end + + +function Handshake.generateInitiatorData() + Handshake.base = math.random(10,99999) + Handshake.secret = math.random(10,99999) + return { + type = "initiate", + prime = Handshake.prime, + base = Handshake.base, + moddedSecret = Handshake.exponentWithModulo(Handshake.base, Handshake.secret, Handshake.prime) + } +end + + +function Handshake.generateResponseData(initiatorData) + local isPrimeANumber = type(initiatorData.prime) == "number" + local isPrimeMatching = initiatorData.prime == Handshake.prime + local isBaseANumber = type(initiatorData.base) == "number" + local isInitiator = initiatorData.type == "initiate" + local isModdedSecretANumber = type(initiatorData.moddedSecret) == "number" + local areAllNumbersNumbers = isPrimeANumber and isBaseANumber and isModdedSecretANumber + + if areAllNumbersNumbers and isPrimeMatching then + if isInitiator then + Handshake.base = initiatorData.base + Handshake.secret = math.random(10,99999) + Handshake.sharedSecret = Handshake.exponentWithModulo(initiatorData.moddedSecret, Handshake.secret, Handshake.prime) + return { + type = "response", + prime = Handshake.prime, + base = Handshake.base, + moddedSecret = Handshake.exponentWithModulo(Handshake.base, Handshake.secret, Handshake.prime) + }, Handshake.sharedSecret + elseif initiatorData.type == "response" and Handshake.base > 0 and Handshake.secret > 0 then + Handshake.sharedSecret = Handshake.exponentWithModulo(initiatorData.moddedSecret, Handshake.secret, Handshake.prime) + return Handshake.sharedSecret + else + return false + end + else + return false + end +end + + + +-- Secure Connection + + +local SecureConnection = {} +SecureConnection.__index = SecureConnection + + +SecureConnection.packetHeaderA = "["..protocolName.."-" +SecureConnection.packetHeaderB = "-SecureConnection-Packet-Header]" +SecureConnection.packetMatchA = "%["..protocolName.."%-" +SecureConnection.packetMatchB = "%-SecureConnection%-Packet%-Header%](.+)" +SecureConnection.connectionTimeout = 0.1 +SecureConnection.successPacketTimeout = 0.1 + + +function SecureConnection.new(secret, key, identifier, distance, isRednet) + local self = setmetatable({}, SecureConnection) + self:setup(secret, key, identifier, distance, isRednet) + return self +end + + +function SecureConnection:setup(secret, key, identifier, distance, isRednet) + local rawSecret + + if isRednet then + self.isRednet = true + self.distance = -1 + self.rednet_id = distance + rawSecret = protocolName .. "|" .. tostring(secret) .. "|" .. tostring(identifier) .. + "|" .. tostring(key) .. "|rednet" + else + self.isRednet = false + self.distance = distance + rawSecret = protocolName .. "|" .. tostring(secret) .. "|" .. tostring(identifier) .. + "|" .. tostring(key) .. "|" .. tostring(distance) + end + + self.identifier = identifier + self.packetMatch = SecureConnection.packetMatchA .. Cryptography.sanatize(identifier) .. SecureConnection.packetMatchB + self.packetHeader = SecureConnection.packetHeaderA .. identifier .. SecureConnection.packetHeaderB + self.secret = Cryptography.sha.sha256(rawSecret) + self.channel = Cryptography.channel(self.secret) + + if not self.isRednet then + Modem.open(self.channel) + end +end + + +function SecureConnection:verifyHeader(msg) + if msg:match(self.packetMatch) then + return true + else + return false + end +end + + +function SecureConnection:sendMessage(msg, rednetProtocol) + local rawEncryptedMsg = Cryptography.aes.encrypt(self.packetHeader .. msg, self.secret) + local encryptedMsg = self.packetHeader .. rawEncryptedMsg + + if self.isRednet then + rednet.send(self.rednet_id, encryptedMsg, rednetProtocol) + return true + else + return Modem.transmit(self.channel, encryptedMsg) + end +end + + +function SecureConnection:decryptMessage(msg) + if self:verifyHeader(msg) then + local encrypted = msg:match(self.packetMatch) + + local unencryptedMsg = nil + pcall(function() unencryptedMsg = Cryptography.aes.decrypt(encrypted, self.secret) end) + if not unencryptedMsg then + return false, "Could not decrypt" + end + + if self:verifyHeader(unencryptedMsg) then + return true, unencryptedMsg:match(self.packetMatch) + else + return false, "Could not verify" + end + else + return false, "Could not stage 1 verify" + end +end + + + +-- RDNT Protocol + + +protocols["rdnt"] = {} + +local header = {} +header.dnsPacket = "[Firewolf-DNS-Packet]" +header.dnsHeaderMatch = "^%[Firewolf%-DNS%-Response%](.+)$" +header.rednetHeader = "[Firewolf-Rednet-Channel-Simulation]" +header.rednetMatch = "^%[Firewolf%-Rednet%-Channel%-Simulation%](%d+)$" +header.responseMatchA = "^%[Firewolf%-" +header.responseMatchB = "%-" +header.responseMatchC = "%-Handshake%-Response%](.+)$" +header.requestHeaderA = "[Firewolf-" +header.requestHeaderB = "-Handshake-Request]" +header.pageRequestHeaderA = "[Firewolf-" +header.pageRequestHeaderB = "-Page-Request]" +header.pageResponseMatchA = "^%[Firewolf%-" +header.pageResponseMatchB = "%-Page%-Response%]%[HEADER%](.-)%[BODY%](.+)$" +header.closeHeaderA = "[Firewolf-" +header.closeHeaderB = "-Connection-Close]" + + +protocols["rdnt"]["setup"] = function() + if not Modem.exists() then + error("No modem found!") + end +end + + +protocols["rdnt"]["fetchAllSearchResults"] = function() + Modem.open(publicDNSChannel) + Modem.open(publicResponseChannel) + Modem.transmit(publicDNSChannel, header.dnsPacket) + Modem.close(publicDNSChannel) + + rednet.broadcast(header.dnsPacket, header.rednetHeader .. publicDNSChannel) + + local uniqueServers = {} + local uniqueDomains = {} + + local timer = os.startTimer(searchResultTimeout) + + while true do + local event, id, channel, protocol, message, dist = os.pullEventRaw() + if event == "modem_message" then + if channel == publicResponseChannel and message:match(header.dnsHeaderMatch) then + if not uniqueServers[tostring(dist)] then + uniqueServers[tostring(dist)] = true + local domain = message:match(header.dnsHeaderMatch) + if not uniqueDomains[domain] then + if not(domain:find("/") or domain:find(":") or domain:find("%?")) and #domain > 4 then + timer = os.startTimer(searchResultTimeout) + uniqueDomains[message:match(header.dnsHeaderMatch)] = tostring(dist) + end + end + end + end + elseif event == "rednet_message" and allowUnencryptedConnections then + if protocol and tonumber(protocol:match(header.rednetMatch)) == publicResponseChannel and channel:match(header.dnsHeaderMatch) then + if not uniqueServers[tostring(id)] then + uniqueServers[tostring(id)] = true + local domain = channel:match(header.dnsHeaderMatch) + if not uniqueDomains[domain] then + if not(domain:find("/") or domain:find(":") or domain:find("%?")) and #domain > 4 then + timer = os.startTimer(searchResultTimeout) + uniqueDomains[domain] = tostring(id) + end + end + end + end + elseif event == "timer" and id == timer then + local results = {} + for k, _ in pairs(uniqueDomains) do + table.insert(results, k) + end + + return results + end + end +end + + +protocols["rdnt"]["fetchConnectionObject"] = function(url) + local serverChannel = Cryptography.channel(url) + local requestHeader = header.requestHeaderA .. url .. header.requestHeaderB + local responseMatch = header.responseMatchA .. Cryptography.sanatize(url) .. header.responseMatchB + + local serializedHandshake = textutils.serialize(Handshake.generateInitiatorData()) + + local rednetResults = {} + local directResults = {} + + local disconnectOthers = function(ignoreDirect) + for k,v in pairs(rednetResults) do + v.close() + end + for k,v in pairs(directResults) do + if k ~= ignoreDirect then + v.close() + end + end + end + + local timer = os.startTimer(initiationTimeout) + + Modem.open(serverChannel) + Modem.transmit(serverChannel, requestHeader .. serializedHandshake) + + rednet.broadcast(requestHeader .. serializedHandshake, header.rednetHeader .. serverChannel) + + -- Extendable to have server selection + + while true do + local event, id, channel, protocol, message, dist = os.pullEventRaw() + if event == "modem_message" then + local fullMatch = responseMatch .. tostring(dist) .. header.responseMatchC + if channel == serverChannel and message:match(fullMatch) and type(textutils.unserialize(message:match(fullMatch))) == "table" then + local key = Handshake.generateResponseData(textutils.unserialize(message:match(fullMatch))) + if key then + local connection = SecureConnection.new(key, url, url, dist) + table.insert(directResults, { + connection = connection, + fetchPage = function(page) + if not connection then + return nil + end + + local fetchTimer = os.startTimer(fetchTimeout) + + local pageRequest = header.pageRequestHeaderA .. url .. header.pageRequestHeaderB .. page + local pageResponseMatch = header.pageResponseMatchA .. Cryptography.sanatize(url) .. header.pageResponseMatchB + + connection:sendMessage(pageRequest, header.rednetHeader .. connection.channel) + + while true do + local event, id, channel, protocol, message, dist = os.pullEventRaw() + if event == "modem_message" and channel == connection.channel and connection:verifyHeader(message) then + local resp, data = connection:decryptMessage(message) + if not resp then + -- Decryption error + elseif data and data ~= page then + if data:match(pageResponseMatch) then + local head, body = data:match(pageResponseMatch) + return body, textutils.unserialize(head) + end + end + elseif event == "timer" and id == fetchTimer then + return nil + end + end + end, + close = function() + if connection ~= nil then + connection:sendMessage(header.closeHeaderA .. url .. header.closeHeaderB, header.rednetHeader..connection.channel) + Modem.close(connection.channel) + connection = nil + end + end + }) + + disconnectOthers(1) + return directResults[1] + end + end + elseif event == "rednet_message" then + local fullMatch = responseMatch .. os.getComputerID() .. header.responseMatchC + if protocol and tonumber(protocol:match(header.rednetMatch)) == serverChannel and channel:match(fullMatch) and type(textutils.unserialize(channel:match(fullMatch))) == "table" then + local key = Handshake.generateResponseData(textutils.unserialize(channel:match(fullMatch))) + if key then + local connection = SecureConnection.new(key, url, url, id, true) + table.insert(rednetResults, { + connection = connection, + fetchPage = function(page) + if not connection then + return nil + end + + local fetchTimer = os.startTimer(fetchTimeout) + + local pageRequest = header.pageRequestHeaderA .. url .. header.pageRequestHeaderB .. page + local pageResponseMatch = header.pageResponseMatchA .. Cryptography.sanatize(url) .. header.pageResponseMatchB + + connection:sendMessage(pageRequest, header.rednetHeader .. connection.channel) + + while true do + local event, id, channel, protocol, message, dist = os.pullEventRaw() + if event == "rednet_message" and protocol and tonumber(protocol:match(header.rednetMatch)) == connection.channel and connection:verifyHeader(channel) then + local resp, data = connection:decryptMessage(channel) + if not resp then + -- Decryption error + elseif data and data ~= page then + if data:match(pageResponseMatch) then + local head, body = data:match(pageResponseMatch) + return body, textutils.unserialize(head) + end + end + elseif event == "timer" and id == fetchTimer then + return nil + end + end + end, + close = function() + connection:sendMessage(header.closeHeaderA .. url .. header.closeHeaderB, header.rednetHeader..connection.channel) + Modem.close(connection.channel) + connection = nil + end + }) + + if #rednetResults == 1 then + timer = os.startTimer(0.2) + end + end + end + elseif event == "timer" and id == timer then + -- Return + if #directResults > 0 then + disconnectOthers(1) + return directResults[1] + elseif #rednetResults > 0 then + local lowestID = math.huge + local lowestResult = nil + for k,v in pairs(rednetResults) do + if v.connection.rednet_id < lowestID then + lowestID = v.connection.rednet_id + lowestResult = v + end + end + + for k,v in pairs(rednetResults) do + if v.connection.rednet_id ~= lowestID then + v.close() + end + end + + return lowestResult + else + return nil + end + end + end +end + + + +-- Fetching Raw Data + + +local fetchSearchResultsForQuery = function(query) + local all = protocols[currentProtocol]["fetchAllSearchResults"]() + local results = {} + if query and query:len() > 0 then + for _, v in pairs(all) do + if v:find(query:lower()) then + table.insert(results, v) + end + end + else + results = all + end + + table.sort(results) + return results +end + + +local getConnectionObjectFromURL = function(url) + local domain = url:match("^([^/]+)") + return protocols[currentProtocol]["fetchConnectionObject"](domain) +end + + +local determineLanguage = function(header) + if type(header) == "table" then + if header.language and header.language == "Firewolf Markup" then + return "fwml" + else + return "lua" + end + else + return "lua" + end +end + + + +-- History + + +local appendToHistory = function(url) + if history[1] ~= url then + table.insert(history, 1, url) + end +end + + + +-- Fetch Websites + + +local loadingAnimation = function() + local state = -2 + + term.setTextColor(theme.text) + term.setBackgroundColor(theme.accent) + + term.setCursorPos(w - 5, 1) + term.write("[= ]") + + local timer = os.startTimer(animationInterval) + + while true do + local event, timerID = os.pullEvent() + if event == "timer" and timerID == timer then + term.setTextColor(theme.text) + term.setBackgroundColor(theme.accent) + + state = state + 1 + + term.setCursorPos(w - 5, 1) + term.write("[ ]") + term.setCursorPos(w - 2 - math.abs(state), 1) + term.write("=") + + if state == 2 then + state = -2 + end + + timer = os.startTimer(animationInterval) + end + end +end + + +local normalizeURL = function(url) + url = url:lower():gsub(" ", "") + if url == "home" or url == "homepage" then + url = "firewolf" + end + + return url +end + + +local normalizePage = function(page) + if not page then page = "" end + page = page:lower() + if page == "" then + page = "/" + end + return page +end + + +local determineActionForURL = function(url) + if url:len() > 0 and url:gsub("/", ""):len() == 0 then + return "none" + end + + if url == "exit" then + return "exit" + elseif builtInSites["display"][url] then + return "internal website" + elseif url == "" then + local results = fetchSearchResultsForQuery() + if #results > 0 then + return "search", results + else + return "none" + end + else + local connection = getConnectionObjectFromURL(url) + if connection then + return "external website", connection + else + local results = fetchSearchResultsForQuery(url) + if #results > 0 then + return "search", results + else + return "none" + end + end + end +end + + +local fetchSearch = function(url, results) + return languages["lua"]["runWithoutAntivirus"](builtInSites["search"], results) +end + + +local fetchInternal = function(url) + return languages["lua"]["runWithoutAntivirus"](builtInSites["display"][url]) +end + + +local fetchError = function(err) + return languages["lua"]["runWithoutAntivirus"](builtInSites["error"], err) +end + + +local fetchExternal = function(url, connection) + if connection.multipleServers then + -- Please forgive me + -- GravityScore forced me to do it like this + -- I don't mean it, I really don't. + connection = connection.servers[1] + end + + local page = normalizePage(url:match("^[^/]+/(.+)")) + local contents, head = connection.fetchPage(page) + if contents then + if type(contents) ~= "string" then + return fetchNone() + else + local language = determineLanguage(head) + return languages[language]["run"](contents, page, connection) + end + else + if connection then + connection.close() + return "retry" + end + return fetchError("A connection error/timeout has occurred!") + end +end + + +local fetchNone = function() + return languages["lua"]["runWithoutAntivirus"](builtInSites["noresults"]) +end + + +local fetchURL = function(url, inheritConnection) + url = normalizeURL(url) + currentWebsiteURL = url + + if inheritConnection then + local resp = fetchExternal(url, inheritConnection) + if resp ~= "retry" then + return resp, false, inheritConnection + end + end + + local action, connection = determineActionForURL(url) + + if action == "search" then + return fetchSearch(url, connection), true + elseif action == "internal website" then + return fetchInternal(url), true + elseif action == "external website" then + local resp = fetchExternal(url, connection) + if resp == "retry" then + return fetchError("A connection error/timeout has occurred!"), false, connection + else + return resp, false, connection + end + elseif action == "none" then + return fetchNone(), true + elseif action == "exit" then + os.queueEvent("terminate") + end + + return nil +end + + + +-- Tabs + + +local switchTab = function(index, shouldntResume) + if not tabs[index] then + return + end + + if tabs[currentTab].win then + tabs[currentTab].win.setVisible(false) + end + + currentTab = index + isMenubarOpen = tabs[currentTab].isMenubarOpen + currentWebsiteURL = tabs[currentTab].url + + term.redirect(originalTerminal) + clear(theme.background, theme.text) + drawMenubar() + + term.redirect(tabs[currentTab].win) + term.setCursorPos(1, 1) + tabs[currentTab].win.setVisible(true) + tabs[currentTab].win.redraw() + + if not shouldntResume then + coroutine.resume(tabs[currentTab].thread) + end +end + + +local closeCurrentTab = function() + if #tabs <= 0 then + return + end + + table.remove(tabs, currentTab) + + currentTab = math.max(currentTab - 1, 1) + switchTab(currentTab, true) +end + + +local loadTab = function(index, url, givenFunc) + url = normalizeURL(url) + + local func = nil + local isOpen = true + local currentConnection = false + + isMenubarOpen = true + currentWebsiteURL = url + drawMenubar() + + if tabs[index] and tabs[index].connection and tabs[index].url then + if url:match("^([^/]+)") == tabs[index].url:match("^([^/]+)") then + currentConnection = tabs[index].connection + else + tabs[index].connection.close() + tabs[index].connection = nil + end + end + + if givenFunc then + func = givenFunc + else + parallel.waitForAny(function() + func, isOpen, connection = fetchURL(url, currentConnection) + end, function() + while true do + local event, key = os.pullEvent() + if event == "key" and (key == 29 or key == 157) then + break + end + end + end, loadingAnimation) + end + + if func then + appendToHistory(url) + + tabs[index] = {} + tabs[index].url = url + tabs[index].connection = connection + tabs[index].win = window.create(originalTerminal, 1, 1, w, h, false) + + tabs[index].thread = coroutine.create(func) + tabs[index].isMenubarOpen = isOpen + tabs[index].isMenubarPermanent = isOpen + + tabs[index].ox = 1 + tabs[index].oy = 1 + + term.redirect(tabs[index].win) + clear(theme.background, theme.text) + + switchTab(index) + end +end + + + +-- Website Environments + + +local getWhitelistedEnvironment = function() + local env = {} + + local function copy(source, destination, key) + destination[key] = {} + for k, v in pairs(source) do + destination[key][k] = v + end + end + + copy(bit, env, "bit") + copy(colors, env, "colors") + copy(colours, env, "colours") + copy(coroutine, env, "coroutine") + + copy(disk, env, "disk") + env["disk"]["setLabel"] = nil + env["disk"]["eject"] = nil + + copy(gps, env, "gps") + copy(help, env, "help") + copy(keys, env, "keys") + copy(math, env, "math") + + copy(os, env, "os") + env["os"]["run"] = nil + env["os"]["shutdown"] = nil + env["os"]["reboot"] = nil + env["os"]["setComputerLabel"] = nil + env["os"]["queueEvent"] = nil + env["os"]["pullEvent"] = function(filter) + while true do + local event = {os.pullEvent(filter)} + if not filter then + return unpack(event) + elseif filter and event[1] == filter then + return unpack(event) + end + end + end + env["os"]["pullEventRaw"] = env["os"]["pullEvent"] + + copy(paintutils, env, "paintutils") + copy(parallel, env, "parallel") + copy(peripheral, env, "peripheral") + copy(rednet, env, "rednet") + copy(redstone, env, "redstone") + copy(redstone, env, "rs") + + copy(shell, env, "shell") + env["shell"]["run"] = nil + env["shell"]["exit"] = nil + env["shell"]["setDir"] = nil + env["shell"]["setAlias"] = nil + env["shell"]["clearAlias"] = nil + env["shell"]["setPath"] = nil + + copy(string, env, "string") + copy(table, env, "table") + + copy(term, env, "term") + env["term"]["redirect"] = nil + env["term"]["restore"] = nil + + copy(textutils, env, "textutils") + copy(vector, env, "vector") + + if turtle then + copy(turtle, env, "turtle") + end + + if http then + copy(http, env, "http") + end + + env["assert"] = assert + env["printError"] = printError + env["tonumber"] = tonumber + env["tostring"] = tostring + env["type"] = type + env["next"] = next + env["unpack"] = unpack + env["pcall"] = pcall + env["xpcall"] = xpcall + env["sleep"] = sleep + env["pairs"] = pairs + env["ipairs"] = ipairs + env["read"] = read + env["write"] = write + env["select"] = select + env["print"] = print + env["setmetatable"] = setmetatable + env["getmetatable"] = getmetatable + + env["_G"] = env + + return env +end + + +local overrideEnvironment = function(env) + local localTerm = {} + for k, v in pairs(term) do + localTerm[k] = v + end + + env["term"]["clear"] = function() + localTerm.clear() + drawMenubar() + end + + env["term"]["scroll"] = function(n) + localTerm.scroll(n) + drawMenubar() + end + + env["shell"]["getRunningProgram"] = function() + return currentWebsiteURL + end +end + +local urlEncode = function(url) + local result = url + + result = result:gsub("%%", "%%a") + result = result:gsub(":", "%%c") + result = result:gsub("/", "%%s") + result = result:gsub("\n", "%%n") + result = result:gsub(" ", "%%w") + result = result:gsub("&", "%%m") + result = result:gsub("%?", "%%q") + result = result:gsub("=", "%%e") + result = result:gsub("%.", "%%d") + + return result +end + +local urlDecode = function(url) + local result = url + + result = result:gsub("%%c", ":") + result = result:gsub("%%s", "/") + result = result:gsub("%%n", "\n") + result = result:gsub("%%w", " ") + result = result:gsub("%%&", "&") + result = result:gsub("%%q", "%?") + result = result:gsub("%%e", "=") + result = result:gsub("%%d", "%.") + result = result:gsub("%%m", "%%") + + return result +end + +local applyAPIFunctions = function(env, connection) + env["firewolf"] = {} + env["firewolf"]["version"] = version + env["firewolf"]["domain"] = currentWebsiteURL:match("^[^/]+") + + env["firewolf"]["redirect"] = function(url) + if type(url) ~= "string" then + return error("string (url) expected, got " .. type(url)) + end + + os.queueEvent(redirectEvent, url) + coroutine.yield() + end + + env["firewolf"]["download"] = function(page) + if type(page) ~= "string" then + return error("string (page) expected") + end + local bannedNames = {"ls", "dir", "delete", "copy", "move", "list", "rm", "cp", "mv", "clear", "cd", "lua"} + + local startSearch, endSearch = page:find(currentWebsiteURL:match("^[^/]+")) + if startSearch == 1 then + if page:sub(endSearch + 1, endSearch + 1) == "/" then + page = page:sub(endSearch + 2, -1) + else + page = page:sub(endSearch + 1, -1) + end + end + + local filename = page:match("([^/]+)$") + if not filename then + return false, "Cannot download index" + end + + for k, v in pairs(bannedNames) do + if filename == v then + return false, "Filename prohibited!" + end + end + + if not fs.exists(downloadsLocation) then + fs.makeDir(downloadsLocation) + elseif not fs.isDir(downloadsLocation) then + return false, "Downloads disabled!" + end + + contents = connection.fetchPage(normalizePage(page)) + if type(contents) ~= "string" then + return false, "Download error!" + else + local f = io.open(downloadsLocation .. "/" .. filename, "w") + f:write(contents) + f:close() + return true, downloadsLocation .. "/" .. filename + end + end + + env["firewolf"]["encode"] = function(vars) + if type(vars) ~= "table" then + return error("table (vars) expected, got " .. type(vars)) + end + + local startSearch, endSearch = page:find(currentWebsiteURL:match("^[^/]+")) + if startSearch == 1 then + if page:sub(endSearch + 1, endSearch + 1) == "/" then + page = page:sub(endSearch + 2, -1) + else + page = page:sub(endSearch + 1, -1) + end + end + + local construct = "?" + for k,v in pairs(vars) do + construct = construct .. urlEncode(tostring(k)) .. "=" .. urlEncode(tostring(v)) .. "&" + end + -- Get rid of that last ampersand + construct = construct:sub(1, -2) + + return construct + end + + env["firewolf"]["query"] = function(page, vars) + if type(page) ~= "string" then + return error("string (page) expected, got " .. type(page)) + end + if vars and type(vars) ~= "table" then + return error("table (vars) expected, got " .. type(vars)) + end + + local startSearch, endSearch = page:find(currentWebsiteURL:match("^[^/]+")) + if startSearch == 1 then + if page:sub(endSearch + 1, endSearch + 1) == "/" then + page = page:sub(endSearch + 2, -1) + else + page = page:sub(endSearch + 1, -1) + end + end + + local construct = page .. "?" + if vars then + for k,v in pairs(vars) do + construct = construct .. urlEncode(tostring(k)) .. "=" .. urlEncode(tostring(v)) .. "&" + end + end + -- Get rid of that last ampersand + construct = construct:sub(1, -2) + + contents = connection.fetchPage(normalizePage(construct)) + if type(contents) == "string" then + return contents + else + return false + end + end + + env["firewolf"]["loadImage"] = function(page) + if type(page) ~= "string" then + return error("string (page) expected, got " .. type(page)) + end + + local startSearch, endSearch = page:find(currentWebsiteURL:match("^[^/]+")) + if startSearch == 1 then + if page:sub(endSearch + 1, endSearch + 1) == "/" then + page = page:sub(endSearch + 2, -1) + else + page = page:sub(endSearch + 1, -1) + end + end + + local filename = page:match("([^/]+)$") + if not filename then + return false, "Cannot load index as an image!" + end + + contents = connection.fetchPage(normalizePage(page)) + if type(contents) ~= "string" then + return false, "Download error!" + else + local colorLookup = {} + for n = 1, 16 do + colorLookup[string.byte("0123456789abcdef", n, n)] = 2 ^ (n - 1) + end + + local image = {} + for line in contents:gmatch("[^\n]+") do + local lines = {} + for x = 1, line:len() do + lines[x] = colorLookup[string.byte(line, x, x)] or 0 + end + table.insert(image, lines) + end + + return image + end + end + + env["center"] = center + env["fill"] = fill +end + + +local getWebsiteEnvironment = function(antivirus, connection) + local env = {} + + if antivirus then + env = getWhitelistedEnvironment() + overrideEnvironment(env) + else + setmetatable(env, {__index = _G}) + end + + applyAPIFunctions(env, connection) + + return env +end + + + +-- FWML Execution + + +local render = {} + +render["functions"] = {} +render["functions"]["public"] = {} +render["alignations"] = {} + +render["variables"] = { + scroll, + maxScroll, + align, + linkData = {}, + blockLength, + link, + linkStart, + markers, + currentOffset, +} + + +local function getLine(loc, data) + local _, changes = data:sub(1, loc):gsub("\n", "") + if not changes then + return 1 + else + return changes + 1 + end +end + + +local function parseData(data) + local commands = {} + local searchPos = 1 + + while #data > 0 do + local sCmd, eCmd = data:find("%[[^%]]+%]", searchPos) + if sCmd then + sCmd = sCmd + 1 + eCmd = eCmd - 1 + + if (sCmd > 2) then + if data:sub(sCmd - 2, sCmd - 2) == "\\" then + local t = data:sub(searchPos, sCmd - 1):gsub("\n", ""):gsub("\\%[", "%["):gsub("\\%]", "%]") + if #t > 0 then + if #commands > 0 and type(commands[#commands][1]) == "string" then + commands[#commands][1] = commands[#commands][1] .. t + else + table.insert(commands, {t}) + end + end + searchPos = sCmd + else + local t = data:sub(searchPos, sCmd - 2):gsub("\n", ""):gsub("\\%[", "%["):gsub("\\%]", "%]") + if #t > 0 then + if #commands > 0 and type(commands[#commands][1]) == "string" then + commands[#commands][1] = commands[#commands][1] .. t + else + table.insert(commands, {t}) + end + end + + t = data:sub(sCmd, eCmd):gsub("\n", "") + table.insert(commands, {getLine(sCmd, data), t}) + searchPos = eCmd + 2 + end + else + local t = data:sub(sCmd, eCmd):gsub("\n", "") + table.insert(commands, {getLine(sCmd, data), t}) + searchPos = eCmd + 2 + end + else + local t = data:sub(searchPos, -1):gsub("\n", ""):gsub("\\%[", "%["):gsub("\\%]", "%]") + if #t > 0 then + if #commands > 0 and type(commands[#commands][1]) == "string" then + commands[#commands][1] = commands[#commands][1] .. t + else + table.insert(commands, {t}) + end + end + + break + end + end + + return commands +end + + +local function proccessData(commands) + searchIndex = 0 + + while searchIndex < #commands do + searchIndex = searchIndex + 1 + + local length = 0 + local origin = searchIndex + + if type(commands[searchIndex][1]) == "string" then + length = length + #commands[searchIndex][1] + local endIndex = origin + for i = origin + 1, #commands do + if commands[i][2] then + local command = commands[i][2]:match("^(%w+)%s-") + if not (command == "c" or command == "color" or command == "bg" + or command == "background" or command == "newlink" or command == "endlink") then + endIndex = i + break + end + elseif commands[i][2] then + + else + length = length + #commands[i][1] + end + if i == #commands then + endIndex = i + end + end + + commands[origin][2] = length + searchIndex = endIndex + length = 0 + end + end + + return commands +end + + +local function parse(original) + return proccessData(parseData(original)) +end + + +render["functions"]["display"] = function(text, length, offset, center) + if not offset then + offset = 0 + end + + return render.variables.align(text, length, w, offset, center); +end + + +render["functions"]["displayText"] = function(source) + if source[2] then + render.variables.blockLength = source[2] + if render.variables.link and not render.variables.linkStart then + render.variables.linkStart = render.functions.display( + source[1], render.variables.blockLength, render.variables.currentOffset, w / 2) + else + render.functions.display(source[1], render.variables.blockLength, render.variables.currentOffset, w / 2) + end + else + if render.variables.link and not render.variables.linkStart then + render.variables.linkStart = render.functions.display(source[1], nil, render.variables.currentOffset, w / 2) + else + render.functions.display(source[1], nil, render.variables.currentOffset, w / 2) + end + end +end + + +render["functions"]["public"]["br"] = function(source) + if render.variables.link then + return "Cannot insert new line within a link on line " .. source[1] + end + + render.variables.scroll = render.variables.scroll + 1 + render.variables.maxScroll = math.max(render.variables.scroll, render.variables.maxScroll) +end + + +render["functions"]["public"]["c "] = function(source) + local sColor = source[2]:match("^%w+%s+(.+)$") or "" + if colors[sColor] then + term.setTextColor(colors[sColor]) + else + return "Invalid color: \"" .. sColor .. "\" on line " .. source[1] + end +end + + +render["functions"]["public"]["color "] = render["functions"]["public"]["c "] + + +render["functions"]["public"]["bg "] = function(source) + local sColor = source[2]:match("^%w+%s+(.+)$") or "" + if colors[sColor] then + term.setBackgroundColor(colors[sColor]) + else + return "Invalid color: \"" .. sColor .. "\" on line " .. source[1] + end +end + + +render["functions"]["public"]["background "] = render["functions"]["public"]["bg "] + + +render["functions"]["public"]["newlink "] = function(source) + if render.variables.link then + return "Cannot nest links on line " .. source[1] + end + + render.variables.link = source[2]:match("^%w+%s+(.+)$") or "" + render.variables.linkStart = false +end + + +render["functions"]["public"]["endlink"] = function(source) + if not render.variables.link then + return "Cannot end a link without a link on line " .. source[1] + end + + local linkEnd = term.getCursorPos()-1 + table.insert(render.variables.linkData, {render.variables.linkStart, + linkEnd, render.variables.scroll, render.variables.link}) + render.variables.link = false + render.variables.linkStart = false +end + + +render["functions"]["public"]["offset "] = function(source) + local offset = tonumber((source[2]:match("^%w+%s+(.+)$") or "")) + if offset then + render.variables.currentOffset = offset + else + return "Invalid offset value: \"" .. (source[2]:match("^%w+%s+(.+)$") or "") .. "\" on line " .. source[1] + end +end + + +render["functions"]["public"]["marker "] = function(source) + render.variables.markers[(source[2]:match("^%w+%s+(.+)$") or "")] = render.variables.scroll +end + + +render["functions"]["public"]["goto "] = function(source) + local location = source[2]:match("%w+%s+(.+)$") + if render.variables.markers[location] then + render.variables.scroll = render.variables.markers[location] + else + return "No such location: \"" .. (source[2]:match("%w+%s+(.+)$") or "") .. "\" on line " .. source[1] + end +end + + +render["functions"]["public"]["box "] = function(source) + local sColor, align, height, width, offset, url = source[2]:match("^box (%a+) (%a+) (%-?%d+) (%-?%d+) (%-?%d+) ?([^ ]*)") + if not sColor then + return "Invalid box syntax on line " .. source[1] + end + + local x, y = term.getCursorPos() + local startX + + if align == "center" or align == "centre" then + startX = math.ceil((w / 2) - width / 2) + offset + elseif align == "left" then + startX = 1 + offset + elseif align == "right" then + startX = (w - width + 1) + offset + else + return "Invalid align option for box on line " .. source[1] + end + + if not colors[sColor] then + return "Invalid color: \"" .. sColor .. "\" for box on line " .. source[1] + end + + term.setBackgroundColor(colors[sColor]) + for i = 0, height - 1 do + term.setCursorPos(startX, render.variables.scroll + i) + term.write(string.rep(" ", width)) + if url:len() > 3 then + table.insert(render.variables.linkData, {startX, startX + width - 1, render.variables.scroll + i, url}) + end + end + + render.variables.maxScroll = math.max(render.variables.scroll + height - 1, render.variables.maxScroll) + term.setCursorPos(x, y) +end + + +render["alignations"]["left"] = function(text, length, _, offset) + local x, y = term.getCursorPos() + if length then + term.setCursorPos(1 + offset, render.variables.scroll) + term.write(text) + return 1 + offset + else + term.setCursorPos(x, render.variables.scroll) + term.write(text) + return x + end +end + + +render["alignations"]["right"] = function(text, length, width, offset) + local x, y = term.getCursorPos() + if length then + term.setCursorPos((width - length + 1) + offset, render.variables.scroll) + term.write(text) + return (width - length + 1) + offset + else + term.setCursorPos(x, render.variables.scroll) + term.write(text) + return x + end +end + + +render["alignations"]["center"] = function(text, length, _, offset, center) + local x, y = term.getCursorPos() + if length then + term.setCursorPos(math.ceil(center - length / 2) + offset, render.variables.scroll) + term.write(text) + return math.ceil(center - length / 2) + offset + else + term.setCursorPos(x, render.variables.scroll) + term.write(text) + return x + end +end + + +render["render"] = function(data, startScroll) + if startScroll == nil then + render.variables.startScroll = 0 + else + render.variables.startScroll = startScroll + end + + render.variables.scroll = startScroll + 1 + render.variables.maxScroll = render.variables.scroll + + render.variables.linkData = {} + + render.variables.align = render.alignations.left + + render.variables.blockLength = 0 + render.variables.link = false + render.variables.linkStart = false + render.variables.markers = {} + render.variables.currentOffset = 0 + + for k, v in pairs(data) do + if type(v[2]) ~= "string" then + render.functions.displayText(v) + elseif v[2] == "<" or v[2] == "left" then + render.variables.align = render.alignations.left + elseif v[2] == ">" or v[2] == "right" then + render.variables.align = render.alignations.right + elseif v[2] == "=" or v[2] == "center" then + render.variables.align = render.alignations.center + else + local existentFunction = false + + for name, func in pairs(render.functions.public) do + if v[2]:find(name) == 1 then + existentFunction = true + local ret = func(v) + if ret then + return ret + end + end + end + + if not existentFunction then + return "Non-existent tag: \"" .. v[2] .. "\" on line " .. v[1] + end + end + end + + return render.variables.linkData, render.variables.maxScroll - render.variables.startScroll +end + + + +-- Lua Execution + + +languages["lua"] = {} +languages["fwml"] = {} + + +languages["lua"]["runWithErrorCatching"] = function(func, ...) + local _, err = pcall(func, ...) + if err then + os.queueEvent(websiteErrorEvent, err) + end +end + + +languages["lua"]["runWithoutAntivirus"] = function(func, ...) + local args = {...} + local env = getWebsiteEnvironment(false) + setfenv(func, env) + return function() + languages["lua"]["runWithErrorCatching"](func, unpack(args)) + end +end + + +languages["lua"]["run"] = function(contents, page, connection, ...) + local func, err = loadstring("sleep(0) " .. contents, page) + if err then + return languages["lua"]["runWithoutAntivirus"](builtInSites["crash"], err) + else + local args = {...} + local env = getWebsiteEnvironment(true, connection) + setfenv(func, env) + return function() + languages["lua"]["runWithErrorCatching"](func, unpack(args)) + end + end +end + + +languages["fwml"]["run"] = function(contents, page, connection, ...) + local err, data = pcall(parse, contents) + if not err then + return languages["lua"]["runWithoutAntivirus"](builtInSites["crash"], data) + end + + return function() + local currentScroll = 0 + local err, links, pageHeight = pcall(render.render, data, currentScroll) + if type(links) == "string" or not err then + term.clear() + os.queueEvent(websiteErrorEvent, links) + else + while true do + local e, scroll, x, y = os.pullEvent() + if e == "mouse_click" then + for k, v in pairs(links) do + if x >= math.min(v[1], v[2]) and x <= math.max(v[1], v[2]) and y == v[3] then + os.queueEvent(redirectEvent, v[4]) + coroutine.yield() + end + end + elseif e == "mouse_scroll" then + if currentScroll - scroll - h >= -pageHeight and currentScroll - scroll <= 0 then + currentScroll = currentScroll - scroll + clear(theme.background, theme.text) + links = render.render(data, currentScroll) + end + elseif e == "key" and scroll == keys.up or scroll == keys.down then + local scrollAmount + + if scroll == keys.up then + scrollAmount = 1 + elseif scroll == keys.down then + scrollAmount = -1 + end + + local scrollLessHeight = currentScroll + scrollAmount - h >= -pageHeight + local scrollZero = currentScroll + scrollAmount <= 0 + if scrollLessHeight and scrollZero then + currentScroll = currentScroll + scrollAmount + clear(theme.background, theme.text) + links = render.render(data, currentScroll) + end + end + end + end + end +end + + + +-- Query Bar + + +local readNewWebsiteURL = function() + local onEvent = function(text, event, key, x, y) + if event == "mouse_click" then + if y == 2 then + local index = determineClickedTab(x, y) + if index == "new" and #tabs < maxTabs then + loadTab(#tabs + 1, "firewolf") + elseif index == "close" then + closeCurrentTab() + elseif index then + switchTab(index) + end + + return {["nullifyText"] = true, ["exit"] = true} + elseif y > 2 then + return {["nullifyText"] = true, ["exit"] = true} + end + elseif event == "key" then + if key == 29 or key == 157 then + return {["nullifyText"] = true, ["exit"] = true} + end + end + end + + isMenubarOpen = true + drawMenubar() + term.setCursorPos(2, 1) + term.setTextColor(theme.text) + term.setBackgroundColor(theme.accent) + term.clearLine() + term.write(currentProtocol .. "://") + + local website = modifiedRead({ + ["onEvent"] = onEvent, + ["displayLength"] = w - 9, + ["history"] = history, + }) + + if not website then + if not tabs[currentTab].isMenubarPermanent then + isMenubarOpen = false + menubarWindow.setVisible(false) + else + isMenubarOpen = true + menubarWindow.setVisible(true) + end + + term.redirect(tabs[currentTab].win) + tabs[currentTab].win.setVisible(true) + tabs[currentTab].win.redraw() + + return + elseif website == "exit" then + error() + end + + loadTab(currentTab, website) +end + + + +-- Event Management + + +local handleKeyDown = function(event) + if event[2] == 29 or event[2] == 157 then + readNewWebsiteURL() + return true + end + + return false +end + + +local handleMouseDown = function(event) + if isMenubarOpen then + if event[4] == 1 then + readNewWebsiteURL() + return true + elseif event[4] == 2 then + local index = determineClickedTab(event[3], event[4]) + if index == "new" and #tabs < maxTabs then + loadTab(#tabs + 1, "firewolf") + elseif index == "close" then + closeCurrentTab() + elseif index then + switchTab(index) + end + + return true + end + end + + return false +end + + +local handleEvents = function() + loadTab(1, "firewolf") + currentTab = 1 + + while true do + drawMenubar() + local event = {os.pullEventRaw()} + drawMenubar() + + local cancelEvent = false + if event[1] == "terminate" then + break + elseif event[1] == "key" then + cancelEvent = handleKeyDown(event) + elseif event[1] == "mouse_click" then + cancelEvent = handleMouseDown(event) + elseif event[1] == websiteErrorEvent then + cancelEvent = true + + loadTab(currentTab, tabs[currentTab].url, function() + builtInSites["crash"](event[2]) + end) + elseif event[1] == redirectEvent then + cancelEvent = true + + if (event[2]:match("^rdnt://(.+)$")) then + event[2] = event[2]:match("^rdnt://(.+)$") + end + + loadTab(currentTab, event[2]) + end + + if not cancelEvent then + term.redirect(tabs[currentTab].win) + term.setCursorPos(tabs[currentTab].ox, tabs[currentTab].oy) + + coroutine.resume(tabs[currentTab].thread, unpack(event)) + + local ox, oy = term.getCursorPos() + tabs[currentTab].ox = ox + tabs[currentTab].oy = oy + end + end +end + + + +-- Main + + +local main = function() + currentProtocol = "rdnt" + currentTab = 1 + + if term.isColor() then + theme = colorTheme + enableTabBar = true + else + theme = grayscaleTheme + enableTabBar = false + end + + setupMenubar() + protocols[currentProtocol]["setup"]() + + clear(theme.background, theme.text) + handleEvents() +end + + +local handleError = function(err) + clear(theme.background, theme.text) + + fill(1, 3, w, 3, theme.subtle) + term.setCursorPos(1, 4) + center("Firewolf has crashed!") + + term.setBackgroundColor(theme.background) + term.setCursorPos(1, 8) + centerSplit(err, w - 4) + print("\n") + center("Please report this error to") + center("GravityScore or 1lann.") + print("") + center("Press any key to exit.") + + os.pullEvent("key") + os.queueEvent("") + os.pullEvent() +end + +local _, err = pcall(main) +term.redirect(originalTerminal) + +Modem.closeAll() + +if err and not err:lower():find("terminate") then + handleError(err) +end + + +clear(colors.black, colors.white) +center("Thanks for using Firewolf " .. version) +center("Made by GravityScore and 1lann") +print("")
\ No newline at end of file diff --git a/Programs/Ink.bup/Contents/Info.meta b/Programs/Ink.bup/Contents/Info.meta new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/Programs/Ink.bup/Contents/Info.meta @@ -0,0 +1 @@ + diff --git a/Programs/Ink.bup/Contents/Resources/resources_here.txt b/Programs/Ink.bup/Contents/Resources/resources_here.txt new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/Programs/Ink.bup/Contents/Resources/resources_here.txt @@ -0,0 +1 @@ + diff --git a/Programs/Ink.bup/Contents/bits-UI/ink.lua b/Programs/Ink.bup/Contents/bits-UI/ink.lua new file mode 100644 index 0000000..49f6119 --- /dev/null +++ b/Programs/Ink.bup/Contents/bits-UI/ink.lua @@ -0,0 +1,3153 @@ +tArgs = {...} + +if OneOS then + --running under OneOS + OneOS.ToolBarColour = colours.grey + OneOS.ToolBarTextColour = colours.white +end + +local _w, _h = term.getSize() + +local round = function(num, idp) + local mult = 10^(idp or 0) + return math.floor(num * mult + 0.5) / mult +end + +UIColours = { + Toolbar = colours.grey, + ToolbarText = colours.lightGrey, + ToolbarSelected = colours.lightBlue, + ControlText = colours.white, + ToolbarItemTitle = colours.black, + Background = colours.lightGrey, + MenuBackground = colours.white, + MenuText = colours.black, + MenuSeparatorText = colours.grey, + MenuDisabledText = colours.lightGrey, + Shadow = colours.grey, + TransparentBackgroundOne = colours.white, + TransparentBackgroundTwo = colours.lightGrey, + MenuBarActive = colours.white +} + +local getNames = peripheral.getNames or function() + local tResults = {} + for n,sSide in ipairs( rs.getSides() ) do + if peripheral.isPresent( sSide ) then + table.insert( tResults, sSide ) + local isWireless = false + if not pcall(function()isWireless = peripheral.call(sSide, 'isWireless') end) then + isWireless = true + end + if peripheral.getType( sSide ) == "modem" and not isWireless then + local tRemote = peripheral.call( sSide, "getNamesRemote" ) + for n,sName in ipairs( tRemote ) do + table.insert( tResults, sName ) + end + end + end + end + return tResults +end + +Peripheral = { + GetPeripheral = function(_type) + for i, p in ipairs(Peripheral.GetPeripherals()) do + if p.Type == _type then + return p + end + end + end, + + Call = function(type, ...) + local tArgs = {...} + local p = Peripheral.GetPeripheral(type) + peripheral.call(p.Side, unpack(tArgs)) + end, + + GetPeripherals = function(filterType) + local peripherals = {} + for i, side in ipairs(getNames()) do + local name = peripheral.getType(side):gsub("^%l", string.upper) + local code = string.upper(side:sub(1,1)) + if side:find('_') then + code = side:sub(side:find('_')+1) + end + + local dupe = false + for i, v in ipairs(peripherals) do + if v[1] == name .. ' ' .. code then + dupe = true + end + end + + if not dupe then + local _type = peripheral.getType(side) + local isWireless = false + if _type == 'modem' then + if not pcall(function()isWireless = peripheral.call(sSide, 'isWireless') end) then + isWireless = true + end + if isWireless then + _type = 'wireless_modem' + name = 'W '..name + end + end + if not filterType or _type == filterType then + table.insert(peripherals, {Name = name:sub(1,8) .. ' '..code, Fullname = name .. ' ('..side:sub(1, 1):upper() .. side:sub(2, -1)..')', Side = side, Type = _type, Wireless = isWireless}) + end + end + end + return peripherals + end, + + PresentNamed = function(name) + return peripheral.isPresent(name) + end, + + CallType = function(type, ...) + local tArgs = {...} + local p = GetPeripheral(type) + return peripheral.call(p.Side, unpack(tArgs)) + end, + + CallNamed = function(name, ...) + local tArgs = {...} + return peripheral.call(name, unpack(tArgs)) + end +} + +TextLine = { + Text = "", + Alignment = AlignmentLeft, + + Initialise = function(self, text, alignment) + local new = {} -- the new instance + setmetatable( new, {__index = self} ) + new.Text = text + new.Alignment = alignment or AlignmentLeft + return new + end +} + +local StripColours = function(str) + return str:gsub('['..string.char(14)..'-'..string.char(29)..']','') +end + +Printer = { + Name = nil, + PeripheralType = 'printer', + + paperLevel = function(self) + return Peripheral.CallNamed(self.Name, 'getPaperLevel') + end, + + newPage = function(self) + return Peripheral.CallNamed(self.Name, 'newPage') + end, + + endPage = function(self) + return Peripheral.CallNamed(self.Name, 'endPage') + end, + + pageWrite = function(self, text) + return Peripheral.CallNamed(self.Name, 'write', text) + end, + + setPageTitle = function(self, title) + return Peripheral.CallNamed(self.Name, 'setPageTitle', title) + end, + + inkLevel = function(self) + return Peripheral.CallNamed(self.Name, 'getInkLevel') + end, + + getCursorPos = function(self) + return Peripheral.CallNamed(self.Name, 'getCursorPos') + end, + + setCursorPos = function(self, x, y) + return Peripheral.CallNamed(self.Name, 'setCursorPos', x, y) + end, + + pageSize = function(self) + return Peripheral.CallNamed(self.Name, 'getPageSize') + end, + + Present = function() + if Peripheral.GetPeripheral(Printer.PeripheralType) == nil then + return false + else + return true + end + end, + + PrintLines = function(self, lines, title, copies) + local pages = {} + local pageLines = {} + for i, line in ipairs(lines) do + table.insert(pageLines, TextLine:Initialise(StripColours(line))) + if i % 25 == 0 then + table.insert(pages, pageLines) + pageLines = {} + end + end + if #pageLines ~= 0 then + table.insert(pages, pageLines) + end + return self:PrintPages(pages, title, copies) + end, + + PrintPages = function(self, pages, title, copies) + copies = copies or 1 + for c = 1, copies do + for p, page in ipairs(pages) do + if self:paperLevel() < #pages * copies then + return 'Add more paper to the printer' + end + if self:inkLevel() < #pages * copies then + return 'Add more ink to the printer' + end + self:newPage() + for i, line in ipairs(page) do + self:setCursorPos(1, i) + self:pageWrite(StripColours(line.Text)) + end + if title then + self:setPageTitle(title) + end + self:endPage() + end + end + end, + + Initialise = function(self, name) + if Printer.Present() then --fix + local new = {} -- the new instance + setmetatable( new, {__index = self} ) + if name and Peripheral.PresentNamed(name) then + new.Name = name + else + new.Name = Peripheral.GetPeripheral(Printer.PeripheralType).Side + end + return new + end + end +} + +Clipboard = { + Content = nil, + Type = nil, + IsCut = false, + + Empty = function() + Clipboard.Content = nil + Clipboard.Type = nil + Clipboard.IsCut = false + end, + + isEmpty = function() + return Clipboard.Content == nil + end, + + Copy = function(content, _type) + Clipboard.Content = content + Clipboard.Type = _type or 'generic' + Clipboard.IsCut = false + end, + + Cut = function(content, _type) + Clipboard.Content = content + Clipboard.Type = _type or 'generic' + Clipboard.IsCut = true + end, + + Paste = function() + local c, t = Clipboard.Content, Clipboard.Type + if Clipboard.IsCut then + Clipboard.Empty() + end + return c, t + end +} + +if OneOS and OneOS.Clipboard then + Clipboard = OneOS.Clipboard +end + +Drawing = { + + Screen = { + Width = _w, + Height = _h + }, + + DrawCharacters = function (x, y, characters, textColour,bgColour) + Drawing.WriteStringToBuffer(x, y, characters, textColour, bgColour) + end, + + DrawBlankArea = function (x, y, w, h, colour) + Drawing.DrawArea (x, y, w, h, " ", 1, colour) + end, + + DrawArea = function (x, y, w, h, character, textColour, bgColour) + --width must be greater than 1, other wise we get a stack overflow + if w < 0 then + w = w * -1 + elseif w == 0 then + w = 1 + end + + for ix = 1, w do + local currX = x + ix - 1 + for iy = 1, h do + local currY = y + iy - 1 + Drawing.WriteToBuffer(currX, currY, character, textColour, bgColour) + end + end + end, + + DrawImage = function(_x,_y,tImage, w, h) + if tImage then + for y = 1, h do + if not tImage[y] then + break + end + for x = 1, w do + if not tImage[y][x] then + break + end + local bgColour = tImage[y][x] + local textColour = tImage.textcol[y][x] or colours.white + local char = tImage.text[y][x] + Drawing.WriteToBuffer(x+_x-1, y+_y-1, char, textColour, bgColour) + end + end + elseif w and h then + Drawing.DrawBlankArea(x, y, w, h, colours.green) + end + end, + --using .nft + LoadImage = function(path) + local image = { + text = {}, + textcol = {} + } + local fs = fs + if OneOS then + fs = OneOS.FS + end + if fs.exists(path) then + local _open = io.open + if OneOS then + _open = OneOS.IO.open + end + local file = _open(path, "r") + local sLine = file:read() + local num = 1 + while sLine do + table.insert(image, num, {}) + table.insert(image.text, num, {}) + table.insert(image.textcol, num, {}) + + --As we're no longer 1-1, we keep track of what index to write to + local writeIndex = 1 + --Tells us if we've hit a 30 or 31 (BG and FG respectively)- next char specifies the curr colour + local bgNext, fgNext = false, false + --The current background and foreground colours + local currBG, currFG = nil,nil + for i=1,#sLine do + local nextChar = string.sub(sLine, i, i) + if nextChar:byte() == 30 then + bgNext = true + elseif nextChar:byte() == 31 then + fgNext = true + elseif bgNext then + currBG = Drawing.GetColour(nextChar) + bgNext = false + elseif fgNext then + currFG = Drawing.GetColour(nextChar) + fgNext = false + else + if nextChar ~= " " and currFG == nil then + currFG = colours.white + end + image[num][writeIndex] = currBG + image.textcol[num][writeIndex] = currFG + image.text[num][writeIndex] = nextChar + writeIndex = writeIndex + 1 + end + end + num = num+1 + sLine = file:read() + end + file:close() + end + return image + end, + + DrawCharactersCenter = function(x, y, w, h, characters, textColour,bgColour) + w = w or Drawing.Screen.Width + h = h or Drawing.Screen.Height + x = x or 0 + y = y or 0 + x = math.ceil((w - #characters) / 2) + x + y = math.floor(h / 2) + y + + Drawing.DrawCharacters(x, y, characters, textColour, bgColour) + end, + + GetColour = function(hex) + if hex == ' ' then + return colours.transparent + end + local value = tonumber(hex, 16) + if not value then return nil end + value = math.pow(2,value) + return value + end, + + Clear = function (_colour) + _colour = _colour or colours.black + Drawing.ClearBuffer() + Drawing.DrawBlankArea(1, 1, Drawing.Screen.Width, Drawing.Screen.Height, _colour) + end, + + Buffer = {}, + BackBuffer = {}, + + DrawBuffer = function() + for y,row in pairs(Drawing.Buffer) do + for x,pixel in pairs(row) do + local shouldDraw = true + local hasBackBuffer = true + if Drawing.BackBuffer[y] == nil or Drawing.BackBuffer[y][x] == nil or #Drawing.BackBuffer[y][x] ~= 3 then + hasBackBuffer = false + end + if hasBackBuffer and Drawing.BackBuffer[y][x][1] == Drawing.Buffer[y][x][1] and Drawing.BackBuffer[y][x][2] == Drawing.Buffer[y][x][2] and Drawing.BackBuffer[y][x][3] == Drawing.Buffer[y][x][3] then + shouldDraw = false + end + if shouldDraw then + term.setBackgroundColour(pixel[3]) + term.setTextColour(pixel[2]) + term.setCursorPos(x, y) + term.write(pixel[1]) + end + end + end + Drawing.BackBuffer = Drawing.Buffer + Drawing.Buffer = {} + term.setCursorPos(1,1) + end, + + ClearBuffer = function() + Drawing.Buffer = {} + end, + + WriteStringToBuffer = function (x, y, characters, textColour,bgColour) + for i = 1, #characters do + local character = characters:sub(i,i) + Drawing.WriteToBuffer(x + i - 1, y, character, textColour, bgColour) + end + end, + + WriteToBuffer = function(x, y, character, textColour,bgColour) + x = round(x) + y = round(y) + if bgColour == colours.transparent then + Drawing.Buffer[y] = Drawing.Buffer[y] or {} + Drawing.Buffer[y][x] = Drawing.Buffer[y][x] or {"", colours.white, colours.black} + Drawing.Buffer[y][x][1] = character + Drawing.Buffer[y][x][2] = textColour + else + Drawing.Buffer[y] = Drawing.Buffer[y] or {} + Drawing.Buffer[y][x] = {character, textColour, bgColour} + end + end, +} + +Current = { + Document = nil, + TextInput = nil, + CursorPos = {1,1}, + CursorColour = colours.black, + Selection = {8, 36}, + Window = nil, + Modified = false, +} + +local isQuitting = false + +function OrderSelection() + if Current.Selection then + if Current.Selection[1] <= Current.Selection[2] then + return Current.Selection + else + return {Current.Selection[2], Current.Selection[1]} + end + end +end + +function StripColours(str) + return str:gsub('['..string.char(14)..'-'..string.char(29)..']','') +end + +function FindColours(str) + local _, count = str:gsub('['..string.char(14)..'-'..string.char(29)..']','') + return count +end + +ColourFromCharacter = function(character) + local n = character:byte() - 14 + if n > 16 then + return nil + else + return 2^n + end +end + +CharacterFromColour = function(colour) + return string.char(math.floor(math.log(colour)/math.log(2))+14) +end + +Events = {} + +Button = { + X = 1, + Y = 1, + Width = 0, + Height = 0, + BackgroundColour = colours.lightGrey, + TextColour = colours.white, + ActiveBackgroundColour = colours.lightGrey, + Text = "", + Parent = nil, + _Click = nil, + Toggle = nil, + + AbsolutePosition = function(self) + return self.Parent:AbsolutePosition() + end, + + Draw = function(self) + local bg = self.BackgroundColour + local tc = self.TextColour + if type(bg) == 'function' then + bg = bg() + end + + if self.Toggle then + tc = UIColours.MenuBarActive + bg = self.ActiveBackgroundColour + end + + local pos = GetAbsolutePosition(self) + Drawing.DrawBlankArea(pos.X, pos.Y, self.Width, self.Height, bg) + Drawing.DrawCharactersCenter(pos.X, pos.Y, self.Width, self.Height, self.Text, tc, bg) + end, + + Initialise = function(self, x, y, width, height, backgroundColour, parent, click, text, textColour, toggle, activeBackgroundColour) + local new = {} -- the new instance + setmetatable( new, {__index = self} ) + height = height or 1 + new.Width = width or #text + 2 + new.Height = height + new.Y = y + new.X = x + new.Text = text or "" + new.BackgroundColour = backgroundColour or colours.lightGrey + new.TextColour = textColour or colours.white + new.ActiveBackgroundColour = activeBackgroundColour or colours.lightGrey + new.Parent = parent + new._Click = click + new.Toggle = toggle + return new + end, + + Click = function(self, side, x, y) + if self._Click then + if self:_Click(side, x, y, not self.Toggle) ~= false and self.Toggle ~= nil then + self.Toggle = not self.Toggle + Draw() + end + return true + else + return false + end + end +} + +TextBox = { + X = 1, + Y = 1, + Width = 0, + Height = 0, + BackgroundColour = colours.lightGrey, + TextColour = colours.black, + Parent = nil, + TextInput = nil, + Placeholder = '', + + AbsolutePosition = function(self) + return self.Parent:AbsolutePosition() + end, + + Draw = function(self) + local pos = GetAbsolutePosition(self) + Drawing.DrawBlankArea(pos.X, pos.Y, self.Width, self.Height, self.BackgroundColour) + local text = self.TextInput.Value + if #tostring(text) > (self.Width - 2) then + text = text:sub(#text-(self.Width - 3)) + if Current.TextInput == self.TextInput then + Current.CursorPos = {pos.X + 1 + self.Width-2, pos.Y} + end + else + if Current.TextInput == self.TextInput then + Current.CursorPos = {pos.X + 1 + self.TextInput.CursorPos, pos.Y} + end + end + + if #tostring(text) == 0 then + Drawing.DrawCharacters(pos.X + 1, pos.Y, self.Placeholder, colours.lightGrey, self.BackgroundColour) + else + Drawing.DrawCharacters(pos.X + 1, pos.Y, text, self.TextColour, self.BackgroundColour) + end + + term.setCursorBlink(true) + + Current.CursorColour = self.TextColour + end, + + Initialise = function(self, x, y, width, height, parent, text, backgroundColour, textColour, done, numerical) + local new = {} -- the new instance + setmetatable( new, {__index = self} ) + height = height or 1 + new.Width = width or #text + 2 + new.Height = height + new.Y = y + new.X = x + new.TextInput = TextInput:Initialise(text or '', function(key) + if done then + done(key) + end + Draw() + end, numerical) + new.BackgroundColour = backgroundColour or colours.lightGrey + new.TextColour = textColour or colours.black + new.Parent = parent + return new + end, + + Click = function(self, side, x, y) + Current.Input = self.TextInput + self:Draw() + end +} + +TextInput = { + Value = "", + Change = nil, + CursorPos = nil, + Numerical = false, + IsDocument = nil, + + Initialise = function(self, value, change, numerical, isDocument) + local new = {} -- the new instance + setmetatable( new, {__index = self} ) + new.Value = tostring(value) + new.Change = change + new.CursorPos = #tostring(value) + new.Numerical = numerical + new.IsDocument = isDocument or false + return new + end, + + Insert = function(self, str) + if self.Numerical then + str = tostring(tonumber(str)) + end + + local selection = OrderSelection() + + if self.IsDocument and selection then + self.Value = string.sub(self.Value, 1, selection[1]-1) .. str .. string.sub( self.Value, selection[2]+2) + self.CursorPos = selection[1] + Current.Selection = nil + else + local _, newLineAdjust = string.gsub(self.Value:sub(1, self.CursorPos), '\n','') + + self.Value = string.sub(self.Value, 1, self.CursorPos + newLineAdjust) .. str .. string.sub( self.Value, self.CursorPos + 1 + newLineAdjust) + self.CursorPos = self.CursorPos + 1 + end + + self.Change(key) + end, + + Extract = function(self, remove) + local selection = OrderSelection() + if self.IsDocument and selection then + local _, newLineAdjust = string.gsub(self.Value:sub(selection[1], selection[2]), '\n','') + local str = string.sub(self.Value, selection[1], selection[2]+1+newLineAdjust) + if remove then + self.Value = string.sub(self.Value, 1, selection[1]-1) .. string.sub( self.Value, selection[2]+2+newLineAdjust) + self.CursorPos = selection[1] - 1 + Current.Selection = nil + end + return str + end + end, + + Char = function(self, char) + if char == 'nil' then + return + end + self:Insert(char) + end, + + Key = function(self, key) + if key == keys.enter then + if self.IsDocument then + self.Value = string.sub(self.Value, 1, self.CursorPos ) .. '\n' .. string.sub( self.Value, self.CursorPos + 1 ) + self.CursorPos = self.CursorPos + 1 + end + self.Change(key) + elseif key == keys.left then + -- Left + if self.CursorPos > 0 then + local colShift = FindColours(string.sub( self.Value, self.CursorPos, self.CursorPos)) + self.CursorPos = self.CursorPos - 1 - colShift + self.Change(key) + end + + elseif key == keys.right then + -- Right + if self.CursorPos < string.len(self.Value) then + local colShift = FindColours(string.sub( self.Value, self.CursorPos+1, self.CursorPos+1)) + self.CursorPos = self.CursorPos + 1 + colShift + self.Change(key) + end + + elseif key == keys.backspace then + -- Backspace + if self.IsDocument and Current.Selection then + self:Extract(true) + self.Change(key) + elseif self.CursorPos > 0 then + local colShift = FindColours(string.sub( self.Value, self.CursorPos, self.CursorPos)) + local _, newLineAdjust = string.gsub(self.Value:sub(1, self.CursorPos), '\n','') + + self.Value = string.sub( self.Value, 1, self.CursorPos - 1 - colShift + newLineAdjust) .. string.sub( self.Value, self.CursorPos + 1 - colShift + newLineAdjust) + self.CursorPos = self.CursorPos - 1 - colShift + self.Change(key) + end + elseif key == keys.home then + -- Home + self.CursorPos = 0 + self.Change(key) + elseif key == keys.delete then + if self.IsDocument and Current.Selection then + self:Extract(true) + self.Change(key) + elseif self.CursorPos < string.len(self.Value) then + self.Value = string.sub( self.Value, 1, self.CursorPos ) .. string.sub( self.Value, self.CursorPos + 2 ) + self.Change(key) + end + elseif key == keys["end"] then + -- End + self.CursorPos = string.len(self.Value) + self.Change(key) + elseif key == keys.up and self.IsDocument then + -- Up + if Current.Document.CursorPos then + local page = Current.Document.Pages[Current.Document.CursorPos.Page] + self.CursorPos = page:GetCursorPosFromPoint(Current.Document.CursorPos.Collum + page.MarginX, Current.Document.CursorPos.Line - page.MarginY - 1 + Current.Document.ScrollBar.Scroll, true) + self.Change(key) + end + elseif key == keys.down and self.IsDocument then + -- Down + if Current.Document.CursorPos then + local page = Current.Document.Pages[Current.Document.CursorPos.Page] + self.CursorPos = page:GetCursorPosFromPoint(Current.Document.CursorPos.Collum + page.MarginX, Current.Document.CursorPos.Line - page.MarginY + 1 + Current.Document.ScrollBar.Scroll, true) + self.Change(key) + end + end + end +} + +Menu = { + X = 0, + Y = 0, + Width = 0, + Height = 0, + Owner = nil, + Items = {}, + RemoveTop = false, + + Draw = function(self) + Drawing.DrawBlankArea(self.X + 1, self.Y + 1, self.Width, self.Height, UIColours.Shadow) + if not self.RemoveTop then + Drawing.DrawBlankArea(self.X, self.Y, self.Width, self.Height, UIColours.MenuBackground) + for i, item in ipairs(self.Items) do + if item.Separator then + Drawing.DrawArea(self.X, self.Y + i, self.Width, 1, '-', colours.grey, UIColours.MenuBackground) + else + local textColour = item.Colour or UIColours.MenuText + if (item.Enabled and type(item.Enabled) == 'function' and item.Enabled() == false) or item.Enabled == false then + textColour = UIColours.MenuDisabledText + end + Drawing.DrawCharacters(self.X + 1, self.Y + i, item.Title, textColour, UIColours.MenuBackground) + end + end + else + Drawing.DrawBlankArea(self.X, self.Y, self.Width, self.Height, UIColours.MenuBackground) + for i, item in ipairs(self.Items) do + if item.Separator then + Drawing.DrawArea(self.X, self.Y + i - 1, self.Width, 1, '-', colours.grey, UIColours.MenuBackground) + else + local textColour = item.Colour or UIColours.MenuText + if (item.Enabled and type(item.Enabled) == 'function' and item.Enabled() == false) or item.Enabled == false then + textColour = UIColours.MenuDisabledText + end + Drawing.DrawCharacters(self.X + 1, self.Y + i - 1, item.Title, textColour, UIColours.MenuBackground) + + Drawing.DrawCharacters(self.X - 1 + self.Width-#item.KeyName, self.Y + i - 1, item.KeyName, textColour, UIColours.MenuBackground) + end + end + end + end, + + NameForKey = function(self, key) + if key == keys.leftCtrl then + return '^' + elseif key == keys.tab then + return 'Tab' + elseif key == keys.delete then + return 'Delete' + elseif key == keys.n then + return 'N' + elseif key == keys.a then + return 'A' + elseif key == keys.s then + return 'S' + elseif key == keys.o then + return 'O' + elseif key == keys.z then + return 'Z' + elseif key == keys.y then + return 'Y' + elseif key == keys.c then + return 'C' + elseif key == keys.x then + return 'X' + elseif key == keys.v then + return 'V' + elseif key == keys.r then + return 'R' + elseif key == keys.l then + return 'L' + elseif key == keys.t then + return 'T' + elseif key == keys.h then + return 'H' + elseif key == keys.e then + return 'E' + elseif key == keys.p then + return 'P' + elseif key == keys.f then + return 'F' + elseif key == keys.m then + return 'M' + elseif key == keys.q then + return 'Q' + else + return '?' + end + end, + + Initialise = function(self, x, y, items, owner, removeTop) + local new = {} -- the new instance + setmetatable( new, {__index = self} ) + if not owner then + return + end + + local keyNames = {} + + for i, v in ipairs(items) do + items[i].KeyName = '' + if v.Keys then + for _i, key in ipairs(v.Keys) do + items[i].KeyName = items[i].KeyName .. self:NameForKey(key) + end + end + if items[i].KeyName ~= '' then + table.insert(keyNames, items[i].KeyName) + end + end + local keysLength = LongestString(keyNames) + if keysLength > 0 then + keysLength = keysLength + 2 + end + + new.Width = LongestString(items, 'Title') + 2 + keysLength + if new.Width < 10 then + new.Width = 10 + end + new.Height = #items + 2 + new.RemoveTop = removeTop or false + if removeTop then + new.Height = new.Height - 1 + end + + if y < 1 then + y = 1 + end + if x < 1 then + x = 1 + end + + if y + new.Height > Drawing.Screen.Height + 1 then + y = Drawing.Screen.Height - new.Height + end + if x + new.Width > Drawing.Screen.Width + 1 then + x = Drawing.Screen.Width - new.Width + end + + + new.Y = y + new.X = x + new.Items = items + new.Owner = owner + return new + end, + + New = function(self, x, y, items, owner, removeTop) + if Current.Menu and Current.Menu.Owner == owner then + Current.Menu = nil + return + end + + local new = self:Initialise(x, y, items, owner, removeTop) + Current.Menu = new + return new + end, + + Click = function(self, side, x, y) + local i = y-1 + if self.RemoveTop then + i = y + end + if i >= 1 and y < self.Height then + if not ((self.Items[i].Enabled and type(self.Items[i].Enabled) == 'function' and self.Items[i].Enabled() == false) or self.Items[i].Enabled == false) and self.Items[i].Click then + self.Items[i]:Click() + if Current.Menu.Owner and Current.Menu.Owner.Toggle then + Current.Menu.Owner.Toggle = false + end + Current.Menu = nil + self = nil + end + return true + end + end +} + +MenuBar = { + X = 1, + Y = 1, + Width = Drawing.Screen.Width, + Height = 1, + MenuBarItems = {}, + + AbsolutePosition = function(self) + return {X = self.X, Y = self.Y} + end, + + Draw = function(self) + --Drawing.DrawArea(self.X - 1, self.Y, 1, self.Height, "|", UIColours.ToolbarText, UIColours.Background) + + Drawing.DrawBlankArea(self.X, self.Y, self.Width, self.Height, colours.grey) + for i, button in ipairs(self.MenuBarItems) do + button:Draw() + end + end, + + Initialise = function(self, items) + local new = {} -- the new instance + setmetatable( new, {__index = self} ) + new.X = 1 + new.Y = 1 + new.MenuBarItems = items + return new + end, + + AddToolbarItem = function(self, item) + table.insert(self.ToolbarItems, item) + self:CalculateToolbarItemPositions() + end, + + CalculateToolbarItemPositions = function(self) + local currY = 1 + for i, toolbarItem in ipairs(self.ToolbarItems) do + toolbarItem.Y = currY + currY = currY + toolbarItem.Height + end + end, + + Click = function(self, side, x, y) + for i, item in ipairs(self.MenuBarItems) do + if item.X <= x and item.X + item.Width > x then + if item:Click(item, side, x - item.X + 1, 1) then + return true + end + end + end + return false + end +} + +TextFormatPlainText = 1 +TextFormatInkText = 2 + +Document = { + X = 1, + Y = 1, + PageSize = {Width = 25, Height = 21}, + TextInput = nil, + Pages = {}, + Format = TextFormatPlainText, + Title = '', + Path = nil, + ScrollBar = nil, + Lines = {}, + CursorPos = nil, + + CalculateLineWrapping = function(self) + local limit = self.PageSize.Width + local text = self.TextInput.Value + local lines = {''} + local words = {} + + for word, space in text:gmatch('(%S+)(%s*)') do + for i = 1, math.ceil(#word/limit) do + local _space = '' + if i == math.ceil(#word/limit) then + _space = space + end + table.insert(words, {word:sub(1+limit*(i-1), limit*i), _space}) + end + end + + for i, ws in ipairs(words) do + local word = ws[1] + local space = ws[2] + local temp = lines[#lines] .. word .. space:gsub('\n','') + if #temp > limit then + table.insert(lines, '') + end + if space:find('\n') then + lines[#lines] = lines[#lines] .. word + + space = space:gsub('\n', function() + table.insert(lines, '') + return '' + end) + else + lines[#lines] = lines[#lines] .. word .. space + end + end + return lines + end, + + CalculateCursorPos = function(self) + local passedCharacters = 0 + Current.CursorPos = nil + for p, page in ipairs(self.Pages) do + page:Draw() + if not Current.CursorPos then + for i, line in ipairs(page.Lines) do + local relCursor = self.TextInput.CursorPos - FindColours(self.TextInput.Value:sub(1,self.TextInput.CursorPos)) + if passedCharacters + #StripColours(line.Text:gsub('\n','')) >= relCursor then + Current.CursorPos = {self.X + page.MarginX + (relCursor - passedCharacters), page.Y + 1 + i} + self.CursorPos = {Page = p, Line = i, Collum = relCursor - passedCharacters - FindColours(self.TextInput.Value:sub(1,self.TextInput.CursorPos-1))} + break + end + passedCharacters = passedCharacters + #StripColours(line.Text:gsub('\n','')) + end + end + end + end, + + Draw = function(self) + self:CalculatePages() + self:CalculateCursorPos() + self.ScrollBar:Draw() + end, + + CalculatePages = function(self) + self.Pages = {} + local lines = self:CalculateLineWrapping() + self.Lines = lines + local pageLines = {} + local totalPageHeight = (3 + self.PageSize.Height + 2 * Page.MarginY) + for i, line in ipairs(lines) do + table.insert(pageLines, TextLine:Initialise(line)) + if i % self.PageSize.Height == 0 then + table.insert(self.Pages, Page:Initialise(self, pageLines, 3 - self.ScrollBar.Scroll + totalPageHeight*(#self.Pages))) + pageLines = {} + end + end + if #pageLines ~= 0 then + table.insert(self.Pages, Page:Initialise(self, pageLines, 3 - self.ScrollBar.Scroll + totalPageHeight*(#self.Pages))) + end + + self.ScrollBar.MaxScroll = totalPageHeight*(#self.Pages) - Drawing.Screen.Height + 1 + end, + + ScrollToCursor = function(self) + self:CalculateCursorPos() + if Current.CursorPos and + (Current.CursorPos[2] > Drawing.Screen.Height + or Current.CursorPos[2] < 2) then + self.ScrollBar:DoScroll(Current.CursorPos[2] - Drawing.Screen.Height) + end + end, + + SetSelectionColour = function(self, colour) + local selection = OrderSelection() + local text = self.TextInput:Extract(true) + local colChar = CharacterFromColour(colour) + local precedingColour = '' + if FindColours(self.TextInput.Value:sub(self.TextInput.CursorPos+1, self.TextInput.CursorPos+1)) == 0 then + for i = 1, self.TextInput.CursorPos do + local c = self.TextInput.Value:sub(self.TextInput.CursorPos - i,self.TextInput.CursorPos - i) + if FindColours(c) == 1 then + precedingColour = c + break + end + end + if precedingColour == '' then + precedingColour = CharacterFromColour(colours.black) + end + end + + self.TextInput:Insert(colChar..StripColours(text)..precedingColour) + --text = text:gsub('['..string.char(14)..'-'..string.char(29)..']','') + end, + + Initialise = function(self, text, title, path) + local new = {} -- the new instance + setmetatable( new, {__index = self} ) + new.Title = title or 'New Document' + new.Path = path + new.X = (Drawing.Screen.Width - (new.PageSize.Width + 2*(Page.MarginX)))/2 + new.Y = 2 + new.TextInput = TextInput:Initialise(text, function() + new:ScrollToCursor() + Current.Modified = true + Draw() + end, false, true) + new.ScrollBar = ScrollBar:Initialise(Drawing.Screen.Width, new.Y, Drawing.Screen.Height-1, 0, nil, nil, nil, function()end) + Current.TextInput = new.TextInput + Current.ScrollBar = new.ScrollBar + return new + end +} + +ScrollBar = { + X = 1, + Y = 1, + Width = 1, + Height = 1, + BackgroundColour = colours.grey, + BarColour = colours.lightBlue, + Parent = nil, + Change = nil, + Scroll = 0, + MaxScroll = 0, + ClickPoint = nil, + + AbsolutePosition = function(self) + return self.Parent:AbsolutePosition() + end, + + Draw = function(self) + local pos = GetAbsolutePosition(self) + local barHeight = self.Height - self.MaxScroll + if barHeight < 3 then + barHeight = 3 + end + local percentage = (self.Scroll/self.MaxScroll) + + Drawing.DrawBlankArea(pos.X, pos.Y, self.Width, self.Height, self.BackgroundColour) + Drawing.DrawBlankArea(pos.X, pos.Y + round(self.Height*percentage - barHeight*percentage), self.Width, barHeight, self.BarColour) + end, + + Initialise = function(self, x, y, height, maxScroll, backgroundColour, barColour, parent, change) + local new = {} -- the new instance + setmetatable( new, {__index = self} ) + new.Width = 1 + new.Height = height + new.Y = y + new.X = x + new.BackgroundColour = backgroundColour or colours.grey + new.BarColour = barColour or colours.lightBlue + new.Parent = parent + new.Change = change or function()end + new.MaxScroll = maxScroll + new.Scroll = 0 + return new + end, + + DoScroll = function(self, amount) + amount = round(amount) + if self.Scroll < 0 or self.Scroll > self.MaxScroll then + return false + end + self.Scroll = self.Scroll + amount + if self.Scroll < 0 then + self.Scroll = 0 + elseif self.Scroll > self.MaxScroll then + self.Scroll = self.MaxScroll + end + self.Change() + return true + end, + + Click = function(self, side, x, y, drag) + local percentage = (self.Scroll/self.MaxScroll) + local barHeight = (self.Height - self.MaxScroll) + if barHeight < 3 then + barHeight = 3 + end + local relScroll = (self.MaxScroll*(y + barHeight*percentage)/self.Height) + if not drag then + self.ClickPoint = self.Scroll - relScroll + 1 + end + + if self.Scroll-1 ~= relScroll then + self:DoScroll(relScroll-self.Scroll-1 + self.ClickPoint) + end + return true + end +} + +AlignmentLeft = 1 +AlignmentCentre = 2 +AlignmentRight = 3 + +TextLine = { + Text = "", + Alignment = AlignmentLeft, + + Initialise = function(self, text, alignment) + local new = {} -- the new instance + setmetatable( new, {__index = self} ) + new.Text = text + new.Alignment = alignment or AlignmentLeft + return new + end +} + +local clickPos = 1 +Page = { + X = 1, + Y = 1, + Width = 1, + Height = 1, + MarginX = 3, + MarginY = 2, + BackgroundColour = colours.white, + TextColour = colours.white, + ActiveBackgroundColour = colours.lightGrey, + Lines = {}, + Parent = nil, + + AbsolutePosition = function(self) + return self.Parent:AbsolutePosition() + end, + + Draw = function(self) + local pos = GetAbsolutePosition(self) + + if pos.Y > Drawing.Screen.Height or pos.Y + self.Height < 1 then + return + end + + Drawing.DrawBlankArea(pos.X+self.Width,pos.Y -1 + 1, 1, self.Height, UIColours.Shadow) + Drawing.DrawBlankArea(pos.X+1, pos.Y -1 + self.Height, self.Width, 1, UIColours.Shadow) + Drawing.DrawBlankArea(pos.X, pos.Y -1, self.Width, self.Height, self.BackgroundColour) + + local textColour = self.TextColour + if not Current.Selection then + for i, line in ipairs(self.Lines) do + local _c = 1 + for c = 1, #line.Text do + local col = ColourFromCharacter(line.Text:sub(c,c)) + if col then + textColour = col + else + Drawing.WriteToBuffer(pos.X + self.MarginX - 1 + _c, pos.Y -2 + i + self.MarginY, line.Text:sub(c,c), textColour, self.BackgroundColour) + _c = _c + 1 + end + end + end + else + local selection = OrderSelection() + local char = 1 + local textColour = self.TextColour + for i, line in ipairs(self.Lines) do + local _c = 1 + for c = 1, #line.Text do + local col = ColourFromCharacter(line.Text:sub(c,c)) + if col then + textColour = col + else + local tc = textColour + local colour = colours.white + if char >= selection[1] and char <= selection[2] then + colour = colours.lightBlue + tc = colours.white + end + + Drawing.WriteToBuffer(pos.X + self.MarginX - 1 + _c, pos.Y -2 + i + self.MarginY, line.Text:sub(c,c), tc, colour) + _c = _c + 1 + end + char = char + 1 + end + end + end + end, + + Initialise = function(self, parent, lines, y) + local new = {} -- the new instanc + setmetatable( new, {__index = self} ) + new.Height = parent.PageSize.Height + 2 * self.MarginY + new.Width = parent.PageSize.Width + 2 * self.MarginX + new.X = 1 + new.Y = y or 1 + new.Lines = lines or {} + new.BackgroundColour = colours.white + new.TextColour = colours.black + new.Parent = parent + new.ClickPos = 1 + return new + end, + + GetCursorPosFromPoint = function(self, x, y, rel) + local pos = GetAbsolutePosition(self) + if rel then + pos = {Y = 0, X = 0} + end + local row = y - pos.Y + self.MarginY - self.Parent.ScrollBar.Scroll + local col = x - self.MarginX - pos.X + 1 + local cursorPos = 0 + if row <= 0 or col <= 0 then + return 0 + end + + if row > #self.Lines then + for i, v in ipairs(self.Lines) do + cursorPos = cursorPos + #v.Text-- - FindColours(v.Text) + end + return cursorPos + end + + --term.setCursorPos(1,3) + local prevLineCount = 0 + for i, v in ipairs(self.Lines) do + if i == row then + if col > #v.Text then + col = #v.Text-- + FindColours(v.Text) + else + col = col + FindColours(v.Text:sub(1, col)) + end + --term.setCursorPos(1,2) + --print(prevLineCount) + cursorPos = cursorPos + col + 2 - i - prevLineCount + break + else + prevLineCount = FindColours(v.Text) + if prevLineCount ~= 0 then + prevLineCount = prevLineCount + end + cursorPos = cursorPos + #v.Text + 2 - i + FindColours(v.Text) + end + end + + return cursorPos - 2 + end, + + Click = function(self, side, x, y, drag) + local cursorPos = self:GetCursorPosFromPoint(x, y) + self.Parent.TextInput.CursorPos = cursorPos + if drag == nil then + Current.Selection = nil + clickPos = x + else + local relCursor = cursorPos-- - FindColours(self.Parent.TextInput.Value:sub(1,cursorPos)) + 1 + if not Current.Selection then + local adder = 1 + if clickPos and clickPos < x then + adder = 0 + end + Current.Selection = {relCursor + adder, relCursor + 1 + adder} + else + Current.Selection[2] = relCursor + 1 + end + end + Draw() + return true + end +} + +function GetAbsolutePosition(object) + local obj = object + local i = 0 + local x = 1 + local y = 1 + while true do + x = x + obj.X - 1 + y = y + obj.Y - 1 + + if not obj.Parent then + return {X = x, Y = y} + end + + obj = obj.Parent + + if i > 32 then + return {X = 1, Y = 1} + end + + i = i + 1 + end + +end + +function Draw() + if not Current.Window then + Drawing.Clear(colours.lightGrey) + else + Drawing.DrawArea(1, 2, Drawing.Screen.Width, Drawing.Screen.Height, '|', colours.black, colours.lightGrey) + end + + if Current.Document then + Current.Document:Draw() + end + + Current.MenuBar:Draw() + + if Current.Window then + Current.Window:Draw() + end + + if Current.Menu then + Current.Menu:Draw() + end + + Drawing.DrawBuffer() + + if Current.TextInput and Current.CursorPos and not Current.Menu and not(Current.Window and Current.Document and Current.TextInput == Current.Document.TextInput) and Current.CursorPos[2] > 1 then + term.setCursorPos(Current.CursorPos[1], Current.CursorPos[2]) + term.setCursorBlink(true) + term.setTextColour(Current.CursorColour) + else + term.setCursorBlink(false) + end +end +MainDraw = Draw + +LongestString = function(input, key) + local length = 0 + for i = 1, #input do + local value = input[i] + if key then + if value[key] then + value = value[key] + else + value = '' + end + end + local titleLength = string.len(value) + if titleLength > length then + length = titleLength + end + end + return length +end + +function LoadMenuBar() + Current.MenuBar = MenuBar:Initialise({ + Button:Initialise(1, 1, nil, nil, colours.grey, Current.MenuBar, function(self, side, x, y, toggle) + if toggle then + Menu:New(1, 2, { + { + Title = "New...", + Click = function() + Current.Document = Document:Initialise('') + end, + Keys = { + keys.leftCtrl, + keys.n + } + }, + { + Title = 'Open...', + Click = function() + DisplayOpenDocumentWindow() + end, + Keys = { + keys.leftCtrl, + keys.o + } + }, + { + Separator = true + }, + { + Title = 'Save...', + Click = function() + SaveDocument() + end, + Keys = { + keys.leftCtrl, + keys.s + }, + Enabled = function() + return true + end + }, + { + Separator = true + }, + { + Title = 'Print...', + Click = function() + PrintDocument() + end, + Keys = { + keys.leftCtrl, + keys.p + }, + Enabled = function() + return true + end + }, + { + Separator = true + }, + { + Title = 'Quit', + Click = function() + Close() + end + }, + --[[ + { + Title = 'Save As...', + Click = function() + + end + } + ]]-- + }, self, true) + else + Current.Menu = nil + end + return true + end, 'File', colours.lightGrey, false), + Button:Initialise(7, 1, nil, nil, colours.grey, Current.MenuBar, function(self, side, x, y, toggle) + if not self.Toggle then + Menu:New(7, 2, { + --[[ + { + Title = "Undo", + Click = function() + end, + Keys = { + keys.leftCtrl, + keys.z + }, + Enabled = function() + return false + end + }, + { + Title = 'Redo', + Click = function() + + end, + Keys = { + keys.leftCtrl, + keys.y + }, + Enabled = function() + return false + end + }, + { + Separator = true + }, + ]]-- + { + Title = 'Cut', + Click = function() + Clipboard.Cut(Current.Document.TextInput:Extract(true), 'text') + end, + Keys = { + keys.leftCtrl, + keys.x + }, + Enabled = function() + return Current.Document ~= nil and Current.Selection and Current.Selection[1] and Current.Selection[2] ~= nil + end + }, + { + Title = 'Copy', + Click = function() + Clipboard.Copy(Current.Document.TextInput:Extract(), 'text') + end, + Keys = { + keys.leftCtrl, + keys.c + }, + Enabled = function() + return Current.Document ~= nil and Current.Selection and Current.Selection[1] and Current.Selection[2] ~= nil + end + }, + { + Title = 'Paste', + Click = function() + local paste = Clipboard.Paste() + Current.Document.TextInput:Insert(paste) + Current.Document.TextInput.CursorPos = Current.Document.TextInput.CursorPos + #paste - 1 + end, + Keys = { + keys.leftCtrl, + keys.v + }, + Enabled = function() + return Current.Document ~= nil and (not Clipboard.isEmpty()) and Clipboard.Type == 'text' + end + }, + { + Separator = true, + }, + { + Title = 'Select All', + Click = function() + Current.Selection = {1, #Current.Document.TextInput.Value:gsub('\n','')} + end, + Keys = { + keys.leftCtrl, + keys.a + }, + Enabled = function() + return Current.Document ~= nil + end + } + }, self, true) + else + Current.Menu = nil + end + return true + end, 'Edit', colours.lightGrey, false) + }) +end + +function LoadMenuBar() + Current.MenuBar = MenuBar:Initialise({ + Button:Initialise(1, 1, nil, nil, colours.grey, Current.MenuBar, function(self, side, x, y, toggle) + if toggle then + Menu:New(1, 2, { + { + Title = "New...", + Click = function() + Current.Document = Document:Initialise('') + end, + Keys = { + keys.leftCtrl, + keys.n + } + }, + { + Title = 'Open...', + Click = function() + DisplayOpenDocumentWindow() + end, + Keys = { + keys.leftCtrl, + keys.o + } + }, + { + Separator = true + }, + { + Title = 'Save...', + Click = function() + SaveDocument() + end, + Keys = { + keys.leftCtrl, + keys.s + }, + Enabled = function() + return Current.Document ~= nil + end + }, + { + Separator = true + }, + { + Title = 'Print...', + Click = function() + PrintDocument() + end, + Keys = { + keys.leftCtrl, + keys.p + }, + Enabled = function() + return true + end + }, + { + Separator = true + }, + { + Title = 'Quit', + Click = function() + if Close() and OneOS then + OneOS.Close() + end + end, + Keys = { + keys.leftCtrl, + keys.q + } + }, + --[[ + { + Title = 'Save As...', + Click = function() + + end + } + ]]-- + }, self, true) + else + Current.Menu = nil + end + return true + end, 'File', colours.lightGrey, false), + Button:Initialise(7, 1, nil, nil, colours.grey, Current.MenuBar, function(self, side, x, y, toggle) + if not self.Toggle then + Menu:New(7, 2, { + --[[ + { + Title = "Undo", + Click = function() + end, + Keys = { + keys.leftCtrl, + keys.z + }, + Enabled = function() + return false + end + }, + { + Title = 'Redo', + Click = function() + + end, + Keys = { + keys.leftCtrl, + keys.y + }, + Enabled = function() + return false + end + }, + { + Separator = true + }, + ]]-- + { + Title = 'Cut', + Click = function() + Clipboard.Cut(Current.Document.TextInput:Extract(true), 'text') + end, + Keys = { + keys.leftCtrl, + keys.x + }, + Enabled = function() + return Current.Document ~= nil and Current.Selection and Current.Selection[1] and Current.Selection[2] ~= nil + end + }, + { + Title = 'Copy', + Click = function() + Clipboard.Copy(Current.Document.TextInput:Extract(), 'text') + end, + Keys = { + keys.leftCtrl, + keys.c + }, + Enabled = function() + return Current.Document ~= nil and Current.Selection and Current.Selection[1] and Current.Selection[2] ~= nil + end + }, + { + Title = 'Paste', + Click = function() + local paste = Clipboard.Paste() + Current.Document.TextInput:Insert(paste) + Current.Document.TextInput.CursorPos = Current.Document.TextInput.CursorPos + #paste - 1 + end, + Keys = { + keys.leftCtrl, + keys.v + }, + Enabled = function() + return Current.Document ~= nil and (not Clipboard.isEmpty()) and Clipboard.Type == 'text' + end + }, + { + Separator = true, + }, + { + Title = 'Select All', + Click = function() + Current.Selection = {1, #Current.Document.TextInput.Value:gsub('\n','')} + end, + Keys = { + keys.leftCtrl, + keys.a + }, + Enabled = function() + return Current.Document ~= nil + end + } + }, self, true) + else + Current.Menu = nil + end + return true + end, 'Edit', colours.lightGrey, false), + Button:Initialise(13, 1, nil, nil, colours.grey, Current.MenuBar, function(self, side, x, y, toggle) + if not self.Toggle then + Menu:New(13, 2, { + { + Title = 'Red', + Click = function(item) + Current.Document:SetSelectionColour(item.Colour) + end, + Colour = colours.red, + Enabled = function() + return (Current.Document ~= nil and Current.Selection ~= nil and Current.Selection[1] ~= nil and Current.Selection[2] ~= nil) + end + }, + { + Title = 'Orange', + Click = function(item) + Current.Document:SetSelectionColour(item.Colour) + end, + Colour = colours.orange, + Enabled = function() + return (Current.Document ~= nil and Current.Selection ~= nil and Current.Selection[1] ~= nil and Current.Selection[2] ~= nil) + end + }, + { + Title = 'Yellow', + Click = function(item) + Current.Document:SetSelectionColour(item.Colour) + end, + Colour = colours.yellow, + Enabled = function() + return (Current.Document ~= nil and Current.Selection ~= nil and Current.Selection[1] ~= nil and Current.Selection[2] ~= nil) + end + }, + { + Title = 'Pink', + Click = function(item) + Current.Document:SetSelectionColour(item.Colour) + end, + Colour = colours.pink, + Enabled = function() + return (Current.Document ~= nil and Current.Selection ~= nil and Current.Selection[1] ~= nil and Current.Selection[2] ~= nil) + end + }, + { + Title = 'Magenta', + Click = function(item) + Current.Document:SetSelectionColour(item.Colour) + end, + Colour = colours.magenta, + Enabled = function() + return (Current.Document ~= nil and Current.Selection ~= nil and Current.Selection[1] ~= nil and Current.Selection[2] ~= nil) + end + }, + { + Title = 'Purple', + Click = function(item) + Current.Document:SetSelectionColour(item.Colour) + end, + Colour = colours.purple, + Enabled = function() + return (Current.Document ~= nil and Current.Selection ~= nil and Current.Selection[1] ~= nil and Current.Selection[2] ~= nil) + end + }, + { + Title = 'Light Blue', + Click = function(item) + Current.Document:SetSelectionColour(item.Colour) + end, + Colour = colours.lightBlue, + Enabled = function() + return (Current.Document ~= nil and Current.Selection ~= nil and Current.Selection[1] ~= nil and Current.Selection[2] ~= nil) + end + }, + { + Title = 'Cyan', + Click = function(item) + Current.Document:SetSelectionColour(item.Colour) + end, + Colour = colours.cyan, + Enabled = function() + return (Current.Document ~= nil and Current.Selection ~= nil and Current.Selection[1] ~= nil and Current.Selection[2] ~= nil) + end + }, + { + Title = 'Blue', + Click = function(item) + Current.Document:SetSelectionColour(item.Colour) + end, + Colour = colours.blue, + Enabled = function() + return (Current.Document ~= nil and Current.Selection ~= nil and Current.Selection[1] ~= nil and Current.Selection[2] ~= nil) + end + }, + { + Title = 'Green', + Click = function(item) + Current.Document:SetSelectionColour(item.Colour) + end, + Colour = colours.green, + Enabled = function() + return (Current.Document ~= nil and Current.Selection ~= nil and Current.Selection[1] ~= nil and Current.Selection[2] ~= nil) + end + }, + { + Title = 'Light Grey', + Click = function(item) + Current.Document:SetSelectionColour(item.Colour) + end, + Colour = colours.lightGrey, + Enabled = function() + return (Current.Document ~= nil and Current.Selection ~= nil and Current.Selection[1] ~= nil and Current.Selection[2] ~= nil) + end + }, + { + Title = 'Grey', + Click = function(item) + Current.Document:SetSelectionColour(item.Colour) + end, + Colour = colours.grey, + Enabled = function() + return (Current.Document ~= nil and Current.Selection ~= nil and Current.Selection[1] ~= nil and Current.Selection[2] ~= nil) + end + }, + { + Title = 'Black', + Click = function(item) + Current.Document:SetSelectionColour(item.Colour) + end, + Colour = colours.black, + Enabled = function() + return (Current.Document ~= nil and Current.Selection ~= nil and Current.Selection[1] ~= nil and Current.Selection[2] ~= nil) + end + }, + { + Title = 'Brown', + Click = function(item) + Current.Document:SetSelectionColour(item.Colour) + end, + Colour = colours.brown, + Enabled = function() + return (Current.Document ~= nil and Current.Selection ~= nil and Current.Selection[1] ~= nil and Current.Selection[2] ~= nil) + end + } + }, self, true) + else + Current.Menu = nil + end + return true + end, 'Colour', colours.lightGrey, false) + }) +end + +function SplashScreen() + local w = colours.white + local b = colours.black + local u = colours.blue + local lb = colours.lightBlue + local splashIcon = {{w,w,w,b,w,w,w,b,w,w,w,},{w,w,w,b,w,w,w,b,w,w,w,},{w,w,w,b,u,u,u,b,w,w,w,},{w,b,b,u,u,u,u,u,b,b,w,},{b,u,u,lb,lb,u,u,u,u,u,b,},{b,u,lb,lb,u,u,u,u,u,u,b,},{b,u,lb,lb,u,u,u,u,u,u,b,},{b,u,u,u,u,u,u,u,u,u,b,},{w,b,b,b,b,b,b,b,b,b,w,}, + ["text"]={{" "," "," "," "," "," "," "," "," "," "," ",},{" "," "," "," "," "," "," "," "," "," "," ",},{" "," "," "," "," "," "," "," "," "," "," ",},{" "," "," "," "," "," "," "," "," "," "," ",},{" "," "," "," "," "," "," "," "," "," "," ",},{" "," "," "," ","I","n","k"," "," "," "," ",},{" "," "," "," "," "," "," "," "," "," "," ",},{" "," ","b","y"," ","o","e","e","d"," "," "},{" "," "," "," "," "," "," "," "," "," "," ",},}, + ["textcol"]={{w,w,w,w,w,w,w,w,w,w,w,},{w,w,w,w,w,w,w,w,w,w,w,},{w,w,w,w,w,w,w,w,w,w,w,},{w,w,w,w,w,w,w,w,w,w,w,},{w,w,w,w,w,w,w,w,w,w,w,},{w,w,w,w,w,w,w,w,w,w,w,},{w,w,w,w,w,w,w,w,w,w,w,},{lb,lb,lb,lb,lb,lb,lb,lb,lb,lb,lb,},{w,w,w,w,w,w,w,w,w,w,w,},},} + Drawing.Clear(colours.white) + Drawing.DrawImage((Drawing.Screen.Width - 11)/2, (Drawing.Screen.Height - 9)/2, splashIcon, 11, 9) + Drawing.DrawBuffer() + Drawing.Clear(colours.black) + parallel.waitForAny(function()sleep(1)end, function()os.pullEvent('mouse_click')end) +end + +function Initialise(arg) + if OneOS then + fs = OneOS.FS + end + + if not OneOS then + SplashScreen() + end + EventRegister('mouse_click', TryClick) + EventRegister('mouse_drag', function(event, side, x, y)TryClick(event, side, x, y, true)end) + EventRegister('mouse_scroll', Scroll) + EventRegister('key', HandleKey) + EventRegister('char', HandleKey) + EventRegister('timer', Timer) + EventRegister('terminate', function(event) if Close() then error( "Terminated", 0 ) end end) + + LoadMenuBar() + + --Current.Document = Document:Initialise('abcdefghijklmnopqrtuvwxy')--'Hello everybody!') + if tArgs[1] then + if fs.exists(tArgs[1]) then + OpenDocument(tArgs[1]) + else + --new + end + else + Current.Document = Document:Initialise('')--'Hello everybody!') + end + + --[[ + if arg and fs.exists(arg) then + OpenDocument(arg) + else + DisplayNewDocumentWindow() + end + ]]-- + Draw() + + EventHandler() +end + +local isControlPushed = false +controlPushedTimer = nil +closeWindowTimer = nil +function Timer(event, timer) + if timer == closeWindowTimer then + if Current.Window then + Current.Window:Close() + end + Draw() + elseif timer == controlPushedTimer then + isControlPushed = false + end +end + +local ignoreNextChar = false +function HandleKey(...) + local args = {...} + local event = args[1] + local keychar = args[2] + --Mac left command character + if event == 'key' and keychar == keys.leftCtrl or keychar == keys.rightCtrl or keychar == 219 then + isControlPushed = true + controlPushedTimer = os.startTimer(0.5) + elseif isControlPushed then + if event == 'key' then + if CheckKeyboardShortcut(keychar) then + isControlPushed = false + ignoreNextChar = true + end + end + elseif ignoreNextChar then + ignoreNextChar = false + elseif Current.TextInput then + if event == 'char' then + Current.TextInput:Char(keychar) + elseif event == 'key' then + Current.TextInput:Key(keychar) + end + end +end + +function CheckKeyboardShortcut(key) + local shortcuts = {} + shortcuts[keys.n] = function() Current.Document = Document:Initialise('') end + shortcuts[keys.o] = function() DisplayOpenDocumentWindow() end + shortcuts[keys.s] = function() if Current.Document ~= nil then SaveDocument() end end + shortcuts[keys.left] = function() if Current.TextInput then Current.TextInput:Key(keys.home) end end + shortcuts[keys.right] = function() if Current.TextInput then Current.TextInput:Key(keys["end"]) end end +-- shortcuts[keys.q] = function() DisplayOpenDocumentWindow() end + if Current.Document ~= nil then + shortcuts[keys.s] = function() SaveDocument() end + shortcuts[keys.p] = function() PrintDocument() end + if Current.Selection and Current.Selection[1] and Current.Selection[2] ~= nil then + shortcuts[keys.x] = function() Clipboard.Cut(Current.Document.TextInput:Extract(true), 'text') end + shortcuts[keys.c] = function() Clipboard.Copy(Current.Document.TextInput:Extract(), 'text') end + end + if (not Clipboard.isEmpty()) and Clipboard.Type == 'text' then + shortcuts[keys.v] = function() local paste = Clipboard.Paste() + Current.Document.TextInput:Insert(paste) + Current.Document.TextInput.CursorPos = Current.Document.TextInput.CursorPos + #paste - 1 + end + end + shortcuts[keys.a] = function() Current.Selection = {1, #Current.Document.TextInput.Value:gsub('\n','')} end + end + + if shortcuts[key] then + shortcuts[key]() + Draw() + return true + else + return false + end +end + +--[[ + Check if the given object falls under the click coordinates +]]-- +function CheckClick(object, x, y) + if object.X <= x and object.Y <= y and object.X + object.Width > x and object.Y + object.Height > y then + return true + end +end + +--[[ + Attempt to clicka given object +]]-- +function DoClick(object, side, x, y, drag) + local obj = GetAbsolutePosition(object) + obj.Width = object.Width + obj.Height = object.Height + if object and CheckClick(obj, x, y) then + return object:Click(side, x - object.X + 1, y - object.Y + 1, drag) + end +end + +--[[ + Try to click at the given coordinates +]]-- +function TryClick(event, side, x, y, drag) + if Current.Menu then + if DoClick(Current.Menu, side, x, y, drag) then + Draw() + return + else + if Current.Menu.Owner and Current.Menu.Owner.Toggle then + Current.Menu.Owner.Toggle = false + end + Current.Menu = nil + Draw() + return + end + elseif Current.Window then + if DoClick(Current.Window, side, x, y, drag) then + Draw() + return + else + Current.Window:Flash() + return + end + end + local interfaceElements = {} + + table.insert(interfaceElements, Current.MenuBar) + table.insert(interfaceElements, Current.ScrollBar) + for i, page in ipairs(Current.Document.Pages) do + table.insert(interfaceElements, page) + end + + for i, object in ipairs(interfaceElements) do + if DoClick(object, side, x, y, drag) then + Draw() + return + end + end + Draw() +end + +function Scroll(event, direction, x, y) + if Current.Window and Current.Window.OpenButton then + Current.Document.Scroll = Current.Document.Scroll + direction + if Current.Window.Scroll < 0 then + Current.Window.Scroll = 0 + elseif Current.Window.Scroll > Current.Window.MaxScroll then + Current.Window.Scroll = Current.Window.MaxScroll + end + Draw() + elseif Current.ScrollBar then + if Current.ScrollBar:DoScroll(direction*2) then + Draw() + end + end +end + + +--[[ + Registers functions to run on certain events +]]-- +function EventRegister(event, func) + if not Events[event] then + Events[event] = {} + end + + table.insert(Events[event], func) +end + +--[[ + The main loop event handler, runs registered event functinos +]]-- +function EventHandler() + while true do + local event, arg1, arg2, arg3, arg4 = os.pullEventRaw() + if Events[event] then + for i, e in ipairs(Events[event]) do + e(event, arg1, arg2, arg3, arg4) + end + end + end +end + + +local function Extension(path, addDot) + if not path then + return nil + elseif not string.find(fs.getName(path), '%.') then + if not addDot then + return fs.getName(path) + else + return '' + end + else + local _path = path + if path:sub(#path) == '/' then + _path = path:sub(1,#path-1) + end + local extension = _path:gmatch('%.[0-9a-z]+$')() + if extension then + extension = extension:sub(2) + else + --extension = nil + return '' + end + if addDot then + extension = '.'..extension + end + return extension:lower() + end +end + +local RemoveExtension = function(path) + if path:sub(1,1) == '.' then + return path + end + local extension = Extension(path) + if extension == path then + return fs.getName(path) + end + return string.gsub(path, extension, ''):sub(1, -2) +end + +local acknowledgedColour = false +function PrintDocument() + if OneOS then + OneOS.LoadAPI('/System/API/Helpers.lua') + OneOS.LoadAPI('/System/API/Peripheral.lua') + OneOS.LoadAPI('/System/API/Printer.lua') + end + + local doPrint = function() + local window = PrintDocumentWindow:Initialise():Show() + end + + if Peripheral.GetPeripheral('printer') == nil then + ButtonDialougeWindow:Initialise('No Printer Found', 'Please place a printer next to your computer. Ensure you also insert dye (left slot) and paper (top slots)', 'Ok', nil, function(window, ok) + window:Close() + end):Show() + elseif not acknowledgedColour and FindColours(Current.Document.TextInput.Value) ~= 0 then + ButtonDialougeWindow:Initialise('Important', 'Due to the way printers work, you can\'t print in more than one colour. The dye you use will be the colour of the text.', 'Ok', nil, function(window, ok) + acknowledgedColour = true + window:Close() + doPrint() + end):Show() + else + doPrint() + end +end + +function SaveDocument() + local function save() + local h = fs.open(Current.Document.Path, 'w') + if h then + if Current.Document.Format == TextFormatPlainText then + h.write(Current.Document.TextInput.Value) + else + local lines = {} + for p, page in ipairs(Current.Document.Pages) do + for i, line in ipairs(page.Lines) do + table.insert(lines, line) + end + end + h.write(textutils.serialize(lines)) + end + Current.Modified = false + else + ButtonDialougeWindow:Initialise('Error', 'An error occured while saving the file, try again.', 'Ok', nil, function(window, ok) + window:Close() + end):Show() + end + h.close() + end + + if not Current.Document.Path then + SaveDocumentWindow:Initialise(function(self, success, path) + self:Close() + if success then + local extension = '' + if Current.Document.Format == TextFormatPlainText then + extension = '.txt' + elseif Current.Document.Format == TextFormatInkText then + extension = '.ink' + end + + if path:sub(-4) ~= extension then + path = path .. extension + end + + Current.Document.Path = path + Current.Document.Title = fs.getName(path) + save() + end + if Current.Document then + Current.TextInput = Current.Document.TextInput + end + end):Show() + else + save() + end +end + +function DisplayOpenDocumentWindow() + OpenDocumentWindow:Initialise(function(self, success, path) + self:Close() + if success then + OpenDocument(path) + end + end):Show() +end + +function OpenDocument(path) + Current.Selection = nil + local h = fs.open(path, 'r') + if h then + Current.Document = Document:Initialise(h.readAll(), RemoveExtension(fs.getName(path)), path) + else + ButtonDialougeWindow:Initialise('Error', 'An error occured while opening the file, try again.', 'Ok', nil, function(window, ok) + window:Close() + if Current.Document then + Current.TextInput = Current.Document.TextInput + end + end):Show() + end + h.close() +end + +local TidyPath = function(path) + path = '/'..path + local fs = fs + if OneOS then + fs = OneOS.FS + end + if fs.isDir(path) then + path = path .. '/' + end + + path, n = path:gsub("//", "/") + while n > 0 do + path, n = path:gsub("//", "/") + end + return path +end + +OpenDocumentWindow = { + X = 1, + Y = 1, + Width = 0, + Height = 0, + CursorPos = 1, + Visible = true, + Return = nil, + OpenButton = nil, + PathTextBox = nil, + CurrentDirectory = '/', + Scroll = 0, + MaxScroll = 0, + GoUpButton = nil, + SelectedFile = '', + Files = {}, + Typed = false, + + AbsolutePosition = function(self) + return {X = self.X, Y = self.Y} + end, + + Draw = function(self) + if not self.Visible then + return + end + Drawing.DrawBlankArea(self.X + 1, self.Y+1, self.Width, self.Height, colours.grey) + Drawing.DrawBlankArea(self.X, self.Y, self.Width, 3, colours.lightGrey) + Drawing.DrawBlankArea(self.X, self.Y+1, self.Width, self.Height-6, colours.white) + Drawing.DrawCharactersCenter(self.X, self.Y, self.Width, 1, self.Title, colours.black, colours.lightGrey) + Drawing.DrawBlankArea(self.X, self.Y + self.Height - 5, self.Width, 5, colours.lightGrey) + self:DrawFiles() + + if (fs.exists(self.PathTextBox.TextInput.Value)) or (self.SelectedFile and #self.SelectedFile > 0 and fs.exists(self.CurrentDirectory .. self.SelectedFile)) then + self.OpenButton.TextColour = colours.black + else + self.OpenButton.TextColour = colours.lightGrey + end + + self.PathTextBox:Draw() + self.OpenButton:Draw() + self.CancelButton:Draw() + self.GoUpButton:Draw() + end, + + DrawFiles = function(self) + for i, file in ipairs(self.Files) do + if i > self.Scroll and i - self.Scroll <= 11 then + if file == self.SelectedFile then + Drawing.DrawCharacters(self.X + 1, self.Y + i - self.Scroll, file, colours.white, colours.lightBlue) + elseif string.find(file, '%.txt') or string.find(file, '%.text') or string.find(file, '%.ink') or fs.isDir(self.CurrentDirectory .. file) then + Drawing.DrawCharacters(self.X + 1, self.Y + i - self.Scroll, file, colours.black, colours.white) + else + Drawing.DrawCharacters(self.X + 1, self.Y + i - self.Scroll, file, colours.grey, colours.white) + end + end + end + self.MaxScroll = #self.Files - 11 + if self.MaxScroll < 0 then + self.MaxScroll = 0 + end + end, + + Initialise = function(self, returnFunc) + local new = {} -- the new instance + setmetatable( new, {__index = self} ) + new.Width = 32 + new.Height = 17 + new.Return = returnFunc + new.X = math.ceil((Drawing.Screen.Width - new.Width) / 2) + new.Y = math.ceil((Drawing.Screen.Height - new.Height) / 2) + new.Title = 'Open Document' + new.Visible = true + new.CurrentDirectory = '/' + new.SelectedFile = nil + if OneOS and fs.exists('/Desktop/Documents/') then + new.CurrentDirectory = '/Desktop/Documents/' + end + new.OpenButton = Button:Initialise(new.Width - 6, new.Height - 1, nil, nil, colours.white, new, function(self, side, x, y, toggle) + if fs.exists(new.PathTextBox.TextInput.Value) and self.TextColour == colours.black and not fs.isDir(new.PathTextBox.TextInput.Value) then + returnFunc(new, true, TidyPath(new.PathTextBox.TextInput.Value)) + elseif new.SelectedFile and self.TextColour == colours.black and fs.isDir(new.CurrentDirectory .. new.SelectedFile) then + new:GoToDirectory(new.CurrentDirectory .. new.SelectedFile) + elseif new.SelectedFile and self.TextColour == colours.black then + returnFunc(new, true, TidyPath(new.CurrentDirectory .. '/' .. new.SelectedFile)) + end + end, 'Open', colours.black) + new.CancelButton = Button:Initialise(new.Width - 15, new.Height - 1, nil, nil, colours.white, new, function(self, side, x, y, toggle) + returnFunc(new, false) + end, 'Cancel', colours.black) + new.GoUpButton = Button:Initialise(2, new.Height - 1, nil, nil, colours.white, new, function(self, side, x, y, toggle) + local folderName = fs.getName(new.CurrentDirectory) + local parentDirectory = new.CurrentDirectory:sub(1, #new.CurrentDirectory-#folderName-1) + new:GoToDirectory(parentDirectory) + end, 'Go Up', colours.black) + new.PathTextBox = TextBox:Initialise(2, new.Height - 3, new.Width - 2, 1, new, new.CurrentDirectory, colours.white, colours.black) + new:GoToDirectory(new.CurrentDirectory) + return new + end, + + Show = function(self) + Current.Window = self + return self + end, + + Close = function(self) + Current.Input = nil + Current.Window = nil + self = nil + end, + + GoToDirectory = function(self, path) + path = TidyPath(path) + self.CurrentDirectory = path + self.Scroll = 0 + self.SelectedFile = nil + self.Typed = false + self.PathTextBox.TextInput.Value = path + local fs = fs + if OneOS then + fs = OneOS.FS + end + self.Files = fs.list(self.CurrentDirectory) + Draw() + end, + + Flash = function(self) + self.Visible = false + Draw() + sleep(0.15) + self.Visible = true + Draw() + sleep(0.15) + self.Visible = false + Draw() + sleep(0.15) + self.Visible = true + Draw() + end, + + Click = function(self, side, x, y) + local items = {self.OpenButton, self.CancelButton, self.PathTextBox, self.GoUpButton} + local found = false + for i, v in ipairs(items) do + if CheckClick(v, x, y) then + v:Click(side, x, y) + found = true + end + end + + if not found then + if y <= 12 then + local fs = fs + if OneOS then + fs = OneOS.FS + end + self.SelectedFile = fs.list(self.CurrentDirectory)[y-1] + self.PathTextBox.TextInput.Value = TidyPath(self.CurrentDirectory .. '/' .. self.SelectedFile) + Draw() + end + end + return true + end +} + +PrintDocumentWindow = { + X = 1, + Y = 1, + Width = 0, + Height = 0, + CursorPos = 1, + Visible = true, + Return = nil, + PrintButton = nil, + CopiesTextBox = nil, + Scroll = 0, + MaxScroll = 0, + PrinterSelectButton = nil, + Title = '', + Status = 0, --0 = neutral, 1 = good, -1 = error + StatusText = '', + SelectedPrinter = nil, + + AbsolutePosition = function(self) + return {X = self.X, Y = self.Y} + end, + + Draw = function(self) + if not self.Visible then + return + end + Drawing.DrawBlankArea(self.X + 1, self.Y+1, self.Width, self.Height, colours.grey) + Drawing.DrawBlankArea(self.X, self.Y, self.Width, 1, colours.lightGrey) + Drawing.DrawBlankArea(self.X, self.Y+1, self.Width, self.Height-1, colours.white) + Drawing.DrawCharactersCenter(self.X, self.Y, self.Width, 1, self.Title, colours.black, colours.lightGrey) + + self.PrinterSelectButton:Draw() + Drawing.DrawCharactersCenter(self.X, self.Y + self.PrinterSelectButton.Y - 2, self.Width, 1, 'Printer', colours.black, colours.white) + Drawing.DrawCharacters(self.X + self.Width - 3, self.Y + self.PrinterSelectButton.Y - 1, '\\/', colours.black, colours.lightGrey) + Drawing.DrawCharacters(self.X + 1, self.Y + self.CopiesTextBox.Y - 1, 'Copies', colours.black, colours.white) + local statusColour = colours.grey + if self.Status == -1 then + statusColour = colours.red + elseif self.Status == 1 then + statusColour = colours.green + end + Drawing.DrawCharacters(self.X + 1, self.Y + self.CopiesTextBox.Y + 1, self.StatusText, statusColour, colours.white) + + self.CopiesTextBox:Draw() + self.PrintButton:Draw() + self.CancelButton:Draw() + end, + + Initialise = function(self) + local new = {} -- the new instance + setmetatable( new, {__index = self} ) + new.Width = 32 + new.Height = 11 + new.Return = returnFunc + new.X = math.ceil((Drawing.Screen.Width - new.Width) / 2) + new.Y = math.ceil((Drawing.Screen.Height - new.Height) / 2) + new.Title = 'Print Document' + new.Visible = true + new.PrintButton = Button:Initialise(new.Width - 7, new.Height - 1, nil, nil, colours.lightGrey, new, function(self, side, x, y, toggle) + local doPrint = true + if new.SelectedPrinter == nil then + local p = Peripheral.GetPeripheral('printer') + if p then + new.SelectedPrinter = p.Side + new.PrinterSelectButton.Text = p.Fullname + else + new.StatusText = 'No Connected Printer' + new.Status = -1 + doPrint = false + end + end + if doPrint then + local printer = Printer:Initialise(new.SelectedPrinter) + local err = printer:PrintLines(Current.Document.Lines, Current.Document.Title, tonumber(new.CopiesTextBox.TextInput.Value)) + if not err then + new.StatusText = 'Document Printed!' + new.Status = 1 + closeWindowTimer = os.startTimer(1) + else + new.StatusText = err + new.Status = -1 + end + end + end, 'Print', colours.black) + new.CancelButton = Button:Initialise(new.Width - 15, new.Height - 1, nil, nil, colours.lightGrey, new, function(self, side, x, y, toggle) + new:Close() + Draw() + end, 'Close', colours.black) + new.PrinterSelectButton = Button:Initialise(2, 4, new.Width - 2, nil, colours.lightGrey, new, function(self, side, x, y, toggle) + local printers = { + { + Title = "Automatic", + Click = function() + new.SelectedPrinter = nil + new.PrinterSelectButton.Text = 'Automatic' + end + }, + { + Separator = true + } + } + for i, p in ipairs(Peripheral.GetPeripherals('printer')) do + table.insert(printers, { + Title = p.Fullname, + Click = function(self) + new.SelectedPrinter = p.Side + new.PrinterSelectButton.Text = p.Fullname + end + }) + end + Current.Menu = Menu:New(x, y+4, printers, self, true) + end, 'Automatic', colours.black) + new.CopiesTextBox = TextBox:Initialise(9, 6, 4, 1, new, 1, colours.lightGrey, colours.black, nil, true) + Current.TextInput = new.CopiesTextBox.TextInput + new.StatusText = 'Waiting...' + new.Status = 0 + return new + end, + + Show = function(self) + Current.Window = self + return self + end, + + Close = function(self) + Current.Input = nil + Current.Window = nil + self = nil + end, + + Flash = function(self) + self.Visible = false + Draw() + sleep(0.15) + self.Visible = true + Draw() + sleep(0.15) + self.Visible = false + Draw() + sleep(0.15) + self.Visible = true + Draw() + end, + + Click = function(self, side, x, y) + local items = {self.PrintButton, self.CancelButton, self.CopiesTextBox, self.PrinterSelectButton} + for i, v in ipairs(items) do + if CheckClick(v, x, y) then + v:Click(side, x, y) + end + end + return true + end +} + +SaveDocumentWindow = { + X = 1, + Y = 1, + Width = 0, + Height = 0, + CursorPos = 1, + Visible = true, + Return = nil, + SaveButton = nil, + PathTextBox = nil, + CurrentDirectory = '/', + Scroll = 0, + MaxScroll = 0, + ScrollBar = nil, + GoUpButton = nil, + Files = {}, + Typed = false, + + AbsolutePosition = function(self) + return {X = self.X, Y = self.Y} + end, + + Draw = function(self) + if not self.Visible then + return + end + Drawing.DrawBlankArea(self.X + 1, self.Y+1, self.Width, self.Height, colours.grey) + Drawing.DrawBlankArea(self.X, self.Y, self.Width, 3, colours.lightGrey) + Drawing.DrawBlankArea(self.X, self.Y+1, self.Width, self.Height-6, colours.white) + Drawing.DrawCharactersCenter(self.X, self.Y, self.Width, 1, self.Title, colours.black, colours.lightGrey) + Drawing.DrawBlankArea(self.X, self.Y + self.Height - 5, self.Width, 5, colours.lightGrey) + Drawing.DrawCharacters(self.X + 1, self.Y + self.Height - 5, self.CurrentDirectory, colours.grey, colours.lightGrey) + self:DrawFiles() + + if (self.PathTextBox.TextInput.Value) then + self.SaveButton.TextColour = colours.black + else + self.SaveButton.TextColour = colours.lightGrey + end + + self.PathTextBox:Draw() + self.SaveButton:Draw() + self.CancelButton:Draw() + self.GoUpButton:Draw() + end, + + DrawFiles = function(self) + for i, file in ipairs(self.Files) do + if i > self.Scroll and i - self.Scroll <= 10 then + if file == self.SelectedFile then + Drawing.DrawCharacters(self.X + 1, self.Y + i - self.Scroll, file, colours.white, colours.lightBlue) + elseif fs.isDir(self.CurrentDirectory .. file) then + Drawing.DrawCharacters(self.X + 1, self.Y + i - self.Scroll, file, colours.black, colours.white) + else + Drawing.DrawCharacters(self.X + 1, self.Y + i - self.Scroll, file, colours.lightGrey, colours.white) + end + end + end + self.MaxScroll = #self.Files - 11 + if self.MaxScroll < 0 then + self.MaxScroll = 0 + end + end, + + Initialise = function(self, returnFunc) + local new = {} -- the new instance + setmetatable( new, {__index = self} ) + new.Width = 32 + new.Height = 16 + new.Return = returnFunc + new.X = math.ceil((Drawing.Screen.Width - new.Width) / 2) + new.Y = math.ceil((Drawing.Screen.Height - new.Height) / 2) + new.Title = 'Save Document' + new.Visible = true + new.CurrentDirectory = '/' + if OneOS and fs.exists('/Desktop/Documents/') then + new.CurrentDirectory = '/Desktop/Documents/' + end + new.SaveButton = Button:Initialise(new.Width - 6, new.Height - 1, nil, nil, colours.white, new, function(self, side, x, y, toggle) + if self.TextColour == colours.black and not fs.isDir(new.CurrentDirectory ..'/' .. new.PathTextBox.TextInput.Value) then + returnFunc(new, true, TidyPath(new.CurrentDirectory ..'/' .. new.PathTextBox.TextInput.Value)) + elseif new.SelectedFile and self.TextColour == colours.black and fs.isDir(new.CurrentDirectory .. new.SelectedFile) then + new:GoToDirectory(new.CurrentDirectory .. new.SelectedFile) + end + end, 'Save', colours.black) + new.CancelButton = Button:Initialise(new.Width - 15, new.Height - 1, nil, nil, colours.white, new, function(self, side, x, y, toggle) + returnFunc(new, false) + end, 'Cancel', colours.black) + new.GoUpButton = Button:Initialise(2, new.Height - 1, nil, nil, colours.white, new, function(self, side, x, y, toggle) + local folderName = fs.getName(new.CurrentDirectory) + local parentDirectory = new.CurrentDirectory:sub(1, #new.CurrentDirectory-#folderName-1) + new:GoToDirectory(parentDirectory) + end, 'Go Up', colours.black) + new.PathTextBox = TextBox:Initialise(2, new.Height - 3, new.Width - 2, 1, new, '', colours.white, colours.black, function(key) + if key == keys.enter then + new.SaveButton:Click() + end + end) + new.PathTextBox.Placeholder = 'Document Name' + Current.TextInput = new.PathTextBox.TextInput + new:GoToDirectory(new.CurrentDirectory) + return new + end, + + Show = function(self) + Current.Window = self + return self + end, + + Close = function(self) + Current.Input = nil + Current.Window = nil + self = nil + end, + + GoToDirectory = function(self, path) + path = TidyPath(path) + self.CurrentDirectory = path + self.Scroll = 0 + self.Typed = false + local fs = fs + if OneOS then + fs = OneOS.FS + end + self.Files = fs.list(self.CurrentDirectory) + Draw() + end, + + Flash = function(self) + self.Visible = false + Draw() + sleep(0.15) + self.Visible = true + Draw() + sleep(0.15) + self.Visible = false + Draw() + sleep(0.15) + self.Visible = true + Draw() + end, + + Click = function(self, side, x, y) + local items = {self.SaveButton, self.CancelButton, self.PathTextBox, self.GoUpButton} + local found = false + for i, v in ipairs(items) do + if CheckClick(v, x, y) then + v:Click(side, x, y) + found = true + end + end + + if not found then + if y <= 11 then + local files = fs.list(self.CurrentDirectory) + if files[y-1] then + self:GoToDirectory(self.CurrentDirectory..files[y-1]) + Draw() + end + end + end + return true + end +} + +local WrapText = function(text, maxWidth) + local lines = {''} + for word, space in text:gmatch('(%S+)(%s*)') do + local temp = lines[#lines] .. word .. space:gsub('\n','') + if #temp > maxWidth then + table.insert(lines, '') + end + if space:find('\n') then + lines[#lines] = lines[#lines] .. word + + space = space:gsub('\n', function() + table.insert(lines, '') + return '' + end) + else + lines[#lines] = lines[#lines] .. word .. space + end + end + return lines +end + +ButtonDialougeWindow = { + X = 1, + Y = 1, + Width = 0, + Height = 0, + CursorPos = 1, + Visible = true, + CancelButton = nil, + OkButton = nil, + Lines = {}, + + AbsolutePosition = function(self) + return {X = self.X, Y = self.Y} + end, + + Draw = function(self) + if not self.Visible then + return + end + Drawing.DrawBlankArea(self.X + 1, self.Y+1, self.Width, self.Height, colours.grey) + Drawing.DrawBlankArea(self.X, self.Y, self.Width, 1, colours.lightGrey) + Drawing.DrawBlankArea(self.X, self.Y+1, self.Width, self.Height-1, colours.white) + Drawing.DrawCharactersCenter(self.X, self.Y, self.Width, 1, self.Title, colours.black, colours.lightGrey) + + for i, text in ipairs(self.Lines) do + Drawing.DrawCharacters(self.X + 1, self.Y + 1 + i, text, colours.black, colours.white) + end + + self.OkButton:Draw() + if self.CancelButton then + self.CancelButton:Draw() + end + end, + + Initialise = function(self, title, message, okText, cancelText, returnFunc) + local new = {} -- the new instance + setmetatable( new, {__index = self} ) + new.Width = 28 + new.Lines = WrapText(message, new.Width - 2) + new.Height = 5 + #new.Lines + new.Return = returnFunc + new.X = math.ceil((Drawing.Screen.Width - new.Width) / 2) + new.Y = math.ceil((Drawing.Screen.Height - new.Height) / 2) + new.Title = title + new.Visible = true + new.Visible = true + new.OkButton = Button:Initialise(new.Width - #okText - 2, new.Height - 1, nil, 1, nil, new, function() + returnFunc(new, true) + end, okText) + if cancelText then + new.CancelButton = Button:Initialise(new.Width - #okText - 2 - 1 - #cancelText - 2, new.Height - 1, nil, 1, nil, new, function() + returnFunc(new, false) + end, cancelText) + end + + return new + end, + + Show = function(self) + Current.Window = self + return self + end, + + Close = function(self) + Current.Window = nil + self = nil + end, + + Flash = function(self) + self.Visible = false + Draw() + sleep(0.15) + self.Visible = true + Draw() + sleep(0.15) + self.Visible = false + Draw() + sleep(0.15) + self.Visible = true + Draw() + end, + + Click = function(self, side, x, y) + local items = {self.OkButton, self.CancelButton} + local found = false + for i, v in ipairs(items) do + if CheckClick(v, x, y) then + v:Click(side, x, y) + found = true + end + end + return true + end +} + +TextDialougeWindow = { + X = 1, + Y = 1, + Width = 0, + Height = 0, + CursorPos = 1, + Visible = true, + CancelButton = nil, + OkButton = nil, + Lines = {}, + TextInput = nil, + + AbsolutePosition = function(self) + return {X = self.X, Y = self.Y} + end, + + Draw = function(self) + if not self.Visible then + return + end + Drawing.DrawBlankArea(self.X + 1, self.Y+1, self.Width, self.Height, colours.grey) + Drawing.DrawBlankArea(self.X, self.Y, self.Width, 1, colours.lightGrey) + Drawing.DrawBlankArea(self.X, self.Y+1, self.Width, self.Height-1, colours.white) + Drawing.DrawCharactersCenter(self.X, self.Y, self.Width, 1, self.Title, colours.black, colours.lightGrey) + + for i, text in ipairs(self.Lines) do + Drawing.DrawCharacters(self.X + 1, self.Y + 1 + i, text, colours.black, colours.white) + end + + + Drawing.DrawBlankArea(self.X + 1, self.Y + self.Height - 4, self.Width - 2, 1, colours.lightGrey) + Drawing.DrawCharacters(self.X + 2, self.Y + self.Height - 4, self.TextInput.Value, colours.black, colours.lightGrey) + Current.CursorPos = {self.X + 2 + self.TextInput.CursorPos, self.Y + self.Height - 4} + Current.CursorColour = colours.black + + self.OkButton:Draw() + if self.CancelButton then + self.CancelButton:Draw() + end + end, + + Initialise = function(self, title, message, okText, cancelText, returnFunc, numerical) + local new = {} -- the new instance + setmetatable( new, {__index = self} ) + new.Width = 28 + new.Lines = WrapText(message, new.Width - 2) + new.Height = 7 + #new.Lines + new.Return = returnFunc + new.X = math.ceil((Drawing.Screen.Width - new.Width) / 2) + new.Y = math.ceil((Drawing.Screen.Height - new.Height) / 2) + new.Title = title + new.Visible = true + new.Visible = true + new.OkButton = Button:Initialise(new.Width - #okText - 2, new.Height - 1, nil, 1, nil, new, function() + if #new.TextInput.Value > 0 then + returnFunc(new, true, new.TextInput.Value) + end + end, okText) + if cancelText then + new.CancelButton = Button:Initialise(new.Width - #okText - 2 - 1 - #cancelText - 2, new.Height - 1, nil, 1, nil, new, function() + returnFunc(new, false) + end, cancelText) + end + new.TextInput = TextInput:Initialise('', function(enter) + if enter then + new.OkButton:Click() + end + Draw() + end, numerical) + + Current.Input = new.TextInput + + return new + end, + + Show = function(self) + Current.Window = self + return self + end, + + Close = function(self) + Current.Window = nil + Current.Input = nil + self = nil + end, + + Flash = function(self) + self.Visible = false + Draw() + sleep(0.15) + self.Visible = true + Draw() + sleep(0.15) + self.Visible = false + Draw() + sleep(0.15) + self.Visible = true + Draw() + end, + + Click = function(self, side, x, y) + local items = {self.OkButton, self.CancelButton} + local found = false + for i, v in ipairs(items) do + if CheckClick(v, x, y) then + v:Click(side, x, y) + found = true + end + end + return true + end +} + +function PrintCentered(text, y) + local w, h = term.getSize() + x = math.ceil(math.ceil((w / 2) - (#text / 2)), 0)+1 + term.setCursorPos(x, y) + print(text) +end + +function DoVanillaClose() + term.setBackgroundColour(colours.black) + term.setTextColour(colours.white) + term.clear() + term.setCursorPos(1, 1) + PrintCentered("Thanks for using Ink!", (Drawing.Screen.Height/2)-1) + term.setTextColour(colours.lightGrey) + PrintCentered("Word Proccessor for ComputerCraft", (Drawing.Screen.Height/2)) + term.setTextColour(colours.white) + PrintCentered("(c) oeed 2014", (Drawing.Screen.Height/2)+3) + term.setCursorPos(1, Drawing.Screen.Height) + error('', 0) +end + +function Close() + if isQuitting or not Current.Document or not Current.Modified then + if not OneOS then + DoVanillaClose() + end + return true + else + local _w = ButtonDialougeWindow:Initialise('Quit Ink?', 'You have unsaved changes, do you want to quit anyway?', 'Quit', 'Cancel', function(window, success) + if success then + if OneOS then + OneOS.Close(true) + else + DoVanillaClose() + end + end + window:Close() + Draw() + end):Show() + --it's hacky but it works + os.queueEvent('mouse_click', 1, _w.X, _w.Y) + return false + end +end + +if OneOS then + OneOS.CanClose = function() + return Close() + end +end + +Initialise()
\ No newline at end of file diff --git a/Programs/Krist Wallet.bup/Contents/Info.meta b/Programs/Krist Wallet.bup/Contents/Info.meta new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/Programs/Krist Wallet.bup/Contents/Info.meta @@ -0,0 +1 @@ + diff --git a/Programs/Krist Wallet.bup/Contents/Resources/resources_here.txt b/Programs/Krist Wallet.bup/Contents/Resources/resources_here.txt new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/Programs/Krist Wallet.bup/Contents/Resources/resources_here.txt @@ -0,0 +1 @@ + diff --git a/Programs/Krist Wallet.bup/Contents/bits-UI/kristwallet.lua b/Programs/Krist Wallet.bup/Contents/bits-UI/kristwallet.lua new file mode 100644 index 0000000..cafd59a --- /dev/null +++ b/Programs/Krist Wallet.bup/Contents/bits-UI/kristwallet.lua @@ -0,0 +1,1750 @@ +--[[----------------------------------------------- +| KristWallet by 3d6 | +--------------------------------------------------- +| This is the reference wallet for Krist. | +| It is the basic definition of a functional | +| Krist program, although it is not as old as the | +| network (we used to just use raw API calls). | +--------------------------------------------------- + /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ +/ \/ \/ \/ \/ \/ \/ \/ \/ \/ \/ \/ \/ +--------------------------------------------------- +| Do whatever you want with this, but if you make | +| it interact with a currency or network other | +| than Krist, please give me credit. Thanks <3 | +--------------------------------------------------- +| This wallet will NEVER save passwords anywhere. |]]local +-----------------------------------------------]]-- + version = 16 +local latest = 0 +local balance = 0 +local balance2 = 0 +local balance3 = 0 +local MOD = 2^32 +local MODM = MOD-1 +local gui = 0 +local page = 0 +local lastpage = 0 +local scroll = 0 +local masterkey = "" +local doublekey = "" +local address = "" +local addressv1 = "" +local addressdv = "" +local addresslv = "" +local subject = "" +local name = "" +local subbal = 0 +local subtxs = "" +local stdate = {} +local stpeer = {} +local stval = {} +local blkpeer = {} +local pagespace = "" +local maxspace = "" +local ar = 0 +local amt = 0 +local availability = 0 +local wallet, hud, update, settle, log, readconfig, checkdir, openwallet, makev2address + +local function split(inputstr, sep) + if sep == nil then + sep = "%s" + end + local t={} ; i=1 + for str in string.gmatch(inputstr, "([^"..sep.."]+)") do + t[i] = str + i = i + 1 + end + return t +end + +local function readURL(url) + local resp = http.get(url) + if not resp then + log("Could not reach "..url) + error("Error connecting to server") + panic() + end + local content = resp.readAll():gsub("\n+$", "") + resp.close() + return content +end + +local function boot() + for i=1,2 do checkdir() end + print("Starting KristWallet v"..tostring(version)) + log("Started KristWallet v"..tostring(version)) + update() + if readconfig("enabled") and latest <= version then + settle() + openwallet() + while page ~= 0 do + wallet() + end + term.setBackgroundColor(32768) + term.setTextColor(16) + term.clear() + term.setCursorPos(1,1) + log("KristWallet closed safely") + else + if not readconfig("enabled") then print("KristWallet is disabled on this computer.") log("Disabled, shutting down") end + end + if readconfig("rebootonexit") then + log("Rebooted computer") + os.reboot() + end +end +function update() + latest = tonumber(readURL(readconfig("versionserver"))) + if latest > version then + print("An update is available!") + log("Discovered update") + if readconfig("autoupdate") and not bench then + local me = fs.open(fs.getName(shell.getRunningProgram()),"w") + local nextversion = readURL(readconfig("updateserver")) + print("Installed update. Run this program again to start v"..latest..".") + me.write(nextversion) + me.close() + log("Installed update") + else + log("Ignored update") + latest = -2 + end + else + log("No updates found") + end +end +function log(text) + local logfile = fs.open("kst/log_wallet","a") + logfile.writeLine(tostring(os.day()).."-"..tostring(os.time()).."/"..text) + logfile.close() +end +local function checkfile(path,default) + if not fs.exists("kst/"..path) or path == "syncnode" then + local file = fs.open("kst/"..path,"w") + file.writeLine(default) + file.close() + log("Created file "..path) + return false + else + return true + end +end +function readconfig(path) + if fs.exists("kst/"..path) then + local file = fs.open("kst/"..path,"r") + local context = file.readAll():gsub("\n+$", "") + file.close() + if context == "true" then return true end + if context == "false" then return false end + return context + else + print("An unknown error happened") + end +end +function settle() + if term.isColor() then gui = 1 end + if term.isColor() and pocket then gui = 2 end +end +local function drawKrist() + local posx, posy = term.getCursorPos() + term.setBackgroundColor(1) + term.setTextColor(32) + term.write("/") + term.setBackgroundColor(32) + term.setTextColor(8192) + term.write("\\") + term.setCursorPos(posx,posy+1) + term.setBackgroundColor(32) + term.setTextColor(8192) + term.write("\\") + term.setBackgroundColor(8192) + term.setTextColor(32) + term.write("/") + term.setCursorPos(posx+2,posy) +end +local function memoize(f) + local mt = {} + local t = setmetatable({}, mt) + function mt:__index(k) + local v = f(k) + t[k] = v + return v + end + return t +end +local function make_bitop_uncached(t, m) + local function bitop(a, b) + local res,p = 0,1 + while a ~= 0 and b ~= 0 do + local am, bm = a % m, b % m + res = res + t[am][bm] * p + a = (a - am) / m + b = (b - bm) / m + p = p*m + end + res = res + (a + b) * p + return res + end + return bitop +end +local function make_bitop(t) + local op1 = make_bitop_uncached(t,2^1) + local op2 = memoize(function(a) return memoize(function(b) return op1(a, b) end) end) + return make_bitop_uncached(op2, 2 ^ (t.n or 1)) +end +local bxor1 = make_bitop({[0] = {[0] = 0,[1] = 1}, [1] = {[0] = 1, [1] = 0}, n = 4}) +local function bxor(a, b, c, ...) + local z = nil + if b then + a = a % MOD + b = b % MOD + z = bxor1(a, b) + if c then z = bxor(z, c, ...) end + return z + elseif a then return a % MOD + else return 0 end +end +local function band(a, b, c, ...) + local z + if b then + a = a % MOD + b = b % MOD + z = ((a + b) - bxor1(a,b)) / 2 + if c then z = bit32_band(z, c, ...) end + return z + elseif a then return a % MOD + else return MODM end +end +local function bnot(x) return (-1 - x) % MOD end +local function rshift1(a, disp) + if disp < 0 then return lshift(a,-disp) end + return math.floor(a % 2 ^ 32 / 2 ^ disp) +end +local function rshift(x, disp) + if disp > 31 or disp < -31 then return 0 end + return rshift1(x % MOD, disp) +end +local function lshift(a, disp) + if disp < 0 then return rshift(a,-disp) end + return (a * 2 ^ disp) % 2 ^ 32 +end +local function rrotate(x, disp) + x = x % MOD + disp = disp % 32 + local low = band(x, 2 ^ disp - 1) + return rshift(x, disp) + lshift(low, 32 - disp) +end +local k = { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, + 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, + 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, + 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, + 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, + 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, + 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, + 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, + 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, +} +local function str2hexa(s) + return (string.gsub(s, ".", function(c) return string.format("%02x", string.byte(c)) end)) +end +local function num2s(l, n) + local s = "" + for i = 1, n do + local rem = l % 256 + s = string.char(rem) .. s + l = (l - rem) / 256 + end + return s +end +local function s232num(s, i) + local n = 0 + for i = i, i + 3 do n = n*256 + string.byte(s, i) end + return n +end +local function preproc(msg, len) + local extra = 64 - ((len + 9) % 64) + len = num2s(8 * len, 8) + msg = msg .. "\128" .. string.rep("\0", extra) .. len + assert(#msg % 64 == 0) + return msg +end +local function initH256(H) + H[1] = 0x6a09e667 + H[2] = 0xbb67ae85 + H[3] = 0x3c6ef372 + H[4] = 0xa54ff53a + H[5] = 0x510e527f + H[6] = 0x9b05688c + H[7] = 0x1f83d9ab + H[8] = 0x5be0cd19 + return H +end +local function digestblock(msg, i, H) + local w = {} + for j = 1, 16 do w[j] = s232num(msg, i + (j - 1)*4) end + for j = 17, 64 do + local v = w[j - 15] + local s0 = bxor(rrotate(v, 7), rrotate(v, 18), rshift(v, 3)) + v = w[j - 2] + w[j] = w[j - 16] + s0 + w[j - 7] + bxor(rrotate(v, 17), rrotate(v, 19), rshift(v, 10)) + end + + local a, b, c, d, e, f, g, h = H[1], H[2], H[3], H[4], H[5], H[6], H[7], H[8] + for i = 1, 64 do + local s0 = bxor(rrotate(a, 2), rrotate(a, 13), rrotate(a, 22)) + local maj = bxor(band(a, b), band(a, c), band(b, c)) + local t2 = s0 + maj + local s1 = bxor(rrotate(e, 6), rrotate(e, 11), rrotate(e, 25)) + local ch = bxor (band(e, f), band(bnot(e), g)) + local t1 = h + s1 + ch + k[i] + w[i] + h, g, f, e, d, c, b, a = g, f, e, d + t1, c, b, a, t1 + t2 + end + + H[1] = band(H[1] + a) + H[2] = band(H[2] + b) + H[3] = band(H[3] + c) + H[4] = band(H[4] + d) + H[5] = band(H[5] + e) + H[6] = band(H[6] + f) + H[7] = band(H[7] + g) + H[8] = band(H[8] + h) +end +local function sha256(msg) + msg = preproc(msg, #msg) + local H = initH256({}) + for i = 1, #msg, 64 do digestblock(msg, i, H) end + return str2hexa(num2s(H[1], 4) .. num2s(H[2], 4) .. num2s(H[3], 4) .. num2s(H[4], 4) .. + num2s(H[5], 4) .. num2s(H[6], 4) .. num2s(H[7], 4) .. num2s(H[8], 4)) +end +local function panic() + page = 0 + log("Panicking! Shutting down KristWallet.") +end +local function makeaddressbyte(j) + if j <= 6 then return "0" + elseif j <= 13 then return "1" + elseif j <= 20 then return "2" + elseif j <= 27 then return "3" + elseif j <= 34 then return "4" + elseif j <= 41 then return "5" + elseif j <= 48 then return "6" + elseif j <= 55 then return "7" + elseif j <= 62 then return "8" + elseif j <= 69 then return "9" + elseif j <= 76 then return "a" + elseif j <= 83 then return "b" + elseif j <= 90 then return "c" + elseif j <= 97 then return "d" + elseif j <= 104 then return "e" + elseif j <= 111 then return "f" + elseif j <= 118 then return "g" + elseif j <= 125 then return "h" + elseif j <= 132 then return "i" + elseif j <= 139 then return "j" + elseif j <= 146 then return "k" + elseif j <= 153 then return "l" + elseif j <= 160 then return "m" + elseif j <= 167 then return "n" + elseif j <= 174 then return "o" + elseif j <= 181 then return "p" + elseif j <= 188 then return "q" + elseif j <= 195 then return "r" + elseif j <= 202 then return "s" + elseif j <= 209 then return "t" + elseif j <= 216 then return "u" + elseif j <= 223 then return "v" + elseif j <= 230 then return "w" + elseif j <= 237 then return "x" + elseif j <= 244 then return "y" + elseif j <= 251 then return "z" + else return "e" + end +end +function checkdir() + if fs.isDir("kst") then + math.randomseed(os.time()) + checkfile("log_wallet","-----KRISTWALLET LOG FILE-----") + checkfile("enabled","true") --Disabling this just makes KristWallet refuse to start. + checkfile("sweepv1","true") + checkfile("appendhashes","true") --Disabling this makes it possible to use KristWallet with extremely old addresses. + checkfile("autoupdate","true") + checkfile("whitelisted","false") + checkfile("rebootonexit","false") + checkfile("autologin","false") + checkfile("keyAL",sha256("")) + checkfile("keyLV",sha256(math.random(1000000)..os.time())) --This is where the local vault's krist is stored. DO NOT DESTROY! + checkfile("versionserver","https://raw.githubusercontent.com/BTCTaras/kristwallet/master/staticapi/version") + checkfile("updateserver","https://raw.githubusercontent.com/BTCTaras/kristwallet/master/kristwallet") + checkfile("syncnode","http://krist.ceriat.net/") + checkfile("whitelist","") + checkfile("blacklist","") + else + fs.makeDir("kst") + end +end +function openwallet() + term.setBackgroundColor(8) + term.clear() + local krists = 0 + repeat + term.setCursorPos(3+(3*krists),3) + drawKrist() + krists = krists + 1 + until krists == 16 + krists = 0 + repeat + term.setCursorPos(3+(3*krists),16) + drawKrist() + krists = krists + 1 + until krists == 16 + term.setBackgroundColor(8) + term.setTextColor(32768) + term.setCursorPos(6,6) + term.write("Password:") + term.setCursorPos(6,8) + -----|---+---------+---------+---------+-----|---+- + term.write("Please enter your secret password to") + term.setCursorPos(6,9) + term.write("use Krist. If this is your first time") + term.setCursorPos(6,10) + term.write("using Krist, type your desired password.") + term.setCursorPos(6,11) + term.write("You will be able to access your Krist") + term.setCursorPos(6,12) + term.write("on any computer on any server as long") + term.setCursorPos(6,13) + term.write("as you type in the same password! It will") + term.setCursorPos(6,14) + term.write("not be saved or shared with anyone.") + term.setCursorPos(16,6) + local password = "" + if readconfig("autologin") then + password = readconfig("keyAL") + else + password = read("*") + if password == "" then term.setCursorPos(16,6) password = read("*") end + if readconfig("appendhashes") then password = sha256("KRISTWALLET"..password) end + end + term.clear() + term.setCursorPos(1,1) + page = 1+gui*(10*(gui-1)) + if readconfig("appendhashes") then masterkey = password.."-000" else masterkey = password end + log("Read password") + addressv1 = string.sub(sha256(masterkey),0,10) + log("Derived address: "..addressv1) + address = makev2address(masterkey) + log("Derived address: "..address) + balance = tonumber(readURL(readconfig("syncnode").."?getbalance="..addressv1)) + if balance > 0 and readconfig("sweepv1") then local transaction = readURL(readconfig("syncnode").."?pushtx&q="..address.."&pkey="..masterkey.."&amt="..balance); log("Swept hex address") end + balance = tonumber(readURL(readconfig("syncnode").."?getbalance="..address)) + if balance >= 100000 then log("Woah! There's a small fortune here!") elseif balance > 0 then log("There is some krist here!") end + if readconfig("whitelisted") then + local whitelist = readconfig("whitelist") + if string.find(whitelist, address) == nil then + log(address.." is not on the whitelist!") + print("Sorry, this wallet is not on the whitelist for this computer!") + page = 0 + os.sleep(3) + end + else + local blacklist = readconfig("blacklist") + if string.find(blacklist, addressv1) ~= nil then + log(addressv1.." is on the blacklist!") + print("Your wallet is blocked from this computer!") + page = 0 + os.sleep(3) + elseif string.find(blacklist, address) ~= nil then + log(address.." is on the blacklist!") + print("Your wallet is blocked from this computer!") + page = 0 + os.sleep(3) + end + end + addresslv = makev2address(readconfig("keyLV")) + log("Loaded local vault") + os.sleep() + http.post(readconfig("syncnode") .. "/login", "privatekey=" .. textutils.urlEncode(masterkey) .. "&v=2") + log("Sent pkey hash to auth server") +end +function makev2address(key) + local protein = {} + local stick = sha256(sha256(key)) + local n = 0 + local link = 0 + local v2 = "k" + repeat + if n < 9 then protein[n] = string.sub(stick,0,2) + stick = sha256(sha256(stick)) end + n = n + 1 + until n == 9 + n = 0 + repeat + link = tonumber(string.sub(stick,1+(2*n),2+(2*n)),16) % 9 + if string.len(protein[link]) ~= 0 then + v2 = v2 .. makeaddressbyte(tonumber(protein[link],16)) + protein[link] = '' + n = n + 1 + else + stick = sha256(stick) + end + until n == 9 + return v2 +end +local function postgraphic(px,py,id) + term.setCursorPos(px,py) + if id == 0 then drawKrist() + elseif id == 1 then + --Mined Krist + term.setCursorPos(px+1,py) + term.setBackgroundColor(256) + term.setTextColor(128) + term.write("/T\\") + term.setCursorPos(px,py+1) + term.write("/") + term.setCursorPos(px+2,py+1) + term.write("|") + term.setCursorPos(px+4,py+1) + term.write("\\") + term.setCursorPos(px+2,py+2) + term.write("|") + term.setCursorPos(px+2,py+3) + term.write("|") + term.setCursorPos(px+4,py+2) + drawKrist() + elseif id == 2 then + --Sent Krist + term.setCursorPos(px,py+2) + term.setBackgroundColor(256) + term.setTextColor(16384) + term.write(" ") + term.setCursorPos(px+1,py+3) + term.write(" ") + term.setCursorPos(px+5,py+2) + term.write(" ") + term.setBackgroundColor(1) + term.setCursorPos(px+2,py) + term.write("/\\") + term.setCursorPos(px+2,py+1) + term.write("||") + elseif id == 3 then + --Received Krist + term.setCursorPos(px,py+2) + term.setBackgroundColor(256) + term.setTextColor(8192) + term.write(" ") + term.setCursorPos(px+1,py+3) + term.write(" ") + term.setCursorPos(px+5,py+2) + term.write(" ") + term.setBackgroundColor(1) + term.setCursorPos(px+2,py) + term.write("||") + term.setCursorPos(px+2,py+1) + term.write("\\/") + elseif id == 4 then + --Sent to yourself + term.setCursorPos(px,py+2) + term.setBackgroundColor(256) + term.setTextColor(16) + term.write(" ") + term.setCursorPos(px+1,py+3) + term.write(" ") + term.setCursorPos(px+5,py+2) + term.write(" ") + term.setBackgroundColor(1) + term.setCursorPos(px+1,py) + term.write("/\\||") + term.setCursorPos(px+1,py+1) + term.write("||\\/") + elseif id == 5 then + --Swept from v1 address + term.setCursorPos(px+1,py) + term.setBackgroundColor(256) + term.setTextColor(128) + term.write(" v1 ") + term.setCursorPos(px+2,py+1) + term.setBackgroundColor(1) + term.setTextColor(2048) + term.write("||") + term.setCursorPos(px+2,py+2) + term.write("\\/") + term.setCursorPos(px+1,py+3) + term.setBackgroundColor(16) + term.setTextColor(32768) + term.write(" v2 ") + elseif id == 6 then + --Name registered + term.setBackgroundColor(32) + term.setTextColor(8192) + term.setCursorPos(px+4,py) + term.write("/") + term.setCursorPos(px+1,py+1) + term.write("\\") + term.setCursorPos(px+3,py+1) + term.write("/") + term.setCursorPos(px+2,py+2) + term.write("V") + term.setCursorPos(px+1,py+3) + term.setBackgroundColor(16384) + term.setTextColor(4) + term.write(".kst") + elseif id == 7 then + --Name operation + term.setBackgroundColor(8) + term.setTextColor(512) + term.setCursorPos(px+1,py) + term.write(" a ") + term.setBackgroundColor(1) + term.write("\\") + term.setBackgroundColor(8) + term.setCursorPos(px+1,py+1) + term.write("====") + term.setCursorPos(px+1,py+2) + term.write("====") + term.setCursorPos(px+1,py+3) + term.setBackgroundColor(16384) + term.setTextColor(4) + term.write(".kst") + elseif id == 8 then + --Name sent + term.setCursorPos(px+1,py+3) + term.setBackgroundColor(16384) + term.setTextColor(4) + term.write(".kst") + term.setTextColor(16384) + term.setBackgroundColor(1) + term.setCursorPos(px+2,py) + term.write("/\\") + term.setCursorPos(px+2,py+1) + term.write("||") + elseif id == 9 then + --Name received + term.setCursorPos(px+1,py+3) + term.setBackgroundColor(16384) + term.setTextColor(4) + term.write(".kst") + term.setTextColor(8192) + term.setBackgroundColor(1) + term.setCursorPos(px+1,py) + term.write("||") + term.setCursorPos(px+1,py+1) + term.write("\\/") + term.setTextColor(16384) + term.setCursorPos(px+3,py) + term.write("/\\") + term.setCursorPos(px+3,py+1) + term.write("||") + end +end +function wallet() + hud() + local pagebefore = page + local event, button, xPos, yPos = os.pullEvent("mouse_click") + if gui == 1 and xPos >= 3 and xPos <= 14 then + if yPos == 5 then + page = 1 + balance = tonumber(readURL(readconfig("syncnode").."?getbalance="..address)) + end + if yPos == 7 then + page = 2 + subject = address + scroll = 0 + end + if yPos == 9 then + page = 3 + balance = tonumber(readURL(readconfig("syncnode").."?getbalance="..address)) + end + if yPos == 11 then + page = 8 + end + if yPos == 13 then + page = 4 + end + if yPos == 15 then + page = 15 + end + if yPos == 17 then + page = 0 + end + elseif gui == 2 then + if yPos == 2 and xPos >= 19 and xPos <= 24 then + page = 0 + end + end + local lexm = http.get(readconfig("syncnode").."?listnames="..address) + local lem = false + local lexmm + if lexm.readAll then + lem = true + lexmm = lexm.readAll():gsub("\n+$", "") + end + + if page == 1 then + balance = tonumber(readURL(readconfig("syncnode").."?getbalance="..address)) + if (yPos-7)%5 == 0 and yPos >= 7 and xPos >= 26 and xPos <= 35 then + subject = string.sub(readURL(readconfig("syncnode").."?listtx="..address.."&overview"),13+(31*((yPos-7)/5)),22+(31*((yPos-7)/5))) + if string.len(subject) == 10 and subject ~= "N/A(Mined)" and subject ~= "N/A(Names)" then + page = 2 + end + end + elseif page == 2 then + if yPos > 2 and yPos <= 2+ar-(16*(scroll)) and xPos >= 31 and xPos < 41 then + if stpeer[(yPos-2)+(16*(scroll))] == "N/A(Mined)" then + --possibly link to a block later? + elseif stpeer[(yPos-2)+(16*(scroll))] == "N/A(Names)" then + --possibly link to a name later?? + else + subject = stpeer[(yPos-2)+(16*(scroll))] + scroll = 0 + end + end + if yPos == 19 and xPos >= 32 and xPos <= 36 then + scroll = 0 + end + if yPos == 19 and xPos >= 38 and xPos <= 41 then + scroll = math.max(0,scroll-1) + end + if yPos == 19 and xPos >= 43 and xPos <= 46 then + scroll = math.min(lastpage,scroll+1) + end + if yPos == 19 and xPos >= 48 then + scroll = lastpage + end + if yPos == 1 and xPos >= 17 then + page = 6 + end + log("Page index is "..scroll) + elseif page == 3 then + if xPos >= 17 then + term.setCursorPos(33,5) + local recipient = read() + term.setCursorPos(33,6) + log("Read recipient for transfer") + local amount = read() + log("Read amount for transfer") + local transaction = readURL(readconfig("syncnode").."?pushtx2&q="..recipient.."&pkey="..masterkey.."&amt="..amount) + balance = tonumber(readURL(readconfig("syncnode").."?getbalance="..address)) + log("Attempting to send "..amount.." KST to "..recipient) + term.setCursorPos(19,8) + if transaction == "Success" then + term.setTextColor(8192) + term.write("Transfer successful") + log("Transfer successful") + term.setTextColor(32768) + elseif string.sub(transaction,0,5) == "Error" then + local problem = "An unknown error happened" + local code = tonumber(string.sub(transaction,6,10)) + if code == 1 then problem = "Insufficient funds available" end + if code == 2 then problem = "Not enough KST in transaction" end + if code == 3 then problem = "Can't comprehend amount to send" end + if code == 4 then problem = "Invalid recipient address" end + term.setTextColor(16384) + term.write(problem) + log(problem) + term.setTextColor(32768) + else + term.setTextColor(16384) + term.write(transaction) + term.setTextColor(32768) + end + os.sleep(2.5) --lower this if you do tons of transfers + log("Unfroze display") + end + elseif page == 4 then + if yPos == 3 and xPos >= 19 and xPos <= 31 then + page = 5 + scroll = 0 + end + if yPos == 4 and xPos >= 19 and xPos <= 31 then + page = 10 + end + if yPos == 3 and xPos >= 35 and xPos <= 48 then + page = 6 + end + if yPos == 4 and xPos >= 35 and xPos <= 46 then + page = 7 + end + elseif page == 5 then + if yPos > 2 and xPos >= 27 and xPos <= 36 then + page = 2 + subject = blkpeer[(yPos-2)] + scroll = 0 + end + elseif page == 6 then + term.setCursorPos(18,1) + term.write(" ") + term.setCursorPos(18,1) + term.write("ADDRESS ") + subject = read() + if string.len(subject) == 10 then + page = 2 + scroll = 0 + else + page = 6 + end + elseif page == 7 then + if yPos > 2 and yPos <= 18 and xPos >= 20 and xPos < 30 then + if blkpeer[(yPos-2)] ~= "N/A(Burnt)" then + page = 2 + subject = blkpeer[(yPos-2)] + scroll = 0 + end + end + elseif page == 15 then + + local function isEdit(xpo) + return xpo >= 39 and xpo <= 42 + end + local function isSend(xpo) + return xpo >= 44 and xpo <= 47 + end + + if xPos and yPos then + local listofnames = split(lexmm, ";") + if yPos == 1 and xPos >= 46 then + page = 16 + elseif lem and yPos >= 3 and isEdit(xPos) then + if listofnames[yPos - 3] then + page = 17 + local nameclicked = yPos - 3 + subject = listofnames[nameclicked] + end + elseif lem and yPos >= 3 and isSend(xPos) then + if listofnames[yPos - 3] then + page = 18 + local nameclicked = yPos - 3 + subject = listofnames[nameclicked] + end + end + end + elseif page == 8 then + if yPos == 3 and xPos >= 19 and xPos <= 30 then + page = 9 + end + if yPos == 3 and xPos >= 35 and xPos <= 47 then + page = 16 + end + if yPos == 4 and xPos >= 35 and xPos <= 47 then + --page = 18 + end + if yPos == 4 and xPos >= 19 and xPos <= 29 then + page = 13 + end + elseif page == 18 then + if yPos == 5 and xPos >= 30 then + term.setCursorPos(30,5) + term.write(" ") + term.setCursorPos(30,5) + maxspace = read():lower() + term.setCursorPos(19,7) + pagespace = readURL(readconfig("syncnode").."?name_transfer&pkey="..masterkey.."&name="..subject.."&q="..maxspace) + if pagespace == "Success" then + end + term.write("Name transferred") + log("Tried sending a name to "..maxspace) + os.sleep(3) + page = 15 + end + elseif page == 16 then + if yPos == 4 and xPos >= 25 then + term.setCursorPos(25,4) + term.write(" ") + term.setCursorPos(25,4) + name = read():lower():gsub(".kst",""):gsub(" ","") + term.setCursorPos(25,4) + term.write("Please wait... ") + if string.len(name) > 0 then + if name == "a" or name == "name" or name == "id" or name == "owner" or name == "registered" or name == "updated" or name == "expires" or name == "unpaid" then + availability = 0 + else + availability = tonumber(readURL(readconfig("syncnode").."?name_check="..name)) + log("Checked "..name..".kst for availability ("..availability..")") + term.setCursorPos(19,7) + if availability then + term.setTextColor(colors.green) + term.write("Available!") + else + term.setTextColor(colors.red) + term.write("Not available!") + end + end + else + name = "" + end + elseif yPos == 7 and xPos >= 30 and xPos <= 39 and availability == 1 and balance >= 500 then + availability = 2 + local k = readURL(readconfig("syncnode").."?name_new&pkey="..masterkey.."&name="..name) + end + elseif page == 17 then + if yPos == 5 and xPos >= 25 then + term.setCursorPos(25,5) + term.write(" ") + term.setCursorPos(25,5) + zone = read():gsub("http://","") + term.setCursorPos(25,5) + term.write("Please wait... ") + local sevenminutesleftuntilmaystartsfuckihavetoreleasethisnow = readURL(readconfig("syncnode").."?name_update&pkey="..masterkey.."&name="..subject.."&ar="..zone) + elseif yPos == 7 and xPos >= 30 and xPos <= 39 and availability == 1 and balance >= 500 then + availability = 2 + local k = readURL(readconfig("syncnode").."?name_new&pkey="..masterkey.."&name="..name) + end + elseif page == 9 then + if yPos == 4 and xPos >= 30 then + term.setCursorPos(30,4) + term.write(" ") + term.setCursorPos(30,4) + doublekey = read("*") + term.setCursorPos(30,4) + term.write("Please wait... ") + if string.len(doublekey) > 0 then + doublekey = sha256(masterkey.."-"..sha256(doublekey)) + addressdv = makev2address(doublekey) + balance2 = tonumber(readURL(readconfig("syncnode").."?getbalance="..addressdv)) + log("Derived double vault "..addressdv) + else + addressdv = "" + balance2 = 0 + end + end + if yPos == 5 and xPos >= 33 then + term.setCursorPos(33,5) + term.write(" ") + term.setCursorPos(33,5) + amt = read() + if tonumber(amt) == nil then + amt = 0 + elseif tonumber(amt) % 1 ~= 0 then + amt = 0 + elseif tonumber(amt) <= 0 then + amt = 0 + end + end + if yPos == 6 and xPos >= 25 and xPos <= 33 then + if tonumber(amt) > 0 and string.len(doublekey) > 0 then + if tonumber(amt) <= balance then + local transaction = readURL(readconfig("syncnode").."?pushtx2&q="..addressdv.."&pkey="..masterkey.."&amt="..tonumber(amt)) + balance = tonumber(readURL(readconfig("syncnode").."?getbalance="..address)) + balance2 = tonumber(readURL(readconfig("syncnode").."?getbalance="..addressdv)) + log("Put "..amt.." KST in a double vault") + end + end + end + if yPos == 6 and xPos >= 35 and xPos <= 44 then + if tonumber(amt) > 0 and string.len(doublekey) > 0 then + if tonumber(amt) <= balance2 then + local transaction = readURL(readconfig("syncnode").."?pushtx2&q="..address.."&pkey="..doublekey.."&amt="..tonumber(amt)) + balance = tonumber(readURL(readconfig("syncnode").."?getbalance="..address)) + balance2 = tonumber(readURL(readconfig("syncnode").."?getbalance="..addressdv)) + log("Took "..amt.." KST from a double vault") + end + end + end + elseif page == 13 then + if yPos == 5 and xPos >= 33 then + term.setCursorPos(33,5) + term.write(" ") + term.setCursorPos(33,5) + term.setTextColor(32768) + amt = read() + if tonumber(amt) == nil then + amt = 0 + elseif tonumber(amt) % 1 ~= 0 then + amt = 0 + elseif tonumber(amt) <= 0 then + amt = 0 + end + end + if yPos == 6 and xPos >= 25 and xPos <= 33 then + if tonumber(amt) > 0 then + if tonumber(amt) <= balance then + local transaction = readURL(readconfig("syncnode").."?pushtx2&q="..addresslv.."&pkey="..masterkey.."&amt="..tonumber(amt)) + balance = tonumber(readURL(readconfig("syncnode").."?getbalance="..address)) + log("Put "..amt.." KST in a local vault") + end + end + end + if yPos == 6 and xPos >= 35 and xPos <= 44 then + if tonumber(amt) > 0 then + if tonumber(amt) <= balance3 then + local transaction = readURL(readconfig("syncnode").."?pushtx2&q="..address.."&pkey="..readconfig("keyLV").."&amt="..tonumber(amt)) + balance = tonumber(readURL(readconfig("syncnode").."?getbalance="..address)) + log("Took "..amt.." KST from a local vault") + end + end + end + end + if pagebefore ~= page then log("Switched to page "..page) end +end +local function drawTab(text) + term.setBackgroundColor(512) + term.write(text) +end +local function drawBtn(text) + term.setBackgroundColor(32) + term.write(text) +end +function hud() + term.setBackgroundColor(1) + term.setTextColor(32768) + term.clear() + if gui == 1 then + local sidebar = 1 + while sidebar < 51 do + term.setCursorPos(1,sidebar) + term.setBackgroundColor(8) + term.write(" ") + sidebar = sidebar + 1 + end + term.setCursorPos(2,2) + drawKrist() + term.setBackgroundColor(8) + term.setTextColor(32768) + term.write(" KristWallet") + term.setCursorPos(5,3) + term.setTextColor(2048) + term.write("release "..version.."") + term.setCursorPos(2,19) + term.write(" by 3d6") + term.setTextColor(32768) + term.setCursorPos(3,5) + drawTab(" Overview ") + term.setCursorPos(3,7) + drawTab("Transactions") + term.setCursorPos(3,9) + drawTab(" Send Krist ") + term.setCursorPos(3,11) + drawTab(" Special TX ") + term.setCursorPos(3,13) + drawTab(" Economicon ") + term.setCursorPos(3,15) + drawTab("Name Manager") + term.setCursorPos(3,17) + drawTab(" Exit ") + term.setBackgroundColor(1) + elseif gui == 2 then + term.setCursorPos(1,1) + term.setBackgroundColor(8) + term.write(" ") + term.setCursorPos(1,2) + term.write(" ") + term.setCursorPos(1,3) + term.write(" ") + term.setCursorPos(1,4) + term.write(" ") + term.setCursorPos(2,2) + drawKrist() + term.setBackgroundColor(8) + term.setTextColor(32768) + term.write(" KristWallet") + term.setCursorPos(5,3) + term.setTextColor(2048) + term.write("release "..version.."") + term.setCursorPos(19,2) + term.setBackgroundColor(16384) + term.setTextColor(32768) + term.write(" Exit ") + end + if page == 1 then + term.setCursorPos(19,2) + term.write("Your address: ") + term.setTextColor(16384) + term.write(address) + term.setTextColor(32768) + term.setCursorPos(19,5) + local recenttransactions = "" + if tostring(balance) ~= 'nil' then recenttransactions = readURL(readconfig("syncnode").."?listtx="..address.."&overview") end + local txtype = 0 + local graphics = 0 + if string.len(recenttransactions) > 25 then + repeat + if string.sub(recenttransactions,13+(31*graphics),22+(31*graphics)) == "N/A(Mined)" then txtype = 1 + elseif string.sub(recenttransactions,13+(31*graphics),22+(31*graphics)) == "N/A(Names)" and tonumber(string.sub(recenttransactions,23+(31*graphics),31+(31*graphics))) == 0 then txtype = 7 + elseif tonumber(string.sub(recenttransactions,23+(31*graphics),31+(31*graphics))) == 0 then txtype = 9 + elseif string.sub(recenttransactions,13+(31*graphics),22+(31*graphics)) == "N/A(Names)" then txtype = 6 + elseif string.sub(recenttransactions,13+(31*graphics),22+(31*graphics)) == address then txtype = 4 + elseif string.sub(recenttransactions,13+(31*graphics),22+(31*graphics)) == addressv1 then txtype = 5 + elseif tonumber(string.sub(recenttransactions,23+(31*graphics),31+(31*graphics))) < 0 then txtype = 2 + elseif tonumber(string.sub(recenttransactions,23+(31*graphics),31+(31*graphics))) > 0 then txtype = 3 + else txtype = 8 + end + postgraphic(19,5+(5*graphics),txtype) + term.setCursorPos(26,5+(5*graphics)) + term.setBackgroundColor(1) + term.setTextColor(32768) + if txtype == 1 then term.write("Mined") + elseif txtype == 2 then term.write("Sent") + elseif txtype == 3 then term.write("Received") + elseif txtype == 4 then term.write("Sent to yourself") + elseif txtype == 5 then term.write("Imported") + elseif txtype == 6 then term.write("Name registered") + elseif txtype == 7 then term.write("Name operation") + elseif txtype == 8 then term.write("Unknown") + elseif txtype == 9 then term.write("Name transfer") + end + term.setCursorPos(26,6+(5*graphics)) + if txtype == 4 then + term.setTextColor(32768) + elseif tonumber(string.sub(recenttransactions,23+(31*graphics),31+(31*graphics))) > 0 then + term.setTextColor(8192) + term.write("+") + elseif tonumber(string.sub(recenttransactions,23+(31*graphics),31+(31*graphics))) == 0 then + term.setTextColor(16) + else + term.setTextColor(16384) + end + if txtype < 7 then term.write(tostring(tonumber(string.sub(recenttransactions,23+(31*graphics),31+(31*graphics)))).." KST") end + term.setCursorPos(26,7+(5*graphics)) + term.setTextColor(32768) + if txtype ~= 6 then term.setTextColor(512) end + if txtype == 9 or (txtype > 1 and txtype < 6) then term.write(string.sub(recenttransactions,13+(31*graphics),22+(31*graphics))) end + --if txtype == 6 then term.write(".kst") end + term.setCursorPos(26,8+(5*graphics)) + term.setTextColor(128) + term.write(string.sub(recenttransactions,1+(31*graphics),12+(31*graphics))) + graphics = graphics + 1 + until graphics >= math.floor(string.len(recenttransactions)/32) + end + term.setTextColor(32768) + term.setCursorPos(19,3) + term.write("Your balance: ") + term.setTextColor(1024) + if tostring(balance) == 'nil' then balance = 0 end + term.write(tostring(balance).." KST ") + term.setTextColor(512) + local names = tonumber(readURL(readconfig("syncnode").."?getnames="..address)) + if names > 0 then term.write("["..tostring(names).."]") end + local alert = http.get(readconfig("syncnode").."?alert="..masterkey).readAll() + if #(alert:gsub("^%s*(.-)%s*$", "%1")) > 0 then + term.setCursorPos(1,1) + term.setBackgroundColor(16384) + term.setTextColor(16) + term.clearLine() + term.write(alert) + end + elseif page == 2 then + term.setCursorPos(18,1) + term.write("Please wait...") + os.sleep(0) + subbal = readURL(readconfig("syncnode").."?getbalance="..subject) + subtxs = readURL(readconfig("syncnode").."?listtx="..subject) + log("Loaded transactions for address "..subject) + log("Page index is "..scroll) + term.setCursorPos(18,1) + if subtxs == "end" then subbal = 0 end + term.write("ADDRESS "..subject.." - "..subbal.." KST") + term.setCursorPos(17,2) + term.setBackgroundColor(256) + term.write(" Time Peer Value ") + term.setBackgroundColor(1) + if subtxs ~= "end" then + local tx = 0 + local s = 0 + ar = 16*scroll + repeat + tx = tx + 1 + stdate[tx] = string.sub(subtxs,1,12) + subtxs = string.sub(subtxs,13) + stpeer[tx] = string.sub(subtxs,1,10) + subtxs = string.sub(subtxs,11) + stval[tx] = tonumber(string.sub(subtxs,1,9)) + subtxs = string.sub(subtxs,10) + if stpeer[tx] == subject then stval[tx] = 0 end + until string.len(subtxs) == 3 + repeat + ar = ar + 1 + term.setTextColor(32768) + term.setCursorPos(18,2+ar-(16*(scroll))) + term.write(stdate[ar]) + if stpeer[ar] ~= "N/A(Mined)" then term.setTextColor(512) end + if stpeer[ar] == subject then term.setTextColor(32768) end + if stpeer[ar] == "N/A(Names)" then term.setTextColor(32768) end + term.setCursorPos(31,2+ar-(16*(scroll))) + term.write(stpeer[ar]) + term.setCursorPos(50-string.len(tostring(math.abs(stval[ar]))),2+ar-(16*(scroll))) + if stval[ar] > 0 then + term.setTextColor(8192) + term.write("+") + elseif stval[ar] < 0 then + term.setTextColor(16384) + else + term.setTextColor(32768) + term.write(" ") + end + term.write(tostring(stval[ar])) + until ar == math.min(tx,16*(scroll+1)) + term.setBackgroundColor(256) + term.setCursorPos(17,19) + term.write(" ") + term.setCursorPos(17,19) + term.setTextColor(32768) + lastpage = math.floor((tx-1)/16) + if (1+lastpage) < 100 then maxspace = maxspace.." " end + if (1+lastpage) < 10 then maxspace = maxspace.." " end + if (1+scroll) < 100 then pagespace = pagespace.." " end + if (1+scroll) < 10 then pagespace = pagespace.." " end + term.write(" Page "..pagespace..(1+scroll).."/"..maxspace..(1+lastpage)) + pagespace = "" + maxspace = "" + term.setCursorPos(32,19) + term.setTextColor(128) + term.write("First Prev Next Last") + if (scroll > 0) then + term.setCursorPos(32,19) + term.setTextColor(2048) + term.write("First Prev") + end + if (scroll < lastpage and tx > 16) then + term.setCursorPos(43,19) + term.setTextColor(2048) + term.write("Next Last") + end + else + term.write("No transactions to display!") + term.setBackgroundColor(256) + term.setCursorPos(17,19) + term.write(" ") + term.setCursorPos(17,19) + term.setTextColor(32768) + term.write(" Page 1/ 1") + term.setCursorPos(32,19) + term.setTextColor(128) + term.write("First Prev Next Last") + end + elseif page == 3 then + term.setCursorPos(19,2) + term.write("Your address: ") + term.setTextColor(16384) + term.write(address) + term.setTextColor(32768) + term.setCursorPos(19,3) + term.write("Your balance: ") + term.setTextColor(1024) + if tostring(balance) == 'nil' then balance = 0 end + term.write(tostring(balance).." KST") + term.setTextColor(32768) + term.setCursorPos(19,5) + term.write("Recipient: ") + term.write(" ") + term.setCursorPos(19,6) + term.write("Amount (KST): ") + term.write(" ") + elseif page == 4 then + term.setCursorPos(19,2) + term.write("Mining Addresses") + term.setTextColor(512) + term.setCursorPos(19,3) + term.write("Latest blocks Address lookup") + term.setCursorPos(19,4) + term.write("Lowest hashes Top balances") + term.setCursorPos(19,5) + --term.write("Lowest nonces ") + term.setTextColor(32768) + term.setCursorPos(19,7) + --term.write("Economy Transactions") + term.setTextColor(512) + term.setCursorPos(19,8) + --term.write("KST issuance Latest transfers") + term.setCursorPos(19,9) + --term.write("KST distrib. Largest transfers") + elseif page == 5 then + local blocks = readURL(readconfig("syncnode").."?blocks") + local tx = 0 + ar = 0 + local height = string.sub(blocks,1,8) + local blktime = {} + blkpeer = {} + local blkhash = {} + height = tonumber(string.sub(blocks,1,8)) + blocks = string.sub(blocks,9) + local today = string.sub(blocks,1,10) + blocks = string.sub(blocks,11) + repeat + tx = tx + 1 + blktime[tx] = string.sub(blocks,1,8) + blocks = string.sub(blocks,9) + blkpeer[tx] = string.sub(blocks,1,10) + blocks = string.sub(blocks,11) + blkhash[tx] = string.sub(blocks,1,12) + blocks = string.sub(blocks,13) + if stpeer[tx] == subject then stval[tx] = 0 end + until string.len(blocks) == 0 + term.setCursorPos(18,1) + term.write("Height: "..tostring(height)) + term.setCursorPos(36,1) + term.write("Date: "..today) + term.setCursorPos(17,2) + term.setBackgroundColor(256) + term.write(" Time Miner Hash ") + ----------(" 00:00:00 0000000000 000000000000 ") + term.setBackgroundColor(1) + repeat + ar = ar + 1 + term.setCursorPos(18,2+ar) + term.write(blktime[ar]) + if blkpeer[ar] ~= "N/A(Burnt)" then term.setTextColor(512) end + term.setCursorPos(27,2+ar) + term.write(blkpeer[ar]) + term.setTextColor(32768) + term.setCursorPos(38,2+ar) + term.write(blkhash[ar]) + until ar == math.min(tx,17*(scroll+1)) + elseif page == 6 then + term.setCursorPos(17,2) + term.setBackgroundColor(256) + term.write(" Time Peer Value ") + term.setBackgroundColor(256) + term.setCursorPos(17,19) + term.write(" ") + term.setCursorPos(17,19) + term.setTextColor(32768) + term.write(" Page /") + term.setCursorPos(32,19) + term.setTextColor(128) + term.write("First Prev Next Last") + term.setBackgroundColor(1) + term.setCursorPos(18,1) + term.write("ADDRESS (click to edit)") + elseif page == 7 then + local blocks = readURL(readconfig("syncnode").."?richapi") + local tx = 0 + ar = 0 + local height = string.sub(blocks,1,8) + local blktime = {} + blkpeer = {} + local blkhash = {} + repeat + tx = tx + 1 + blkpeer[tx] = string.sub(blocks,1,10) + blocks = string.sub(blocks,11) + blktime[tx] = tonumber(string.sub(blocks,1,8)) + blocks = string.sub(blocks,9) + blkhash[tx] = string.sub(blocks,1,11) + blocks = string.sub(blocks,12) + until string.len(blocks) == 0 + term.setCursorPos(18,1) + term.write("Krist address rich list") + term.setCursorPos(17,2) + term.setBackgroundColor(256) + term.write("R# Address Balance First seen ") + term.setBackgroundColor(1) + repeat + ar = ar + 1 + term.setCursorPos(17,2+ar) + if ar < 10 then term.write(" ") end + term.write(ar) + term.setCursorPos(20,2+ar) + if blkpeer[ar] ~= "N/A(Burnt)" then term.setTextColor(512) end + term.write(blkpeer[ar]) + term.setTextColor(32768) + term.setCursorPos(39-string.len(tostring(math.abs(blktime[ar]))),2+ar) + term.write(blktime[ar]) + term.setCursorPos(40,2+ar) + term.write(blkhash[ar]) + until ar == 16 + elseif page == 8 then + term.setCursorPos(19,2) + term.write("Storage Names") + term.setTextColor(512) + term.setCursorPos(19,3) + term.write("Double vault Register name") + term.setCursorPos(19,4) + term.write("Local vault") + term.setCursorPos(19,5) + --term.write("Disk vault v1 SHA vault") + term.setCursorPos(19,6) + --term.write("SHA vault v1 wallet") + elseif page == 9 then + term.setCursorPos(25,2) + term.write("Double vault manager") + term.setCursorPos(19,8) + term.write("Using double vaults is a way to") + term.setCursorPos(19,9) + term.write("store your Krist under an extra") + term.setCursorPos(19,10) + term.write("layer of security. You can only") + term.setCursorPos(19,11) + term.write("access a double vault from your") + term.setCursorPos(19,12) + term.write("wallet (on any server) and then") + term.setCursorPos(19,13) + term.write("only after typing an extra pass") + term.setCursorPos(19,14) + term.write("code. Double wallets are wholly") + term.setCursorPos(19,15) + term.write("invisible to unauthorized users") + term.setCursorPos(19,16) + term.write("of your wallet; they can not be") + term.setCursorPos(19,17) + term.write("seen or opened without the pass") + term.setCursorPos(19,18) + term.write("code set by you.") + term.setCursorPos(19,4) + term.write("Pass code: ") + term.setCursorPos(19,5) + term.write("Amount (KST): ") + term.setCursorPos(30,4) + if string.len(doublekey) == 0 then + term.setTextColor(256) + term.write("(click to set)") + else + term.setTextColor(8192) + term.write("Ready: "..balance2.." KST") + if tonumber(amt) > 0 then + term.setCursorPos(25,6) + term.setTextColor(32768) + term.setBackgroundColor(128) + if tonumber(amt) <= balance then + term.setBackgroundColor(2) + end + term.write(" Deposit ") + term.setBackgroundColor(1) + term.write(" ") + term.setBackgroundColor(128) + if tonumber(amt) <= balance2 then + term.setBackgroundColor(2) + end + term.write(" Withdraw ") + term.setBackgroundColor(1) + end + end + term.setCursorPos(33,5) + if amt == 0 then + term.setTextColor(256) + term.write("(click to set)") + else + term.setTextColor(32768) + term.write(amt) + end + term.setTextColor(32768) + elseif page == 10 then + local blocks = readURL(readconfig("syncnode").."?blocks&low") + local tx = 0 + ar = 0 + local blktime = {} + blkpeer = {} + local blkhash = {} + repeat + tx = tx + 1 + blktime[tx] = string.sub(blocks,1,6) + blocks = string.sub(blocks,7) + blkpeer[tx] = string.sub(blocks,1,6) + blocks = string.sub(blocks,7) + blkhash[tx] = string.sub(blocks,1,20) + blocks = string.sub(blocks,21) + until string.len(blocks) == 0 + term.setCursorPos(17,1) + term.setBackgroundColor(256) + term.write(" Date Block# Hash ") + ----------(" Feb 28 000000 000000000000oooooooo") + term.setBackgroundColor(1) + repeat + ar = ar + 1 + term.setCursorPos(18,1+ar) + term.write(blktime[ar]) + term.setCursorPos(31-string.len(tostring(math.abs(tonumber(blkpeer[ar])))),1+ar) + term.write(tonumber(blkpeer[ar])) + term.setTextColor(256) + term.setCursorPos(32,1+ar) + term.write(blkhash[ar]) + term.setTextColor(32768) + term.setCursorPos(32,1+ar) + term.write(string.sub(blkhash[ar],1,12)) + until ar == math.min(tx,18) + elseif page == 11 then + local blocks = readURL(readconfig("syncnode").."?blocks&low&lownonce") + local tx = 0 + ar = 0 + local blktime = {} + blkpeer = {} + local blkhash = {} + repeat + tx = tx + 1 + blktime[tx] = string.sub(blocks,1,6) + blocks = string.sub(blocks,7) + blkpeer[tx] = string.sub(blocks,1,6) + blocks = string.sub(blocks,7) + blkhash[tx] = string.sub(blocks,1,12) + blocks = string.sub(blocks,13) + until string.len(blocks) == 0 + term.setCursorPos(17,1) + term.setBackgroundColor(256) + term.write(" Date Block# Nonce ") + ----------(" Feb 28 000000 000000000000") + term.setBackgroundColor(1) + repeat + ar = ar + 1 + term.setCursorPos(18,1+ar) + term.write(blktime[ar]) + term.setCursorPos(31-string.len(tostring(math.abs(tonumber(blkpeer[ar])))),1+ar) + term.write(tonumber(blkpeer[ar])) + term.setTextColor(32768) + term.setCursorPos(32,1+ar) + term.write(tonumber(blkhash[ar])) + until ar == math.min(tx,18) + elseif page == 12 then + local blocks = readURL(readconfig("syncnode").."?blocks&low&highnonce") + local tx = 0 + ar = 0 + local blktime = {} + blkpeer = {} + local blkhash = {} + repeat + tx = tx + 1 + blktime[tx] = string.sub(blocks,1,6) + blocks = string.sub(blocks,7) + blkpeer[tx] = string.sub(blocks,1,6) + blocks = string.sub(blocks,7) + blkhash[tx] = string.sub(blocks,1,12) + blocks = string.sub(blocks,13) + until string.len(blocks) == 0 + term.setCursorPos(17,1) + term.setBackgroundColor(256) + term.write(" Date Block# Nonce ") + ----------(" Feb 28 000000 000000000000") + term.setBackgroundColor(1) + repeat + ar = ar + 1 + term.setCursorPos(18,1+ar) + term.write(blktime[ar]) + term.setCursorPos(31-string.len(tostring(math.abs(tonumber(blkpeer[ar])))),1+ar) + term.write(tonumber(blkpeer[ar])) + term.setTextColor(32768) + term.setCursorPos(32,1+ar) + term.write(tonumber(blkhash[ar])) + until ar == math.min(tx,18) + elseif page == 13 then + balance3 = tonumber(readURL(readconfig("syncnode").."?getbalance="..addresslv)) + term.setCursorPos(25,2) + term.write("Local vault manager") + term.setCursorPos(19,8) + term.write("Local vaults are a place to put") + term.setCursorPos(19,9) + term.write("Krist in the form of a file on") + term.setCursorPos(19,10) + term.write("a computer. Unlike traditional") + term.setCursorPos(19,11) + term.write("wallets, local vaults can only") + term.setCursorPos(19,12) + term.write("be accessed on the computer") + term.setCursorPos(19,13) + term.write("they were initially created on.") + term.setCursorPos(19,14) + term.write("If you do this, please ensure") + term.setCursorPos(19,15) + term.write("that this computer is never") + term.setCursorPos(19,16) + term.write("stolen or broken, as your money") + term.setCursorPos(19,17) + term.write("may be lost if you don't have a") + term.setCursorPos(19,18) + term.write("backup.") + term.setCursorPos(19,4) + term.write("KST put here: "..balance3) + term.setCursorPos(19,5) + term.write("Amount (KST): ") + term.setCursorPos(33,5) + if amt == 0 then + term.setTextColor(256) + term.write("(click to set)") + else + term.setTextColor(32768) + term.write(amt) + end + if tonumber(amt) > 0 then + term.setCursorPos(25,6) + term.setTextColor(32768) + term.setBackgroundColor(128) + if tonumber(amt) <= balance then + term.setBackgroundColor(2) + end + term.write(" Deposit ") + term.setBackgroundColor(1) + term.write(" ") + term.setBackgroundColor(128) + if tonumber(amt) <= balance3 then + term.setBackgroundColor(2) + end + term.write(" Withdraw ") + term.setBackgroundColor(1) + end + elseif page == 14 then + term.setBackgroundColor(1) + term.setCursorPos(19,2) + term.write("Local settings") + --deprecated for now + elseif page == 15 then + term.setBackgroundColor(1) + term.setCursorPos(18,1) + term.write(".KST domain name manager [New]") + term.setCursorPos(46,1) + term.setBackgroundColor(32) + term.setTextColor(1) + term.write(" + NEW") + term.setCursorPos(17,2) + term.setBackgroundColor(256) + term.setTextColor(32768) + term.write(" Name Actions ") + term.setBackgroundColor(1) + term.setCursorPos(18,3) + local namelist = readURL(readconfig("syncnode").."?listnames="..address) + local splitname = split(namelist, ";") + + + if #splitname == 0 then + term.setTextColor(256) + term.write("No names to display!") + else + local namecount = 1 + repeat + local thisname = splitname[namecount] + --namelist:sub(0,namelist:find(";")-1) + term.setTextColor(32768) + term.setCursorPos(18,3+namecount) + term.write(splitname[namecount]..".kst") + term.setCursorPos(39,3+namecount) + term.setTextColor(512) + if thisname == "a" or thisname == "name" or thisname == "owner" or thisname == "updated" or thisname == "registered" or thisname == "expires" or thisname == "id" or thisname == "unpaid" then term.setTextColor(256) end + term.write("Edit Send ") + term.setTextColor(256) + term.write("Go") + namecount = namecount + 1 + until namecount == #splitname+1 + end + --term.write("a.kst Edit Send Go") + term.setBackgroundColor(1) + elseif page == 16 then + term.setBackgroundColor(1) + term.setCursorPos(20,2) + term.write(".KST domain name registration") + term.setCursorPos(19,4) + term.write("Name: ") + if name == "" then + term.setTextColor(colors.lightGray) + term.write("(click to set)") + else + term.write(name) + term.setTextColor(colors.lightGray) + term.write(".kst") + end + term.setTextColor(colors.black) + term.setCursorPos(19,5) + term.write("Cost: 500 KST") + term.setCursorPos(19,7) + --term.write("Available! [Register]") + if name == "" then + term.setTextColor(colors.blue) + term.write("Please select a name!") + elseif availability == 1 then + term.setTextColor(colors.green) + term.write("Available! ") + --if balance >= 500 then + term.setBackgroundColor(colors.green) + term.setTextColor(colors.lime) + term.write(" Register ") + term.setBackgroundColor(colors.white) + --end + elseif availability == 2 then + term.setTextColor(colors.yellow) + term.write("Name registered!") + else + term.setTextColor(colors.red) + term.write("Not available!") + end + term.setTextColor(colors.black) + term.setCursorPos(19,9) + term.write(".KST domain names are used on") + term.setCursorPos(19,10) + term.write("the KristScape browser. For") + term.setCursorPos(19,11) + term.write("more information, please see") + term.setCursorPos(19,12) + term.write("the Krist thread.") + term.setCursorPos(19,14) + term.write("All Krist spent on names will") + term.setCursorPos(19,15) + term.write("be added to the value of") + term.setCursorPos(19,16) + term.write("future blocks; essentially") + term.setCursorPos(19,17) + term.write("being \"re-mined.\"") + elseif page == 17 then + term.setBackgroundColor(1) + term.setCursorPos(28,2) + term.write(".KST zone file") + term.setCursorPos(19,4) + term.write("Name: "..subject) + term.setTextColor(colors.lightGray) + term.write(".kst") + term.setTextColor(colors.black) + term.setCursorPos(19,7) + term.write("Your name's zone file is the") + term.setCursorPos(19,8) + term.write("URL of the site it is pointing") + term.setCursorPos(19,9) + term.write("to. When KristScape navigates") + term.setCursorPos(19,10) + term.write("to a name, it will make an HTTP") + term.setCursorPos(19,11) + term.write("get request to the above URL.") + term.setCursorPos(19,12) + term.write("The zone record should not") + term.setCursorPos(19,13) + term.write("include a protocol (http://)") + term.setCursorPos(19,14) + term.write("and shouldn't end with a") + term.setCursorPos(19,15) + term.write("slash. You can redirect a name") + term.setCursorPos(19,16) + term.write("to another name by making the") + term.setCursorPos(19,17) + term.write("first character of the record") + term.setCursorPos(19,18) + term.write("a dollar sign; e.g. $krist.kst") + term.setTextColor(colors.black) + term.setCursorPos(19,5) + term.write("Zone: ") + zone = readURL(readconfig("syncnode").."?a="..subject) + if zone == "" then + term.setTextColor(colors.lightGray) + term.write("(click to set)") + else + term.write(zone) + end + elseif page == 18 then + term.setBackgroundColor(1) + term.setCursorPos(28,2) + term.write("Name transfer") + term.setCursorPos(19,4) + term.write("Name: "..subject) + term.setTextColor(colors.lightGray) + term.write(".kst") + term.setTextColor(colors.black) + term.setCursorPos(19,5) + term.write("Recipient: ") + elseif page == 21 then + term.setBackgroundColor(1) + term.setCursorPos(4,6) + term.write("Address - ") + term.setTextColor(16384) + term.write(address) + term.setTextColor(32768) + term.setCursorPos(4,7) + term.write("Balance - ") + term.setTextColor(1024) + if tostring(balance) == 'nil' then balance = 0 end + term.write(tostring(balance).." KST") + term.setTextColor(32768) + term.setCursorPos(3,9) + end +end +boot()
\ No newline at end of file diff --git a/Programs/LuaIDE.bup/Contents/Info.meta b/Programs/LuaIDE.bup/Contents/Info.meta new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/Programs/LuaIDE.bup/Contents/Info.meta @@ -0,0 +1 @@ + diff --git a/Programs/LuaIDE.bup/Contents/Resources/resources_here.txt b/Programs/LuaIDE.bup/Contents/Resources/resources_here.txt new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/Programs/LuaIDE.bup/Contents/Resources/resources_here.txt @@ -0,0 +1 @@ + diff --git a/Programs/LuaIDE.bup/Contents/bits-UI/luaide.lua b/Programs/LuaIDE.bup/Contents/bits-UI/luaide.lua new file mode 100644 index 0000000..5e06e6b --- /dev/null +++ b/Programs/LuaIDE.bup/Contents/bits-UI/luaide.lua @@ -0,0 +1,2224 @@ + +-- +-- Lua IDE +-- Made by GravityScore +-- + + + + +-- Variables + +local version = "1.1" +local arguments = {...} + + +local w, h = term.getSize() +local tabWidth = 2 + + +local autosaveInterval = 20 +local allowEditorEvent = true +local keyboardShortcutTimeout = 0.4 + + +local clipboard = nil + + +local theme = { + background = colors.gray, + titleBar = colors.lightGray, + + top = colors.lightBlue, + bottom = colors.cyan, + + button = colors.cyan, + buttonHighlighted = colors.lightBlue, + + dangerButton = colors.red, + dangerButtonHighlighted = colors.pink, + + text = colors.white, + folder = colors.lime, + readOnly = colors.red, +} + + +local languages = {} +local currentLanguage = {} + + +local updateURL = "https://raw.github.com/GravityScore/LuaIDE/master/computercraft/ide.lua" +local ideLocation = "/" .. shell.getRunningProgram() +local themeLocation = "/.luaide_theme" + +local function isAdvanced() + return term.isColor and term.isColor() +end + + + + +-- -------- Utilities + +local function modRead(properties) + local w, h = term.getSize() + local defaults = {replaceChar = nil, history = nil, visibleLength = nil, textLength = nil, + liveUpdates = nil, exitOnKey = nil} + if not properties then properties = {} end + for k, v in pairs(defaults) do if not properties[k] then properties[k] = v end end + if properties.replaceChar then properties.replaceChar = properties.replaceChar:sub(1, 1) end + if not properties.visibleLength then properties.visibleLength = w end + + local sx, sy = term.getCursorPos() + local line = "" + local pos = 0 + local historyPos = nil + + local function redraw(repl) + local scroll = 0 + if properties.visibleLength and sx + pos > properties.visibleLength + 1 then + scroll = (sx + pos) - (properties.visibleLength + 1) + end + + term.setCursorPos(sx, sy) + local a = repl or properties.replaceChar + if a then term.write(string.rep(a, line:len() - scroll)) + else term.write(line:sub(scroll + 1, -1)) end + term.setCursorPos(sx + pos - scroll, sy) + end + + local function sendLiveUpdates(event, ...) + if type(properties.liveUpdates) == "function" then + local ox, oy = term.getCursorPos() + local a, data = properties.liveUpdates(line, event, ...) + if a == true and data == nil then + term.setCursorBlink(false) + return line + elseif a == true and data ~= nil then + term.setCursorBlink(false) + return data + end + term.setCursorPos(ox, oy) + end + end + + term.setCursorBlink(true) + while true do + local e, but, x, y, p4, p5 = os.pullEvent() + + if e == "char" then + local s = false + if properties.textLength and line:len() < properties.textLength then s = true + elseif not properties.textLength then s = true end + + local canType = true + if not properties.grantPrint and properties.refusePrint then + local canTypeKeys = {} + if type(properties.refusePrint) == "table" then + for _, v in pairs(properties.refusePrint) do + table.insert(canTypeKeys, tostring(v):sub(1, 1)) + end + elseif type(properties.refusePrint) == "string" then + for char in properties.refusePrint:gmatch(".") do + table.insert(canTypeKeys, char) + end + end + for _, v in pairs(canTypeKeys) do if but == v then canType = false end end + elseif properties.grantPrint then + canType = false + local canTypeKeys = {} + if type(properties.grantPrint) == "table" then + for _, v in pairs(properties.grantPrint) do + table.insert(canTypeKeys, tostring(v):sub(1, 1)) + end + elseif type(properties.grantPrint) == "string" then + for char in properties.grantPrint:gmatch(".") do + table.insert(canTypeKeys, char) + end + end + for _, v in pairs(canTypeKeys) do if but == v then canType = true end end + end + + if s and canType then + line = line:sub(1, pos) .. but .. line:sub(pos + 1, -1) + pos = pos + 1 + redraw() + end + elseif e == "key" then + if but == keys.enter then break + elseif but == keys.left then if pos > 0 then pos = pos - 1 redraw() end + elseif but == keys.right then if pos < line:len() then pos = pos + 1 redraw() end + elseif (but == keys.up or but == keys.down) and properties.history then + redraw(" ") + if but == keys.up then + if historyPos == nil and #properties.history > 0 then + historyPos = #properties.history + elseif historyPos > 1 then + historyPos = historyPos - 1 + end + elseif but == keys.down then + if historyPos == #properties.history then historyPos = nil + elseif historyPos ~= nil then historyPos = historyPos + 1 end + end + + if properties.history and historyPos then + line = properties.history[historyPos] + pos = line:len() + else + line = "" + pos = 0 + end + + redraw() + local a = sendLiveUpdates("history") + if a then return a end + elseif but == keys.backspace and pos > 0 then + redraw(" ") + line = line:sub(1, pos - 1) .. line:sub(pos + 1, -1) + pos = pos - 1 + redraw() + local a = sendLiveUpdates("delete") + if a then return a end + elseif but == keys.home then + pos = 0 + redraw() + elseif but == keys.delete and pos < line:len() then + redraw(" ") + line = line:sub(1, pos) .. line:sub(pos + 2, -1) + redraw() + local a = sendLiveUpdates("delete") + if a then return a end + elseif but == keys["end"] then + pos = line:len() + redraw() + elseif properties.exitOnKey then + if but == properties.exitOnKey or (properties.exitOnKey == "control" and + (but == 29 or but == 157)) then + term.setCursorBlink(false) + return nil + end + end + end + local a = sendLiveUpdates(e, but, x, y, p4, p5) + if a then return a end + end + + term.setCursorBlink(false) + if line ~= nil then line = line:gsub("^%s*(.-)%s*$", "%1") end + return line +end + + +-- -------- Themes + +local defaultTheme = { + background = "gray", + backgroundHighlight = "lightGray", + prompt = "cyan", + promptHighlight = "lightBlue", + err = "red", + errHighlight = "pink", + + editorBackground = "gray", + editorLineHightlight = "lightBlue", + editorLineNumbers = "gray", + editorLineNumbersHighlight = "lightGray", + editorError = "pink", + editorErrorHighlight = "red", + + textColor = "white", + conditional = "yellow", + constant = "orange", + ["function"] = "magenta", + string = "red", + comment = "lime" +} + +local normalTheme = { + background = "black", + backgroundHighlight = "black", + prompt = "black", + promptHighlight = "black", + err = "black", + errHighlight = "black", + + editorBackground = "black", + editorLineHightlight = "black", + editorLineNumbers = "black", + editorLineNumbersHighlight = "white", + editorError = "black", + editorErrorHighlight = "black", + + textColor = "white", + conditional = "white", + constant = "white", + ["function"] = "white", + string = "white", + comment = "white" +} + +local availableThemes = { + {"Water (Default)", "https://raw.github.com/GravityScore/LuaIDE/master/themes/default.txt"}, + {"Fire", "https://raw.github.com/GravityScore/LuaIDE/master/themes/fire.txt"}, + {"Sublime Text 2", "https://raw.github.com/GravityScore/LuaIDE/master/themes/st2.txt"}, + {"Midnight", "https://raw.github.com/GravityScore/LuaIDE/master/themes/midnight.txt"}, + {"TheOriginalBIT", "https://raw.github.com/GravityScore/LuaIDE/master/themes/bit.txt"}, + {"Superaxander", "https://raw.github.com/GravityScore/LuaIDE/master/themes/superaxander.txt"}, + {"Forest", "https://raw.github.com/GravityScore/LuaIDE/master/themes/forest.txt"}, + {"Night", "https://raw.github.com/GravityScore/LuaIDE/master/themes/night.txt"}, + {"Original", "https://raw.github.com/GravityScore/LuaIDE/master/themes/original.txt"}, +} + +local function loadTheme(path) + local f = io.open(path) + local l = f:read("*l") + local config = {} + while l ~= nil do + local k, v = string.match(l, "^(%a+)=(%a+)") + if k and v then config[k] = v end + l = f:read("*l") + end + f:close() + return config +end + +-- Load Theme +if isAdvanced() then theme = defaultTheme +else theme = normalTheme end + + +-- -------- Drawing + +local function centerPrint(text, ny) + if type(text) == "table" then for _, v in pairs(text) do centerPrint(v) end + else + local x, y = term.getCursorPos() + local w, h = term.getSize() + term.setCursorPos(w/2 - text:len()/2 + (#text % 2 == 0 and 1 or 0), ny or y) + print(text) + end +end + +local function title(t) + term.setTextColor(colors[theme.textColor]) + term.setBackgroundColor(colors[theme.background]) + term.clear() + + term.setBackgroundColor(colors[theme.backgroundHighlight]) + for i = 2, 4 do term.setCursorPos(1, i) term.clearLine() end + term.setCursorPos(3, 3) + term.write(t) +end + +local function centerRead(wid, begt) + local function liveUpdate(line, e, but, x, y, p4, p5) + if isAdvanced() and e == "mouse_click" and x >= w/2 - wid/2 and x <= w/2 - wid/2 + 10 + and y >= 13 and y <= 15 then + return true, "" + end + end + + if not begt then begt = "" end + term.setTextColor(colors[theme.textColor]) + term.setBackgroundColor(colors[theme.promptHighlight]) + for i = 8, 10 do + term.setCursorPos(w/2 - wid/2, i) + term.write(string.rep(" ", wid)) + end + + if isAdvanced() then + term.setBackgroundColor(colors[theme.errHighlight]) + for i = 13, 15 do + term.setCursorPos(w/2 - wid/2 + 1, i) + term.write(string.rep(" ", 10)) + end + term.setCursorPos(w/2 - wid/2 + 2, 14) + term.write("> Cancel") + end + + term.setBackgroundColor(colors[theme.promptHighlight]) + term.setCursorPos(w/2 - wid/2 + 1, 9) + term.write("> " .. begt) + return modRead({visibleLength = w/2 + wid/2, liveUpdates = liveUpdate}) +end + + +-- -------- Prompt + +local function prompt(list, dir, isGrid) + local function draw(sel) + for i, v in ipairs(list) do + if i == sel then term.setBackgroundColor(v.highlight or colors[theme.promptHighlight]) + else term.setBackgroundColor(v.bg or colors[theme.prompt]) end + term.setTextColor(v.tc or colors[theme.textColor]) + for i = -1, 1 do + term.setCursorPos(v[2], v[3] + i) + term.write(string.rep(" ", v[1]:len() + 4)) + end + + term.setCursorPos(v[2], v[3]) + if i == sel then + term.setBackgroundColor(v.highlight or colors[theme.promptHighlight]) + term.write(" > ") + else term.write(" - ") end + term.write(v[1] .. " ") + end + end + + local key1 = dir == "horizontal" and 203 or 200 + local key2 = dir == "horizontal" and 205 or 208 + local sel = 1 + draw(sel) + + while true do + local e, but, x, y = os.pullEvent() + if e == "key" and but == 28 then + return list[sel][1] + elseif e == "key" and but == key1 and sel > 1 then + sel = sel - 1 + draw(sel) + elseif e == "key" and but == key2 and ((err == true and sel < #list - 1) or (sel < #list)) then + sel = sel + 1 + draw(sel) + elseif isGrid and e == "key" and but == 203 and sel > 2 then + sel = sel - 2 + draw(sel) + elseif isGrid and e == "key" and but == 205 and sel < 3 then + sel = sel + 2 + draw(sel) + elseif e == "mouse_click" then + for i, v in ipairs(list) do + if x >= v[2] - 1 and x <= v[2] + v[1]:len() + 3 and y >= v[3] - 1 and y <= v[3] + 1 then + return list[i][1] + end + end + end + end +end + +local function scrollingPrompt(list) + local function draw(items, sel, loc) + for i, v in ipairs(items) do + local bg = colors[theme.prompt] + local bghigh = colors[theme.promptHighlight] + if v:find("Back") or v:find("Return") then + bg = colors[theme.err] + bghigh = colors[theme.errHighlight] + end + + if i == sel then term.setBackgroundColor(bghigh) + else term.setBackgroundColor(bg) end + term.setTextColor(colors[theme.textColor]) + for x = -1, 1 do + term.setCursorPos(3, (i * 4) + x + 4) + term.write(string.rep(" ", w - 13)) + end + + term.setCursorPos(3, i * 4 + 4) + if i == sel then + term.setBackgroundColor(bghigh) + term.write(" > ") + else term.write(" - ") end + term.write(v .. " ") + end + end + + local function updateDisplayList(items, loc, len) + local ret = {} + for i = 1, len do + local item = items[i + loc - 1] + if item then table.insert(ret, item) end + end + return ret + end + + -- Variables + local sel = 1 + local loc = 1 + local len = 3 + local disList = updateDisplayList(list, loc, len) + draw(disList, sel, loc) + + -- Loop + while true do + local e, key, x, y = os.pullEvent() + + if e == "mouse_click" then + for i, v in ipairs(disList) do + if x >= 3 and x <= w - 11 and y >= i * 4 + 3 and y <= i * 4 + 5 then return v end + end + elseif e == "key" and key == 200 then + if sel > 1 then + sel = sel - 1 + draw(disList, sel, loc) + elseif loc > 1 then + loc = loc - 1 + disList = updateDisplayList(list, loc, len) + draw(disList, sel, loc) + end + elseif e == "key" and key == 208 then + if sel < len then + sel = sel + 1 + draw(disList, sel, loc) + elseif loc + len - 1 < #list then + loc = loc + 1 + disList = updateDisplayList(list, loc, len) + draw(disList, sel, loc) + end + elseif e == "mouse_scroll" then + os.queueEvent("key", key == -1 and 200 or 208) + elseif e == "key" and key == 28 then + return disList[sel] + end + end +end + +function monitorKeyboardShortcuts() + local ta, tb = nil, nil + local allowChar = false + local shiftPressed = false + while true do + local event, char = os.pullEvent() + if event == "key" and (char == 42 or char == 52) then + shiftPressed = true + tb = os.startTimer(keyboardShortcutTimeout) + elseif event == "key" and (char == 29 or char == 157 or char == 219 or char == 220) then + allowEditorEvent = false + allowChar = true + ta = os.startTimer(keyboardShortcutTimeout) + elseif event == "key" and allowChar then + local name = nil + for k, v in pairs(keys) do + if v == char then + if shiftPressed then os.queueEvent("shortcut", "ctrl shift", k:lower()) + else os.queueEvent("shortcut", "ctrl", k:lower()) end + sleep(0.005) + allowEditorEvent = true + end + end + if shiftPressed then os.queueEvent("shortcut", "ctrl shift", char) + else os.queueEvent("shortcut", "ctrl", char) end + elseif event == "timer" and char == ta then + allowEditorEvent = true + allowChar = false + elseif event == "timer" and char == tb then + shiftPressed = false + end + end +end + + +-- -------- Saving and Loading + +local function download(url, path) + for i = 1, 3 do + local response = http.get(url) + if response then + local data = response.readAll() + response.close() + if path then + local f = io.open(path, "w") + f:write(data) + f:close() + end + return true + end + end + + return false +end + +local function saveFile(path, lines) + local dir = path:sub(1, path:len() - fs.getName(path):len()) + if not fs.exists(dir) then fs.makeDir(dir) end + if not fs.isDir(path) and not fs.isReadOnly(path) then + local a = "" + for _, v in pairs(lines) do a = a .. v .. "\n" end + + local f = io.open(path, "w") + f:write(a) + f:close() + return true + else return false end +end + +local function loadFile(path) + if not fs.exists(path) then + local dir = path:sub(1, path:len() - fs.getName(path):len()) + if not fs.exists(dir) then fs.makeDir(dir) end + local f = io.open(path, "w") + f:write("") + f:close() + end + + local l = {} + if fs.exists(path) and not fs.isDir(path) then + local f = io.open(path, "r") + if f then + local a = f:read("*l") + while a do + table.insert(l, a) + a = f:read("*l") + end + f:close() + end + else return nil end + + if #l < 1 then table.insert(l, "") end + return l +end + + +-- -------- Languages + +languages.lua = {} +languages.brainfuck = {} +languages.none = {} + +-- Lua + +languages.lua.helpTips = { + "A function you tried to call doesn't exist.", + "You made a typo.", + "The index of an array is nil.", + "The wrong variable type was passed.", + "A function/variable doesn't exist.", + "You missed an 'end'.", + "You missed a 'then'.", + "You declared a variable incorrectly.", + "One of your variables is mysteriously nil." +} + +languages.lua.defaultHelpTips = { + 2, 5 +} + +languages.lua.errors = { + ["Attempt to call nil."] = {1, 2}, + ["Attempt to index nil."] = {3, 2}, + [".+ expected, got .+"] = {4, 2, 9}, + ["'end' expected"] = {6, 2}, + ["'then' expected"] = {7, 2}, + ["'=' expected"] = {8, 2} +} + +languages.lua.keywords = { + ["and"] = "conditional", + ["break"] = "conditional", + ["do"] = "conditional", + ["else"] = "conditional", + ["elseif"] = "conditional", + ["end"] = "conditional", + ["for"] = "conditional", + ["function"] = "conditional", + ["if"] = "conditional", + ["in"] = "conditional", + ["local"] = "conditional", + ["not"] = "conditional", + ["or"] = "conditional", + ["repeat"] = "conditional", + ["return"] = "conditional", + ["then"] = "conditional", + ["until"] = "conditional", + ["while"] = "conditional", + + ["true"] = "constant", + ["false"] = "constant", + ["nil"] = "constant", + + ["print"] = "function", + ["write"] = "function", + ["sleep"] = "function", + ["pairs"] = "function", + ["ipairs"] = "function", + ["loadstring"] = "function", + ["loadfile"] = "function", + ["dofile"] = "function", + ["rawset"] = "function", + ["rawget"] = "function", + ["setfenv"] = "function", + ["getfenv"] = "function", +} + +languages.lua.parseError = function(e) + local ret = {filename = "unknown", line = -1, display = "Unknown!", err = ""} + if e and e ~= "" then + ret.err = e + if e:find(":") then + ret.filename = e:sub(1, e:find(":") - 1):gsub("^%s*(.-)%s*$", "%1") + -- The "" is needed to circumvent a CC bug + e = (e:sub(e:find(":") + 1) .. ""):gsub("^%s*(.-)%s*$", "%1") + if e:find(":") then + ret.line = e:sub(1, e:find(":") - 1) + e = e:sub(e:find(":") + 2):gsub("^%s*(.-)%s*$", "%1") .. "" + end + end + ret.display = e:sub(1, 1):upper() .. e:sub(2, -1) .. "." + end + + return ret +end + +languages.lua.getCompilerErrors = function(code) + code = "local function ee65da6af1cb6f63fee9a081246f2fd92b36ef2(...)\n\n" .. code .. "\n\nend" + local fn, err = loadstring(code) + if not err then + local _, e = pcall(fn) + if e then err = e end + end + + if err then + local a = err:find("]", 1, true) + if a then err = "string" .. err:sub(a + 1, -1) end + local ret = languages.lua.parseError(err) + if tonumber(ret.line) then ret.line = tonumber(ret.line) end + return ret + else return languages.lua.parseError(nil) end +end + +languages.lua.run = function(path, ar) + local fn, err = loadfile(path) + setfenv(fn, getfenv()) + if not err then + _, err = pcall(function() fn(unpack(ar)) end) + end + return err +end + + +-- Brainfuck + +languages.brainfuck.helpTips = { + "Well idk...", + "Isn't this the whole point of the language?", + "Ya know... Not being able to debug it?", + "You made a typo." +} + +languages.brainfuck.defaultHelpTips = { + 1, 2, 3 +} + +languages.brainfuck.errors = { + ["No matching '['"] = {1, 2, 3, 4} +} + +languages.brainfuck.keywords = {} + +languages.brainfuck.parseError = function(e) + local ret = {filename = "unknown", line = -1, display = "Unknown!", err = ""} + if e and e ~= "" then + ret.err = e + ret.line = e:sub(1, e:find(":") - 1) + e = e:sub(e:find(":") + 2):gsub("^%s*(.-)%s*$", "%1") .. "" + ret.display = e:sub(1, 1):upper() .. e:sub(2, -1) .. "." + end + + return ret +end + +languages.brainfuck.mapLoops = function(code) + -- Map loops + local loopLocations = {} + local loc = 1 + local line = 1 + for let in string.gmatch(code, ".") do + if let == "[" then + loopLocations[loc] = true + elseif let == "]" then + local found = false + for i = loc, 1, -1 do + if loopLocations[i] == true then + loopLocations[i] = loc + found = true + end + end + + if not found then + return line .. ": No matching '['" + end + end + + if let == "\n" then line = line + 1 end + loc = loc + 1 + end + return loopLocations +end + +languages.brainfuck.getCompilerErrors = function(code) + local a = languages.brainfuck.mapLoops(code) + if type(a) == "string" then return languages.brainfuck.parseError(a) + else return languages.brainfuck.parseError(nil) end +end + +languages.brainfuck.run = function(path) + -- Read from file + local f = io.open(path, "r") + local content = f:read("*a") + f:close() + + -- Define environment + local dataCells = {} + local dataPointer = 1 + local instructionPointer = 1 + + -- Map loops + local loopLocations = languages.brainfuck.mapLoops(content) + if type(loopLocations) == "string" then return loopLocations end + + -- Execute code + while true do + local let = content:sub(instructionPointer, instructionPointer) + + if let == ">" then + dataPointer = dataPointer + 1 + if not dataCells[tostring(dataPointer)] then dataCells[tostring(dataPointer)] = 0 end + elseif let == "<" then + if not dataCells[tostring(dataPointer)] then dataCells[tostring(dataPointer)] = 0 end + dataPointer = dataPointer - 1 + if not dataCells[tostring(dataPointer)] then dataCells[tostring(dataPointer)] = 0 end + elseif let == "+" then + if not dataCells[tostring(dataPointer)] then dataCells[tostring(dataPointer)] = 0 end + dataCells[tostring(dataPointer)] = dataCells[tostring(dataPointer)] + 1 + elseif let == "-" then + if not dataCells[tostring(dataPointer)] then dataCells[tostring(dataPointer)] = 0 end + dataCells[tostring(dataPointer)] = dataCells[tostring(dataPointer)] - 1 + elseif let == "." then + if not dataCells[tostring(dataPointer)] then dataCells[tostring(dataPointer)] = 0 end + if term.getCursorPos() >= w then print("") end + write(string.char(math.max(1, dataCells[tostring(dataPointer)]))) + elseif let == "," then + if not dataCells[tostring(dataPointer)] then dataCells[tostring(dataPointer)] = 0 end + term.setCursorBlink(true) + local e, but = os.pullEvent("char") + term.setCursorBlink(false) + dataCells[tostring(dataPointer)] = string.byte(but) + if term.getCursorPos() >= w then print("") end + write(but) + elseif let == "/" then + if not dataCells[tostring(dataPointer)] then dataCells[tostring(dataPointer)] = 0 end + if term.getCursorPos() >= w then print("") end + write(dataCells[tostring(dataPointer)]) + elseif let == "[" then + if dataCells[tostring(dataPointer)] == 0 then + for k, v in pairs(loopLocations) do + if k == instructionPointer then instructionPointer = v end + end + end + elseif let == "]" then + for k, v in pairs(loopLocations) do + if v == instructionPointer then instructionPointer = k - 1 end + end + end + + instructionPointer = instructionPointer + 1 + if instructionPointer > content:len() then print("") break end + end +end + +-- None + +languages.none.helpTips = {} +languages.none.defaultHelpTips = {} +languages.none.errors = {} +languages.none.keywords = {} + +languages.none.parseError = function(err) + return {filename = "", line = -1, display = "", err = ""} +end + +languages.none.getCompilerErrors = function(code) + return languages.none.parseError(nil) +end + +languages.none.run = function(path) end + + +-- Load language +currentLanguage = languages.lua + + +-- -------- Run GUI + +local function viewErrorHelp(e) + title("LuaIDE - Error Help") + + local tips = nil + for k, v in pairs(currentLanguage.errors) do + if e.display:find(k) then tips = v break end + end + + term.setBackgroundColor(colors[theme.err]) + for i = 6, 8 do + term.setCursorPos(5, i) + term.write(string.rep(" ", 35)) + end + + term.setBackgroundColor(colors[theme.prompt]) + for i = 10, 18 do + term.setCursorPos(5, i) + term.write(string.rep(" ", 46)) + end + + if tips then + term.setBackgroundColor(colors[theme.err]) + term.setCursorPos(6, 7) + term.write("Error Help") + + term.setBackgroundColor(colors[theme.prompt]) + for i, v in ipairs(tips) do + term.setCursorPos(7, i + 10) + term.write("- " .. currentLanguage.helpTips[v]) + end + else + term.setBackgroundColor(colors[theme.err]) + term.setCursorPos(6, 7) + term.write("No Error Tips Available!") + + term.setBackgroundColor(colors[theme.prompt]) + term.setCursorPos(6, 11) + term.write("There are no error tips available, but") + term.setCursorPos(6, 12) + term.write("you could see if it was any of these:") + + for i, v in ipairs(currentLanguage.defaultHelpTips) do + term.setCursorPos(7, i + 12) + term.write("- " .. currentLanguage.helpTips[v]) + end + end + + prompt({{"Back", w - 8, 7}}, "horizontal") +end + +local function run(path, lines, useArgs) + local ar = {} + if useArgs then + title("LuaIDE - Run " .. fs.getName(path)) + local s = centerRead(w - 13, fs.getName(path) .. " ") + for m in string.gmatch(s, "[^ \t]+") do ar[#ar + 1] = m:gsub("^%s*(.-)%s*$", "%1") end + end + + saveFile(path, lines) + term.setCursorBlink(false) + term.setBackgroundColor(colors.black) + term.setTextColor(colors.white) + term.clear() + term.setCursorPos(1, 1) + local err = currentLanguage.run(path, ar) + + term.setBackgroundColor(colors.black) + print("\n") + if err then + if isAdvanced() then term.setTextColor(colors.red) end + centerPrint("The program has crashed!") + end + term.setTextColor(colors.white) + centerPrint("Press any key to return to LuaIDE...") + while true do + local e = os.pullEvent() + if e == "key" then break end + end + + -- To prevent key from showing up in editor + os.queueEvent("") + os.pullEvent() + + if err then + if currentLanguage == languages.lua and err:find("]") then + err = fs.getName(path) .. err:sub(err:find("]", 1, true) + 1, -1) + end + + while true do + title("LuaIDE - Error!") + + term.setBackgroundColor(colors[theme.err]) + for i = 6, 8 do + term.setCursorPos(3, i) + term.write(string.rep(" ", w - 5)) + end + term.setCursorPos(4, 7) + term.write("The program has crashed!") + + term.setBackgroundColor(colors[theme.prompt]) + for i = 10, 14 do + term.setCursorPos(3, i) + term.write(string.rep(" ", w - 5)) + end + + local formattedErr = currentLanguage.parseError(err) + term.setCursorPos(4, 11) + term.write("Line: " .. formattedErr.line) + term.setCursorPos(4, 12) + term.write("Error:") + term.setCursorPos(5, 13) + + local a = formattedErr.display + local b = nil + if a:len() > w - 8 then + for i = a:len(), 1, -1 do + if a:sub(i, i) == " " then + b = a:sub(i + 1, -1) + a = a:sub(1, i) + break + end + end + end + + term.write(a) + if b then + term.setCursorPos(5, 14) + term.write(b) + end + + local opt = prompt({{"Error Help", w/2 - 15, 17}, {"Go To Line", w/2 + 2, 17}}, + "horizontal") + if opt == "Error Help" then + viewErrorHelp(formattedErr) + elseif opt == "Go To Line" then + -- To prevent key from showing up in editor + os.queueEvent("") + os.pullEvent() + + return "go to", tonumber(formattedErr.line) + end + end + end +end + + +-- -------- Functions + +local function goto() + term.setBackgroundColor(colors[theme.backgroundHighlight]) + term.setCursorPos(2, 1) + term.clearLine() + term.write("Line: ") + local line = modRead({visibleLength = w - 2}) + + local num = tonumber(line) + if num and num > 0 then return num + else + term.setCursorPos(2, 1) + term.clearLine() + term.write("Not a line number!") + sleep(1.6) + return nil + end +end + +local function setsyntax() + local opts = { + "[Lua] Brainfuck None ", + " Lua [Brainfuck] None ", + " Lua Brainfuck [None]" + } + local sel = 1 + + term.setCursorBlink(false) + term.setBackgroundColor(colors[theme.backgroundHighlight]) + term.setCursorPos(2, 1) + term.clearLine() + term.write(opts[sel]) + while true do + local e, but, x, y = os.pullEvent("key") + if but == 203 then + sel = math.max(1, sel - 1) + term.setCursorPos(2, 1) + term.clearLine() + term.write(opts[sel]) + elseif but == 205 then + sel = math.min(#opts, sel + 1) + term.setCursorPos(2, 1) + term.clearLine() + term.write(opts[sel]) + elseif but == 28 then + if sel == 1 then currentLanguage = languages.lua + elseif sel == 2 then currentLanguage = languages.brainfuck + elseif sel == 3 then currentLanguage = languages.none end + term.setCursorBlink(true) + return + end + end +end + + +-- -------- Re-Indenting + +local tabWidth = 2 + +local comments = {} +local strings = {} + +local increment = { + "if%s+.+%s+then%s*$", + "for%s+.+%s+do%s*$", + "while%s+.+%s+do%s*$", + "repeat%s*$", + "function%s+[a-zA-Z_0-9]\(.*\)%s*$" +} + +local decrement = { + "end", + "until%s+.+" +} + +local special = { + "else%s*$", + "elseif%s+.+%s+then%s*$" +} + +local function check(func) + for _, v in pairs(func) do + local cLineStart = v["lineStart"] + local cLineEnd = v["lineEnd"] + local cCharStart = v["charStart"] + local cCharEnd = v["charEnd"] + + if line >= cLineStart and line <= cLineEnd then + if line == cLineStart then return cCharStart < charNumb + elseif line == cLineEnd then return cCharEnd > charNumb + else return true end + end + end +end + +local function isIn(line, loc) + if check(comments) then return true end + if check(strings) then return true end + return false +end + +local function setComment(ls, le, cs, ce) + comments[#comments + 1] = {} + comments[#comments].lineStart = ls + comments[#comments].lineEnd = le + comments[#comments].charStart = cs + comments[#comments].charEnd = ce +end + +local function setString(ls, le, cs, ce) + strings[#strings + 1] = {} + strings[#strings].lineStart = ls + strings[#strings].lineEnd = le + strings[#strings].charStart = cs + strings[#strings].charEnd = ce +end + +local function map(contents) + local inCom = false + local inStr = false + + for i = 1, #contents do + if content[i]:find("%-%-%[%[") and not inStr and not inCom then + local cStart = content[i]:find("%-%-%[%[") + setComment(i, nil, cStart, nil) + inCom = true + elseif content[i]:find("%-%-%[=%[") and not inStr and not inCom then + local cStart = content[i]:find("%-%-%[=%[") + setComment(i, nil, cStart, nil) + inCom = true + elseif content[i]:find("%[%[") and not inStr and not inCom then + local cStart = content[i]:find("%[%[") + setString(i, nil, cStart, nil) + inStr = true + elseif content[i]:find("%[=%[") and not inStr and not inCom then + local cStart = content[i]:find("%[=%[") + setString(i, nil, cStart, nil) + inStr = true + end + + if content[i]:find("%]%]") and inStr and not inCom then + local cStart, cEnd = content[i]:find("%]%]") + strings[#strings].lineEnd = i + strings[#strings].charEnd = cEnd + inStr = false + elseif content[i]:find("%]=%]") and inStr and not inCom then + local cStart, cEnd = content[i]:find("%]=%]") + strings[#strings].lineEnd = i + strings[#strings].charEnd = cEnd + inStr = false + end + + if content[i]:find("%]%]") and not inStr and inCom then + local cStart, cEnd = content[i]:find("%]%]") + comments[#comments].lineEnd = i + comments[#comments].charEnd = cEnd + inCom = false + elseif content[i]:find("%]=%]") and not inStr and inCom then + local cStart, cEnd = content[i]:find("%]=%]") + comments[#comments].lineEnd = i + comments[#comments].charEnd = cEnd + inCom = false + end + + if content[i]:find("%-%-") and not inStr and not inCom then + local cStart = content[i]:find("%-%-") + setComment(i, i, cStart, -1) + elseif content[i]:find("'") and not inStr and not inCom then + local cStart, cEnd = content[i]:find("'") + local nextChar = content[i]:sub(cEnd + 1, string.len(content[i])) + local _, cEnd = nextChar:find("'") + setString(i, i, cStart, cEnd) + elseif content[i]:find('"') and not inStr and not inCom then + local cStart, cEnd = content[i]:find('"') + local nextChar = content[i]:sub(cEnd + 1, string.len(content[i])) + local _, cEnd = nextChar:find('"') + setString(i, i, cStart, cEnd) + end + end +end + +local function reindent(contents) + local err = nil + if currentLanguage ~= languages.lua then + err = "Cannot indent languages other than Lua!" + elseif currentLanguage.getCompilerErrors(table.concat(contents, "\n")).line ~= -1 then + err = "Cannot indent a program with errors!" + end + + if err then + term.setCursorBlink(false) + term.setCursorPos(2, 1) + term.setBackgroundColor(colors[theme.backgroundHighlight]) + term.clearLine() + term.write(err) + sleep(1.6) + return contents + end + + local new = {} + local level = 0 + for k, v in pairs(contents) do + local incrLevel = false + local foundIncr = false + for _, incr in pairs(increment) do + if v:find(incr) and not isIn(k, v:find(incr)) then + incrLevel = true + end + if v:find(incr:sub(1, -2)) and not isIn(k, v:find(incr)) then + foundIncr = true + end + end + + local decrLevel = false + if not incrLevel then + for _, decr in pairs(decrement) do + if v:find(decr) and not isIn(k, v:find(decr)) and not foundIncr then + level = math.max(0, level - 1) + decrLevel = true + end + end + end + + if not decrLevel then + for _, sp in pairs(special) do + if v:find(sp) and not isIn(k, v:find(sp)) then + incrLevel = true + level = math.max(0, level - 1) + end + end + end + + new[k] = string.rep(" ", level * tabWidth) .. v + if incrLevel then level = level + 1 end + end + + return new +end + + +-- -------- Menu + +local menu = { + [1] = {"File", +-- "About", +-- "Settings", +-- "", + "New File ^+N", + "Open File ^+O", + "Save File ^+S", + "Close ^+W", + "Print ^+P", + "Quit ^+Q" + }, [2] = {"Edit", + "Cut Line ^+X", + "Copy Line ^+C", + "Paste Line ^+V", + "Delete Line", + "Clear Line" + }, [3] = {"Functions", + "Go To Line ^+G", + "Re-Indent ^+I", + "Set Syntax ^+E", + "Start of Line ^+<", + "End of Line ^+>" + }, [4] = {"Run", + "Run Program ^+R", + "Run w/ Args ^+Shift+R" + } +} + +local shortcuts = { + -- File + ["ctrl n"] = "New File ^+N", + ["ctrl o"] = "Open File ^+O", + ["ctrl s"] = "Save File ^+S", + ["ctrl w"] = "Close ^+W", + ["ctrl p"] = "Print ^+P", + ["ctrl q"] = "Quit ^+Q", + + -- Edit + ["ctrl x"] = "Cut Line ^+X", + ["ctrl c"] = "Copy Line ^+C", + ["ctrl v"] = "Paste Line ^+V", + + -- Functions + ["ctrl g"] = "Go To Line ^+G", + ["ctrl i"] = "Re-Indent ^+I", + ["ctrl e"] = "Set Syntax ^+E", + ["ctrl 203"] = "Start of Line ^+<", + ["ctrl 205"] = "End of Line ^+>", + + -- Run + ["ctrl r"] = "Run Program ^+R", + ["ctrl shift r"] = "Run w/ Args ^+Shift+R" +} + +local menuFunctions = { + -- File +-- ["About"] = function() end, +-- ["Settings"] = function() end, + ["New File ^+N"] = function(path, lines) saveFile(path, lines) return "new" end, + ["Open File ^+O"] = function(path, lines) saveFile(path, lines) return "open" end, + ["Save File ^+S"] = function(path, lines) saveFile(path, lines) end, + ["Close ^+W"] = function(path, lines) saveFile(path, lines) return "menu" end, + ["Print ^+P"] = function(path, lines) saveFile(path, lines) return nil end, + ["Quit ^+Q"] = function(path, lines) saveFile(path, lines) return "exit" end, + + -- Edit + ["Cut Line ^+X"] = function(path, lines, y) + clipboard = lines[y] table.remove(lines, y) return nil, lines end, + ["Copy Line ^+C"] = function(path, lines, y) clipboard = lines[y] end, + ["Paste Line ^+V"] = function(path, lines, y) + if clipboard then table.insert(lines, y, clipboard) end return nil, lines end, + ["Delete Line"] = function(path, lines, y) table.remove(lines, y) return nil, lines end, + ["Clear Line"] = function(path, lines, y) lines[y] = "" return nil, lines, "cursor" end, + + -- Functions + ["Go To Line ^+G"] = function() return nil, "go to", goto() end, + ["Re-Indent ^+I"] = function(path, lines) + local a = reindent(lines) saveFile(path, lines) return nil, a + end, + ["Set Syntax ^+E"] = function(path, lines) + setsyntax() + if currentLanguage == languages.brainfuck and lines[1] ~= "-- Syntax: Brainfuck" then + table.insert(lines, 1, "-- Syntax: Brainfuck") + return nil, lines + end + end, + ["Start of Line ^+<"] = function() os.queueEvent("key", 199) end, + ["End of Line ^+>"] = function() os.queueEvent("key", 207) end, + + -- Run + ["Run Program ^+R"] = function(path, lines) + saveFile(path, lines) + return nil, run(path, lines, false) + end, + ["Run w/ Args ^+Shift+R"] = function(path, lines) + saveFile(path, lines) + return nil, run(path, lines, true) + end, +} + +local function drawMenu(open) + term.setCursorPos(1, 1) + term.setTextColor(colors[theme.textColor]) + term.setBackgroundColor(colors[theme.backgroundHighlight]) + term.clearLine() + local curX = 0 + for _, v in pairs(menu) do + term.setCursorPos(3 + curX, 1) + term.write(v[1]) + curX = curX + v[1]:len() + 3 + end + + if open then + local it = {} + local x = 1 + for _, v in pairs(menu) do + if open == v[1] then + it = v + break + end + x = x + v[1]:len() + 3 + end + x = x + 1 + + local items = {} + for i = 2, #it do + table.insert(items, it[i]) + end + + local len = 1 + for _, v in pairs(items) do if v:len() + 2 > len then len = v:len() + 2 end end + + for i, v in ipairs(items) do + term.setCursorPos(x, i + 1) + term.write(string.rep(" ", len)) + term.setCursorPos(x + 1, i + 1) + term.write(v) + end + term.setCursorPos(x, #items + 2) + term.write(string.rep(" ", len)) + return items, len + end +end + +local function triggerMenu(cx, cy) + -- Determine clicked menu + local curX = 0 + local open = nil + for _, v in pairs(menu) do + if cx >= curX + 3 and cx <= curX + v[1]:len() + 2 then + open = v[1] + break + end + curX = curX + v[1]:len() + 3 + end + local menux = curX + 2 + if not open then return false end + + -- Flash menu item + term.setCursorBlink(false) + term.setCursorPos(menux, 1) + term.setBackgroundColor(colors[theme.background]) + term.write(string.rep(" ", open:len() + 2)) + term.setCursorPos(menux + 1, 1) + term.write(open) + sleep(0.1) + local items, len = drawMenu(open) + + local ret = true + + -- Pull events on menu + local ox, oy = term.getCursorPos() + while type(ret) ~= "string" do + local e, but, x, y = os.pullEvent() + if e == "mouse_click" then + -- If clicked outside menu + if x < menux - 1 or x > menux + len - 1 then break + elseif y > #items + 2 then break + elseif y == 1 then break end + + for i, v in ipairs(items) do + if y == i + 1 and x >= menux and x <= menux + len - 2 then + -- Flash when clicked + term.setCursorPos(menux, y) + term.setBackgroundColor(colors[theme.background]) + term.write(string.rep(" ", len)) + term.setCursorPos(menux + 1, y) + term.write(v) + sleep(0.1) + drawMenu(open) + + -- Return item + ret = v + break + end + end + end + end + + term.setCursorPos(ox, oy) + term.setCursorBlink(true) + return ret +end + + +-- -------- Editing + +local standardsCompletions = { + "if%s+.+%s+then%s*$", + "for%s+.+%s+do%s*$", + "while%s+.+%s+do%s*$", + "repeat%s*$", + "function%s+[a-zA-Z_0-9]?\(.*\)%s*$", + "=%s*function%s*\(.*\)%s*$", + "else%s*$", + "elseif%s+.+%s+then%s*$" +} + +local liveCompletions = { + ["("] = ")", + ["{"] = "}", + ["["] = "]", + ["\""] = "\"", + ["'"] = "'", +} + +local x, y = 0, 0 +local edw, edh = 0, h - 1 +local offx, offy = 0, 1 +local scrollx, scrolly = 0, 0 +local lines = {} +local liveErr = currentLanguage.parseError(nil) +local displayCode = true +local lastEventClock = os.clock() + +local function attemptToHighlight(line, regex, col) + local match = string.match(line, regex) + if match then + if type(col) == "number" then term.setTextColor(col) + elseif type(col) == "function" then term.setTextColor(col(match)) end + term.write(match) + term.setTextColor(colors[theme.textColor]) + return line:sub(match:len() + 1, -1) + end + return nil +end + +local function writeHighlighted(line) + if currentLanguage == languages.lua then + while line:len() > 0 do + line = attemptToHighlight(line, "^%-%-%[%[.-%]%]", colors[theme.comment]) or + attemptToHighlight(line, "^%-%-.*", colors[theme.comment]) or + attemptToHighlight(line, "^\".*[^\\]\"", colors[theme.string]) or + attemptToHighlight(line, "^\'.*[^\\]\'", colors[theme.string]) or + attemptToHighlight(line, "^%[%[.-%]%]", colors[theme.string]) or + attemptToHighlight(line, "^[%w_]+", function(match) + if currentLanguage.keywords[match] then + return colors[theme[currentLanguage.keywords[match]]] + end + return colors[theme.textColor] + end) or + attemptToHighlight(line, "^[^%w_]", colors[theme.textColor]) + end + else term.write(line) end +end + +local function draw() + -- Menu + term.setTextColor(colors[theme.textColor]) + term.setBackgroundColor(colors[theme.editorBackground]) + term.clear() + drawMenu() + + -- Line numbers + offx, offy = tostring(#lines):len() + 1, 1 + edw, edh = w - offx, h - 1 + + -- Draw text + for i = 1, edh do + local a = lines[scrolly + i] + if a then + local ln = string.rep(" ", offx - 1 - tostring(scrolly + i):len()) .. tostring(scrolly + i) + local l = a:sub(scrollx + 1, edw + scrollx + 1) + ln = ln .. ":" + + if liveErr.line == scrolly + i then ln = string.rep(" ", offx - 2) .. "!:" end + + term.setCursorPos(1, i + offy) + term.setBackgroundColor(colors[theme.editorBackground]) + if scrolly + i == y then + if scrolly + i == liveErr.line and os.clock() - lastEventClock > 3 then + term.setBackgroundColor(colors[theme.editorErrorHighlight]) + else term.setBackgroundColor(colors[theme.editorLineHightlight]) end + term.clearLine() + elseif scrolly + i == liveErr.line then + term.setBackgroundColor(colors[theme.editorError]) + term.clearLine() + end + + term.setCursorPos(1 - scrollx + offx, i + offy) + if scrolly + i == y then + if scrolly + i == liveErr.line and os.clock() - lastEventClock > 3 then + term.setBackgroundColor(colors[theme.editorErrorHighlight]) + else term.setBackgroundColor(colors[theme.editorLineHightlight]) end + elseif scrolly + i == liveErr.line then term.setBackgroundColor(colors[theme.editorError]) + else term.setBackgroundColor(colors[theme.editorBackground]) end + if scrolly + i == liveErr.line then + if displayCode then term.write(a) + else term.write(liveErr.display) end + else writeHighlighted(a) end + + term.setCursorPos(1, i + offy) + if scrolly + i == y then + if scrolly + i == liveErr.line and os.clock() - lastEventClock > 3 then + term.setBackgroundColor(colors[theme.editorError]) + else term.setBackgroundColor(colors[theme.editorLineNumbersHighlight]) end + elseif scrolly + i == liveErr.line then + term.setBackgroundColor(colors[theme.editorErrorHighlight]) + else term.setBackgroundColor(colors[theme.editorLineNumbers]) end + term.write(ln) + end + end + term.setCursorPos(x - scrollx + offx, y - scrolly + offy) +end + +local function drawLine(...) + local ls = {...} + offx = tostring(#lines):len() + 1 + for _, ly in pairs(ls) do + local a = lines[ly] + if a then + local ln = string.rep(" ", offx - 1 - tostring(ly):len()) .. tostring(ly) + local l = a:sub(scrollx + 1, edw + scrollx + 1) + ln = ln .. ":" + + if liveErr.line == ly then ln = string.rep(" ", offx - 2) .. "!:" end + + term.setCursorPos(1, (ly - scrolly) + offy) + term.setBackgroundColor(colors[theme.editorBackground]) + if ly == y then + if ly == liveErr.line and os.clock() - lastEventClock > 3 then + term.setBackgroundColor(colors[theme.editorErrorHighlight]) + else term.setBackgroundColor(colors[theme.editorLineHightlight]) end + elseif ly == liveErr.line then + term.setBackgroundColor(colors[theme.editorError]) + end + term.clearLine() + + term.setCursorPos(1 - scrollx + offx, (ly - scrolly) + offy) + if ly == y then + if ly == liveErr.line and os.clock() - lastEventClock > 3 then + term.setBackgroundColor(colors[theme.editorErrorHighlight]) + else term.setBackgroundColor(colors[theme.editorLineHightlight]) end + elseif ly == liveErr.line then term.setBackgroundColor(colors[theme.editorError]) + else term.setBackgroundColor(colors[theme.editorBackground]) end + if ly == liveErr.line then + if displayCode then term.write(a) + else term.write(liveErr.display) end + else writeHighlighted(a) end + + term.setCursorPos(1, (ly - scrolly) + offy) + if ly == y then + if ly == liveErr.line and os.clock() - lastEventClock > 3 then + term.setBackgroundColor(colors[theme.editorError]) + else term.setBackgroundColor(colors[theme.editorLineNumbersHighlight]) end + elseif ly == liveErr.line then + term.setBackgroundColor(colors[theme.editorErrorHighlight]) + else term.setBackgroundColor(colors[theme.editorLineNumbers]) end + term.write(ln) + end + end + term.setCursorPos(x - scrollx + offx, y - scrolly + offy) +end + +local function cursorLoc(x, y, force) + local sx, sy = x - scrollx, y - scrolly + local redraw = false + if sx < 1 then + scrollx = x - 1 + sx = 1 + redraw = true + elseif sx > edw then + scrollx = x - edw + sx = edw + redraw = true + end if sy < 1 then + scrolly = y - 1 + sy = 1 + redraw = true + elseif sy > edh then + scrolly = y - edh + sy = edh + redraw = true + end if redraw or force then draw() end + term.setCursorPos(sx + offx, sy + offy) +end + +local function executeMenuItem(a, path) + if type(a) == "string" and menuFunctions[a] then + local opt, nl, gtln = menuFunctions[a](path, lines, y) + if type(opt) == "string" then term.setCursorBlink(false) return opt end + if type(nl) == "table" then + if #lines < 1 then table.insert(lines, "") end + y = math.min(y, #lines) + x = math.min(x, lines[y]:len() + 1) + lines = nl + elseif type(nl) == "string" then + if nl == "go to" and gtln then + x, y = 1, math.min(#lines, gtln) + cursorLoc(x, y) + end + end + end + term.setCursorBlink(true) + draw() + term.setCursorPos(x - scrollx + offx, y - scrolly + offy) +end + +local function edit(path) + -- Variables + x, y = 1, 1 + offx, offy = 0, 1 + scrollx, scrolly = 0, 0 + lines = loadFile(path) + if not lines then return "menu" end + + -- Enable brainfuck + if lines[1] == "-- Syntax: Brainfuck" then + currentLanguage = languages.brainfuck + end + + -- Clocks + local autosaveClock = os.clock() + local scrollClock = os.clock() -- To prevent redraw flicker + local liveErrorClock = os.clock() + local hasScrolled = false + + -- Draw + draw() + term.setCursorPos(x + offx, y + offy) + term.setCursorBlink(true) + + -- Main loop + local tid = os.startTimer(3) + while true do + local e, key, cx, cy = os.pullEvent() + if e == "key" and allowEditorEvent then + if key == 200 and y > 1 then + -- Up + x, y = math.min(x, lines[y - 1]:len() + 1), y - 1 + drawLine(y, y + 1) + cursorLoc(x, y) + elseif key == 208 and y < #lines then + -- Down + x, y = math.min(x, lines[y + 1]:len() + 1), y + 1 + drawLine(y, y - 1) + cursorLoc(x, y) + elseif key == 203 and x > 1 then + -- Left + x = x - 1 + local force = false + if y - scrolly + offy < offy + 1 then force = true end + cursorLoc(x, y, force) + elseif key == 205 and x < lines[y]:len() + 1 then + -- Right + x = x + 1 + local force = false + if y - scrolly + offy < offy + 1 then force = true end + cursorLoc(x, y, force) + elseif (key == 28 or key == 156) and (displayCode and true or y + scrolly - 1 == + liveErr.line) then + -- Enter + local f = nil + for _, v in pairs(standardsCompletions) do + if lines[y]:find(v) and x == #lines[y] + 1 then f = v end + end + + local _, spaces = lines[y]:find("^[ ]+") + if not spaces then spaces = 0 end + if f then + table.insert(lines, y + 1, string.rep(" ", spaces + 2)) + if not f:find("else", 1, true) and not f:find("elseif", 1, true) then + table.insert(lines, y + 2, string.rep(" ", spaces) .. + (f:find("repeat", 1, true) and "until " or f:find("{", 1, true) and "}" or + "end")) + end + x, y = spaces + 3, y + 1 + cursorLoc(x, y, true) + else + local oldLine = lines[y] + + lines[y] = lines[y]:sub(1, x - 1) + table.insert(lines, y + 1, string.rep(" ", spaces) .. oldLine:sub(x, -1)) + + x, y = spaces + 1, y + 1 + cursorLoc(x, y, true) + end + elseif key == 14 and (displayCode and true or y + scrolly - 1 == liveErr.line) then + -- Backspace + if x > 1 then + local f = false + for k, v in pairs(liveCompletions) do + if lines[y]:sub(x - 1, x - 1) == k then f = true end + end + + lines[y] = lines[y]:sub(1, x - 2) .. lines[y]:sub(x + (f and 1 or 0), -1) + drawLine(y) + x = x - 1 + cursorLoc(x, y) + elseif y > 1 then + local prevLen = lines[y - 1]:len() + 1 + lines[y - 1] = lines[y - 1] .. lines[y] + table.remove(lines, y) + x, y = prevLen, y - 1 + cursorLoc(x, y, true) + end + elseif key == 199 then + -- Home + x = 1 + local force = false + if y - scrolly + offy < offy + 1 then force = true end + cursorLoc(x, y, force) + elseif key == 207 then + -- End + x = lines[y]:len() + 1 + local force = false + if y - scrolly + offy < offy + 1 then force = true end + cursorLoc(x, y, force) + elseif key == 211 and (displayCode and true or y + scrolly - 1 == liveErr.line) then + -- Forward Delete + if x < lines[y]:len() + 1 then + lines[y] = lines[y]:sub(1, x - 1) .. lines[y]:sub(x + 1) + local force = false + if y - scrolly + offy < offy + 1 then force = true end + drawLine(y) + cursorLoc(x, y, force) + elseif y < #lines then + lines[y] = lines[y] .. lines[y + 1] + table.remove(lines, y + 1) + draw() + cursorLoc(x, y) + end + elseif key == 15 and (displayCode and true or y + scrolly - 1 == liveErr.line) then + -- Tab + lines[y] = string.rep(" ", tabWidth) .. lines[y] + x = x + 2 + local force = false + if y - scrolly + offy < offy + 1 then force = true end + drawLine(y) + cursorLoc(x, y, force) + elseif key == 201 then + -- Page up + y = math.min(math.max(y - edh, 1), #lines) + x = math.min(lines[y]:len() + 1, x) + cursorLoc(x, y, true) + elseif key == 209 then + -- Page down + y = math.min(math.max(y + edh, 1), #lines) + x = math.min(lines[y]:len() + 1, x) + cursorLoc(x, y, true) + end + elseif e == "char" and allowEditorEvent and (displayCode and true or + y + scrolly - 1 == liveErr.line) then + local shouldIgnore = false + for k, v in pairs(liveCompletions) do + if key == v and lines[y]:find(k, 1, true) and lines[y]:sub(x, x) == v then + shouldIgnore = true + end + end + + local addOne = false + if not shouldIgnore then + for k, v in pairs(liveCompletions) do + if key == k and lines[y]:sub(x, x) ~= k then key = key .. v addOne = true end + end + lines[y] = lines[y]:sub(1, x - 1) .. key .. lines[y]:sub(x, -1) + end + + x = x + (addOne and 1 or key:len()) + local force = false + if y - scrolly + offy < offy + 1 then force = true end + drawLine(y) + cursorLoc(x, y, force) + elseif e == "mouse_click" and key == 1 then + if cy > 1 then + if cx <= offx and cy - offy == liveErr.line - scrolly then + displayCode = not displayCode + drawLine(liveErr.line) + else + local oldy = y + y = math.min(math.max(scrolly + cy - offy, 1), #lines) + x = math.min(math.max(scrollx + cx - offx, 1), lines[y]:len() + 1) + if oldy ~= y then drawLine(oldy, y) end + cursorLoc(x, y) + end + else + local a = triggerMenu(cx, cy) + if a then + local opt = executeMenuItem(a, path) + if opt then return opt end + end + end + elseif e == "shortcut" then + local a = shortcuts[key .. " " .. cx] + if a then + local parent = nil + local curx = 0 + for i, mv in ipairs(menu) do + for _, iv in pairs(mv) do + if iv == a then + parent = menu[i][1] + break + end + end + if parent then break end + curx = curx + mv[1]:len() + 3 + end + local menux = curx + 2 + + -- Flash menu item + term.setCursorBlink(false) + term.setCursorPos(menux, 1) + term.setBackgroundColor(colors[theme.background]) + term.write(string.rep(" ", parent:len() + 2)) + term.setCursorPos(menux + 1, 1) + term.write(parent) + sleep(0.1) + drawMenu() + + -- Execute item + local opt = executeMenuItem(a, path) + if opt then return opt end + end + elseif e == "mouse_scroll" then + if key == -1 and scrolly > 0 then + scrolly = scrolly - 1 + if os.clock() - scrollClock > 0.0005 then + draw() + term.setCursorPos(x - scrollx + offx, y - scrolly + offy) + end + scrollClock = os.clock() + hasScrolled = true + elseif key == 1 and scrolly < #lines - edh then + scrolly = scrolly + 1 + if os.clock() - scrollClock > 0.0005 then + draw() + term.setCursorPos(x - scrollx + offx, y - scrolly + offy) + end + scrollClock = os.clock() + hasScrolled = true + end + elseif e == "timer" and key == tid then + drawLine(y) + tid = os.startTimer(3) + end + + -- Draw + if hasScrolled and os.clock() - scrollClock > 0.1 then + draw() + term.setCursorPos(x - scrollx + offx, y - scrolly + offy) + hasScrolled = false + end + + -- Autosave + if os.clock() - autosaveClock > autosaveInterval then + saveFile(path, lines) + autosaveClock = os.clock() + end + + -- Errors + if os.clock() - liveErrorClock > 1 then + local prevLiveErr = liveErr + liveErr = currentLanguage.parseError(nil) + local code = "" + for _, v in pairs(lines) do code = code .. v .. "\n" end + + liveErr = currentLanguage.getCompilerErrors(code) + liveErr.line = math.min(liveErr.line - 2, #lines) + if liveErr ~= prevLiveErr then draw() end + liveErrorClock = os.clock() + end + end + + return "menu" +end + + +-- -------- Open File + +local function newFile() + local wid = w - 13 + + -- Get name + title("Lua IDE - New File") + local name = centerRead(wid, "/") + if not name or name == "" then return "menu" end + name = "/" .. name + + -- Clear + title("Lua IDE - New File") + term.setTextColor(colors[theme.textColor]) + term.setBackgroundColor(colors[theme.promptHighlight]) + for i = 8, 10 do + term.setCursorPos(w/2 - wid/2, i) + term.write(string.rep(" ", wid)) + end + term.setCursorPos(1, 9) + if fs.isDir(name) then + centerPrint("Cannot Edit a Directory!") + sleep(1.6) + return "menu" + elseif fs.exists(name) then + centerPrint("File Already Exists!") + local opt = prompt({{"Open", w/2 - 9, 14}, {"Cancel", w/2 + 2, 14}}, "horizontal") + if opt == "Open" then return "edit", name + elseif opt == "Cancel" then return "menu" end + else return "edit", name end +end + +local function openFile() + local wid = w - 13 + + -- Get name + title("Lua IDE - Open File") + local name = centerRead(wid, "/") + if not name or name == "" then return "menu" end + name = "/" .. name + + -- Clear + title("Lua IDE - New File") + term.setTextColor(colors[theme.textColor]) + term.setBackgroundColor(colors[theme.promptHighlight]) + for i = 8, 10 do + term.setCursorPos(w/2 - wid/2, i) + term.write(string.rep(" ", wid)) + end + term.setCursorPos(1, 9) + if fs.isDir(name) then + centerPrint("Cannot Open a Directory!") + sleep(1.6) + return "menu" + elseif not fs.exists(name) then + centerPrint("File Doesn't Exist!") + local opt = prompt({{"Create", w/2 - 11, 14}, {"Cancel", w/2 + 2, 14}}, "horizontal") + if opt == "Create" then return "edit", name + elseif opt == "Cancel" then return "menu" end + else return "edit", name end +end + + +-- -------- Settings + +local function update() + local function draw(status) + title("LuaIDE - Update") + term.setBackgroundColor(colors[theme.prompt]) + term.setTextColor(colors[theme.textColor]) + for i = 8, 10 do + term.setCursorPos(w/2 - (status:len() + 4), i) + write(string.rep(" ", status:len() + 4)) + end + term.setCursorPos(w/2 - (status:len() + 4), 9) + term.write(" - " .. status .. " ") + + term.setBackgroundColor(colors[theme.errHighlight]) + for i = 8, 10 do + term.setCursorPos(w/2 + 2, i) + term.write(string.rep(" ", 10)) + end + term.setCursorPos(w/2 + 2, 9) + term.write(" > Cancel ") + end + + if not http then + draw("HTTP API Disabled!") + sleep(1.6) + return "settings" + end + + draw("Updating...") + local tID = os.startTimer(10) + http.request(updateURL) + while true do + local e, but, x, y = os.pullEvent() + if (e == "key" and but == 28) or + (e == "mouse_click" and x >= w/2 + 2 and x <= w/2 + 12 and y == 9) then + draw("Cancelled") + sleep(1.6) + break + elseif e == "http_success" and but == updateURL then + local new = x.readAll() + local curf = io.open(ideLocation, "r") + local cur = curf:read("*a") + curf:close() + + if cur ~= new then + draw("Update Found") + sleep(1.6) + local f = io.open(ideLocation, "w") + f:write(new) + f:close() + + draw("Click to Exit") + while true do + local e = os.pullEvent() + if e == "mouse_click" or (not isAdvanced() and e == "key") then break end + end + return "exit" + else + draw("No Updates Found!") + sleep(1.6) + break + end + elseif e == "http_failure" or (e == "timer" and but == tID) then + draw("Update Failed!") + sleep(1.6) + break + end + end + + return "settings" +end + +local function changeTheme() + title("LuaIDE - Theme") + + if isAdvanced() then + local disThemes = {"Back"} + for _, v in pairs(availableThemes) do table.insert(disThemes, v[1]) end + local t = scrollingPrompt(disThemes) + local url = nil + for _, v in pairs(availableThemes) do if v[1] == t then url = v[2] end end + + if not url then return "settings" end + if t == "Dawn (Default)" then + term.setBackgroundColor(colors[theme.backgroundHighlight]) + term.setCursorPos(3, 3) + term.clearLine() + term.write("LuaIDE - Loaded Theme!") + sleep(1.6) + + fs.delete(themeLocation) + theme = defaultTheme + return "menu" + end + + term.setBackgroundColor(colors[theme.backgroundHighlight]) + term.setCursorPos(3, 3) + term.clearLine() + term.write("LuaIDE - Downloading...") + + fs.delete("/.LuaIDE_temp_theme_file") + download(url, "/.LuaIDE_temp_theme_file") + local a = loadTheme("/.LuaIDE_temp_theme_file") + + term.setCursorPos(3, 3) + term.clearLine() + if a then + term.write("LuaIDE - Loaded Theme!") + fs.delete(themeLocation) + fs.move("/.LuaIDE_temp_theme_file", themeLocation) + theme = a + sleep(1.6) + return "menu" + end + + term.write("LuaIDE - Could Not Load Theme!") + fs.delete("/.LuaIDE_temp_theme_file") + sleep(1.6) + return "settings" + else + term.setCursorPos(1, 8) + centerPrint("Themes are not available on") + centerPrint("normal computers!") + end +end + +local function settings() + title("LuaIDE - Settings") + + local opt = prompt({{"Change Theme", w/2 - 17, 8}, {"Return to Menu", w/2 - 22, 13}, + {"Check for Updates", w/2 + 2, 8}, {"Exit IDE", w/2 + 2, 13, bg = colors[theme.err], + highlight = colors[theme.errHighlight]}}, "vertical", true) + if opt == "Change Theme" then return changeTheme() + elseif opt == "Check for Updates" then return update() + elseif opt == "Return to Menu" then return "menu" + elseif opt == "Exit IDE" then return "exit" end +end + + +-- -------- Menu + +local function menu() + title("Welcome to LuaIDE " .. version) + + local opt = prompt({{"New File", w/2 - 13, 8}, {"Open File", w/2 - 14, 13}, + {"Settings", w/2 + 2, 8}, {"Exit IDE", w/2 + 2, 13, bg = colors[theme.err], + highlight = colors[theme.errHighlight]}}, "vertical", true) + if opt == "New File" then return "new" + elseif opt == "Open File" then return "open" + elseif opt == "Settings" then return "settings" + elseif opt == "Exit IDE" then return "exit" end +end + + +-- -------- Main + +local function main(arguments) + local opt, data = "menu", nil + + -- Check arguments + if type(arguments) == "table" and #arguments > 0 then + local f = "/" .. shell.resolve(arguments[1]) + if fs.isDir(f) then print("Cannot edit a directory.") end + opt, data = "edit", f + end + + -- Main run loop + while true do + -- Menu + if opt == "menu" then opt = menu() end + + -- Other + if opt == "new" then opt, data = newFile() + elseif opt == "open" then opt, data = openFile() + elseif opt == "settings" then opt = settings() + end if opt == "exit" then break end + + -- Edit + if opt == "edit" and data then opt = edit(data) end + end +end + +-- Load Theme +if fs.exists(themeLocation) then theme = loadTheme(themeLocation) end +if not theme and isAdvanced() then theme = defaultTheme +elseif not theme then theme = normalTheme end + +-- Run +local _, err = pcall(function() + parallel.waitForAny(function() main(arguments) end, monitorKeyboardShortcuts) +end) + +-- Catch errors +if err and not err:find("Terminated") then + term.setCursorBlink(false) + title("LuaIDE - Crash! D:") + + term.setBackgroundColor(colors[theme.err]) + for i = 6, 8 do + term.setCursorPos(5, i) + term.write(string.rep(" ", 36)) + end + term.setCursorPos(6, 7) + term.write("LuaIDE Has Crashed! D:") + + term.setBackgroundColor(colors[theme.background]) + term.setCursorPos(2, 10) + print(err) + + term.setBackgroundColor(colors[theme.prompt]) + local _, cy = term.getCursorPos() + for i = cy + 1, cy + 4 do + term.setCursorPos(5, i) + term.write(string.rep(" ", 36)) + end + term.setCursorPos(6, cy + 2) + term.write("Please report this error to") + term.setCursorPos(6, cy + 3) + term.write("GravityScore! ") + + term.setBackgroundColor(colors[theme.background]) + if isAdvanced() then centerPrint("Click to Exit...", h - 1) + else centerPrint("Press Any Key to Exit...", h - 1) end + while true do + local e = os.pullEvent() + if e == "mouse_click" or (not isAdvanced() and e == "key") then break end + end + + -- Prevent key from being shown + os.queueEvent("") + os.pullEvent() +end + +-- Exit +term.setBackgroundColor(colors.black) +term.setTextColor(colors.white) +term.clear() +term.setCursorPos(1, 1) +centerPrint("Thank You for Using Lua IDE " .. version) +centerPrint("Made by GravityScore")
\ No newline at end of file diff --git a/Programs/Mouse Browser.bup/Contents/Info.meta b/Programs/Mouse Browser.bup/Contents/Info.meta new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/Programs/Mouse Browser.bup/Contents/Info.meta @@ -0,0 +1 @@ + diff --git a/Programs/Mouse Browser.bup/Contents/Resources/resources_here.txt b/Programs/Mouse Browser.bup/Contents/Resources/resources_here.txt new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/Programs/Mouse Browser.bup/Contents/Resources/resources_here.txt @@ -0,0 +1 @@ + diff --git a/Programs/Mouse Browser.bup/Contents/bits-UI/mouse.cfg b/Programs/Mouse Browser.bup/Contents/bits-UI/mouse.cfg new file mode 100644 index 0000000..d549589 --- /dev/null +++ b/Programs/Mouse Browser.bup/Contents/bits-UI/mouse.cfg @@ -0,0 +1,60 @@ +{ + txt = 32768, + back = 2048, +} +{ + txt = 32768, + back = 256, +} +{ + txt = 32768, + back = 512, +} +{ + txt = 32768, + back = 256, +} +{ + txt = 32768, + back = 2048, +} +{ + txt = 32768, + back = 2048, +} +{ + txt = 32768, + back = 1, +} +{ + back = 256, + button = 128, + off = 128, +} +{ + folder = { + txt = "[=]", + bCol = "blue", + tCol = "lightGray", + }, + audio = { + txt = "(o)", + bCol = "red", + tCol = "yellow", + }, + disk = { + txt = "[*]", + bCol = "green", + tCol = "lime", + }, + file = {}, + back = { + txt = " < ", + bCol = "blue", + tCol = "lightGray", + }, +} +{ + Paint = "rom/programs/color/paint", + Edit = "rom/programs/edit", +} diff --git a/Programs/Mouse Browser.bup/Contents/bits-UI/mousebrowser.lua b/Programs/Mouse Browser.bup/Contents/bits-UI/mousebrowser.lua new file mode 100644 index 0000000..6757273 --- /dev/null +++ b/Programs/Mouse Browser.bup/Contents/bits-UI/mousebrowser.lua @@ -0,0 +1,1298 @@ +--[[ + Mouse File Browser + by: + Stiepen irc(Kilobyte) + Cruor + BigSHinyToys + + note: send link to nightin9ale on CC forums +--]] + +local tArgs = {...} +local ver = "1.4" +local sTitle = "File Browser" +local bugTest, norun, dir, showAll +local _tArgs = {} +local config = "mouse.cfg" + +local temp +if shell and shell.getRunningProgram then + temp = shell.getRunningProgram() +end + +temp = temp or "/bla" +local localPath = string.sub(temp,1,#temp-string.len(fs.getName(temp))) +temp = nil -- just because not needed + +-- load config file + +local configSet = {} +local cnf = {} + +if fs.exists(localPath.."/"..config) then + local file = fs.open(localPath.."/"..config,"r") + if file then + local item = file.readLine() + while item do + table.insert(cnf,item) + item = file.readLine() + end + file.close() + end +end + +for i = 1,10 do + local test,data = pcall(textutils.unserialize,cnf[i]) + if test then + configSet[i] = data + else + configSet[i] = nil + end +end +cnf = nil + +-- color configuration work in progress +local titleBar = configSet[1] or {txt = colors.black,back = colors.blue} +local addressBar = configSet[2] or {txt = colors.black,back = colors.lightGray} +local itemWindo = configSet[3] or {txt = colors.black,back = colors.cyan} +local rcmList = configSet[4] or {txt = colors.black,back = colors.lightGray} -- rcm = Right Click Menu List +local rcmTitle = configSet[5] or {txt = colors.black,back = colors.blue} +local dialogTitle = configSet[6] or {txt = colors.black,back = colors.blue} +local dialogWindo = configSet[7] or {txt = colors.black,back = colors.white} +local scrollCol = configSet[8] or {off = colors.gray, button = colors.gray,back = colors.lightGray} + +local tIcons = configSet[9] or { + back = {tCol = "lightGray",bCol = "blue",txt = " < "}, + disk = {tCol = "lime",bCol = "green",txt = "[*]"}, + audio = {tCol = "yellow",bCol = "red",txt = "(o)"}, + folder = {tCol = "lightGray",bCol = "blue",txt = "[=]"}, + file = {tCol = nil ,bCol = nil ,txt = nil} +} + +local customLaunch = configSet[10] or { + ["Edit"] = "rom/programs/edit", + ["Paint"] = "rom/programs/color/paint" +} + +local function saveCFG(overWrite) + if not fs.exists(localPath.."/"..config) or overWrite then + local cnf = {} + local file = fs.open(localPath.."/"..config,"w") + if file then + file.write(textutils.serialize(titleBar).."\n") + file.write(textutils.serialize(addressBar).."\n") + file.write(textutils.serialize(itemWindo).."\n") + file.write(textutils.serialize(rcmList).."\n") + file.write(textutils.serialize(rcmTitle).."\n") + file.write(textutils.serialize(dialogTitle).."\n") + file.write(textutils.serialize(dialogWindo).."\n") + file.write(textutils.serialize(scrollCol).."\n") + file.write(textutils.serialize(tIcons).."\n") + file.write(textutils.serialize(customLaunch).."\n") + file.close() + elseif overWrite then + end + end +end + +saveCFG() + +-- end configuration + +local function help() + print([[Usage: browser [-d] [-h] [-a] [-u] [--debug] [--help] [--dir <dir>] [--all] [--update] +--debug or -d: enable debug mode +--help or -h: display this screen +--dir: define initial directory +--all or -a: show hidden files +--update -u: update]]) +end + +local function inBouwndry(clickX,clickY,boxX,boxY,width,hight) + return ( clickX >= boxX and clickX < boxX + width and clickY >= boxY and clickY < boxY + hight ) +end + +local function update() + print("Checking for Updates") + local isHTTP = false + local response + if http then + isHTTP = true + print("http on") + response = http.get("http://pastebin.com/raw.php?i=rLbnyM1U") + end + local flag = false + local isNewFlag = false + local newVerID + if response and isHTTP then + print("recived") + local sInfo = response.readLine() + print(sInfo) + while sInfo do + print(sInfo) + if flag then + if sInfo == ver then + print("Mouse File Browser is up to date") + break + else + newVerID = sInfo + flag = false + isNewFlag = true + end + elseif sInfo == sTitle then + flag = true + elseif isNewFlag then + isNewFlag = sInfo + response.close() + break + end + sInfo = response.readLine() + end + if isNewFlag then + print("New vershion avalible "..newVerID) + print('downloading to \Browser') + if fs.exists("Browser") then + write("Browser exists OverWrite Browser Y/N : ") + local input = string.lower(read()) + while input ~= "y" and input ~= "n" do + print("y or n required") + input = string.lower(read()) + end + if input == "y" then + print("Over Writeing Browser") + print("Downloading new File") + local response = http.get("http://pastebin.com/raw.php?i="..isNewFlag) + if response then + print("file downloaded") + print("installing") + fs.delete("Browser") + local handel = fs.open("Browser","w") + if handel then + handel.write(response.readAll()) + handel.close() + print("Update Complete") + end + response.close() + end + else + print("Update aborted") + end + else + print("Downloading new File") + local response = http.get("http://pastebin.com/raw.php?i="..isNewFlag) + if response then + print("file downloaded") + print("installing") + local handel = fs.open("Browser","w") + if handel then + handel.write(response.readAll()) + handel.close() + print("Update Complete") + end + response.close() + end + end + end + elseif isHTTP then + print("Error downloading update file Please contact BigSHinyToys on the CC forums") + print("http://www.computercraft.info/forums2/index.php?/topic/5509-advanced-computer-mouse-file-browser/") + elseif not isHTTP then + print("HTTP API is turned off") + print("Access Computer Craft Configer and change line") + print([[enableapi_http { +# Enable the "http" API on Computers +general=false +} +TO THIS : +enableapi_http { +# Enable the "http" API on Computers +general=true +}]]) + end + notCMD = false + norun = true +end + +for a = 1, #tArgs do + if tArgs[a]:sub(1,2) == "--" then + local cmd = tArgs[a]:sub(3):lower() + if cmd == "debug" then + bugTest = true + elseif cmd == "help" then + help() + norun = true + elseif cmd == "dir" then + dir = tArgs[a+1] + a = a + 1 + elseif cmd == "all" then + showAll = true + elseif cmd == "update" then + update() + end + elseif tArgs[a]:sub(1,1) == "-" then + for b = 2, #tArgs[a] do + cmd = tArgs[a]:sub(b, b) + if cmd == "d" then + bugTest = true + elseif cmd == "h" then + help() + norun = true + elseif cmd == "p" then + dir = tArgs[a+1] + a = a + 1 + elseif cmd == "a" then + showAll = true + elseif cmd == "u" then + update() + end + end + else + table.insert(_tArgs, tArgs[a]) + end +end + +if (not dir) and shell and shell.dir then + dir = shell.dir() +end + +if dir and shell and shell.resolve then + dir = shell.resolve(dir) +end + +dir = dir or "/" + +if bugTest then -- this is that the var is for testing + print("Dir: "..dir) + os.startTimer(4) + os.pullEvent() +end + +local function clear() + term.setBackgroundColor(colors.black) + term.setTextColor(colors.white) + term.clear() + term.setCursorBlink(false) + term.setCursorPos(1,1) +end + +--[[ + Code thanks to Cruor + http://www.computercraft.info/forums2/index.php?/topic/5802-support-for-shell/ +]]-- + +local function fixArgs(...) + local tReturn={} + local str=table.concat({...}," ") + local sMatch + while str and #str>0 do + if string.sub(str,1,1)=="\"" then + sMatch, str=string.match(str, "\"(.-)\"%s*(.*)") + else + sMatch, str=string.match(str, "(%S+)%s*(.*)") + end + table.insert(tReturn,sMatch) + end + return tReturn +end + +--[[ end Cruor function ]]-- + + +-- modified read made to play nice with coroutines + +local function readMOD( _sReplaceChar, _tHistory,_wdth) + local sLine = "" + term.setCursorBlink( true ) + + local nHistoryPos = nil + local nPos = 0 + if _sReplaceChar then + _sReplaceChar = string.sub( _sReplaceChar, 1, 1 ) + end + + local sx, sy = term.getCursorPos() + + local w, h = term.getSize() + if _wdth and type(_wdth) == "number" then + w = sx + _wdth - 1 + end + + local function redraw( _sCustomReplaceChar ) + local nScroll = 0 + if sx + nPos >= w then + nScroll = (sx + nPos) - w + end + + term.setCursorPos( sx + _wdth - 1, sy ) + term.write(" ") + term.setCursorPos( sx, sy ) + local sReplace = _sCustomReplaceChar or _sReplaceChar + if sReplace then + term.write( string.rep(sReplace,_wdth) ) + else + term.write( string.sub( sLine, nScroll + 1 ,nScroll + _wdth) ) + end + term.setCursorPos( sx + nPos - nScroll, sy ) + end + + while true do + local sEvent, param = os.pullEvent() + if sEvent == "char" then + sLine = string.sub( sLine, 1, nPos ) .. param .. string.sub( sLine, nPos + 1 ) + nPos = nPos + 1 + redraw() + + elseif sEvent == "key" then + + if param == keys.left then + -- Left + if nPos > 0 then + nPos = nPos - 1 + redraw() + end + + elseif param == keys.right then + -- Right + if nPos < string.len(sLine) then + nPos = nPos + 1 + redraw() + end + + elseif param == keys.up or param == keys.down then + -- Up or down + if _tHistory then + redraw(" "); + if param == keys.up then + -- Up + if nHistoryPos == nil then + if #_tHistory > 0 then + nHistoryPos = #_tHistory + end + elseif nHistoryPos > 1 then + nHistoryPos = nHistoryPos - 1 + end + else + -- Down + if nHistoryPos == #_tHistory then + nHistoryPos = nil + elseif nHistoryPos ~= nil then + nHistoryPos = nHistoryPos + 1 + end + end + + if nHistoryPos then + sLine = _tHistory[nHistoryPos] + nPos = string.len( sLine ) + else + sLine = "" + nPos = 0 + end + redraw() + end + elseif param == keys.backspace then + -- Backspace + if nPos > 0 then + redraw(" "); + sLine = string.sub( sLine, 1, nPos - 1 ) .. string.sub( sLine, nPos + 1 ) + nPos = nPos - 1 + redraw() + end + elseif param == keys.home then + -- Home + nPos = 0 + redraw() + elseif param == keys.delete then + if nPos < string.len(sLine) then + redraw(" "); + sLine = string.sub( sLine, 1, nPos ) .. string.sub( sLine, nPos + 2 ) + redraw() + end + elseif param == keys["end"] then + -- End + nPos = string.len(sLine) + redraw() + end + elseif sEvent == "redraw" then + redraw() + elseif sEvent == "return" then + term.setCursorBlink( false ) + return sLine + end + end + + term.setCursorBlink( false ) + + return sLine +end + +-- end modified read + +local function printC(posX,posY,textCol,backCol,text) + term.setCursorPos(posX,posY) + term.setTextColor(colors[textCol] or textCol) + term.setBackgroundColor(colors[backCol] or backCol) + term.write(text) +end + +local function InputBox(title) + local boxW,boxH = 26,3 + local termX,termY = term.getSize() + local ofsX,ofsY = math.ceil((termX/2) - (boxW/2)) , math.ceil((termY/2) - (boxH/2)) -- offset from top left + local options = {"ok","cancel"} + + local selected = 1 + local space = 0 + local range = {} + for i = 1,#options do + range[i] = {s = space,f = space + string.len(options[i])} + space = space + string.len(options[i])+3 + end + local ofC = (boxW/2) - (space/2) + + local function drawBox() + printC(ofsX,ofsY,colors.black,colors.blue,string.rep(" ",boxW)) + printC(ofsX+1,ofsY,colors.black,colors.blue,(title or "User Input")) + printC(ofsX,ofsY+1,colors.black,colors.white,string.rep(" ",boxW)) + printC(ofsX,ofsY+2,colors.black,colors.white,string.rep(" ",boxW)) + printC(ofsX,ofsY+3,colors.black,colors.white,string.rep(" ",boxW)) + + for i = 1,#options do + if i == selected then + term.setBackgroundColor(colors.lightGray) + term.setCursorPos(range[i].s + ofC + ofsX - 1,ofsY + 3) + term.write("["..options[i].."]") + term.setBackgroundColor(colors.white) + term.write(" ") + else + term.setCursorPos(range[i].s + ofC + ofsX - 1,ofsY + 3) + term.write(" "..options[i].." ") + end + end + + printC(ofsX+2,ofsY+2,colors.black,colors.lightGray,string.rep(" ",boxW-4)) + end + drawBox() + term.setCursorPos(ofsX+2,ofsY+2) + local co = coroutine.create(function() return readMOD(nil,nil,boxW - 4) end) + while true do + local event = {os.pullEvent()} + if event[1] == "key" or event[1] == "char" then + if event[2] == 28 then + local test,data = coroutine.resume(co,"return") + return data + else + coroutine.resume(co,unpack(event)) + end + elseif event[1] == "mouse_click" then + if event[4] == ofsY + 3 then + for i = 1,#options do + if event[3] >= range[i].s + ofC + ofsX - 1 and event[3] <= range[i].f + ofC + ofsX then + if options[i] == "ok" then + local test,data = coroutine.resume(co,"return") + return data + elseif options[i] == "cancel" then + return false + end + end + end + end + end + end +end + +local function dialogBox(title,message,options, h, w) + term.setCursorBlink(false) + local selected = 1 + title = title or "" + message = message or "" + options = options or {} + local boxW,boxH = (w or 26), (h or 3) + local termX,termY = term.getSize() + local ofsX,ofsY = math.ceil((termX/2) - (boxW/2)) , math.ceil((termY/2) - (boxH/2)) -- offset from top left + + local space = 0 + local range = {} + for i = 1,#options do + range[i] = {s = space,f = space + string.len(options[i])} + space = space + string.len(options[i])+3 + end + local ofC = math.ceil((boxW/2)) - math.ceil((space/2)) + + local function drawBox() + printC(ofsX,ofsY,dialogTitle.txt,dialogTitle.back," "..title..string.rep(" ",boxW-#title-5).."_[]") + term.setBackgroundColor(colors.red) + term.setTextColor(colors.white) + term.write("X") + printC(ofsX,ofsY+1,dialogWindo.txt,dialogWindo.back,string.sub(" "..message..string.rep(" ",boxW),1,boxW)) + term.setCursorPos(ofsX,ofsY+2) + term.write(string.rep(" ",boxW)) + term.setCursorPos(ofsX,ofsY+3) + term.write(string.rep(" ",boxW)) + for i = 1,#options do + if i == selected then + printC(range[i].s + ofC + ofsX - 1,ofsY + 3,"black","lightGray","["..options[i].."]") + term.setBackgroundColor(dialogWindo.back) + term.setTextColor(dialogWindo.txt) + term.write(" ") + else + term.setCursorPos(range[i].s + ofC + ofsX - 1,ofsY + 3) + term.write(" "..options[i].." ") + end + end + term.setCursorPos(ofsX + ofC + space,ofsY + 3) + term.write(string.rep(" ",boxW - (ofC + space))) + term.setBackgroundColor(colors.black) + term.setTextColor(colors.white) + end + while true do + drawBox() + event = {os.pullEvent()} + if event[1] == "key" then + if event[2] == 203 then -- left + selected = selected - 1 + if selected < 1 then + selected = #options + end + elseif event[2] == 205 then -- right + selected = selected + 1 + if selected > #options then + selected = 1 + end + elseif event[2] == 28 then -- enter + return selected , options[selected] + end + elseif event[1] == "mouse_click" then + + if bugTest then term.write("M "..event[2].." X "..event[3].." Y "..event[4].." ") end + + if event[2] == 1 then + if event[4] == ofsY + 3 then + for i = 1,#options do + if event[3] >= range[i].s + ofC + ofsX - 1 and event[3] <= range[i].f + ofC + ofsX then + return i , options[i] + end + end + end + end + end + end +end + +local flag = true +local fSlash = "/" +local path = {dir:match("[^/]+")} +local function stringPath() -- compacted this a lot + return fSlash..table.concat(path,fSlash) +end + +local function osRunSpaces(sFileLocation,...) -- getRunningProgram() ["shell"] = shell + clear() + if os.newThread then + os.newThread(false,...) + else + local fProg,probblem = loadfile(sFileLocation) + if fProg then + local tEnv = {["shell"] = {}} + setmetatable(tEnv.shell,{ __index = shell}) + tEnv.shell.getRunningProgram = function() + return sFileLocation + end + setmetatable(tEnv,{ __index = _G}) + setfenv(fProg,tEnv) + local test,probblem = pcall(fProg,...) + if not test then + print(probblem) + dialogBox("ERROR",tostring(probblem),{"ok"},3,30) + else + return true + end + else + print(probblem) + dialogBox("ERROR",tostring(probblem),{"ok"},3,30) + end + end +end + +local function rClickMenu(title,tList,tItem,posX,posY) + + term.setCursorBlink(false) + local BoxTitle = title + local choices = {} + local termX,termY = term.getSize() + local offX,offY + + local width = #BoxTitle + 2 + local hight + + for k,v in pairs(tList) do + if v ~= nil then + table.insert(choices,k) + end + if width < #k + 2 then + width = #k + 2 + end + end + + if #choices == 0 then + return + end + + hight = #choices + 1 + table.sort(choices) + + offX,offY = math.ceil((termX/2) - (width/2)),math.ceil((termY/2) - (hight/2)) + + if posX and posY then -- offX,offY = posX,posY + if posX >= termX - width - 1 then + offX = termX - width - 1 + else + offX = posX + end + if posY >= termY - hight then + offY = termY - hight + else + offY = posY + end + end + + local function reDrawer() + printC(offX,offY,rcmTitle.txt,rcmTitle.back," "..BoxTitle..string.rep(" ",width - #BoxTitle - 1)) + for i = 1,#choices do + printC(offX,offY + i,rcmList.txt,rcmList.back," "..choices[i]..string.rep(" ",width - #choices[i] - 1)) + end + end + + while true do + reDrawer() + local event = {os.pullEvent()} + if event[1] == "mouse_click" then + if event[2] == 1 then -- event[3] = x event[4] = y + if event[4] > offY and event[4] < hight + offY and event[3] >= offX and event[3] < width + offX then + --dialogBox("ERROR:",type(tList[choices[event[4] - offY]]),{"ok"}) + if type(tList[choices[event[4] - offY]]) == "function" then + return tList[choices[event[4] - offY]](tItem) + elseif type(tList[choices[event[4] - offY]]) == "table" then + return rClickMenu("Options",tList[choices[event[4] - offY]],tItem,event[3],event[4]) + elseif type(tList[choices[event[4] - offY]]) == "string" then + return osRunSpaces( + unpack( + fixArgs( + tList[choices[event[4] - offY]].." \""..stringPath()..fSlash..tItem.n.."\"" + ) + ) + ) + else + dialogBox("ERROR:","somthing up with new rMenu",{"ok"}) + end + else + return + end + elseif event[2] == 2 then + return + end + end + end + +end + +local function preferences() + local tItem = { + {txt = "Title Bar",it = titleBar}, + {txt = "Address Bar",it = addressBar}, + {txt = "Item Windo", it = itemWindo}, + {txt = "Title Right Click Title",it = rcmTitle}, + {txt = "Right Click Menu",it = rcmList}, + {txt = "Title Dialog Box",it = dialogTitle}, + {txt = "Dialog Box",it = dialogWindo}, + {txt = "Scroll Bar",it = scrollCol} + } + local topL,topR = 13,5 + local width,hight = 23,6 + local bottomL,bottomR = topL + width,topR + hight + + local listOffset = 0 + local sel = 1 + local otherSel = 1 + local otherItems = {} + + if tItem[sel] then + for k,v in pairs(tItem[sel].it) do + table.insert(otherItems,{txt = k,it = v}) + end + end + + local function draw() + printC(topL,topR,titleBar.txt,titleBar.back,string.sub(" Preferences "..string.rep(" ",width),1,width)) + for i = 0,12,4 do + for a = 1,4 do + --printC(topL + (a*12)-12 ,topR + ((i+4)/4),4,2^(a+i-1)," "..tostring(2^(a+i-1))) + printC(topL + a-1 ,topR + ((i+4)/4),4,2^(a+i-1)," ") + end + end + local sSel = " " + for i = 1,hight - 2 do + if i == sel - listOffset then + sSel = ">" + end + if tItem[i+listOffset] then + printC(topL + 4 ,topR + i,colors.black,colors.white,string.sub(sSel..tItem[i+listOffset].txt..string.rep(" ",width),1,width - 4)) + else + printC(topL + 4 ,topR + i,colors.black,colors.white,sSel..string.rep(" ",width-5)) + end + if i == sel - listOffset then + sSel = " " + end + end + term.setCursorPos(topL,topR + hight - 1) + local loop = 1 + local length = 0 + for i = 1,#otherItems do + if otherSel == i then + sSel = ">" + end + if colors.black == otherItems[i].it or colors.gray == otherItems[i].it then + term.setTextColor(colors.white) + else + term.setTextColor(colors.black) + end + term.setBackgroundColor(otherItems[i].it) + term.write(sSel..tostring(otherItems[i].txt).." ") + length = length + #otherItems[i].txt + 2 + if otherSel == i then + sSel = " " + end + loop = loop+1 + end + term.setBackgroundColor(colors.white) + term.write(string.rep(" ",width - length)) + end + while true do + draw() + local event = {os.pullEvent()} + if event[1] == "mouse_click" and event[2] == 1 then + if inBouwndry(event[3],event[4],topL,topR,width,hight) then + local inSideX,inSideY = event[3] - topL,event[4] - topR + if inBouwndry(inSideX+1,inSideY,1,1,4,4) and tItem[sel] then + --[[ + term.setCursorPos(1,1) + term.setBackgroundColor(2^(inSideX + ((inSideY*4)-4))) + print(2^(inSideX + ((inSideY*4)-4))," ",inSideX + ((inSideY*4)-4)," ") + ]]-- + tItem[sel]["it"][otherItems[otherSel].txt] = (2^(inSideX + ((inSideY*4)-4))) + end + end + elseif event[1] == "key" then + if event[2] == 200 then + sel = sel - 1 + elseif event[2] == 208 then + sel = sel + 1 + elseif event[2] == 203 then + otherSel = otherSel - 1 + elseif event[2] == 205 then + otherSel = otherSel + 1 + elseif event[2] == 28 then + if dialogBox("Confirm","Save prefrences?",{"Yes","No"}) == 1 then + saveCFG(true) + end + return + end + end + if sel < 1 then + sel = 1 + elseif sel > #tItem then + sel = #tItem + end + if sel > listOffset + hight - 2 then + listOffset = listOffset + 1 + elseif sel - listOffset < 1 then + listOffset = listOffset - 1 + end + + otherItems = {} + if tItem[sel] then + for k,v in pairs(tItem[sel].it) do + table.insert(otherItems,{txt = k,it = v}) + end + end + + if otherSel < 1 then + otherSel = 1 + elseif otherSel > #otherItems then + otherSel = #otherItems + end + + if bugTest then + term.setBackgroundColor(colors.black) + term.setTextColor(colors.white) + term.setCursorPos(1,1) + term.clearLine() + term.write("sel "..sel.." offset "..listOffset) + end + end +end + +local function fileSelect(mode) -- save_file open_file browse < not yet implemented + + local title = sTitle.." "..ver + local bRun = true + local clipboard = nil + local cut = false + + local termX,termY = term.getSize() + local offsetX,offsetY = 1,1 + local hight,width = math.ceil(termY-2),math.ceil(termX-2) + local oldHight,oldWidth + + -- offsets + local boxOffX,boxOffY = offsetX,offsetY + 2 + local boxH,boxW = hight - 2 ,width - 2 + + local barX,barY = offsetX + 1,offsetY + 2 + local barH,barW = 1,width - 1 + + local tbarX,tbarY = offsetX + 1,offsetY + 1 + local tbarH,tbarW = 1,width - 1 + + local exitX,exitY = offsetX + width - 1 ,offsetY + 1 + + local pading = string.rep(" ",boxW) + local list + + local listOff = 0 + + local sPath + local tItemList = {} + + local function newList() + listOff = 0 + flag = true + tItemList = {{n = "..", id = "back"}} -- adds a back item at top of list + sPath = stringPath() + local folders = {} + local files = {} + local disks = {} + if not fs.exists(sPath) then + path = {} + sPath = stringPath() + dialogBox("ERROR:","Path no longer exists",{"ok"}) + end + local test,list = pcall(fs.list,sPath) -- stopes fs.list crash + if list == nil then + list = {} + dialogBox("ERROR : ","fs.list crashed",{"ok"}) + end + if #path == 0 then + for i,v in pairs(rs.getSides()) do + if disk.isPresent(v) then + if disk.hasData(v) then + table.insert(tItemList,{n = disk.getMountPath(v), id = "disk",s = v}) + disks[disk.getMountPath(v)] = true + elseif disk.hasAudio(v) then + table.insert(tItemList,{n = disk.getAudioTitle(v), id = "audio",s = v}) + end + end + end + end + for i,v in pairs(list) do + if fs.isDir(sPath..fSlash..v) then + table.insert(folders,v) + else + table.insert(files,v) + end + end + table.sort(folders) + table.sort(files) + for i,v in pairs(folders) do + if disks[v] == nil then + table.insert(tItemList,{n = v, id = "folder"}) + end + end + for i,v in pairs(files) do + table.insert(tItemList,{n = v, id = "file"}) + end + end + + local function paste() + if cut then + local s, m = pcall( + function() + fs.move(clipboard[1]..fSlash..clipboard[2], stringPath()..fSlash..clipboard[2]) + cut = false + clipboard = nil + end) + if not s then + dialogBox("Error", (m or "Couldn't move"), {"ok"}, 4, 30) + end + if bugTest then + local x, y = term.getCursorPos() + term.setCursorPos(1, ({term.getSize()})[2]) + write("from "..clipboard[1]..fSlash..clipboard[2].." to "..stringPath()..fSlash..clipboard[2]) + end + else + local s, m = pcall(function() + if fs.exists(stringPath()..fSlash..clipboard[2]) then + fs.copy(clipboard[1]..fSlash..clipboard[2], stringPath()..fSlash.."copy-"..clipboard[2]) + else + fs.copy(clipboard[1]..fSlash..clipboard[2], stringPath()..fSlash..clipboard[2]) + end + end) + if not s then + dialogBox("Error", (m or "Couldn't copy"), {"ok"}, 4, 30) + end + if bugTest then + local x, y = term.getCursorPos() + term.setCursorPos(1, ({term.getSize()})[2]) + write("from "..clipboard[1]..fSlash..clipboard[2].." to "..stringPath()..fSlash..clipboard[2]) + end + end + newList() + end + + -- this section bellow handles the right click menu + + local tmenu = { + disk = { + ["Open"] = function(tItem) + table.insert(path,tItem.n) + newList() + end, + ["Copy"] = function(tItem) + clipboard = {stringPath(), tItem.n} + cut = false + end, + ["Eject"] = function(tItem) + if dialogBox("Confirm","Eject "..fSlash..tItem.n.." "..tItem.s,{"yes","no"}) == 1 then + disk.eject(tItem.s) + newList() + end + end, + ["ID label"] = function(tItem) + dialogBox("ID label",disk.getDiskID(tItem.s).." "..tostring(disk.getLabel(tItem.s)),{"ok"}) + end, + ["Set label"] = function(tItem) + local name = InputBox("Label?") + if name then + disk.setLabel(tItem.s,name) + end + end, + ["Clear label"] = function(tItem) + if dialogBox("Confirm","Cleal Label from "..tItem.s,{"yes","no"}) == 1 then + disk.setLabel(tItem.s) + end + end + }, + folder = { + ["Open"] = function(temp) + table.insert(path,temp.n) + newList() + end, + ["Copy"] = function(tItem) + clipboard = {stringPath(), tItem.n} + cut = false + end, + ["Cut"] = function(tItem) + clipboard = {stringPath(), tItem.n} + cut = true + end, + ["Delete"] = function(tItem) + if dialogBox("Confirm","Delete "..tItem.id.." "..tItem.n,{"yes","no"}) == 1 then + if fs.isReadOnly(stringPath()..fSlash..tItem.n) then + dialogBox("ERROR",tItem.id.." Is read Only",{"ok"}) + else + fs.delete(stringPath()..fSlash..tItem.n) + newList() + end + end + end, + ["Rename"] = function(tItem) + local sName = InputBox("New Name") + if type(sName) == "string" and sName ~= "" then + local s, m = pcall(function() + fs.move(stringPath()..fSlash..tItem.n,stringPath()..fSlash..sName) + end) + if not s then + dialogBox("Error", (m or "Rename failed"), {"ok"}) + end + end + newList() + end + }, + file = { + ["Run"] = { + ["Run"] = function(tItem) + osRunSpaces(stringPath()..fSlash..tItem.n) + end, + ["Run CMD"] = function(tItem) + local cmd = InputBox("Commands") + if cmd then + osRunSpaces(stringPath()..fSlash..tItem.n,unpack(fixArgs(cmd))) + end + end, + }, + ["Open With"] = customLaunch, + ["Rename"] = function(tItem) + local sName = InputBox("New Name") + if type(sName) == "string" and sName ~= "" then + local s, m = pcall(function() + fs.move(stringPath()..fSlash..tItem.n,stringPath()..fSlash..sName) + end) + if not s then + dialogBox("Error", (m or "Rename failed"), {"ok"}) + end + end + newList() + end, + ["Delete"] = function(tItem) + if dialogBox("Confirm","Delete "..tItem.id.." "..tItem.n,{"yes","no"}) == 1 then + if fs.isReadOnly(stringPath()..fSlash..tItem.n) then + dialogBox("ERROR",tItem.id.." Is read Only",{"ok"}) + else + fs.delete(stringPath()..fSlash..tItem.n) + newList() + end + end + end, + ["Cut"] = function(tItem) + clipboard = {stringPath(), tItem.n} + cut = true + end, + ["Copy"] = function(tItem) + clipboard = {stringPath(), tItem.n} + cut = false + end + }, + audio = { + ["Play"] = 1, + ["Eject"] = 1 + }, + back = { + }, + blank = { -- tmenu.blank.Paste = + ["Paste"] = nil, + ["New File"] = function() + local name = InputBox() + if name then + if fs.exists(stringPath()..fSlash..name) then + dialogBox("ERROR","Name exists",{"ok"}) + else + local file = fs.open(stringPath()..fSlash..name,"w") + if file then + file.write("") + file.close() + newList() + else + dialogBox("ERROR","File not created",{"ok"}) + end + end + end + end, + ["New Folder"] = function() + local name = InputBox() + if name then + if fs.exists(stringPath()..fSlash..name) then + dialogBox("ERROR","Name exists",{"ok"}) + else + if pcall(fs.makeDir,stringPath()..fSlash..name) then + newList() + else + dialogBox("ERROR","Access Denied",{"ok"}) + end + end + end + end, + ["Preferences"] = preferences + }, + } + + -- end right click menu + + local function scrollBar(posX,posY) + if posX == boxOffX+boxW+1 and posY > boxOffY and posY <= boxOffY+boxH then + if #tItemList > boxH then + if posY == boxOffY + 1 then + listOff = 0 + elseif posY == boxOffY+boxH then + listOff = #tItemList + 1 - boxH + else + listOff = math.ceil((posY - boxOffY - 1 )*(((#tItemList - boxH+2)/boxH))) + end + flag = true + end + end + end + + newList() + + while bRun do + if flag then + flag = false + -- clear + if oldHight ~= hight and oldWidth ~= width then + term.setBackgroundColor(colors.black) + term.clear() + oldHight,oldWidth = hight,width + end + -- draw top title bar + local b = tbarW - #title -2 + if b < 0 then + b = 0 + end + printC(tbarX,tbarY,titleBar.txt,titleBar.back,string.sub(" "..title,1,tbarW)..string.rep(" ",b)) + term.setTextColor(colors.white) + term.setBackgroundColor(colors.red) + term.write("X") + + -- draw location bar + local a = barW - #sPath - 1 + if a < 0 then + a = 0 + end + local tmppath = sPath + if shell and shell.getDisplayName then + tmppath = shell.getDisplayName(sPath) + --dialogBox("yay") + else + --dialogBox("moop") + end + tmppath = tmppath or sPath + local a = barW - #tmppath - 1 + if a < 0 then + a = 0 + end + printC(barX,barY,addressBar.txt,addressBar.back,string.sub(" "..tmppath,1,barW)..string.rep(" ",a)) + + -- draw scroll bar + if #tItemList > boxH then + term.setBackgroundColor(scrollCol.back) + for i = 1,boxH do + term.setCursorPos(boxOffX+boxW+1,i + boxOffY) + local scroll = math.floor( boxH* (listOff/(#tItemList-boxH+2)) )+1 + if i == scroll then + term.setBackgroundColor(scrollCol.button) + term.write(" ") + term.setBackgroundColor(scrollCol.back) + else + term.write(" ") + end + end + else + term.setBackgroundColor(scrollCol.off) + for i = 1,boxH do + term.setCursorPos(boxOffX+boxW+1,i + boxOffY) + term.write(" ") + end + end + + -- draw main section + + for i = 1,boxH do -- listOff + local sel = i+listOff + if tItemList[sel] then + printC(1+boxOffX,i+boxOffY,(tIcons[tItemList[sel].id].tCol or itemWindo.txt),(tIcons[tItemList[sel].id].bCol or itemWindo.back),( tIcons[tItemList[sel].id].txt or " ")) + printC(4+boxOffX,i+boxOffY,itemWindo.txt,itemWindo.back,string.sub(" "..tItemList[sel].n..pading,1,boxW-3)) + else + printC(1+boxOffX,i+boxOffY,itemWindo.txt,itemWindo.back,pading) + end + end + + if bugTest then + printC(1,1,"black","white",listOff.." "..boxOffY.." "..boxH) + end + + end + + -- react to events + local event = {os.pullEvent()} + + if event[1] == "mouse_click" then + if inBouwndry(event[3],event[4],boxOffX+1,boxOffY+1,boxW,boxH) then + local selected = tItemList[event[4]+listOff-boxOffY] + if selected and inBouwndry(event[3],event[4],boxOffX+1,event[4],#selected.n + 4,1) then + if event[2] == 1 then -- left mouse + if selected.id == "back" then + table.remove(path,#path) + newList() + elseif selected.id == "folder" or selected.id == "disk" then + table.insert(path,selected.n) + newList() + elseif selected.id == "file" then + if dialogBox("Run file ?",selected.n,{"yes","no"}) == 1 then + osRunSpaces(stringPath()..fSlash..selected.n) + end + flag = true + end + elseif event[2] == 2 then -- right mouse + rClickMenu("Options",tmenu[selected.id],selected,event[3],event[4]) + flag = true + end + elseif event[2] == 2 then -- right clicking not on object + if clipboard then + tmenu.blank.Paste = paste + else + tmenu.blank.Paste = nil + end + rClickMenu("Options",tmenu["blank"],selected,event[3],event[4]) + flag = true + end + elseif event[2] == 1 and event[3] == exitX and event[4] == exitY then + if dialogBox("Confirm","Exit application",{"yes","no"}) == 1 then + bRun = false + end + flag = true + elseif event[2] == 1 then + scrollBar(event[3],event[4]) + end + elseif event[1] == "mouse_scroll" then -- flag this needs new math + local old = listOff + listOff = listOff + event[2] + if listOff < 0 then + listOff = 0 + end + if #tItemList + 1 - boxH > 0 and listOff > #tItemList + 1 - boxH then + listOff = #tItemList + 1 - boxH + elseif listOff > 0 and #tItemList + 1 - boxH < 0 then + listOff = 0 + end + if listOff ~= old then + flag = true + end + + elseif event[1] == "mouse_drag" then -- scroll bar + scrollBar(event[3],event[4]) + elseif event[1] == "disk" or event[1] == "disk_eject" then + newList() + elseif event[1] == "window_resize" then + termX,termY = term.getSize() + offsetX,offsetY = 1,1 + hight,width = math.ceil(termY-2),math.ceil(termX-2) + + boxOffX,boxOffY = offsetX,offsetY + 2 + boxH,boxW = hight - 2 ,width - 2 + + barX,barY = offsetX + 1,offsetY + 2 + barH,barW = 1,width - 1 + + tbarX,tbarY = offsetX + 1,offsetY + 1 + tbarH,tbarW = 1,width - 1 + + exitX,exitY = offsetX + width - 1 ,offsetY + 1 + pading = string.rep(" ",boxW) + + flag = true + elseif event[1] == "redraw" then + flag = true + end + end +end + +local function main() + if term.isColor() then + clear() + fileSelect() + clear() + else + error("Not an Advanced Computer (gold) ") + end +end + +local trash = (norun or main())
\ No newline at end of file diff --git a/Programs/NPaintPro.bup/Contents/Info.meta b/Programs/NPaintPro.bup/Contents/Info.meta new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/Programs/NPaintPro.bup/Contents/Info.meta @@ -0,0 +1 @@ + diff --git a/Programs/NPaintPro.bup/Contents/Resources/resources_here.txt b/Programs/NPaintPro.bup/Contents/Resources/resources_here.txt new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/Programs/NPaintPro.bup/Contents/Resources/resources_here.txt @@ -0,0 +1 @@ + diff --git a/Programs/NPaintPro.bup/Contents/bits-UI/npaintpro.lua b/Programs/NPaintPro.bup/Contents/bits-UI/npaintpro.lua new file mode 100644 index 0000000..56f7ef0 --- /dev/null +++ b/Programs/NPaintPro.bup/Contents/bits-UI/npaintpro.lua @@ -0,0 +1,2827 @@ +--[[ + NPaintPro + By NitrogenFingers +]]-- + +--The screen size +local w,h = term.getSize() +--Whether or not the program is currently waiting on user input +local inMenu = false +--Whether or not a drop down menu is active +local inDropDown = false +--Whether or not animation tools are enabled (use -a to turn them on) +local animated = false +--Whether or not the text tools are enabled (use -t to turn them on) +local textual = false +--Whether or not "blueprint" display mode is on +local blueprint = false +--Whether or not the "layer" display is on +local layerDisplay = false +--Whether or not the interface is presently hidden +local interfaceHidden = false +--Whether or not the "direction" display is on +local printDirection = false +--The tool/mode npaintpro is currently in. Default is "paint" +--For a list of modes, check out the help file +local state = "paint" +--Whether or not the program is presently running +local isRunning = true +--The rednet address of the 3D printer, if one has been attached +local printer = nil + +--The list of every frame, containing every image in the picture/animation +--Note: nfp files always have the picture at frame 1 +local frames = { } +--How many frames are currently in the given animation. +local frameCount = 1 +--The Colour Picker column +local column = {} +--The offset of visible colours in the picker column, if the screen cannot fit all 16 +local columnoffset = 0 +--The currently selected left and right colours +local lSel,rSel = colours.white,nil +--The amount of scrolling on the X and Y axis +local sx,sy = 0,0 +--The alpha channel colour +--Change this to change default canvas colour +local alphaC = colours.black +--The currently selected frame. Default is 1 +local sFrame = 1 +--The contents of the image buffer- contains contents, width and height +local buffer = nil +--The position, width and height of the selection rectangle +local selectrect = nil + +--Whether or not text tools are enabled for this document +local textEnabled = false +--The X and Y positions of the text cursor +local textCurX, textCurY = 1,1 + +--The currently calculated required materials +local requiredMaterials = {} +--Whether or not required materials are being displayed in the pallette +local requirementsDisplayed = false +--A list of the rednet ID's all in-range printers located +local printerList = { } +--A list of the names of all in-range printers located. Same as the printerList in reference +local printerNames = { } +--The selected printer +local selectedPrinter = 1 +--The X,Y,Z and facing of the printer +local px,py,pz,pfx,pfz = 0,0,0,0,0 +--The form of layering used +local layering = "up" + +--The animation state of the selection rectangle and image buffer +local rectblink = 0 +--The ID for the timer +local recttimer = nil +--The radius of the brush tool +local brushsize = 3 +--Whether or not "record" mode is activated (animation mode only) +local record = false +--The time between each frame when in play mode (animation mode only) +local animtime = 0.3 + +--The current "cursor position" in text mode +local cursorTexX,cursorTexY = 1,1 + +--A list of hexidecimal conversions from numbers to hex digits +local hexnums = { [10] = "a", [11] = "b", [12] = "c", [13] = "d", [14] = "e" , [15] = "f" } +--The NPaintPro logo (divine, isn't it?) +local logo = { +"fcc 3 339"; +" fcc 9333 33"; +" fcc 933 333 33"; +" fcc 933 33 33"; +" fcc 933 33 33"; +" c88 333 93333"; +" 888 333 9333"; +" 333 3 333 939"; +} +--The Layer Up and Layer Forward printing icons +local layerUpIcon = { + "0000000"; + "0088880"; + "0888870"; + "07777f0"; + "0ffff00"; + "0000000"; +} +local layerForwardIcon = { + "0000000"; + "000fff0"; + "00777f0"; + "0888700"; + "0888000"; + "0000000"; +} +--The available menu options in the ctrl menu +local mChoices = {"Save","Exit"} +--The available modes from the dropdown menu- tables indicate submenus (include a name!) +local ddModes = { { "paint", "brush", "pippette", "flood", "move", "clear", "select", name = "painting" }, { "alpha to left", "alpha to right", "hide interface", name = "display" }, "help", { "print", "save", "exit", name = "file" }, name = "menu" } +--The available modes from the selection right-click menu +local srModes = { "cut", "copy", "paste", "clear", "hide", name = "selection" } +--The list of available help topics for each mode 127 +local helpTopics = { + [1] = { + name = "Paint Mode", + key = nil, + animonly = false, + textonly = false, + message = "The default mode for NPaintPro, for painting pixels." + .." Controls here that are not overridden will apply for all other modes. Leaving a mode by selecting that mode " + .." again will always send the user back to paint mode.", + controls = { + { "Arrow keys", "Scroll the canvas" }, + { "Left Click", "Paint/select left colour" }, + { "Right Click", "Paint/select right colour" }, + { "Z Key", "Clear image on screen" }, + { "Tab Key", "Hide selection rectangle if visible" }, + { "Q Key", "Set alpha mask to left colour" }, + { "W Key", "Set alpha mask to right colour" }, + { "Number Keys", "Swich between frames 1-9" }, + { "</> keys", "Move to the next/last frame" }, + { "R Key", "Removes every frame after the current frame"} + } + }, + [2] = { + name = "Brush Mode", + key = "b", + animonly = false, + textonly = false, + message = "Brush mode allows painting a circular area of variable diameter rather than a single pixel, working in ".. + "the exact same way as paint mode in all other regards.", + controls = { + { "Left Click", "Paints a brush blob with the left colour" }, + { "Right Click", "Paints a brush blob with the right colour" }, + { "Number Keys", "Changes the radius of the brush blob from 2-9" } + } + }, + [3] = { + name = "Pippette Mode", + key = "p", + animonly = false, + textonly = false, + message = "Pippette mode allows the user to click the canvas and set the colour clicked to the left or right ".. + "selected colour, for later painting.", + controls = { + { "Left Click", "Sets clicked colour to the left selected colour" }, + { "Right Click", "Sets clicked colour to the right selected colour" } + } + }, + [4] = { + name = "Move Mode", + key = "m", + animonly = false, + textonly = false, + message = "Mode mode allows the moving of the entire image on the screen. This is especially useful for justifying".. + " the image to the top-left for animations or game assets.", + controls = { + { "Left/Right Click", "Moves top-left corner of image to selected square" }, + { "Arrow keys", "Moves image one pixel in any direction" } + } + }, + [5] = { + name = "Flood Mode", + key = "f", + animonly = false, + textonly = false, + message = "Flood mode allows the changing of an area of a given colour to that of the selected colour. ".. + "The tool uses a flood4 algorithm and will not fill diagonally. Transparency cannot be flood filled.", + controls = { + { "Left Click", "Flood fills selected area to left colour" }, + { "Right Click", "Flood fills selected area to right colour" } + } + }, + [6] = { + name = "Select Mode", + key = "s", + animonly = false, + textonly = false, + message = "Select mode allows the creation and use of the selection rectangle, to highlight specific areas on ".. + "the screen and perform operations on the selected area of the image. The selection rectangle can contain an ".. + "image on the clipboard- if it does, the image will flash inside the rectangle, and the rectangle edges will ".. + "be light grey instead of dark grey.", + controls = { + { "C Key", "Copy: Moves selection into the clipboard" }, + { "X Key", "Cut: Clears canvas under the rectangle, and moves it into the clipboard" }, + { "V Key", "Paste: Copys clipboard to the canvas" }, + { "Z Key", "Clears clipboard" }, + { "Left Click", "Moves top-left corner of rectangle to selected pixel" }, + { "Right Click", "Opens selection menu" }, + { "Arrow Keys", "Moves rectangle one pixel in any direction" } + } + }, + [7] = { + name = "Corner Select Mode", + key = nil, + animonly = false, + textonly = false, + message = "If a selection rectangle isn't visible, this mode is selected automatically. It allows the ".. + "defining of the corners of the rectangle- one the top-left and bottom-right corners have been defined, ".. + "NPaintPro switches to selection mode. Note rectangle must be at least 2 pixels wide and high.", + controls = { + { "Left/Right Click", "Defines a corner of the selection rectangle" } + } + }, + [8] = { + name = "Play Mode", + key = "space", + animonly = true, + textonly = false, + message = "Play mode will loop through each frame in your animation at a constant rate. Editing tools are ".. + "locked in this mode, and the coordinate display will turn green to indicate it is on.", + controls = { + { "</> Keys", "Increases/Decreases speed of the animation" }, + { "Space Bar", "Returns to paint mode" } + } + }, + [9] = { + name = "Record Mode", + key = "\\", + animonly = true, + textonly = false, + message = "Record mode is not a true mode, but influences how other modes work. Changes made that modify the ".. + "canvas in record mode will affect ALL frames in the animation. The coordinates will turn red to indicate that ".. + "record mode is on.", + controls = { + { "", "Affects:" }, + { "- Paint Mode", "" }, + { "- Brush Mode", "" }, + { "- Cut and Paste in Select Mode", ""}, + { "- Move Mode", ""} + } + }, + [10] = { + name = "Hide Interface", + key = "~", + animonly = false, + textonly = false, + message = "Hides the sidebar and colour picker so only the image is visible.".. + " The program can be started with the interface hidden using the -d command line option.".. + " When hidden, if a file is animated it will automatically go to play mode.\n".. + "Note that all other input is locked until the display is revealed again in this".. + " mode.", + controls = { + { "</> Keys", "Increases/Decreases speed of the animation" }, + { "~ Key", "Shows interface"} + } + }, + [11] = { + name = "Help Mode", + key = "h", + animonly = false, + textonly = false, + message = "Displays this help screen. Clicking on options will display help on that topic. Clicking out of the screen".. + " will leave this mode.", + controls = { + { "Left/Right Click", "Displays a topic/Leaves the mode" } + } + }, + [12] = { + name = "File Mode", + key = nil, + animonly = false, + textonly = false, + message = "Clicking on the mode display at the bottom of the screen will open the options menu. Here you can".. + " activate all of the modes in the program with a simple mouse click. Pressing left control will open up the".. + " file menu automatically.", + controls = { + { "leftCtrl", "Opens the file menu" }, + { "leftAlt", "Opens the paint menu" } + } + }, + [13] = { + name = "Text Mode", + key = "t", + animonly = false, + textonly = true, + message = "In this mode, the user is able to type letters onto the document for display. The left colour ".. + "pallette value determines what colour the text will be, and the right determines what colour the background ".. + "will be (set either to nil to keep the same colours as already there).", + controls = { + { "Backspace", "Deletes the character on the previous line" }, + { "Arrow Keys", "Moves the cursor in any direction" }, + { "Left Click", "Moves the cursor to beneath the mouse cursor" } + } + }, + [14] = { + name = "Textpaint Mode", + key = "y", + animonly = false, + textonly = true, + message = "Allows the user to paint any text on screen to the desired colour with the mouse. If affects the text colour".. + " values rather than the background values, but operates identically to paint mode in all other regards.", + controls = { + { "Left Click", "Paints the text with the left colour" }, + { "Right Click", "Paints the text with the right colour" } + } + }, + [15] = { + name = "About NPaintPro", + keys = nil, + animonly = false, + textonly = false, + message = "NPaintPro: The feature-bloated paint program for ComputerCraft by Nitrogen Fingers.", + controls = { + { "Testers:", " "}, + { " ", "Faubiguy"}, + { " ", "TheOriginalBIT"} + } + } +} +--The "bounds" of the image- the first/last point on both axes where a pixel appears +local toplim,botlim,leflim,riglim = nil,nil,nil,nil +--The selected path +local sPath = nil + + +--Screen Size Parameters- decided dynamically further down the program +--Whether or not the help screen is available +local helpAvailable = true +--Whether or not the main menu is available +local mainAvailable = true +--Whether or not selection box dropdowns are available +local boxdropAvailable = true +--Whether or not a manual file descriptor option is available (part of the title) +local filemakerAvailable = true + +--[[ + Section: Helpers +]]-- + +--[[Converts a colour parameter into a single-digit hex coordinate for the colour + Params: colour:int = The colour to be converted + Returns:string A string conversion of the colour +]]-- +local function getHexOf(colour) + if not colour or not tonumber(colour) then + return " " + end + local value = math.log(colour)/math.log(2) + if value > 9 then + value = hexnums[value] + end + return value +end + +--[[Converts a hex digit into a colour value + Params: hex:?string = the hex digit to be converted + Returns:string A colour value corresponding to the hex, or nil if the character is invalid +]]-- +local function getColourOf(hex) + local value = tonumber(hex, 16) + if not value then return nil end + value = math.pow(2,value) + return value +end + +--[[Finds the largest width and height of the text in a given menu. Should conform to the format + of all standard menus (number indexed values and a 'name' field). + This is done recursively. It's just easier that way. + Params: menu:table = the table being tested for the max width and height + Returns:number,number = the max width and height of the text or names of any menu or submenu. +]]-- +local function findMaxWH(menu) + local wmax,hmax = #menu.name, #menu + for _,entry in pairs(menu) do + if type(entry) == "table" then + local nw,nh = findMaxWH(entry) + wmax = math.max(wmax,nw) + hmax = math.max(hmax,nh) + else + wmax = math.max(wmax,#entry) + end + end + return wmax,hmax +end + +--[[Determines what services are available depending on the size of the screen. Certain features + may be disabled with screen real estate does not allow for it. + Params: none + Returns:nil +]]-- +local function determineAvailableServices() + --Help files were designed to fit a 'standard' CC screen, of 51 x 19. The height of the screen + --needs to match the number of available options plus white space, but for consistency with + --the files themselves, a natural size of 51 is required for the screen width as well. + if w < 51 or h < #helpTopics+3 then helpAvailable = false end + if not helpAvailable then table.remove(ddModes,3) end + --These hard-coded values mirror the drawLogo values, with extra consideration for the + --additional menu options + if h < 14 or w < 24 then filemakerAvailable = false end + + --Menus can't cover the picker and need 2 spaces for branches. 4 whitespace on X total. + --Menus need a title and can't eclipse the footer. 2 whitespace on Y total. + local wmin,hmin = findMaxWH(ddModes) + if w < wmin+4 or h < hmin+2 then mainAvailable = false end + wmin,hmin = findMaxWH(srModes) + if w < wmin+4 or h < hmin+2 then boxdropAvailable = false end +end + +--[[Finds the biggest and smallest bounds of the image- the outside points beyond which pixels do not appear + These values are assigned to the "lim" parameters for access by other methods + Params: forAllFrames:bool = True if all frames should be used to find bounds, otherwise false or nil + Returns:nil +]]-- +local function updateImageLims(forAllFrames) + local f,l = sFrame,sFrame + if forAllFrames == true then f,l = 1,framecount end + + toplim,botlim,leflim,riglim = nil,nil,nil,nil + for locf = f,l do + for y,_ in pairs(frames[locf]) do + if type(y) == "number" then + for x,_ in pairs(frames[locf][y]) do + if frames[locf][y][x] ~= nil then + if leflim == nil or x < leflim then leflim = x end + if toplim == nil or y < toplim then toplim = y end + if riglim == nil or x > riglim then riglim = x end + if botlim == nil or y > botlim then botlim = y end + end + end + end + end + end + + --There is just... no easier way to do this. It's horrible, but necessary + if textEnabled then + for locf = f,l do + for y,_ in pairs(frames[locf].text) do + for x,_ in pairs(frames[locf].text[y]) do + if frames[locf].text[y][x] ~= nil then + if leflim == nil or x < leflim then leflim = x end + if toplim == nil or y < toplim then toplim = y end + if riglim == nil or x > riglim then riglim = x end + if botlim == nil or y > botlim then botlim = y end + end + end + end + for y,_ in pairs(frames[locf].textcol) do + for x,_ in pairs(frames[locf].textcol[y]) do + if frames[locf].textcol[y][x] ~= nil then + if leflim == nil or x < leflim then leflim = x end + if toplim == nil or y < toplim then toplim = y end + if riglim == nil or x > riglim then riglim = x end + if botlim == nil or y > botlim then botlim = y end + end + end + end + end + end +end + +--[[Determines how much of each material is required for a print. Done each time printing is called. + Params: none + Returns:table A complete list of how much of each material is required. +]]-- +function calculateMaterials() + updateImageLims(animated) + requiredMaterials = {} + for i=1,16 do + requiredMaterials[i] = 0 + end + + if not toplim then return end + + for i=1,#frames do + for y = toplim, botlim do + for x = leflim, riglim do + if type(frames[i][y][x]) == "number" then + requiredMaterials[math.log10(frames[i][y][x])/math.log10(2) + 1] = + requiredMaterials[math.log10(frames[i][y][x])/math.log10(2) + 1] + 1 + end + end + end + end +end + + +--[[Updates the rectangle blink timer. Should be called anywhere events are captured, along with a timer capture. + Params: nil + Returns:nil +]]-- +local function updateTimer(id) + if id == recttimer then + recttimer = os.startTimer(0.5) + rectblink = (rectblink % 2) + 1 + end +end + +--[[Constructs a message based on the state currently selected + Params: nil + Returns:string A message regarding the state of the application +]]-- +local function getStateMessage() + local msg = " "..string.upper(string.sub(state, 1, 1))..string.sub(state, 2, #state).." mode" + if state == "brush" then msg = msg..", size="..brushsize end + return msg +end + +--[[Calls the rednet_message event, but also looks for timer events to keep then + system timer ticking. + Params: timeout:number how long before the event times out + Returns:number the id of the sender + :number the message send +]]-- +local function rsTimeReceive(timeout) + local timerID + if timeout then timerID = os.startTimer(timeout) end + + local id,key,msg = nil,nil + while true do + id,key,msg = os.pullEvent() + + if id == "timer" then + if key == timerID then return + else updateTimer(key) end + end + if id == "rednet_message" then + return key,msg + end + end +end + +--[[Draws a picture, in paint table format on the screen + Params: image:table = the image to display + xinit:number = the x position of the top-left corner of the image + yinit:number = the y position of the top-left corner of the image + alpha:number = the color to display for the alpha channel. Default is white. + Returns:nil +]]-- +local function drawPictureTable(image, xinit, yinit, alpha) + if not alpha then alpha = 1 end + for y=1,#image do + for x=1,#image[y] do + term.setCursorPos(xinit + x-1, yinit + y-1) + local col = getColourOf(string.sub(image[y], x, x)) + if not col then col = alpha end + term.setBackgroundColour(col) + term.write(" ") + end + end +end + +--[[ + Section: Loading +]]-- + +--[[Loads a non-animted paint file into the program + Params: path:string = The path in which the file is located + Returns:nil +]]-- +local function loadNFP(path) + sFrame = 1 + frames[sFrame] = { } + if fs.exists(path) then + local file = io.open(path, "r" ) + local sLine = file:read() + local num = 1 + while sLine do + table.insert(frames[sFrame], num, {}) + for i=1,#sLine do + frames[sFrame][num][i] = getColourOf(string.sub(sLine,i,i)) + end + num = num+1 + sLine = file:read() + end + file:close() + end +end + +--[[Loads a text-paint file into the program + Params: path:string = The path in which the file is located + Returns:nil +]]-- +local function loadNFT(path) + sFrame = 1 + frames[sFrame] = { } + frames[sFrame].text = { } + frames[sFrame].textcol = { } + + if fs.exists(path) then + local file = io.open(path, "r") + local sLine = file:read() + local num = 1 + while sLine do + table.insert(frames[sFrame], num, {}) + table.insert(frames[sFrame].text, num, {}) + table.insert(frames[sFrame].textcol, num, {}) + + --As we're no longer 1-1, we keep track of what index to write to + local writeIndex = 1 + --Tells us if we've hit a 30 or 31 (BG and FG respectively)- next char specifies the curr colour + local bgNext, fgNext = false, false + --The current background and foreground colours + local currBG, currFG = nil,nil + term.setCursorPos(1,1) + for i=1,#sLine do + local nextChar = string.sub(sLine, i, i) + if nextChar:byte() == 30 then + bgNext = true + elseif nextChar:byte() == 31 then + fgNext = true + elseif bgNext then + currBG = getColourOf(nextChar) + bgNext = false + elseif fgNext then + currFG = getColourOf(nextChar) + fgNext = false + else + if nextChar ~= " " and currFG == nil then + currFG = colours.white + end + frames[sFrame][num][writeIndex] = currBG + frames[sFrame].textcol[num][writeIndex] = currFG + frames[sFrame].text[num][writeIndex] = nextChar + writeIndex = writeIndex + 1 + end + end + num = num+1 + sLine = file:read() + end + file:close() + end +end + +--[[Loads an animated paint file into the program + Params: path:string = The path in which the file is located + Returns:nil +]]-- +local function loadNFA(path) + frames[sFrame] = { } + if fs.exists(path) then + local file = io.open(path, "r" ) + local sLine = file:read() + local num = 1 + while sLine do + table.insert(frames[sFrame], num, {}) + if sLine == "~" then + sFrame = sFrame + 1 + frames[sFrame] = { } + num = 1 + else + for i=1,#sLine do + frames[sFrame][num][i] = getColourOf(string.sub(sLine,i,i)) + end + num = num+1 + end + sLine = file:read() + end + file:close() + end + framecount = sFrame + sFrame = 1 +end + +--[[Saves a non-animated paint file to the specified path + Params: path:string = The path to save the file to + Returns:nil +]]-- +local function saveNFP(path) + local sDir = string.sub(sPath, 1, #sPath - #fs.getName(sPath)) + if not fs.exists(sDir) then + fs.makeDir(sDir) + end + + local file = io.open(path, "w") + updateImageLims(false) + if not toplim then + file:close() + return + end + for y=1,botlim do + local line = "" + if frames[sFrame][y] then + for x=1,riglim do + line = line..getHexOf(frames[sFrame][y][x]) + end + end + file:write(line.."\n") + end + file:close() +end + +--[[Saves a text-paint file to the specified path + Params: path:string = The path to save the file to + Returns:nil +]]-- +local function saveNFT(path) + local sDir = string.sub(sPath, 1, #sPath - #fs.getName(sPath)) + if not fs.exists(sDir) then + fs.makeDir(sDir) + end + + local file = io.open(path, "w") + updateImageLims(false) + if not toplim then + file:close() + return + end + for y=1,botlim do + local line = "" + local currBG, currFG = nil,nil + for x=1,riglim do + if frames[sFrame][y] and frames[sFrame][y][x] ~= currBG then + line = line..string.char(30)..getHexOf(frames[sFrame][y][x]) + currBG = frames[sFrame][y][x] + end + if frames[sFrame].textcol[y] and frames[sFrame].textcol[y][x] ~= currFG then + line = line..string.char(31)..getHexOf(frames[sFrame].textcol[y][x]) + currFG = frames[sFrame].textcol[y][x] + end + if frames[sFrame].text[y] then + local char = frames[sFrame].text[y][x] + if not char then char = " " end + line = line..char + end + end + file:write(line.."\n") + end + file:close() +end + +--[[Saves a animated paint file to the specified path + Params: path:string = The path to save the file to + Returns:nil +]]-- +local function saveNFA(path) + local sDir = string.sub(sPath, 1, #sPath - #fs.getName(sPath)) + if not fs.exists(sDir) then + fs.makeDir(sDir) + end + + local file = io.open(path, "w") + updateImageLims(true) + if not toplim then + file:close() + return + end + for i=1,#frames do + for y=1,botlim do + local line = "" + if frames[i][y] then + for x=1,riglim do + line = line..getHexOf(frames[i][y][x]) + end + end + file:write(line.."\n") + end + if i < #frames then file:write("~\n") end + end + file:close() +end + +--[[Runs a special pre-program dialogue to determine the filename and filepath. Done if + there's room, and a file name hasn't been specified + Params: none + Returns:bool= true if file is created; false otherwise +]]-- +local function runFileMaker() + local newFName = "" + local fileType = 1 + if animated then fileType = 2 + elseif textEnabled then fileType = 3 end + + local tlx,tly = math.floor(w/2 - #logo[1]/2), math.floor(h/2 + #logo/2 + 1) + + --This is done on top of the logo, so it backpedals a bit. + term.setCursorPos(tlx, tly) + term.clearLine() + term.write("Name: ") + term.setCursorPos(tlx, tly + 1) + term.clearLine() + term.write("Filetype: Sprite") + term.setCursorPos(tlx + 12, tly + 2) + term.write("Animation") + term.setCursorPos(tlx + 12, tly + 3) + term.write("Text") + + while true do + term.setCursorPos(tlx + 6, tly) + term.setBackgroundColour(colours.lightGrey) + term.setTextColour(colours.grey) + term.write(newFName..string.rep(" ", 15-#newFName)) + term.setBackgroundColour(colours.white) + term.setTextColour(colours.black) + local extension = ".nfp" + if fileType == 2 then extension = ".nfa" + elseif fileType == 3 then extension = ".nft" end + term.write(extension) + + term.setBackgroundColour(colours.lightGrey) + term.setTextColour(colours.grey) + for i=1,3 do + term.setCursorPos(tlx + 24, tly + i) + if i==fileType then term.write("X") + else term.write(" ") end + end + + local fPath = shell.resolve(newFName..extension) + term.setCursorPos(tlx, tly + 3) + local fileValid = true + if (fs.exists(fPath) and fs.isDir(fPath)) or newFName == "" then + term.setBackgroundColour(colours.white) + term.setTextColour(colours.red) + term.write("Invalid ") + fileValid = false + elseif fs.exists(fPath) then + term.setBackgroundColour(colours.grey) + term.setTextColour(colours.lightGrey) + term.write(" Edit ") + else + term.setBackgroundColour(colours.grey) + term.setTextColour(colours.lightGrey) + term.write(" Create ") + end + + term.setTextColour(colours.grey) + term.setCursorPos(tlx + 6 + #newFName, tly) + term.setCursorBlink(true) + + local id,p1,p2,p3 = os.pullEvent() + if id == "key" then + if p1 == keys.backspace and #newFName > 0 then + newFName = string.sub(newFName, 1, #newFName-1) + elseif p1 == keys.enter and fileValid then + sPath = fPath + return true + end + elseif id == "char" and p1 ~= "." and p1 ~= " " and #newFName < 15 then + newFName = newFName..p1 + elseif id == "mouse_click" then + --The option boxes. Man, hardcoding is ugly... + if p2 == tlx + 24 then + for i=1,3 do + if p3 == tly+i then fileType = i end + end + end + if p3 == tly + 3 and p2 >= tlx and p2 <= tlx + 8 and fileValid then + sPath = fPath + return true + end + end + end +end + +--[[Initializes the program, by loading in the paint file. Called at the start of each program. + Params: none + Returns:nil +]]-- +local function init() + if textEnabled then + loadNFT(sPath) + table.insert(ddModes, 2, { "text", "textpaint", name = "text"}) + elseif animated then + loadNFA(sPath) + table.insert(ddModes, #ddModes, { "record", "play", name = "anim" }) + table.insert(ddModes, #ddModes, { "go to", "remove", name = "frames"}) + table.insert(ddModes[2], #ddModes[2], "blueprint on") + table.insert(ddModes[2], #ddModes[2], "layers on") + else + loadNFP(sPath) + table.insert(ddModes[2], #ddModes[2], "blueprint on") + end + + for i=0,15 do + table.insert(column, math.pow(2,i)) + end +end + +--[[ + Section: Drawing +]]-- + + +--[[Draws the rather superflous logo. Takes about 1 second, before user is able to move to the + actual program. + Params: none + Returns:bool= true if the file select ran successfully; false otherwise. +]]-- +local function drawLogo() + term.setBackgroundColour(colours.white) + term.clear() + if h >= 12 and w >= 24 then + drawPictureTable(logo, w/2 - #logo[1]/2, h/2 - #logo/2, colours.white) + term.setBackgroundColour(colours.white) + term.setTextColour(colours.black) + local msg = "NPaintPro" + term.setCursorPos(w/2 - #msg/2, h/2 + #logo/2 + 1) + term.write(msg) + msg = "By NitrogenFingers" + term.setCursorPos(w/2 - #msg/2, h/2 + #logo/2 + 2) + term.write(msg) + elseif w >= 15 then + local msg = "NPaintPro" + term.setCursorPos(math.ceil(w/2 - #msg/2), h/2) + term.setTextColour(colours.cyan) + term.write(msg) + msg = "NitrogenFingers" + term.setCursorPos(math.ceil(w/2 - #msg/2), h/2 + 1) + term.setTextColour(colours.black) + term.write(msg) + else + local msg = "NPP" + term.setCursorPos(math.ceil(w/2 - #msg/2), math.floor(h/2)) + term.setTextColour(colours.cyan) + term.write(msg) + msg = "By NF" + term.setCursorPos(math.ceil(w/2 - #msg/2), math.ceil(h/2)) + term.setTextColour(colours.black) + term.write(msg) + end + os.pullEvent() +end + +--[[Clears the display to the alpha channel colour, draws the canvas, the image buffer and the selection + rectanlge if any of these things are present. + Params: none + Returns:nil +]]-- +local function drawCanvas() + --We have to readjust the position of the canvas if we're printing + turtlechar = "@" + if state == "active print" then + if layering == "up" then + if py >= 1 and py <= #frames then + sFrame = py + end + if pz < sy then sy = pz + elseif pz > sy + h - 1 then sy = pz + h - 1 end + if px < sx then sx = px + elseif px > sx + w - 2 then sx = px + w - 2 end + else + if pz >= 1 and pz <= #frames then + sFrame = pz + end + + if py < sy then sy = py + elseif py > sy + h - 1 then sy = py + h - 1 end + if px < sx then sx = px + elseif px > sx + w - 2 then sx = px + w - 2 end + end + + if pfx == 1 then turtlechar = ">" + elseif pfx == -1 then turtlechar = "<" + elseif pfz == 1 then turtlechar = "V" + elseif pfz == -1 then turtlechar = "^" + end + end + + --Picture next + local topLayer, botLayer + if layerDisplay then + topLayer = sFrame + botLayer = 1 + else + topLayer,botLayer = sFrame,sFrame + end + + --How far the canvas draws. If the interface is visible, it stops short of that. + local wlim,hlim = 0,0 + if not interfaceHidden then + wlim = 2 + hlim = 1 + end + + for currframe = botLayer,topLayer,1 do + for y=sy+1,sy+h-hlim do + if frames[currframe][y] then + for x=sx+1,sx+w-wlim do + term.setCursorPos(x-sx,y-sy) + if frames[currframe][y][x] then + term.setBackgroundColour(frames[currframe][y][x]) + if textEnabled and frames[currframe].textcol[y][x] and frames[currframe].text[y][x] then + term.setTextColour(frames[currframe].textcol[y][x]) + term.write(frames[currframe].text[y][x]) + else + term.write(" ") + end + else + tileExists = false + for i=currframe-1,botLayer,-1 do + if frames[i][y][x] then + tileExists = true + break + end + end + + if not tileExists then + if blueprint then + term.setBackgroundColour(colours.blue) + term.setTextColour(colours.white) + if x == sx+1 and y % 4 == 1 then + term.write(""..((y/4) % 10)) + elseif y == sy + 1 and x % 4 == 1 then + term.write(""..((x/4) % 10)) + elseif x % 2 == 1 and y % 2 == 1 then + term.write("+") + elseif x % 2 == 1 then + term.write("|") + elseif y % 2 == 1 then + term.write("-") + else + term.write(" ") + end + else + term.setBackgroundColour(alphaC) + if textEnabled and frames[currframe].textcol[y][x] and frames[currframe].text[y][x] then + term.setTextColour(frames[currframe].textcol[y][x]) + term.write(frames[currframe].text[y][x]) + else + term.write(" ") + end + end + end + end + end + else + for x=sx+1,sx+w-wlim do + term.setCursorPos(x-sx,y-sy) + + tileExists = false + for i=currframe-1,botLayer,-1 do + if frames[i][y] and frames[i][y][x] then + tileExists = true + break + end + end + + if not tileExists then + if blueprint then + term.setBackgroundColour(colours.blue) + term.setTextColour(colours.white) + if x == sx+1 and y % 4 == 1 then + term.write(""..((y/4) % 10)) + elseif y == sy + 1 and x % 4 == 1 then + term.write(""..((x/4) % 10)) + elseif x % 2 == 1 and y % 2 == 1 then + term.write("+") + elseif x % 2 == 1 then + term.write("|") + elseif y % 2 == 1 then + term.write("-") + else + term.write(" ") + end + else + term.setBackgroundColour(alphaC) + term.write(" ") + end + end + end + end + end + end + + --Then the printer, if he's on + if state == "active print" then + local bgColour = alphaC + if layering == "up" then + term.setCursorPos(px-sx,pz-sy) + if frames[sFrame] and frames[sFrame][pz-sy] and frames[sFrame][pz-sy][px-sx] then + bgColour = frames[sFrame][pz-sy][px-sx] + elseif blueprint then bgColour = colours.blue end + else + term.setCursorPos(px-sx,py-sy) + if frames[sFrame] and frames[sFrame][py-sy] and frames[sFrame][py-sy][px-sx] then + bgColour = frames[sFrame][py-sy][px-sx] + elseif blueprint then bgColour = colours.blue end + end + + term.setBackgroundColour(bgColour) + if bgColour == colours.black then term.setTextColour(colours.white) + else term.setTextColour(colours.black) end + + term.write(turtlechar) + end + + --Then the buffer + if selectrect then + if buffer and rectblink == 1 then + for y=selectrect.y1, math.min(selectrect.y2, selectrect.y1 + buffer.height-1) do + for x=selectrect.x1, math.min(selectrect.x2, selectrect.x1 + buffer.width-1) do + if buffer.contents[y-selectrect.y1+1][x-selectrect.x1+1] then + term.setCursorPos(x+sx,y+sy) + term.setBackgroundColour(buffer.contents[y-selectrect.y1+1][x-selectrect.x1+1]) + term.write(" ") + end + end + end + end + + --This draws the "selection" box + local add = nil + if buffer then + term.setBackgroundColour(colours.lightGrey) + else + term.setBackgroundColour(colours.grey) + end + for i=selectrect.x1, selectrect.x2 do + add = (i + selectrect.y1 + rectblink) % 2 == 0 + term.setCursorPos(i-sx,selectrect.y1-sy) + if add then term.write(" ") end + add = (i + selectrect.y2 + rectblink) % 2 == 0 + term.setCursorPos(i-sx,selectrect.y2-sy) + if add then term.write(" ") end + end + for i=selectrect.y1 + 1, selectrect.y2 - 1 do + add = (i + selectrect.x1 + rectblink) % 2 == 0 + term.setCursorPos(selectrect.x1-sx,i-sy) + if add then term.write(" ") end + add = (i + selectrect.x2 + rectblink) % 2 == 0 + term.setCursorPos(selectrect.x2-sx,i-sy) + if add then term.write(" ") end + end + end +end + +--[[Draws the colour picker on the right side of the screen, the colour pallette and the footer with any + messages currently being displayed + Params: none + Returns:nil +]]-- +local function drawInterface() + --Picker + local coffset,ioffset = 0,0 + local maxcsize = h-2 + if h < #column + 2 then + maxcsize = h-4 + coffset = columnoffset + ioffset = 1 + term.setBackgroundColour(colours.lightGrey) + term.setTextColour(colours.grey) + term.setCursorPos(w-1,1) + term.write("^^") + term.setCursorPos(w-1,h-2) + term.write("VV") + end + for i=1,math.min(#column+1,maxcsize) do + term.setCursorPos(w-1, i + ioffset) + local ci = i+coffset + if ci == #column+1 then + term.setBackgroundColour(colours.black) + term.setTextColour(colours.red) + term.write("XX") + elseif state == "print" then + term.setBackgroundColour(column[ci]) + if column[ci] == colours.black then + term.setTextColour(colours.white) + else term.setTextColour(colours.black) end + + if requirementsDisplayed then + if requiredMaterials[i] < 10 then term.write(" ") end + term.setCursorPos(w-#tostring(requiredMaterials[i])+1, i) + term.write(requiredMaterials[i]) + else + if i+coffset < 10 then term.write(" ") end + term.write(i+coffset) + end + else + term.setBackgroundColour(column[ci]) + term.write(" ") + end + end + --Filling the whitespace with... 'greyspace' *shudder* + if h > #column+3 then + term.setTextColour(colours.grey) + term.setBackgroundColour(colours.lightGrey) + for y=#column+2,h-2 do + term.setCursorPos(w-1,y) + term.write("| ") + end + end + --Pallette + term.setCursorPos(w-1,h-1) + if not lSel then + term.setBackgroundColour(colours.black) + term.setTextColour(colours.red) + term.write("X") + else + term.setBackgroundColour(lSel) + term.setTextColour(lSel) + term.write(" ") + end + if not rSel then + term.setBackgroundColour(colours.black) + term.setTextColour(colours.red) + term.write("X") + else + term.setBackgroundColour(rSel) + term.setTextColour(rSel) + term.write(" ") + end + --Footer + if inMenu then return end + + term.setCursorPos(1, h) + term.setBackgroundColour(colours.lightGrey) + term.setTextColour(colours.grey) + term.clearLine() + if mainAvailable then + if inDropDown then + term.write(string.rep(" ", #ddModes.name + 2)) + else + term.setBackgroundColour(colours.grey) + term.setTextColour(colours.lightGrey) + term.write(ddModes.name.." ") + end + end + term.setBackgroundColour(colours.lightGrey) + term.setTextColour(colours.grey) + term.write(getStateMessage()) + + local coords="X:"..sx.." Y:"..sy + if animated then coords = coords.." Frame:"..sFrame.."/"..framecount.." " end + term.setCursorPos(w-#coords+1,h) + if state == "play" then term.setBackgroundColour(colours.lime) + elseif record then term.setBackgroundColour(colours.red) end + term.write(coords) + + if animated then + term.setCursorPos(w-1,h) + term.setBackgroundColour(colours.grey) + term.setTextColour(colours.lightGrey) + term.write("<>") + end +end + +--[[Runs an interface where users can select topics of help. Will return once the user quits the help screen. + Params: none + Returns:nil +]]-- +local function drawHelpScreen() + local selectedHelp = nil + while true do + term.setBackgroundColour(colours.lightGrey) + term.clear() + if not selectedHelp then + term.setCursorPos(4, 1) + term.setTextColour(colours.brown) + term.write("Available modes (click for info):") + for i=1,#helpTopics do + term.setCursorPos(2, 2 + i) + term.setTextColour(colours.black) + term.write(helpTopics[i].name) + if helpTopics[i].key then + term.setTextColour(colours.red) + term.write(" ("..helpTopics[i].key..")") + end + end + term.setCursorPos(4,h) + term.setTextColour(colours.black) + term.write("Press any key to exit") + else + term.setCursorPos(4,1) + term.setTextColour(colours.brown) + term.write(helpTopics[selectedHelp].name) + if helpTopics[selectedHelp].key then + term.setTextColour(colours.red) + term.write(" ("..helpTopics[selectedHelp].key..")") + end + term.setCursorPos(1,3) + term.setTextColour(colours.black) + print(helpTopics[selectedHelp].message.."\n") + for i=1,#helpTopics[selectedHelp].controls do + term.setTextColour(colours.brown) + term.write(helpTopics[selectedHelp].controls[i][1].." ") + term.setTextColour(colours.black) + print(helpTopics[selectedHelp].controls[i][2]) + end + end + + local id,p1,p2,p3 = os.pullEvent() + + if id == "timer" then updateTimer(p1) + elseif id == "key" then + if selectedHelp then selectedHelp = nil + else break end + elseif id == "mouse_click" then + if not selectedHelp then + if p3 >=3 and p3 <= 2+#helpTopics then + selectedHelp = p3-2 + else break end + else + selectedHelp = nil + end + end + end +end + +--[[Draws a message in the footer bar. A helper for DrawInterface, but can be called for custom messages, if the + inMenu paramter is set to true while this is being done (remember to set it back when done!) + Params: message:string = The message to be drawn + Returns:nil +]]-- +local function drawMessage(message) + term.setCursorPos(1,h) + term.setBackgroundColour(colours.lightGrey) + term.setTextColour(colours.grey) + term.clearLine() + term.write(message) +end + +--[[ + Section: Generic Interfaces +]]-- + + +--[[One of my generic text printing methods, printing a message at a specified position with width and offset. + No colour materials included. + Params: msg:string = The message to print off-center + height:number = The starting height of the message + width:number = The limit as to how many characters long each line may be + offset:number = The starting width offset of the message + Returns:number the number of lines used in printing the message +]]-- +local function wprintOffCenter(msg, height, width, offset) + local inc = 0 + local ops = 1 + while #msg - ops > width do + local nextspace = 0 + while string.find(msg, " ", ops + nextspace) and + string.find(msg, " ", ops + nextspace) - ops < width do + nextspace = string.find(msg, " ", nextspace + ops) + 1 - ops + end + local ox,oy = term.getCursorPos() + term.setCursorPos(width/2 - (nextspace)/2 + offset, height + inc) + inc = inc + 1 + term.write(string.sub(msg, ops, nextspace + ops - 1)) + ops = ops + nextspace + end + term.setCursorPos(width/2 - #string.sub(msg, ops)/2 + offset, height + inc) + term.write(string.sub(msg, ops)) + + return inc + 1 +end + +--[[Draws a message that must be clicked on or a key struck to be cleared. No options, so used for displaying + generic information. + Params: ctitle:string = The title of the confirm dialogue + msg:string = The message displayed in the dialogue + Returns:nil +]]-- +local function displayConfirmDialogue(ctitle, msg) + local dialogoffset = 8 + --We actually print twice- once to get the lines, second time to print proper. Easier this way. + local lines = wprintOffCenter(msg, 5, w - (dialogoffset+2) * 2, dialogoffset + 2) + + term.setCursorPos(dialogoffset, 3) + term.setBackgroundColour(colours.grey) + term.setTextColour(colours.lightGrey) + term.write(string.rep(" ", w - dialogoffset * 2)) + term.setCursorPos(dialogoffset + (w - dialogoffset * 2)/2 - #ctitle/2, 3) + term.write(ctitle) + term.setTextColour(colours.grey) + term.setBackgroundColour(colours.lightGrey) + term.setCursorPos(dialogoffset, 4) + term.write(string.rep(" ", w - dialogoffset * 2)) + for i=5,5+lines do + term.setCursorPos(dialogoffset, i) + term.write(" "..string.rep(" ", w - (dialogoffset) * 2 - 2).." ") + end + wprintOffCenter(msg, 5, w - (dialogoffset+2) * 2, dialogoffset + 2) + + --In the event of a message, the player hits anything to continue + while true do + local id,key = os.pullEvent() + if id == "timer" then updateTimer(key); + elseif id == "key" or id == "mouse_click" or id == "mouse_drag" then break end + end +end + +--[[Produces a nice dropdown menu based on a table of strings. Depending on the position, this will auto-adjust the position + of the menu drawn, and allows nesting of menus and sub menus. Clicking anywhere outside the menu will cancel and return nothing + Params: x:int = the x position the menu should be displayed at + y:int = the y position the menu should be displayed at + options:table = the list of options available to the user, as strings or submenus (tables of strings, with a name parameter) + Returns:string the selected menu option. +]]-- +local function displayDropDown(x, y, options) + inDropDown = true + --Figures out the dimensions of our thing + local longestX = #options.name + for i=1,#options do + local currVal = options[i] + if type(currVal) == "table" then currVal = currVal.name end + + longestX = math.max(longestX, #currVal) + end + local xOffset = math.max(0, longestX - ((w-2) - x) + 1) + local yOffset = math.max(0, #options - ((h-1) - y)) + + local clickTimes = 0 + local tid = nil + local selection = nil + while clickTimes < 2 do + drawCanvas() + drawInterface() + + term.setCursorPos(x-xOffset,y-yOffset) + term.setBackgroundColour(colours.grey) + term.setTextColour(colours.lightGrey) + term.write(options.name..string.rep(" ", longestX-#options.name + 2)) + + for i=1,#options do + term.setCursorPos(x-xOffset, y-yOffset+i) + if i==selection and clickTimes % 2 == 0 then + term.setBackgroundColour(colours.grey) + term.setTextColour(colours.lightGrey) + else + term.setBackgroundColour(colours.lightGrey) + term.setTextColour(colours.grey) + end + local currVal = options[i] + if type(currVal) == "table" then + term.write(currVal.name..string.rep(" ", longestX-#currVal.name + 1)) + term.setBackgroundColour(colours.grey) + term.setTextColour(colours.lightGrey) + term.write(">") + else + term.write(currVal..string.rep(" ", longestX-#currVal + 2)) + end + end + + local id, p1, p2, p3 = os.pullEvent() + if id == "timer" then + if p1 == tid then + clickTimes = clickTimes + 1 + if clickTimes > 2 then + break + else + tid = os.startTimer(0.1) + end + else + updateTimer(p1) + drawCanvas() + drawInterface() + end + elseif id == "mouse_click" then + if p2 >=x-xOffset and p2 <= x-xOffset + longestX + 1 and p3 >= y-yOffset+1 and p3 <= y-yOffset+#options then + selection = p3-(y-yOffset) + tid = os.startTimer(0.1) + else + selection = "" + break + end + end + end + + if type(selection) == "number" then + selection = options[selection] + end + + if type(selection) == "string" then + inDropDown = false + return selection + elseif type(selection) == "table" then + return displayDropDown(x, y, selection) + end +end + +--[[A custom io.read() function with a few differences- it limits the number of characters being printed, + waits a 1/100th of a second so any keys still in the event library are removed before input is read and + the timer for the selectionrectangle is continuously updated during the process. + Params: lim:int = the number of characters input is allowed + Returns:string the inputted string, trimmed of leading and tailing whitespace +]]-- +local function readInput(lim) + term.setCursorBlink(true) + + local inputString = "" + if not lim or type(lim) ~= "number" or lim < 1 then lim = w - ox end + local ox,oy = term.getCursorPos() + --We only get input from the footer, so this is safe. Change if recycling + term.setBackgroundColour(colours.lightGrey) + term.setTextColour(colours.grey) + term.write(string.rep(" ", lim)) + term.setCursorPos(ox, oy) + --As events queue immediately, we may get an unwanted key... this will solve that problem + local inputTimer = os.startTimer(0.01) + local keysAllowed = false + + while true do + local id,key = os.pullEvent() + + if keysAllowed then + if id == "key" and key == 14 and #inputString > 0 then + inputString = string.sub(inputString, 1, #inputString-1) + term.setCursorPos(ox + #inputString,oy) + term.write(" ") + elseif id == "key" and key == 28 and inputString ~= string.rep(" ", #inputString) then + break + elseif id == "key" and key == keys.leftCtrl then + return "" + elseif id == "char" and #inputString < lim then + inputString = inputString..key + end + end + + if id == "timer" then + if key == inputTimer then + keysAllowed = true + else + updateTimer(key) + drawCanvas() + drawInterface() + term.setBackgroundColour(colours.lightGrey) + term.setTextColour(colours.grey) + end + end + term.setCursorPos(ox,oy) + term.write(inputString) + term.setCursorPos(ox + #inputString, oy) + end + + while string.sub(inputString, 1, 1) == " " do + inputString = string.sub(inputString, 2, #inputString) + end + while string.sub(inputString, #inputString, #inputString) == " " do + inputString = string.sub(inputString, 1, #inputString-1) + end + term.setCursorBlink(false) + + return inputString +end + +--[[ + Section: Image tools +]]-- + + +--[[Copies all pixels beneath the selection rectangle into the image buffer. Empty buffers are converted to nil. + Params: removeImage:bool = true if the image is to be erased after copying, false otherwise + Returns:nil +]]-- +local function copyToBuffer(removeImage) + buffer = { width = selectrect.x2 - selectrect.x1 + 1, height = selectrect.y2 - selectrect.y1 + 1, contents = { } } + + local containsSomething = false + for y=1,buffer.height do + buffer.contents[y] = { } + local f,l = sFrame,sFrame + if record then f,l = 1, framecount end + + for fra = f,l do + if frames[fra][selectrect.y1 + y - 1] then + for x=1,buffer.width do + buffer.contents[y][x] = frames[sFrame][selectrect.y1 + y - 1][selectrect.x1 + x - 1] + if removeImage then frames[fra][selectrect.y1 + y - 1][selectrect.x1 + x - 1] = nil end + if buffer.contents[y][x] then containsSomething = true end + end + end + end + end + --I don't classify an empty buffer as a real buffer- confusing to the user. + if not containsSomething then buffer = nil end +end + +--[[Replaces all pixels under the selection rectangle with the image buffer (or what can be seen of it). Record-dependent. + Params: removeBuffer:bool = true if the buffer is to be emptied after copying, false otherwise + Returns:nil +]]-- +local function copyFromBuffer(removeBuffer) + if not buffer then return end + + for y = 1, math.min(buffer.height,selectrect.y2-selectrect.y1+1) do + local f,l = sFrame, sFrame + if record then f,l = 1, framecount end + + for fra = f,l do + if not frames[fra][selectrect.y1+y-1] then frames[fra][selectrect.y1+y-1] = { } end + for x = 1, math.min(buffer.width,selectrect.x2-selectrect.x1+1) do + frames[fra][selectrect.y1+y-1][selectrect.x1+x-1] = buffer.contents[y][x] + end + end + end + + if removeBuffer then buffer = nil end +end + +--[[Moves the entire image (or entire animation) to the specified coordinates. Record-dependent. + Params: newx:int = the X coordinate to move the image to + newy:int = the Y coordinate to move the image to + Returns:nil +]]-- +local function moveImage(newx,newy) + if not leflim or not toplim then return end + if newx <=0 or newy <=0 then return end + local f,l = sFrame,sFrame + if record then f,l = 1,framecount end + + for i=f,l do + local newlines = { } + for y=toplim,botlim do + local line = frames[i][y] + if line then + newlines[y-toplim+newy] = { } + for x,char in pairs(line) do + newlines[y-toplim+newy][x-leflim+newx] = char + end + end + end + --Exceptions that allow us to move the text as well + if textEnabled then + newlines.text = { } + for y=toplim,botlim do + local line = frames[i].text[y] + if line then + newlines.text[y-toplim+newy] = { } + for x,char in pairs(line) do + newlines.text[y-toplim+newy][x-leflim+newx] = char + end + end + end + + newlines.textcol = { } + for y=toplim,botlim do + local line = frames[i].textcol[y] + if line then + newlines.textcol[y-toplim+newy] = { } + for x,char in pairs(line) do + newlines.textcol[y-toplim+newy][x-leflim+newx] = char + end + end + end + end + + frames[i] = newlines + end +end + +--[[Prompts the user to clear the current frame or all frames. Record-dependent., + Params: none + Returns:nil +]]-- +local function clearImage() + inMenu = true + if not animated then + drawMessage("Clear image? Y/N: ") + elseif record then + drawMessage("Clear ALL frames? Y/N: ") + else + drawMessage("Clear current frame? Y/N :") + end + if string.find(string.upper(readInput(1)), "Y") then + local f,l = sFrame,sFrame + if record then f,l = 1,framecount end + + for i=f,l do + frames[i] = { } + end + end + inMenu = false +end + +--[[A recursively called method (watch out for big calls!) in which every pixel of a set colour is + changed to another colour. Does not work on the nil colour, for obvious reasons. + Params: x:int = The X coordinate of the colour to flood-fill + y:int = The Y coordinate of the colour to flood-fill + targetColour:colour = the colour that is being flood-filled + newColour:colour = the colour with which to replace the target colour + Returns:nil +]]-- +local function floodFill(x, y, targetColour, newColour) + if not newColour or not targetColour then return end + local nodeList = { } + + table.insert(nodeList, {x = x, y = y}) + + while #nodeList > 0 do + local node = nodeList[1] + if frames[sFrame][node.y] and frames[sFrame][node.y][node.x] == targetColour then + frames[sFrame][node.y][node.x] = newColour + table.insert(nodeList, { x = node.x + 1, y = node.y}) + table.insert(nodeList, { x = node.x, y = node.y + 1}) + if x > 1 then table.insert(nodeList, { x = node.x - 1, y = node.y}) end + if y > 1 then table.insert(nodeList, { x = node.x, y = node.y - 1}) end + end + table.remove(nodeList, 1) + end +end + +--[[ + Section: Animation Tools +]]-- + +--[[Enters play mode, allowing the animation to play through. Interface is restricted to allow this, + and method only leaves once the player leaves play mode. + Params: none + Returns:nil +]]-- +local function playAnimation() + state = "play" + selectedrect = nil + + local animt = os.startTimer(animtime) + repeat + drawCanvas() + drawInterface() + + local id,key,_,y = os.pullEvent() + + if id=="timer" then + if key == animt then + animt = os.startTimer(animtime) + sFrame = (sFrame % framecount) + 1 + else + updateTimer(key) + end + elseif id=="key" then + if key == keys.comma and animtime > 0.1 then animtime = animtime - 0.05 + elseif key == keys.period and animtime < 0.5 then animtime = animtime + 0.05 + elseif key == keys.space then state = "paint" end + elseif id=="mouse_click" and y == h then + state = "paint" + end + until state ~= "play" + os.startTimer(0.5) +end + +--[[Changes the selected frame (sFrame) to the chosen frame. If this frame is above the framecount, + additional frames are created with a copy of the image on the selected frame. + Params: newframe:int = the new frame to move to + Returns:nil +]]-- +local function changeFrame(newframe) + inMenu = true + if not tonumber(newframe) then + term.setCursorPos(1,h) + term.setBackgroundColour(colours.lightGrey) + term.setTextColour(colours.grey) + term.clearLine() + + term.write("Go to frame: ") + newframe = tonumber(readInput(2)) + if not newframe or newframe <= 0 then + inMenu = false + return + end + elseif newframe <= 0 then return end + + if newframe > framecount then + for i=framecount+1,newframe do + frames[i] = {} + for y,line in pairs(frames[sFrame]) do + frames[i][y] = { } + for x,v in pairs(line) do + frames[i][y][x] = v + end + end + end + framecount = newframe + end + sFrame = newframe + inMenu = false +end + +--[[Removes every frame leading after the frame passed in + Params: frame:int the non-inclusive lower bounds of the delete + Returns:nil +]]-- +local function removeFramesAfter(frame) + inMenu = true + if frame==framecount then return end + drawMessage("Remove frames "..(frame+1).."/"..framecount.."? Y/N :") + local answer = string.upper(readInput(1)) + + if string.find(answer, string.upper("Y")) ~= 1 then + inMenu = false + return + end + + for i=frame+1, framecount do + frames[i] = nil + end + framecount = frame + inMenu = false +end + +--[[ + Section: Printing Tools +]]-- + +--[[Constructs a new facing to the left of the current facing + Params: curx:number = The facing on the X axis + curz:number = The facing on the Z axis + hand:string = The hand of the axis ("right" or "left") + Returns:number,number = the new facing on the X and Z axis after a left turn +]]-- +local function getLeft(curx, curz) + local hand = "left" + if layering == "up" then hand = "right" end + + if hand == "right" then + if curx == 1 then return 0,-1 end + if curx == -1 then return 0,1 end + if curz == 1 then return 1,0 end + if curz == -1 then return -1,0 end + else + if curx == 1 then return 0,1 end + if curx == -1 then return 0,-1 end + if curz == 1 then return -1,0 end + if curz == -1 then return 1,0 end + end +end + +--[[Constructs a new facing to the right of the current facing + Params: curx:number = The facing on the X axis + curz:number = The facing on the Z axis + hand:string = The hand of the axis ("right" or "left") + Returns:number,number = the new facing on the X and Z axis after a right turn +]]-- +local function getRight(curx, curz) + local hand = "left" + if layering == "up" then hand = "right" end + + if hand == "right" then + if curx == 1 then return 0,1 end + if curx == -1 then return 0,-1 end + if curz == 1 then return -1,0 end + if curz == -1 then return 1,0 end + else + if curx == 1 then return 0,-1 end + if curx == -1 then return 0,1 end + if curz == 1 then return 1,0 end + if curz == -1 then return -1,0 end + end +end + + +--[[Sends out a rednet signal requesting local printers, and will listen for any responses. Printers found are added to the + printerList (for ID's) and printerNames (for names) + Params: nil + Returns:nil +]]-- +local function locatePrinters() + printerList = { } + printerNames = { name = "Printers" } + local oldState = state + state = "Locating printers, please wait... " + drawCanvas() + drawInterface() + state = oldState + + local modemOpened = false + for k,v in pairs(rs.getSides()) do + if peripheral.isPresent(v) and peripheral.getType(v) == "modem" then + rednet.open(v) + modemOpened = true + break + end + end + + if not modemOpened then + displayConfirmDialogue("Modem not found!", "No modem peripheral. Must have network modem to locate printers.") + return false + end + + rednet.broadcast("$3DPRINT IDENTIFY") + + while true do + local id, msg = rsTimeReceive(1) + + if not id then break end + if string.find(msg, "$3DPRINT IDACK") == 1 then + msg = string.gsub(msg, "$3DPRINT IDACK ", "") + table.insert(printerList, id) + table.insert(printerNames, msg) + end + end + + if #printerList == 0 then + displayConfirmDialogue("Printers not found!", "No active printers found in proximity of this computer.") + return false + else + return true + end +end + +--[[Sends a request to the printer. Waits on a response and updates the state of the application accordingly. + Params: command:string the command to send + param:string a parameter to send, if any + Returns:nil +]]-- +local function sendPC(command,param) + local msg = "$PC "..command + if param then msg = msg.." "..param end + rednet.send(printerList[selectedPrinter], msg) + + while true do + local id,key = rsTimeReceive() + if id == printerList[selectedPrinter] then + if key == "$3DPRINT ACK" then + break + elseif key == "$3DPRINT DEP" then + displayConfirmDialogue("Printer Empty", "The printer has exhasted a material. Please refill slot "..param.. + ", and click this message when ready to continue.") + rednet.send(printerList[selectedPrinter], msg) + elseif key == "$3DPRINT OOF" then + displayConfirmDialogue("Printer Out of Fuel", "The printer has no fuel. Please replace the material ".. + "in slot 1 with a fuel source, then click this message.") + rednet.send(printerList[selectedPrinter], "$PC SS 1") + id,key = rsTimeReceive() + rednet.send(printerList[selectedPrinter], "$PC RF") + id,key = rsTimeReceive() + rednet.send(printerList[selectedPrinter], msg) + end + end + end + + --Changes to position are handled after the event has been successfully completed + if command == "FW" then + px = px + pfx + pz = pz + pfz + elseif command == "BK" then + px = px - pfx + pz = pz - pfz + elseif command == "UP" then + if layering == "up" then + py = py + 1 + else + py = py - 1 + end + elseif command == "DW" then + if layering == "up" then + py = py - 1 + else + py = py + 1 + end + elseif command == "TL" then + pfx,pfz = getLeft(pfx,pfz) + elseif command == "TR" then + pfx,pfz = getRight(pfx,pfz) + elseif command == "TU" then + pfx = -pfx + pfz = -pfz + end + + drawCanvas() + drawInterface() +end + +--[[A printing function that commands the printer to turn to face the desired direction, if it is not already doing so + Params: desx:number = the normalized x direction to face + desz:number = the normalized z direction to face + Returns:nil +]]-- +local function turnToFace(desx,desz) + if desx ~= 0 then + if pfx ~= desx then + local temppfx,_ = getLeft(pfx,pfz) + if temppfx == desx then + sendPC("TL") + elseif temppfx == -desx then + sendPC("TR") + else + sendPC("TU") + end + end + else + print("on the z axis") + if pfz ~= desz then + local _,temppfz = getLeft(pfx,pfz) + if temppfz == desz then + sendPC("TL") + elseif temppfz == -desz then + sendPC("TR") + else + sendPC("TU") + end + end + end +end + +--[[Performs the print + Params: nil + Returns:nil +]]-- +local function performPrint() + state = "active print" + if layering == "up" then + --An up layering starts our builder bot on the bottom left corner of our build + px,py,pz = leflim, 0, botlim + 1 + pfx,pfz = 0,-1 + + --We move him forward and up a bit from his original position. + sendPC("FW") + sendPC("UP") + --For each layer that needs to be completed, we go up by one each time + for layers=1,#frames do + --We first decide if we're going forwards or back, depending on what side we're on + local rowbot,rowtop,rowinc = nil,nil,nil + if pz == botlim then + rowbot,rowtop,rowinc = botlim,toplim,-1 + else + rowbot,rowtop,rowinc = toplim,botlim,1 + end + + for rows = rowbot,rowtop,rowinc do + --Then we decide if we're going left or right, depending on what side we're on + local linebot,linetop,lineinc = nil,nil,nil + if px == leflim then + --Facing from the left side has to be easterly- it's changed here + turnToFace(1,0) + linebot,linetop,lineinc = leflim,riglim,1 + else + --Facing from the right side has to be westerly- it's changed here + turnToFace(-1,0) + linebot,linetop,lineinc = riglim,leflim,-1 + end + + for lines = linebot,linetop,lineinc do + --We move our turtle forward, placing the right material at each step + local material = frames[py][pz][px] + if material then + material = math.log10(frames[py][pz][px])/math.log10(2) + 1 + sendPC("SS", material) + sendPC("PD") + end + if lines ~= linetop then + sendPC("FW") + end + end + + --The printer then has to do a U-turn, depending on which way he's facing and + --which way he needs to go + local temppfx,temppfz = getLeft(pfx,pfz) + if temppfz == rowinc and rows ~= rowtop then + sendPC("TL") + sendPC("FW") + sendPC("TL") + elseif temppfz == -rowinc and rows ~= rowtop then + sendPC("TR") + sendPC("FW") + sendPC("TR") + end + end + --Now at the end of a run he does a 180 and moves up to begin the next part of the print + sendPC("TU") + if layers ~= #frames then + sendPC("UP") + end + end + --All done- now we head back to where we started. + if px ~= leflim then + turnToFace(-1,0) + while px ~= leflim do + sendPC("FW") + end + end + if pz ~= botlim then + turnToFace(0,-1) + while pz ~= botlim do + sendPC("BK") + end + end + turnToFace(0,-1) + sendPC("BK") + while py > 0 do + sendPC("DW") + end + else + --The front facing is at the top-left corner, facing south not north + px,py,pz = leflim, botlim, 1 + pfx,pfz = 0,1 + --We move the printer to the last layer- he prints from the back forwards + while pz < #frames do + sendPC("FW") + end + + --For each layer in the frame we build our wall, the move back + for layers = 1,#frames do + --We first decide if we're going left or right based on our position + local rowbot,rowtop,rowinc = nil,nil,nil + if px == leflim then + rowbot,rowtop,rowinc = leflim,riglim,1 + else + rowbot,rowtop,rowinc = riglim,leflim,-1 + end + + for rows = rowbot,rowtop,rowinc do + --Then we decide if we're going up or down, depending on our given altitude + local linebot,linetop,lineinc = nil,nil,nil + if py == botlim then + linebot,linetop,lineinc = botlim,toplim,-1 + else + linebot,linetop,lineinc = toplim,botlim,1 + end + + for lines = linebot,linetop,lineinc do + --We move our turtle up/down, placing the right material at each step + local material = frames[pz][py][px] + if material then + material = math.log10(frames[pz][py][px])/math.log10(2) + 1 + sendPC("SS", material) + sendPC("PF") + end + if lines ~= linetop then + if lineinc == 1 then sendPC("DW") + else sendPC("UP") end + end + end + + if rows ~= rowtop then + turnToFace(rowinc,0) + sendPC("FW") + turnToFace(0,1) + end + end + + if layers ~= #frames then + sendPC("TU") + sendPC("FW") + sendPC("TU") + end + end + --He's easy to reset + while px ~= leflim do + turnToFace(-1,0) + sendPC("FW") + end + turnToFace(0,1) + end + + sendPC("DE") + + displayConfirmDialogue("Print complete", "The 3D print was successful.") +end + +--[[ + Section: Interface +]]-- + +--[[Runs the printing interface. Allows users to find/select a printer, the style of printing to perform and to begin the operation + Params: none + Returns:boolean true if printing was started, false otherwse +]]-- +local function runPrintInterface() + calculateMaterials() + --There's nothing on canvas yet! + if not botlim then + displayConfirmDialogue("Cannot Print Empty Canvas", "There is nothing on canvas that ".. + "can be printed, and the operation cannot be completed.") + return false + end + --No printers nearby + if not locatePrinters() then + return false + end + + layering = "up" + requirementsDisplayed = false + selectedPrinter = 1 + while true do + drawCanvas() + term.setBackgroundColour(colours.lightGrey) + for i=1,10 do + term.setCursorPos(1,i) + term.clearLine() + end + drawInterface() + term.setBackgroundColour(colours.lightGrey) + term.setTextColour(colours.black) + + local msg = "3D Printing" + term.setCursorPos(w/2-#msg/2 - 2, 1) + term.write(msg) + term.setBackgroundColour(colours.grey) + term.setTextColour(colours.lightGrey) + if(requirementsDisplayed) then + msg = "Count:" + else + msg = " Slot:" + end + term.setCursorPos(w-3-#msg, 1) + term.write(msg) + term.setBackgroundColour(colours.lightGrey) + term.setTextColour(colours.black) + + term.setCursorPos(7, 2) + term.write("Layering") + drawPictureTable(layerUpIcon, 3, 3, colours.white) + drawPictureTable(layerForwardIcon, 12, 3, colours.white) + if layering == "up" then + term.setBackgroundColour(colours.red) + else + term.setBackgroundColour(colours.lightGrey) + end + term.setCursorPos(3, 9) + term.write("Upwards") + if layering == "forward" then + term.setBackgroundColour(colours.red) + else + term.setBackgroundColour(colours.lightGrey) + end + term.setCursorPos(12, 9) + term.write("Forward") + + term.setBackgroundColour(colours.lightGrey) + term.setTextColour(colours.black) + term.setCursorPos(31, 2) + term.write("Printer ID") + term.setCursorPos(33, 3) + if #printerList > 1 then + term.setBackgroundColour(colours.grey) + term.setTextColour(colours.lightGrey) + else + term.setTextColour(colours.red) + end + term.write(" "..printerNames[selectedPrinter].." ") + + term.setBackgroundColour(colours.grey) + term.setTextColour(colours.lightGrey) + term.setCursorPos(25, 10) + term.write(" Cancel ") + term.setCursorPos(40, 10) + term.write(" Print ") + + local id, p1, p2, p3 = os.pullEvent() + + if id == "timer" then + updateTimer(p1) + elseif id == "mouse_click" then + --Layering Buttons + if p2 >= 3 and p2 <= 9 and p3 >= 3 and p3 <= 9 then + layering = "up" + elseif p2 >= 12 and p2 <= 18 and p3 >= 3 and p3 <= 9 then + layering = "forward" + --Count/Slot + elseif p2 >= w - #msg - 3 and p2 <= w - 3 and p3 == 1 then + requirementsDisplayed = not requirementsDisplayed + --Printer ID + elseif p2 >= 33 and p2 <= 33 + #printerNames[selectedPrinter] and p3 == 3 and #printerList > 1 then + local chosenName = displayDropDown(33, 3, printerNames) + for i=1,#printerNames do + if printerNames[i] == chosenName then + selectedPrinter = i + break; + end + end + --Print and Cancel + elseif p2 >= 25 and p2 <= 32 and p3 == 10 then + break + elseif p2 >= 40 and p2 <= 46 and p3 == 10 then + rednet.send(printerList[selectedPrinter], "$3DPRINT ACTIVATE") + ready = false + while true do + local id,msg = rsTimeReceive(10) + + if id == printerList[selectedPrinter] and msg == "$3DPRINT ACTACK" then + ready = true + break + end + end + if ready then + performPrint() + break + else + displayConfirmDialogue("Printer Didn't Respond", "The printer didn't respond to the activation command. Check to see if it's online") + end + end + end + end + state = "paint" +end + +--[[Performs a legacy save. When the dropdown menu is unavailable, it requests the user to save + or exit using keyboard shortcuts rather than selecting a menu option from the dropdown. + Pressing the control key again will cancel the save operation. + Params: none + Returns:string = the selection made +]]-- +local function performLegacySaveExit() + local saveMsg = "(S)ave/(E)xit?" + if w < #saveMsg then saveMsg = "S/E?" end + + term.setCursorPos(1,h) + term.setBackgroundColour(colours.lightGrey) + term.setTextColour(colours.grey) + term.clearLine() + term.write(saveMsg) + + while true do + local id,val = os.pullEvent() + if id == "timer" then updateTimer(val) + elseif id == "key" then + if val == keys.s then return "save" + elseif val == keys.e then + --Get rid of the extra event + os.pullEvent("char") + return "exit" + elseif val == keys.leftCtrl then return nil + end + end + end +end + +--[[This function changes the current paint program to another tool or mode, depending on user input. Handles + any necessary changes in logic involved in that. + Params: mode:string = the name of the mode to change to + Returns:nil +]]-- +local function performSelection(mode) + if not mode or mode == "" then return + + elseif mode == "help" and helpAvailable then + drawHelpScreen() + + elseif mode == "blueprint on" then + blueprint = true + for i=1,#ddModes[2] do if ddModes[2][i] == "blueprint on" then + ddModes[2][i] = "blueprint off" + end end + + elseif mode == "blueprint off" then + blueprint = false + for i=1,#ddModes[2] do if ddModes[2][i] == "blueprint off" then + ddModes[2][i] = "blueprint on" + end end + + elseif mode == "layers on" then + layerDisplay = true + for i=1,#ddModes[2] do if ddModes[2][i] == "layers on" then + ddModes[2][i] = "layers off" + end end + + elseif mode == "layers off" then + layerDisplay = false + for i=1,#ddModes[2] do if ddModes[2][i] == "layers off" then + ddModes[2][i] = "layers on" + end end + + elseif mode == "direction on" then + printDirection = true + for i=1,#ddModes[2] do if ddModes[2][i] == "direction on" then + ddModes[2][i] = "direction off" + end end + + elseif mode == "direction off" then + printDirection = false + for i=1,#ddModes[2] do if ddModes[2][i] == "direction off" then + ddModes[2][i] = "direction on" + end end + + elseif mode == "hide interface" then + interfaceHidden = true + + elseif mode == "show interface" then + interfaceHidden = false + + elseif mode == "go to" then + changeFrame() + + elseif mode == "remove" then + removeFramesAfter(sFrame) + + elseif mode == "play" then + playAnimation() + + elseif mode == "copy" then + if selectrect and selectrect.x1 ~= selectrect.x2 then + copyToBuffer(false) + end + + elseif mode == "cut" then + if selectrect and selectrect.x1 ~= selectrect.x2 then + copyToBuffer(true) + end + + elseif mode == "paste" then + if selectrect and selectrect.x1 ~= selectrect.x2 then + copyFromBuffer(false) + end + + elseif mode == "hide" then + selectrect = nil + if state == "select" then state = "corner select" end + + elseif mode == "alpha to left" then + if lSel then alphaC = lSel end + + elseif mode == "alpha to right" then + if rSel then alphaC = rSel end + + elseif mode == "record" then + record = not record + + elseif mode == "clear" then + if state=="select" then buffer = nil + else clearImage() end + + elseif mode == "select" then + if state=="corner select" or state=="select" then + state = "paint" + elseif selectrect and selectrect.x1 ~= selectrect.x2 then + state = "select" + else + state = "corner select" + end + + elseif mode == "print" then + state = "print" + runPrintInterface() + state = "paint" + + elseif mode == "save" then + if animated then saveNFA(sPath) + elseif textEnabled then saveNFT(sPath) + else saveNFP(sPath) end + + elseif mode == "exit" then + isRunning = false + + elseif mode ~= state then state = mode + else state = "paint" + end +end + +--[[The main function of the program, reads and handles all events and updates them accordingly. Mode changes, + painting to the canvas and general selections are done here. + Params: none + Returns:nil +]]-- +local function handleEvents() + recttimer = os.startTimer(0.5) + while isRunning do + drawCanvas() + if not interfaceHidden then drawInterface() end + + if state == "text" then + term.setCursorPos(textCurX - sx, textCurY - sy) + term.setCursorBlink(true) + end + + local id,p1,p2,p3 = os.pullEvent() + term.setCursorBlink(false) + if id=="timer" then + updateTimer(p1) + elseif (id=="mouse_click" or id=="mouse_drag") and not interfaceHidden then + if p2 >=w-1 and p3 < #column+1 then + local off = 0 + local cansel = true + if h < #column + 2 then + if p3 == 1 then + if columnoffset > 0 then columnoffset = columnoffset-1 end + cansel = false + elseif p3 == h-2 then + if columnoffset < #column-(h-4)+1 then columnoffset = columnoffset+1 end + cansel = false + else + off = columnoffset - 1 + end + end + --This rather handily accounts for the nil case (p3+off=#column+1) + if p1==1 and cansel then lSel = column[p3+off] + elseif p1==2 and cansel then rSel = column[p3+off] end + elseif p2 >=w-1 and p3==#column+1 then + if p1==1 then lSel = nil + else rSel = nil end + elseif p2==w-1 and p3==h and animated then + changeFrame(sFrame-1) + elseif p2==w and p3==h and animated then + changeFrame(sFrame+1) + elseif p2 <= #ddModes.name + 2 and p3==h and mainAvailable then + local sel = displayDropDown(1, h-1, ddModes) + performSelection(sel) + elseif p2 < w-1 and p3 <= h-1 then + if state=="pippette" then + if p1==1 then + if frames[sFrame][p3+sy] and frames[sFrame][p3+sy][p2+sx] then + lSel = frames[sFrame][p3+sy][p2+sx] + end + elseif p1==2 then + if frames[sFrame][p3+sy] and frames[sFrame][p3+sy][p2+sx] then + rSel = frames[sFrame][p3+sy][p2+sx] + end + end + elseif state=="move" then + updateImageLims(record) + moveImage(p2,p3) + elseif state=="flood" then + if p1 == 1 and lSel and frames[sFrame][p3+sy] then + floodFill(p2,p3,frames[sFrame][p3+sy][p2+sx],lSel) + elseif p1 == 2 and rSel and frames[sFrame][p3+sy] then + floodFill(p2,p3,frames[sFrame][p3+sy][p2+sx],rSel) + end + elseif state=="corner select" then + if not selectrect then + selectrect = { x1=p2+sx, x2=p2+sx, y1=p3+sy, y2=p3+sy } + elseif selectrect.x1 ~= p2+sx and selectrect.y1 ~= p3+sy then + if p2+sx<selectrect.x1 then selectrect.x1 = p2+sx + else selectrect.x2 = p2+sx end + + if p3+sy<selectrect.y1 then selectrect.y1 = p3+sy + else selectrect.y2 = p3+sy end + + state = "select" + end + elseif state=="textpaint" then + local paintCol = lSel + if p1 == 2 then paintCol = rSel end + if frames[sFrame].textcol[p3+sy] then + frames[sFrame].textcol[p3+sy][p2+sx] = paintCol + end + elseif state=="text" then + textCurX = p2 + sx + textCurY = p3 + sy + elseif state=="select" then + if p1 == 1 then + local swidth = selectrect.x2 - selectrect.x1 + local sheight = selectrect.y2 - selectrect.y1 + + selectrect.x1 = p2 + sx + selectrect.y1 = p3 + sy + selectrect.x2 = p2 + swidth + sx + selectrect.y2 = p3 + sheight + sy + elseif p1 == 2 and p2 < w-2 and p3 < h-1 and boxdropAvailable then + inMenu = true + local sel = displayDropDown(p2, p3, srModes) + inMenu = false + performSelection(sel) + end + else + local f,l = sFrame,sFrame + if record then f,l = 1,framecount end + local bwidth = 0 + if state == "brush" then bwidth = brushsize-1 end + + for i=f,l do + for x = math.max(1,p2+sx-bwidth),p2+sx+bwidth do + for y = math.max(1,p3+sy-bwidth), p3+sy+bwidth do + if math.abs(x - (p2+sx)) + math.abs(y - (p3+sy)) <= bwidth then + if not frames[i][y] then frames[i][y] = {} end + if p1==1 then frames[i][y][x] = lSel + else frames[i][y][x] = rSel end + + if textEnabled then + if not frames[i].text[y] then frames[i].text[y] = { } end + if not frames[i].textcol[y] then frames[i].textcol[y] = { } end + end + end + end + end + end + end + end + elseif id=="char" then + if state=="text" then + if not frames[sFrame][textCurY] then frames[sFrame][textCurY] = { } end + if not frames[sFrame].text[textCurY] then frames[sFrame].text[textCurY] = { } end + if not frames[sFrame].textcol[textCurY] then frames[sFrame].textcol[textCurY] = { } end + + if rSel then frames[sFrame][textCurY][textCurX] = rSel end + if lSel then + frames[sFrame].text[textCurY][textCurX] = p1 + frames[sFrame].textcol[textCurY][textCurX] = lSel + else + frames[sFrame].text[textCurY][textCurX] = " " + frames[sFrame].textcol[textCurY][textCurX] = rSel + end + + textCurX = textCurX+1 + if textCurX > w + sx - 2 then sx = textCurX - w + 2 end + elseif tonumber(p1) then + if state=="brush" and tonumber(p1) > 1 then + brushsize = tonumber(p1) + elseif animated and tonumber(p1) > 0 then + changeFrame(tonumber(p1)) + end + end + elseif id=="key" then + --All standard interface methods are locked when the interface is hidden + if interfaceHidden then + if p1==keys.grave then + performSelection("show interface") + end + --Text needs special handlers (all other keyboard shortcuts are of course reserved for typing) + elseif state=="text" then + if p1==keys.backspace and textCurX > 1 then + textCurX = textCurX-1 + if frames[sFrame].text[textCurY] then + frames[sFrame].text[textCurY][textCurX] = nil + frames[sFrame].textcol[textCurY][textCurX] = nil + end + if textCurX < sx then sx = textCurX end + elseif p1==keys.left and textCurX > 1 then + textCurX = textCurX-1 + if textCurX-1 < sx then sx = textCurX-1 end + elseif p1==keys.right then + textCurX = textCurX+1 + if textCurX > w + sx - 2 then sx = textCurX - w + 2 end + elseif p1==keys.up and textCurY > 1 then + textCurY = textCurY-1 + if textCurY-1 < sy then sy = textCurY-1 end + elseif p1==keys.down then + textCurY = textCurY+1 + if textCurY > h + sy - 1 then sy = textCurY - h + 1 end + end + + elseif p1==keys.leftCtrl then + local sel = nil + if mainAvailable then + sel = displayDropDown(1, h-1, ddModes[#ddModes]) + else sel = performLegacySaveExit() end + performSelection(sel) + elseif p1==keys.leftAlt then + local sel = displayDropDown(1, h-1, ddModes[1]) + performSelection(sel) + elseif p1==keys.h then + performSelection("help") + elseif p1==keys.x then + performSelection("cut") + elseif p1==keys.c then + performSelection("copy") + elseif p1==keys.v then + performSelection("paste") + elseif p1==keys.z then + performSelection("clear") + elseif p1==keys.s then + performSelection("select") + elseif p1==keys.tab then + performSelection("hide") + elseif p1==keys.q then + performSelection("alpha to left") + elseif p1==keys.w then + performSelection("alpha to right") + elseif p1==keys.f then + performSelection("flood") + elseif p1==keys.b then + performSelection("brush") + elseif p1==keys.m then + performSelection("move") + elseif p1==keys.backslash and animated then + performSelection("record") + elseif p1==keys.p then + performSelection("pippette") + elseif p1==keys.g and animated then + performSelection("go to") + elseif p1==keys.grave then + performSelection("hide interface") + elseif p1==keys.period and animated then + changeFrame(sFrame+1) + elseif p1==keys.comma and animated then + changeFrame(sFrame-1) + elseif p1==keys.r and animated then + performSelection("remove") + elseif p1==keys.space and animated then + performSelection("play") + elseif p1==keys.t and textEnabled then + performSelection("text") + sleep(0.01) + elseif p1==keys.y and textEnabled then + performSelection("textpaint") + elseif p1==keys.left then + if state == "move" and toplim then + updateImageLims(record) + if toplim and leflim then + moveImage(leflim-1,toplim) + end + elseif state=="select" and selectrect.x1 > 1 then + selectrect.x1 = selectrect.x1-1 + selectrect.x2 = selectrect.x2-1 + elseif sx > 0 then sx=sx-1 end + elseif p1==keys.right then + if state == "move" then + updateImageLims(record) + if toplim and leflim then + moveImage(leflim+1,toplim) + end + elseif state=="select" then + selectrect.x1 = selectrect.x1+1 + selectrect.x2 = selectrect.x2+1 + else sx=sx+1 end + elseif p1==keys.up then + if state == "move" then + updateImageLims(record) + if toplim and leflim then + moveImage(leflim,toplim-1) + end + elseif state=="select" and selectrect.y1 > 1 then + selectrect.y1 = selectrect.y1-1 + selectrect.y2 = selectrect.y2-1 + elseif sy > 0 then sy=sy-1 end + elseif p1==keys.down then + if state == "move" then + updateImageLims(record) + if toplim and leflim then + moveImage(leflim,toplim+1) + end + elseif state=="select" then + selectrect.y1 = selectrect.y1+1 + selectrect.y2 = selectrect.y2+1 + else sy=sy+1 end + end + end + end +end + +--[[ + Section: Main +]]-- + +--The first thing done is deciding what features we actually have, given the screen size +if w < 7 or h < 4 then + --NPaintPro simply doesn't work at certain configurations + shell.run("clear") + print("Screen too small") + os.pullEvent("key") + return +end +--And reduces the number of features in others. +determineAvailableServices() + +--There is no b&w support for NPP. +if not term.isColour() then + shell.run("clear") + print("NPaintPro\nBy NitrogenFingers\n\nNPaintPro can only be run on advanced ".. + "computers. Please reinstall on an advanced computer.") + return +end + +--Taken almost directly from edit (for consistency) +local tArgs = {...} + +--Command line options can appear before the file path to specify the file format +local ca = 1 +while ca <= #tArgs do + if tArgs[ca] == "-a" then animated = true + elseif tArgs[ca] == "-t" then textEnabled = true + elseif tArgs[ca] == "-d" then interfaceHidden = true + elseif string.sub(tArgs[ca], 1, 1) == "-" then + print("Unrecognized option: "..tArgs[ca]) + return + else break end + ca = ca + 1 +end + +--Presently, animations and text files are not supported +if animated and textEnabled then + print("No support for animated text files- cannot have both -a and -t") + return +end + +--Filepaths must be added if the screen is too small +if #tArgs < ca then + if not filemakerAvailable then + print("Usage: npaintpro [-a,-t,-d] <path>") + return + else + --Otherwise do the logo draw early, to determine the file. + drawLogo() + if not runFileMaker() then return end + end +else + if not interfaceHidden then drawLogo() end + sPath = shell.resolve(tArgs[ca]) +end + +if fs.exists(sPath) then + if fs.isDir(sPath) then + print("Cannot edit a directory.") + return + elseif string.find(sPath, ".nfp") ~= #sPath-3 and string.find(sPath, ".nfa") ~= #sPath-3 and + string.find(sPath, ".nft") ~= #sPath-3 then + print("Can only edit .nfp, .nft and .nfa files:",string.find(sPath, ".nfp"),#sPath-3) + return + end + + if string.find(sPath, ".nfa") == #sPath-3 then + animated = true + end + + if string.find(sPath, ".nft") == #sPath-3 then + textEnabled = true + end + + if string.find(sPath, ".nfp") == #sPath-3 and animated then + print("Convert to nfa? Y/N") + if string.find(string.lower(io.read()), "y") then + local nsPath = string.sub(sPath, 1, #sPath-1).."a" + fs.move(sPath, nsPath) + sPath = nsPath + else + animated = false + end + end + + --Again this is possible, I just haven't done it. Maybe I will? + if textEnabled and (string.find(sPath, ".nfp") == #sPath-3 or string.find(sPath, ".nfa") == #sPath-3) then + print("Cannot convert to nft") + end +else + if not animated and not textEnabled and string.find(sPath, ".nfp") ~= #sPath-3 then + sPath = sPath..".nfp" + elseif animated and string.find(sPath, ".nfa") ~= #sPath-3 then + sPath = sPath..".nfa" + elseif textEnabled and string.find(sPath, ".nft") ~= #sPath-3 then + sPath = sPath..".nft" + end +end + +init() +handleEvents() + +term.setBackgroundColour(colours.black) +shell.run("clear") |
