mirror of
				https://github.com/kepler155c/opus
				synced 2025-10-30 23:23:03 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			605 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
			
		
		
	
	
			605 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, desc, open)
 | |
|     if not fs.exists(directory) then
 | |
|       return
 | |
|     end
 | |
| 
 | |
|     local files = fs.list(directory)
 | |
|     table.sort(files)
 | |
| 
 | |
|     for _,file in ipairs(files) do
 | |
|       print(desc .. file)
 | |
|       os.sleep(0)
 | |
|       local result, err = open(directory .. '/' .. file)
 | |
|       if not result then
 | |
|         printError(err)
 | |
|         hasError = true
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   runDir('/sys/extensions', '[ ext ] ', shell.run)
 | |
| 
 | |
|   local overviewId = multishell.openTab({
 | |
|     path = '/apps/Overview.lua',
 | |
|     focused = true,
 | |
|     hidden = true,
 | |
|     isOverview = true,
 | |
|   })
 | |
|   overviewTab = tabs[overviewId]
 | |
| 
 | |
|   runDir('/sys/services', '[ svc ] ', shell.openHiddenTab)
 | |
|   runDir('/autorun', '[ aut ] ', 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 == 'key_up' then
 | |
|     processKeyEvent(sEvent, tEventData[1])
 | |
|   end
 | |
| 
 | |
|   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
 | 
