-- 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