· 5 months ago · Apr 24, 2025, 01:45 PM
1local component = require("component")
2local term = require("term")
3local event = require("event")
4local unicode = require("unicode")
5local computer = require("computer")
6local shell = require("shell")
7local fs = require("filesystem")
8local math = require("math") -- Added math for centering
9local string = require("string") -- Added string for line cleaning and padding
10
11local gpu = component.gpu -- Get the GPU component
12
13-- --- CONFIG LOADING (Updated) ---
14local config = {}
15local configFilePath = "Omnia/mainConfig.cfg" -- Path to the config file
16local configFile, err = io.open(configFilePath, "r") -- Use io.open
17
18if not configFile then
19 -- More graceful handling than just error()
20 print("Error: Config file not found:", configFilePath)
21 print("Reason:", err) -- Print the specific error message
22 print("Using default settings and art file path.")
23 -- Set default config values if file is missing or cannot be read
24 config = {
25 width = 160, -- Default to a common OC resolution
26 height = 50,
27 backgroundColor = 0x4FA6A7,
28 inputColor = 0x333333,
29 textColor = 0xFFFFFF,
30 accentColor = 0x1B1B1B,
31 cursorColor = 0x00FF00,
32 cursorChar = "_",
33 artFile = "Omnia/art.txt" -- Default art file path
34 }
35else
36 print("Config file found. Reading...")
37 for line in configFile:lines() do
38 -- This pattern captures key = value, handling potential whitespace
39 local key, value = line:match("^(%w+)%s*=%s*(.+)$")
40 if key and value then
41 -- Basic type conversion (handles 0x hex colors, numbers, strings)
42 if value:match("^0x") then
43 config[key] = tonumber(value)
44 elseif tonumber(value) then
45 config[key] = tonumber(value)
46 else
47 config[key] = value
48 end
49 end
50 end
51 -- Use io.close for closing the config file handle
52 local success, close_err = pcall(io.close, configFile) -- Use pcall for safety
53
54 if success then
55 print("Config file closed successfully!")
56 else
57 print("Error closing config file using io.close:")
58 print("Reason:", close_err)
59 -- Continue script execution even if closing config fails
60 end
61 print("Config loading finished.")
62end
63
64-- Get colors from config (using defaults if not found)
65local backgroundColor = config.backgroundColor or 0x4FA6A7
66local inputColor = config.inputColor or 0x333333
67local textColor = config.textColor or 0xFFFFFF
68local accentColor = config.accentColor or 0x1B1B1B
69local cursorColor = config.cursorColor or 0x00FF00
70local cursorChar = config.cursorChar or "_"
71
72-- --- SCREEN SETUP (Updated) ---
73-- Get screen resolution from the GPU component first
74local screenWidth, screenHeight
75if gpu then
76 -- Use pcall for safety, as component.gpu or getResolution might fail
77 local success, res_err = pcall(gpu.getResolution, gpu)
78 if success then
79 screenWidth, screenHeight = res_err -- pcall returns results as second value on success
80 print("GPU resolution detected:", screenWidth, "x", screenHeight)
81 else
82 print("Warning: Could not get GPU resolution.", res_err)
83 print("Using terminal resolution fallback.")
84 -- Fallback to terminal resolution if GPU resolution fails
85 screenWidth, screenHeight = term.getResolution()
86 end
87else
88 print("Warning: No GPU component found. Using terminal resolution.")
89 -- Fallback to terminal resolution if no GPU component
90 screenWidth, screenHeight = term.getResolution()
91end
92
93-- Use resolution from config if specified and valid, otherwise use detected/fallback
94if config.width and config.height and config.width > 0 and config.height > 0 then
95 screenWidth = config.width
96 screenHeight = config.height
97 if gpu then -- Only set GPU resolution if GPU exists
98 local success = pcall(gpu.setResolution, gpu, screenWidth, screenHeight)
99 if not success then
100 print("Warning: Could not set GPU resolution to config values:", screenWidth, "x", screenHeight)
101 end
102 else
103 print("Warning: Config resolution specified, but no GPU to set it.")
104 end
105end
106
107-- If detection/fallback failed or resolution is invalid, use a safe default
108if not screenWidth or not screenHeight or screenWidth <= 0 or screenHeight <= 0 then
109 print("Error: Could not determine screen resolution. Assuming 80x25.")
110 screenWidth = 80
111 screenHeight = 25
112end
113
114
115gpu.setBackground(backgroundColor) -- Set background color
116gpu.setForeground(textColor) -- Set foreground color
117term.clear() -- Clear the screen
118
119
120-- --- LOGIN VARS ---
121local users = { admin = "password123", user = "letmein" } -- Consider storing this securely/externally
122local username, password, selectedField, showCursor, showPassword = "", "", "username", true, false
123local inputWidth = 24
124local inputX = math.floor((screenWidth - inputWidth) / 2)
125local offset = math.floor(screenHeight * 0.15)
126local usernameY = math.floor(screenHeight / 2) - 3 + offset
127local passwordY = usernameY + 3
128local loginButtonY = passwordY + 4
129local showPasswordButtonY = passwordY + 6
130
131-- --- ASCII ART (Updated Loading) ---
132local asciiArt = {} -- Table to store the art lines
133local artPath = config.artFile or "Omnia/art.txt" -- Get path from config or use default
134
135print("Attempting to load ASCII art from:", artPath)
136local artFileHandle, art_err = io.open(artPath, "r") -- Use io.open
137
138if not artFileHandle then
139 -- More graceful handling - store error and continue without art
140 print("Error: Art file not found or could not be read:", artPath)
141 print("Reason:", art_err)
142 -- asciiArt will remain empty, which is handled by drawArt()
143else
144 print("Art file found. Reading lines...")
145 -- Read all lines into the table, cleaning them
146 for line in artFileHandle:lines() do
147 -- Clean the line: Remove trailing whitespace and carriage return
148 line = line:match("^(.-)%s*[\r]?$") or line
149 -- Remove leading whitespace (optional)
150 line = line:match("^%s*(.+)$") or line
151 if line == nil then line = "" end -- Ensure line is a string
152
153 table.insert(asciiArt, line)
154 end
155
156 -- Use io.close for closing the art file handle
157 local success_art_close, close_art_err = pcall(io.close, artFileHandle)
158
159 if success_art_close then
160 print("Art file closed successfully!")
161 else
162 print("Error closing art file using io.close:")
163 print("Reason:", close_art_err)
164 end
165 print("ASCII art loading finished.")
166end
167
168local artTopY = math.floor(screenHeight / 7) -- Calculate art starting Y based on screen height
169
170-- --- UI FUNCTIONS (Login Screen - Updated drawArt) ---
171local function drawArt()
172 -- Only attempt to draw if there is art loaded
173 if #asciiArt > 0 then
174 gpu.setForeground(textColor) -- Use textColor from config
175 local startY = artTopY
176 for i, line in ipairs(asciiArt) do
177 local y = startY + i - 1
178 -- Only draw if the line is within screen bounds
179 if y > 0 and y <= screenHeight then
180 local lineLength = unicode.len(line)
181 local x = math.floor((screenWidth - lineLength) / 2) -- Calculate X for centering
182 -- Ensure x is within screen bounds
183 if x < 1 then x = 1 end
184 if x > screenWidth then x = screenWidth end -- Should not happen if lineLength < screenWidth
185
186 gpu.set(x, y, line) -- Use gpu.set for drawing
187 end
188 end
189 else
190 -- Optionally draw a placeholder message if art is not loaded
191 local message = "ASCII Art failed to load."
192 local x = math.floor((screenWidth - unicode.len(message)) / 2)
193 local y = artTopY + 2
194 if y > 0 and y <= screenHeight then
195 gpu.setForeground(0xFF0000) -- Red color for error message
196 gpu.set(x, y, message)
197 gpu.setForeground(textColor) -- Reset color
198 end
199 end
200end
201
202local function drawInput(x, y, width, label, text, selected, masked)
203 local displayText = masked and text:gsub(".", "*") or text
204 -- Ensure text doesn't overflow the buffer width
205 local displayBuffer = unicode.sub(displayText, 1, width)
206 local padding = string.rep(" ", width - unicode.len(displayBuffer))
207
208 gpu.setForeground(textColor)
209 -- Position label to the left of the input box
210 local labelX = x - unicode.len(label) - 1
211 if labelX < 1 then labelX = 1 end -- Ensure label is on screen
212 gpu.set(labelX, y, label)
213
214 gpu.setBackground(selected and accentColor or inputColor)
215 gpu.set(x, y, displayBuffer .. padding)
216
217 -- Draw cursor if selected
218 if selected and showCursor then
219 local cursorX = x + unicode.len(displayBuffer)
220 if cursorX < x + width then -- Ensure cursor stays within the input box width
221 gpu.setForeground(cursorColor)
222 gpu.set(cursorX, y, cursorChar)
223 end
224 end
225 gpu.setBackground(backgroundColor) -- Reset background color after drawing
226end
227
228local function drawButton(text, y, selected)
229 local width = unicode.len(text) + 4 -- Use unicode.len for text width
230 local x = math.floor((screenWidth - width) / 2)
231 if x < 1 then x = 1 end -- Ensure button is on screen
232 if x + width > screenWidth then width = screenWidth - x + 1 end -- Ensure button fits
233
234 gpu.setBackground(selected and accentColor or inputColor)
235 gpu.setForeground(textColor)
236 gpu.fill(x, y, width, 1, " ") -- Fill background with space
237 gpu.set(x + 2, y, text) -- Place text within the button
238 gpu.setBackground(backgroundColor) -- Reset background color
239end
240
241local function renderLogin()
242 gpu.setBackground(backgroundColor) -- Clear screen with background color
243 gpu.fill(1, 1, screenWidth, screenHeight, " ")
244 drawArt()
245 drawInput(inputX, usernameY, inputWidth, "Username:", username, selectedField == "username", false) -- Added colon for clarity
246 drawInput(inputX, passwordY, inputWidth, "Password:", password, selectedField == "password", not showPassword) -- Added colon for clarity
247 drawButton("Login", loginButtonY, selectedField == "login")
248 drawButton(showPassword and "Hide Password" or "Show Password", showPasswordButtonY, false)
249end
250
251-- --- WINDOWS and APP MANAGEMENT (Existing Code) ---
252local windows = {} -- Stores window objects and their associated app coroutines
253local windowOrder = {} -- List of window names in drawing order (Z-index)
254local focusedWindow = nil -- The name of the currently focused window
255
256local draggingWindow = nil
257local resizingWindow = nil
258
259local startMenuVisible = false
260
261-- --- APP API (Existing Code) ---
262-- This table will be provided to launched applications
263local os_window_api = {}
264
265function os_window_api.draw(winName, relX, relY, text, fgColor, bgColor)
266 local win = windows[winName]
267 if not win or win.minimized or not gpu then return end -- Check for GPU existence
268
269 -- Ensure coordinates are within the window content area
270 if relX >= 0 and relX < win.width - 2 and
271 relY >= 0 and relY < win.height - 2 then
272
273 local absX = win.x + 1 + relX
274 local absY = win.y + 1 + relY
275
276 -- Ensure absolute coordinates are within screen bounds
277 if absX >= 1 and absX <= screenWidth and absY >= 1 and absY <= screenHeight then
278
279 local oldFG, oldBG = gpu.getForeground(), gpu.getBackground()
280 if fgColor then gpu.setForeground(fgColor) end
281 if bgColor then gpu.setBackground(bgColor) end
282
283 gpu.set(absX, absY, text)
284
285 gpu.setForeground(oldFG)
286 gpu.setBackground(oldBG)
287 end
288 end
289end
290
291function os_window_api.getSize(winName)
292 local win = windows[winName]
293 if not win then return 0, 0 end
294 -- Return inner dimensions (excluding border/title)
295 return math.max(0, win.width - 2), math.max(0, win.height - 2)
296end
297
298function os_window_api.setTitle(winName, title)
299 local win = windows[winName]
300 if win then
301 win.title = title
302 end
303end
304
305-- --- OS EVENT ROUTING (Existing Code) ---
306local eventQueue = {}
307
308local function app_event_pull(winName, timeout)
309 local win = windows[winName]
310 if not win or win.appCoroutine ~= coroutine.running() then
311 print("Error: Invalid call to event.pull from app")
312 return nil
313 end
314 local eventData = coroutine.yield("pull_event", winName, timeout)
315 return table.unpack(eventData)
316end
317
318-- --- WINDOW MANAGEMENT FUNCTIONS (Existing Code) ---
319
320local function bringToFront(name)
321 if not windows[name] then return end
322 for i, winName in ipairs(windowOrder) do
323 if winName == name then
324 table.remove(windowOrder, i)
325 break
326 end
327 end
328 table.insert(windowOrder, name)
329 focusedWindow = name
330end
331
332local function drawWindowFrame(win)
333 if win.minimized or not gpu then return end -- Check for GPU
334
335 local titleBarColor = (focusedWindow == win.name) and accentColor or 0x555555
336 gpu.setBackground(titleBarColor)
337 gpu.fill(win.x, win.y, win.width, 1, " ")
338 gpu.setForeground(0xFFFFFF)
339 gpu.set(win.x + 1, win.y, unicode.sub(win.title, 1, win.width - 6))
340
341 local btnX = win.x + win.width - 2
342 local function drawBtn(sym, color)
343 if btnX >= win.x + 1 then -- Ensure button position is within window frame
344 gpu.setForeground(color)
345 gpu.set(btnX, win.y, sym)
346 btnX = btnX - 2
347 end
348 end
349 drawBtn("x", 0xFF0000) -- Close button
350 drawBtn("◉", 0x00FF00) -- Maximize button
351 drawBtn("○", 0xFFFF00) -- Minimize button
352
353 gpu.setBackground(0x999999) -- Default content background
354 for i = 1, win.height - 1 do
355 gpu.fill(win.x, win.y + i, win.width, 1, " ")
356 end
357end
358
359local function drawWindowContent(win)
360 if win.minimized or not win.appCoroutine or not gpu then return end -- Check for GPU
361
362 local status, err = coroutine.resume(win.appCoroutine, "draw")
363 if not status then
364 print("App '" .. win.name .. "' crashed during drawing: " .. tostring(err))
365 windows[win.name] = nil
366 focusedWindow = nil
367 for i, name in ipairs(windowOrder) do if name == win.name then table.remove(windowOrder, i) break end end
368 end
369end
370
371-- --- BUILT-IN APP DRAWING (Existing Code - will be replaced by actual apps) ---
372local function drawFilesystem(win)
373 if win.minimized or not gpu then return end -- Check for GPU
374 local path = win.path or "/"
375 local list = { ".." }
376 if fs.exists(path) and fs.isDirectory(path) then -- Check if path exists and is a directory
377 for name in fs.list(path) do
378 table.insert(list, name)
379 end
380 else
381 print("Warning: Filesystem path invalid:", path)
382 list = {".."} -- Just show .. if path is bad
383 end
384 table.sort(list)
385 win.listCache = list
386
387 local contentWidth, contentHeight = os_window_api.getSize(win.name)
388
389 for i = 1, math.min(#list, contentHeight) do
390 local entry = list[i]
391 local display = entry
392 os_window_api.draw(win.name, 0, i - 1, unicode.sub(display, 1, contentWidth)) -- Start at relX 0
393 end
394end
395
396local function drawAppsMenu(win)
397 if win.minimized or not gpu then return end -- Check for GPU
398 local appsPath = fs.concat(shell.getWorkingDirectory(), "Apps")
399 local appList = {}
400 if fs.exists(appsPath) and fs.isDirectory(appsPath) then
401 for name in fs.list(appsPath) do
402 local appPath = fs.concat(appsPath, name)
403 if fs.isDirectory(appPath) and fs.exists(fs.concat(appPath, "main.lua")) then
404 table.insert(appList, name)
405 end
406 end
407 end
408 table.sort(appList)
409 win.appListCache = appList
410
411 local contentWidth, contentHeight = os_window_api.getSize(win.name)
412
413 for i = 1, math.min(#appList, contentHeight) do
414 local appName = appList[i]
415 os_window_api.draw(win.name, 0, i - 1, unicode.sub(appName, 1, contentWidth)) -- Start at relX 0
416 end
417end
418-- --- END OF BUILT-IN APP DRAWING ---
419
420
421local function drawDesktop()
422 if not gpu then return end -- Check for GPU
423
424 gpu.setBackground(backgroundColor)
425 gpu.fill(1, 1, screenWidth, screenHeight - 1, " ") -- Clear everything except taskbar
426 gpu.setBackground(accentColor)
427 gpu.fill(1, screenHeight, screenWidth, 1, " ") -- Taskbar background
428 gpu.setForeground(textColor)
429 gpu.set(2, screenHeight, " Ω ") -- Start button
430 gpu.set(screenWidth - 4, screenHeight, " ⚙ ") -- Settings button
431
432 local x = 7
433 for name, win in pairs(windows) do
434 if win.minimized then
435 local label = "[" .. unicode.sub(win.title, 1, 20) .. "]"
436 gpu.setForeground(0xFFFFFF)
437 -- Ensure taskbar label is within bounds
438 if x + unicode.len(label) <= screenWidth then
439 gpu.set(x, screenHeight, label)
440 win.taskbarX = x
441 win.taskbarWidth = unicode.len(label)
442 x = x + win.taskbarWidth + 2
443 else
444 -- Stop adding labels if they go off screen
445 break
446 end
447 end
448 end
449
450 if startMenuVisible then
451 local menuW, menuH = 24, 6
452 local menuX, menuY = 2, screenHeight - menuH
453 -- Ensure menu is on screen
454 if menuX < 1 then menuX = 1 end
455 if menuY < 1 then menuY = 1 end
456 if menuX + menuW - 1 > screenWidth then menuW = screenWidth - menuX + 1 end
457 if menuY + menuH - 1 > screenHeight then menuH = screenHeight - menuY + 1 end
458
459
460 gpu.setBackground(0x222222)
461 gpu.fill(menuX, menuY, menuW, menuH, " ")
462 gpu.setForeground(0xFFFFFF)
463 gpu.set(menuX + 2, menuY, "Start Menu")
464 gpu.set(menuX + 2, menuY + 1, "1. Filesystem")
465 gpu.set(menuX + 2, menuY + 2, "2. Apps")
466 gpu.set(menuX + 2, menuY + 3, "3. User (TODO)")
467 gpu.set(menuX + 2, menuY + 4, "4. Logout")
468 gpu.set(menuX + 2, menuY + 5, "5. Exit")
469 end
470end
471
472local function renderDesktop()
473 drawDesktop()
474 for _, name in ipairs(windowOrder) do
475 local win = windows[name]
476 if win then
477 drawWindowFrame(win)
478 if win.appCoroutine then
479 drawWindowContent(win)
480 else
481 if name == "filesystem" then
482 drawFilesystem(win)
483 elseif name == "apps" then
484 drawAppsMenu(win)
485 end
486 end
487 end
488 end
489end
490
491
492-- --- APP LAUNCHING (Existing Code) ---
493local function launchApp(appName, appPath)
494 if windows[appName] then
495 bringToFront(appName)
496 return
497 end
498
499 local win = {
500 name = appName,
501 title = appName,
502 x = 10, y = 5, width = 60, height = 20,
503 minimized = false,
504 maximized = false,
505 path = "/", -- For filesystem browser (will move to app state)
506 appCoroutine = nil
507 }
508
509 local appEnv = {
510 print = function(...)
511 local args = {...}
512 local output = table.concat(args, "\t")
513 if gpu then -- Only print to screen if GPU exists
514 gpu.setForeground(0xFFFF00)
515 gpu.set(1, screenHeight - 2, "APP LOG [" .. appName .. "]: " .. unicode.sub(output, 1, screenWidth - 20))
516 gpu.setBackground(backgroundColor)
517 else -- Fallback to standard print if no GPU
518 _G.print("APP LOG [" .. appName .. "]: " .. output)
519 end
520 end,
521 math = math,
522 string = string,
523 table = table,
524 unicode = unicode,
525 shell = shell,
526 filesystem = fs,
527
528 window = {},
529 event = { pull = app_event_pull }
530 }
531
532 appEnv.window.draw = function(relX, relY, text, fgColor, bgColor)
533 os_window_api.draw(win.name, relX, relY, text, fgColor, bgColor)
534 end
535 appEnv.window.getSize = function() return os_window_api.getSize(win.name) end
536 appEnv.window.setTitle = function(title) os_window_api.setTitle(win.name, title) end
537
538 setmetatable(appEnv, {__index = _G})
539
540 local code, err = loadfile(appPath, "bt", appEnv)
541
542 if not code then
543 if gpu then -- Only print error on screen if GPU exists
544 gpu.setForeground(0xFF0000)
545 gpu.set(1, screenHeight - 2, "Error loading app '" .. appName .. "': " .. unicode.sub(tostring(err), 1, screenWidth - 20))
546 gpu.setBackground(backgroundColor)
547 else -- Fallback to standard print if no GPU
548 _G.print("Error loading app '" .. appName .. "': " .. tostring(err))
549 end
550 return
551 end
552
553 local appCo = coroutine.create(code)
554 win.appCoroutine = appCo
555
556 windows[win.name] = win
557 bringToFront(win.name)
558
559 local status, result = coroutine.resume(appCo, win.name, appEnv)
560
561 if not status then
562 if gpu then -- Only print crash message on screen if GPU exists
563 gpu.setForeground(0xFF0000)
564 gpu.set(1, screenHeight - 2, "App '" .. appName .. "' crashed during startup: " .. unicode.sub(tostring(result), 1, screenWidth - 20))
565 gpu.setBackground(backgroundColor)
566 else -- Fallback to standard print if no GPU
567 _G.print("App '" .. appName .. "' crashed during startup: " .. tostring(result))
568 end
569 windows[win.name] = nil
570 focusedWindow = nil
571 for i, name in ipairs(windowOrder) do if name == win.name then table.remove(windowOrder, i) break end end
572 end
573end
574
575
576local function desktopLoop()
577 renderDesktop()
578
579 while true do
580 local e = {event.pull()}
581 local eventType = e[1]
582
583 if focusedWindow and windows[focusedWindow] and windows[focusedWindow].appCoroutine and
584 coroutine.status(windows[focusedWindow].appCoroutine) == "suspended" then
585 local status, yield_type, app_win_name, timeout = coroutine.resume(windows[focusedWindow].appCoroutine)
586
587 if status and yield_type == "pull_event" and app_win_name == focusedWindow then
588 local event_should_route = false
589
590 if eventType == "touch" then
591 local _, _, x, y = table.unpack(e)
592 local win = windows[focusedWindow]
593 if win and x >= win.x + 1 and x < win.x + win.width - 1 and
594 y >= win.y + 1 and y < win.y + win.height - 1 then
595 event_should_route = true
596 end
597 elseif eventType == "key_down" or eventType == "key_up" then
598 event_should_route = true
599 else
600 event_should_route = true
601 end
602
603 if event_should_route then
604 local app_status, app_err = coroutine.resume(windows[focusedWindow].appCoroutine, table.unpack(e))
605 if not app_status then
606 if gpu then
607 gpu.setForeground(0xFF0000)
608 gpu.set(1, screenHeight - 2, "App '" .. focusedWindow .. "' crashed during event handling: " .. unicode.sub(tostring(app_err), 1, screenWidth - 20))
609 gpu.setBackground(backgroundColor)
610 else
611 _G.print("App '" .. focusedWindow .. "' crashed during event handling: " .. tostring(app_err))
612 end
613 windows[focusedWindow] = nil
614 focusedWindow = nil
615 for i, name in ipairs(windowOrder) do if name == focusedWindow then table.remove(windowOrder, i) break end end
616 end
617 end
618 end
619 end
620
621
622 if eventType == "touch" then
623 local _, _, x, y = table.unpack(e)
624
625 if y == screenHeight then
626 if x >= 2 and x <= 5 then
627 startMenuVisible = not startMenuVisible
628 elseif gpu and x >= screenWidth - 4 then -- Check settings button area and GPU existence
629 launchApp("Settings", "Omnia/Apps/settings/main.lua")
630 startMenuVisible = false
631 else
632 for winName, win in pairs(windows) do
633 if win.minimized and win.taskbarX and win.taskbarWidth then
634 if x >= win.taskbarX and x < win.taskbarX + win.taskbarWidth then
635 win.minimized = false
636 bringToFront(winName)
637 startMenuVisible = false
638 break
639 end
640 end
641 end
642 end
643 elseif startMenuVisible then
644 local menuW, menuH = 24, 6
645 local menuX, menuY = 2, screenHeight - menuH
646 if x >= menuX and x < menuX + menuW and y >= menuY and y < menuY + menuH then
647 if y == menuY + 1 then
648 launchApp("Filesystem", "Omnia/Apps/filesystem/main.lua")
649 elseif y == menuY + 2 then
650 launchApp("Apps", "Omnia/Apps/appsmenu/main.lua")
651 elseif y == menuY + 3 then
652 if gpu then gpu.setForeground(0xFFFF00) gpu.set(1, screenHeight - 2, "User management TODO") gpu.setBackground(backgroundColor) end
653 elseif y == menuY + 4 then
654 return
655 elseif y == menuY + 5 then
656 computer.shutdown()
657 end
658 startMenuVisible = false
659 else
660 startMenuVisible = false
661 end
662 else
663 local clickedWinName = nil
664 for i = #windowOrder, 1, -1 do
665 local winName = windowOrder[i]
666 local win = windows[winName]
667 if win and not win.minimized and
668 x >= win.x and x < win.x + win.width and
669 y >= win.y and y < win.y + win.height then
670
671 clickedWinName = winName
672 break
673 end
674 end
675
676 if clickedWinName then
677 bringToFront(clickedWinName)
678 focusedWindow = clickedWinName
679
680 local win = windows[clickedWinName]
681 if y == win.y then
682 if gpu then -- Only handle button clicks if GPU exists
683 local closeX = win.x + win.width - 2
684 local maxX = closeX - 2
685 local minX = maxX - 2
686
687 if x == closeX then
688 if win.appCoroutine then
689 coroutine.yield(win.appCoroutine, "quit")
690 windows[clickedWinName] = nil
691 focusedWindow = nil
692 for i, name in ipairs(windowOrder) do if name == clickedWinName then table.remove(windowOrder, i) break end end
693 else
694 windows[clickedWinName] = nil
695 focusedWindow = nil
696 for i, name in ipairs(windowOrder) do if name == clickedWinName then table.remove(windowOrder, i) break end end
697 end
698 elseif x == maxX then
699 win.maximized = not win.maximized
700 if win.maximized then
701 win.prevX, win.prevY, win.prevW, win.prevH = win.x, win.y, win.width, win.height
702 win.x, win.y = 1, 1
703 win.width, win.height = screenWidth, screenHeight - 1
704 else
705 win.x, win.y = win.prevX or 10, win.prevY or 5
706 win.width, win.height = win.prevW or 60, win.prevH or 20
707 end
708 elseif x == minX then
709 win.minimized = true
710 focusedWindow = nil
711 else
712 draggingWindow = clickedWinName
713 win.dragOffsetX = x - win.x
714 win.dragOffsetY = y - win.y
715 end
716 end
717 elseif gpu and x == win.x + win.width - 1 and y == win.y + win.height - 1 then -- Check resize handle and GPU existence
718 resizingWindow = clickedWinName
719 else
720 if win.appCoroutine and coroutine.status(win.appCoroutine) == "suspended" then
721 -- App event routing is handled earlier
722 else
723 if win.name == "filesystem" then
724 local index = y - win.y - 1
725 local selected = win.listCache and win.listCache[index + 1]
726 if selected then
727 if fs.exists(fs.concat(win.path, selected)) then -- Check if the entry exists
728 if selected == ".." then
729 local parent = fs.path(fs.concat(win.path, ".."))
730 if fs.exists(parent) then
731 win.path = fs.canonical(parent)
732 end
733 else
734 local fullPath = fs.concat(win.path, selected)
735 if fs.isDirectory(fullPath) then
736 win.path = fs.canonical(fullPath)
737 else
738 if gpu then gpu.setForeground(0x00FF00) gpu.set(1, screenHeight - 2, "Selected file: " .. unicode.sub(fullPath, 1, screenWidth - 20)) gpu.setBackground(backgroundColor) end
739 end
740 end
741 end
742 end
743 elseif win.name == "apps" then
744 local index = y - win.y - 1
745 local selectedApp = win.appListCache and win.appListCache[index + 1]
746 if selectedApp then
747 local appPath = fs.concat(shell.getWorkingDirectory(), "Apps", selectedApp, "main.lua")
748 launchApp(selectedApp, appPath)
749 end
750 end
751 end
752 end
753 end
754 end
755
756 elseif eventType == "drag" then
757 if (draggingWindow and windows[draggingWindow]) then
758 local win = windows[draggingWindow]
759 -- Clamp dragging within screen bounds, leaving space for taskbar
760 win.x = math.max(1, math.min(screenWidth - win.width + 1, x - win.dragOffsetX))
761 win.y = math.max(1, math.min(screenHeight - 1 - win.height + 1, y - win.dragOffsetY))
762
763 elseif (resizingWindow and windows[resizingWindow]) then
764 local win = windows[resizingWindow]
765 -- Clamp resizing to minimum size and screen bounds
766 win.width = math.max(10, math.min(screenWidth - win.x + 1, x - win.x + 1))
767 win.height = math.max(5, math.min(screenHeight - 1 - win.y + 1, y - win.y + 1))
768 end
769 elseif eventType == "drop" then
770 draggingWindow, resizingWindow = nil, nil
771 if focusedWindow and windows[focusedWindow] and windows[focusedWindow].appCoroutine and
772 coroutine.status(windows[focusedWindow].appCoroutine) == "suspended" then
773 local status, yield_type, app_win_name, timeout = coroutine.resume(windows[focusedWindow].appCoroutine)
774 if status and yield_type == "pull_event" and app_win_name == focusedWindow then
775 local app_status, app_err = coroutine.resume(windows[focusedWindow].appCoroutine, table.unpack(e))
776 if not app_status then
777 if gpu then gpu.setForeground(0xFF0000) gpu.set(1, screenHeight - 2, "App '" .. focusedWindow .. "' crashed during drop handling: " .. unicode.sub(tostring(app_err), 1, screenWidth - 20)) gpu.setBackground(backgroundColor) end
778 windows[focusedWindow] = nil
779 focusedWindow = nil
780 for i, name in ipairs(windowOrder) do if name == focusedWindow then table.remove(windowOrder, i) break end end
781 end
782 end
783 end
784 elseif eventType == "key_down" or eventType == "key_up" then
785 if focusedWindow and windows[focusedWindow] and windows[focusedWindow].appCoroutine and
786 coroutine.status(windows[focusedWindow].appCoroutine) == "suspended" then
787 local status, yield_type, app_win_name, timeout = coroutine.resume(windows[focusedWindow].appCoroutine)
788 if status and yield_type == "pull_event" and app_win_name == focusedWindow then
789 local app_status, app_err = coroutine.resume(windows[focusedWindow].appCoroutine, table.unpack(e))
790 if not app_status then
791 if gpu then gpu.setForeground(0xFF0000) gpu.set(1, screenHeight - 2, "App '" .. focusedWindow .. "' crashed during key event handling: " .. unicode.sub(tostring(app_err), 1, screenWidth - 20)) gpu.setBackground(backgroundColor) end
792 windows[focusedWindow] = nil
793 focusedWindow = nil
794 for i, name in ipairs(windowOrder) do if name == focusedWindow then table.remove(windowOrder, i) break end end
795 end
796 end
797 end
798 end
799
800 renderDesktop()
801 end
802end
803
804
805-- --- LOGIN LOOP ---
806while true do
807 -- Use event.pull with timeout for cursor blinking
808 local e = {event.pull(0.5)}
809 local eventType = e[1]
810
811 if not eventType then
812 -- Timeout occurred (0.5 seconds), toggle cursor visibility
813 showCursor = not showCursor
814 renderLogin()
815 elseif eventType == "key_down" then
816 local char, keyCode = e[3], e[4]
817 if keyCode == 28 then -- Enter key
818 if selectedField == "username" then
819 selectedField = "password"
820 elseif selectedField == "password" then
821 selectedField = "login"
822 elseif selectedField == "login" then
823 if users[username] and users[username] == password then
824 if gpu then gpu.setBackground(backgroundColor) term.clear() end -- Clear before switching
825 if gpu then gpu.setForeground(0x00FF00) end
826 print("\nLogin successful!") -- print works even without GPU
827 os.sleep(1)
828 desktopLoop() -- Enter the desktop environment
829 if gpu then gpu.setBackground(backgroundColor) term.clear() end -- Clear desktop artifacts
830 username, password = "", "" -- Reset for next login attempt
831 selectedField = "username"
832 else
833 if gpu then gpu.setForeground(0xFF0000) end
834 print("\nInvalid username or password.")
835 os.sleep(2)
836 username, password = "", ""
837 selectedField = "username"
838 renderLogin() -- Redraw login after message
839 end
840 end
841 elseif keyCode == 14 then -- Backspace
842 if selectedField == "username" then
843 username = unicode.sub(username, 1, -2) or ""
844 elseif selectedField == "password" then
845 password = unicode.sub(password, 1, -2) or ""
846 end
847 elseif char > 0 then -- Printable character
848 local ch = unicode.char(char)
849 if selectedField == "username" and unicode.len(username) < inputWidth then
850 username = username .. ch
851 elseif selectedField == "password" and unicode.len(password) < inputWidth then
852 password = password .. ch
853 end
854 end
855 renderLogin() -- Redraw after key press
856
857 elseif eventType == "touch" then
858 local x, y = e[3], e[4]
859 if y == usernameY then
860 selectedField = "username"
861 elseif y == passwordY then
862 selectedField = "password"
863 elseif y == loginButtonY then
864 if users[username] and users[username] == password then
865 if gpu then gpu.setBackground(backgroundColor) term.clear() end
866 if gpu then gpu.setForeground(0x00FF00) end
867 print("\nLogin successful!")
868 os.sleep(1)
869 desktopLoop()
870 if gpu then gpu.setBackground(backgroundColor) term.clear() end
871 username, password = "", ""
872 selectedField = "username"
873 else
874 if gpu then gpu.setForeground(0xFF0000) end
875 print("\nInvalid username or password.")
876 os.sleep(2)
877 username, password = "", ""
878 selectedField = "username"
879 end
880 elseif y == showPasswordButtonY then
881 showPassword = not showPassword
882 end
883 renderLogin()
884
885 else
886 renderLogin()
887 end
888end
889