local sandboxEnv = { } for k,v in pairs(_ENV) do sandboxEnv[k] = v end _G.requireInjector() local Config = require('config') local Util = require('util') local colors = _G.colors local fs = _G.fs local kernel = _G.kernel local keyboard = _G.device.keyboard local keys = _G.keys local multishell = _ENV.multishell local os = _G.os local printError = _G.printError local shell = _ENV.shell 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 overviewId local runningTab local tabsDirty = false local closeInd = '*' 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.76 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 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 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 = tabs[overviewId] end if currentTab and currentTab ~= tab then currentTab.window.setVisible(false) --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 if tab and not currentTab.hidden then tab.previousTabId = currentTab.tabId end end if tab ~= currentTab then currentTab = tab tab.window.setVisible(true) Util.clear(keyboard.state) --- reset keyboard state -- why not just queue event with tab ID if we want to notify --resumeTab(tab, 'multishell_focus') 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 sandboxEnv) 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 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.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, title) local tab = tabs[tabId] if tab then if not tab.isOverview then tab.title = title or '' end 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) 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 kernel.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) kernel.hook('multishell_redraw', function() tabsDirty = false 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() local function compareTab(a, b) return a.tabId < b.tabId end for _,tab in pairs(tabs) do if tab.hidden and tab ~= currentTab then tab.width = 0 else tab.width = #tab.title + 1 end end local function width() local tw = 0 Util.each(tabs, function(t) tw = tw + t.width end) return tw end while width() > w - 3 do local tab = select(2, Util.spairs(tabs, function(a, b) return a.width > b.width end)()) tab.width = tab.width - 1 end local tabX = 0 for _,tab in Util.spairs(tabs, compareTab) do if tab.width > 0 then tab.sx = tabX + 1 tab.ex = tabX + tab.width tabX = tabX + tab.width if tab ~= currentTab then write(tab.sx, tab.title:sub(1, tab.width - 1), _colors.backgroundColor, _colors.textColor) end end end if currentTab then write(currentTab.sx - 1, ' ' .. currentTab.title:sub(1, currentTab.width - 1) .. ' ', _colors.focusBackgroundColor, _colors.focusTextColor) if not currentTab.isOverview then write(w, closeInd, _colors.backgroundColor, _colors.focusTextColor) end end if currentTab then currentTab.window.restoreCursor() end return true end) kernel.hook('term_resize', function(_, eventData) if not eventData[1] then --- TEST w,h = parentTerm.getSize() 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, 2, w, windowHeight) end redrawMenu() end end) --[[ kernel.hook('key_up', function(_, eventData) local code = eventData[1] if not keyboard.state[code] then return true end end) ]] kernel.hook('mouse_click', function(_, eventData) local x, y = eventData[2], eventData[3] if y == 1 then if x == 1 then multishell.setFocus(overviewId) 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) kernel.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) kernel.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 success = true local function runDir(directory, open) if not fs.exists(directory) then return true end local files = fs.list(directory) table.sort(files) for _,file in ipairs(files) do os.sleep(0) local result, err = open(directory .. '/' .. file) if result then if term.isColor() then term.setTextColor(colors.green) end term.write('[PASS] ') term.setTextColor(colors.white) term.write(fs.combine(directory, file)) else if term.isColor() then term.setTextColor(colors.red) end term.write('[FAIL] ') term.setTextColor(colors.white) term.write(fs.combine(directory, file)) if err then _G.printError(err) end success = false end print() end end runDir('sys/services', shell.openHiddenTab) runDir('sys/autorun', shell.run) runDir('usr/autorun', shell.run) if not success then print() error('An autorun program has errored') end end overviewId = multishell.openTab({ path = 'sys/apps/Overview.lua', isOverview = true, }) tabs[overviewId].title = '+' multishell.openTab({ focused = true, fn = startup, title = 'Autorun', }) 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 local eventHooks = kernel.hooks[sEvent] if eventHooks then for i = #eventHooks, 1, -1 do stopPropagation = eventHooks[i](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