opus/sys/apps/multishell

606 lines
13 KiB
Plaintext
Raw Normal View History

local defaultEnv = { }
2017-10-08 21:45:01 +00:00
for k,v in pairs(_ENV) do
defaultEnv[k] = v
end
2017-10-08 21:45:01 +00:00
_G.requireInjector()
2016-12-11 19:24:52 +00:00
local Config = require('config')
2017-10-15 23:55:05 +00:00
local Input = require('input')
2017-09-06 01:21:43 +00:00
local Opus = require('opus')
local Util = require('util')
2016-12-11 19:24:52 +00:00
2017-10-08 21:45:01 +00:00
local colors = _G.colors
local fs = _G.fs
local keys = _G.keys
local multishell = _ENV.multishell
local os = _G.os
local printError = _G.printError
local term = _G.term
local window = _G.window
2017-10-05 17:07:48 +00:00
2016-12-11 19:24:52 +00:00
local parentTerm = term.current()
local w,h = parentTerm.getSize()
2017-10-15 06:36:54 +00:00
local tabs = { }
2016-12-11 19:24:52 +00:00
local currentTab
local _tabId = 0
local overviewTab
local runningTab
local tabsDirty = false
2017-09-30 02:30:01 +00:00
local closeInd = '*'
2017-10-14 07:41:54 +00:00
local hooks = { }
2017-10-15 06:36:54 +00:00
local hotkeys = { }
local downState = { }
2017-09-30 02:30:01 +00:00
2017-10-08 21:45:01 +00:00
multishell.term = term.current()
-- Default label
if not os.getComputerLabel() then
local id = os.getComputerID()
if _G.turtle then
os.setComputerLabel('turtle_' .. id)
elseif _G.pocket then
os.setComputerLabel('pocket_' .. id)
elseif _G.commands then
os.setComputerLabel('command_' .. id)
else
os.setComputerLabel('computer_' .. id)
end
end
2017-10-15 23:55:05 +00:00
if Util.getVersion() >= 1.76 then
2017-09-30 02:30:01 +00:00
closeInd = '\215'
end
2016-12-11 19:24:52 +00:00
local config = {
standard = {
2017-09-26 05:40:02 +00:00
textColor = colors.lightGray,
tabBarTextColor = colors.lightGray,
2017-09-26 03:23:10 +00:00
focusTextColor = colors.white,
2017-09-26 05:40:02 +00:00
backgroundColor = colors.gray,
tabBarBackgroundColor = colors.gray,
2016-12-11 19:24:52 +00:00
focusBackgroundColor = colors.gray,
},
color = {
2017-09-26 02:49:44 +00:00
textColor = colors.lightGray,
2016-12-11 19:24:52 +00:00
tabBarTextColor = colors.lightGray,
2017-09-26 05:40:02 +00:00
focusTextColor = colors.white,
backgroundColor = colors.gray,
tabBarBackgroundColor = colors.gray,
focusBackgroundColor = colors.gray,
2016-12-11 19:24:52 +00:00
},
}
Config.load('multishell', config)
local _colors = config.standard
if parentTerm.isColor() then
_colors = config.color
end
local function redrawMenu()
if not tabsDirty then
2017-10-15 06:36:54 +00:00
os.queueEvent('multishell_redraw')
2016-12-11 19:24:52 +00:00
tabsDirty = true
end
end
2017-10-15 23:55:05 +00:00
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
2017-10-12 02:39:04 +00:00
local function selectTab(tab)
2016-12-11 19:24:52 +00:00
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)
2017-10-15 23:55:05 +00:00
if coroutine.status(currentTab.co) == 'suspended' then
-- the process that opens a new tab won't get the lose focus event
-- os.queueEvent('multishell_notifyfocus', currentTab.tabId)
resumeTab(currentTab, 'multishell_losefocus')
end
2016-12-11 19:24:52 +00:00
if tab and not currentTab.hidden then
tab.previousTabId = currentTab.tabId
end
end
2017-10-15 23:55:05 +00:00
if tab ~= currentTab then
2016-12-11 19:24:52 +00:00
currentTab = tab
tab.window.setVisible(true)
2017-10-15 23:55:05 +00:00
resumeTab(tab, 'multishell_focus')
2016-12-11 19:24:52 +00:00
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
2017-09-27 19:42:40 +00:00
result, err = Util.run(tab.env, tab.path, table.unpack(tab.args or { } ))
2016-12-11 19:24:52 +00:00
else
err = 'multishell: invalid tab'
end
2017-09-27 19:42:40 +00:00
if not result and err and err ~= 'Terminated' then
2016-12-11 19:24:52 +00:00
if err then
printError(tostring(err))
end
2017-09-26 02:49:44 +00:00
printError('Press enter to close')
2016-12-11 19:24:52 +00:00
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
2017-10-15 06:36:54 +00:00
-- os.queueEvent('multishell', 'terminate')
2016-12-11 19:24:52 +00:00
end
break
end
end
end
tabs[tab.tabId] = nil
if tab == currentTab then
local previousTab
if tab.previousTabId then
previousTab = tabs[tab.previousTabId]
2017-10-12 02:39:04 +00:00
if previousTab and previousTab.hidden then
previousTab = nil
end
2016-12-11 19:24:52 +00:00
end
selectTab(previousTab)
end
redrawMenu()
end)
tabs[tab.tabId] = tab
resumeTab(tab)
return tab
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
2017-10-15 23:55:05 +00:00
function multishell.setTitle(tabId, title)
2016-12-11 19:24:52 +00:00
local tab = tabs[tabId]
if tab then
2017-10-15 23:55:05 +00:00
tab.title = title or ''
2016-12-11 19:24:52 +00:00
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)
2017-10-15 06:36:54 +00:00
os.queueEvent('multishell_terminate', tabId)
--[[
2016-12-11 19:24:52 +00:00
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
2017-10-15 06:36:54 +00:00
]]
2016-12-11 19:24:52 +00:00
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
2017-10-05 17:07:48 +00:00
2016-12-11 19:24:52 +00:00
return tab.tabId
end
function multishell.hideTab(tabId)
local tab = tabs[tabId]
if tab then
tab.hidden = true
2017-10-11 15:37:52 +00:00
if currentTab.tabId == tabId then
2017-10-12 02:39:04 +00:00
selectTab(tabs[currentTab.previousTabId])
2017-10-11 15:37:52 +00:00
end
2016-12-11 19:24:52 +00:00
redrawMenu()
end
end
function multishell.unhideTab(tabId)
local tab = tabs[tabId]
if tab then
tab.hidden = false
redrawMenu()
end
end
function multishell.getCount()
2017-10-08 21:45:01 +00:00
return Util.size(tabs)
2016-12-11 19:24:52 +00:00
end
2017-10-14 07:41:54 +00:00
function multishell.hook(event, fn)
2017-10-15 06:36:54 +00:00
if type(event) == 'table' then
2017-10-14 07:41:54 +00:00
for _,v in pairs(event) do
multishell.hook(v, fn)
end
else
if not hooks[event] then
hooks[event] = { }
end
2017-10-15 23:55:05 +00:00
table.insert(hooks[event], fn)
end
end
-- you can only unhook from within the function that hooked
function multishell.unhook(event, fn)
local eventHooks = hooks[event]
if eventHooks then
Util.removeByValue(eventHooks, fn)
if #eventHooks == 0 then
hooks[event] = nil
end
2017-10-14 07:41:54 +00:00
end
end
2017-10-15 06:36:54 +00:00
multishell.hook('multishell_terminate', function(_, eventData)
local tabId = eventData[1] or -1
2016-12-11 19:24:52 +00:00
local tab = tabs[tabId]
2017-10-15 06:36:54 +00:00
if tab and not tab.isOverview then
if coroutine.status(tab.co) ~= 'dead' then
resumeTab(tab, "terminate")
end
2016-12-11 19:24:52 +00:00
end
2017-10-15 06:36:54 +00:00
return true
2016-12-11 19:24:52 +00:00
end)
2017-10-15 06:36:54 +00:00
multishell.hook('multishell_redraw', function()
tabsDirty = false
2017-10-15 23:55:05 +00:00
local function write(x, text, bg, fg)
parentTerm.setBackgroundColor(bg)
parentTerm.setTextColor(fg)
parentTerm.setCursorPos(x, 1)
parentTerm.write(text)
end
local bg = _colors.tabBarBackgroundColor
parentTerm.setBackgroundColor(bg)
parentTerm.setCursorPos(1, 1)
parentTerm.clearLine()
2017-10-15 06:36:54 +00:00
if currentTab and currentTab.isOverview then
2017-10-15 23:55:05 +00:00
write(1, '+', bg, _colors.focusTextColor)
2017-10-15 06:36:54 +00:00
else
2017-10-15 23:55:05 +00:00
write(1, '+', bg, _colors.tabBarTextColor)
2017-10-15 06:36:54 +00:00
end
local tabX = 2
2016-12-11 19:24:52 +00:00
local function compareTab(a, b)
return a.tabId < b.tabId
end
for _,tab in Util.spairs(tabs, compareTab) do
2017-10-15 06:36:54 +00:00
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
2016-12-11 19:24:52 +00:00
end
end
2017-10-15 06:36:54 +00:00
for _,tab in Util.spairs(tabs) do
if tab.sx then
if tab == currentTab then
2017-10-15 23:55:05 +00:00
write(tab.sx, tab.title, _colors.focusBackgroundColor, _colors.focusTextColor)
2017-10-15 06:36:54 +00:00
else
2017-10-15 23:55:05 +00:00
write(tab.sx, tab.title, _colors.backgroundColor, _colors.textColor)
2017-10-15 06:36:54 +00:00
end
end
end
2017-10-15 23:55:05 +00:00
2017-10-15 06:36:54 +00:00
if currentTab and not currentTab.isOverview then
2017-10-15 23:55:05 +00:00
write(w, closeInd, _colors.backgroundColor, _colors.focusTextColor)
2017-10-15 06:36:54 +00:00
end
if currentTab then
currentTab.window.restoreCursor()
end
return true
end)
multishell.hook('term_resize', function(_, eventData)
if not eventData[1] then --- TEST
w,h = parentTerm.getSize()
local windowY = 2
local windowHeight = h-1
for _,key in pairs(Util.keys(tabs)) 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 )
2016-12-11 19:24:52 +00:00
end
2017-10-15 06:36:54 +00:00
tab.window.reposition( 1, windowY, w, windowHeight )
2016-12-11 19:24:52 +00:00
end
2017-10-15 06:36:54 +00:00
redrawMenu()
end
end)
2017-10-15 23:55:05 +00:00
-- downstate should be stored in the tab (maybe)
2017-10-15 06:36:54 +00:00
multishell.hook('key_up', function(_, eventData)
local code = eventData[1]
if downState[code] ~= currentTab then
downState[code] = nil
return true
end
downState[code] = nil
end)
multishell.hook('key', function(_, eventData)
local code = eventData[1]
local firstPress = not eventData[2]
if firstPress then
downState[code] = currentTab
else
--key was pressed initially in a previous window
if downState[code] ~= currentTab then
return true
end
2016-12-11 19:24:52 +00:00
end
end)
2017-10-15 23:55:05 +00:00
multishell.hook({ 'key', 'key_up', 'char', 'paste' }, function(event, eventData)
local code = Input:translate(event, eventData[1], eventData[2])
if code and hotkeys[code] then
hotkeys[code](event, eventData)
end
end)
multishell.hook('mouse_click', function(_, eventData)
2017-10-15 06:36:54 +00:00
local x, y = eventData[2], eventData[3]
if y == 1 then
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
downState.mouse = nil
return true
end
downState.mouse = currentTab
eventData[3] = eventData[3] - 1
end)
multishell.hook({ 'mouse_up', 'mouse_drag' }, function(event, eventData)
if downState.mouse ~= currentTab then
-- don't send mouse up as the mouse click event was on another window
if event == 'mouse_up' then
downState.mouse = nil
end
return true -- stop propagation
end
eventData[3] = eventData[3] - 1
end)
2017-10-15 23:55:05 +00:00
multishell.hook('mouse_scroll', function(_, eventData)
2017-10-15 06:36:54 +00:00
local dir, y = eventData[1], eventData[3]
if y == 1 then
return true
end
if currentTab.terminal.scrollUp then
if dir == -1 then
currentTab.terminal.scrollUp()
else
currentTab.terminal.scrollDown()
end
end
eventData[3] = y - 1
end)
2016-12-11 19:24:52 +00:00
local function startup()
local hasError
if not Opus.loadServices() then
hasError = true
end
2016-12-11 19:24:52 +00:00
local overviewId = multishell.openTab({
2017-05-20 22:27:26 +00:00
path = 'sys/apps/Overview.lua',
2016-12-11 19:24:52 +00:00
focused = true,
hidden = true,
isOverview = true,
})
overviewTab = tabs[overviewId]
2017-09-06 01:21:43 +00:00
if not Opus.autorun() then
hasError = true
end
2016-12-11 19:24:52 +00:00
if hasError then
2017-09-26 02:49:44 +00:00
print()
2016-12-11 19:24:52 +00:00
error('An autorun program has errored')
end
end
multishell.openTab({
focused = true,
fn = startup,
env = defaultEnv,
title = 'Autorun',
})
if not currentTab then
multishell.setFocus(overviewTab.tabId)
end
2017-10-15 06:36:54 +00:00
redrawMenu()
2016-12-11 19:24:52 +00:00
2017-10-15 06:36:54 +00:00
local currentTabEvents = Util.transpose {
'char', 'key', 'key_up',
'mouse_click', 'mouse_drag', 'mouse_scroll', 'mouse_up',
'paste', 'terminate',
}
2017-05-26 01:06:17 +00:00
2016-12-11 19:24:52 +00:00
while true do
local tEventData = { os.pullEventRaw() }
local sEvent = table.remove(tEventData, 1)
2017-10-15 06:36:54 +00:00
local stopPropagation
2017-01-02 02:46:37 +00:00
2017-10-15 06:36:54 +00:00
local eventHooks = hooks[sEvent]
if eventHooks then
2017-10-15 23:55:05 +00:00
for i = #eventHooks, 1, -1 do
stopPropagation = eventHooks[i](sEvent, tEventData)
2017-10-15 06:36:54 +00:00
if stopPropagation then
break
2016-12-11 19:24:52 +00:00
end
2017-05-26 01:06:17 +00:00
end
2017-10-15 06:36:54 +00:00
end
2017-05-26 01:06:17 +00:00
2017-10-15 06:36:54 +00:00
if not stopPropagation then
if currentTabEvents[sEvent] then
resumeTab(currentTab, sEvent, tEventData)
2016-12-11 19:24:52 +00:00
2017-10-15 06:36:54 +00:00
else
-- Passthrough to all processes
for _,key in pairs(Util.keys(tabs)) do
resumeTab(tabs[key], sEvent, tEventData)
2016-12-11 19:24:52 +00:00
end
end
end
end