mirror of
https://github.com/kepler155c/opus
synced 2024-12-27 09:00:41 +00:00
636 lines
16 KiB
Plaintext
636 lines
16 KiB
Plaintext
local parentShell = shell
|
|
|
|
local shell = { }
|
|
|
|
local sandboxEnv = setmetatable({ }, { __index = _G })
|
|
for k,v in pairs(getfenv(1)) do
|
|
sandboxEnv[k] = v
|
|
end
|
|
sandboxEnv.shell = shell
|
|
sandboxEnv.multishell = multishell or { }
|
|
|
|
requireInjector(getfenv(1))
|
|
|
|
local Util = require('util')
|
|
|
|
local DIR = (parentShell and parentShell.dir()) or ""
|
|
local PATH = (parentShell and parentShell.path()) or ".:/rom/programs"
|
|
local ALIASES = (parentShell and parentShell.aliases()) or {}
|
|
local tCompletionInfo = (parentShell and parentShell.getCompletionInfo()) or {}
|
|
|
|
local bExit = false
|
|
local tProgramStack = {}
|
|
|
|
local function parseCommandLine( ... )
|
|
local sLine = table.concat( { ... }, " " )
|
|
local tWords = {}
|
|
local bQuoted = false
|
|
for match in string.gmatch( sLine .. "\"", "(.-)\"" ) do
|
|
if bQuoted then
|
|
table.insert( tWords, match )
|
|
else
|
|
for m in string.gmatch( match, "[^ \t]+" ) do
|
|
table.insert( tWords, m )
|
|
end
|
|
end
|
|
bQuoted = not bQuoted
|
|
end
|
|
|
|
return table.remove(tWords, 1), tWords
|
|
end
|
|
|
|
-- Install shell API
|
|
function shell.run(...)
|
|
|
|
local path, args = parseCommandLine(...)
|
|
local isUrl = not not path:match("^(https?:)//(([^/:]+):?([0-9]*))(/?.*)$")
|
|
|
|
if not isUrl then
|
|
path = shell.resolveProgram(path)
|
|
end
|
|
|
|
if path then
|
|
tProgramStack[#tProgramStack + 1] = path
|
|
local oldTitle
|
|
|
|
if multishell and multishell.getTitle then
|
|
oldTitle = multishell.getTitle(multishell.getCurrent())
|
|
multishell.setTitle(multishell.getCurrent(), fs.getName(path))
|
|
end
|
|
|
|
local result, err
|
|
|
|
if isUrl then
|
|
local env = Util.shallowCopy(sandboxEnv)
|
|
setmetatable(env, { __index = _G })
|
|
result, err = Util.runUrl(env, path, unpack(args))
|
|
else
|
|
result, err = os.run(Util.shallowCopy(sandboxEnv), path, unpack(args))
|
|
end
|
|
|
|
if multishell and multishell.getTitle then
|
|
local title = 'shell'
|
|
if #tProgramStack > 0 then
|
|
title = fs.getName(tProgramStack[#tProgramStack])
|
|
end
|
|
multishell.setTitle(multishell.getCurrent(), oldTitle or 'shell')
|
|
end
|
|
|
|
return result, err
|
|
end
|
|
return false, 'No such program'
|
|
end
|
|
|
|
function shell.exit()
|
|
bExit = true
|
|
end
|
|
|
|
function shell.dir() return DIR end
|
|
function shell.setDir(d) DIR = d end
|
|
function shell.path() return PATH end
|
|
function shell.setPath(p) PATH = p end
|
|
|
|
function shell.resolve( _sPath )
|
|
local sStartChar = string.sub( _sPath, 1, 1 )
|
|
if sStartChar == "/" or sStartChar == "\\" then
|
|
return fs.combine( "", _sPath )
|
|
else
|
|
return fs.combine(DIR, _sPath )
|
|
end
|
|
end
|
|
|
|
function shell.resolveProgram( _sCommand )
|
|
|
|
if ALIASES[ _sCommand ] ~= nil then
|
|
_sCommand = ALIASES[ _sCommand ]
|
|
end
|
|
|
|
local path = shell.resolve(_sCommand)
|
|
if fs.exists(path) and not fs.isDir(path) then
|
|
return path
|
|
end
|
|
if fs.exists(path .. '.lua') then
|
|
return path .. '.lua'
|
|
end
|
|
|
|
-- If the path is a global path, use it directly
|
|
local sStartChar = string.sub( _sCommand, 1, 1 )
|
|
if sStartChar == "/" or sStartChar == "\\" then
|
|
local sPath = fs.combine( "", _sCommand )
|
|
if fs.exists( sPath ) and not fs.isDir( sPath ) then
|
|
return sPath
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- Otherwise, look on the path variable
|
|
for sPath in string.gmatch(PATH or '', "[^:]+") do
|
|
sPath = fs.combine(sPath, _sCommand )
|
|
if fs.exists( sPath ) and not fs.isDir( sPath ) then
|
|
return sPath
|
|
end
|
|
if fs.exists(sPath .. '.lua') then
|
|
return sPath .. '.lua'
|
|
end
|
|
end
|
|
|
|
-- Not found
|
|
return nil
|
|
end
|
|
|
|
function shell.programs( _bIncludeHidden )
|
|
local tItems = {}
|
|
|
|
-- Add programs from the path
|
|
for sPath in string.gmatch(PATH, "[^:]+") do
|
|
sPath = shell.resolve(sPath)
|
|
if fs.isDir( sPath ) then
|
|
local tList = fs.list( sPath )
|
|
for n,sFile in pairs( tList ) do
|
|
if not fs.isDir( fs.combine( sPath, sFile ) ) and
|
|
(_bIncludeHidden or string.sub( sFile, 1, 1 ) ~= ".") then
|
|
tItems[ sFile ] = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Sort and return
|
|
local tItemList = {}
|
|
for sItem, b in pairs( tItems ) do
|
|
table.insert( tItemList, sItem )
|
|
end
|
|
table.sort( tItemList )
|
|
return tItemList
|
|
end
|
|
|
|
function shell.complete(sLine) end
|
|
function shell.completeProgram(sProgram) end
|
|
|
|
function shell.setCompletionFunction(sProgram, fnComplete)
|
|
tCompletionInfo[sProgram] = { fnComplete = fnComplete }
|
|
end
|
|
|
|
function shell.getCompletionInfo()
|
|
return tCompletionInfo
|
|
end
|
|
|
|
function shell.getRunningProgram()
|
|
return tProgramStack[#tProgramStack]
|
|
end
|
|
|
|
function shell.setAlias( _sCommand, _sProgram )
|
|
ALIASES[ _sCommand ] = _sProgram
|
|
end
|
|
|
|
function shell.clearAlias( _sCommand )
|
|
ALIASES[ _sCommand ] = nil
|
|
end
|
|
|
|
function shell.aliases()
|
|
local tCopy = {}
|
|
for sAlias, sCommand in pairs(ALIASES) do
|
|
tCopy[sAlias] = sCommand
|
|
end
|
|
return tCopy
|
|
end
|
|
|
|
function shell.newTab(tabInfo, ...)
|
|
local path, args = parseCommandLine(...)
|
|
path = shell.resolveProgram(path)
|
|
|
|
if path then
|
|
tabInfo.path = path
|
|
tabInfo.env = sandboxEnv
|
|
tabInfo.args = Util.shallowCopy(args)
|
|
tabInfo.title = fs.getName(path)
|
|
|
|
if path ~= 'sys/apps/shell' then
|
|
table.insert(tabInfo.args, 1, tabInfo.path)
|
|
tabInfo.path = 'sys/apps/shell'
|
|
end
|
|
return multishell.openTab(tabInfo)
|
|
end
|
|
return nil, 'No such program'
|
|
end
|
|
|
|
function shell.openTab( ... )
|
|
return shell.newTab({ }, ...)
|
|
end
|
|
|
|
function shell.openForegroundTab( ... )
|
|
return shell.newTab({ focused = true }, ...)
|
|
end
|
|
|
|
function shell.openHiddenTab( ... )
|
|
return shell.newTab({ hidden = true }, ...)
|
|
end
|
|
|
|
function shell.switchTab(tabId)
|
|
multishell.setFocus(tabId)
|
|
end
|
|
|
|
local tArgs = { ... }
|
|
if #tArgs > 0 then
|
|
-- "shell x y z"
|
|
-- Run the program specified in this new shell
|
|
local s, m = shell.run( ... )
|
|
if not s and m ~= 'Terminated' then
|
|
error(m or '')
|
|
end
|
|
return s, m
|
|
end
|
|
|
|
local Config = require('config')
|
|
local History = require('history')
|
|
|
|
local config = {
|
|
standard = {
|
|
textColor = colors.white,
|
|
commandTextColor = colors.lightGray,
|
|
directoryTextColor = colors.gray,
|
|
directoryBackgroundColor = colors.black,
|
|
promptTextColor = colors.gray,
|
|
promptBackgroundColor = colors.black,
|
|
directoryColor = colors.gray,
|
|
},
|
|
color = {
|
|
textColor = colors.white,
|
|
commandTextColor = colors.yellow,
|
|
directoryTextColor = colors.orange,
|
|
directoryBackgroundColor = colors.black,
|
|
promptTextColor = colors.blue,
|
|
promptBackgroundColor = colors.black,
|
|
directoryColor = colors.green,
|
|
},
|
|
displayDirectory = true,
|
|
}
|
|
|
|
--Config.load('shell', config)
|
|
|
|
local _colors = config.standard
|
|
if term.isColor() then
|
|
_colors = config.color
|
|
end
|
|
|
|
local function autocompleteFile(results, words)
|
|
|
|
local function getBaseDir(path)
|
|
if #path > 1 then
|
|
if path:sub(-1) ~= '/' then
|
|
path = fs.getDir(path)
|
|
end
|
|
end
|
|
if path:sub(1, 1) == '/' then
|
|
path = fs.combine(path, '')
|
|
else
|
|
path = fs.combine(shell.dir(), path)
|
|
end
|
|
while not fs.isDir(path) do
|
|
path = fs.getDir(path)
|
|
end
|
|
return path
|
|
end
|
|
|
|
local function getRawPath(path)
|
|
local baseDir = ''
|
|
if path:sub(1, 1) ~= '/' then
|
|
baseDir = shell.dir()
|
|
end
|
|
if #path > 1 then
|
|
if path:sub(-1) ~= '/' then
|
|
path = fs.getDir(path)
|
|
end
|
|
end
|
|
if fs.isDir(fs.combine(baseDir, path)) then
|
|
return path
|
|
end
|
|
return fs.getDir(path)
|
|
end
|
|
|
|
local match = words[#words] or ''
|
|
local startDir = getBaseDir(match)
|
|
local rawPath = getRawPath(match)
|
|
|
|
if fs.isDir(startDir) then
|
|
local files = fs.list(startDir)
|
|
for _,f in pairs(files) do
|
|
local path = fs.combine(rawPath, f)
|
|
if fs.isDir(fs.combine(startDir, f)) then
|
|
results[path .. '/'] = 'directory'
|
|
else
|
|
results[path .. ' '] = 'program'
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function autocompleteProgram(results, words)
|
|
if #words == 1 then
|
|
local files = shell.programs(true)
|
|
for _,f in ipairs(files) do
|
|
results[f .. ' '] = 'program'
|
|
end
|
|
for f in pairs(ALIASES) do
|
|
results[f .. ' '] = 'program'
|
|
end
|
|
end
|
|
end
|
|
|
|
local function autocompleteArgument(results, program, words)
|
|
local word = ''
|
|
if #words > 1 then
|
|
word = words[#words]
|
|
end
|
|
|
|
local tInfo = tCompletionInfo[program]
|
|
local args = tInfo.fnComplete(shell, #words - 1, word, words)
|
|
if args then
|
|
Util.filterInplace(args, function(f)
|
|
return not Util.key(args, f .. '/')
|
|
end)
|
|
for _,arg in ipairs(args) do
|
|
results[word .. arg] = 'argument'
|
|
end
|
|
end
|
|
end
|
|
|
|
local function autocomplete(line, suggestions)
|
|
local words = { }
|
|
for word in line:gmatch("%S+") do
|
|
table.insert(words, word)
|
|
end
|
|
if line:match(' $') then
|
|
table.insert(words, '')
|
|
end
|
|
|
|
local results = { }
|
|
|
|
if #words == 0 then
|
|
files = autocompleteFile(results, words)
|
|
else
|
|
local program = shell.resolveProgram(words[1])
|
|
if tCompletionInfo[program] then
|
|
autocompleteArgument(results, program, words)
|
|
else
|
|
autocompleteProgram(results, words)
|
|
autocompleteFile(results, words)
|
|
end
|
|
end
|
|
|
|
local match = words[#words] or ''
|
|
local files = { }
|
|
for f in pairs(results) do
|
|
if f:sub(1, #match) == match then
|
|
table.insert(files, f)
|
|
end
|
|
end
|
|
|
|
if #files == 1 then
|
|
words[#words] = files[1]
|
|
return table.concat(words, ' ')
|
|
elseif #files > 1 and suggestions then
|
|
print()
|
|
|
|
local word = words[#words] or ''
|
|
local prefix = word:match("(.*/)") or ''
|
|
if #prefix > 0 then
|
|
for _,f in ipairs(files) do
|
|
if f:match("^" .. prefix) ~= prefix then
|
|
prefix = ''
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
local tDirs, tFiles = { }, { }
|
|
for _,f in ipairs(files) do
|
|
if results[f] == 'directory' then
|
|
f = f:gsub(prefix, '', 1)
|
|
table.insert(tDirs, f)
|
|
else
|
|
f = f:gsub(prefix, '', 1)
|
|
table.insert(tFiles, f)
|
|
end
|
|
end
|
|
table.sort(tDirs)
|
|
table.sort(tFiles)
|
|
|
|
if #tDirs > 0 and #tDirs < #tFiles then
|
|
local w = term.getSize()
|
|
local nMaxLen = w / 8
|
|
for n, sItem in pairs(files) do
|
|
nMaxLen = math.max(string.len(sItem) + 1, nMaxLen)
|
|
end
|
|
local nCols = math.floor(w / nMaxLen)
|
|
if #tDirs < nCols then
|
|
for i = #tDirs + 1, nCols do
|
|
table.insert(tDirs, '')
|
|
end
|
|
end
|
|
end
|
|
|
|
if #tDirs > 0 then
|
|
textutils.tabulate(_colors.directoryColor, tDirs, colors.white, tFiles)
|
|
else
|
|
textutils.tabulate(colors.white, tFiles)
|
|
end
|
|
|
|
term.setTextColour(_colors.promptTextColor)
|
|
term.setBackgroundColor(_colors.promptBackgroundColor)
|
|
write("$ " )
|
|
|
|
term.setTextColour(_colors.commandTextColor)
|
|
term.setBackgroundColor(colors.black)
|
|
return line
|
|
elseif #files > 1 then
|
|
|
|
-- ugly (complete as much as possible)
|
|
local word = words[#words] or ''
|
|
local i = #word + 1
|
|
while true do
|
|
local ch
|
|
for _,f in ipairs(files) do
|
|
if #f < i then
|
|
words[#words] = string.sub(f, 1, i - 1)
|
|
return table.concat(words, ' ')
|
|
end
|
|
if not ch then
|
|
ch = string.sub(f, i, i)
|
|
elseif string.sub(f, i, i) ~= ch then
|
|
if i == #word + 1 then
|
|
return
|
|
end
|
|
words[#words] = string.sub(f, 1, i - 1)
|
|
return table.concat(words, ' ')
|
|
end
|
|
end
|
|
i = i + 1
|
|
end
|
|
end
|
|
end
|
|
|
|
local function shellRead(_tHistory )
|
|
term.setCursorBlink( true )
|
|
|
|
local sLine = ""
|
|
local nHistoryPos
|
|
local nPos = 0
|
|
local lastPattern
|
|
|
|
local w = term.getSize()
|
|
local sx = term.getCursorPos()
|
|
|
|
local function redraw( sReplace )
|
|
local nScroll = 0
|
|
if sx + nPos >= w then
|
|
nScroll = (sx + nPos) - w
|
|
end
|
|
|
|
local cx,cy = term.getCursorPos()
|
|
term.setCursorPos( sx, cy )
|
|
if sReplace then
|
|
term.write( string.rep( sReplace, math.max( string.len(sLine) - nScroll, 0 ) ) )
|
|
else
|
|
term.write( string.sub( sLine, nScroll + 1 ) )
|
|
end
|
|
term.setCursorPos( sx + nPos - nScroll, cy )
|
|
end
|
|
|
|
while true do
|
|
local sEvent, param, param2 = os.pullEventRaw()
|
|
|
|
if sEvent == "char" then
|
|
sLine = string.sub( sLine, 1, nPos ) .. param .. string.sub( sLine, nPos + 1 )
|
|
nPos = nPos + 1
|
|
redraw()
|
|
elseif sEvent == "paste" then
|
|
sLine = string.sub( sLine, 1, nPos ) .. param .. string.sub( sLine, nPos + 1 )
|
|
nPos = nPos + string.len( param )
|
|
redraw()
|
|
elseif sEvent == 'mouse_click' and param == 2 then
|
|
redraw(string.rep(' ', #sLine))
|
|
sLine = ''
|
|
nPos = 0
|
|
redraw()
|
|
elseif sEvent == 'terminate' then
|
|
bExit = true
|
|
break
|
|
elseif sEvent == "key" then
|
|
if param == keys.enter then
|
|
-- Enter
|
|
break
|
|
elseif param == keys.tab then
|
|
if nPos == #sLine then
|
|
local showSuggestions = lastPattern == sLine
|
|
lastPattern = sLine
|
|
|
|
local cline = autocomplete(sLine, showSuggestions)
|
|
if cline then
|
|
sLine = cline
|
|
nPos = #sLine
|
|
redraw()
|
|
end
|
|
end
|
|
elseif param == keys.left then
|
|
if nPos > 0 then
|
|
nPos = nPos - 1
|
|
redraw()
|
|
end
|
|
elseif param == keys.right then
|
|
if nPos < string.len(sLine) then
|
|
redraw(" ")
|
|
nPos = nPos + 1
|
|
redraw()
|
|
end
|
|
elseif param == keys.up or param == keys.down then
|
|
if _tHistory then
|
|
redraw(" ")
|
|
if param == keys.up then
|
|
if nHistoryPos == nil then
|
|
if #_tHistory > 0 then
|
|
nHistoryPos = #_tHistory
|
|
end
|
|
elseif nHistoryPos > 1 then
|
|
nHistoryPos = nHistoryPos - 1
|
|
end
|
|
else
|
|
if nHistoryPos == #_tHistory then
|
|
nHistoryPos = nil
|
|
elseif nHistoryPos ~= nil then
|
|
nHistoryPos = nHistoryPos + 1
|
|
end
|
|
end
|
|
if nHistoryPos then
|
|
sLine = _tHistory[nHistoryPos]
|
|
nPos = string.len( sLine )
|
|
else
|
|
sLine = ""
|
|
nPos = 0
|
|
end
|
|
redraw()
|
|
end
|
|
elseif param == keys.backspace then
|
|
if nPos > 0 then
|
|
redraw(" ")
|
|
sLine = string.sub( sLine, 1, nPos - 1 ) .. string.sub( sLine, nPos + 1 )
|
|
nPos = nPos - 1
|
|
redraw()
|
|
end
|
|
elseif param == keys.home then
|
|
redraw(" ")
|
|
nPos = 0
|
|
redraw()
|
|
elseif param == keys.delete then
|
|
if nPos < string.len(sLine) then
|
|
redraw(" ")
|
|
sLine = string.sub( sLine, 1, nPos ) .. string.sub( sLine, nPos + 2 )
|
|
redraw()
|
|
end
|
|
elseif param == keys["end"] then
|
|
redraw(" ")
|
|
nPos = string.len(sLine)
|
|
redraw()
|
|
end
|
|
elseif sEvent == "term_resize" then
|
|
w = term.getSize()
|
|
redraw()
|
|
end
|
|
end
|
|
|
|
local cx, cy = term.getCursorPos()
|
|
term.setCursorPos( w + 1, cy )
|
|
print()
|
|
term.setCursorBlink( false )
|
|
return sLine
|
|
end
|
|
|
|
local history = History.load('usr/.shell_history', 25)
|
|
|
|
while not bExit do
|
|
if config.displayDirectory then
|
|
term.setTextColour(_colors.directoryTextColor)
|
|
term.setBackgroundColor(_colors.directoryBackgroundColor)
|
|
print('==' .. os.getComputerLabel() .. ':/' .. DIR)
|
|
end
|
|
term.setTextColour(_colors.promptTextColor)
|
|
term.setBackgroundColor(_colors.promptBackgroundColor)
|
|
write("$ " )
|
|
term.setTextColour(_colors.commandTextColor)
|
|
term.setBackgroundColor(colors.black)
|
|
local sLine = shellRead(history.entries)
|
|
if bExit then -- terminated
|
|
break
|
|
end
|
|
sLine = Util.trim(sLine)
|
|
if #sLine > 0 and sLine ~= 'exit' then
|
|
history.add(sLine)
|
|
end
|
|
term.setTextColour(_colors.textColor)
|
|
if #sLine > 0 then
|
|
local result, err = shell.run( sLine )
|
|
if not result and err then
|
|
printError(err)
|
|
end
|
|
end
|
|
end |