local defaultEnv = { } for k,v in pairs(_ENV) do defaultEnv[k] = v end _G.requireInjector() local Config = require('config') local Opus = require('opus') local Util = require('util') 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 local parentTerm = term.current() local w,h = parentTerm.getSize() local tabs = { } local currentTab local _tabId = 0 local overviewTab local runningTab local tabsDirty = false local closeInd = '*' local redrawTimer local hooks = { } local control local hotkeys = { } local downState = { } 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 if Util.getVersion() >= 1.79 then closeInd = '\215' end local config = { standard = { textColor = colors.lightGray, tabBarTextColor = colors.lightGray, focusTextColor = colors.white, backgroundColor = colors.gray, tabBarBackgroundColor = colors.gray, focusBackgroundColor = colors.gray, }, color = { textColor = colors.lightGray, tabBarTextColor = colors.lightGray, focusTextColor = colors.white, backgroundColor = colors.gray, tabBarBackgroundColor = colors.gray, focusBackgroundColor = colors.gray, }, } Config.load('multishell', config) local _colors = config.standard if parentTerm.isColor() then _colors = config.color end local function redrawMenu() if not tabsDirty then os.queueEvent('multishell_redraw') tabsDirty = true 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 = Util.run(tab.env, tab.path, table.unpack(tab.args or { } )) else err = 'multishell: invalid tab' end if not result and err and err ~= 'Terminated' then if err then printError(tostring(err)) end printError('Press enter to close') 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] if previousTab and previousTab.hidden then previousTab = nil end 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 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) os.queueEvent('multishell_terminate', tabId) --[[ 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 if currentTab.tabId == tabId then selectTab(tabs[currentTab.previousTabId]) end redrawMenu() end end function multishell.unhideTab(tabId) local tab = tabs[tabId] if tab then tab.hidden = false redrawMenu() end end function multishell.getCount() return Util.size(tabs) end function multishell.showMessage(text) parentTerm.setCursorPos(3, 1) parentTerm.setTextColor(_colors.textColor) parentTerm.setBackgroundColor(_colors.backgroundColor) if #text + 3 < w then text = text .. string.rep(' ', w - #text - 3) end parentTerm.write(text) redrawTimer = os.startTimer(2) if currentTab then currentTab.window.restoreCursor() end end function multishell.hook(event, fn) if type(event) == 'table' then for _,v in pairs(event) do multishell.hook(v, fn) end else if not hooks[event] then hooks[event] = { } end table.insert(hooks[event], 1, fn) end end multishell.hook('multishell_terminate', function(_, eventData) local tabId = eventData[1] or -1 local tab = tabs[tabId] if tab and not tab.isOverview then if coroutine.status(tab.co) ~= 'dead' then resumeTab(tab, "terminate") end end return true end) multishell.hook('multishell_redraw', function() 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.focusTextColor) parentTerm.setBackgroundColor(_colors.backgroundColor) parentTerm.setCursorPos( w, 1 ) parentTerm.write(closeInd) 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 ) end tab.window.reposition( 1, windowY, w, windowHeight ) end redrawMenu() end end) -- downstate should be stored in the tab multishell.hook('key_up', function(_, eventData) local code = eventData[1] if code == keys.leftCtrl or code == keys.rightCtrl then control = false end if downState[code] ~= currentTab then downState[code] = nil return true end downState[code] = nil end) multishell.hook('char', function() control = false -- is this right ?? end) multishell.hook('key', function(_, eventData) local code = eventData[1] local firstPress = not eventData[2] if firstPress then downState[code] = currentTab 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 else --key was pressed initially in a previous window if downState[code] ~= currentTab then return true end end end) multishell.hook({ 'mouse_click' }, function(_, eventData) 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) multishell.hook({ 'mouse_scroll' }, function(_, eventData) 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) local function startup() local hasError if not Opus.loadServices() then hasError = true end local overviewId = multishell.openTab({ path = 'sys/apps/Overview.lua', focused = true, hidden = true, isOverview = true, }) overviewTab = tabs[overviewId] if not Opus.autorun() then hasError = true end if hasError then print() 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 redrawMenu() local currentTabEvents = Util.transpose { 'char', 'key', 'key_up', 'mouse_click', 'mouse_drag', 'mouse_scroll', 'mouse_up', 'paste', 'terminate', } while true do local tEventData = { os.pullEventRaw() } local sEvent = table.remove(tEventData, 1) local stopPropagation if sEvent == 'timer' and tEventData[1] == redrawTimer then redrawMenu() end local eventHooks = hooks[sEvent] if eventHooks then for _,fn in pairs(eventHooks) do stopPropagation = fn(sEvent, tEventData) if stopPropagation then break end end end if not stopPropagation then if currentTabEvents[sEvent] then resumeTab(currentTab, sEvent, tEventData) else -- Passthrough to all processes for _,key in pairs(Util.keys(tabs)) do resumeTab(tabs[key], sEvent, tEventData) end end end end