Sys.lua (41579B)
1 #!/usr/bin/env luajit 2 -- System Management Library 3 -- Handles running applications, active window, and input routing 4 5 local sys = {} 6 7 -- Initialize global hook instance (Hook should be loaded as _G.Hook in init.lua) 8 if _G.Hook then 9 sys.hook = _G.Hook.newInstance() 10 else 11 osprint("Warning: Hook library not available, sys.hook not initialized\n") 12 end 13 14 -- Running applications table 15 sys.applications = {} 16 17 -- Sandbox environments table (indexed by PID) 18 sys.environments = {} 19 20 -- Prompt mode state (for admin password prompts, etc.) 21 sys.promptMode = { 22 active = false, 23 window = nil, -- The only window allowed to draw/receive events 24 pid = nil -- PID of the app that owns the prompt window 25 } 26 27 -- Active window for keyboard input 28 sys.activeWindow = nil 29 30 -- Application registry 31 sys.appRegistry = {} 32 33 -- Hotkey registry 34 -- Format: hotkeys["scancode_modifiers_event"] = function 35 -- Example: hotkeys["59_0_down"] = function() ... end (F1 key down, no modifiers) 36 -- Modifiers: 1=shift, 2=ctrl, 4=alt (can be combined) 37 -- Events: "down", "up", "press" 38 sys.hotkeys = {} 39 40 -- Screen API 41 sys.screen = {} 42 sys.screen[1] = { 43 width = DEFAULT_SCREEN_WIDTH or 1200, -- Get from C constants 44 height = DEFAULT_SCREEN_HEIGHT or 900, 45 setResolution = function(self, w, h) 46 -- Check if caller has display-settings permission 47 local caller = debug.getinfo(2, "f").func 48 local hasPermission = false 49 50 -- Find the calling app's environment 51 for pid, env in pairs(sys.environments) do 52 if env._permissions and env._permissions["display-settings"] then 53 -- Check if this function was called from that environment 54 hasPermission = true 55 break 56 end 57 end 58 59 if not hasPermission then 60 error("Permission denied: 'display-settings' permission required to change screen resolution") 61 end 62 63 -- Validate resolution 64 if type(w) ~= "number" or type(h) ~= "number" then 65 error("setResolution requires numeric width and height") 66 end 67 68 if w <= 0 or h <= 0 then 69 error("Resolution must be positive") 70 end 71 72 -- Call VESA to set the mode 73 if VESASetMode then 74 local result = VESASetMode(w, h, 32) 75 if result == 0 then 76 self.width = w 77 self.height = h 78 79 -- Trigger ScreenResolutionChanged hook 80 if sys.hook then 81 sys.hook:run("ScreenResolutionChanged", self) 82 end 83 84 return true 85 else 86 error("Failed to set resolution " .. w .. "x" .. h) 87 end 88 else 89 error("VESASetMode not available") 90 end 91 end 92 } 93 94 -- Modifier key state 95 local modifierState = { 96 shift = false, 97 ctrl = false, 98 alt = false, 99 meta = false -- Windows/Meta key 100 } 101 102 -- Modifier constants 103 sys.MOD_NONE = 0 104 sys.MOD_SHIFT = 1 105 sys.MOD_CTRL = 2 106 sys.MOD_ALT = 4 107 sys.MOD_META = 8 108 sys.MOD_SHIFT_CTRL = 3 109 sys.MOD_SHIFT_ALT = 5 110 sys.MOD_CTRL_ALT = 6 111 sys.MOD_SHIFT_META = 9 112 sys.MOD_CTRL_META = 10 113 sys.MOD_ALT_META = 12 114 sys.MOD_ALL = 15 115 116 -- Event constants 117 sys.EVENT_DOWN = "down" 118 sys.EVENT_UP = "up" 119 sys.EVENT_PRESS = "press" 120 121 -- Common key scancodes 122 sys.KEY_ESC = 1 123 sys.KEY_1 = 2 124 sys.KEY_2 = 3 125 sys.KEY_3 = 4 126 sys.KEY_4 = 5 127 sys.KEY_5 = 6 128 sys.KEY_6 = 7 129 sys.KEY_7 = 8 130 sys.KEY_8 = 9 131 sys.KEY_9 = 10 132 sys.KEY_0 = 11 133 sys.KEY_MINUS = 12 134 sys.KEY_EQUALS = 13 135 sys.KEY_BACKSPACE = 14 136 sys.KEY_TAB = 15 137 sys.KEY_Q = 16 138 sys.KEY_W = 17 139 sys.KEY_E = 18 140 sys.KEY_R = 19 141 sys.KEY_T = 20 142 sys.KEY_Y = 21 143 sys.KEY_U = 22 144 sys.KEY_I = 23 145 sys.KEY_O = 24 146 sys.KEY_P = 25 147 sys.KEY_LBRACKET = 26 148 sys.KEY_RBRACKET = 27 149 sys.KEY_ENTER = 28 150 sys.KEY_LCTRL = 29 151 sys.KEY_A = 30 152 sys.KEY_S = 31 153 sys.KEY_D = 32 154 sys.KEY_F = 33 155 sys.KEY_G = 34 156 sys.KEY_H = 35 157 sys.KEY_J = 36 158 sys.KEY_K = 37 159 sys.KEY_L = 38 160 sys.KEY_SEMICOLON = 39 161 sys.KEY_QUOTE = 40 162 sys.KEY_BACKTICK = 41 163 sys.KEY_LSHIFT = 42 164 sys.KEY_BACKSLASH = 43 165 sys.KEY_Z = 44 166 sys.KEY_X = 45 167 sys.KEY_C = 46 168 sys.KEY_V = 47 169 sys.KEY_B = 48 170 sys.KEY_N = 49 171 sys.KEY_M = 50 172 sys.KEY_COMMA = 51 173 sys.KEY_PERIOD = 52 174 sys.KEY_SLASH = 53 175 sys.KEY_RSHIFT = 54 176 sys.KEY_KPASTERISK = 55 177 sys.KEY_LALT = 56 178 sys.KEY_SPACE = 57 179 sys.KEY_CAPSLOCK = 58 180 sys.KEY_F1 = 59 181 sys.KEY_F2 = 60 182 sys.KEY_F3 = 61 183 sys.KEY_F4 = 62 184 sys.KEY_F5 = 63 185 sys.KEY_F6 = 64 186 sys.KEY_F7 = 65 187 sys.KEY_F8 = 66 188 sys.KEY_F9 = 67 189 sys.KEY_F10 = 68 190 sys.KEY_NUMLOCK = 69 191 sys.KEY_SCROLLLOCK = 70 192 sys.KEY_F11 = 87 193 sys.KEY_F12 = 88 194 sys.KEY_LMETA = 91 -- Left Windows/Meta key (0x5B) 195 sys.KEY_RMETA = 92 -- Right Windows/Meta key (0x5C) 196 197 ---Convert scancode and modifiers to hotkey string format 198 ---@param scancode number The scancode 199 ---@param modifiers number The modifier bitmask 200 ---@return string|nil The hotkey string in format "ctrl+alt+meta+shift+key" or nil if unknown 201 function sys.scancodeToHotkeyString(scancode, modifiers) 202 -- Build modifier prefix in canonical order: ctrl+alt+meta+shift 203 local parts = {} 204 205 if bit.band(modifiers, sys.MOD_CTRL) ~= 0 then 206 table.insert(parts, "ctrl") 207 end 208 if bit.band(modifiers, sys.MOD_ALT) ~= 0 then 209 table.insert(parts, "alt") 210 end 211 if bit.band(modifiers, sys.MOD_META) ~= 0 then 212 table.insert(parts, "meta") 213 end 214 if bit.band(modifiers, sys.MOD_SHIFT) ~= 0 then 215 table.insert(parts, "shift") 216 end 217 218 -- Map scancode to key name 219 local keyName = nil 220 221 -- Letters 222 if scancode == 16 then keyName = "q" 223 elseif scancode == 17 then keyName = "w" 224 elseif scancode == 18 then keyName = "e" 225 elseif scancode == 19 then keyName = "r" 226 elseif scancode == 20 then keyName = "t" 227 elseif scancode == 21 then keyName = "y" 228 elseif scancode == 22 then keyName = "u" 229 elseif scancode == 23 then keyName = "i" 230 elseif scancode == 24 then keyName = "o" 231 elseif scancode == 25 then keyName = "p" 232 elseif scancode == 30 then keyName = "a" 233 elseif scancode == 31 then keyName = "s" 234 elseif scancode == 32 then keyName = "d" 235 elseif scancode == 33 then keyName = "f" 236 elseif scancode == 34 then keyName = "g" 237 elseif scancode == 35 then keyName = "h" 238 elseif scancode == 36 then keyName = "j" 239 elseif scancode == 37 then keyName = "k" 240 elseif scancode == 38 then keyName = "l" 241 elseif scancode == 44 then keyName = "z" 242 elseif scancode == 45 then keyName = "x" 243 elseif scancode == 46 then keyName = "c" 244 elseif scancode == 47 then keyName = "v" 245 elseif scancode == 48 then keyName = "b" 246 elseif scancode == 49 then keyName = "n" 247 elseif scancode == 50 then keyName = "m" 248 -- Numbers 249 elseif scancode == 2 then keyName = "1" 250 elseif scancode == 3 then keyName = "2" 251 elseif scancode == 4 then keyName = "3" 252 elseif scancode == 5 then keyName = "4" 253 elseif scancode == 6 then keyName = "5" 254 elseif scancode == 7 then keyName = "6" 255 elseif scancode == 8 then keyName = "7" 256 elseif scancode == 9 then keyName = "8" 257 elseif scancode == 10 then keyName = "9" 258 elseif scancode == 11 then keyName = "0" 259 -- Special keys 260 elseif scancode == 1 then keyName = "esc" 261 elseif scancode == 12 then keyName = "minus" 262 elseif scancode == 13 then keyName = "equals" 263 elseif scancode == 14 then keyName = "backspace" 264 elseif scancode == 15 then keyName = "tab" 265 elseif scancode == 28 then keyName = "enter" 266 elseif scancode == 57 then keyName = "space" 267 -- Function keys 268 elseif scancode == 59 then keyName = "f1" 269 elseif scancode == 60 then keyName = "f2" 270 elseif scancode == 61 then keyName = "f3" 271 elseif scancode == 62 then keyName = "f4" 272 elseif scancode == 63 then keyName = "f5" 273 elseif scancode == 64 then keyName = "f6" 274 elseif scancode == 65 then keyName = "f7" 275 elseif scancode == 66 then keyName = "f8" 276 elseif scancode == 67 then keyName = "f9" 277 elseif scancode == 68 then keyName = "f10" 278 elseif scancode == 87 then keyName = "f11" 279 elseif scancode == 88 then keyName = "f12" 280 end 281 282 if not keyName then 283 return nil -- Unknown scancode 284 end 285 286 table.insert(parts, keyName) 287 return table.concat(parts, "+") 288 end 289 290 ---Add a hotkey handler 291 ---@param scancode number The key scancode (use sys.KEY_* constants) 292 ---@param modifiers number Modifier flags (use sys.MOD_* constants) 293 ---@param event string Event type (use sys.EVENT_* constants, default "down") 294 ---@param handler function Handler function that receives (scancode, modifiers, event) 295 ---@return string The hotkey ID for later removal 296 function sys.addHotkey(scancode, modifiers, event, handler) 297 -- Handle optional event parameter 298 if type(event) == "function" then 299 handler = event 300 event = "down" 301 end 302 303 modifiers = modifiers or 0 304 event = event or "down" 305 306 local hotkeyId = scancode .. "_" .. modifiers .. "_" .. event 307 sys.hotkeys[hotkeyId] = handler 308 309 return hotkeyId 310 end 311 312 ---Remove a hotkey handler 313 ---@param hotkeyId string The hotkey ID returned by addHotkey, or scancode/modifiers/event 314 ---@param modifiers number Optional: modifier flags if first param is scancode 315 ---@param event string Optional: event type if first param is scancode 316 function sys.removeHotkey(hotkeyId, modifiers, event) 317 -- Support both formats: removeHotkey(id) or removeHotkey(scancode, modifiers, event) 318 if type(hotkeyId) == "number" then 319 -- Construct ID from components 320 modifiers = modifiers or 0 321 event = event or "down" 322 hotkeyId = hotkeyId .. "_" .. modifiers .. "_" .. event 323 end 324 325 sys.hotkeys[hotkeyId] = nil 326 end 327 328 ---Add a hotkey using string notation (e.g., "ctrl+alt+r", "meta+r") 329 ---@param keyCombo string Key combination like "ctrl+r", "ctrl+alt+shift+f", "meta+r" 330 ---@param handler function Handler function 331 ---@param event string Optional event type (default "down") 332 ---@return string The hotkey ID for later removal 333 function sys.addHotkeyString(keyCombo, handler, event) 334 event = event or "down" 335 336 -- Parse the key combination 337 local parts = {} 338 for part in string.gmatch(keyCombo, "[^+]+") do 339 table.insert(parts, string.lower(part)) 340 end 341 342 -- Build modifiers 343 local modifiers = 0 344 local keyName = nil 345 346 for _, part in ipairs(parts) do 347 if part == "ctrl" or part == "control" then 348 modifiers = bit.bor(modifiers, sys.MOD_CTRL) 349 elseif part == "alt" then 350 modifiers = bit.bor(modifiers, sys.MOD_ALT) 351 elseif part == "shift" then 352 modifiers = bit.bor(modifiers, sys.MOD_SHIFT) 353 elseif part == "meta" or part == "win" or part == "super" then 354 modifiers = bit.bor(modifiers, sys.MOD_META) 355 else 356 keyName = part 357 end 358 end 359 360 if not keyName then 361 error("No key specified in key combination: " .. keyCombo) 362 end 363 364 -- Map key name to scancode 365 local scancode = nil 366 367 -- Check for special key names first 368 local keyUpper = string.upper(keyName) 369 if keyUpper == "PLUS" then scancode = 13 -- + key 370 elseif keyUpper == "MINUS" then scancode = 12 -- - key 371 elseif keyUpper == "EQUALS" then scancode = 13 -- = key (same as +) 372 elseif keyUpper == "SPACE" then scancode = 57 -- Space 373 elseif keyUpper == "ENTER" or keyUpper == "RETURN" then scancode = 28 -- Enter 374 elseif keyUpper == "BACKSPACE" then scancode = 14 -- Backspace 375 elseif keyUpper == "TAB" then scancode = 15 -- Tab 376 elseif keyUpper == "ESC" or keyUpper == "ESCAPE" then scancode = 1 -- Escape 377 elseif #keyName == 1 then 378 -- Single character - map to scancode 379 local char = string.upper(keyName) 380 -- Letters 381 if char == "Q" then scancode = 16 382 elseif char == "W" then scancode = 17 383 elseif char == "E" then scancode = 18 384 elseif char == "R" then scancode = 19 385 elseif char == "T" then scancode = 20 386 elseif char == "Y" then scancode = 21 387 elseif char == "U" then scancode = 22 388 elseif char == "I" then scancode = 23 389 elseif char == "O" then scancode = 24 390 elseif char == "P" then scancode = 25 391 elseif char == "A" then scancode = 30 392 elseif char == "S" then scancode = 31 393 elseif char == "D" then scancode = 32 394 elseif char == "F" then scancode = 33 395 elseif char == "G" then scancode = 34 396 elseif char == "H" then scancode = 35 397 elseif char == "J" then scancode = 36 398 elseif char == "K" then scancode = 37 399 elseif char == "L" then scancode = 38 400 elseif char == "Z" then scancode = 44 401 elseif char == "X" then scancode = 45 402 elseif char == "C" then scancode = 46 403 elseif char == "V" then scancode = 47 404 elseif char == "B" then scancode = 48 405 elseif char == "N" then scancode = 49 406 elseif char == "M" then scancode = 50 407 -- Numbers 408 elseif char == "1" then scancode = 2 409 elseif char == "2" then scancode = 3 410 elseif char == "3" then scancode = 4 411 elseif char == "4" then scancode = 5 412 elseif char == "5" then scancode = 6 413 elseif char == "6" then scancode = 7 414 elseif char == "7" then scancode = 8 415 elseif char == "8" then scancode = 9 416 elseif char == "9" then scancode = 10 417 elseif char == "0" then scancode = 11 418 -- Special characters (based on US keyboard layout) 419 elseif char == "!" then scancode = 2 -- Shift+1 420 elseif char == "@" then scancode = 3 -- Shift+2 421 elseif char == "#" then scancode = 4 -- Shift+3 422 elseif char == "$" then scancode = 5 -- Shift+4 423 elseif char == "%" then scancode = 6 -- Shift+5 424 elseif char == "^" then scancode = 7 -- Shift+6 425 elseif char == "&" then scancode = 8 -- Shift+7 426 elseif char == "*" then scancode = 9 -- Shift+8 427 elseif char == "(" then scancode = 10 -- Shift+9 428 elseif char == ")" then scancode = 11 -- Shift+0 429 elseif char == "-" then scancode = 12 -- Minus 430 elseif char == "_" then scancode = 12 -- Shift+Minus 431 elseif char == "=" then scancode = 13 -- Equals 432 elseif char == "+" then scancode = 13 -- Shift+Equals 433 elseif char == "[" then scancode = 26 -- Left bracket 434 elseif char == "{" then scancode = 26 -- Shift+Left bracket 435 elseif char == "]" then scancode = 27 -- Right bracket 436 elseif char == "}" then scancode = 27 -- Shift+Right bracket 437 elseif char == ";" then scancode = 39 -- Semicolon 438 elseif char == ":" then scancode = 39 -- Shift+Semicolon 439 elseif char == "'" then scancode = 40 -- Quote 440 elseif char == '"' then scancode = 40 -- Shift+Quote 441 elseif char == "\\" then scancode = 43 -- Backslash 442 elseif char == "|" then scancode = 43 -- Shift+Backslash 443 elseif char == "," then scancode = 51 -- Comma 444 elseif char == "<" then scancode = 51 -- Shift+Comma 445 elseif char == "." then scancode = 52 -- Period 446 elseif char == ">" then scancode = 52 -- Shift+Period 447 elseif char == "/" then scancode = 53 -- Slash 448 elseif char == "?" then scancode = 53 -- Shift+Slash 449 elseif char == "`" then scancode = 41 -- Backtick 450 elseif char == "~" then scancode = 41 -- Shift+Backtick 451 elseif char == "£" then scancode = 4 -- UK keyboard Shift+3 452 else 453 error("Unsupported key: " .. keyName) 454 end 455 else 456 error("Multi-character key names not yet supported: " .. keyName) 457 end 458 459 return sys.addHotkey(scancode, modifiers, event, handler) 460 end 461 462 ---Register a running application 463 ---@param appInstance table Application instance 464 function sys.registerApplication(appInstance) 465 if not appInstance or not appInstance.pid then 466 return false 467 end 468 469 sys.applications[appInstance.pid] = appInstance 470 471 return true 472 end 473 474 ---Register a sandbox environment for an application 475 ---@param pid number Process ID 476 ---@param environment table Sandbox environment 477 function sys.registerEnvironment(pid, environment) 478 if not pid or not environment then 479 return false 480 end 481 482 sys.environments[pid] = environment 483 484 return true 485 end 486 487 ---Get sandbox environment for an application 488 ---@param pid number Process ID 489 ---@return table|nil Sandbox environment 490 function sys.getEnvironment(pid) 491 return sys.environments[pid] 492 end 493 494 ---Unregister a running application 495 ---@param pid number Process ID 496 function sys.unregisterApplication(pid) 497 sys.applications[pid] = nil 498 sys.environments[pid] = nil 499 500 -- Clear active window if it belonged to this app 501 if sys.activeWindow and sys.activeWindow.appInstance and sys.activeWindow.appInstance.pid == pid then 502 sys.activeWindow = nil 503 end 504 end 505 506 ---Get application by PID 507 ---@param pid number Process ID 508 ---@return table|nil Application instance 509 function sys.getApplication(pid) 510 return sys.applications[pid] 511 end 512 513 ---Get all running applications 514 ---@return table List of application instances 515 function sys.getAllApplications() 516 local apps = {} 517 for _, app in pairs(sys.applications) do 518 table.insert(apps, app) 519 end 520 return apps 521 end 522 523 ---Set the active window for keyboard input 524 ---@param window table Window object 525 function sys.setActiveWindow(window) 526 sys.activeWindow = window 527 end 528 529 ---Get the active window 530 ---@return table|nil Active window 531 function sys.getActiveWindow() 532 return sys.activeWindow 533 end 534 535 ---Find and set the first available window as active 536 ---@return boolean True if a window was found and set 537 function sys.findAndSetActiveWindow() 538 -- Iterate through all running applications 539 -- Find the window with onInput that has the highest createdAt (topmost) 540 local foundWindow = nil 541 local highestTime = -1 542 for pid, app in pairs(sys.applications) do 543 if app.windows then 544 for i, window in ipairs(app.windows) do 545 if window.onInput and not window.isBackground then 546 local windowTime = window.createdAt or 0 547 if windowTime > highestTime then 548 highestTime = windowTime 549 foundWindow = window 550 end 551 end 552 end 553 end 554 end 555 if foundWindow then 556 sys.setActiveWindow(foundWindow) 557 return true 558 end 559 return false 560 end 561 562 ---Send keyboard input to the active window 563 ---@param key string The key character 564 ---@param scancode number The scancode 565 ---@return boolean True if input was sent successfully 566 function sys.sendInput(key, scancode) 567 -- Track modifier key state 568 -- Scancodes: 42=LShift, 54=RShift, 29=LCtrl, 56=LAlt, 91=LMeta, 92=RMeta 569 local isKeyDown = scancode < 128 -- High bit not set = key down 570 local baseScancode = scancode % 128 -- Remove high bit for key up events 571 572 -- Debug: Print the baseScancode for all keys 573 574 if baseScancode == 42 or baseScancode == 54 then 575 modifierState.shift = isKeyDown 576 elseif baseScancode == 29 then 577 modifierState.ctrl = isKeyDown 578 elseif baseScancode == 56 then 579 modifierState.alt = isKeyDown 580 elseif baseScancode == 91 or baseScancode == 92 then 581 modifierState.meta = isKeyDown 582 end 583 584 -- Calculate modifier bitmask 585 local modifiers = 0 586 if modifierState.shift then modifiers = modifiers + 1 end 587 if modifierState.ctrl then modifiers = modifiers + 2 end 588 if modifierState.alt then modifiers = modifiers + 4 end 589 if modifierState.meta then modifiers = modifiers + 8 end 590 591 592 -- Determine event type 593 local event = isKeyDown and "down" or "up" 594 595 -- Check for hotkey match (down events) 596 if isKeyDown then 597 local hotkeyDown = baseScancode .. "_" .. modifiers .. "_down" 598 if sys.hotkeys[hotkeyDown] then 599 local success, shouldCancel = pcall(sys.hotkeys[hotkeyDown]) 600 if success and shouldCancel == true then 601 -- Hotkey handled and requested cancellation 602 return true 603 end 604 end 605 606 -- Also check the new hotkey manager 607 if sys.hotkey then 608 local hotkeyStr = sys.scancodeToHotkeyString(baseScancode, modifiers) 609 if hotkeyStr then 610 local hasHotkey = sys.hotkey.has(hotkeyStr) 611 if hasHotkey then 612 local success, result = pcall(sys.hotkey.trigger, hotkeyStr) 613 if success then 614 -- Hotkey was triggered successfully, don't send input to window 615 return true 616 end 617 end 618 end 619 end 620 end 621 622 -- Check for hotkey match (up events) 623 if not isKeyDown then 624 local hotkeyUp = baseScancode .. "_" .. modifiers .. "_up" 625 if sys.hotkeys[hotkeyUp] then 626 local success, shouldCancel = pcall(sys.hotkeys[hotkeyUp]) 627 if success and shouldCancel == true then 628 -- Hotkey handled and requested cancellation 629 return true 630 end 631 end 632 end 633 634 -- Check for press event (any key with modifiers) 635 local hotkeyPress = baseScancode .. "_" .. modifiers .. "_press" 636 if sys.hotkeys[hotkeyPress] then 637 local success, shouldCancel = pcall(sys.hotkeys[hotkeyPress]) 638 if success and shouldCancel == true then 639 -- Hotkey handled and requested cancellation 640 return true 641 end 642 end 643 644 -- Don't send modifier key presses/releases to windows 645 -- Modifier scancodes: 42,54=Shift, 29=Ctrl, 56=Alt, 91,92=Meta 646 local isModifierKey = (baseScancode == 42 or baseScancode == 54 or 647 baseScancode == 29 or baseScancode == 56 or 648 baseScancode == 91 or baseScancode == 92) 649 650 if isModifierKey then 651 -- Modifier keys don't get sent to windows, only tracked for hotkeys 652 return false 653 end 654 655 -- Don't send key release events to windows (only key down) 656 if not isKeyDown then 657 return false 658 end 659 660 -- If no active window, try to find one 661 if not sys.activeWindow then 662 if not sys.findAndSetActiveWindow() then 663 -- No window available to receive input 664 return false 665 end 666 end 667 668 -- Check if there's an active selection and a printable key was pressed 669 local win = sys.activeWindow 670 local hasSelection = win.selection and win.selection.start and win.selection.finish and 671 win.selection.content and win.selection.content ~= "" 672 673 -- Check if this is a printable character or special editing key (backspace, delete, enter) 674 local isPrintable = key and #key == 1 and key:byte() >= 32 675 local isBackspace = key == "\b" 676 local isEnter = key == "\n" 677 local isDelete = baseScancode == 83 678 679 if hasSelection and (isPrintable or isBackspace or isEnter or isDelete) then 680 -- Selection is active and an editing key was pressed 681 if win.onSelectionEditted then 682 local newContent = "" 683 if isPrintable then 684 newContent = key 685 elseif isEnter then 686 newContent = "\n" 687 end 688 -- Backspace and Delete replace selection with empty string 689 690 -- Convert selection indices to x,y coordinates 691 local startText = win.selectableText and win.selectableText[win.selection.start.index] 692 local finishText = win.selectableText and win.selectableText[win.selection.finish.index] 693 694 local point1, point2 695 if startText then 696 local charWidth = 8 * (startText.scale or 1) 697 point1 = { 698 x = startText.x + (win.selection.start.pos - 1) * charWidth, 699 y = startText.y 700 } 701 end 702 if finishText then 703 local charWidth = 8 * (finishText.scale or 1) 704 point2 = { 705 x = finishText.x + (win.selection.finish.pos - 1) * charWidth, 706 y = finishText.y 707 } 708 end 709 710 if not point1 or not point2 then 711 -- Fall back to passing the raw selection if we can't get coordinates 712 point1 = win.selection.start 713 point2 = win.selection.finish 714 end 715 716 local success, err = pcall(function() 717 win.onSelectionEditted(point1, point2, newContent) 718 end) 719 720 if not success and osprint then 721 osprint("ERROR: onSelectionEditted callback failed: " .. tostring(err) .. "\n") 722 end 723 724 -- Clear selection after edit 725 win.selection = nil 726 win.dirty = true 727 return true 728 end 729 end 730 731 -- Call the window's input callback if it exists 732 if win.onInput then 733 local success, err = pcall(function() 734 win.onInput(key, scancode) 735 end) 736 737 if not success and osprint then 738 osprint("ERROR: Input callback failed: " .. tostring(err) .. "\n") 739 return false 740 end 741 742 return true 743 else 744 return false 745 end 746 end 747 748 ---Register an application in the app registry 749 ---@param appName string Application name 750 ---@param appPath string Path to application 751 function sys.registerAppInRegistry(appName, appPath) 752 sys.appRegistry[appName] = appPath 753 end 754 755 ---Get application path from registry 756 ---@param appName string Application name 757 ---@return string|nil Application path 758 function sys.getAppPath(appName) 759 return sys.appRegistry[appName] 760 end 761 762 ---Add a permission to a running application (admin only) 763 ---@param pid number Process ID 764 ---@param permission string Permission to add 765 ---@return boolean True if successful 766 function sys.ADMIN_AppAddPermission(pid, permission) 767 local app = sys.applications[pid] 768 if not app then 769 error("Application with PID " .. pid .. " not found") 770 end 771 772 if not app.permissions then 773 app.permissions = {} 774 end 775 776 -- Check if permission already exists 777 for _, perm in ipairs(app.permissions) do 778 if perm == permission then 779 -- Already has this permission 780 return true 781 end 782 end 783 784 -- Add the permission 785 table.insert(app.permissions, permission) 786 787 788 -- Re-apply permissions to the sandbox environment if it exists 789 local env = sys.environments[pid] 790 if env and env._reapplyPermissions then 791 env._reapplyPermissions() 792 end 793 794 return true 795 end 796 797 ---Add an allowed path to a running application (admin only) 798 ---@param pid number Process ID 799 ---@param path string Path pattern to add (e.g., "$/data/*", "/tmp/*") 800 ---@return boolean True if successful 801 function sys.ADMIN_AppAddPath(pid, path) 802 local app = sys.applications[pid] 803 if not app then 804 error("Application with PID " .. pid .. " not found") 805 end 806 807 if not app.allowedPaths then 808 app.allowedPaths = {} 809 end 810 811 -- Check if path already exists 812 for _, p in ipairs(app.allowedPaths) do 813 if p == path then 814 -- Already has this path 815 return true 816 end 817 end 818 819 -- Add the path 820 table.insert(app.allowedPaths, path) 821 822 823 -- Update the SafeFS instance if it exists in the environment 824 local env = sys.environments[pid] 825 if env and env.fs and env.fs._updateAllowedPaths then 826 env.fs._updateAllowedPaths(app.allowedPaths) 827 end 828 829 return true 830 end 831 832 ---Start exclusive prompt mode (admin only) 833 ---Only the specified window will be drawn and receive events 834 ---All other apps are frozen until ADMIN_FinishPrompt is called 835 ---@param window table The window to display exclusively 836 ---@return boolean True if successful 837 function sys.ADMIN_StartPrompt(window) 838 if not window then 839 error("ADMIN_StartPrompt: window parameter required") 840 end 841 842 -- Verify window has an appInstance with PID 843 if not window.appInstance or not window.appInstance.pid then 844 error("ADMIN_StartPrompt: window must have valid appInstance with PID") 845 end 846 847 sys.promptMode.active = true 848 sys.promptMode.window = window 849 sys.promptMode.pid = window.appInstance.pid 850 851 -- Force the prompt window to be visible and on top 852 window.visible = true 853 window.alwaysOnTop = true 854 window.dirty = true 855 856 857 return true 858 end 859 860 ---Finish exclusive prompt mode (admin only) 861 ---Re-enables drawing and events for all applications 862 ---@return boolean True if successful 863 function sys.ADMIN_FinishPrompt() 864 if not sys.promptMode.active then 865 return false 866 end 867 868 local prev_pid = sys.promptMode.pid 869 870 sys.promptMode.active = false 871 sys.promptMode.window = nil 872 sys.promptMode.pid = nil 873 874 875 -- Mark all windows as dirty to force redraw 876 for pid, app in pairs(sys.applications) do 877 if app.windows then 878 for _, window in ipairs(app.windows) do 879 window.dirty = true 880 end 881 end 882 end 883 884 return true 885 end 886 887 ---Internal function to add allowed path (used by Dialog system only) 888 ---This is not exposed to sandboxed apps 889 ---@param app table Application instance 890 ---@param path string Path pattern to add 891 ---@param fs table SafeFS instance to update 892 ---@return boolean True if successful 893 function sys._internal_addAllowedPath(app, path, fs) 894 if not app then 895 return false, "Invalid app instance" 896 end 897 898 if not app.allowedPaths then 899 app.allowedPaths = {} 900 end 901 902 -- Check if path already exists 903 for _, p in ipairs(app.allowedPaths) do 904 if p == path then 905 -- Already has this path 906 return true 907 end 908 end 909 910 -- Add the path 911 table.insert(app.allowedPaths, path) 912 913 914 -- Update the SafeFS instance by adding the new path to its allowed roots 915 if fs then 916 -- Load SafeFS module to access the new() function 917 local SafeFS = require or package.loaded.SafeFS 918 if not SafeFS and _G.SafeFS then 919 SafeFS = _G.SafeFS 920 end 921 922 if SafeFS and fs.rootNode and fs.allowedRoots then 923 -- Get the filesystem root 924 local fsRoot = fs.rootNode 925 926 -- Resolve $ in path 927 local app_dir = app.appPath or "" 928 local resolved_path = string.gsub(path, "^%$", app_dir) 929 930 -- Remove /* suffix if present 931 local cleanPath = resolved_path 932 if cleanPath:sub(-2) == "/*" then 933 cleanPath = cleanPath:sub(1, -3) 934 end 935 936 -- Find node in original filesystem using traverse or manual traversal 937 local function findNode(rootNode, targetPath) 938 -- Check if rootNode has a traverse function (C-based filesystem) 939 if type(rootNode) == "table" and type(rootNode.traverse) == "function" then 940 local node, err = rootNode:traverse(targetPath) 941 return node, err 942 end 943 return nil, "Cannot traverse filesystem" 944 end 945 946 local node, err = findNode(fsRoot, cleanPath) 947 if node and node.type == "directory" then 948 -- Deep copy the node (simplified version) 949 local function deepCopyNode(srcNode, parentNode) 950 if not srcNode then return nil end 951 952 local copy = { 953 name = srcNode.name, 954 type = srcNode.type, 955 parent = parentNode 956 } 957 958 if srcNode.type == "directory" then 959 copy.dirs = {} 960 copy.files = {} 961 962 if srcNode.dirs then 963 for _, dir in ipairs(srcNode.dirs) do 964 local dirCopy = deepCopyNode(dir, copy) 965 if dirCopy then 966 table.insert(copy.dirs, dirCopy) 967 end 968 end 969 end 970 971 if srcNode.files then 972 for _, file in ipairs(srcNode.files) do 973 local fileCopy = { 974 name = file.name, 975 type = "file", 976 parent = copy, 977 content = file.content 978 } 979 table.insert(copy.files, fileCopy) 980 end 981 end 982 elseif srcNode.type == "file" then 983 copy.content = srcNode.content 984 end 985 986 return copy 987 end 988 989 local isolatedNode = deepCopyNode(node, nil) 990 if isolatedNode then 991 isolatedNode.name = cleanPath 992 fs.roots[cleanPath] = isolatedNode 993 table.insert(fs.allowedRoots, cleanPath) 994 return true 995 else 996 return false, "Failed to copy directory node" 997 end 998 else 999 return false, "Directory not found: " .. cleanPath 1000 end 1001 end 1002 end 1003 1004 return true 1005 end 1006 1007 ---Open a file using the registered file handler for its extension 1008 ---If the handler app is not running, it will be launched 1009 ---@param filepath string The full path to the file to open 1010 ---@return boolean success True if handler was found and called 1011 ---@return string|nil error Error message if failed 1012 function sys.openFile(filepath) 1013 if not filepath or type(filepath) ~= "string" then 1014 return false, "Invalid filepath" 1015 end 1016 1017 -- Extract extension from filepath 1018 local extension = filepath:match("%.([^%.]+)$") 1019 if not extension then 1020 return false, "No file extension found" 1021 end 1022 1023 extension = extension:lower() 1024 1025 -- Get the SafeFS module to access the file handler registry 1026 -- SafeFS should be loaded globally or we need to load it 1027 local SafeFS = _G.SafeFS 1028 if not SafeFS then 1029 -- Try to load SafeFS module 1030 local safefs_path = "/os/libs/SafeFS.lua" 1031 if CRamdiskOpen and CRamdiskRead and CRamdiskClose then 1032 local handle = CRamdiskOpen(safefs_path, "r") 1033 if handle then 1034 local code = CRamdiskRead(handle) 1035 CRamdiskClose(handle) 1036 if code then 1037 local safefs_func, err = loadstring(code, safefs_path) 1038 if safefs_func then 1039 local success, module = pcall(safefs_func) 1040 if success then 1041 SafeFS = module 1042 _G.SafeFS = module -- Cache it globally 1043 end 1044 end 1045 end 1046 end 1047 end 1048 end 1049 1050 if not SafeFS or not SafeFS.getFileHandler then 1051 return false, "SafeFS module not available" 1052 end 1053 1054 -- Look up the handler for this extension 1055 local handler = SafeFS.getFileHandler(extension) 1056 if not handler then 1057 return false, "No handler registered for ." .. extension 1058 end 1059 1060 local appId = handler.appId 1061 local functionName = handler.functionName 1062 1063 if osprint then 1064 osprint("[sys.openFile] Opening " .. filepath .. " with " .. appId .. ":" .. functionName .. "\n") 1065 end 1066 1067 -- Find the app in running applications 1068 local targetApp = nil 1069 local targetEnv = nil 1070 1071 for pid, app in pairs(sys.applications) do 1072 -- Check if app path contains the app ID 1073 if app.appPath and app.appPath:find(appId, 1, true) then 1074 targetApp = app 1075 targetEnv = sys.environments[pid] 1076 break 1077 end 1078 end 1079 1080 -- If app is not running, launch it 1081 if not targetApp then 1082 if osprint then 1083 osprint("[sys.openFile] App " .. appId .. " not running, launching...\n") 1084 end 1085 1086 -- Use the run module to launch the app 1087 if _G.run and _G.run.execute then 1088 local success, app = _G.run.execute(appId, _G.fsRoot) 1089 if success and app then 1090 targetApp = app 1091 targetEnv = sys.environments[app.pid] 1092 else 1093 return false, "Failed to launch app: " .. appId 1094 end 1095 else 1096 return false, "Run module not available to launch app" 1097 end 1098 end 1099 1100 -- Now call the registered function on the app's environment 1101 if targetEnv then 1102 local func = targetEnv[functionName] 1103 if func and type(func) == "function" then 1104 local success, err = pcall(func, filepath) 1105 if not success then 1106 return false, "Handler function error: " .. tostring(err) 1107 end 1108 return true 1109 else 1110 return false, "Function '" .. functionName .. "' not found or not a function in app " .. appId 1111 end 1112 else 1113 return false, "Could not get environment for app " .. appId 1114 end 1115 end 1116 1117 -- Browser interface for HTML windows 1118 -- Loaded on demand when first accessed 1119 sys.browser = { 1120 _htmlWindowModule = nil, 1121 1122 -- Load HTMLWindow module on demand 1123 _loadModule = function(self) 1124 if self._htmlWindowModule then 1125 return self._htmlWindowModule 1126 end 1127 1128 local htmlWindowPath = "/os/libs/HTMLWindow.luac" 1129 local htmlWindowCode = nil 1130 1131 if CRamdiskExists and CRamdiskExists(htmlWindowPath) then 1132 local handle = CRamdiskOpen(htmlWindowPath, "r") 1133 if handle then 1134 htmlWindowCode = CRamdiskRead(handle) 1135 CRamdiskClose(handle) 1136 end 1137 end 1138 1139 if not htmlWindowCode then 1140 htmlWindowPath = "/os/libs/HTMLWindow.lua" 1141 if CRamdiskExists and CRamdiskExists(htmlWindowPath) then 1142 local handle = CRamdiskOpen(htmlWindowPath, "r") 1143 if handle then 1144 htmlWindowCode = CRamdiskRead(handle) 1145 CRamdiskClose(handle) 1146 end 1147 end 1148 end 1149 1150 if not htmlWindowCode then 1151 return nil, "HTMLWindow module not found" 1152 end 1153 1154 local env = setmetatable({}, { __index = _G, __metatable = false }) 1155 local func, err = loadstring(htmlWindowCode, "HTMLWindow") 1156 if not func then 1157 return nil, "Failed to compile HTMLWindow: " .. tostring(err) 1158 end 1159 setfenv(func, env) 1160 local ok, module = pcall(func) 1161 if not ok then 1162 return nil, "Failed to execute HTMLWindow: " .. tostring(module) 1163 end 1164 1165 self._htmlWindowModule = module 1166 return module 1167 end, 1168 1169 --- Create a new HTML window for an app 1170 -- @param options Table with: 1171 -- - app: Application instance (required) 1172 -- - html: HTML string to render (either html or file required) 1173 -- - file: Path to HTML file (either html or file required) 1174 -- - fs: SafeFS instance for file access 1175 -- - width: Window width (default 800) 1176 -- - height: Window height (default 600) 1177 -- - title: Window title 1178 -- - Dialog: Dialog module for alert/confirm/prompt 1179 -- - loadstring: loadstring function for onclick handlers 1180 -- - showStatusBar: Show status bar (default false) 1181 -- - onSubmit: Callback for form submissions 1182 -- @return HTMLWindow object or nil, error 1183 newHTMLWindow = function(self, options) 1184 local module, err = self:_loadModule() 1185 if not module then 1186 return nil, err 1187 end 1188 return module.new(options) 1189 end, 1190 } 1191 1192 -- Cursor API (sys.cursor.add, sys.cursor.remove, etc.) 1193 sys.cursor = { 1194 -- Add a global cursor drawing function to the stack 1195 -- @param draw_func function(state, gfx) - Called with cursor state and SafeGfx for 30x30 cursor buffer 1196 -- state: "cursor", "left-click", "right-click", "grab", "loading", "denied" 1197 -- gfx: SafeGfx with clear(), fillRect(), drawRect(), drawPixel(), drawLine(), getWidth(), getHeight() 1198 -- Use color 0x00FF00 (green) for transparent pixels 1199 -- @return number Index in the cursor stack for removal 1200 add = function(draw_func) 1201 if _G._cursor_api and _G._cursor_api.add then 1202 return _G._cursor_api.add(draw_func) 1203 end 1204 error("Cursor API not available") 1205 end, 1206 1207 -- Remove a global cursor drawing function from stack 1208 -- @param index number The index returned by add() 1209 -- @return boolean True if removed successfully 1210 remove = function(index) 1211 if _G._cursor_api and _G._cursor_api.remove then 1212 return _G._cursor_api.remove(index) 1213 end 1214 return false 1215 end, 1216 1217 -- Pop the top global cursor from the stack 1218 -- @return boolean True if a cursor was popped 1219 pop = function() 1220 if _G._cursor_api and _G._cursor_api.pop then 1221 return _G._cursor_api.pop() 1222 end 1223 return false 1224 end, 1225 1226 -- Set the current cursor mode (triggers redraw) 1227 -- @param mode string One of: "cursor", "left-click", "right-click", "grab", "loading", "denied" 1228 setMode = function(mode) 1229 if _G._cursor_api and _G._cursor_api.setMode then 1230 _G._cursor_api.setMode(mode) 1231 end 1232 end, 1233 1234 -- Get the current cursor mode 1235 -- @return string Current cursor mode 1236 getMode = function() 1237 if _G.cursor_state then 1238 return _G.cursor_state.mode or "cursor" 1239 end 1240 return "cursor" 1241 end 1242 } 1243 1244 return sys