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:
Erica Marigold 2024-12-13 16:25:42 +00:00
parent ead60c003e
commit c69a7417a0
Signed by: DevComp
GPG key ID: 429EF1C337871656
2 changed files with 160 additions and 7 deletions

View file

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

View 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