opus/apps/multishell

601 lines
13 KiB
Plaintext

-- Default label
if not os.getComputerLabel() then
local id = os.getComputerID()
if turtle then
os.setComputerLabel('turtle_' .. id)
elseif pocket then
os.setComputerLabel('pocket_' .. id)
else
os.setComputerLabel('computer_' .. id)
end
end
multishell.term = term.current()
local defaultEnv = Util.shallowCopy(getfenv(1))
require = requireInjector(getfenv(1))
local Config = require('config')
-- Begin multishell
local parentTerm = term.current()
local w,h = parentTerm.getSize()
local tabs = {}
local currentTab
local _tabId = 0
local overviewTab
local runningTab
local tabsDirty = false
local config = {
standard = {
focusTextColor = colors.lightGray,
focusBackgroundColor = colors.gray,
textColor = colors.black,
backgroundColor = colors.lightGray,
tabBarTextColor = colors.black,
tabBarBackgroundColor = colors.lightGray,
},
color = {
focusTextColor = colors.white,
focusBackgroundColor = colors.brown,
textColor = colors.gray,
backgroundColor = colors.brown,
tabBarTextColor = colors.lightGray,
tabBarBackgroundColor = colors.brown,
},
-- path = '.:/apps:' .. shell.path():sub(3),
path = '/apps:' .. shell.path(),
}
Config.load('multishell', config)
shell.setPath(config.path)
if config.aliases then
for k in pairs(shell.aliases()) do
shell.clearAlias(k)
end
for k,v in pairs(config.aliases) do
shell.setAlias(k, v)
end
end
local _colors = config.standard
if parentTerm.isColor() then
_colors = config.color
end
local function redrawMenu()
if not tabsDirty then
os.queueEvent('multishell', 'draw')
tabsDirty = true
end
end
-- Draw menu
local function draw()
tabsDirty = false
parentTerm.setBackgroundColor( _colors.tabBarBackgroundColor )
if currentTab and currentTab.isOverview then
parentTerm.setTextColor( _colors.focusTextColor )
else
parentTerm.setTextColor( _colors.tabBarTextColor )
end
parentTerm.setCursorPos( 1, 1 )
parentTerm.clearLine()
parentTerm.write('+')
local tabX = 2
local function compareTab(a, b)
return a.tabId < b.tabId
end
for _,tab in Util.spairs(tabs, compareTab) do
if tab.hidden and tab ~= currentTab or tab.isOverview then
tab.sx = nil
tab.ex = nil
else
tab.sx = tabX + 1
tab.ex = tabX + #tab.title
tabX = tabX + #tab.title + 1
end
end
for _,tab in Util.spairs(tabs) do
if tab.sx then
if tab == currentTab then
parentTerm.setTextColor(_colors.focusTextColor)
parentTerm.setBackgroundColor(_colors.focusBackgroundColor)
else
parentTerm.setTextColor(_colors.textColor)
parentTerm.setBackgroundColor(_colors.backgroundColor)
end
parentTerm.setCursorPos(tab.sx, 1)
parentTerm.write(tab.title)
end
end
if currentTab and not currentTab.isOverview then
parentTerm.setTextColor(_colors.textColor)
parentTerm.setBackgroundColor(_colors.backgroundColor)
parentTerm.setCursorPos( w, 1 )
parentTerm.write('*')
end
if currentTab then
currentTab.window.restoreCursor()
end
end
local function selectTab( tab )
if not tab then
for _,ftab in pairs(tabs) do
if not ftab.hidden then
tab = ftab
break
end
end
end
if not tab then
tab = overviewTab
end
if currentTab and currentTab ~= tab then
currentTab.window.setVisible(false)
if tab and not currentTab.hidden then
tab.previousTabId = currentTab.tabId
end
end
if tab then
currentTab = tab
tab.window.setVisible(true)
end
end
local function resumeTab(tab, event, eventData)
if not tab or coroutine.status(tab.co) == 'dead' then
return
end
if not tab.filter or tab.filter == event or event == "terminate" then
eventData = eventData or { }
term.redirect(tab.terminal)
local previousTab = runningTab
runningTab = tab
local ok, result = coroutine.resume(tab.co, event, unpack(eventData))
tab.terminal = term.current()
if ok then
tab.filter = result
else
printError(result)
end
runningTab = previousTab
return ok, result
end
end
local function nextTabId()
_tabId = _tabId + 1
return _tabId
end
local function launchProcess(tab)
tab.tabId = nextTabId()
tab.timestamp = os.clock()
tab.window = window.create(parentTerm, 1, 2, w, h - 1, false)
tab.terminal = tab.window
tab.env = Util.shallowCopy(tab.env or defaultEnv)
tab.co = coroutine.create(function()
local result, err
if tab.fn then
result, err = Util.runFunction(tab.env, tab.fn, table.unpack(tab.args or { } ))
elseif tab.path then
result, err = os.run(tab.env, tab.path, table.unpack(tab.args or { } ))
else
err = 'multishell: invalid tab'
end
if not result and err ~= 'Terminated' then
if err then
printError(tostring(err))
end
printError('Press enter to exit')
tab.isDead = true
while true do
local e, code = os.pullEventRaw('key')
if e == 'terminate' or e == 'key' and code == keys.enter then
if tab.isOverview then
os.queueEvent('multishell', 'terminate')
end
break
end
end
end
tabs[tab.tabId] = nil
if tab == currentTab then
local previousTab
if tab.previousTabId then
previousTab = tabs[tab.previousTabId]
end
selectTab(previousTab)
end
redrawMenu()
end)
tabs[tab.tabId] = tab
resumeTab(tab)
return tab
end
local function resizeWindows()
local windowY = 2
local windowHeight = h-1
local keys = Util.keys(tabs)
for _,key in pairs(keys) do
local tab = tabs[key]
local x,y = tab.window.getCursorPos()
if y > windowHeight then
tab.window.scroll( y - windowHeight )
tab.window.setCursorPos( x, windowHeight )
end
tab.window.reposition( 1, windowY, w, windowHeight )
end
-- Pass term_resize to all processes
local keys = Util.keys(tabs)
for _,key in pairs(keys) do
resumeTab(tabs[key], "term_resize")
end
end
local control
local hotkeys = { }
local function processKeyEvent(event, code)
if event == 'key_up' then
if code == keys.leftCtrl or code == keys.rightCtrl then
control = false
end
elseif event == 'char' then
control = false
elseif event == 'key' then
if code == keys.leftCtrl or code == keys.rightCtrl then
control = true
elseif control then
local hotkey = hotkeys[code]
control = false
if hotkey then
hotkey()
end
end
end
end
function multishell.addHotkey(code, fn)
hotkeys[code] = fn
end
function multishell.removeHotkey(code)
hotkeys[code] = nil
end
function multishell.getFocus()
return currentTab.tabId
end
function multishell.setFocus(tabId)
local tab = tabs[tabId]
if tab then
selectTab(tab)
redrawMenu()
return true
end
return false
end
function multishell.getTitle(tabId)
local tab = tabs[tabId]
if tab then
return tab.title
end
end
function multishell.setTitle(tabId, sTitle)
local tab = tabs[tabId]
if tab then
tab.title = sTitle or ''
redrawMenu()
end
end
function multishell.getCurrent()
if runningTab then
return runningTab.tabId
end
end
function multishell.getTab(tabId)
return tabs[tabId]
end
function multishell.terminate(tabId)
local tab = tabs[tabId]
if tab and not tab.isOverview then
if coroutine.status(tab.co) ~= 'dead' then
--os.queueEvent('multishell', 'terminate', tab)
resumeTab(tab, "terminate")
else
tabs[tabId] = nil
if tab == currentTab then
local previousTab
if tab.previousTabId then
previousTab = tabs[tab.previousTabId]
end
selectTab(previousTab)
end
redrawMenu()
end
end
end
function multishell.getTabs()
return tabs
end
function multishell.launch( tProgramEnv, sProgramPath, ... )
-- backwards compatibility
return multishell.openTab({
env = tProgramEnv,
path = sProgramPath,
args = { ... },
})
end
function multishell.openTab(tab)
if not tab.title and tab.path then
tab.title = fs.getName(tab.path)
end
tab.title = tab.title or 'untitled'
local previousTerm = term.current()
launchProcess(tab)
term.redirect(previousTerm)
if tab.hidden then
if coroutine.status(tab.co) == 'dead' or tab.isDead then
tab.hidden = false
end
elseif tab.focused then
multishell.setFocus(tab.tabId)
else
redrawMenu()
end
return tab.tabId
end
function multishell.hideTab(tabId)
local tab = tabs[tabId]
if tab then
tab.hidden = true
redrawMenu()
end
end
function multishell.unhideTab(tabId)
local tab = tabs[tabId]
if tab then
tab.hidden = false
redrawMenu()
end
end
function multishell.getCount()
local count
for _,tab in pairs(tabs) do
count = count + 1
end
return count
end
-- control-o - overview
multishell.addHotkey(24, function()
multishell.setFocus(overviewTab.tabId)
end)
-- control-backspace
multishell.addHotkey(14, function()
local tabId = multishell.getFocus()
local tab = tabs[tabId]
if not tab.isOverview then
os.queueEvent('multishell', 'terminateTab', tabId)
tab = Util.shallowCopy(tab)
tab.isDead = false
tab.focused = true
multishell.openTab(tab)
end
end)
-- control-tab - next tab
multishell.addHotkey(15, function()
local function compareTab(a, b)
return a.tabId < b.tabId
end
local visibleTabs = { }
for _,tab in Util.spairs(tabs, compareTab) do
if not tab.hidden then
table.insert(visibleTabs, tab)
end
end
for k,tab in ipairs(visibleTabs) do
if tab.tabId == currentTab.tabId then
if k < #visibleTabs then
multishell.setFocus(visibleTabs[k + 1].tabId)
return
end
end
end
if #visibleTabs > 0 then
multishell.setFocus(visibleTabs[1].tabId)
end
end)
local function startup()
local hasError
local function runDir(directory, open)
if not fs.exists(directory) then
return
end
local files = fs.list(directory)
table.sort(files)
for _,file in ipairs(files) do
print('Autorunning: ' .. file)
local result, err = open(directory .. '/' .. file)
if not result then
printError(err)
hasError = true
end
end
end
runDir('/sys/extensions', shell.run)
local overviewId = multishell.openTab({
path = '/apps/Overview.lua',
focused = true,
hidden = true,
isOverview = true,
})
overviewTab = tabs[overviewId]
runDir('/sys/services', shell.openHiddenTab)
runDir('/autorun', shell.run)
if hasError then
error('An autorun program has errored')
end
end
-- Begin
parentTerm.clear()
multishell.openTab({
focused = true,
fn = startup,
env = defaultEnv,
title = 'Autorun',
})
if not overviewTab or coroutine.status(overviewTab.co) == 'dead' then
--error('Overview aborted')
end
if not currentTab then
multishell.setFocus(overviewTab.tabId)
end
draw()
while true do
-- Get the event
local tEventData = { os.pullEventRaw() }
local sEvent = table.remove(tEventData, 1)
if sEvent == "term_resize" then
-- Resize event
w,h = parentTerm.getSize()
resizeWindows()
redrawMenu()
elseif sEvent == 'multishell' then
local action = tEventData[1]
if action == 'terminate' then
break
elseif action == 'terminateTab' then
multishell.terminate(tEventData[2])
elseif action == 'draw' then
draw()
end
elseif sEvent == "char" or
sEvent == "key" or
sEvent == "paste" or
sEvent == "terminate" then
processKeyEvent(sEvent, tEventData[1])
-- Keyboard event - Passthrough to current process
resumeTab(currentTab, sEvent, tEventData)
elseif sEvent == "mouse_click" then
local button, x, y = tEventData[1], tEventData[2], tEventData[3]
if y == 1 and os.locked then
-- ignore
elseif y == 1 then
-- Switch process
local w, h = parentTerm.getSize()
if x == 1 then
multishell.setFocus(overviewTab.tabId)
elseif x == w then
if currentTab then
multishell.terminate(currentTab.tabId)
end
else
for _,tab in pairs(tabs) do
if not tab.hidden and tab.sx then
if x >= tab.sx and x <= tab.ex then
multishell.setFocus(tab.tabId)
break
end
end
end
end
elseif currentTab then
-- Passthrough to current process
resumeTab(currentTab, sEvent, { button, x, y-1 })
end
elseif sEvent == "mouse_drag" or sEvent == "mouse_scroll" then
-- Other mouse event
local p1, x, y = tEventData[1], tEventData[2], tEventData[3]
if currentTab and (y ~= 1) then
if currentTab.terminal.scrollUp then
if p1 == -1 then
currentTab.terminal.scrollUp()
else
currentTab.terminal.scrollDown()
end
else
-- Passthrough to current process
resumeTab(currentTab, sEvent, { p1, x, y-1 })
end
end
else
-- Other event
-- Passthrough to all processes
local keys = Util.keys(tabs)
for _,key in pairs(keys) do
resumeTab(tabs[key], sEvent, tEventData)
end
end
end