Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- -- === Palette Setup ===
- function set256Palette()
- local cubeSize = 6
- for i = 16, 231 do
- local index = i - 16
- local rIndex = math.floor(index / (cubeSize * cubeSize))
- local gIndex = math.floor((index % (cubeSize * cubeSize)) / cubeSize)
- local bIndex = index % cubeSize
- local r = math.floor(rIndex * 255 / (cubeSize - 1))
- local g = math.floor(gIndex * 255 / (cubeSize - 1))
- local b = math.floor(bIndex * 255 / (cubeSize - 1))
- term.setPaletteColor(i, r / 255, g / 255, b / 255)
- end
- for i = 232, 255 do
- local gray = math.floor((i - 232) * 255 / 23)
- term.setPaletteColor(i, gray / 255, gray / 255, gray / 255)
- end
- end
- -- === BMP Font and Color Helpers ===
- local FONT_BMP_URL = "https://files.catbox.moe/zkdsi9.bmp"
- local FONT_BMP_FILE = "font.bmp"
- local TRANSPARENT_COLOR = { r = 34, g = 177, b = 76 }
- local function download(url, file)
- if fs.exists(file) then return end
- local h = http.get(url, nil, true)
- assert(h, "Download failed: " .. url)
- local f = fs.open(file, "wb")
- f.write(h.readAll())
- f.close()
- h.close()
- end
- local function readBMP(filename)
- local file = fs.open(filename, "rb")
- if not file then return nil, "Failed to open file" end
- local header = file.read(54)
- local width = string.byte(header, 19) + string.byte(header, 20) * 256
- local height = string.byte(header, 23) + string.byte(header, 24) * 256
- local pixels = {}
- for y = height, 1, -1 do
- pixels[y] = {}
- for x = 1, width do
- local b = file.read(1)
- local g = file.read(1)
- local r = file.read(1)
- pixels[y][x] = {r = string.byte(r), g = string.byte(g), b = string.byte(b)}
- end
- if y % 10 == 0 then os.queueEvent("yield") os.pullEvent("yield") end
- end
- file.close()
- return {width = width, height = height, pixels = pixels}
- end
- local function mapColor256(r, g, b)
- local cubeSize = 6
- local rIndex = math.floor(r / (255 / (cubeSize - 1)))
- local gIndex = math.floor(g / (255 / (cubeSize - 1)))
- local bIndex = math.floor(b / (255 / (cubeSize - 1)))
- return 16 + rIndex * cubeSize * cubeSize + gIndex * cubeSize + bIndex
- end
- -- Accepts color as palette index
- local function drawTextHalfRes(font, x, y, str, color)
- color = color or 231 -- Default to white for best visibility
- for i=1,#str do
- local c = string.byte(str,i)-32
- if c>=0 and c<120 then
- local sx = (c%15)*20
- local sy = math.floor(c/15)*20
- for dy=0,9 do
- for dx=0,9 do
- local p = font.pixels[sy+dy*2+1][sx+dx*2+1]
- if not (p.r==TRANSPARENT_COLOR.r and p.g==TRANSPARENT_COLOR.g and p.b==TRANSPARENT_COLOR.b) then
- term.setPixel(x+(i-1)*10+dx, y+dy, color)
- end
- end
- end
- end
- end
- end
- local function drawBMPScaled(bmp, x, y, maxw, maxh)
- local iw, ih = bmp.width, bmp.height
- local scale = math.min(maxw/iw, maxh/ih, 1)
- local w, h = math.floor(iw*scale), math.floor(ih*scale)
- for dy = 0, h-1 do
- local srcY = math.floor(dy * ih / h) + 1
- for dx = 0, w-1 do
- local srcX = math.floor(dx * iw / w) + 1
- local p = bmp.pixels[srcY][srcX]
- term.setPixel(x+dx, y+dy, mapColor256(p.r, p.g, p.b))
- end
- end
- return w, h
- end
- -- === Base64 decode for BMP data:images ===
- local b='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
- local function base64_decode(data)
- data = data:gsub("[^"..b.."=]", "")
- return (data:gsub('.', function(x)
- if (x == '=') then return '' end
- local r,f='',(b:find(x)-1)
- for i=6,1,-1 do r=r..(f%2^i - f%2^(i-1) > 0 and '1' or '0') end
- return r
- end):gsub('%d%d%d?%d?%d?%d?%d?%d?', function(x)
- if #x ~= 8 then return '' end
- local c=0
- for i=1,8 do c=c + (x:sub(i,i)=='1' and 2^(8-i) or 0) end
- return string.char(c)
- end))
- end
- -- === CSS helpers ===
- local css_colors = { red=196, green=46, blue=21, black=16, white=231, yellow=226, gray=244 }
- local function cssColorToPalette(c)
- if css_colors[c] then return css_colors[c]
- elseif c:sub(1,1) == "#" and #c == 7 then
- local r = tonumber(c:sub(2,3),16)
- local g = tonumber(c:sub(4,5),16)
- local b = tonumber(c:sub(6,7),16)
- return mapColor256(r,g,b)
- else return 231 end
- end
- local function parseFontSize(sz)
- if not sz then return 12 end
- local n = tonumber(sz:match("^(%d+)"))
- if not n then return 12 end
- return math.max(8, math.min(36, n))
- end
- -- Check if a palette index is light or dark for contrast
- local function isLightPalette(idx)
- local r, g, b = term.getPaletteColor(idx)
- local brightness = 0.299*r + 0.587*g + 0.114*b
- return brightness > 0.5
- end
- -- === HTML/CSS Parsing ===
- -- Minimal HTML to DOM tree
- local function parseHTMLtoDOM(html)
- local dom = { tag="body", children={}, style={}, attrs={} }
- local stack = { dom }
- local pos = 1
- while pos <= #html do
- local tagStart, tagEnd, tagText = html:find("<([/%w][^>]*)>", pos)
- if tagStart then
- -- Text before tag
- if tagStart > pos then
- local text = html:sub(pos, tagStart-1):gsub("\n", "")
- if #text > 0 then
- table.insert(stack[#stack].children, { tag="text", text=text, style={}, attrs={} })
- end
- end
- -- Tag
- local isClose = tagText:sub(1,1) == "/"
- local tagName = tagText:match("^/?([%w]+)")
- if isClose then
- table.remove(stack) -- pop
- else
- local attrs = {}
- for k,v in tagText:gmatch("([%w-]+)%s*=%s*['\"](.-)['\"]") do
- attrs[k] = v
- end
- local node = { tag=tagName, children={}, style={}, attrs=attrs }
- table.insert(stack[#stack].children, node)
- if not tagText:find("/$") then -- not self-closing
- table.insert(stack, node)
- end
- end
- pos = tagEnd+1
- else
- -- Remaining text
- local text = html:sub(pos):gsub("\n", "")
- if #text > 0 then
- table.insert(stack[#stack].children, { tag="text", text=text, style={}, attrs={} })
- end
- break
- end
- end
- return dom
- end
- -- Simple CSS parser (only color, background, font-size)
- local function parseCSS(css)
- local rules = {}
- for selector, body in css:gmatch("([%w%.#%s]+)%s*{%s*([^}]+)}") do
- local props = {}
- for k,v in body:gmatch("([%w-]+)%s*:%s*([^;]+)") do
- props[k] = v
- end
- table.insert(rules, { selector=selector:match("^%s*(.-)%s*$"), props=props })
- end
- return rules
- end
- -- Apply CSS rules to DOM nodes (very basic: tag, .class, #id)
- local function applyCSS(dom, rules)
- local function matches(node, selector)
- if selector:sub(1,1) == "." then
- return node.attrs.class == selector:sub(2)
- elseif selector:sub(1,1) == "#" then
- return node.attrs.id == selector:sub(2)
- else
- return node.tag == selector
- end
- end
- local function apply(node)
- for _,rule in ipairs(rules) do
- if matches(node, rule.selector) then
- for k,v in pairs(rule.props) do
- node.style[k] = v
- end
- end
- end
- -- Inline style
- if node.attrs.style then
- for k,v in node.attrs.style:gmatch("([%w-]+)%s*:%s*([^;]+)") do
- node.style[k] = v
- end
- end
- for _,child in ipairs(node.children or {}) do apply(child) end
- end
- apply(dom)
- end
- -- === DOM Renderer ===
- local function renderDOM(node, font, x, y, maxw)
- if node.tag == "text" then
- local bg = node.style["background"] and cssColorToPalette(node.style["background"]) or 16 -- black default
- local color = node.style.color and cssColorToPalette(node.style.color) or (isLightPalette(bg) and 16 or 231)
- local fontSize = parseFontSize(node.style["font-size"])
- term.drawPixels(x, y, bg, maxw, fontSize)
- drawTextHalfRes(font, x, y, node.text, color)
- return y + fontSize
- elseif node.tag == "img" and node.attrs.src and node.attrs.src:find("^data:image/bmp;base64,") then
- local b64 = node.attrs.src:match("^data:image/bmp;base64,(.+)")
- if b64 then
- local bmpData = base64_decode(b64)
- local tmpFile = ".browser_img_"..os.epoch("utc")..".bmp"
- local f = fs.open(tmpFile, "wb")
- f.write(bmpData)
- f.close()
- local bmp = readBMP(tmpFile)
- if bmp then
- local w, h = drawBMPScaled(bmp, x, y, 200, 60)
- return y + h + 2
- end
- end
- return y + 12
- else
- local newY = y
- for _,child in ipairs(node.children or {}) do
- newY = renderDOM(child, font, x, newY, maxw)
- end
- return newY
- end
- end
- -- === Browser Logic ===
- term.setGraphicsMode(2)
- set256Palette()
- local pxw, pxh = term.getSize(2)
- download(FONT_BMP_URL, FONT_BMP_FILE)
- local font = readBMP(FONT_BMP_FILE)
- local function getURLInput(current)
- term.setGraphicsMode(0)
- term.setCursorPos(1, 1)
- term.clearLine()
- write("URL: ")
- local url = read()
- if url == "" then url = current end
- term.setGraphicsMode(2)
- return url
- end
- local url = "https://example.com"
- local dom, cssRules = nil, {}
- local scroll = 0
- local contentHeight = 0
- while true do
- term.setFrozen(true)
- term.drawPixels(0, 0, 240, pxw, 20)
- term.drawPixels(0, 20, 255, pxw, pxh-20)
- drawTextHalfRes(font, 10, 5, url)
- if dom then
- -- Measure content height for scroll bounds
- local function measureDOM(node, font, x, y, maxw)
- if node.tag == "text" then
- local fontSize = parseFontSize(node.style["font-size"])
- return y + fontSize
- elseif node.tag == "img" and node.attrs.src and node.attrs.src:find("^data:image/bmp;base64,") then
- local b64 = node.attrs.src:match("^data:image/bmp;base64,(.+)")
- if b64 then
- local bmpData = base64_decode(b64)
- local tmpFile = ".browser_img_"..os.epoch("utc")..".bmp"
- local f = fs.open(tmpFile, "wb")
- f.write(bmpData)
- f.close()
- local bmp = readBMP(tmpFile)
- if bmp then
- local w, h = drawBMPScaled(bmp, -1000, -1000, 200, 60) -- offscreen
- return y + h + 2
- end
- end
- return y + 12
- else
- local newY = y
- for _,child in ipairs(node.children or {}) do
- newY = measureDOM(child, font, x, newY, maxw)
- end
- return newY
- end
- end
- contentHeight = measureDOM(dom, font, 10, 25, pxw-20) - 25
- renderDOM(dom, font, 10, 25 - scroll, pxw-20)
- end
- term.setFrozen(false)
- local event, p1, p2, p3 = os.pullEvent()
- if event == "mouse_click" then
- local btn, x, y = p1, p2, p3
- if y <= 20 then
- url = getURLInput(url)
- term.setFrozen(true)
- drawTextHalfRes(font, 10, 25, "Loading...")
- term.setFrozen(false)
- local ok, resp = pcall(function() return http.get(url) end)
- if ok and resp then
- local html = resp.readAll()
- resp.close()
- dom = parseHTMLtoDOM(html)
- -- Extract <style>...</style> and parse CSS
- local css = ""
- for style in html:gmatch("<style[^>]*>(.-)</style>") do css = css .. style end
- cssRules = parseCSS(css)
- applyCSS(dom, cssRules)
- else
- dom = { tag="body", children={ {tag="text", text="Failed to load: "..tostring(url), style={}, attrs={}} }, style={}, attrs={} }
- end
- scroll = 0
- end
- elseif event == "mouse_scroll" then
- local dir = p1
- local maxScroll = math.max(0, contentHeight - (pxh-30))
- scroll = math.max(0, math.min(scroll + dir*12, maxScroll))
- elseif event == "key" then
- if p1 == keys.up then
- scroll = math.max(scroll-12, 0)
- elseif p1 == keys.down then
- local maxScroll = math.max(0, contentHeight - (pxh-30))
- scroll = math.min(scroll+12, maxScroll)
- elseif p1 == keys.pageup then
- scroll = math.max(scroll-(pxh-30), 0)
- elseif p1 == keys.pagedown then
- local maxScroll = math.max(0, contentHeight - (pxh-30))
- scroll = math.min(scroll+(pxh-30), maxScroll)
- elseif p1 == keys.q or p1 == keys.escape then
- break
- end
- elseif event == "terminate" then
- break
- end
- end
- term.setGraphicsMode(0)
- term.clear()
- print("Browser exited.")
Add Comment
Please, Sign In to add comment