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