luajitos

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

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