mirror of
https://github.com/pesde-pkg/tooling.git
synced 2025-01-09 15:59:09 +00:00
feat(lib): include optional progress bar
Includes an optional progress bar which gets enabled when lib is called using the default convenience `__call` metamethod.
This commit is contained in:
parent
ead60c003e
commit
c69a7417a0
2 changed files with 160 additions and 7 deletions
|
@ -11,6 +11,7 @@ local net = require("@lune/net")
|
|||
local process = require("@lune/process")
|
||||
local stdio = require("@lune/stdio")
|
||||
local serde = require("@lune/serde")
|
||||
local task = require("@lune/task")
|
||||
|
||||
local pathfs = require("../lune_packages/pathfs")
|
||||
local dirs = require("../lune_packages/dirs")
|
||||
|
@ -23,6 +24,7 @@ type Option<T> = Option.Option<T>
|
|||
|
||||
local Github = require("./github")
|
||||
local PlatformDescriptor = require("./platform/descriptor")
|
||||
local ProgressBar = require("./utils/progress")
|
||||
local compression = require("./compression")
|
||||
local eq = require("./utils/eq")
|
||||
local manifest = require("./manifest")
|
||||
|
@ -36,6 +38,8 @@ export type ToolId = {
|
|||
-- TODO: Remove this in a breaking change
|
||||
export type GithubReleases = Github.GithubReleases
|
||||
|
||||
local ERROR_PREFIX = `{stdio.color("red")}{stdio.style("bold")}error{stdio.color("reset")}:`
|
||||
local _INFO_PREFIX = `{stdio.color("green")}{stdio.style("bold")}info{stdio.color("reset")}:`
|
||||
local WARN_PREFIX = `{stdio.color("yellow")}{stdio.style("bold")}warn{stdio.color("reset")}:`
|
||||
|
||||
local function warn(...)
|
||||
|
@ -87,6 +91,13 @@ end
|
|||
local LINK_INSTALL_DIR = (dirs.homeDir() or error("Couldn't get home dir :(")):join(".pesde"):join("bin")
|
||||
local TOOL_STORAGE_DIR = LINK_INSTALL_DIR:join("tool_storage")
|
||||
|
||||
local bar = ProgressBar.new()
|
||||
:withStage("init", "Initializing")
|
||||
:withStage("fetch", "Fetching release")
|
||||
:withStage("locate", "Identifying asset")
|
||||
:withStage("download", "Downloading")
|
||||
:withStage("install", "Installing")
|
||||
|
||||
function runTool(tool: ToolId | pathfs.Path): number
|
||||
-- FIXME: `process.spawn` has a bug where interactive features don't
|
||||
-- forward properly
|
||||
|
@ -102,7 +113,36 @@ function runTool(tool: ToolId | pathfs.Path): number
|
|||
}).code
|
||||
end
|
||||
|
||||
function installTool(tool: ToolId, installPath: pathfs.Path)
|
||||
local function makeCondInc(yes: boolean?)
|
||||
return function()
|
||||
if yes then
|
||||
bar:nextStage()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function makeCondPause(yes: boolean?)
|
||||
return function()
|
||||
if yes then
|
||||
bar:pause()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function makeCondResume(yes: boolean?)
|
||||
return function()
|
||||
if yes then
|
||||
bar:resume()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function installTool(tool: ToolId, installPath: pathfs.Path, interactive: boolean?)
|
||||
local next = makeCondInc(interactive)
|
||||
local pause = makeCondPause(interactive)
|
||||
local resume = makeCondResume(interactive)
|
||||
next()
|
||||
|
||||
local toolAlias = toolAliasOrDefault(tool)
|
||||
local client = Github.new(
|
||||
tool.repo,
|
||||
|
@ -113,6 +153,7 @@ function installTool(tool: ToolId, installPath: pathfs.Path)
|
|||
}) :: Option<Github.Config>
|
||||
)
|
||||
|
||||
next()
|
||||
local releases = client:queueTransactions({ "FetchReleases" })[1]:unwrap() :: GithubReleases
|
||||
local assets = tool.version:match({
|
||||
Some = function(version: string)
|
||||
|
@ -122,6 +163,7 @@ function installTool(tool: ToolId, installPath: pathfs.Path)
|
|||
end
|
||||
end
|
||||
|
||||
pause()
|
||||
return error(`No release found for version {version}`)
|
||||
end,
|
||||
|
||||
|
@ -130,6 +172,7 @@ function installTool(tool: ToolId, installPath: pathfs.Path)
|
|||
end,
|
||||
})
|
||||
|
||||
next()
|
||||
-- TODO: Use index type fn in solver v2
|
||||
local matchingAsset: {
|
||||
name: string,
|
||||
|
@ -156,8 +199,13 @@ function installTool(tool: ToolId, installPath: pathfs.Path)
|
|||
|
||||
local binaryPath: pathfs.Path
|
||||
if matchingAsset == nil then
|
||||
pause()
|
||||
warn("Pesde could not find a matching binary for your system")
|
||||
warn("Will now attempt to download all binaries and find a matching one")
|
||||
resume()
|
||||
|
||||
next()
|
||||
|
||||
for _, asset in assets do
|
||||
local decompressedPath = downloadAndDecompress(asset)
|
||||
if decompressedPath:isSome() then
|
||||
|
@ -177,10 +225,12 @@ function installTool(tool: ToolId, installPath: pathfs.Path)
|
|||
local decompressedPath = downloadAndDecompress(matchingAsset):unwrap()
|
||||
binaryPath = decompressedPath:join(aliasPath)
|
||||
if not pathfs.isFile(binaryPath) then
|
||||
pause()
|
||||
error(`No matching binary found in {decompressedPath}`)
|
||||
end
|
||||
end
|
||||
|
||||
next()
|
||||
-- Maintain multiple versions of a tool, and avoid downloading
|
||||
-- the binary for a version again if it's already there
|
||||
local toolDir = Option.from(installPath:parent()):unwrap()
|
||||
|
@ -211,6 +261,11 @@ function installTool(tool: ToolId, installPath: pathfs.Path)
|
|||
-- ...
|
||||
]]
|
||||
|
||||
-- Stop the progress bar and clean it up from stdout
|
||||
bar:stop()
|
||||
task.cancel(bar.thread :: thread)
|
||||
stdio.write("\x1b[2K\x1b[0G")
|
||||
|
||||
-- NOTE: This is equivalent to `0o755` or `rwxr-xr-x`
|
||||
chmod(installPath, 0b10101010011)
|
||||
runTool(installPath)
|
||||
|
@ -218,7 +273,7 @@ end
|
|||
|
||||
type LibExports = {
|
||||
runTool: (pathfs.Path | ToolId) -> number,
|
||||
installTool: (ToolId, pathfs.Path) -> never,
|
||||
installTool: (ToolId, pathfs.Path, boolean?) -> never,
|
||||
}
|
||||
type LibExportsImpl = typeof(setmetatable(
|
||||
{} :: LibExports,
|
||||
|
@ -232,10 +287,6 @@ return setmetatable(
|
|||
} :: LibExports,
|
||||
{
|
||||
__call = function(lib: LibExportsImpl, tool: string, pesdeRoot: string?): number
|
||||
-- TODO: Progress bar maybe? :D
|
||||
|
||||
local ERROR_PREFIX = `{stdio.color("red")}{stdio.style("bold")}error{stdio.color("reset")}:`
|
||||
|
||||
local repo, version = string.match(tool, "([^@]+)@?(.*)")
|
||||
if repo == nil or version == nil then
|
||||
stdio.ewrite(`{ERROR_PREFIX} Invalid tool provided\n`)
|
||||
|
@ -280,6 +331,7 @@ return setmetatable(
|
|||
return lib.runTool(toolInstallPath)
|
||||
end
|
||||
|
||||
bar:start()
|
||||
local ok, err = pcall(
|
||||
lib.installTool,
|
||||
{
|
||||
|
@ -287,7 +339,8 @@ return setmetatable(
|
|||
repo = repo,
|
||||
version = Option.Some(versionOrDefault),
|
||||
} :: ToolId,
|
||||
toolInstallPath
|
||||
toolInstallPath,
|
||||
true
|
||||
)
|
||||
|
||||
if not ok then
|
||||
|
|
100
toolchainlib/src/utils/progress.luau
Normal file
100
toolchainlib/src/utils/progress.luau
Normal file
|
@ -0,0 +1,100 @@
|
|||
--> Inspired by Rokit's progress bar: https://github.com/rojo-rbx/rokit/blob/a303faf/src/util/progress.rs
|
||||
|
||||
local task = require("@lune/task")
|
||||
local stdio = require("@lune/stdio")
|
||||
|
||||
local Option = require("../../lune_packages/option")
|
||||
type Option<T> = Option.Option<T>
|
||||
|
||||
-- FORMAT: {SPINNER} {MESSAGE} {BAR} {STAGE}
|
||||
local SPINNERS = { "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" }
|
||||
local BAR_COMPONENT = "▇"
|
||||
local MAX_BAR_LENGTH = 30
|
||||
|
||||
local ProgressBar = {}
|
||||
type ProgressBar = {
|
||||
stages: { { tag: string, message: string } },
|
||||
currentStageIndex: number,
|
||||
finished: boolean,
|
||||
thread: thread?,
|
||||
}
|
||||
export type ProgressBarImpl = typeof(setmetatable({} :: ProgressBar, { __index = ProgressBar }))
|
||||
|
||||
function ProgressBar.new(): ProgressBarImpl
|
||||
return setmetatable(
|
||||
{
|
||||
stages = {},
|
||||
currentStageIndex = 1,
|
||||
finished = false,
|
||||
} :: ProgressBar,
|
||||
{
|
||||
__index = ProgressBar,
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
function ProgressBar.withStage(self: ProgressBarImpl, tag: string, msg: string): ProgressBarImpl
|
||||
table.insert(self.stages, { tag = tag, message = msg })
|
||||
return self
|
||||
end
|
||||
|
||||
function ProgressBar.start(self: ProgressBarImpl): ()
|
||||
local BAR_LENGTH = MAX_BAR_LENGTH // #self.stages
|
||||
local TOTAL_BAR_LENGTH = BAR_LENGTH * #self.stages
|
||||
local BAR = string.rep(BAR_COMPONENT, BAR_LENGTH)
|
||||
local MAX_MESSAGE_LENGTH = 0
|
||||
for _, stage in self.stages do
|
||||
local len = #stage.message
|
||||
if len > MAX_MESSAGE_LENGTH then
|
||||
MAX_MESSAGE_LENGTH = len
|
||||
end
|
||||
end
|
||||
|
||||
self.thread = task.spawn(function()
|
||||
while not self.finished do
|
||||
if self.currentStageIndex == #self.stages then
|
||||
self:stop()
|
||||
break
|
||||
end
|
||||
|
||||
for _, spinner in SPINNERS do
|
||||
local stage = self.stages[self.currentStageIndex]
|
||||
stdio.write(
|
||||
`\x1b[2K\x1b[0G{spinner} {stage.message}{string.rep(" ", MAX_MESSAGE_LENGTH - #stage.message)} [{string.rep(
|
||||
BAR,
|
||||
self.currentStageIndex
|
||||
)}{string.rep(" ", TOTAL_BAR_LENGTH - (BAR_LENGTH * self.currentStageIndex))}] {self.currentStageIndex} / {#self.stages}`
|
||||
)
|
||||
|
||||
task.wait(0.1)
|
||||
end
|
||||
end
|
||||
|
||||
stdio.write("\n")
|
||||
end)
|
||||
end
|
||||
|
||||
function ProgressBar.stop(self: ProgressBarImpl): ()
|
||||
self.finished = true
|
||||
end
|
||||
|
||||
function ProgressBar.pause(self: ProgressBarImpl): ()
|
||||
local _ = self.thread and coroutine.yield(self.thread)
|
||||
end
|
||||
|
||||
function ProgressBar.resume(self: ProgressBarImpl): ()
|
||||
local _ = self.thread and coroutine.resume(self.thread)
|
||||
end
|
||||
|
||||
function ProgressBar.nextStage(self: ProgressBarImpl): ()
|
||||
local inc = self.currentStageIndex + 1
|
||||
if inc > #self.stages then
|
||||
-- TODO: Make this a result
|
||||
self.finished = true
|
||||
return error("Out of stage bounds")
|
||||
end
|
||||
|
||||
self.currentStageIndex = inc
|
||||
end
|
||||
|
||||
return ProgressBar
|
Loading…
Reference in a new issue