luajitos

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

Application.lua (32214B)


      1 -- Application.lua - Application instance management with function exports
      2 -- Manages application lifecycle, process information, and exported functions
      3 
      4 local Application = {}
      5 Application.__index = Application
      6 Application.__metatable = false  -- Prevent metatable access/modification
      7 
      8 -- Track used PIDs to avoid collisions
      9 if not _G._used_pids then
     10     _G._used_pids = {}
     11 end
     12 
     13 -- Helper: Generate a random PID that hasn't been used
     14 local function generatePid()
     15     local max_attempts = 1000
     16     for i = 1, max_attempts do
     17         -- Generate random PID between 1000 and 65535
     18         local pid = math.random(1000, 65535)
     19 
     20         -- Check if this PID is already in use
     21         if not _G._used_pids[pid] then
     22             _G._used_pids[pid] = true
     23             return pid
     24         end
     25     end
     26 
     27     -- Fallback: if we somehow can't find a free PID after 1000 attempts,
     28     -- just use a sequential counter
     29     if not _G._fallback_pid then
     30         _G._fallback_pid = 1000
     31     end
     32 
     33     while _G._used_pids[_G._fallback_pid] do
     34         _G._fallback_pid = _G._fallback_pid + 1
     35     end
     36 
     37     local pid = _G._fallback_pid
     38     _G._used_pids[pid] = true
     39     _G._fallback_pid = _G._fallback_pid + 1
     40     return pid
     41 end
     42 
     43 -- Helper: Free a PID when application terminates
     44 local function freePid(pid)
     45     if pid and _G._used_pids[pid] then
     46         _G._used_pids[pid] = nil
     47     end
     48 end
     49 
     50 -- Helper: Get current timestamp (simplified for bare metal)
     51 local function getTimestamp()
     52     -- In a real implementation, this would query a hardware timer
     53     -- For now, we'll use a simple counter
     54     if not _G._app_timestamp then
     55         _G._app_timestamp = 0
     56     end
     57     _G._app_timestamp = _G._app_timestamp + 1
     58     return _G._app_timestamp
     59 end
     60 
     61 -- Helper: Parse parameter string like "arg1=number,arg2=string,..." or "string,number,..."
     62 local function parseParameters(paramStr)
     63     if not paramStr or paramStr == "" then
     64         return {}
     65     end
     66 
     67     local params = {}
     68     local argIndex = 1
     69 
     70     for param in paramStr:gmatch("[^,]+") do
     71         -- Trim whitespace
     72         param = param:match("^%s*(.-)%s*$")
     73 
     74         -- Parse name=type format
     75         local name, paramType = param:match("^([%w_]+)=([%w_]+)$")
     76         if name and paramType then
     77             table.insert(params, {
     78                 name = name,
     79                 type = paramType
     80             })
     81         else
     82             -- No equals sign - treat whole string as type, generate arg name
     83             table.insert(params, {
     84                 name = "arg" .. argIndex,
     85                 type = param
     86             })
     87             argIndex = argIndex + 1
     88         end
     89     end
     90 
     91     return params
     92 end
     93 
     94 -- Helper: Validate export definition
     95 local function validateExport(exportDef)
     96     -- Required fields
     97     if type(exportDef) ~= "table" then
     98         return false, "Export definition must be a table"
     99     end
    100 
    101     if type(exportDef.name) ~= "string" or exportDef.name == "" then
    102         return false, "Export must have a non-empty 'name' field"
    103     end
    104 
    105     if type(exportDef.func) ~= "function" then
    106         return false, "Export must have a 'func' field that is a function"
    107     end
    108 
    109     -- Optional fields validation
    110     if exportDef.args and type(exportDef.args) ~= "string" then
    111         return false, "Export 'args' field must be a string"
    112     end
    113 
    114     if exportDef.rets and type(exportDef.rets) ~= "string" then
    115         return false, "Export 'rets' field must be a string"
    116     end
    117 
    118     if exportDef.description and type(exportDef.description) ~= "string" then
    119         return false, "Export 'description' field must be a string"
    120     end
    121 
    122     return true
    123 end
    124 
    125 --- Create a new Application instance
    126 -- @param appPath: Path to the application (e.g., "/apps/com.example.myapp")
    127 -- @param options: Optional table with configuration
    128 --   - pid: Override auto-generated process ID
    129 --   - name: Application name (defaults to last component of appPath)
    130 -- @return Application instance
    131 function Application.new(appPath, options)
    132     options = options or {}
    133 
    134     if type(appPath) ~= "string" or appPath == "" then
    135         error("Application.new requires a valid appPath string")
    136     end
    137 
    138     -- Extract app name from path if not provided
    139     local appName = options.name
    140     if not appName then
    141         appName = appPath:match("([^/]+)$") or "unknown"
    142     end
    143 
    144     local self = setmetatable({}, Application)
    145 
    146     -- Process information
    147     self.pid = options.pid or generatePid()
    148 
    149     self.appPath = appPath
    150     self.appName = appName
    151     self.startTime = getTimestamp()
    152 
    153     -- Export registry
    154     self.exports = {}
    155 
    156     -- Application state
    157     self.status = "initialized"  -- initialized, running, paused, stopped
    158 
    159     -- Output capture
    160     self.stdout = ""  -- Captured standard output
    161     self.stderr = ""  -- Captured standard error (for future use)
    162 
    163     -- Window management
    164     self.windows = {}  -- Array of windows created by this app
    165 
    166     if osprint then
    167         osprint("Application created: " .. appName .. " (PID: " .. self.pid .. ", Path: " .. appPath .. ")\n")
    168     end
    169 
    170     return self
    171 end
    172 
    173 --- Export a function from this application
    174 -- @param exportDef: Table containing export definition
    175 --   - name: Function name (required)
    176 --   - func: The function to export (required)
    177 --   - args: Input parameter specification (optional, e.g., "arg1=number,arg2=string")
    178 --   - rets: Output type specification (optional, e.g., "number" or "result=table")
    179 --   - description: Human-readable description (optional)
    180 -- @return true on success, or (nil, error_message) on failure
    181 function Application:export(exportDef)
    182     -- Validate the export definition
    183     local valid, err = validateExport(exportDef)
    184     if not valid then
    185         if osprint then
    186             osprint("ERROR: Invalid export definition: " .. err .. "\n")
    187         end
    188         return nil, err
    189     end
    190 
    191     -- Check for duplicate exports
    192     if self.exports[exportDef.name] then
    193         local err = "Function '" .. exportDef.name .. "' is already exported"
    194         if osprint then
    195             osprint("WARNING: " .. err .. "\n")
    196         end
    197         return nil, err
    198     end
    199 
    200     -- Parse input and output specifications
    201     local inputParams = parseParameters(exportDef.args or "")
    202     local outputParams = parseParameters(exportDef.rets or "")
    203 
    204     -- Store the export with metadata
    205     self.exports[exportDef.name] = {
    206         name = exportDef.name,
    207         func = exportDef.func,
    208         input = inputParams,
    209         output = outputParams,
    210         description = exportDef.description or "",
    211         exportedAt = getTimestamp()
    212     }
    213 
    214     if osprint then
    215         osprint("Exported function: " .. exportDef.name .. " from " .. self.appName .. "\n")
    216     end
    217 
    218     return true
    219 end
    220 
    221 --- Call an exported function
    222 -- @param functionName: Name of the exported function
    223 -- @param ...: Arguments to pass to the function
    224 -- @return Function result, or (nil, error_message) on failure
    225 function Application:call(functionName, ...)
    226     local exportDef = self.exports[functionName]
    227     if not exportDef then
    228         return nil, "Function '" .. functionName .. "' is not exported"
    229     end
    230 
    231     -- Call the function with provided arguments
    232     local success, result = pcall(exportDef.func, ...)
    233     if not success then
    234         return nil, "Function call failed: " .. tostring(result)
    235     end
    236 
    237     return result
    238 end
    239 
    240 --- Get information about an exported function
    241 -- @param functionName: Name of the exported function
    242 -- @return Export metadata table, or nil if not found
    243 function Application:getExport(functionName)
    244     return self.exports[functionName]
    245 end
    246 
    247 --- List all exported functions
    248 -- @return Array of export names
    249 function Application:listExports()
    250     local names = {}
    251     for name, _ in pairs(self.exports) do
    252         table.insert(names, name)
    253     end
    254     table.sort(names)
    255     return names
    256 end
    257 
    258 --- Get application status information
    259 -- @return Table with application metadata
    260 function Application:getInfo()
    261     return {
    262         pid = self.pid,
    263         name = self.appName,
    264         path = self.appPath,
    265         status = self.status,
    266         startTime = self.startTime,
    267         uptime = getTimestamp() - self.startTime,
    268         exportCount = self:getExportCount()
    269     }
    270 end
    271 
    272 --- Get count of exported functions
    273 -- @return Number of exported functions
    274 function Application:getExportCount()
    275     local count = 0
    276     for _ in pairs(self.exports) do
    277         count = count + 1
    278     end
    279     return count
    280 end
    281 
    282 --- Update application status
    283 -- @param newStatus: New status string (running, paused, stopped, etc.)
    284 function Application:setStatus(newStatus, reason)
    285     self.status = newStatus
    286     self.stopReason = reason
    287     if osprint then
    288         local msg = "Application " .. self.appName .. " status changed to: " .. newStatus
    289         if reason then
    290             msg = msg .. " (reason: " .. reason .. ")"
    291         end
    292         osprint(msg .. "\n")
    293     end
    294 end
    295 
    296 --- Generate a help message for an exported function
    297 -- @param functionName: Name of the exported function
    298 -- @return Help string, or nil if not found
    299 function Application:getHelp(functionName)
    300     local exportDef = self.exports[functionName]
    301     if not exportDef then
    302         return nil
    303     end
    304 
    305     local help = {}
    306     table.insert(help, "Function: " .. exportDef.name)
    307 
    308     if exportDef.description and exportDef.description ~= "" then
    309         table.insert(help, "Description: " .. exportDef.description)
    310     end
    311 
    312     if #exportDef.input > 0 then
    313         local inputs = {}
    314         for _, param in ipairs(exportDef.input) do
    315             table.insert(inputs, param.name .. ": " .. param.type)
    316         end
    317         table.insert(help, "Input: " .. table.concat(inputs, ", "))
    318     else
    319         table.insert(help, "Input: (none)")
    320     end
    321 
    322     if #exportDef.output > 0 then
    323         local outputs = {}
    324         for _, param in ipairs(exportDef.output) do
    325             table.insert(outputs, param.type)
    326         end
    327         table.insert(help, "Output: " .. table.concat(outputs, ", "))
    328     else
    329         table.insert(help, "Output: (unspecified)")
    330     end
    331 
    332     return table.concat(help, "\n")
    333 end
    334 
    335 --- Print all exported functions with their signatures
    336 function Application:printExports()
    337     local exportNames = self:listExports()
    338 
    339     if #exportNames == 0 then
    340         if osprint then
    341             osprint("No exported functions\n")
    342         end
    343         return
    344     end
    345 
    346     if osprint then
    347         osprint("Exported functions from " .. self.appName .. ":\n")
    348         osprint(string.rep("=", 50) .. "\n")
    349 
    350         for _, name in ipairs(exportNames) do
    351             local help = self:getHelp(name)
    352             if help then
    353                 osprint(help .. "\n")
    354                 osprint(string.rep("-", 50) .. "\n")
    355             end
    356         end
    357     end
    358 end
    359 
    360 --- Write to stdout (append text to stdout buffer)
    361 -- @param text: Text to append to stdout
    362 function Application:writeStdout(text)
    363     if type(text) ~= "string" then
    364         text = tostring(text)
    365     end
    366     self.stdout = self.stdout .. text
    367 end
    368 
    369 --- Get stdout content
    370 -- @return Current stdout buffer content
    371 function Application:getStdout()
    372     return self.stdout
    373 end
    374 
    375 --- Clear stdout buffer
    376 function Application:clearStdout()
    377     self.stdout = ""
    378 end
    379 
    380 --- Terminate the application and free its PID
    381 function Application:terminate()
    382     self.status = "terminated"
    383     freePid(self.pid)
    384 
    385     if osprint then
    386         osprint("Application terminated: " .. self.appName .. " (PID: " .. self.pid .. ")\n")
    387     end
    388 end
    389 
    390 --- Get process information
    391 -- @return Table with pid, appName, appPath, startTime, status
    392 function Application:getProcessInfo()
    393     return {
    394         pid = self.pid,
    395         appName = self.appName,
    396         appPath = self.appPath,
    397         startTime = self.startTime,
    398         status = self.status
    399     }
    400 end
    401 
    402 --- Create a new window for this application
    403 -- Supported overloads:
    404 -- @param x, y, width, height, resizable: Position and size
    405 -- @param title, x, y, width, height, resizable: Title with position and size
    406 -- @param title, width, height, resizable: Title with centered position
    407 -- @param width, height, resizable: Centered position
    408 -- @return Window instance
    409 function Application:newWindow(arg1, arg2, arg3, arg4, arg5, arg6)
    410     local title = nil
    411     local x, y, width, height, resizable
    412 
    413     -- Handle overload: newWindow(title, x, y, width, height, resizable) - title with position
    414     if type(arg1) == "string" and type(arg2) == "number" and type(arg3) == "number" and type(arg4) == "number" and type(arg5) == "number" then
    415         title = arg1
    416         x = arg2
    417         y = arg3
    418         width = arg4
    419         height = arg5
    420         resizable = arg6
    421     -- Handle overload: newWindow(title, width, height, resizable) - center on screen with title
    422     elseif type(arg1) == "string" and type(arg2) == "number" and type(arg3) == "number" then
    423         title = arg1
    424         width = arg2
    425         height = arg3
    426         resizable = arg4
    427         -- Center on screen (assume 1024x768 for now, should get from gfx API)
    428         x = math.floor((1024 - width) / 2)
    429         y = math.floor((768 - height) / 2)
    430     -- Handle overload: newWindow(x, y, width, height, resizable) - position and size
    431     elseif type(arg1) == "number" and type(arg2) == "number" and type(arg3) == "number" and type(arg4) == "number" then
    432         x = arg1
    433         y = arg2
    434         width = arg3
    435         height = arg4
    436         resizable = arg5
    437     -- Handle overload: newWindow(width, height, resizable) - center on screen
    438     elseif type(arg1) == "number" and type(arg2) == "number" then
    439         width = arg1
    440         height = arg2
    441         resizable = arg3
    442         -- Center on screen (assume 1024x768 for now, should get from gfx API)
    443         x = math.floor((1024 - width) / 2)
    444         y = math.floor((768 - height) / 2)
    445     end
    446 
    447     -- Validate parameters
    448     if type(x) ~= "number" or type(y) ~= "number" or type(width) ~= "number" or type(height) ~= "number" then
    449         error("newWindow requires (x, y, width, height), (width, height), or (title, width, height)")
    450     end
    451 
    452     if width <= 0 or height <= 0 then
    453         error("Window width and height must be positive")
    454     end
    455 
    456     -- Default resizable to false if not provided
    457     if resizable == nil then
    458         resizable = false
    459     end
    460 
    461     -- Create window instance
    462     local window = {
    463         x = x,
    464         y = y,
    465         width = width,
    466         height = height,
    467         title = title,  -- Window title (optional)
    468         resizable = resizable,  -- Whether window can be resized
    469         visible = true,
    470         app = self,
    471         appInstance = self,  -- Reference for icon drawing in title bar
    472         drawCallback = nil,  -- User-defined draw function
    473         inputCallback = nil,  -- User-defined input handler
    474         buffer = nil,  -- Per-window pixel buffer (allocated on first draw)
    475         dirty = true,  -- Needs redraw
    476         createdAt = getTimestamp(),  -- Track creation order for Z-ordering
    477         -- Minimize/maximize state
    478         minimized = false,
    479         maximized = false,
    480         -- Store pre-maximize position/size for restore
    481         restoreX = nil,
    482         restoreY = nil,
    483         restoreWidth = nil,
    484         restoreHeight = nil,
    485         -- Text selection support
    486         selectable = true,  -- Whether text selection is enabled for this window
    487         selectableText = {},  -- Array of {text, x, y, w, h, color, scale}
    488         selection = nil  -- {start={index,pos}, finish={index,pos}, content, type}
    489     }
    490 
    491     -- Window cursor API (window.cursor.add, window.cursor.remove, etc.)
    492     window.cursor = {
    493         -- Add a cursor drawing function for this window (overrides global when window is active)
    494         -- @param draw_func function(state, gfx) - Called with cursor state and SafeGfx
    495         -- @return number Index for removal
    496         add = function(draw_func)
    497             if _G._cursor_api and _G._cursor_api.windowAdd then
    498                 return _G._cursor_api.windowAdd(window, draw_func)
    499             end
    500             error("Cursor API not available")
    501         end,
    502 
    503         -- Remove a cursor drawing function from this window's stack
    504         -- @param index number The index returned by add()
    505         -- @return boolean True if removed
    506         remove = function(index)
    507             if _G._cursor_api and _G._cursor_api.windowRemove then
    508                 return _G._cursor_api.windowRemove(window, index)
    509             end
    510             return false
    511         end,
    512 
    513         -- Pop the top cursor from this window's stack
    514         -- @return boolean True if popped
    515         pop = function()
    516             if _G._cursor_api and _G._cursor_api.windowPop then
    517                 return _G._cursor_api.windowPop(window)
    518             end
    519             return false
    520         end,
    521 
    522         -- Clear all cursors from this window's stack
    523         clear = function()
    524             if _G._cursor_api and _G._cursor_api.windowClear then
    525                 _G._cursor_api.windowClear(window)
    526             end
    527         end
    528     }
    529 
    530     -- Window draw method (will be called by LPM drawScreen)
    531     function window:draw(safeGfx)
    532         -- Clear selectable text array before each draw
    533         self.selectableText = {}
    534         if self.onDraw and self.visible then
    535             self.onDraw(safeGfx)
    536         end
    537     end
    538 
    539     -- Window visibility control
    540     function window:show()
    541         self.visible = true
    542         self.dirty = true  -- Mark for redraw when shown
    543     end
    544 
    545     function window:hide()
    546         self.visible = false
    547     end
    548 
    549     -- Center window on screen
    550     function window:centerScreen()
    551         self.x = math.floor((1024 - self.width) / 2)
    552         self.y = math.floor((768 - self.height) / 2)
    553         self.dirty = true
    554     end
    555 
    556     -- Close window (with optional cancel via onClose callback)
    557     function window:close()
    558         -- Call onClose callback if it exists
    559         if self.onClose then
    560             local success, shouldCancel = pcall(self.onClose)
    561             if success and shouldCancel == true then
    562                 -- onClose returned true, cancel the close
    563                 return false
    564             end
    565         end
    566 
    567         -- Free window buffer to release memory
    568         if self.buffer and VESAFreeWindowBuffer then
    569             VESAFreeWindowBuffer(self.buffer)
    570             self.buffer = nil
    571         end
    572 
    573         -- Hide the window
    574         self:hide()
    575 
    576         -- Fire WindowClosed hook
    577         if _G.sys and _G.sys.hook and _G.sys.hook.run then
    578             _G.sys.hook:run("WindowClosed", self)
    579         end
    580 
    581         return true
    582     end
    583 
    584     -- Minimize window
    585     function window:minimize()
    586         if self.minimized then return end
    587         self.minimized = true
    588         self.visible = false
    589         self.dirty = true
    590 
    591         -- Fire WindowMinimized hook
    592         if _G.sys and _G.sys.hook and _G.sys.hook.run then
    593             _G.sys.hook:run("WindowMinimized", self)
    594         end
    595     end
    596 
    597     -- Restore from minimized state
    598     function window:restore()
    599         if not self.minimized then return end
    600         self.minimized = false
    601         self.visible = true
    602         self.dirty = true
    603 
    604         -- Bring to front by updating timestamp
    605         self.createdAt = (_G.getTimestamp and _G.getTimestamp()) or (os.time() * 1000)
    606 
    607         -- Fire WindowRestored hook
    608         if _G.sys and _G.sys.hook and _G.sys.hook.run then
    609             _G.sys.hook:run("WindowRestored", self)
    610         end
    611     end
    612 
    613     -- Maximize/restore window
    614     function window:maximize()
    615         local TITLE_BAR_HEIGHT = self.TITLE_BAR_HEIGHT or 20
    616         local BORDER_WIDTH = self.BORDER_WIDTH or 2
    617         local TASKBAR_HEIGHT = 32  -- Account for taskbar at bottom
    618 
    619         -- Get screen dimensions from sys.screens
    620         local screenW = 1024
    621         local screenH = 768
    622         if _G.sys and _G.sys.screens and _G.sys.screens[1] then
    623             screenW = _G.sys.screens[1].width or screenW
    624             screenH = _G.sys.screens[1].height or screenH
    625         end
    626 
    627         if self.maximized then
    628             -- Restore to previous size/position
    629             if self.restoreX then
    630                 self.x = self.restoreX
    631                 self.y = self.restoreY
    632                 self.width = self.restoreWidth
    633                 self.height = self.restoreHeight
    634             end
    635             self.maximized = false
    636         else
    637             -- Save current position/size
    638             self.restoreX = self.x
    639             self.restoreY = self.y
    640             self.restoreWidth = self.width
    641             self.restoreHeight = self.height
    642 
    643             -- Maximize to fill screen (accounting for title bar and taskbar)
    644             self.x = BORDER_WIDTH
    645             self.y = BORDER_WIDTH + TITLE_BAR_HEIGHT
    646             self.width = screenW - (BORDER_WIDTH * 2)
    647             self.height = screenH - TASKBAR_HEIGHT - TITLE_BAR_HEIGHT - (BORDER_WIDTH * 2)
    648             self.maximized = true
    649         end
    650 
    651         -- Free old buffer and clear to force recreation with new size
    652         if self.buffer and VESAFreeWindowBuffer then
    653             VESAFreeWindowBuffer(self.buffer)
    654         end
    655         self.buffer = nil
    656         self.dirty = true
    657 
    658         if _G.osprint then
    659             _G.osprint(string.format("[MAXIMIZE] Window now %dx%d, buffer cleared, maximized=%s\n",
    660                 self.width, self.height, tostring(self.maximized)))
    661         end
    662 
    663         -- Call onResize callback if it exists
    664         if self.onResize then
    665             local oldWidth = self.restoreWidth or self.width
    666             local oldHeight = self.restoreHeight or self.height
    667             if self.maximized then
    668                 -- We just maximized, old size is restore size
    669                 pcall(self.onResize, self.width, self.height, self.restoreWidth, self.restoreHeight)
    670             else
    671                 -- We just restored, old size was the maximized size (screen size)
    672                 local screenW = 1024
    673                 local screenH = 768
    674                 if _G.sys and _G.sys.screens and _G.sys.screens[1] then
    675                     screenW = _G.sys.screens[1].width or screenW
    676                     screenH = _G.sys.screens[1].height or screenH
    677                 end
    678                 local maxW = screenW - (BORDER_WIDTH * 2)
    679                 local maxH = screenH - TASKBAR_HEIGHT - TITLE_BAR_HEIGHT - (BORDER_WIDTH * 2)
    680                 pcall(self.onResize, self.width, self.height, maxW, maxH)
    681             end
    682         end
    683     end
    684 
    685     -- Resize window
    686     function window:resize(newWidth, newHeight)
    687         if type(newWidth) ~= "number" or type(newHeight) ~= "number" then
    688             error("resize requires numeric width and height")
    689         end
    690 
    691         if newWidth <= 0 or newHeight <= 0 then
    692             error("Window dimensions must be positive")
    693         end
    694 
    695         local oldWidth = self.width
    696         local oldHeight = self.height
    697 
    698         self.width = newWidth
    699         self.height = newHeight
    700 
    701         -- Free old buffer and clear to force recreation with new size
    702         if self.buffer and VESAFreeWindowBuffer then
    703             VESAFreeWindowBuffer(self.buffer)
    704         end
    705         self.buffer = nil
    706         self.dirty = true
    707 
    708         -- Call onResize callback if it exists
    709         if self.onResize then
    710             pcall(self.onResize, newWidth, newHeight, oldWidth, oldHeight)
    711         end
    712     end
    713 
    714     -- Set window size (alias for resize)
    715     function window:setSize(newWidth, newHeight)
    716         self:resize(newWidth, newHeight)
    717     end
    718 
    719     -- Set window position
    720     function window:setPos(newX, newY)
    721         if type(newX) ~= "number" or type(newY) ~= "number" then
    722             error("setPos requires numeric x and y coordinates")
    723         end
    724 
    725         self.x = newX
    726         self.y = newY
    727         self.dirty = true
    728     end
    729 
    730     -- Mark window for redraw
    731     function window:markDirty()
    732         self.dirty = true
    733     end
    734 
    735     -- Render - draw window frame and flush buffer to screen
    736     -- Call this after drawing to gfx to update the display
    737     function window:render()
    738         self.dirty = true
    739     end
    740 
    741     -- Internal draw operations buffer for imperative drawing
    742     window._draw_ops = {}
    743 
    744     -- Create gfx object for imperative drawing (draw anytime, call render() to display)
    745     window.gfx = {
    746         _window = window,
    747 
    748         -- Clear the drawing buffer (call before redrawing)
    749         clear = function(gfxSelf)
    750             gfxSelf._window._draw_ops = {}
    751         end,
    752 
    753         fillRect = function(gfxSelf, x, y, w, h, color)
    754             if type(gfxSelf) == "number" then
    755                 color = h
    756                 h = w
    757                 w = y
    758                 y = x
    759                 x = gfxSelf
    760                 gfxSelf = window.gfx
    761             end
    762             local r = bit.band(bit.rshift(color, 16), 0xFF)
    763             local g = bit.band(bit.rshift(color, 8), 0xFF)
    764             local b = bit.band(color, 0xFF)
    765             table.insert(gfxSelf._window._draw_ops, {2, x, y, w, h, r, g, b})
    766         end,
    767 
    768         drawRect = function(gfxSelf, x, y, w, h, color)
    769             if type(gfxSelf) == "number" then
    770                 color = h
    771                 h = w
    772                 w = y
    773                 y = x
    774                 x = gfxSelf
    775                 gfxSelf = window.gfx
    776             end
    777             local r = bit.band(bit.rshift(color, 16), 0xFF)
    778             local g = bit.band(bit.rshift(color, 8), 0xFF)
    779             local b = bit.band(color, 0xFF)
    780             table.insert(gfxSelf._window._draw_ops, {2, x, y, w, 1, r, g, b})
    781             table.insert(gfxSelf._window._draw_ops, {2, x, y + h - 1, w, 1, r, g, b})
    782             table.insert(gfxSelf._window._draw_ops, {2, x, y, 1, h, r, g, b})
    783             table.insert(gfxSelf._window._draw_ops, {2, x + w - 1, y, 1, h, r, g, b})
    784         end,
    785 
    786         drawText = function(gfxSelf, x, y, text, color, scale)
    787             if type(gfxSelf) == "string" then
    788                 scale = color
    789                 color = text
    790                 text = y
    791                 y = x
    792                 x = gfxSelf
    793                 gfxSelf = window.gfx
    794             end
    795             local r = bit.band(bit.rshift(color, 16), 0xFF)
    796             local g = bit.band(bit.rshift(color, 8), 0xFF)
    797             local b = bit.band(color, 0xFF)
    798             scale = scale or 1
    799             table.insert(gfxSelf._window._draw_ops, {10, x, y, text, r, g, b, scale})
    800 
    801             -- Record text for selection support (6 pixels per char at scale 1, 12 pixels height)
    802             local charWidth = 8 * scale
    803             local charHeight = 12 * scale
    804             local textWidth = #text * charWidth
    805             table.insert(gfxSelf._window.selectableText, {
    806                 text = text,
    807                 x = x,
    808                 y = y,
    809                 w = textWidth,
    810                 h = charHeight,
    811                 color = color,
    812                 scale = scale
    813             })
    814         end,
    815 
    816         drawUText = function(gfxSelf, x, y, text, color, scale)
    817             -- Draw unselectable text (not recorded for selection)
    818             if type(gfxSelf) == "number" then
    819                 scale = color
    820                 color = text
    821                 text = y
    822                 y = x
    823                 x = gfxSelf
    824                 gfxSelf = window.gfx
    825             end
    826             local r = bit.band(bit.rshift(color, 16), 0xFF)
    827             local g = bit.band(bit.rshift(color, 8), 0xFF)
    828             local b = bit.band(color, 0xFF)
    829             scale = scale or 1
    830             table.insert(gfxSelf._window._draw_ops, {10, x, y, text, r, g, b, scale})
    831             -- Note: not recording in selectableText
    832         end,
    833 
    834         drawImage = function(gfxSelf, image, x, y, w, h)
    835             if type(gfxSelf) == "userdata" then
    836                 h = w
    837                 w = y
    838                 y = x
    839                 x = image
    840                 image = gfxSelf
    841                 gfxSelf = window.gfx
    842             end
    843             table.insert(gfxSelf._window._draw_ops, {11, image, x, y, w, h, 1})
    844         end,
    845 
    846         drawPixel = function(gfxSelf, x, y, color)
    847             if type(gfxSelf) == "number" then
    848                 color = y
    849                 y = x
    850                 x = gfxSelf
    851                 gfxSelf = window.gfx
    852             end
    853             local r = bit.band(bit.rshift(color, 16), 0xFF)
    854             local g = bit.band(bit.rshift(color, 8), 0xFF)
    855             local b = bit.band(color, 0xFF)
    856             table.insert(gfxSelf._window._draw_ops, {0, x, y, r, g, b})
    857         end,
    858 
    859         -- Draw a raw BGRA buffer (from Image object) directly
    860         -- luaImage: Image object with .buffer and :getSize()
    861         -- x, y: destination position
    862         -- srcX, srcY, srcW, srcH: optional source region (defaults to full image)
    863         drawBuffer = function(gfxSelf, luaImage, x, y, srcX, srcY, srcW, srcH)
    864             if type(gfxSelf) ~= "table" or not gfxSelf._window then
    865                 -- Called without self, shift args
    866                 srcH = srcW
    867                 srcW = srcY
    868                 srcY = srcX
    869                 srcX = y
    870                 y = x
    871                 x = luaImage
    872                 luaImage = gfxSelf
    873                 gfxSelf = window.gfx
    874             end
    875             if not luaImage or not luaImage.buffer then return end
    876             local bufW, bufH = luaImage:getSize()
    877             -- {12, buffer, x, y, bufWidth, bufHeight, srcX, srcY, srcW, srcH}
    878             table.insert(gfxSelf._window._draw_ops, {12, luaImage.buffer, x, y, bufW, bufH, srcX or 0, srcY or 0, srcW or 0, srcH or 0})
    879         end,
    880 
    881         getWidth = function(gfxSelf)
    882             if type(gfxSelf) == "table" and gfxSelf._window then
    883                 return gfxSelf._window.width
    884             end
    885             return window.width
    886         end,
    887 
    888         getHeight = function(gfxSelf)
    889             if type(gfxSelf) == "table" and gfxSelf._window then
    890                 return gfxSelf._window.height
    891             end
    892             return window.height
    893         end
    894     }
    895 
    896     -- Getters for window dimensions
    897     function window:getWidth()
    898         return self.width
    899     end
    900 
    901     function window:getHeight()
    902         return self.height
    903     end
    904 
    905     function window:getX()
    906         return self.x
    907     end
    908 
    909     function window:getY()
    910         return self.y
    911     end
    912 
    913     -- Add to app's window list
    914     table.insert(self.windows, window)
    915 
    916     -- Set as active window in LPM
    917     -- LPM removed - no longer needed
    918 
    919     if osprint then
    920         osprint("Window created for " .. self.appName .. " at (" .. x .. "," .. y .. ") size " .. width .. "x" .. height .. "\n")
    921     end
    922 
    923     -- Call onOpen callback after window is fully initialized
    924     if window.onOpen then
    925         pcall(window.onOpen)
    926     end
    927 
    928     -- Fire WindowCreated hook
    929     if _G.sys and _G.sys.hook and _G.sys.hook.run then
    930         _G.sys.hook:run("WindowCreated", window)
    931     end
    932 
    933     return window
    934 end
    935 
    936 --- Enter exclusive fullscreen mode
    937 -- Creates a fullscreen window (1024x768)
    938 function Application:enterFullscreen()
    939     -- LPM is no longer required for fullscreen mode
    940 
    941     -- Create fullscreen window if it doesn't exist
    942     if not self.fullscreenWindow then
    943         self.fullscreenWindow = {
    944             x = 0,
    945             y = 0,
    946             width = 1024,
    947             height = 768,
    948             visible = true,
    949             app = self,
    950             appInstance = self,
    951             drawCallback = nil,
    952             inputCallback = nil,
    953             isFullscreen = true,
    954             isBorderless = true
    955         }
    956 
    957         -- Window draw method
    958         function self.fullscreenWindow:draw(safeGfx)
    959             if self.drawCallback and self.visible then
    960                 self.drawCallback(safeGfx)
    961             end
    962         end
    963 
    964         -- Set draw callback
    965         function self.fullscreenWindow:onDraw(callback)
    966             if type(callback) ~= "function" then
    967                 error("onDraw requires a function")
    968             end
    969             self.drawCallback = callback
    970         end
    971 
    972         -- Set input callback
    973         function self.fullscreenWindow:onInput(callback)
    974             if type(callback) ~= "function" then
    975                 error("onInput requires a function")
    976             end
    977             self.inputCallback = callback
    978         end
    979 
    980         -- Add to app's window list
    981         table.insert(self.windows, self.fullscreenWindow)
    982 
    983         if osprint then
    984             osprint("Fullscreen window created for " .. self.appName .. "\n")
    985         end
    986     end
    987 
    988     return self.fullscreenWindow
    989 end
    990 
    991 --- Exit exclusive fullscreen mode
    992 function Application:exitFullscreen()
    993     -- LPM removed - exclusive fullscreen disabled
    994 end
    995 
    996 return Application