browser.lua (15591B)
1 -- browser.lua - Moonbrowser main entry point 2 -- Opens HTML files and renders them to a window 3 4 local parser = require("parser", true) -- Use bytecode if available 5 local Renderer = require("render", true) -- Use bytecode if available 6 7 local Parser = parser.Parser 8 9 -- Default file to open 10 local htmlPath = "/home/Documents/test.html" 11 12 -- Check for command line arguments 13 if args then 14 local argPath = args.o or args.open or args[1] 15 if argPath and argPath ~= "" then 16 htmlPath = argPath 17 end 18 end 19 20 print("Moonbrowser: Opening " .. htmlPath) 21 22 -- Expand ~ to /home 23 if htmlPath:sub(1, 1) == "~" then 24 htmlPath = "/home" .. htmlPath:sub(2) 25 end 26 27 -- Check if SafeFS is available 28 if not fs then 29 print("ERROR: SafeFS not available (missing 'filesystem' permission)") 30 return 31 end 32 33 -- Check if file exists - if not, show a "file not found" page 34 local htmlContent 35 if not fs:exists(htmlPath) then 36 print("Moonbrowser: File not found, showing error page: " .. htmlPath) 37 htmlContent = [[ 38 <!DOCTYPE html> 39 <html> 40 <head> 41 <title>File Not Found</title> 42 </head> 43 <body style="background-color: #2d2d2d; color: #ffffff; font-family: sans-serif; padding: 20px;"> 44 <h1 style="color: #ff6b6b;">File Not Found</h1> 45 <p>The requested file could not be found:</p> 46 <p style="color: #aaaaaa; font-family: monospace; background-color: #1a1a1a; padding: 10px;">]] .. htmlPath .. [[</p> 47 <p style="margin-top: 20px;">Please check the path and try again.</p> 48 <p style="color: #888888; font-size: 12px; margin-top: 30px;">Tip: Use the file dialog in Explorer to open HTML files, or pass a path as an argument.</p> 49 </body> 50 </html> 51 ]] 52 else 53 -- Read the HTML file 54 local err 55 htmlContent, err = fs:read(htmlPath) 56 if not htmlContent then 57 print("Moonbrowser: Failed to read file, showing error page: " .. tostring(err)) 58 htmlContent = [[ 59 <!DOCTYPE html> 60 <html> 61 <head> 62 <title>Read Error</title> 63 </head> 64 <body style="background-color: #2d2d2d; color: #ffffff; font-family: sans-serif; padding: 20px;"> 65 <h1 style="color: #ff6b6b;">Failed to Read File</h1> 66 <p>Could not read the file:</p> 67 <p style="color: #aaaaaa; font-family: monospace; background-color: #1a1a1a; padding: 10px;">]] .. htmlPath .. [[</p> 68 <p style="color: #ff9999;">Error: ]] .. tostring(err) .. [[</p> 69 </body> 70 </html> 71 ]] 72 end 73 end 74 75 print("Moonbrowser: Read " .. #htmlContent .. " bytes") 76 77 -- Parse HTML 78 local htmlParser = Parser.new() 79 local dom = htmlParser:parse(htmlContent) 80 81 if not dom then 82 print("ERROR: Failed to parse HTML") 83 return 84 end 85 86 print("Moonbrowser: HTML parsed successfully") 87 88 -- Parse meta tags for width and height 89 local windowWidth = 800 90 local windowHeight = 600 91 92 -- Look for <meta name="width" content="..."> and <meta name="height" content="..."> 93 for name, content in htmlContent:gmatch('<meta[^>]+name%s*=%s*["\']([^"\']+)["\'][^>]+content%s*=%s*["\']([^"\']+)["\']') do 94 if name == "width" then 95 local w = tonumber(content) 96 if w and w > 0 and w <= 2048 then 97 windowWidth = w 98 end 99 elseif name == "height" then 100 local h = tonumber(content) 101 if h and h > 0 and h <= 2048 then 102 windowHeight = h 103 end 104 end 105 end 106 107 -- Also check reverse order: content before name 108 for content, name in htmlContent:gmatch('<meta[^>]+content%s*=%s*["\']([^"\']+)["\'][^>]+name%s*=%s*["\']([^"\']+)["\']') do 109 if name == "width" then 110 local w = tonumber(content) 111 if w and w > 0 and w <= 2048 then 112 windowWidth = w 113 end 114 elseif name == "height" then 115 local h = tonumber(content) 116 if h and h > 0 and h <= 2048 then 117 windowHeight = h 118 end 119 end 120 end 121 122 -- Toolbar settings 123 local toolbarHeight = 30 124 local statusBarHeight = 20 125 126 print("Moonbrowser: Window size " .. windowWidth .. "x" .. windowHeight) 127 128 -- Adjust window height to include toolbar 129 local totalWindowHeight = windowHeight + toolbarHeight 130 131 -- Create window 132 local window = app:newWindow("Moonbrowser - " .. htmlPath, windowWidth, totalWindowHeight, true) 133 134 if not window then 135 print("ERROR: Failed to create window") 136 return 137 end 138 139 -- Create renderer with content area (excluding toolbar and status bar) 140 local contentHeight = windowHeight - statusBarHeight 141 local renderer = Renderer.new(nil, windowWidth, contentHeight) 142 renderer.offsetY = toolbarHeight -- Set offset for toolbar 143 144 -- Store DOM in renderer for query() support 145 renderer:setDOM(dom) 146 147 -- Create page sandbox for executing onclick handlers and inline scripts 148 -- Query function used by both query() and document.querySelector() 149 local function queryFunc(selector) 150 return renderer:query(selector) 151 end 152 153 local function queryAllFunc(selector) 154 return renderer:queryAll(selector) 155 end 156 157 local pageSandbox = { 158 -- DOM query functions 159 query = queryFunc, 160 queryAll = queryAllFunc, 161 document = { 162 querySelector = queryFunc, 163 querySelectorAll = queryAllFunc, 164 getElementById = function(id) 165 return renderer:query("#" .. id) 166 end, 167 }, 168 169 -- Dialog functions (browser-like) - wrapped to use Moonbrowser's app context 170 alert = function(msg) 171 if Dialog and Dialog.alert then 172 Dialog.alert(tostring(msg), { app = app }) 173 else 174 print("ALERT: " .. tostring(msg)) 175 end 176 end, 177 confirm = function(msg) 178 if Dialog and Dialog.confirm then 179 return Dialog.confirm(tostring(msg), { app = app }) 180 else 181 print("CONFIRM: " .. tostring(msg)) 182 return true 183 end 184 end, 185 prompt = function(msg, default) 186 if Dialog and Dialog.prompt then 187 return Dialog.prompt(tostring(msg), { app = app, default = default }) 188 else 189 print("PROMPT: " .. tostring(msg)) 190 return default or "" 191 end 192 end, 193 194 -- Console 195 console = { 196 log = function(...) 197 local args = {...} 198 local parts = {} 199 for i = 1, #args do 200 parts[i] = tostring(args[i]) 201 end 202 print("[console] " .. table.concat(parts, " ")) 203 end, 204 error = function(...) 205 local args = {...} 206 local parts = {} 207 for i = 1, #args do 208 parts[i] = tostring(args[i]) 209 end 210 print("[console.error] " .. table.concat(parts, " ")) 211 end, 212 warn = function(...) 213 local args = {...} 214 local parts = {} 215 for i = 1, #args do 216 parts[i] = tostring(args[i]) 217 end 218 print("[console.warn] " .. table.concat(parts, " ")) 219 end, 220 }, 221 222 -- Basic Lua functions 223 print = print, 224 tostring = tostring, 225 tonumber = tonumber, 226 type = type, 227 pairs = pairs, 228 ipairs = ipairs, 229 string = string, 230 table = table, 231 math = math, 232 233 -- Window control 234 window = { 235 setSize = function(w, h) 236 if type(w) == "number" and type(h) == "number" and w > 0 and h > 0 then 237 windowWidth = w 238 windowHeight = h 239 window:setSize(w, h) 240 renderer.width = w 241 renderer.height = h 242 window:markDirty() 243 end 244 end, 245 getWidth = function() 246 return windowWidth 247 end, 248 getHeight = function() 249 return windowHeight 250 end, 251 }, 252 } 253 254 -- Function to execute code in page sandbox 255 local function executeInSandbox(code) 256 if not code or code == "" then return end 257 258 -- Use loadstring with custom environment 259 -- The third parameter to loadstring sets the environment 260 local func, err = loadstring(code, "onclick", pageSandbox) 261 if not func then 262 print("Moonbrowser: Script error: " .. tostring(err)) 263 return 264 end 265 266 local ok, result = pcall(func) 267 if not ok then 268 print("Moonbrowser: Script runtime error: " .. tostring(result)) 269 end 270 return result 271 end 272 273 -- Store executeInSandbox for use in click handler 274 renderer.executeScript = executeInSandbox 275 276 -- Toolbar button definitions 277 local toolbarButtons = { 278 { x = 5, y = 5, width = 50, height = 20, label = "Open", action = "open" }, 279 { x = 60, y = 5, width = 50, height = 20, label = "Back", action = "back" }, 280 { x = 115, y = 5, width = 60, height = 20, label = "Refresh", action = "refresh" }, 281 } 282 283 -- History for back button 284 local history = {} 285 286 -- Draw callback 287 window.onDraw = function(gfx) 288 -- Draw toolbar background 289 gfx:fillRect(0, 0, windowWidth, toolbarHeight, 0x404040) 290 291 -- Draw toolbar buttons 292 for _, btn in ipairs(toolbarButtons) do 293 -- Button background 294 gfx:fillRect(btn.x, btn.y, btn.width, btn.height, 0x606060) 295 -- Button border 296 gfx:drawRect(btn.x, btn.y, btn.width, btn.height, 0x808080) 297 -- Button label 298 local textX = btn.x + (btn.width - #btn.label * 6) / 2 299 local textY = btn.y + 6 300 gfx:drawText(textX, textY, btn.label, 0xFFFFFF) 301 end 302 303 -- Draw separator line below toolbar 304 gfx:fillRect(0, toolbarHeight - 1, windowWidth, 1, 0x303030) 305 306 -- Render DOM to content area (renderer handles its own offset) 307 renderer:render(dom, gfx) 308 309 -- Draw status bar at bottom 310 local statusY = totalWindowHeight - statusBarHeight 311 gfx:fillRect(0, statusY, windowWidth, statusBarHeight, 0x333333) 312 gfx:drawText(5, statusY + 4, htmlPath, 0xFFFFFF) 313 gfx:drawText(windowWidth - 150, statusY + 4, "Arrows: Scroll Q: Quit", 0xAAAAAA) 314 end 315 316 -- Input callback 317 window.onInput = function(key, scancode) 318 -- First, let renderer handle input if it has focus 319 if renderer:hasFocus() then 320 if renderer:handleKey(key, scancode) then 321 window:markDirty() 322 return 323 end 324 end 325 326 if key == "q" or key == "Q" then 327 window:close() 328 elseif scancode == 72 then -- Up arrow 329 renderer:scroll(-40) 330 window:markDirty() 331 elseif scancode == 80 then -- Down arrow 332 renderer:scroll(40) 333 window:markDirty() 334 end 335 end 336 337 -- Helper to resolve relative paths 338 local function resolvePath(href, currentPath) 339 if not href then return nil end 340 341 -- Handle javascript: URLs 342 if href:sub(1, 11) == "javascript:" then 343 return nil, href:sub(12) -- Return nil path, but return the JS code 344 end 345 346 -- Absolute path 347 if href:sub(1, 1) == "/" then 348 return href 349 end 350 351 -- Get directory of current file 352 local currentDir = currentPath:match("(.*/)") 353 if not currentDir then 354 currentDir = "/" 355 end 356 357 -- Handle ../ (parent directory) 358 while href:sub(1, 3) == "../" do 359 href = href:sub(4) 360 -- Go up one directory 361 currentDir = currentDir:match("(.*/)[^/]+/$") or "/" 362 end 363 364 -- Handle ./ (current directory) 365 if href:sub(1, 2) == "./" then 366 href = href:sub(3) 367 end 368 369 return currentDir .. href 370 end 371 372 -- Function to navigate to a new page 373 local function navigateTo(newPath) 374 print("Moonbrowser: Navigating to " .. newPath) 375 376 -- Check if file exists 377 if not fs:exists(newPath) then 378 print("ERROR: File not found: " .. newPath) 379 return false 380 end 381 382 -- Read the new HTML file 383 local newContent, err = fs:read(newPath) 384 if not newContent then 385 print("ERROR: Failed to read file: " .. tostring(err)) 386 return false 387 end 388 389 -- Update current path 390 htmlPath = newPath 391 392 -- Parse new HTML 393 local newDom = htmlParser:parse(newContent) 394 if not newDom then 395 print("ERROR: Failed to parse HTML") 396 return false 397 end 398 399 -- Update DOM and renderer 400 dom = newDom 401 renderer:setDOM(dom) 402 renderer:resetScroll() 403 404 print("Moonbrowser: Navigation complete") 405 return true 406 end 407 408 -- Function to open file dialog 409 local function openFileDialog() 410 if not Dialog or not Dialog.fileOpen then 411 print("Moonbrowser: Dialog.fileOpen not available") 412 return 413 end 414 415 local dialog = Dialog.fileOpen("/home", { 416 app = app, 417 fs = fs, 418 title = "Open HTML File" 419 }) 420 421 dialog:openDialog(function(selectedPath) 422 if selectedPath then 423 -- Check if it's an HTML file 424 if selectedPath:match("%.html?$") then 425 -- Save current path to history 426 table.insert(history, htmlPath) 427 -- Navigate to selected file 428 if navigateTo(selectedPath) then 429 window.title = "Moonbrowser - " .. selectedPath 430 window:markDirty() 431 end 432 else 433 print("Moonbrowser: Not an HTML file: " .. selectedPath) 434 if Dialog.alert then 435 Dialog.alert("Please select an HTML file (.html or .htm)", { app = app }) 436 end 437 end 438 end 439 end) 440 end 441 442 -- Function to go back in history 443 local function goBack() 444 if #history > 0 then 445 local prevPath = table.remove(history) 446 if navigateTo(prevPath) then 447 window.title = "Moonbrowser - " .. prevPath 448 window:markDirty() 449 end 450 end 451 end 452 453 -- Function to refresh current page 454 local function refreshPage() 455 if navigateTo(htmlPath) then 456 window:markDirty() 457 end 458 end 459 460 -- Click callback 461 window.onClick = function(x, y, button) 462 -- Check if click is in toolbar area 463 if y < toolbarHeight then 464 -- Check toolbar buttons 465 for _, btn in ipairs(toolbarButtons) do 466 if x >= btn.x and x < btn.x + btn.width and 467 y >= btn.y and y < btn.y + btn.height then 468 print("Moonbrowser: Toolbar button clicked: " .. btn.action) 469 if btn.action == "open" then 470 openFileDialog() 471 elseif btn.action == "back" then 472 goBack() 473 elseif btn.action == "refresh" then 474 refreshPage() 475 end 476 return 477 end 478 end 479 return 480 end 481 482 -- Adjust click position for content area 483 local contentY = y - toolbarHeight 484 485 local result = renderer:handleClick(x, contentY) 486 if result then 487 if result.type == "button" then 488 print("Button clicked: " .. (result.text or "(no text)")) 489 if result.onclick then 490 -- Execute onclick handler in page sandbox 491 executeInSandbox(result.onclick) 492 end 493 elseif result.type == "submit" then 494 print("Form submitted with data:") 495 for name, value in pairs(result.formData) do 496 print(" " .. name .. " = " .. tostring(value)) 497 end 498 elseif result.type == "link" then 499 print("Link clicked: " .. (result.href or "(no href)")) 500 if result.href then 501 local newPath, jsCode = resolvePath(result.href, htmlPath) 502 if jsCode then 503 -- javascript: URL 504 executeInSandbox(jsCode) 505 elseif newPath then 506 -- Save current path to history before navigating 507 table.insert(history, htmlPath) 508 -- Navigate to new page 509 navigateTo(newPath) 510 end 511 end 512 elseif result.type == "focus" then 513 -- Input focused, no action needed 514 elseif result.type == "toggle" then 515 -- Checkbox toggled, no action needed 516 end 517 window:markDirty() 518 end 519 end 520 521 print("Moonbrowser: Window ready")