shell.lua (20016B)
1 -- Create windowed shell (400x200) 2 local window = app:newWindow(400, 200) 3 if osprint then 4 osprint("[SHELL] Window type: " .. type(window) .. "\n") 5 osprint("[SHELL] Window is table: " .. tostring(type(window) == "table") .. "\n") 6 osprint("[SHELL] Window.isBackground: " .. tostring(window.isBackground) .. "\n") 7 end 8 local text = "LuajitOS Shell\n> " 9 local current_line = "" 10 local i = 0 11 local scroll_offset = 0 -- Number of lines to scroll up (0 = show bottom) 12 13 -- Track error line ranges (pairs of {start_line, end_line} in the wrapped output) 14 local error_lines = {} -- List of lines that are errors (by content prefix) 15 local cwd = "/home" -- Current working directory (local to shell), ~ = /home 16 17 -- Get app path for $ expansion (shell's own app path) 18 local appPath = app and app.path or "/apps/com.luajitos.shell" 19 20 -- Centralized path expansion function 21 -- Expands: ~ -> /home, $ -> current app path, relative paths -> cwd-based 22 local function expandPath(path) 23 if not path or path == "" then 24 return cwd 25 end 26 27 -- Trim whitespace 28 path = path:match("^%s*(.-)%s*$") 29 30 -- Expand ~ to /home 31 if path == "~" then 32 return "/home" 33 elseif path:sub(1, 2) == "~/" then 34 path = "/home" .. path:sub(2) 35 elseif path:sub(1, 1) == "~" then 36 -- ~something without slash - treat as /home/something 37 path = "/home/" .. path:sub(2) 38 end 39 40 -- Expand $ to app path 41 if path == "$" then 42 return appPath 43 elseif path:sub(1, 2) == "$/" then 44 path = appPath .. path:sub(2) 45 elseif path:sub(1, 1) == "$" then 46 -- $something without slash - treat as appPath/something 47 path = appPath .. "/" .. path:sub(2) 48 end 49 50 -- Handle absolute paths (already done after ~ and $ expansion) 51 if path:sub(1, 1) == "/" then 52 return path 53 end 54 55 -- Handle .. (go up one directory) 56 if path == ".." then 57 local parts = {} 58 for part in cwd:gmatch("[^/]+") do 59 table.insert(parts, part) 60 end 61 table.remove(parts) 62 local result = "/" .. table.concat(parts, "/") 63 return result == "" and "/" or result 64 end 65 66 -- Handle . (current directory) 67 if path == "." then 68 return cwd 69 end 70 71 -- Handle paths starting with ../ 72 if path:sub(1, 3) == "../" then 73 local parts = {} 74 for part in cwd:gmatch("[^/]+") do 75 table.insert(parts, part) 76 end 77 table.remove(parts) 78 local parentDir = "/" .. table.concat(parts, "/") 79 if parentDir == "" then parentDir = "/" end 80 local rest = path:sub(4) 81 if parentDir == "/" then 82 return "/" .. rest 83 else 84 return parentDir .. "/" .. rest 85 end 86 end 87 88 -- Handle paths starting with ./ 89 if path:sub(1, 2) == "./" then 90 path = path:sub(3) 91 end 92 93 -- Relative path - prepend cwd 94 if cwd == "/" then 95 return "/" .. path 96 else 97 return cwd .. "/" .. path 98 end 99 end 100 101 -- Helper function to deep copy tables (for isolation) 102 local function deepCopy(original, seen) 103 if type(original) ~= "table" then 104 return original 105 end 106 107 -- Handle circular references 108 seen = seen or {} 109 if seen[original] then 110 return seen[original] 111 end 112 113 local copy = {} 114 seen[original] = copy 115 116 for k, v in pairs(original) do 117 copy[deepCopy(k, seen)] = deepCopy(v, seen) 118 end 119 120 return copy 121 end 122 123 -- Cache for deep copied library tables 124 local cachedLibraries = { 125 string = nil, 126 table = nil, 127 math = nil, 128 bit = nil, 129 crypto = nil, 130 apps = nil, 131 Image = nil 132 } 133 134 -- Initialize cached libraries once 135 local function initCachedLibraries() 136 if not cachedLibraries.string then 137 cachedLibraries.string = deepCopy(string) 138 end 139 if not cachedLibraries.table then 140 cachedLibraries.table = deepCopy(table) 141 end 142 if not cachedLibraries.math then 143 cachedLibraries.math = deepCopy(math) 144 end 145 if not cachedLibraries.bit then 146 cachedLibraries.bit = deepCopy(bit) 147 end 148 -- Note: crypto, apps, Image are optional - only cache if available in sandbox 149 -- These are provided via the sandbox_env table, not as undefined globals 150 end 151 152 -- Initialize on first load 153 initCachedLibraries() 154 155 -- Command execution function 156 local function executeCommand(cmd) 157 cmd = cmd:match("^%s*(.-)%s*$") -- Trim whitespace 158 if cmd == "" then 159 return "" 160 end 161 162 -- Built-in commands 163 if cmd == "help" then 164 return "Commands: help, clear, run <app>, ls [dir], echo <text>, cd <dir>, cwd, read <file>, write <file> <text>, open <file>" 165 elseif cmd == "clear" then 166 text = "LuajitOS Shell\n> " 167 current_line = "" 168 return nil -- Special: don't add output 169 elseif cmd == "cwd" then 170 -- Print current working directory 171 return cwd 172 elseif cmd:sub(1, 3) == "cd " or cmd == "cd" then 173 -- Change directory using expandPath 174 local dir = cmd:sub(4) 175 cwd = expandPath(dir) 176 return "" 177 elseif cmd:sub(1, 4) == "run " then 178 local app_name = cmd:sub(5) 179 if run then 180 local success, result = pcall(run, app_name) 181 if success and result then 182 return "Started: " .. app_name 183 else 184 return "Error: " .. tostring(result) 185 end 186 else 187 return "Error: run function not available" 188 end 189 elseif cmd:sub(1, 5) == "echo " then 190 return cmd:sub(6) 191 elseif cmd:sub(1, 5) == "read " then 192 -- Read file contents via clitools 193 local filepath = expandPath(cmd:sub(6)) 194 local clitools = apps and apps["com.luajitos.clitools"] 195 if clitools and clitools.read then 196 local content, err = clitools:call("read", filepath) 197 if content then 198 return content 199 else 200 return "Error: " .. tostring(err) 201 end 202 else 203 return "Error: clitools not available" 204 end 205 elseif cmd:sub(1, 6) == "write " then 206 -- Write to file via clitools 207 -- Parse: write <file> <text> 208 local rest = cmd:sub(7) 209 local spacePos = rest:find(" ") 210 211 if not spacePos then 212 return "Error: usage: write <file> <text>" 213 end 214 215 local filepath = expandPath(rest:sub(1, spacePos - 1)) 216 local content = rest:sub(spacePos + 1) 217 218 -- Process escape sequences in content 219 -- Handle quotes 220 if content:sub(1, 1) == "'" and content:sub(-1) == "'" then 221 content = content:sub(2, -2) 222 elseif content:sub(1, 1) == '"' and content:sub(-1) == '"' then 223 content = content:sub(2, -2) 224 end 225 226 -- Process escape sequences 227 content = content:gsub("\\n", "\n") 228 content = content:gsub("\\t", "\t") 229 content = content:gsub("\\r", "\r") 230 content = content:gsub("\\\\", "\\") 231 232 local clitools = apps and apps["com.luajitos.clitools"] 233 if clitools then 234 local result, err = clitools:call("write", filepath, content) 235 if result then 236 return result 237 else 238 return "Error: " .. tostring(err) 239 end 240 else 241 return "Error: clitools not available" 242 end 243 elseif cmd:sub(1, 5) == "open " then 244 -- Open file with registered handler via clitools 245 local filepath = expandPath(cmd:sub(6)) 246 local clitools = apps and apps["com.luajitos.clitools"] 247 if clitools then 248 local result, err = clitools:call("open", filepath) 249 if result then 250 return result 251 else 252 return "Error: " .. tostring(err) 253 end 254 else 255 return "Error: clitools not available" 256 end 257 elseif cmd == "ls" or cmd:sub(1, 3) == "ls " then 258 -- List files in directory via clitools 259 local targetDir = nil 260 if cmd:sub(1, 3) == "ls " then 261 targetDir = cmd:sub(4) 262 end 263 264 local listPath = expandPath(targetDir) 265 local clitools = apps and apps["com.luajitos.clitools"] 266 if clitools then 267 local result, err = clitools:call("ls", listPath) 268 if result then 269 return result 270 else 271 return "Error: " .. tostring(err) 272 end 273 else 274 return "Error: filesystem not available" 275 end 276 else 277 -- Try to run as an app with arguments 278 -- Parse: first word is app name, rest are arguments 279 local spacePos = cmd:find(" ") 280 local appName = spacePos and cmd:sub(1, spacePos - 1) or cmd 281 local argString = spacePos and cmd:sub(spacePos + 1) or "" 282 283 -- Check if app exists by trying to get its manifest 284 if GetManifest and run then 285 local manifest = GetManifest(appName) 286 287 -- If not found, try with com.luajitos. prefix 288 if not manifest and not appName:match("%.") then 289 local fullAppName = "com.luajitos." .. appName 290 manifest = GetManifest(fullAppName) 291 if manifest then 292 appName = fullAppName 293 end 294 end 295 296 if manifest then 297 -- App exists, run it with arguments 298 local pcall_success, run_success, run_result = pcall(run, appName, argString) 299 if pcall_success and run_success and run_result then 300 -- Check if app has CLI output 301 if run_result.cli and run_result.cli.getText then 302 local cli_output = run_result.cli.getText() 303 if cli_output and cli_output ~= "" then 304 return cli_output 305 end 306 end 307 return "Started: " .. appName 308 else 309 if not pcall_success then 310 -- pcall itself failed 311 return "Error: " .. tostring(run_success) 312 elseif not run_success then 313 -- run returned false with error message 314 return "Error: " .. tostring(run_result) 315 else 316 return "Error: Failed to run " .. appName 317 end 318 end 319 end 320 end 321 322 -- Check if it's a .lua script file 323 if cmd:match("%.lua$") then 324 local scriptName = cmd 325 -- First try expanded path (allows ~/scripts/test.lua, ./test.lua, etc) 326 local scriptPath = expandPath(scriptName) 327 -- Fall back to /scripts/ if not found 328 if CRamdiskExists and not CRamdiskExists(scriptPath) then 329 scriptPath = "/scripts/" .. scriptName:match("([^/]+)$") 330 end 331 332 if CRamdiskExists and CRamdiskExists(scriptPath) then 333 local handle = CRamdiskOpen(scriptPath, "r") 334 if handle then 335 local scriptContent = CRamdiskRead(handle) 336 CRamdiskClose(handle) 337 338 if scriptContent then 339 local tmpScriptPath = "/tmp/" .. scriptName 340 341 if CRamdiskOpen and CRamdiskWrite and CRamdiskClose then 342 local writeHandle = CRamdiskOpen(tmpScriptPath, "w") 343 if writeHandle then 344 CRamdiskWrite(writeHandle, scriptContent) 345 CRamdiskClose(writeHandle) 346 347 if CRamdiskExists and not CRamdiskExists(tmpScriptPath) then 348 return "Error: Script was not created in /tmp" 349 end 350 else 351 return "Error: Could not create temp script file" 352 end 353 else 354 return "Error: Ramdisk functions not available" 355 end 356 357 if run then 358 local pcall_success, run_success, run_result = pcall(run, tmpScriptPath) 359 if pcall_success and run_success and run_result then 360 if run_result.cli and run_result.cli.getText then 361 local cli_output = run_result.cli.getText() 362 if cli_output and cli_output ~= "" then 363 return cli_output 364 end 365 end 366 return "Script executed: " .. scriptName 367 else 368 if not pcall_success then 369 -- pcall itself failed 370 return "Error: " .. tostring(run_success) 371 elseif not run_success then 372 -- run returned false with error message 373 return "Error: " .. tostring(run_result) 374 else 375 return "Error: Failed to run script" 376 end 377 end 378 else 379 return "Error: run function not available" 380 end 381 else 382 return "Error: Could not read script content" 383 end 384 else 385 return "Error: Could not open script" 386 end 387 else 388 return "Error: Script not found: " .. scriptPath 389 end 390 end 391 392 -- Not an app or script - try interpreting as Lua code 393 local output_buffer = {} 394 395 local sandbox = { 396 print = function(...) 397 local result = {} 398 for i = 1, select('#', ...) do 399 table.insert(result, tostring(select(i, ...))) 400 end 401 table.insert(output_buffer, table.concat(result, "\t")) 402 end, 403 tonumber = tonumber, 404 tostring = tostring, 405 type = type, 406 pairs = pairs, 407 ipairs = ipairs, 408 next = next, 409 select = select, 410 string = cachedLibraries.string, 411 table = cachedLibraries.table, 412 math = cachedLibraries.math, 413 bit = cachedLibraries.bit, 414 os = { 415 date = os.date, 416 time = os.time, 417 difftime = os.difftime, 418 clock = os.clock, 419 } 420 } 421 422 if cachedLibraries.crypto then 423 sandbox.crypto = cachedLibraries.crypto 424 end 425 if cachedLibraries.apps then 426 sandbox.apps = cachedLibraries.apps 427 end 428 if cachedLibraries.Image then 429 sandbox.Image = cachedLibraries.Image 430 end 431 432 local lua_func, err 433 local ok = pcall(function() 434 lua_func, err = loadstring("return " .. cmd, "shell") 435 end) 436 437 if not ok or not lua_func then 438 ok = pcall(function() 439 lua_func, err = loadstring(cmd, "shell") 440 end) 441 if not ok then 442 return "Error compiling code" 443 end 444 end 445 446 if lua_func then 447 setfenv(lua_func, sandbox) 448 local exec_success, result = pcall(lua_func) 449 if exec_success then 450 local output = table.concat(output_buffer, "\n") 451 if result ~= nil then 452 if output ~= "" then 453 return output .. "\n" .. tostring(result) 454 else 455 return tostring(result) 456 end 457 else 458 return output 459 end 460 else 461 return "Error: " .. tostring(result) 462 end 463 else 464 return "Syntax error: " .. tostring(err) 465 end 466 end 467 end 468 469 -- Word wrap helper function 470 local function wrapText(str, maxWidth) 471 local charsPerLine = math.floor(maxWidth / 8) 472 local lines = {} 473 474 for line in (str.."\n"):gmatch("([^\n]*)\n") do 475 if #line <= charsPerLine then 476 table.insert(lines, line) 477 else 478 local words = {} 479 for word in line:gmatch("%S+") do 480 table.insert(words, word) 481 end 482 483 local currentLine = "" 484 for _, word in ipairs(words) do 485 if #word > charsPerLine then 486 if #currentLine > 0 then 487 table.insert(lines, currentLine) 488 currentLine = "" 489 end 490 local pos = 1 491 while pos <= #word do 492 local chunk = word:sub(pos, pos + charsPerLine - 1) 493 table.insert(lines, chunk) 494 pos = pos + charsPerLine 495 end 496 elseif #currentLine == 0 then 497 currentLine = word 498 elseif #currentLine + #word + 1 <= charsPerLine then 499 currentLine = currentLine .. " " .. word 500 else 501 table.insert(lines, currentLine) 502 currentLine = word 503 end 504 end 505 506 if #currentLine > 0 then 507 table.insert(lines, currentLine) 508 end 509 end 510 end 511 512 return lines 513 end 514 515 -- Set up draw callback 516 window.onDraw = function(gfx) 517 -- Fill background (black) 518 gfx:fillRect(0, 0, 400, 200, 0x000000) 519 520 -- Draw shell text with word wrapping (green on black) 521 local fullText = text .. current_line 522 local wrappedLines = wrapText(fullText, 380) 523 524 local y = 10 525 local lineHeight = 12 526 local maxLines = math.floor((200 - 20) / lineHeight) 527 528 local totalLines = #wrappedLines 529 local bottomLine = totalLines - scroll_offset 530 local startLine = math.max(1, bottomLine - maxLines + 1) 531 local endLine = math.min(totalLines, bottomLine) 532 533 for i = startLine, endLine do 534 local line = wrappedLines[i] 535 local color = 0x00FF00 -- Default green 536 537 -- Check if this line is an error (starts with "Error:" or is continuation of error) 538 if line:match("^Error:") or line:match("^%s+at ") or line:match("^%s+in ") then 539 color = 0xFF4444 -- Red for errors 540 end 541 542 gfx:drawText(10, y, line, color) 543 y = y + lineHeight 544 end 545 end 546 547 -- Set up input callback 548 window.onInput = function(key, scancode) 549 i = 0 550 551 -- Handle arrow keys for scrolling (scancodes: up=72, down=80) 552 local baseScancode = scancode % 128 553 554 if baseScancode == 72 then 555 scroll_offset = scroll_offset + 1 556 window:markDirty() 557 return 558 elseif baseScancode == 80 then 559 scroll_offset = math.max(0, scroll_offset - 1) 560 window:markDirty() 561 return 562 end 563 564 -- Regular key handling 565 if key == "\b" then 566 if #current_line > 0 then 567 current_line = current_line:sub(1, -2) 568 window:markDirty() 569 end 570 elseif key == "\n" then 571 local output = executeCommand(current_line) 572 573 if output == nil then 574 window:markDirty() 575 return 576 end 577 578 text = text .. current_line .. "\n" 579 if output ~= "" then 580 text = text .. output .. "\n" 581 end 582 text = text .. "> " 583 current_line = "" 584 scroll_offset = 0 585 window:markDirty() 586 else 587 current_line = current_line .. key 588 window:markDirty() 589 end 590 end 591 592 if osprint then 593 osprint("[SHELL] After setting onInput, type(window.onInput) = " .. type(window.onInput) .. "\n") 594 end