init.lua (45488B)
1 -- LuajitOS Installer 2 -- Runs with global environment access for system-level operations 3 4 if osprint then 5 osprint("Installer: Starting...\n") 6 end 7 8 -- Available encryption algorithms 9 local AVAILABLE_ALGORITHMS = { 10 "AES-256-GCM", 11 "Twofish-256-GCM", 12 "Serpent-256-GCM", 13 "XChaCha20-Poly1305" 14 } 15 16 -- Map encryption chain to FDE cipher mode 17 -- FDE only supports: NONE (0), AES (1), XCHACHA (2), CASCADE (3) 18 -- We map the chain to the best available FDE mode 19 local function getCipherModeFromChain(chain) 20 if not chain or #chain == 0 then 21 return fde and fde.CIPHER_NONE or 0 22 end 23 24 local hasAES = false 25 local hasXChaCha = false 26 27 for _, algo in ipairs(chain) do 28 if algo == "AES-256-GCM" then 29 hasAES = true 30 elseif algo == "XChaCha20-Poly1305" then 31 hasXChaCha = true 32 end 33 -- Note: Twofish and Serpent are shown in UI but FDE doesn't support them yet 34 -- They will be treated as AES for now (future: add support in fde.c) 35 end 36 37 if hasAES and hasXChaCha then 38 return fde and fde.CIPHER_CASCADE or 3 39 elseif hasXChaCha then 40 return fde and fde.CIPHER_XCHACHA or 2 41 elseif hasAES or #chain > 0 then 42 -- Default to AES for any encryption 43 return fde and fde.CIPHER_AES or 1 44 else 45 return fde and fde.CIPHER_NONE or 0 46 end 47 end 48 49 -- State 50 local state = { 51 drives = {}, 52 selectedDrive = nil, 53 step = 1, -- 1 = drive selection, 2 = encryption selection, 3 = password, 4 = install 54 -- Encryption chain (order matters for cascading encryption) 55 encryptionChain = {"XChaCha20-Poly1305"}, -- Default to XChaCha (software-only, no AES-NI needed) 56 -- Password for encryption 57 password = "", 58 passwordConfirm = "", 59 passwordError = nil, 60 passwordInputActive = false, -- Which input is active (false = password, true = confirm) 61 -- Installation progress 62 installing = false, 63 progress = 0, -- 0 to 100 64 statusText = "Ready to install", 65 installError = nil 66 } 67 68 -- Get list of drives from diskfs 69 local function refreshDrives() 70 state.drives = {} 71 if diskfs and diskfs.getMounts then 72 local mounts = diskfs.getMounts() 73 if mounts then 74 for i, mount in ipairs(mounts) do 75 table.insert(state.drives, { 76 index = i, 77 bus = mount.bus, 78 drive = mount.drive, 79 model = mount.model or "Unknown", 80 sectors = mount.sectors or 0, 81 sizeMB = math.floor((mount.sectors or 0) * 512 / (1024 * 1024)), 82 formatted = mount.formatted, 83 volume_name = mount.volume_name 84 }) 85 end 86 end 87 end 88 89 -- If no diskfs, try ata directly 90 if #state.drives == 0 and ata and ata.listDrives then 91 local drives = ata.listDrives() 92 if drives then 93 for i, drive in ipairs(drives) do 94 table.insert(state.drives, { 95 index = i, 96 bus = drive.bus, 97 drive = drive.drive, 98 model = drive.model or "Unknown", 99 sectors = drive.sectors or 0, 100 sizeMB = math.floor((drive.sectors or 0) * 512 / (1024 * 1024)), 101 formatted = false, 102 volume_name = nil 103 }) 104 end 105 end 106 end 107 108 if osprint then 109 osprint("Installer: Found " .. #state.drives .. " drive(s)\n") 110 end 111 end 112 113 -- Create main window 114 local screenWidth = 1024 115 local screenHeight = 768 116 local windowWidth = 500 117 local windowHeight = 400 118 119 local window = app:newWindow( 120 math.floor((screenWidth - windowWidth) / 2), 121 math.floor((screenHeight - windowHeight) / 2), 122 windowWidth, 123 windowHeight 124 ) 125 window.title = "LuajitOS Installer" 126 window.resizable = false 127 128 -- Colors 129 local COLOR_BG = 0xF0F0F0 130 local COLOR_TEXT = 0x000000 131 local COLOR_TITLE = 0x333333 132 local COLOR_BUTTON = 0x4A90D9 133 local COLOR_BUTTON_TEXT = 0xFFFFFF 134 local COLOR_BUTTON_DISABLED = 0xAAAAAA 135 local COLOR_RADIO_BG = 0xFFFFFF 136 local COLOR_RADIO_BORDER = 0x888888 137 local COLOR_RADIO_SELECTED = 0x4A90D9 138 local COLOR_DRIVE_BG = 0xFFFFFF 139 local COLOR_DRIVE_SELECTED = 0xE0E8F0 140 local COLOR_ROW_BG = 0xFFFFFF 141 local COLOR_ROW_BORDER = 0xCCCCCC 142 local COLOR_SMALL_BTN = 0x666666 143 local COLOR_SMALL_BTN_TEXT = 0xFFFFFF 144 local COLOR_ADD_BTN = 0x5A9A5A 145 local COLOR_PROGRESS_BG = 0xDDDDDD 146 local COLOR_PROGRESS_FG = 0x4A90D9 147 local COLOR_PROGRESS_BORDER = 0x888888 148 local COLOR_ERROR = 0xCC4444 149 local COLOR_SUCCESS = 0x44AA44 150 151 -- Helper: Check if encryption is enabled 152 local function isEncryptionEnabled() 153 return #state.encryptionChain > 0 154 end 155 156 -- Helper: Get encryption chain summary string 157 local function getEncryptionSummary() 158 if #state.encryptionChain == 0 then 159 return "None (No Encryption)" 160 end 161 return table.concat(state.encryptionChain, " + ") 162 end 163 164 -- Helper: Move item up in encryption chain 165 local function moveUp(index) 166 if index > 1 then 167 local temp = state.encryptionChain[index] 168 state.encryptionChain[index] = state.encryptionChain[index - 1] 169 state.encryptionChain[index - 1] = temp 170 end 171 end 172 173 -- Helper: Move item down in encryption chain 174 local function moveDown(index) 175 if index < #state.encryptionChain then 176 local temp = state.encryptionChain[index] 177 state.encryptionChain[index] = state.encryptionChain[index + 1] 178 state.encryptionChain[index + 1] = temp 179 end 180 end 181 182 -- Helper: Remove item from encryption chain 183 local function removeAlgorithm(index) 184 if #state.encryptionChain > 0 then 185 table.remove(state.encryptionChain, index) 186 end 187 end 188 189 -- Helper: Add algorithm to chain (duplicates allowed for stacking, max 5) 190 local function addAlgorithm(algo) 191 if #state.encryptionChain >= 5 then 192 return false -- Maximum reached 193 end 194 table.insert(state.encryptionChain, algo) 195 return true 196 end 197 198 -- Directories to skip during installation 199 local SKIP_DIRS = { 200 ["/mnt"] = true, 201 ["/proc"] = true, 202 ["/tmp"] = true, 203 ["/os/public/res"] = true, -- Skip large resource files (background.bmp is 6MB) 204 ["/home"] = true, -- Skip user files 205 ["/keys"] = true -- Skip key files 206 } 207 208 -- Helper: Check if path should be skipped 209 local function shouldSkip(path) 210 for skipPath, _ in pairs(SKIP_DIRS) do 211 if path == skipPath or path:sub(1, #skipPath + 1) == skipPath .. "/" then 212 return true 213 end 214 end 215 return false 216 end 217 218 -- Helper: Recursively list all files in ramdisk 219 local function listAllFiles(path, files) 220 files = files or {} 221 222 if shouldSkip(path) then 223 return files 224 end 225 226 local entries = CRamdiskList(path) 227 if not entries then 228 return files 229 end 230 231 for _, entry in ipairs(entries) do 232 local fullPath = path == "/" and ("/" .. entry.name) or (path .. "/" .. entry.name) 233 234 if not shouldSkip(fullPath) then 235 if entry.type == "file" then 236 table.insert(files, fullPath) 237 elseif entry.type == "directory" or entry.type == "dir" then 238 -- Add directory marker 239 table.insert(files, fullPath .. "/") 240 -- Recurse into directory 241 listAllFiles(fullPath, files) 242 end 243 end 244 end 245 246 return files 247 end 248 249 -- Installation state 250 local installState = { 251 files = nil, 252 fileIndex = 0, 253 totalFiles = 0, 254 drive = nil, 255 batchSize = 10 256 } 257 258 -- Helper functions for installation 259 local function updateProgress(percent, text) 260 state.progress = percent 261 state.statusText = text 262 window:markDirty() 263 end 264 265 local function installError(msg) 266 state.installing = false 267 state.installError = msg 268 state.statusText = "Installation failed" 269 window:markDirty() 270 if osprint then 271 osprint("Installer: Error - " .. msg .. "\n") 272 end 273 end 274 275 local function installComplete() 276 state.installing = false 277 updateProgress(100, "Installation complete!") 278 if osprint then 279 osprint("Installer: Installation complete!\n") 280 end 281 end 282 283 -- Forward declare step functions 284 local copyFileBatch 285 local startCopyPhase 286 local startFormatPhase 287 288 -- Time constants (workaround: LuaJIT hangs on decimal literals like 0.1) 289 local DELAY_FAST = 1/20 -- 0.05 seconds 290 local DELAY_NORMAL = 1/10 -- 0.1 seconds 291 292 -- Timer counter for unique names 293 local timerCounter = 0 294 local function nextTimerName() 295 timerCounter = timerCounter + 1 296 return "install_" .. timerCounter 297 end 298 299 -- Helper to schedule timer (use sandboxed Timer API) 300 local function scheduleTimer(name, delay, callback) 301 if Timer and Timer.simple then 302 Timer.simple(name, delay, callback) 303 elseif osprint then 304 osprint("Installer: Timer not available!\n") 305 end 306 end 307 308 -- Copy files in batches using diskfs 309 copyFileBatch = function() 310 local copied = 0 311 local driveIndex = state.selectedDrive - 1 312 local mountPath = "/mnt/hd" .. driveIndex 313 314 while copied < installState.batchSize and installState.fileIndex < installState.totalFiles do 315 installState.fileIndex = installState.fileIndex + 1 316 local filepath = installState.files[installState.fileIndex] 317 if filepath then 318 local destPath = mountPath .. filepath 319 if filepath:sub(-1) == "/" then 320 -- Directory 321 local dirPath = destPath:sub(1, -2) 322 if osprint then 323 osprint("Installer: mkdir " .. dirPath .. "\n") 324 end 325 diskfs.mkdir(dirPath) 326 else 327 -- File - read from ramdisk and write to disk 328 local srcHandle = CRamdiskOpen(filepath, "r") 329 if srcHandle then 330 local content = CRamdiskRead(srcHandle) 331 CRamdiskClose(srcHandle) 332 if content then 333 if osprint then 334 osprint("Installer: copy " .. filepath .. " -> " .. destPath .. " (" .. #content .. " bytes)\n") 335 end 336 local handle = diskfs.open(destPath, "w") 337 if handle then 338 local writeResult = diskfs.write(handle, content) 339 diskfs.close(handle) 340 if osprint and not writeResult then 341 osprint("Installer: WRITE FAILED for " .. destPath .. "\n") 342 end 343 else 344 if osprint then 345 osprint("Installer: OPEN FAILED for " .. destPath .. "\n") 346 end 347 end 348 else 349 if osprint then 350 osprint("Installer: READ FAILED for " .. filepath .. "\n") 351 end 352 end 353 else 354 if osprint then 355 osprint("Installer: OPEN SRC FAILED for " .. filepath .. "\n") 356 end 357 end 358 end 359 copied = copied + 1 360 end 361 end 362 local percent = math.floor((installState.fileIndex / installState.totalFiles) * 80) + 15 363 updateProgress(percent, "Copying " .. installState.fileIndex .. "/" .. installState.totalFiles) 364 if installState.fileIndex >= installState.totalFiles then 365 updateProgress(95, "Finalizing...") 366 scheduleTimer(nextTimerName(), DELAY_NORMAL, function() 367 installComplete() 368 end) 369 else 370 scheduleTimer(nextTimerName(), DELAY_FAST, copyFileBatch) 371 end 372 end 373 374 -- Start copy phase 375 startCopyPhase = function() 376 updateProgress(10, "Scanning filesystem...") 377 if osprint then 378 osprint("Installer: Scanning for files...\n") 379 end 380 installState.files = listAllFiles("/") 381 installState.totalFiles = #installState.files 382 installState.fileIndex = 0 383 if osprint then 384 osprint("Installer: Found " .. installState.totalFiles .. " files to copy\n") 385 -- Print first 5 files for debugging 386 for i = 1, math.min(5, installState.totalFiles) do 387 osprint(" File " .. i .. ": " .. installState.files[i] .. "\n") 388 end 389 end 390 if installState.totalFiles == 0 then 391 if osprint then 392 osprint("Installer: WARNING - No files found to copy!\n") 393 end 394 installComplete() 395 return 396 end 397 updateProgress(15, "Copying files...") 398 scheduleTimer(nextTimerName(), DELAY_FAST, copyFileBatch) 399 end 400 401 -- Forward declare boot partition setup 402 local setupBootPartition 403 404 -- Format phase - sets up partitions, FDE encryption layer, then formats filesystem 405 startFormatPhase = function() 406 local drive = installState.drive 407 408 -- Step 1: Create partition layout if encryption is enabled 409 if isEncryptionEnabled() then 410 updateProgress(1, "Creating partition layout...") 411 if osprint then 412 osprint("Installer: Creating partition layout...\n") 413 end 414 415 if not partition then 416 installError("Partition module not available") 417 return 418 end 419 420 -- Create LuajitOS partition layout: 16MB boot + encrypted data 421 -- partition.createLayout(bus, drive, bootSizeMb) -> boolean 422 local ok = partition.createLayout(drive.bus, drive.drive, 16) 423 if not ok then 424 installError("Failed to create partition layout") 425 return 426 end 427 428 if osprint then 429 osprint("Installer: Partition layout created\n") 430 end 431 432 -- Get boot partition info (partition 1, index 0) 433 local bootPartInfo = partition.getInfo(drive.bus, drive.drive, 0) 434 if not bootPartInfo or not bootPartInfo.exists then 435 installError("Failed to get boot partition info") 436 return 437 end 438 439 -- Store boot partition info for later 440 installState.bootPartStart = bootPartInfo.startLba 441 installState.bootPartSize = bootPartInfo.sectorCount 442 443 if osprint then 444 osprint("Installer: Boot partition: start=" .. installState.bootPartStart .. " size=" .. installState.bootPartSize .. "\n") 445 end 446 447 -- Get encrypted partition info (partition 2, index 1) 448 local partInfo = partition.getInfo(drive.bus, drive.drive, 1) 449 if not partInfo or not partInfo.exists then 450 installError("Failed to get encrypted partition info") 451 return 452 end 453 454 local partStart = partInfo.startLba 455 local partSize = partInfo.sectorCount 456 457 if osprint then 458 osprint("Installer: Encrypted partition: start=" .. partStart .. " size=" .. partSize .. "\n") 459 end 460 461 -- Step 2: Set up encryption on the data partition 462 updateProgress(2, "Setting up encryption...") 463 if osprint then 464 osprint("Installer: Setting up encryption with " .. getEncryptionSummary() .. "...\n") 465 end 466 467 if not fde then 468 installError("FDE module not available") 469 return 470 end 471 472 -- Get cipher mode from chain 473 local cipher = getCipherModeFromChain(state.encryptionChain) 474 if osprint then 475 osprint("Installer: Using cipher mode " .. cipher .. "\n") 476 end 477 478 -- Format the encrypted partition with FDE 479 local ok, err = fde.formatPartition(drive.bus, drive.drive, partStart, partSize, state.password, cipher) 480 if not ok then 481 installError("Encryption setup failed: " .. tostring(err)) 482 return 483 end 484 485 updateProgress(4, "Opening encrypted volume...") 486 487 -- Open the encrypted partition 488 ok, err = fde.openPartition(drive.bus, drive.drive, partStart, state.password) 489 if not ok then 490 installError("Failed to open encrypted volume: " .. tostring(err)) 491 return 492 end 493 494 if osprint then 495 osprint("Installer: Encrypted partition opened successfully\n") 496 end 497 498 -- Step 2.5: Format boot partition with FAT16 and install bootloader 499 updateProgress(5, "Setting up boot partition...") 500 scheduleTimer(nextTimerName(), DELAY_FAST, function() 501 setupBootPartition(drive) 502 end) 503 else 504 -- No encryption - skip boot partition setup 505 scheduleTimer(nextTimerName(), DELAY_FAST, function() 506 -- Step 3: Format filesystem (no encryption) 507 updateProgress(6, "Creating filesystem...") 508 if osprint then 509 osprint("Installer: Formatting drive hd" .. (state.selectedDrive - 1) .. "...\n") 510 end 511 512 local ok, err = diskfs.format(drive.bus, drive.drive, "LUAJITOS") 513 if not ok then 514 installError("Format failed: " .. tostring(err)) 515 return 516 end 517 518 updateProgress(8, "Filesystem created...") 519 scheduleTimer(nextTimerName(), DELAY_NORMAL, startCopyPhase) 520 end) 521 end 522 end 523 524 -- Setup boot partition with FAT16 and copy kernel/GRUB files 525 setupBootPartition = function(drive) 526 if not fat16 then 527 installError("FAT16 module not available") 528 return 529 end 530 531 updateProgress(5, "Formatting boot partition (FAT16)...") 532 if osprint then 533 osprint("Installer: Formatting boot partition with FAT16...\n") 534 end 535 536 -- Format the boot partition with FAT16 537 local ok, err = fat16.format(drive.bus, drive.drive, installState.bootPartStart, installState.bootPartSize, "BOOT") 538 if not ok then 539 installError("FAT16 format failed: " .. tostring(err)) 540 return 541 end 542 543 -- Mount the FAT16 partition 544 ok, err = fat16.mount(drive.bus, drive.drive, installState.bootPartStart) 545 if not ok then 546 installError("FAT16 mount failed: " .. tostring(err)) 547 return 548 end 549 550 if osprint then 551 osprint("Installer: FAT16 boot partition mounted\n") 552 end 553 554 updateProgress(6, "Creating boot directories...") 555 556 -- Create /boot and /boot/grub directories 557 fat16.mkdir("/boot") 558 fat16.mkdir("/boot/grub") 559 560 updateProgress(6, "Copying kernel to boot partition...") 561 562 -- Read kernel from current boot location (ramdisk or iso) 563 -- The kernel binary should be available at the path where we booted from 564 -- For now, read from ramdisk if available 565 local kernelData = nil 566 567 -- Try to read kernel from the ramdisk (packed during build) 568 -- The kernel is embedded in the ISO, we need to get it from the boot media 569 -- Since we're running from ramdisk, we can try to read the kernel from the boot device 570 571 -- Actually, we need to copy the running kernel. Let's read it from sector 0 of the CD-ROM 572 -- or from wherever GRUB loaded it. For simplicity, embed a small marker. 573 574 -- For now, create a placeholder - the kernel will need to be written by the user 575 -- or we need a different approach to get the kernel binary 576 577 -- Create GRUB configuration 578 updateProgress(7, "Creating GRUB configuration...") 579 580 local grubCfg = [[ 581 set timeout=5 582 set default=0 583 584 menuentry "LuajitOS" { 585 multiboot /boot/kernel.bin 586 boot 587 } 588 589 menuentry "LuajitOS (Safe Mode)" { 590 multiboot /boot/kernel.bin safemode 591 boot 592 } 593 ]] 594 595 ok, err = fat16.writeFile("/boot/grub/grub.cfg", grubCfg) 596 if not ok then 597 if osprint then 598 osprint("Installer: Warning - failed to write grub.cfg: " .. tostring(err) .. "\n") 599 end 600 else 601 if osprint then 602 osprint("Installer: GRUB config written to /boot/grub/grub.cfg\n") 603 end 604 end 605 606 -- Try to read kernel from ramdisk at /os/boot/kernel.bin 607 updateProgress(7, "Copying kernel to boot partition...") 608 609 local kernelData = nil 610 local kernelCopied = false 611 612 -- Read kernel from ramdisk 613 local kernelHandle = CRamdiskOpen("/os/boot/kernel.bin", "r") 614 if kernelHandle then 615 kernelData = CRamdiskRead(kernelHandle) 616 CRamdiskClose(kernelHandle) 617 618 if kernelData and #kernelData > 0 then 619 if osprint then 620 osprint("Installer: Found kernel in ramdisk (" .. #kernelData .. " bytes)\n") 621 end 622 623 -- Write kernel to boot partition 624 local ok, err = fat16.writeFile("/boot/kernel.bin", kernelData) 625 if ok then 626 kernelCopied = true 627 if osprint then 628 osprint("Installer: Kernel copied to /boot/kernel.bin\n") 629 end 630 else 631 if osprint then 632 osprint("Installer: Warning - failed to write kernel: " .. tostring(err) .. "\n") 633 end 634 end 635 else 636 if osprint then 637 osprint("Installer: Warning - kernel data is empty\n") 638 end 639 end 640 else 641 if osprint then 642 osprint("Installer: Warning - kernel not found in ramdisk at /os/boot/kernel.bin\n") 643 osprint("Installer: Run build.sh twice to include kernel in ramdisk\n") 644 end 645 end 646 647 -- Create README with installation status 648 local bootReadme 649 if kernelCopied then 650 bootReadme = [[ 651 LuajitOS Boot Partition 652 ======================= 653 654 This partition was created by the LuajitOS installer. 655 Kernel has been installed successfully. 656 657 To complete the installation, install GRUB from a Linux system: 658 grub-install --boot-directory=/mnt/boot /dev/sdX 659 660 Or boot from this drive using the installed GRUB. 661 ]] 662 else 663 bootReadme = [[ 664 LuajitOS Boot Partition 665 ======================= 666 667 This partition was created by the LuajitOS installer. 668 669 WARNING: Kernel was not copied automatically! 670 671 To complete the installation: 672 1. Run build.sh twice (first builds kernel, second includes it in ramdisk) 673 2. Re-run the installer 674 675 Or manually copy the kernel: 676 mount /dev/sdX1 /mnt 677 cp /path/to/kernel.bin /mnt/boot/kernel.bin 678 679 Then install GRUB: 680 grub-install --boot-directory=/mnt/boot /dev/sdX 681 ]] 682 end 683 684 fat16.writeFile("/README.TXT", bootReadme) 685 686 -- Unmount FAT16 partition 687 fat16.unmount() 688 689 if osprint then 690 osprint("Installer: Boot partition setup complete\n") 691 end 692 693 -- Install bootloader to MBR 694 updateProgress(7, "Installing bootloader...") 695 if grub and grub.install then 696 if osprint then 697 osprint("Installer: Installing bootloader to MBR...\n") 698 end 699 local ok, err = grub.install(drive.bus, drive.drive) 700 if ok then 701 if osprint then 702 osprint("Installer: Bootloader installed successfully\n") 703 end 704 else 705 if osprint then 706 osprint("Installer: Warning - bootloader install failed: " .. tostring(err) .. "\n") 707 osprint("Installer: You may need to run grub-install manually from Linux\n") 708 end 709 end 710 else 711 if osprint then 712 osprint("Installer: Warning - grub module not available\n") 713 osprint("Installer: Run 'grub-install --boot-directory=/mnt/boot /dev/sdX' from Linux\n") 714 end 715 end 716 717 -- Continue with main filesystem format 718 updateProgress(8, "Creating encrypted filesystem...") 719 if osprint then 720 osprint("Installer: Formatting encrypted partition with DiskFS...\n") 721 end 722 723 local ok, err = diskfs.format(drive.bus, drive.drive, "LUAJITOS") 724 if not ok then 725 installError("Format failed: " .. tostring(err)) 726 return 727 end 728 729 updateProgress(9, "Filesystem created...") 730 scheduleTimer(nextTimerName(), DELAY_NORMAL, startCopyPhase) 731 end 732 733 -- Start installation 734 local function startInstall() 735 if state.installing then 736 return 737 end 738 state.installing = true 739 state.progress = 0 740 state.installError = nil 741 state.statusText = "Starting installation..." 742 window:markDirty() 743 installState.drive = state.drives[state.selectedDrive] 744 if osprint then 745 osprint("Installer: Starting to hd" .. (state.selectedDrive - 1) .. "\n") 746 osprint("Installer: Encryption: " .. getEncryptionSummary() .. "\n") 747 end 748 if not diskfs then 749 installError("DiskFS module not available") 750 return 751 end 752 if not Timer then 753 installError("Timer not available") 754 return 755 end 756 if isEncryptionEnabled() and not fde then 757 installError("FDE module not available for encryption") 758 return 759 end 760 scheduleTimer(nextTimerName(), DELAY_NORMAL, startFormatPhase) 761 end 762 763 -- Refresh drives on start 764 refreshDrives() 765 766 -- Draw callback 767 window.onDraw = function(gfx) 768 local width = window:getWidth() 769 local height = window:getHeight() 770 771 -- Background 772 gfx:fillRect(0, 0, width, height, COLOR_BG) 773 774 -- Title 775 gfx:drawText(20, 20, "LuajitOS Installer", COLOR_TITLE) 776 777 if state.step == 1 then 778 -- Step 1: Drive Selection 779 gfx:drawText(20, 60, "Select a drive to install LuajitOS:", COLOR_TEXT) 780 781 local y = 100 782 local itemHeight = 50 783 784 if #state.drives == 0 then 785 gfx:drawText(40, y, "No drives detected.", 0x888888) 786 else 787 for i, drive in ipairs(state.drives) do 788 local isSelected = (state.selectedDrive == i) 789 790 -- Drive item background 791 local bgColor = isSelected and COLOR_DRIVE_SELECTED or COLOR_DRIVE_BG 792 gfx:fillRect(20, y, width - 40, itemHeight - 5, bgColor) 793 gfx:drawRect(20, y, width - 40, itemHeight - 5, COLOR_RADIO_BORDER) 794 795 -- Radio button 796 local radioX = 35 797 local radioY = y + 17 798 local radioRadius = 8 799 800 -- Radio circle (outer) 801 gfx:fillRect(radioX - radioRadius, radioY - radioRadius, radioRadius * 2, radioRadius * 2, COLOR_RADIO_BG) 802 gfx:drawRect(radioX - radioRadius, radioY - radioRadius, radioRadius * 2, radioRadius * 2, COLOR_RADIO_BORDER) 803 804 -- Radio dot (if selected) 805 if isSelected then 806 gfx:fillRect(radioX - 4, radioY - 4, 8, 8, COLOR_RADIO_SELECTED) 807 end 808 809 -- Drive info 810 local driveLabel = "hd" .. (i - 1) .. ": " .. drive.model 811 local driveSize = drive.sizeMB .. " MB" 812 if drive.formatted and drive.volume_name then 813 driveSize = driveSize .. " (" .. drive.volume_name .. ")" 814 elseif drive.formatted then 815 driveSize = driveSize .. " (formatted)" 816 else 817 driveSize = driveSize .. " (unformatted)" 818 end 819 820 gfx:drawText(60, y + 8, driveLabel, COLOR_TEXT) 821 gfx:drawText(60, y + 26, driveSize, 0x666666) 822 823 y = y + itemHeight 824 end 825 end 826 827 -- Next button 828 local buttonWidth = 100 829 local buttonHeight = 35 830 local buttonX = width - buttonWidth - 20 831 local buttonY = height - buttonHeight - 20 832 833 local buttonColor = state.selectedDrive and COLOR_BUTTON or COLOR_BUTTON_DISABLED 834 gfx:fillRect(buttonX, buttonY, buttonWidth, buttonHeight, buttonColor) 835 gfx:drawText(buttonX + 35, buttonY + 10, "Next", COLOR_BUTTON_TEXT) 836 837 elseif state.step == 2 then 838 -- Step 2: Encryption Algorithm Selection (chain builder) 839 gfx:drawText(20, 60, "Configure encryption chain:", COLOR_TEXT) 840 gfx:drawText(20, 80, "(First algorithm is innermost layer)", 0x666666) 841 842 -- Draw encryption chain table 843 local tableY = 110 844 local rowHeight = 30 845 local tableWidth = width - 40 846 847 -- Draw existing algorithms in chain 848 for i, algo in ipairs(state.encryptionChain) do 849 local rowY = tableY + (i - 1) * rowHeight 850 851 -- Row background 852 gfx:fillRect(20, rowY, tableWidth, rowHeight - 2, COLOR_ROW_BG) 853 gfx:drawRect(20, rowY, tableWidth, rowHeight - 2, COLOR_ROW_BORDER) 854 855 -- Index number and algorithm name 856 gfx:drawText(30, rowY + 8, i .. ". " .. algo, COLOR_TEXT) 857 858 -- Button positions (right side of row) 859 local btnY = rowY + 3 860 local btnH = rowHeight - 8 861 local btnW = 50 862 local removeX = width - 20 - btnW - 5 863 local downX = removeX - btnW - 5 864 local upX = downX - btnW - 5 865 866 -- UP button 867 local upColor = (i > 1) and COLOR_SMALL_BTN or COLOR_BUTTON_DISABLED 868 gfx:fillRect(upX, btnY, btnW, btnH, upColor) 869 gfx:drawText(upX + 17, btnY + 4, "UP", COLOR_SMALL_BTN_TEXT) 870 871 -- DOWN button 872 local downColor = (i < #state.encryptionChain) and COLOR_SMALL_BTN or COLOR_BUTTON_DISABLED 873 gfx:fillRect(downX, btnY, btnW, btnH, downColor) 874 gfx:drawText(downX + 10, btnY + 4, "DOWN", COLOR_SMALL_BTN_TEXT) 875 876 -- REMOVE button 877 gfx:fillRect(removeX, btnY, btnW, btnH, 0xCC4444) 878 gfx:drawText(removeX + 8, btnY + 4, "DEL", COLOR_SMALL_BTN_TEXT) 879 end 880 881 -- "Add Algorithm" section 882 local addSectionY = tableY + #state.encryptionChain * rowHeight + 20 883 gfx:drawText(20, addSectionY, "Add algorithm:", COLOR_TEXT) 884 885 -- Draw add buttons for each available algorithm 886 local addBtnY = addSectionY + 25 887 local addBtnHeight = 28 888 local addBtnMargin = 5 889 890 local maxReached = #state.encryptionChain >= 5 891 892 for i, algo in ipairs(AVAILABLE_ALGORITHMS) do 893 local btnWidth = 140 894 895 -- Layout: 2 buttons per row 896 local col = (i - 1) % 2 897 local row = math.floor((i - 1) / 2) 898 local btnX = 20 + col * (btnWidth + addBtnMargin) 899 local btnY = addBtnY + row * (addBtnHeight + addBtnMargin) 900 901 local btnColor = maxReached and COLOR_BUTTON_DISABLED or COLOR_ADD_BTN 902 gfx:fillRect(btnX, btnY, btnWidth, addBtnHeight, btnColor) 903 -- Center text in button (approximate) 904 local textX = btnX + 10 905 gfx:drawText(textX, btnY + 7, algo, COLOR_BUTTON_TEXT) 906 end 907 908 -- Next button 909 local buttonWidth = 100 910 local buttonHeight = 35 911 local buttonX = width - buttonWidth - 20 912 local buttonY = height - buttonHeight - 20 913 914 gfx:fillRect(buttonX, buttonY, buttonWidth, buttonHeight, COLOR_BUTTON) 915 gfx:drawText(buttonX + 35, buttonY + 10, "Next", COLOR_BUTTON_TEXT) 916 917 elseif state.step == 3 then 918 -- Step 3: Password Entry (if encryption enabled) 919 if isEncryptionEnabled() then 920 gfx:drawText(20, 60, "Enter encryption password:", COLOR_TEXT) 921 gfx:drawText(20, 80, "This password will be required to boot.", 0x666666) 922 923 -- Password input field 924 local inputX = 20 925 local inputY = 120 926 local inputWidth = width - 40 927 local inputHeight = 30 928 929 -- First password field 930 gfx:drawText(inputX, inputY - 18, "Password:", COLOR_TEXT) 931 local pw1Color = (not state.passwordInputActive) and 0xDDEEFF or COLOR_ROW_BG 932 gfx:fillRect(inputX, inputY, inputWidth, inputHeight, pw1Color) 933 gfx:drawRect(inputX, inputY, inputWidth, inputHeight, COLOR_RADIO_BORDER) 934 -- Show asterisks for password 935 local pwDisplay = string.rep("*", #state.password) 936 if #pwDisplay == 0 then pwDisplay = "(click to type)" end 937 gfx:drawText(inputX + 10, inputY + 8, pwDisplay, #state.password > 0 and COLOR_TEXT or 0x888888) 938 939 -- Confirm password field 940 local confirmY = inputY + 60 941 gfx:drawText(inputX, confirmY - 18, "Confirm Password:", COLOR_TEXT) 942 local pw2Color = state.passwordInputActive and 0xDDEEFF or COLOR_ROW_BG 943 gfx:fillRect(inputX, confirmY, inputWidth, inputHeight, pw2Color) 944 gfx:drawRect(inputX, confirmY, inputWidth, inputHeight, COLOR_RADIO_BORDER) 945 local pwConfirmDisplay = string.rep("*", #state.passwordConfirm) 946 if #pwConfirmDisplay == 0 then pwConfirmDisplay = "(click to type)" end 947 gfx:drawText(inputX + 10, confirmY + 8, pwConfirmDisplay, #state.passwordConfirm > 0 and COLOR_TEXT or 0x888888) 948 949 -- Password error 950 if state.passwordError then 951 gfx:drawText(inputX, confirmY + 45, state.passwordError, COLOR_ERROR) 952 end 953 954 -- Password requirements 955 gfx:drawText(inputX, confirmY + 70, "Min 8 characters recommended", 0x666666) 956 else 957 gfx:drawText(20, 60, "No encryption selected.", COLOR_TEXT) 958 gfx:drawText(20, 80, "Data will NOT be encrypted.", 0x666666) 959 gfx:drawText(20, 110, "Click Next to continue with installation.", COLOR_TEXT) 960 end 961 962 -- Next button 963 local buttonWidth = 100 964 local buttonHeight = 35 965 local buttonX = width - buttonWidth - 20 966 local buttonY = height - buttonHeight - 20 967 968 -- Can proceed if encryption disabled, or if passwords match and are long enough 969 local canProceed = (not isEncryptionEnabled()) or 970 (#state.password >= 1 and state.password == state.passwordConfirm) 971 local buttonColor = canProceed and COLOR_BUTTON or COLOR_BUTTON_DISABLED 972 gfx:fillRect(buttonX, buttonY, buttonWidth, buttonHeight, buttonColor) 973 gfx:drawText(buttonX + 35, buttonY + 10, "Next", COLOR_BUTTON_TEXT) 974 975 elseif state.step == 4 then 976 -- Step 4: Installation 977 local drive = state.drives[state.selectedDrive] 978 local driveName = drive and ("hd" .. (state.selectedDrive - 1) .. ": " .. drive.model) or "Unknown" 979 980 gfx:drawText(20, 60, "Install LuajitOS to:", COLOR_TEXT) 981 gfx:drawText(20, 80, driveName, COLOR_TITLE) 982 983 -- Show encryption summary 984 gfx:drawText(20, 110, "Encryption: " .. getEncryptionSummary(), 0x666666) 985 986 -- Progress bar 987 local barX = 20 988 local barY = 160 989 local barWidth = width - 40 990 local barHeight = 30 991 992 -- Progress bar background 993 gfx:fillRect(barX, barY, barWidth, barHeight, COLOR_PROGRESS_BG) 994 gfx:drawRect(barX, barY, barWidth, barHeight, COLOR_PROGRESS_BORDER) 995 996 -- Progress bar fill 997 local fillWidth = math.floor((state.progress / 100) * (barWidth - 4)) 998 if fillWidth > 0 then 999 gfx:fillRect(barX + 2, barY + 2, fillWidth, barHeight - 4, COLOR_PROGRESS_FG) 1000 end 1001 1002 -- Progress percentage 1003 local percentText = state.progress .. "%" 1004 gfx:drawText(barX + barWidth / 2 - 15, barY + 8, percentText, COLOR_TEXT) 1005 1006 -- Status text 1007 local statusY = barY + barHeight + 20 1008 if state.installError then 1009 gfx:drawText(20, statusY, "Error: " .. state.installError, COLOR_ERROR) 1010 else 1011 gfx:drawText(20, statusY, state.statusText, COLOR_TEXT) 1012 end 1013 1014 -- Install button (or completion message) 1015 local buttonWidth = 100 1016 local buttonHeight = 35 1017 local buttonX = width - buttonWidth - 20 1018 local buttonY = height - buttonHeight - 20 1019 1020 if state.progress == 100 then 1021 -- Installation complete 1022 gfx:fillRect(buttonX, buttonY, buttonWidth, buttonHeight, COLOR_SUCCESS) 1023 gfx:drawText(buttonX + 30, buttonY + 10, "Done", COLOR_BUTTON_TEXT) 1024 elseif state.installing then 1025 -- Installing - show disabled button 1026 gfx:fillRect(buttonX, buttonY, buttonWidth, buttonHeight, COLOR_BUTTON_DISABLED) 1027 gfx:drawText(buttonX + 15, buttonY + 10, "Installing...", COLOR_BUTTON_TEXT) 1028 elseif state.installError then 1029 -- Error - allow retry 1030 gfx:fillRect(buttonX, buttonY, buttonWidth, buttonHeight, COLOR_ERROR) 1031 gfx:drawText(buttonX + 30, buttonY + 10, "Retry", COLOR_BUTTON_TEXT) 1032 else 1033 -- Ready to install 1034 gfx:fillRect(buttonX, buttonY, buttonWidth, buttonHeight, COLOR_BUTTON) 1035 gfx:drawText(buttonX + 25, buttonY + 10, "Install", COLOR_BUTTON_TEXT) 1036 end 1037 end 1038 end 1039 1040 -- Click handler 1041 window.onClick = function(mx, my) 1042 local width = window:getWidth() 1043 local height = window:getHeight() 1044 1045 if state.step == 1 then 1046 -- Check drive selection 1047 local itemY = 100 1048 local itemHeight = 50 1049 1050 for i, drive in ipairs(state.drives) do 1051 if mx >= 20 and mx < width - 20 and my >= itemY and my < itemY + itemHeight - 5 then 1052 state.selectedDrive = i 1053 if osprint then 1054 osprint("Installer: Selected drive " .. i .. " (hd" .. (i-1) .. ")\n") 1055 end 1056 window:markDirty() 1057 return 1058 end 1059 itemY = itemY + itemHeight 1060 end 1061 1062 -- Check Next button 1063 local buttonWidth = 100 1064 local buttonHeight = 35 1065 local buttonX = width - buttonWidth - 20 1066 local buttonY = height - buttonHeight - 20 1067 1068 if state.selectedDrive and mx >= buttonX and mx < buttonX + buttonWidth and my >= buttonY and my < buttonY + buttonHeight then 1069 if osprint then 1070 local drive = state.drives[state.selectedDrive] 1071 osprint("Installer: Next clicked, selected hd" .. (state.selectedDrive - 1) .. "\n") 1072 osprint("Installer: Drive: " .. drive.model .. " (" .. drive.sizeMB .. " MB)\n") 1073 end 1074 state.step = 2 1075 window:markDirty() 1076 end 1077 1078 elseif state.step == 2 then 1079 -- Step 2: Encryption Algorithm Selection (chain builder) 1080 local tableY = 110 1081 local rowHeight = 30 1082 local btnW = 50 1083 local btnH = rowHeight - 8 1084 1085 -- Check clicks on existing algorithm rows (UP, DOWN, REMOVE buttons) 1086 for i, algo in ipairs(state.encryptionChain) do 1087 local rowY = tableY + (i - 1) * rowHeight 1088 local btnY = rowY + 3 1089 1090 local removeX = width - 20 - btnW - 5 1091 local downX = removeX - btnW - 5 1092 local upX = downX - btnW - 5 1093 1094 -- Check UP button 1095 if mx >= upX and mx < upX + btnW and my >= btnY and my < btnY + btnH then 1096 if i > 1 then 1097 moveUp(i) 1098 if osprint then 1099 osprint("Installer: Moved " .. algo .. " up\n") 1100 end 1101 window:markDirty() 1102 end 1103 return 1104 end 1105 1106 -- Check DOWN button 1107 if mx >= downX and mx < downX + btnW and my >= btnY and my < btnY + btnH then 1108 if i < #state.encryptionChain then 1109 moveDown(i) 1110 if osprint then 1111 osprint("Installer: Moved " .. algo .. " down\n") 1112 end 1113 window:markDirty() 1114 end 1115 return 1116 end 1117 1118 -- Check REMOVE button 1119 if mx >= removeX and mx < removeX + btnW and my >= btnY and my < btnY + btnH then 1120 removeAlgorithm(i) 1121 if osprint then 1122 osprint("Installer: Removed " .. algo .. "\n") 1123 end 1124 window:markDirty() 1125 return 1126 end 1127 end 1128 1129 -- Check clicks on "Add Algorithm" buttons 1130 local addSectionY = tableY + #state.encryptionChain * rowHeight + 20 1131 local addBtnY = addSectionY + 25 1132 local addBtnHeight = 28 1133 local addBtnMargin = 5 1134 local addBtnWidth = 140 1135 1136 for i, algo in ipairs(AVAILABLE_ALGORITHMS) do 1137 local col = (i - 1) % 2 1138 local row = math.floor((i - 1) / 2) 1139 local btnX = 20 + col * (addBtnWidth + addBtnMargin) 1140 local btnY = addBtnY + row * (addBtnHeight + addBtnMargin) 1141 1142 if mx >= btnX and mx < btnX + addBtnWidth and my >= btnY and my < btnY + addBtnHeight then 1143 if addAlgorithm(algo) then 1144 if osprint then 1145 osprint("Installer: Added " .. algo .. " to chain\n") 1146 end 1147 window:markDirty() 1148 end 1149 return 1150 end 1151 end 1152 1153 -- Check Next button 1154 local buttonWidth = 100 1155 local buttonHeight = 35 1156 local buttonX = width - buttonWidth - 20 1157 local buttonY = height - buttonHeight - 20 1158 1159 if mx >= buttonX and mx < buttonX + buttonWidth and my >= buttonY and my < buttonY + buttonHeight then 1160 if osprint then 1161 osprint("Installer: Encryption chain configured:\n") 1162 for i, algo in ipairs(state.encryptionChain) do 1163 osprint(" " .. i .. ". " .. algo .. "\n") 1164 end 1165 end 1166 state.step = 3 1167 window:markDirty() 1168 end 1169 1170 elseif state.step == 3 then 1171 -- Step 3: Password Entry 1172 local inputX = 20 1173 local inputY = 120 1174 local inputWidth = width - 40 1175 local inputHeight = 30 1176 local confirmY = inputY + 60 1177 1178 -- Check password field click 1179 if mx >= inputX and mx < inputX + inputWidth and my >= inputY and my < inputY + inputHeight then 1180 state.passwordInputActive = false -- First field 1181 window:markDirty() 1182 return 1183 end 1184 1185 -- Check confirm field click 1186 if mx >= inputX and mx < inputX + inputWidth and my >= confirmY and my < confirmY + inputHeight then 1187 state.passwordInputActive = true -- Second field 1188 window:markDirty() 1189 return 1190 end 1191 1192 -- Check Next button 1193 local buttonWidth = 100 1194 local buttonHeight = 35 1195 local buttonX = width - buttonWidth - 20 1196 local buttonY = height - buttonHeight - 20 1197 1198 if mx >= buttonX and mx < buttonX + buttonWidth and my >= buttonY and my < buttonY + buttonHeight then 1199 -- Validate passwords if encryption enabled 1200 if isEncryptionEnabled() then 1201 if #state.password < 1 then 1202 state.passwordError = "Password cannot be empty" 1203 window:markDirty() 1204 return 1205 end 1206 if state.password ~= state.passwordConfirm then 1207 state.passwordError = "Passwords do not match" 1208 window:markDirty() 1209 return 1210 end 1211 state.passwordError = nil 1212 end 1213 if osprint then 1214 osprint("Installer: Password set, proceeding to install\n") 1215 end 1216 state.step = 4 1217 window:markDirty() 1218 end 1219 1220 elseif state.step == 4 then 1221 -- Step 4: Installation 1222 local buttonWidth = 100 1223 local buttonHeight = 35 1224 local buttonX = width - buttonWidth - 20 1225 local buttonY = height - buttonHeight - 20 1226 1227 if mx >= buttonX and mx < buttonX + buttonWidth and my >= buttonY and my < buttonY + buttonHeight then 1228 if state.progress == 100 then 1229 -- Done - close window 1230 if osprint then 1231 osprint("Installer: Closing after successful installation\n") 1232 end 1233 app:terminate() 1234 elseif not state.installing then 1235 -- Start or retry installation 1236 if osprint then 1237 osprint("Installer: Starting installation...\n") 1238 end 1239 startInstall() 1240 end 1241 end 1242 end 1243 end 1244 1245 -- Keyboard handler for password input (uses onInput, not onKey) 1246 window.onInput = function(key, scancode) 1247 if state.step == 3 and isEncryptionEnabled() then 1248 -- Scancode 14 = backspace, 15 = tab, 28 = enter 1249 if scancode == 14 then 1250 -- Backspace 1251 if state.passwordInputActive then 1252 if #state.passwordConfirm > 0 then 1253 state.passwordConfirm = state.passwordConfirm:sub(1, -2) 1254 end 1255 else 1256 if #state.password > 0 then 1257 state.password = state.password:sub(1, -2) 1258 end 1259 end 1260 state.passwordError = nil 1261 window:markDirty() 1262 elseif scancode == 15 then 1263 -- Tab to switch fields 1264 state.passwordInputActive = not state.passwordInputActive 1265 window:markDirty() 1266 elseif scancode == 28 then 1267 -- Enter to proceed (if valid) 1268 if #state.password >= 1 and state.password == state.passwordConfirm then 1269 state.step = 4 1270 window:markDirty() 1271 elseif state.password ~= state.passwordConfirm then 1272 state.passwordError = "Passwords do not match" 1273 window:markDirty() 1274 end 1275 elseif key and #key == 1 and key:byte() >= 32 and key:byte() < 127 then 1276 -- Printable character 1277 if state.passwordInputActive then 1278 state.passwordConfirm = state.passwordConfirm .. key 1279 else 1280 state.password = state.password .. key 1281 end 1282 state.passwordError = nil 1283 window:markDirty() 1284 end 1285 end 1286 end 1287 1288 if osprint then 1289 osprint("Installer: Window created\n") 1290 end