luajitos

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

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