上传文件至 /
This commit is contained in:
704
multishell.lua
Normal file
704
multishell.lua
Normal file
@@ -0,0 +1,704 @@
|
|||||||
|
-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: LicenseRef-CCPL
|
||||||
|
|
||||||
|
--- Multishell allows multiple programs to be run at the same time.
|
||||||
|
--
|
||||||
|
-- When multiple programs are running, it displays a tab bar at the top of the
|
||||||
|
-- screen, which allows you to switch between programs. New programs can be
|
||||||
|
-- launched using the `fg` or `bg` programs, or using the [`shell.openTab`] and
|
||||||
|
-- [`multishell.launch`] functions.
|
||||||
|
--
|
||||||
|
-- Each process is identified by its ID, which corresponds to its position in
|
||||||
|
-- the tab list. As tabs may be opened and closed, this ID is _not_ constant
|
||||||
|
-- over a program's run. As such, be careful not to use stale IDs.
|
||||||
|
--
|
||||||
|
-- As with [`shell`], [`multishell`] is not a "true" API. Instead, it is a
|
||||||
|
-- standard program, which launches a shell and injects its API into the shell's
|
||||||
|
-- environment. This API is not available in the global environment, and so is
|
||||||
|
-- not available to [APIs][`os.loadAPI`].
|
||||||
|
--
|
||||||
|
-- @module[module] multishell
|
||||||
|
-- @since 1.6
|
||||||
|
|
||||||
|
local expect = dofile("rom/modules/main/cc/expect.lua").expect
|
||||||
|
|
||||||
|
-- Setup process switching
|
||||||
|
local parentTerm = term.current()
|
||||||
|
local w, h = parentTerm.getSize()
|
||||||
|
|
||||||
|
local tProcesses = {}
|
||||||
|
local nCurrentProcess = nil
|
||||||
|
local nRunningProcess = nil
|
||||||
|
local bShowMenu = false
|
||||||
|
local bWindowsResized = false
|
||||||
|
local nScrollPos = 1
|
||||||
|
local bScrollRight = false
|
||||||
|
|
||||||
|
-- Helper: count visible (non-hidden) processes
|
||||||
|
local function setVisibleProcessCount()
|
||||||
|
local count = 0
|
||||||
|
for _, p in ipairs(tProcesses) do
|
||||||
|
if not p.bHideTab then
|
||||||
|
count = count + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return count
|
||||||
|
end
|
||||||
|
|
||||||
|
local function selectProcess(n)
|
||||||
|
if nCurrentProcess ~= n then
|
||||||
|
if nCurrentProcess then
|
||||||
|
local tOldProcess = tProcesses[nCurrentProcess]
|
||||||
|
tOldProcess.window.setVisible(false)
|
||||||
|
end
|
||||||
|
nCurrentProcess = n
|
||||||
|
if nCurrentProcess then
|
||||||
|
local tNewProcess = tProcesses[nCurrentProcess]
|
||||||
|
tNewProcess.window.setVisible(true)
|
||||||
|
tNewProcess.bInteracted = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function setProcessTitle(n, sTitle)
|
||||||
|
tProcesses[n].sTitle = sTitle
|
||||||
|
end
|
||||||
|
|
||||||
|
local function resumeProcess(nProcess, sEvent, ...)
|
||||||
|
local tProcess = tProcesses[nProcess]
|
||||||
|
local sFilter = tProcess.sFilter
|
||||||
|
if sFilter == nil or sFilter == sEvent or sEvent == "terminate" then
|
||||||
|
local nPreviousProcess = nRunningProcess
|
||||||
|
nRunningProcess = nProcess
|
||||||
|
term.redirect(tProcess.terminal)
|
||||||
|
local ok, result = coroutine.resume(tProcess.co, sEvent, ...)
|
||||||
|
tProcess.terminal = term.current()
|
||||||
|
if ok then
|
||||||
|
tProcess.sFilter = result
|
||||||
|
else
|
||||||
|
printError(result)
|
||||||
|
end
|
||||||
|
nRunningProcess = nPreviousProcess
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function launchProcess(bFocus, tProgramEnv, sProgramPath, ...)
|
||||||
|
local tProgramArgs = table.pack(...)
|
||||||
|
local nProcess = #tProcesses + 1
|
||||||
|
local tProcess = {}
|
||||||
|
tProcess.sProgramPath = sProgramPath
|
||||||
|
tProcess.env = tProgramEnv
|
||||||
|
tProcess.args = tProgramArgs
|
||||||
|
|
||||||
|
tProcess.sTitle = fs.getName(sProgramPath)
|
||||||
|
if bShowMenu then
|
||||||
|
tProcess.window = window.create(parentTerm, 1, 2, w, h - 1, false)
|
||||||
|
else
|
||||||
|
tProcess.window = window.create(parentTerm, 1, 1, w, h, false)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Restrict the public view of the window to normal redirect functions.
|
||||||
|
tProcess.terminal = {}
|
||||||
|
for k in pairs(term.native()) do tProcess.terminal[k] = tProcess.window[k] end
|
||||||
|
|
||||||
|
tProcess.co = coroutine.create(function()
|
||||||
|
os.run(tProgramEnv, sProgramPath, table.unpack(tProgramArgs, 1, tProgramArgs.n))
|
||||||
|
if not tProcess.bInteracted then
|
||||||
|
term.setCursorBlink(false)
|
||||||
|
print("Press any key to continue")
|
||||||
|
os.pullEvent("char")
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
tProcess.sFilter = nil
|
||||||
|
tProcess.bInteracted = false
|
||||||
|
tProcess.bHideTab = false
|
||||||
|
tProcesses[nProcess] = tProcess
|
||||||
|
if bFocus then
|
||||||
|
selectProcess(nProcess)
|
||||||
|
end
|
||||||
|
resumeProcess(nProcess)
|
||||||
|
return nProcess
|
||||||
|
end
|
||||||
|
|
||||||
|
local function cullProcess(nProcess)
|
||||||
|
local tProcess = tProcesses[nProcess]
|
||||||
|
if coroutine.status(tProcess.co) == "dead" then
|
||||||
|
if nCurrentProcess == nProcess then
|
||||||
|
selectProcess(nil)
|
||||||
|
end
|
||||||
|
table.remove(tProcesses, nProcess)
|
||||||
|
if nCurrentProcess == nil then
|
||||||
|
if nProcess > 1 then
|
||||||
|
selectProcess(nProcess - 1)
|
||||||
|
elseif #tProcesses > 0 then
|
||||||
|
selectProcess(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if nScrollPos ~= 1 then
|
||||||
|
nScrollPos = nScrollPos - 1
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function cullProcesses()
|
||||||
|
local culled = false
|
||||||
|
for n = #tProcesses, 1, -1 do
|
||||||
|
culled = culled or cullProcess(n)
|
||||||
|
end
|
||||||
|
return culled
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Setup the main menu
|
||||||
|
local menuMainTextColor, menuMainBgColor, menuOtherTextColor, menuOtherBgColor
|
||||||
|
if parentTerm.isColor() then
|
||||||
|
menuMainTextColor, menuMainBgColor = colors.yellow, colors.black
|
||||||
|
menuOtherTextColor, menuOtherBgColor = colors.black, colors.gray
|
||||||
|
else
|
||||||
|
menuMainTextColor, menuMainBgColor = colors.white, colors.black
|
||||||
|
menuOtherTextColor, menuOtherBgColor = colors.black, colors.gray
|
||||||
|
end
|
||||||
|
|
||||||
|
local function redrawMenu()
|
||||||
|
if not bShowMenu then return end
|
||||||
|
|
||||||
|
-- Build list of visible processes
|
||||||
|
local visibleProcesses = {}
|
||||||
|
for i, p in ipairs(tProcesses) do
|
||||||
|
if not p.bHideTab then
|
||||||
|
table.insert(visibleProcesses, { index = i, proc = p })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
parentTerm.setCursorPos(1, 1)
|
||||||
|
parentTerm.setBackgroundColor(menuOtherBgColor)
|
||||||
|
parentTerm.clearLine()
|
||||||
|
|
||||||
|
local nCharCount = 0
|
||||||
|
local nSize = parentTerm.getSize()
|
||||||
|
local totalVisible = #visibleProcesses
|
||||||
|
|
||||||
|
-- Adjust scroll if out of bounds
|
||||||
|
if nScrollPos < 1 then nScrollPos = 1 end
|
||||||
|
if nScrollPos > totalVisible then nScrollPos = math.max(1, totalVisible) end
|
||||||
|
|
||||||
|
if nScrollPos > 1 then
|
||||||
|
parentTerm.setTextColor(menuOtherTextColor)
|
||||||
|
parentTerm.setBackgroundColor(menuOtherBgColor)
|
||||||
|
parentTerm.write("<")
|
||||||
|
nCharCount = 1
|
||||||
|
end
|
||||||
|
|
||||||
|
for i = nScrollPos, totalVisible do
|
||||||
|
local item = visibleProcesses[i]
|
||||||
|
local isCurrent = (nCurrentProcess == item.index)
|
||||||
|
if isCurrent then
|
||||||
|
parentTerm.setTextColor(menuMainTextColor)
|
||||||
|
parentTerm.setBackgroundColor(menuMainBgColor)
|
||||||
|
else
|
||||||
|
parentTerm.setTextColor(menuOtherTextColor)
|
||||||
|
parentTerm.setBackgroundColor(menuOtherBgColor)
|
||||||
|
end
|
||||||
|
parentTerm.write(" " .. item.proc.sTitle .. " ")
|
||||||
|
nCharCount = nCharCount + #item.proc.sTitle + 2
|
||||||
|
end
|
||||||
|
|
||||||
|
if nCharCount > nSize then
|
||||||
|
parentTerm.setTextColor(menuOtherTextColor)
|
||||||
|
parentTerm.setBackgroundColor(menuOtherBgColor)
|
||||||
|
parentTerm.setCursorPos(nSize, 1)
|
||||||
|
parentTerm.write(">")
|
||||||
|
bScrollRight = true
|
||||||
|
else
|
||||||
|
bScrollRight = false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Restore cursor for current process
|
||||||
|
if nCurrentProcess and tProcesses[nCurrentProcess] then
|
||||||
|
tProcesses[nCurrentProcess].window.restoreCursor()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function resizeWindows()
|
||||||
|
local windowY, windowHeight
|
||||||
|
if bShowMenu then
|
||||||
|
windowY = 2
|
||||||
|
windowHeight = h - 1
|
||||||
|
else
|
||||||
|
windowY = 1
|
||||||
|
windowHeight = h
|
||||||
|
end
|
||||||
|
for n = 1, #tProcesses do
|
||||||
|
local tProcess = tProcesses[n]
|
||||||
|
local x, y = tProcess.window.getCursorPos()
|
||||||
|
if y > windowHeight then
|
||||||
|
tProcess.window.scroll(y - windowHeight)
|
||||||
|
tProcess.window.setCursorPos(x, windowHeight)
|
||||||
|
end
|
||||||
|
tProcess.window.reposition(1, windowY, w, windowHeight)
|
||||||
|
end
|
||||||
|
bWindowsResized = true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function setMenuVisible(bVis)
|
||||||
|
if bShowMenu ~= bVis then
|
||||||
|
bShowMenu = bVis
|
||||||
|
resizeWindows()
|
||||||
|
redrawMenu()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local multishell = {} --- @export
|
||||||
|
|
||||||
|
--- Get the currently visible process. This will be the one selected on
|
||||||
|
-- the tab bar.
|
||||||
|
--
|
||||||
|
-- Note, this is different to [`getCurrent`], which returns the process which is
|
||||||
|
-- currently executing.
|
||||||
|
--
|
||||||
|
-- @treturn number The currently visible process's index.
|
||||||
|
-- @see setFocus
|
||||||
|
function multishell.getFocus()
|
||||||
|
return nCurrentProcess
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Change the currently visible process.
|
||||||
|
--
|
||||||
|
-- @tparam number n The process index to switch to.
|
||||||
|
-- @treturn boolean If the process was changed successfully. This will
|
||||||
|
-- return [`false`] if there is no process with this id.
|
||||||
|
-- @see getFocus
|
||||||
|
function multishell.setFocus(n)
|
||||||
|
expect(1, n, "number")
|
||||||
|
if n >= 1 and n <= #tProcesses then
|
||||||
|
selectProcess(n)
|
||||||
|
redrawMenu()
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Get the title of the given tab.
|
||||||
|
--
|
||||||
|
-- This starts as the name of the program, but may be changed using
|
||||||
|
-- [`multishell.setTitle`].
|
||||||
|
-- @tparam number n The process index.
|
||||||
|
-- @treturn string|nil The current process title, or [`nil`] if the
|
||||||
|
-- process doesn't exist.
|
||||||
|
function multishell.getTitle(n)
|
||||||
|
expect(1, n, "number")
|
||||||
|
if n >= 1 and n <= #tProcesses then
|
||||||
|
return tProcesses[n].sTitle
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Set the title of the given process.
|
||||||
|
--
|
||||||
|
-- @tparam number n The process index.
|
||||||
|
-- @tparam string title The new process title.
|
||||||
|
-- @see getTitle
|
||||||
|
-- @usage Change the title of the current process
|
||||||
|
--
|
||||||
|
-- multishell.setTitle(multishell.getCurrent(), "Hello")
|
||||||
|
function multishell.setTitle(n, title)
|
||||||
|
expect(1, n, "number")
|
||||||
|
expect(2, title, "string")
|
||||||
|
if n >= 1 and n <= #tProcesses then
|
||||||
|
setProcessTitle(n, title)
|
||||||
|
redrawMenu()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Get the index of the currently running process.
|
||||||
|
--
|
||||||
|
-- @treturn number The currently running process.
|
||||||
|
function multishell.getCurrent()
|
||||||
|
return nRunningProcess
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Start a new process, with the given environment, program and arguments.
|
||||||
|
--
|
||||||
|
-- The returned process index is not constant over the program's run. It can be
|
||||||
|
-- safely used immediately after launching (for instance, to update the title or
|
||||||
|
-- switch to that tab). However, after your program has yielded, it may no
|
||||||
|
-- longer be correct.
|
||||||
|
--
|
||||||
|
-- @tparam table tProgramEnv The environment to load the path under.
|
||||||
|
-- @tparam string sProgramPath The path to the program to run.
|
||||||
|
-- @param ... Additional arguments to pass to the program.
|
||||||
|
-- @treturn number The index of the created process.
|
||||||
|
-- @see os.run
|
||||||
|
-- @usage Run the "hello" program, and set its title to "Hello!"
|
||||||
|
--
|
||||||
|
-- local id = multishell.launch({}, "/rom/programs/fun/hello.lua")
|
||||||
|
-- multishell.setTitle(id, "Hello!")
|
||||||
|
function multishell.launch(tProgramEnv, sProgramPath, ...)
|
||||||
|
expect(1, tProgramEnv, "table")
|
||||||
|
expect(2, sProgramPath, "string")
|
||||||
|
local previousTerm = term.current()
|
||||||
|
|
||||||
|
local nResult = launchProcess(false, tProgramEnv, sProgramPath, ...)
|
||||||
|
redrawMenu()
|
||||||
|
setMenuVisible(setVisibleProcessCount() >= 2)
|
||||||
|
term.redirect(previousTerm)
|
||||||
|
|
||||||
|
return nResult
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Get the number of processes within this multishell.
|
||||||
|
--
|
||||||
|
-- @treturn number The number of processes.
|
||||||
|
function multishell.getCount()
|
||||||
|
return #tProcesses
|
||||||
|
end
|
||||||
|
|
||||||
|
-- - Stop a running process by its ID.
|
||||||
|
|
||||||
|
-- @tparam number n The process index to stop.
|
||||||
|
-- @treturn boolean Whether the process existed and was stopped.
|
||||||
|
function multishell.stop(n)
|
||||||
|
local previousTerm1 = term.current()
|
||||||
|
|
||||||
|
|
||||||
|
expect(1, n, "number")
|
||||||
|
if n < 1 or n > #tProcesses then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Send terminate event
|
||||||
|
resumeProcess(n, "terminate")
|
||||||
|
|
||||||
|
-- Force cleanup if dead, or mark as interacted to skip prompt
|
||||||
|
local tProcess = tProcesses[n]
|
||||||
|
if coroutine.status(tProcess.co) == "dead" then
|
||||||
|
cullProcess(n)
|
||||||
|
else
|
||||||
|
tProcess.bInteracted = true -- skip "Press any key"
|
||||||
|
end
|
||||||
|
|
||||||
|
setMenuVisible(setVisibleProcessCount() >= 2)
|
||||||
|
redrawMenu()
|
||||||
|
|
||||||
|
term.redirect(previousTerm1)
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
--- Get detailed information about a process.
|
||||||
|
--
|
||||||
|
-- @tparam number n The process index.
|
||||||
|
-- @treturn table|nil A table containing process info, or nil if invalid.
|
||||||
|
-- Fields:
|
||||||
|
-- - id: process index (same as n)
|
||||||
|
-- - type: "Path" or "Function"
|
||||||
|
-- - path: program path (if type == "Path")
|
||||||
|
-- - func: function reference (if type == "Function")
|
||||||
|
-- - env: environment table
|
||||||
|
-- - args: arguments table
|
||||||
|
-- - autoClose: boolean
|
||||||
|
-- - hideTab: boolean
|
||||||
|
-- - title: current title
|
||||||
|
-- - status: coroutine status ("running", "suspended", "dead")
|
||||||
|
function multishell.getProcessInfo(n)
|
||||||
|
expect(1, n, "number")
|
||||||
|
if n < 1 or n > #tProcesses then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local p = tProcesses[n]
|
||||||
|
local info = {
|
||||||
|
id = n,
|
||||||
|
hideTab = p.bHideTab,
|
||||||
|
autoClose = p.bInteracted and not (p.co and coroutine.status(p.co) == "suspended" and not p.bInteracted), -- approximate
|
||||||
|
title = p.sTitle,
|
||||||
|
status = p.co and coroutine.status(p.co) or "dead",
|
||||||
|
env = p.env or {}, -- 注意:原代码未保存 env,需额外记录(见下方说明)
|
||||||
|
args = p.args or {}, -- 同上
|
||||||
|
}
|
||||||
|
|
||||||
|
-- 判断是 Path 还是 Function
|
||||||
|
if p.sProgramPath then
|
||||||
|
info.type = "Path"
|
||||||
|
info.path = p.sProgramPath
|
||||||
|
elseif p.fFunction then
|
||||||
|
info.type = "Function"
|
||||||
|
info.func = p.fFunction
|
||||||
|
else
|
||||||
|
info.type = "Unknown"
|
||||||
|
end
|
||||||
|
|
||||||
|
return info
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
--- Launch a new process with advanced options.
|
||||||
|
--
|
||||||
|
-- Accepts either a program path or a function to run.
|
||||||
|
--
|
||||||
|
-- @tparam table options A table containing launch options:
|
||||||
|
-- - `path` (string): Path to a program to run (mutually exclusive with `func`).
|
||||||
|
-- - `func` (function): A function to execute directly (mutually exclusive with `path`).
|
||||||
|
-- - `args` (table): Arguments to pass to the program/function.
|
||||||
|
-- - `env` (table): Environment table (default: `{}`).
|
||||||
|
-- - `hideTab` (boolean): If true, do not show in tab bar (default: false).
|
||||||
|
-- - `autoClose` (boolean): If true, close immediately after finish without "Press any key" (default: false).
|
||||||
|
-- @treturn number The index of the created process.
|
||||||
|
function multishell.newlaunch(options)
|
||||||
|
expect(1, options, "table")
|
||||||
|
|
||||||
|
local sProgramPath = options.path
|
||||||
|
local fFunction = options.func
|
||||||
|
local tArgs = type(options.args) == "table" and options.args or {}
|
||||||
|
local tEnv = type(options.env) == "table" and options.env or {}
|
||||||
|
local bHideTab = options.hideTab == true
|
||||||
|
local bAutoClose = options.autoClose == true
|
||||||
|
|
||||||
|
if sProgramPath and fFunction then
|
||||||
|
error("Cannot specify both 'path' and 'func'", 2)
|
||||||
|
elseif not sProgramPath and not fFunction then
|
||||||
|
error("Must specify either 'path' or 'func'", 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
if sProgramPath then
|
||||||
|
expect(1, sProgramPath, "string")
|
||||||
|
else
|
||||||
|
expect(1, fFunction, "function")
|
||||||
|
end
|
||||||
|
|
||||||
|
local previousTerm = term.current()
|
||||||
|
local sTitle = sProgramPath and fs.getName(sProgramPath) or "function"
|
||||||
|
|
||||||
|
local nProcess = #tProcesses + 1
|
||||||
|
local tProcess = {}
|
||||||
|
|
||||||
|
tProcess.sProgramPath = sProgramPath
|
||||||
|
tProcess.fFunction = fFunction
|
||||||
|
tProcess.env = tEnv
|
||||||
|
tProcess.args = tArgs
|
||||||
|
tProcess.bAutoClose = bAutoClose -- 显式保存,便于查询
|
||||||
|
|
||||||
|
-- Create window
|
||||||
|
local visibleCountBefore = setVisibleProcessCount()
|
||||||
|
local willBeVisible = not bHideTab
|
||||||
|
local showMenuNow = (visibleCountBefore + (willBeVisible and 1 or 0)) >= 2
|
||||||
|
|
||||||
|
if showMenuNow then
|
||||||
|
tProcess.window = window.create(parentTerm, 1, 2, w, h - 1, false)
|
||||||
|
else
|
||||||
|
tProcess.window = window.create(parentTerm, 1, 1, w, h, false)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Terminal proxy
|
||||||
|
tProcess.terminal = {}
|
||||||
|
for k in pairs(term.native()) do
|
||||||
|
tProcess.terminal[k] = tProcess.window[k]
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Coroutine
|
||||||
|
if sProgramPath then
|
||||||
|
tProcess.co = coroutine.create(function()
|
||||||
|
os.run(tEnv, sProgramPath, table.unpack(tArgs))
|
||||||
|
if not bAutoClose and not tProcess.bInteracted then
|
||||||
|
term.setCursorBlink(false)
|
||||||
|
print("Press any key to continue")
|
||||||
|
os.pullEvent("char")
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
else
|
||||||
|
tProcess.co = coroutine.create(function()
|
||||||
|
local success, err = pcall(function()
|
||||||
|
fFunction(table.unpack(tArgs))
|
||||||
|
end)
|
||||||
|
if not success then
|
||||||
|
printError(err)
|
||||||
|
end
|
||||||
|
if not bAutoClose and not tProcess.bInteracted then
|
||||||
|
term.setCursorBlink(false)
|
||||||
|
print("Press any key to continue")
|
||||||
|
os.pullEvent("char")
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
tProcess.sTitle = sTitle
|
||||||
|
tProcess.sFilter = nil
|
||||||
|
tProcess.bInteracted = bAutoClose -- 保持原逻辑
|
||||||
|
tProcess.bHideTab = bHideTab
|
||||||
|
|
||||||
|
tProcesses[nProcess] = tProcess
|
||||||
|
|
||||||
|
setMenuVisible(setVisibleProcessCount() >= 2)
|
||||||
|
resumeProcess(nProcess)
|
||||||
|
redrawMenu()
|
||||||
|
term.redirect(previousTerm)
|
||||||
|
|
||||||
|
return nProcess
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Set whether a process's tab is hidden in the tab bar.
|
||||||
|
--
|
||||||
|
-- @tparam number n The process index.
|
||||||
|
-- @tparam boolean hide Whether to hide the tab.
|
||||||
|
-- @treturn boolean True if successful, false if process doesn't exist.
|
||||||
|
function multishell.setHideTab(n, hide)
|
||||||
|
expect(1, n, "number")
|
||||||
|
expect(2, hide, "boolean")
|
||||||
|
if n < 1 or n > #tProcesses then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local p = tProcesses[n]
|
||||||
|
local wasHidden = p.bHideTab
|
||||||
|
p.bHideTab = hide
|
||||||
|
|
||||||
|
-- If visibility changed, update menu visibility logic
|
||||||
|
if wasHidden ~= hide then
|
||||||
|
setMenuVisible(setVisibleProcessCount() >= 2)
|
||||||
|
redrawMenu()
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Get information about all currently running processes.
|
||||||
|
--
|
||||||
|
-- @treturn table A list of process info tables, each in the same format as
|
||||||
|
-- [`multishell.getProcessInfo`]. The list is ordered by process ID (1 to N).
|
||||||
|
-- @see getProcessInfo
|
||||||
|
function multishell.listProcesses()
|
||||||
|
local result = {}
|
||||||
|
for i = 1, #tProcesses do
|
||||||
|
-- Only include valid (non-nil) processes
|
||||||
|
if tProcesses[i] then
|
||||||
|
table.insert(result, multishell.getProcessInfo(i))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Begin
|
||||||
|
parentTerm.clear()
|
||||||
|
setMenuVisible(false)
|
||||||
|
launchProcess(true, {
|
||||||
|
["shell"] = shell,
|
||||||
|
["multishell"] = multishell,
|
||||||
|
}, "/rom/programs/shell.lua")
|
||||||
|
|
||||||
|
-- Run processes
|
||||||
|
while #tProcesses > 0 do
|
||||||
|
-- Get the event
|
||||||
|
local tEventData = table.pack(os.pullEventRaw())
|
||||||
|
local sEvent = tEventData[1]
|
||||||
|
if sEvent == "term_resize" then
|
||||||
|
-- Resize event
|
||||||
|
w, h = parentTerm.getSize()
|
||||||
|
resizeWindows()
|
||||||
|
redrawMenu()
|
||||||
|
|
||||||
|
elseif sEvent == "char" or sEvent == "key" or sEvent == "key_up" or sEvent == "paste" or sEvent == "terminate" or sEvent == "file_transfer" then
|
||||||
|
-- Basic input, just passthrough to current process
|
||||||
|
if nCurrentProcess then
|
||||||
|
resumeProcess(nCurrentProcess, table.unpack(tEventData, 1, tEventData.n))
|
||||||
|
if cullProcess(nCurrentProcess) then
|
||||||
|
setMenuVisible(setVisibleProcessCount() >= 2)
|
||||||
|
redrawMenu()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
elseif sEvent == "mouse_click" then
|
||||||
|
-- Click event
|
||||||
|
local button, x, y = tEventData[2], tEventData[3], tEventData[4]
|
||||||
|
if bShowMenu and y == 1 then
|
||||||
|
-- Switch process via tab bar
|
||||||
|
local visibleProcesses = {}
|
||||||
|
for i, p in ipairs(tProcesses) do
|
||||||
|
if not p.bHideTab then
|
||||||
|
table.insert(visibleProcesses, { index = i, proc = p })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if x == 1 and nScrollPos ~= 1 then
|
||||||
|
nScrollPos = nScrollPos - 1
|
||||||
|
redrawMenu()
|
||||||
|
elseif bScrollRight and x == term.getSize() then
|
||||||
|
nScrollPos = nScrollPos + 1
|
||||||
|
redrawMenu()
|
||||||
|
else
|
||||||
|
local tabStart = 1
|
||||||
|
if nScrollPos ~= 1 then
|
||||||
|
tabStart = 2
|
||||||
|
end
|
||||||
|
for i = nScrollPos, #visibleProcesses do
|
||||||
|
local item = visibleProcesses[i]
|
||||||
|
local tabEnd = tabStart + #item.proc.sTitle + 1
|
||||||
|
if x >= tabStart and x <= tabEnd then
|
||||||
|
selectProcess(item.index)
|
||||||
|
redrawMenu()
|
||||||
|
break
|
||||||
|
end
|
||||||
|
tabStart = tabEnd + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- Passthrough to current process
|
||||||
|
if nCurrentProcess then
|
||||||
|
resumeProcess(nCurrentProcess, sEvent, button, x, bShowMenu and y - 1 or y)
|
||||||
|
if cullProcess(nCurrentProcess) then
|
||||||
|
setMenuVisible(setVisibleProcessCount() >= 2)
|
||||||
|
redrawMenu()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
elseif sEvent == "mouse_drag" or sEvent == "mouse_up" or sEvent == "mouse_scroll" then
|
||||||
|
-- Other mouse event
|
||||||
|
local p1, x, y = tEventData[2], tEventData[3], tEventData[4]
|
||||||
|
if bShowMenu and sEvent == "mouse_scroll" and y == 1 then
|
||||||
|
if p1 == -1 and nScrollPos ~= 1 then
|
||||||
|
nScrollPos = nScrollPos - 1
|
||||||
|
redrawMenu()
|
||||||
|
elseif bScrollRight and p1 == 1 then
|
||||||
|
nScrollPos = nScrollPos + 1
|
||||||
|
redrawMenu()
|
||||||
|
end
|
||||||
|
elseif not (bShowMenu and y == 1) then
|
||||||
|
if nCurrentProcess then
|
||||||
|
resumeProcess(nCurrentProcess, sEvent, p1, x, bShowMenu and y - 1 or y)
|
||||||
|
if cullProcess(nCurrentProcess) then
|
||||||
|
setMenuVisible(setVisibleProcessCount() >= 2)
|
||||||
|
redrawMenu()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
else
|
||||||
|
-- Other event: broadcast to all processes
|
||||||
|
local nLimit = #tProcesses
|
||||||
|
for n = 1, nLimit do
|
||||||
|
resumeProcess(n, table.unpack(tEventData, 1, tEventData.n))
|
||||||
|
end
|
||||||
|
if cullProcesses() then
|
||||||
|
setMenuVisible(setVisibleProcessCount() >= 2)
|
||||||
|
redrawMenu()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if bWindowsResized then
|
||||||
|
local nLimit = #tProcesses
|
||||||
|
for n = 1, nLimit do
|
||||||
|
resumeProcess(n, "term_resize")
|
||||||
|
end
|
||||||
|
bWindowsResized = false
|
||||||
|
if cullProcesses() then
|
||||||
|
setMenuVisible(setVisibleProcessCount() >= 2)
|
||||||
|
redrawMenu()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Shutdown
|
||||||
|
term.redirect(parentTerm)
|
||||||
Reference in New Issue
Block a user