nonogamer9

test

Apr 20th, 2025 (edited)
232
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 12.01 KB | None | 0 0
  1. -- === Palette Setup ===
  2. function set256Palette()
  3. local cubeSize = 6
  4. for i = 16, 231 do
  5. local index = i - 16
  6. local rIndex = math.floor(index / (cubeSize * cubeSize))
  7. local gIndex = math.floor((index % (cubeSize * cubeSize)) / cubeSize)
  8. local bIndex = index % cubeSize
  9. local r = math.floor(rIndex * 255 / (cubeSize - 1))
  10. local g = math.floor(gIndex * 255 / (cubeSize - 1))
  11. local b = math.floor(bIndex * 255 / (cubeSize - 1))
  12. term.setPaletteColor(i, r / 255, g / 255, b / 255)
  13. end
  14. for i = 232, 255 do
  15. local gray = math.floor((i - 232) * 255 / 23)
  16. term.setPaletteColor(i, gray / 255, gray / 255, gray / 255)
  17. end
  18. end
  19.  
  20. -- === BMP Font and Color Helpers ===
  21. local FONT_BMP_URL = "https://files.catbox.moe/zkdsi9.bmp"
  22. local FONT_BMP_FILE = "font.bmp"
  23. local TRANSPARENT_COLOR = { r = 34, g = 177, b = 76 }
  24.  
  25. local function download(url, file)
  26. if fs.exists(file) then return end
  27. local h = http.get(url, nil, true)
  28. assert(h, "Download failed: " .. url)
  29. local f = fs.open(file, "wb")
  30. f.write(h.readAll())
  31. f.close()
  32. h.close()
  33. end
  34.  
  35. local function readBMP(filename)
  36. local file = fs.open(filename, "rb")
  37. if not file then return nil, "Failed to open file" end
  38. local header = file.read(54)
  39. local width = string.byte(header, 19) + string.byte(header, 20) * 256
  40. local height = string.byte(header, 23) + string.byte(header, 24) * 256
  41. local pixels = {}
  42. for y = height, 1, -1 do
  43. pixels[y] = {}
  44. for x = 1, width do
  45. local b = file.read(1)
  46. local g = file.read(1)
  47. local r = file.read(1)
  48. pixels[y][x] = {r = string.byte(r), g = string.byte(g), b = string.byte(b)}
  49. end
  50. if y % 10 == 0 then os.queueEvent("yield") os.pullEvent("yield") end
  51. end
  52. file.close()
  53. return {width = width, height = height, pixels = pixels}
  54. end
  55.  
  56. local function mapColor256(r, g, b)
  57. local cubeSize = 6
  58. local rIndex = math.floor(r / (255 / (cubeSize - 1)))
  59. local gIndex = math.floor(g / (255 / (cubeSize - 1)))
  60. local bIndex = math.floor(b / (255 / (cubeSize - 1)))
  61. return 16 + rIndex * cubeSize * cubeSize + gIndex * cubeSize + bIndex
  62. end
  63.  
  64. -- Accepts color as palette index
  65. local function drawTextHalfRes(font, x, y, str, color)
  66. color = color or 231 -- Default to white for best visibility
  67. for i=1,#str do
  68. local c = string.byte(str,i)-32
  69. if c>=0 and c<120 then
  70. local sx = (c%15)*20
  71. local sy = math.floor(c/15)*20
  72. for dy=0,9 do
  73. for dx=0,9 do
  74. local p = font.pixels[sy+dy*2+1][sx+dx*2+1]
  75. if not (p.r==TRANSPARENT_COLOR.r and p.g==TRANSPARENT_COLOR.g and p.b==TRANSPARENT_COLOR.b) then
  76. term.setPixel(x+(i-1)*10+dx, y+dy, color)
  77. end
  78. end
  79. end
  80. end
  81. end
  82. end
  83.  
  84. local function drawBMPScaled(bmp, x, y, maxw, maxh)
  85. local iw, ih = bmp.width, bmp.height
  86. local scale = math.min(maxw/iw, maxh/ih, 1)
  87. local w, h = math.floor(iw*scale), math.floor(ih*scale)
  88. for dy = 0, h-1 do
  89. local srcY = math.floor(dy * ih / h) + 1
  90. for dx = 0, w-1 do
  91. local srcX = math.floor(dx * iw / w) + 1
  92. local p = bmp.pixels[srcY][srcX]
  93. term.setPixel(x+dx, y+dy, mapColor256(p.r, p.g, p.b))
  94. end
  95. end
  96. return w, h
  97. end
  98.  
  99. -- === Base64 decode for BMP data:images ===
  100. local b='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
  101. local function base64_decode(data)
  102. data = data:gsub("[^"..b.."=]", "")
  103. return (data:gsub('.', function(x)
  104. if (x == '=') then return '' end
  105. local r,f='',(b:find(x)-1)
  106. for i=6,1,-1 do r=r..(f%2^i - f%2^(i-1) > 0 and '1' or '0') end
  107. return r
  108. end):gsub('%d%d%d?%d?%d?%d?%d?%d?', function(x)
  109. if #x ~= 8 then return '' end
  110. local c=0
  111. for i=1,8 do c=c + (x:sub(i,i)=='1' and 2^(8-i) or 0) end
  112. return string.char(c)
  113. end))
  114. end
  115.  
  116. -- === CSS helpers ===
  117. local css_colors = { red=196, green=46, blue=21, black=16, white=231, yellow=226, gray=244 }
  118. local function cssColorToPalette(c)
  119. if css_colors[c] then return css_colors[c]
  120. elseif c:sub(1,1) == "#" and #c == 7 then
  121. local r = tonumber(c:sub(2,3),16)
  122. local g = tonumber(c:sub(4,5),16)
  123. local b = tonumber(c:sub(6,7),16)
  124. return mapColor256(r,g,b)
  125. else return 231 end
  126. end
  127.  
  128. local function parseFontSize(sz)
  129. if not sz then return 12 end
  130. local n = tonumber(sz:match("^(%d+)"))
  131. if not n then return 12 end
  132. return math.max(8, math.min(36, n))
  133. end
  134.  
  135. -- Check if a palette index is light or dark for contrast
  136. local function isLightPalette(idx)
  137. local r, g, b = term.getPaletteColor(idx)
  138. local brightness = 0.299*r + 0.587*g + 0.114*b
  139. return brightness > 0.5
  140. end
  141.  
  142. -- === HTML/CSS Parsing ===
  143.  
  144. -- Minimal HTML to DOM tree
  145. local function parseHTMLtoDOM(html)
  146. local dom = { tag="body", children={}, style={}, attrs={} }
  147. local stack = { dom }
  148. local pos = 1
  149. while pos <= #html do
  150. local tagStart, tagEnd, tagText = html:find("<([/%w][^>]*)>", pos)
  151. if tagStart then
  152. -- Text before tag
  153. if tagStart > pos then
  154. local text = html:sub(pos, tagStart-1):gsub("\n", "")
  155. if #text > 0 then
  156. table.insert(stack[#stack].children, { tag="text", text=text, style={}, attrs={} })
  157. end
  158. end
  159. -- Tag
  160. local isClose = tagText:sub(1,1) == "/"
  161. local tagName = tagText:match("^/?([%w]+)")
  162. if isClose then
  163. table.remove(stack) -- pop
  164. else
  165. local attrs = {}
  166. for k,v in tagText:gmatch("([%w-]+)%s*=%s*['\"](.-)['\"]") do
  167. attrs[k] = v
  168. end
  169. local node = { tag=tagName, children={}, style={}, attrs=attrs }
  170. table.insert(stack[#stack].children, node)
  171. if not tagText:find("/$") then -- not self-closing
  172. table.insert(stack, node)
  173. end
  174. end
  175. pos = tagEnd+1
  176. else
  177. -- Remaining text
  178. local text = html:sub(pos):gsub("\n", "")
  179. if #text > 0 then
  180. table.insert(stack[#stack].children, { tag="text", text=text, style={}, attrs={} })
  181. end
  182. break
  183. end
  184. end
  185. return dom
  186. end
  187.  
  188. -- Simple CSS parser (only color, background, font-size)
  189. local function parseCSS(css)
  190. local rules = {}
  191. for selector, body in css:gmatch("([%w%.#%s]+)%s*{%s*([^}]+)}") do
  192. local props = {}
  193. for k,v in body:gmatch("([%w-]+)%s*:%s*([^;]+)") do
  194. props[k] = v
  195. end
  196. table.insert(rules, { selector=selector:match("^%s*(.-)%s*$"), props=props })
  197. end
  198. return rules
  199. end
  200.  
  201. -- Apply CSS rules to DOM nodes (very basic: tag, .class, #id)
  202. local function applyCSS(dom, rules)
  203. local function matches(node, selector)
  204. if selector:sub(1,1) == "." then
  205. return node.attrs.class == selector:sub(2)
  206. elseif selector:sub(1,1) == "#" then
  207. return node.attrs.id == selector:sub(2)
  208. else
  209. return node.tag == selector
  210. end
  211. end
  212. local function apply(node)
  213. for _,rule in ipairs(rules) do
  214. if matches(node, rule.selector) then
  215. for k,v in pairs(rule.props) do
  216. node.style[k] = v
  217. end
  218. end
  219. end
  220. -- Inline style
  221. if node.attrs.style then
  222. for k,v in node.attrs.style:gmatch("([%w-]+)%s*:%s*([^;]+)") do
  223. node.style[k] = v
  224. end
  225. end
  226. for _,child in ipairs(node.children or {}) do apply(child) end
  227. end
  228. apply(dom)
  229. end
  230.  
  231. -- === DOM Renderer ===
  232. local function renderDOM(node, font, x, y, maxw)
  233. if node.tag == "text" then
  234. local bg = node.style["background"] and cssColorToPalette(node.style["background"]) or 16 -- black default
  235. local color = node.style.color and cssColorToPalette(node.style.color) or (isLightPalette(bg) and 16 or 231)
  236. local fontSize = parseFontSize(node.style["font-size"])
  237. term.drawPixels(x, y, bg, maxw, fontSize)
  238. drawTextHalfRes(font, x, y, node.text, color)
  239. return y + fontSize
  240. elseif node.tag == "img" and node.attrs.src and node.attrs.src:find("^data:image/bmp;base64,") then
  241. local b64 = node.attrs.src:match("^data:image/bmp;base64,(.+)")
  242. if b64 then
  243. local bmpData = base64_decode(b64)
  244. local tmpFile = ".browser_img_"..os.epoch("utc")..".bmp"
  245. local f = fs.open(tmpFile, "wb")
  246. f.write(bmpData)
  247. f.close()
  248. local bmp = readBMP(tmpFile)
  249. if bmp then
  250. local w, h = drawBMPScaled(bmp, x, y, 200, 60)
  251. return y + h + 2
  252. end
  253. end
  254. return y + 12
  255. else
  256. local newY = y
  257. for _,child in ipairs(node.children or {}) do
  258. newY = renderDOM(child, font, x, newY, maxw)
  259. end
  260. return newY
  261. end
  262. end
  263.  
  264. -- === Browser Logic ===
  265. term.setGraphicsMode(2)
  266. set256Palette()
  267. local pxw, pxh = term.getSize(2)
  268. download(FONT_BMP_URL, FONT_BMP_FILE)
  269. local font = readBMP(FONT_BMP_FILE)
  270.  
  271. local function getURLInput(current)
  272. term.setGraphicsMode(0)
  273. term.setCursorPos(1, 1)
  274. term.clearLine()
  275. write("URL: ")
  276. local url = read()
  277. if url == "" then url = current end
  278. term.setGraphicsMode(2)
  279. return url
  280. end
  281.  
  282. local url = "https://example.com"
  283. local dom, cssRules = nil, {}
  284. local scroll = 0
  285. local contentHeight = 0
  286.  
  287. while true do
  288. term.setFrozen(true)
  289. term.drawPixels(0, 0, 240, pxw, 20)
  290. term.drawPixels(0, 20, 255, pxw, pxh-20)
  291. drawTextHalfRes(font, 10, 5, url)
  292. if dom then
  293. -- Measure content height for scroll bounds
  294. local function measureDOM(node, font, x, y, maxw)
  295. if node.tag == "text" then
  296. local fontSize = parseFontSize(node.style["font-size"])
  297. return y + fontSize
  298. elseif node.tag == "img" and node.attrs.src and node.attrs.src:find("^data:image/bmp;base64,") then
  299. local b64 = node.attrs.src:match("^data:image/bmp;base64,(.+)")
  300. if b64 then
  301. local bmpData = base64_decode(b64)
  302. local tmpFile = ".browser_img_"..os.epoch("utc")..".bmp"
  303. local f = fs.open(tmpFile, "wb")
  304. f.write(bmpData)
  305. f.close()
  306. local bmp = readBMP(tmpFile)
  307. if bmp then
  308. local w, h = drawBMPScaled(bmp, -1000, -1000, 200, 60) -- offscreen
  309. return y + h + 2
  310. end
  311. end
  312. return y + 12
  313. else
  314. local newY = y
  315. for _,child in ipairs(node.children or {}) do
  316. newY = measureDOM(child, font, x, newY, maxw)
  317. end
  318. return newY
  319. end
  320. end
  321. contentHeight = measureDOM(dom, font, 10, 25, pxw-20) - 25
  322. renderDOM(dom, font, 10, 25 - scroll, pxw-20)
  323. end
  324. term.setFrozen(false)
  325. local event, p1, p2, p3 = os.pullEvent()
  326. if event == "mouse_click" then
  327. local btn, x, y = p1, p2, p3
  328. if y <= 20 then
  329. url = getURLInput(url)
  330. term.setFrozen(true)
  331. drawTextHalfRes(font, 10, 25, "Loading...")
  332. term.setFrozen(false)
  333. local ok, resp = pcall(function() return http.get(url) end)
  334. if ok and resp then
  335. local html = resp.readAll()
  336. resp.close()
  337. dom = parseHTMLtoDOM(html)
  338. -- Extract <style>...</style> and parse CSS
  339. local css = ""
  340. for style in html:gmatch("<style[^>]*>(.-)</style>") do css = css .. style end
  341. cssRules = parseCSS(css)
  342. applyCSS(dom, cssRules)
  343. else
  344. dom = { tag="body", children={ {tag="text", text="Failed to load: "..tostring(url), style={}, attrs={}} }, style={}, attrs={} }
  345. end
  346. scroll = 0
  347. end
  348. elseif event == "mouse_scroll" then
  349. local dir = p1
  350. local maxScroll = math.max(0, contentHeight - (pxh-30))
  351. scroll = math.max(0, math.min(scroll + dir*12, maxScroll))
  352. elseif event == "key" then
  353. if p1 == keys.up then
  354. scroll = math.max(scroll-12, 0)
  355. elseif p1 == keys.down then
  356. local maxScroll = math.max(0, contentHeight - (pxh-30))
  357. scroll = math.min(scroll+12, maxScroll)
  358. elseif p1 == keys.pageup then
  359. scroll = math.max(scroll-(pxh-30), 0)
  360. elseif p1 == keys.pagedown then
  361. local maxScroll = math.max(0, contentHeight - (pxh-30))
  362. scroll = math.min(scroll+(pxh-30), maxScroll)
  363. elseif p1 == keys.q or p1 == keys.escape then
  364. break
  365. end
  366. elseif event == "terminate" then
  367. break
  368. end
  369. end
  370.  
  371. term.setGraphicsMode(0)
  372. term.clear()
  373. print("Browser exited.")
  374.  
Add Comment
Please, Sign In to add comment