luajitos

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

init.lua (27366B)


      1 -- LuaJIT OS Taskbar App
      2 -- System taskbar with start menu and running applications list
      3 
      4 -- Taskbar dimensions
      5 -- Get screen dimensions from sys.screen
      6 local screenWidth = (sys and sys.screen and sys.screen[1] and sys.screen[1].width) or 1200
      7 local screenHeight = (sys and sys.screen and sys.screen[1] and sys.screen[1].height) or 900
      8 local taskbarHeight = 50
      9 local taskbarY = screenHeight - taskbarHeight
     10 
     11 -- Create main taskbar window (borderless, full width at bottom)
     12 local taskbar = app:newWindow(0, taskbarY, screenWidth, taskbarHeight)
     13 taskbar.isBorderless = true
     14 taskbar.alwaysOnTop = true  -- Always draw on top of all other windows
     15 taskbar.noTaskbar = true  -- Don't show in taskbar
     16 -- Note: removed isBackground so taskbar can receive click events
     17 
     18 -- Start menu window (initially hidden)
     19 local startMenuWindow = nil
     20 local startMenuVisible = false
     21 local startMenuJustClosed = false  -- Track if menu was just closed by focus lost
     22 
     23 -- Window popup for multi-window apps
     24 local windowPopup = nil
     25 local windowPopupVisible = false
     26 
     27 -- Start button dimensions (2px margin on each side)
     28 local startButtonMargin = 2
     29 local startButtonWidth = 76  -- 80 - 4 (2px on each side)
     30 local startButtonHeight = taskbarHeight - 4  -- 2px top + 2px bottom
     31 local startButtonX = startButtonMargin
     32 local startButtonY = startButtonMargin
     33 
     34 -- App button dimensions
     35 local appButtonWidth = 120
     36 local appButtonHeight = 40
     37 local appButtonY = 5
     38 local appButtonStartX = startButtonX + startButtonWidth + 5
     39 
     40 -- Helper: Check if click is inside a rectangle
     41 local function isInside(x, y, rx, ry, rw, rh)
     42     return x >= rx and x < rx + rw and y >= ry and y < ry + rh
     43 end
     44 
     45 -- Helper: Format time from os.time() (unix timestamp)
     46 local function formatTime()
     47     local timestamp = os.time()
     48 
     49     -- Simple time formatting (HH:MM:SS)
     50     local seconds = timestamp % 60
     51     local minutes = math.floor(timestamp / 60) % 60
     52     local hours = math.floor(timestamp / 3600) % 24
     53 
     54     return string.format("%02d:%02d:%02d", hours, minutes, seconds)
     55 end
     56 
     57 -- Helper: Get running applications grouped by app, ordered by start time
     58 local function getRunningApplications()
     59     local appMap = {}
     60     local appList = {}
     61 
     62     -- Access global applications from sys
     63     if sys and sys.apps then
     64         for pid, application in pairs(sys.apps) do
     65             -- Check if app has any visible or minimized non-taskbar windows
     66             local hasWindows = false
     67             if application.windows then
     68                 for _, window in ipairs(application.windows) do
     69                     if (window.visible or window.minimized) and not window.noTaskbar and not window.isBackground then
     70                         hasWindows = true
     71                         break
     72                     end
     73                 end
     74             end
     75 
     76             if hasWindows then
     77                 -- Get display name from manifest, with fallbacks
     78                 local displayName = "Unknown App"
     79                 if application.manifest then
     80                     displayName = application.manifest.pretty or application.manifest.name or displayName
     81                 end
     82 
     83                 -- If still no good name, extract from app ID
     84                 if displayName == "Unknown App" and application.appName then
     85                     local fullName = application.appName
     86                     displayName = fullName:match("%.([^%.]+)$") or fullName
     87                 end
     88 
     89                 local appInfo = {
     90                     app = application,
     91                     name = displayName,
     92                     startTime = application.startTime or 0,
     93                     windows = {},
     94                     hasMinimized = false
     95                 }
     96 
     97                 -- Collect visible and minimized windows for this app
     98                 if application.windows then
     99                     for _, window in ipairs(application.windows) do
    100                         if (window.visible or window.minimized) and not window.noTaskbar and not window.isBackground then
    101                             table.insert(appInfo.windows, window)
    102                             if window.minimized then
    103                                 appInfo.hasMinimized = true
    104                             end
    105                         end
    106                     end
    107                 end
    108 
    109                 table.insert(appList, appInfo)
    110             end
    111         end
    112     end
    113 
    114     -- Sort by start time (oldest first)
    115     table.sort(appList, function(a, b)
    116         return a.startTime < b.startTime
    117     end)
    118 
    119     return appList
    120 end
    121 
    122 -- Local timestamp for window focusing
    123 local app_timestamp = 0
    124 
    125 -- Helper: Bring window to front and focus it (restores if minimized)
    126 local function focusWindow(window)
    127     -- Restore if minimized
    128     if window.minimized and window.restore then
    129         window:restore()
    130     end
    131 
    132     -- Bring window to front by updating its creation timestamp
    133     app_timestamp = app_timestamp + 1
    134     window.createdAt = app_timestamp
    135 
    136     -- Mark as dirty so it redraws on top
    137     window.dirty = true
    138 end
    139 
    140 -- Helper: Close window popup
    141 local function closeWindowPopup()
    142     if windowPopup then
    143         windowPopup:hide()
    144         windowPopupVisible = false
    145     end
    146 end
    147 
    148 -- Helper: Show window popup for multi-window app
    149 local function showWindowPopup(appInfo, buttonX)
    150     closeWindowPopup()
    151 
    152     local numWindows = #appInfo.windows
    153     local popupWidth = appButtonWidth
    154     local popupHeight = numWindows * appButtonHeight
    155     local popupX = buttonX
    156     local popupY = taskbarY - popupHeight
    157 
    158     if not windowPopup then
    159         windowPopup = app:newWindow(popupX, popupY, popupWidth, popupHeight)
    160         windowPopup.noTaskbar = true
    161         windowPopup.isBorderless = false
    162     else
    163         -- Update position and size
    164         windowPopup.x = popupX
    165         windowPopup.y = popupY
    166         windowPopup.width = popupWidth
    167         windowPopup.height = popupHeight
    168     end
    169 
    170     -- Store reference to current app windows for click handling
    171     windowPopup.appWindows = appInfo.windows
    172 
    173     -- Draw window list
    174     windowPopup.onDraw = function(gfx)
    175         gfx:fillRect(0, 0, popupWidth, popupHeight, 0x2C2C2C)
    176 
    177         for i, window in ipairs(appInfo.windows) do
    178             local yPos = (i - 1) * appButtonHeight
    179             local title = window.title or "Window " .. i
    180 
    181             -- Truncate long titles
    182             if #title > 15 then
    183                 title = title:sub(1, 12) .. "..."
    184             end
    185 
    186             -- Draw button background
    187             gfx:fillRect(0, yPos, popupWidth, appButtonHeight, 0x404040)
    188             gfx:drawRect(0, yPos, popupWidth, appButtonHeight, 0x606060)
    189 
    190             -- Draw text
    191             gfx:drawUText(5, yPos + 13, title, 0xFFFFFF)
    192         end
    193     end
    194 
    195     -- Handle clicks on window entries
    196     windowPopup.onClick = function(mx, my)
    197         local clickedIndex = math.floor(my / appButtonHeight) + 1
    198         if clickedIndex >= 1 and clickedIndex <= #appInfo.windows then
    199             local window = appInfo.windows[clickedIndex]
    200             focusWindow(window)
    201             windowPopup:hide()
    202             windowPopupVisible = false
    203             taskbar:markDirty()
    204         end
    205     end
    206 
    207     -- Add onFocusLost to close popup when clicking elsewhere
    208     windowPopup.onFocusLost = function()
    209         windowPopup:hide()
    210         windowPopupVisible = false
    211     end
    212 
    213     windowPopup:show()
    214     windowPopupVisible = true
    215     windowPopup:markDirty()
    216 end
    217 
    218 -- Cache for app icons
    219 local appIconCache = {}
    220 
    221 -- Helper: Load icon for an app
    222 local function loadAppIcon(appId)
    223     -- Check cache first
    224     if appIconCache[appId] then
    225         return appIconCache[appId]
    226     end
    227 
    228     -- Try PNG first, then JPEG, then BMP
    229     local iconPaths = {
    230         "/apps/" .. appId .. "/icon.png",
    231         "/apps/" .. appId .. "/icon.jpg",
    232         "/apps/" .. appId .. "/icon.jpeg",
    233         "/apps/" .. appId .. "/icon.bmp"
    234     }
    235     local defaultIconPath = "/os/res/default.bmp"
    236 
    237     if CRamdiskExists and CRamdiskOpen and CRamdiskRead and CRamdiskClose then
    238         for _, iconPath in ipairs(iconPaths) do
    239             if CRamdiskExists(iconPath) then
    240                 local handle = CRamdiskOpen(iconPath, "r")
    241                 if handle then
    242                     local iconData = CRamdiskRead(handle)
    243                     CRamdiskClose(handle)
    244 
    245                     if iconData then
    246                         local img = nil
    247                         if iconPath:match("%.png$") and PNGLoad then
    248                             img = PNGLoad(iconData)
    249                         elseif (iconPath:match("%.jpg$") or iconPath:match("%.jpeg$")) and JPEGLoad then
    250                             img = JPEGLoad(iconData)
    251                         elseif iconPath:match("%.bmp$") and BMPLoad then
    252                             img = BMPLoad(iconData)
    253                         end
    254 
    255                         if img then
    256                             local info = ImageGetInfo and ImageGetInfo(img)
    257                             local width = info and info.width or 0
    258                             local height = info and info.height or 0
    259 
    260                             if width > 0 and height > 0 and width <= 128 and height <= 128 then
    261                                 appIconCache[appId] = {
    262                                     buffer = img,
    263                                     width = width,
    264                                     height = height
    265                                 }
    266                                 return appIconCache[appId]
    267                             else
    268                                 if ImageDestroy then
    269                                     ImageDestroy(img)
    270                                 end
    271                             end
    272                         end
    273                     end
    274                 end
    275             end
    276         end
    277 
    278         -- Fall back to default icon
    279         if CRamdiskExists(defaultIconPath) then
    280             local handle = CRamdiskOpen(defaultIconPath, "r")
    281             if handle then
    282                 local iconData = CRamdiskRead(handle)
    283                 CRamdiskClose(handle)
    284 
    285                 if iconData and BMPLoad then
    286                     local img = BMPLoad(iconData)
    287                     if img then
    288                         local info = ImageGetInfo and ImageGetInfo(img)
    289                         local width = info and info.width or 0
    290                         local height = info and info.height or 0
    291 
    292                         if width > 0 and height > 0 then
    293                             appIconCache[appId] = {
    294                                 buffer = img,
    295                                 width = width,
    296                                 height = height
    297                             }
    298                             return appIconCache[appId]
    299                         end
    300                     end
    301                 end
    302             end
    303         end
    304     end
    305 
    306     return nil
    307 end
    308 
    309 -- Helper: Get available applications from /apps directory organized by category
    310 local function getAvailableApplications()
    311     local categorizedApps = {}
    312 
    313     -- Scan /apps directory for applications
    314     if CRamdiskList then
    315         local entries = CRamdiskList("/apps")
    316         if entries then
    317             for _, entry in ipairs(entries) do
    318                 if entry.type == "directory" or entry.type == "dir" then
    319                     local appId = entry.name
    320 
    321                     -- Try to load manifest using GetManifest
    322                     local displayName = appId:match("%.([^%.]+)$") or appId  -- Fallback to short name
    323                     local category = "Other"  -- Default category
    324                     local shouldShow = true
    325 
    326                     if GetManifest then
    327                         local manifest = GetManifest(appId)
    328                         if manifest then
    329                             -- Use manifest.pretty or manifest.name
    330                             displayName = manifest.pretty or manifest.name or displayName
    331                             -- Get category from manifest
    332                             category = manifest.category or "Other"
    333 
    334                             -- Skip CLI apps, background apps, and hidden apps from the start menu
    335                             -- Check both 'mode' and 'type' fields for compatibility
    336                             local appMode = manifest.mode or manifest.type
    337                             if appMode == "cli" or appMode == "background" or manifest.hidden then
    338                                 shouldShow = false
    339                             end
    340                         end
    341                     end
    342 
    343                     if shouldShow then
    344                         -- Capitalize first letter of category
    345                         category = category:sub(1,1):upper() .. category:sub(2)
    346 
    347                         -- Initialize category if it doesn't exist
    348                         if not categorizedApps[category] then
    349                             categorizedApps[category] = {}
    350                         end
    351 
    352                         table.insert(categorizedApps[category], {
    353                             id = appId,
    354                             name = displayName,
    355                             icon = loadAppIcon(appId),
    356                             category = category
    357                         })
    358                     end
    359                 end
    360             end
    361         end
    362     end
    363 
    364     -- Sort apps within each category alphabetically
    365     for _, apps in pairs(categorizedApps) do
    366         table.sort(apps, function(a, b) return a.name < b.name end)
    367     end
    368 
    369     return categorizedApps
    370 end
    371 
    372 -- Create or show start menu
    373 local function showStartMenu()
    374     if not startMenuWindow then
    375         -- Position menu just above the start button
    376         local menuWidth = 300
    377         local menuHeight = 500
    378         local menuX = startButtonX
    379         local menuY = taskbarY - menuHeight
    380 
    381         startMenuWindow = app:newWindow(menuX, menuY, menuWidth, menuHeight)
    382         startMenuWindow.noTaskbar = true  -- Don't show this window in taskbar
    383         startMenuWindow.isBorderless = true  -- No window decorations
    384         startMenuWindow.title = "Start Menu"
    385 
    386         -- Get available apps once
    387         local availableApps = getAvailableApplications()
    388         startMenuWindow.availableApps = availableApps
    389 
    390         -- Draw start menu
    391         startMenuWindow.onDraw = function(gfx)
    392             -- Background
    393             gfx:fillRect(0, 0, menuWidth, menuHeight, 0x2C2C2C)
    394 
    395             -- Title bar
    396             gfx:fillRect(0, 0, menuWidth, 30, 0x0066CC)
    397             gfx:drawUText(10, 8, "Applications", 0xFFFFFF)
    398 
    399             -- List available apps by category
    400             local yPos = 40
    401             local itemHeight = 30
    402             local categoryHeaderHeight = 25
    403 
    404             -- Check if we have any categories
    405             local hasApps = false
    406             for _ in pairs(availableApps) do
    407                 hasApps = true
    408                 break
    409             end
    410 
    411             if not hasApps then
    412                 gfx:drawUText(10, yPos, "No applications found", 0x888888)
    413             else
    414                 -- Sort categories alphabetically
    415                 local sortedCategories = {}
    416                 for category in pairs(availableApps) do
    417                     table.insert(sortedCategories, category)
    418                 end
    419                 table.sort(sortedCategories)
    420 
    421                 for _, category in ipairs(sortedCategories) do
    422                     local apps = availableApps[category]
    423 
    424                     -- Don't overflow menu
    425                     if yPos + categoryHeaderHeight > menuHeight - 10 then
    426                         break
    427                     end
    428 
    429                     -- Draw category header
    430                     gfx:fillRect(5, yPos, menuWidth - 10, categoryHeaderHeight, 0x1A1A1A)
    431                     gfx:drawUText(10, yPos + 6, category, 0xCCCCCC)
    432                     yPos = yPos + categoryHeaderHeight + 2
    433 
    434                     -- Draw apps in this category
    435                     for i, appInfo in ipairs(apps) do
    436                         -- Don't overflow menu
    437                         if yPos + itemHeight > menuHeight - 10 then
    438                             break
    439                         end
    440 
    441                         -- App item background
    442                         gfx:fillRect(5, yPos, menuWidth - 10, itemHeight, 0x404040)
    443                         gfx:drawRect(5, yPos, menuWidth - 10, itemHeight, 0x606060)
    444 
    445                         -- Draw icon if available
    446                         local text_x_offset = 15
    447                         if appInfo.icon and appInfo.icon.buffer then
    448                             local icon = appInfo.icon.buffer
    449                             local icon_w = appInfo.icon.width
    450                             local icon_h = appInfo.icon.height
    451 
    452                             -- Scale icon to fit item (24x24 max)
    453                             local max_icon_size = 24
    454                             local scale = math.min(max_icon_size / icon_w, max_icon_size / icon_h, 1.0)
    455                             local draw_icon_w = math.floor(icon_w * scale)
    456                             local draw_icon_h = math.floor(icon_h * scale)
    457                             local icon_x = 10
    458                             local icon_y = yPos + math.floor((itemHeight - draw_icon_h) / 2)
    459 
    460                             -- Draw icon using gfx:drawImage
    461                             gfx:drawImage(icon, icon_x, icon_y, draw_icon_w, draw_icon_h)
    462 
    463                             text_x_offset = 10 + draw_icon_w + 6
    464                         end
    465 
    466                         -- Draw app name
    467                         gfx:drawUText(text_x_offset, yPos + 8, appInfo.name, 0xFFFFFF)
    468 
    469                         yPos = yPos + itemHeight + 2
    470                     end
    471                 end
    472             end
    473         end
    474 
    475         -- Click handler to launch apps
    476         startMenuWindow.onClick = function(mx, my)
    477             -- Check if click is in title bar
    478             if my < 30 then
    479                 return
    480             end
    481 
    482             -- Calculate which app was clicked
    483             local yPos = 40
    484             local itemHeight = 30
    485             local categoryHeaderHeight = 25
    486 
    487             -- Sort categories alphabetically (same order as drawing)
    488             local sortedCategories = {}
    489             for category in pairs(availableApps) do
    490                 table.insert(sortedCategories, category)
    491             end
    492             table.sort(sortedCategories)
    493 
    494             for _, category in ipairs(sortedCategories) do
    495                 local apps = availableApps[category]
    496 
    497                 -- Check if click is on category header - ignore it
    498                 if my >= yPos and my < yPos + categoryHeaderHeight + 2 then
    499                     return  -- Click on category header, do nothing
    500                 end
    501 
    502                 -- Skip past category header
    503                 yPos = yPos + categoryHeaderHeight + 2
    504 
    505                 -- Check clicks on apps in this category
    506                 for i, appInfo in ipairs(apps) do
    507                     if yPos + itemHeight > menuHeight - 10 then
    508                         break
    509                     end
    510 
    511                     -- Check if click is in the gap after this item (before checking item itself)
    512                     if my >= yPos + itemHeight and my < yPos + itemHeight + 2 then
    513                         return  -- Click in gap between items, do nothing
    514                     end
    515 
    516                     if my >= yPos and my < yPos + itemHeight then
    517                         -- Launch this app
    518                         if run then
    519                             local success, result = pcall(run, appInfo.id)
    520                             if not success then
    521                                 osprint("Failed to launch " .. appInfo.id .. ": " .. tostring(result) .. "\n")
    522                             end
    523                         end
    524                         -- Hide menu
    525                         startMenuWindow:hide()
    526                         startMenuVisible = false
    527                         return
    528                     end
    529 
    530                     yPos = yPos + itemHeight + 2
    531                 end
    532             end
    533 
    534             -- Click outside items closes menu
    535             startMenuWindow:hide()
    536             startMenuVisible = false
    537         end
    538 
    539         -- Add onFocusLost to close menu when clicking elsewhere
    540         startMenuWindow.onFocusLost = function()
    541             startMenuWindow:hide()
    542             startMenuVisible = false
    543             startMenuJustClosed = true  -- Mark that we just closed via focus lost
    544         end
    545     else
    546         -- Show existing window
    547         startMenuWindow:show()
    548     end
    549 
    550     startMenuVisible = true
    551     startMenuWindow:markDirty()
    552 end
    553 
    554 -- Hide start menu
    555 local function hideStartMenu()
    556     if startMenuWindow then
    557         startMenuWindow:hide()
    558     end
    559     startMenuVisible = false
    560 end
    561 
    562 -- Mouse click handling for taskbar
    563 taskbar.onClick = function(mx, my)
    564     -- Check if start button was clicked (extend clickable area to bottom-left corner)
    565     local startClickableRight = startButtonX + startButtonWidth
    566     local startClickableBottom = taskbarHeight  -- Extend to bottom edge
    567     if mx >= 0 and mx < startClickableRight and my >= 0 and my < startClickableBottom then
    568         closeWindowPopup()  -- Close any window popup
    569         -- If menu was just closed by focus lost (clicking start button), don't reopen
    570         if startMenuJustClosed then
    571             startMenuJustClosed = false
    572             taskbar:markDirty()
    573             return
    574         end
    575         if startMenuVisible then
    576             hideStartMenu()
    577         else
    578             showStartMenu()
    579         end
    580         taskbar:markDirty()
    581         return
    582     end
    583 
    584     -- Check if any app button was clicked
    585     local apps = getRunningApplications()
    586     local currentX = appButtonStartX
    587 
    588     for i, appInfo in ipairs(apps) do
    589         -- Don't overflow past the time display
    590         if currentX + appButtonWidth > screenWidth - 120 then
    591             break
    592         end
    593 
    594         if isInside(mx, my, currentX, appButtonY, appButtonWidth, appButtonHeight) then
    595             hideStartMenu()  -- Close start menu if open
    596 
    597             if #appInfo.windows == 1 then
    598                 -- Single window: focus it
    599                 focusWindow(appInfo.windows[1])
    600                 closeWindowPopup()
    601             else
    602                 -- Multiple windows: show popup
    603                 showWindowPopup(appInfo, currentX)
    604             end
    605             taskbar:markDirty()
    606             return
    607         end
    608 
    609         currentX = currentX + appButtonWidth + 5
    610     end
    611 end
    612 
    613 -- Draw the taskbar
    614 taskbar.onDraw = function(gfx)
    615     -- Background
    616     gfx:fillRect(0, 0, screenWidth, taskbarHeight, 0x1E1E1E)
    617 
    618     -- Draw start button
    619     gfx:fillRect(startButtonX, startButtonY, startButtonWidth, startButtonHeight, 0x0066CC)
    620 
    621     -- Start button text (centered vertically)
    622     local textY = startButtonY + math.floor((startButtonHeight - 8) / 2)
    623     gfx:drawUText(startButtonX + 20, textY, "Start", 0xFFFFFF)
    624 
    625     -- Draw application buttons
    626     local apps = getRunningApplications()
    627     local currentX = appButtonStartX
    628 
    629     for i, appInfo in ipairs(apps) do
    630         -- Don't overflow past the time display
    631         if currentX + appButtonWidth > screenWidth - 120 then
    632             break
    633         end
    634 
    635         -- Draw app button (grayed out if all windows minimized)
    636         local bgColor = appInfo.hasMinimized and 0x303030 or 0x404040
    637         local borderColor = appInfo.hasMinimized and 0x505050 or 0x606060
    638         gfx:fillRect(currentX, appButtonY, appButtonWidth, appButtonHeight, bgColor)
    639         gfx:drawRect(currentX, appButtonY, appButtonWidth, appButtonHeight, borderColor)
    640 
    641         -- Draw icon if available
    642         local text_x_offset = 5
    643         if appInfo.app and appInfo.app.iconBuffer then
    644             local icon = appInfo.app.iconBuffer
    645             local icon_w = appInfo.app.iconWidth
    646             local icon_h = appInfo.app.iconHeight
    647 
    648             -- Scale icon to fit button (24x24 max)
    649             local max_icon_size = 24
    650             local scale = math.min(max_icon_size / icon_w, max_icon_size / icon_h, 1.0)
    651             local draw_icon_w = math.floor(icon_w * scale)
    652             local draw_icon_h = math.floor(icon_h * scale)
    653             local icon_x = currentX + 3
    654             local icon_y = appButtonY + math.floor((appButtonHeight - draw_icon_h) / 2)
    655 
    656             -- Draw icon using gfx:drawImage
    657             gfx:drawImage(icon, icon_x, icon_y, draw_icon_w, draw_icon_h)
    658 
    659             text_x_offset = 3 + draw_icon_w + 4
    660         end
    661 
    662         -- App name (truncate if needed)
    663         local appName = appInfo.name
    664         local max_chars = text_x_offset > 5 and 10 or 14
    665         if #appName > max_chars then
    666             appName = appName:sub(1, max_chars - 3) .. "..."
    667         end
    668 
    669         -- Add window count if multiple windows
    670         if #appInfo.windows > 1 then
    671             appName = appName .. " (" .. #appInfo.windows .. ")"
    672         end
    673 
    674         -- Draw text (dimmed if minimized)
    675         local textColor = appInfo.hasMinimized and 0xAAAAAA or 0xFFFFFF
    676         gfx:drawUText(currentX + text_x_offset, appButtonY + 13, appName, textColor)
    677 
    678         currentX = currentX + appButtonWidth + 5
    679     end
    680 
    681     -- Draw time on the right side
    682     local timeText = formatTime()
    683     local timeX = screenWidth - 100  -- Position near right edge
    684     local timeY = 15  -- Vertically centered
    685     gfx:drawUText(timeX, timeY, timeText, 0xFFFFFF)
    686 
    687     -- Draw separator line at top
    688     gfx:fillRect(0, 0, screenWidth, 1, 0x444444)
    689 end
    690 
    691 -- Force periodic redraw to update time (every second would be ideal)
    692 -- Note: This is a simple approach - could be optimized with a timer
    693 local lastSecond = -1
    694 local function updateTaskbar()
    695     local currentSecond = os.time() % 60
    696     if currentSecond ~= lastSecond then
    697         lastSecond = currentSecond
    698         taskbar:markDirty()
    699 
    700         -- Also update start menu if visible
    701         if startMenuVisible and startMenuWindow then
    702             startMenuWindow:markDirty()
    703         end
    704 
    705         -- Also update window popup if visible
    706         if windowPopupVisible and windowPopup then
    707             windowPopup:markDirty()
    708         end
    709     end
    710 end
    711 
    712 -- Hook into the main draw loop to update periodically
    713 -- This is a workaround since we don't have a proper timer system yet
    714 -- TODO: Implement periodic updates when timer system is available
    715 -- For now, the taskbar updates when clicked or when windows change
    716 
    717 -- Register hooks to redraw taskbar when windows are created or closed
    718 if sys and sys.hook then
    719     sys.hook:add("WindowCreated", "taskbar-redraw", function(window)
    720         taskbar:markDirty()
    721     end)
    722 
    723     sys.hook:add("WindowClosed", "taskbar-redraw", function(window)
    724         taskbar:markDirty()
    725     end)
    726 
    727     sys.hook:add("WindowMinimized", "taskbar-redraw", function(window)
    728         taskbar:markDirty()
    729     end)
    730 
    731     sys.hook:add("WindowRestored", "taskbar-redraw", function(window)
    732         taskbar:markDirty()
    733     end)
    734 
    735     sys.hook:add("ScreenResolutionChanged", "taskbar-reposition", function(screen)
    736         -- Update local screen dimensions
    737         screenWidth = screen.width
    738         screenHeight = screen.height
    739         taskbarY = screenHeight - taskbarHeight
    740 
    741         -- Update taskbar window position and size
    742         taskbar:setPos(0, taskbarY)
    743         taskbar:setSize(screenWidth, taskbarHeight)
    744 
    745         -- Mark for redraw
    746         taskbar:markDirty()
    747     end)
    748 
    749     -- Close start menu when background is clicked
    750     sys.hook:add("BackgroundFocused", "taskbar-close-menu", function()
    751         if startMenuVisible then
    752             hideStartMenu()
    753             taskbar:markDirty()
    754         end
    755     end)
    756 end