mirror of
https://github.com/kepler155c/opus
synced 2025-01-01 03:10:28 +00:00
684 lines
15 KiB
Lua
684 lines
15 KiB
Lua
local parentShell = _ENV.shell
|
|
_ENV.shell = { }
|
|
|
|
local trace = require('opus.trace')
|
|
local Util = require('opus.util')
|
|
|
|
local fs = _G.fs
|
|
local settings = _G.settings
|
|
local shell = _ENV.shell
|
|
|
|
local DIR = (parentShell and parentShell.dir()) or ""
|
|
local PATH = (parentShell and parentShell.path()) or ".:/rom/programs"
|
|
local tAliases = (parentShell and parentShell.aliases()) or {}
|
|
local tCompletionInfo = (parentShell and parentShell.getCompletionInfo()) or {}
|
|
|
|
local bExit = false
|
|
local tProgramStack = {}
|
|
|
|
local function tokenise(...)
|
|
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 tWords
|
|
end
|
|
|
|
local defaultHandlers = {
|
|
function(env, command, args)
|
|
return command:match("^(https?:)") and {
|
|
title = fs.getName(command),
|
|
path = command,
|
|
args = args,
|
|
load = Util.loadUrl,
|
|
env = env,
|
|
}
|
|
end,
|
|
|
|
function(env, command, args)
|
|
command = env.shell.resolveProgram(command)
|
|
or error('No such program')
|
|
|
|
_G.requireInjector(env, fs.getDir(command))
|
|
return {
|
|
title = fs.getName(command):match('([^%.]+)'),
|
|
path = command,
|
|
args = args,
|
|
load = loadfile,
|
|
env = env,
|
|
}
|
|
end,
|
|
}
|
|
|
|
function shell.getHandlers()
|
|
if parentShell and parentShell.getHandlers then
|
|
return parentShell.getHandlers()
|
|
end
|
|
return defaultHandlers
|
|
end
|
|
|
|
local handlers = shell.getHandlers()
|
|
|
|
function shell.registerHandler(fn)
|
|
table.insert(handlers, 1, fn)
|
|
end
|
|
|
|
local function handleCommand(env, command, args)
|
|
for _,v in pairs(handlers) do
|
|
local pi = v(env, command, args)
|
|
if pi then
|
|
return pi
|
|
end
|
|
end
|
|
end
|
|
|
|
local function run(...)
|
|
local args = tokenise(...)
|
|
if #args == 0 then
|
|
error('No such program')
|
|
end
|
|
|
|
local pi = handleCommand(shell.makeEnv(_ENV), table.remove(args, 1), args)
|
|
|
|
local O_v_O, err = pi.load(pi.path, pi.env)
|
|
if not O_v_O then
|
|
error(err, -1)
|
|
end
|
|
|
|
if _ENV.multishell then
|
|
_ENV.multishell.setTitle(_ENV.multishell.getCurrent(), pi.title)
|
|
end
|
|
|
|
tProgramStack[#tProgramStack + 1] = pi
|
|
|
|
pi.env[ "arg" ] = { [0] = pi.path, table.unpack(pi.args) }
|
|
local r = { O_v_O(table.unpack(pi.args)) }
|
|
|
|
tProgramStack[#tProgramStack] = nil
|
|
|
|
return table.unpack(r)
|
|
end
|
|
|
|
-- Install shell API
|
|
function shell.run(...)
|
|
local oldTitle
|
|
|
|
if _ENV.multishell then
|
|
oldTitle = _ENV.multishell.getTitle(_ENV.multishell.getCurrent())
|
|
end
|
|
|
|
local r = { trace(run, ...) }
|
|
|
|
if _ENV.multishell then
|
|
_ENV.multishell.setTitle(_ENV.multishell.getCurrent(), oldTitle or 'shell')
|
|
end
|
|
|
|
return table.unpack(r)
|
|
end
|
|
|
|
function shell.exit()
|
|
bExit = true
|
|
end
|
|
|
|
function shell.dir() return DIR end
|
|
function shell.setDir(d)
|
|
d = fs.combine(d, '')
|
|
if not fs.isDir(d) then
|
|
error("Not a directory", 2)
|
|
end
|
|
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 tAliases[_sCommand] ~= nil then
|
|
_sCommand = tAliases[_sCommand]
|
|
end
|
|
|
|
local function check(f)
|
|
return fs.exists(f) and not fs.isDir(f) and f
|
|
end
|
|
|
|
local function inPath()
|
|
-- Otherwise, look on the path variable
|
|
for sPath in string.gmatch(PATH or '', "[^:]+") do
|
|
sPath = fs.combine(sPath, _sCommand )
|
|
if check(sPath) then
|
|
return sPath
|
|
end
|
|
if check(sPath .. '.lua') then
|
|
return sPath .. '.lua'
|
|
end
|
|
end
|
|
end
|
|
|
|
if not _sCommand:find('/') then
|
|
return inPath()
|
|
end
|
|
|
|
-- so... even if you are in the rom directory and you run:
|
|
-- 'packages/common/edit.lua', allow this even though it
|
|
-- does not use a leading slash. Ideally, fs.combine would
|
|
-- provide the leading slash... but it does not.
|
|
return check(shell.resolve(_sCommand))
|
|
or check(shell.resolve(_sCommand) .. '.lua')
|
|
or check(_sCommand)
|
|
or check(_sCommand .. '.lua')
|
|
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 _,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 in pairs(tItems) do
|
|
table.insert(tItemList, sItem)
|
|
end
|
|
table.sort(tItemList)
|
|
return tItemList
|
|
end
|
|
|
|
function shell.completeProgram(sLine)
|
|
if #sLine > 0 and string.sub(sLine, 1, 1) == '/' then
|
|
-- Add programs from the root
|
|
return fs.complete(sLine, '', true, false)
|
|
end
|
|
|
|
local tResults = { }
|
|
local tSeen = { }
|
|
|
|
-- Add aliases
|
|
for sAlias in pairs( tAliases ) do
|
|
if #sAlias > #sLine and string.sub(sAlias, 1, #sLine) == sLine then
|
|
local sResult = string.sub(sAlias, #sLine + 1)
|
|
if not tSeen[sResult] then
|
|
table.insert(tResults, sResult .. ' ')
|
|
tSeen[sResult] = true
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Add programs from the path
|
|
local tPrograms = shell.programs()
|
|
for n=1,#tPrograms do
|
|
local sProgram = tPrograms[n]
|
|
if #sProgram >= #sLine and string.sub(sProgram, 1, #sLine) == sLine then
|
|
local sResult = string.sub(sProgram, #sLine + 1)
|
|
if not tSeen[sResult] then
|
|
table.insert(tResults, sResult .. ' ')
|
|
tSeen[sResult] = true
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Sort and return
|
|
table.sort(tResults)
|
|
return tResults
|
|
end
|
|
|
|
function shell.complete(sLine)
|
|
local tWords = tokenise(sLine)
|
|
local nIndex = #tWords
|
|
if string.sub(sLine, #sLine, #sLine) == ' ' and #Util.trim(sLine) > 0 then
|
|
nIndex = nIndex + 1
|
|
end
|
|
|
|
if nIndex == 0 then
|
|
return fs.complete('', shell.dir(), true, false)
|
|
|
|
elseif nIndex == 1 then
|
|
local results = shell.completeProgram(tWords[1] or '')
|
|
for _, v in pairs(fs.complete(table.concat(tWords, ' '), shell.dir(), true, false)) do
|
|
table.insert(results, v)
|
|
end
|
|
return results
|
|
|
|
else
|
|
local sPath = shell.resolveProgram(tWords[1])
|
|
local sPart = tWords[nIndex] or ''
|
|
local tPreviousParts = tWords
|
|
tPreviousParts[nIndex] = nil
|
|
local results
|
|
local tInfo = tCompletionInfo[sPath]
|
|
if tInfo then
|
|
results = tInfo.fnComplete(shell, nIndex - 1, sPart, tPreviousParts)
|
|
end
|
|
return results and #results > 0 and results
|
|
or fs.complete(sPart, shell.dir(), true, false)
|
|
end
|
|
end
|
|
|
|
function shell.setCompletionFunction(sProgram, fnComplete)
|
|
tCompletionInfo[sProgram] = { fnComplete = fnComplete }
|
|
end
|
|
|
|
function shell.getCompletionInfo()
|
|
return tCompletionInfo
|
|
end
|
|
|
|
function shell.getRunningProgram()
|
|
return tProgramStack[#tProgramStack] and tProgramStack[#tProgramStack].path
|
|
end
|
|
|
|
function shell.getRunningInfo()
|
|
return tProgramStack[#tProgramStack]
|
|
end
|
|
|
|
-- convenience function for making a runnable env
|
|
function shell.makeEnv(env, dir)
|
|
env = setmetatable(Util.shallowCopy(env), { __index = _G })
|
|
_G.requireInjector(env, dir)
|
|
return env
|
|
end
|
|
|
|
function shell.setAlias(_sCommand, _sProgram)
|
|
tAliases[_sCommand] = _sProgram
|
|
end
|
|
|
|
function shell.clearAlias(_sCommand)
|
|
tAliases[_sCommand] = nil
|
|
end
|
|
|
|
function shell.aliases()
|
|
local tCopy = {}
|
|
for sAlias, sCommand in pairs(tAliases) do
|
|
tCopy[sAlias] = sCommand
|
|
end
|
|
return tCopy
|
|
end
|
|
|
|
function shell.newTab(tabInfo, ...)
|
|
local args = tokenise(...)
|
|
local path = table.remove(args, 1)
|
|
path = shell.resolveProgram(path)
|
|
|
|
if path then
|
|
tabInfo.path = path
|
|
tabInfo.args = args
|
|
tabInfo.title = fs.getName(path):match('([^%.]+)')
|
|
|
|
if path ~= 'sys/apps/shell.lua' then
|
|
table.insert(tabInfo.args, 1, tabInfo.path)
|
|
tabInfo.path = 'sys/apps/shell.lua'
|
|
end
|
|
return _ENV.multishell.openTab(_ENV, tabInfo)
|
|
end
|
|
return nil, 'No such program'
|
|
end
|
|
|
|
if not _ENV.multishell then
|
|
function shell.newTab()
|
|
error('Multishell is not available')
|
|
end
|
|
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)
|
|
_ENV.multishell.setFocus(tabId)
|
|
end
|
|
|
|
local tArgs = { ... }
|
|
if #tArgs > 0 then
|
|
return run(...)
|
|
end
|
|
|
|
local Config = require('opus.config')
|
|
local Entry = require('opus.entry')
|
|
local History = require('opus.history')
|
|
local Input = require('opus.input')
|
|
local Sound = require('opus.sound')
|
|
local Terminal = require('opus.terminal')
|
|
|
|
local colors = _G.colors
|
|
local os = _G.os
|
|
local term = _G.term
|
|
local textutils = _G.textutils
|
|
|
|
local oldTerm
|
|
local terminal = term.current()
|
|
local _len = string.len
|
|
local _rep = string.rep
|
|
local _sub = string.sub
|
|
|
|
local config = {
|
|
color = {
|
|
textColor = colors.white,
|
|
commandTextColor = colors.yellow,
|
|
directoryTextColor = colors.orange,
|
|
promptTextColor = colors.blue,
|
|
directoryColor = colors.green,
|
|
fileColor = colors.white,
|
|
backgroundColor = colors.black,
|
|
},
|
|
displayDirectory = true,
|
|
}
|
|
|
|
Config.load('shellprompt', config)
|
|
|
|
local _colors = config.color
|
|
-- temp
|
|
if not _colors.backgroundColor then
|
|
_colors.backgroundColor = colors.black
|
|
_colors.fileColor = colors.white
|
|
end
|
|
|
|
if not terminal.scrollUp then
|
|
terminal = Terminal.window(term.current())
|
|
terminal.setMaxScroll(200)
|
|
oldTerm = term.redirect(terminal)
|
|
term.setBackgroundColor(_colors.backgroundColor)
|
|
term.clear()
|
|
end
|
|
|
|
local palette = terminal.canvas.palette
|
|
|
|
local function autocomplete(line)
|
|
local words = { }
|
|
for word in line:gmatch("%S+") do
|
|
table.insert(words, word)
|
|
end
|
|
if line:match(' $') then
|
|
table.insert(words, '')
|
|
end
|
|
if #words == 0 then
|
|
words = { '' }
|
|
end
|
|
|
|
local results = shell.complete(line) or { }
|
|
|
|
Util.filterInplace(results, function(f)
|
|
return not Util.key(results, f .. '/')
|
|
end)
|
|
local w = words[#words] or ''
|
|
for k,arg in pairs(results) do
|
|
results[k] = w .. arg
|
|
end
|
|
|
|
if #results == 1 then
|
|
words[#words] = results[1]
|
|
return table.concat(words, ' ')
|
|
|
|
elseif #results > 1 then
|
|
local function someComplete()
|
|
-- ugly (complete as much as possible)
|
|
local word = words[#words] or ''
|
|
local i = #word + 1
|
|
while true do
|
|
local ch
|
|
for _,f in ipairs(results) do
|
|
if #f < i then
|
|
words[#words] = _sub(f, 1, i - 1)
|
|
return table.concat(words, ' ')
|
|
end
|
|
if not ch then
|
|
ch = _sub(f, i, i)
|
|
elseif _sub(f, i, i) ~= ch then
|
|
if i == #word + 1 then
|
|
return
|
|
end
|
|
words[#words] = _sub(f, 1, i - 1)
|
|
return table.concat(words, ' ')
|
|
end
|
|
end
|
|
i = i + 1
|
|
end
|
|
end
|
|
|
|
local t = someComplete()
|
|
if t then
|
|
return t
|
|
end
|
|
|
|
print()
|
|
|
|
local word = words[#words] or ''
|
|
local prefix = word:match("(.*/)") or ''
|
|
if #prefix > 0 then
|
|
for _,f in ipairs(results) do
|
|
if f:match("^" .. prefix) ~= prefix then
|
|
prefix = ''
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
local tDirs, tFiles = { }, { }
|
|
for _,f in ipairs(results) do
|
|
if fs.isDir(shell.resolve(f)) 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 tw = term.getSize()
|
|
local nMaxLen = tw / 8
|
|
for _,sItem in pairs(results) do
|
|
nMaxLen = math.max(_len(sItem) + 1, nMaxLen)
|
|
end
|
|
local nCols = math.floor(tw / nMaxLen)
|
|
if #tDirs < nCols then
|
|
for _ = #tDirs + 1, nCols do
|
|
table.insert(tDirs, '')
|
|
end
|
|
end
|
|
end
|
|
|
|
if #tDirs > 0 then
|
|
textutils.tabulate(_colors.directoryColor, tDirs, _colors.fileColor, tFiles)
|
|
else
|
|
textutils.tabulate(_colors.fileColor, tFiles)
|
|
end
|
|
|
|
term.setTextColour(_colors.promptTextColor)
|
|
term.write("$ " )
|
|
|
|
term.setTextColour(_colors.commandTextColor)
|
|
return line
|
|
end
|
|
end
|
|
|
|
local function shellRead(history)
|
|
local lastLen = 0
|
|
local entry = Entry({
|
|
width = term.getSize() - 3,
|
|
offset = 3,
|
|
})
|
|
|
|
history:reset()
|
|
term.setCursorBlink(true)
|
|
|
|
local function updateCursor()
|
|
term.setCursorPos(3 + entry.pos - entry.scroll, select(2, term.getCursorPos()))
|
|
end
|
|
|
|
local function redraw()
|
|
if terminal.scrollBottom then
|
|
terminal.scrollBottom()
|
|
end
|
|
local _,cy = term.getCursorPos()
|
|
term.setCursorPos(3, cy)
|
|
entry.value = entry.value or ''
|
|
local filler = #entry.value < lastLen
|
|
and _rep(' ', lastLen - #entry.value)
|
|
or ''
|
|
local str = _sub(entry.value, entry.scroll + 1, entry.width + entry.scroll) .. filler
|
|
local fg = _rep(palette[_colors.commandTextColor], #str)
|
|
local bg = _rep(palette[_colors.backgroundColor], #str)
|
|
if entry.mark.active then
|
|
bg = _rep('f', entry.mark.x) ..
|
|
_rep('7', entry.mark.ex - entry.mark.x) ..
|
|
_rep('f', #entry.value - entry.mark.ex + #filler + 1)
|
|
bg = _sub(bg, entry.scroll + 1, entry.scroll + #str)
|
|
end
|
|
term.blit(str, fg, bg)
|
|
updateCursor()
|
|
lastLen = #entry.value
|
|
end
|
|
|
|
while true do
|
|
local event, p1, p2, p3 = os.pullEventRaw()
|
|
|
|
local ie = Input:translate(event, p1, p2, p3)
|
|
if ie then
|
|
if ie.code == 'scroll_up' and terminal.scrollUp then
|
|
terminal.scrollUp()
|
|
|
|
elseif ie.code == 'scroll_down' and terminal.scrollDown then
|
|
terminal.scrollDown()
|
|
|
|
elseif ie.code == 'terminate' then
|
|
bExit = true
|
|
break
|
|
|
|
elseif ie.code == 'enter' then
|
|
break
|
|
|
|
elseif ie.code == 'up' or ie.code == 'control-p' or
|
|
ie.code == 'down' or ie.code == 'control-n' then
|
|
entry:reset()
|
|
if ie.code == 'up' or ie.code == 'control-p' then
|
|
entry.value = history:back() or ''
|
|
else
|
|
entry.value = history:forward() or ''
|
|
end
|
|
entry.pos = #entry.value
|
|
entry:updateScroll()
|
|
redraw()
|
|
|
|
elseif ie.code == 'tab' then
|
|
entry.value = entry.value or ''
|
|
if entry.pos == #entry.value then
|
|
local cline = autocomplete(entry.value)
|
|
if cline then
|
|
entry.value = cline
|
|
entry.pos = #entry.value
|
|
entry:unmark()
|
|
entry:updateScroll()
|
|
redraw()
|
|
else
|
|
Sound.play('entity.villager.no')
|
|
end
|
|
end
|
|
|
|
elseif ie.code == 'control-l' then
|
|
term.clear()
|
|
term.setCursorPos(1, 0) -- Y:0 ?
|
|
break
|
|
|
|
else
|
|
entry:process(ie)
|
|
entry.value = entry.value or ''
|
|
if entry.textChanged then
|
|
redraw()
|
|
elseif entry.posChanged then
|
|
updateCursor()
|
|
end
|
|
end
|
|
|
|
elseif event == "term_resize" then
|
|
terminal.reposition(1, 1, oldTerm.getSize())
|
|
entry.width = term.getSize() - 3
|
|
entry:updateScroll()
|
|
redraw()
|
|
end
|
|
end
|
|
|
|
print()
|
|
term.setCursorBlink(false)
|
|
return entry.value or ''
|
|
end
|
|
|
|
local history = History.load('usr/.shell_history', 100)
|
|
|
|
term.setBackgroundColor(_colors.backgroundColor)
|
|
|
|
if settings.get("motd.enable") then
|
|
shell.run("motd")
|
|
end
|
|
|
|
while not bExit do
|
|
if config.displayDirectory then
|
|
term.setTextColour(_colors.directoryTextColor)
|
|
print('==' .. os.getComputerLabel() .. ':/' .. DIR)
|
|
end
|
|
term.setTextColour(_colors.promptTextColor)
|
|
term.write("$ " )
|
|
term.setTextColour(_colors.commandTextColor)
|
|
local sLine = shellRead(history)
|
|
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)
|
|
local cx = term.getCursorPos()
|
|
if cx ~= 1 then
|
|
print()
|
|
end
|
|
term.setBackgroundColor(_colors.backgroundColor)
|
|
if not result and err then
|
|
_G.printError(err)
|
|
end
|
|
end
|
|
end
|
|
|
|
if oldTerm then
|
|
term.redirect(oldTerm)
|
|
end
|