Skip to content

Add remote download progress#371

Merged
kindermax merged 4 commits into
masterfrom
add-download-progress
Jun 14, 2026
Merged

Add remote download progress#371
kindermax merged 4 commits into
masterfrom
add-download-progress

Conversation

@kindermax

@kindermax kindermax commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add interactive download progress for remote configs and remote mixins
  • Animate known-size downloads with Bubble Tea/bubbles progress and keep a completed bar line
  • Thread progress and cancellation through config/mixin loading
  • Document progress behavior and update changelog

Fixes #360

Verification

  • go test ./...
  • lets test-unit
  • lets lint

Summary by Sourcery

Add a user-facing download progress indicator for remote configs and mixins and thread progress tracking through config loading and fetching.

New Features:

  • Introduce a download progress observer and tracker abstraction for HTTP fetches, with support for differentiating remote configs and remote mixins.
  • Add an interactive, animated download progress UI for known-size remote downloads that renders to stderr when running in an interactive terminal.
  • Expose CLI and config loader options to enable progress reporting for remote configs and remote mixins, including remote main configs loaded via -c/--config.

Enhancements:

  • Disable HTTP compression and surface content length to support accurate progress reporting while retaining existing timeouts.
  • Propagate context and progress settings through config loading, remote config caching, and mixin loading so that remote downloads share cancellation and reporting behavior.
  • Redact secrets and truncate long URLs in download labels to keep progress output concise and safe.

Build:

  • Promote the bubbles progress dependency to a direct requirement for the animated progress UI.

Documentation:

  • Document remote config usage and behavior, including caching, work directory semantics, and when download progress is shown.
  • Update terminology and changelog entries to cover remote configs, remote mixins, and the new download progress indicator.

Tests:

  • Add unit tests for fetch download progress behavior, including known/unknown sizes, validation ordering, and progress completion.
  • Add tests to verify config and mixin loaders emit progress only on remote download cache misses and use the correct source kinds.
  • Add tests for the download progress UI to ensure correct rendering, throttling, byte formatting, and label generation.

@sourcery-ai

sourcery-ai Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Implements a configurable download progress observation system for remote configs and mixins, threads it through config loading and CLI wiring, and adds a Bubble Tea/bubbles-based animated progress UI plus tests and docs.

Sequence diagram for remote download with progress reporting

sequenceDiagram
    actor User
    participant CLI as Main
    participant Loader as LoadRemote
    participant Cfg as Config
    participant Fetch as Download
    participant Obs as Observer
    participant Trk as ProgressTracker

    User->>CLI: run lets -c https://url
    CLI->>Loader: LoadRemote(ctx, url, noCache, version, WithProgress(...))
    Loader->>Loader: ensureRemoteConfig(ctx, url, noCache, progress)
    Loader->>Fetch: Download(ctx, url, WithProgress(SourceRemoteConfig, progress))
    Fetch->>Obs: Start(ProgressInfo)
    Obs-->>Fetch: ProgressTracker
    Fetch->>Fetch: readAll(resp.Body, tracker)
    loop while body.Read
        Fetch->>Trk: Add(n)
    end
    Fetch->>Trk: Done(err)
    Fetch-->>Loader: []byte, error
    Loader-->>CLI: *config.Config, error
    CLI->>Cfg: SetDownloadOptions(ctx, progress)
    Cfg->>RemoteMixin: download(context(), downloadProgress)
    RemoteMixin->>Fetch: Download(ctx, url, WithProgress(SourceRemoteMixin, progress))
Loading

File-Level Changes

Change Details Files
Add pluggable progress observation to HTTP downloads and ensure it is only started after successful response validation.
  • Introduce SourceKind, ProgressInfo, ProgressObserver, and ProgressTracker types and option plumbing in fetch.Download.
  • Disable default HTTP compression via a cloned http.Transport to preserve Content-Length for known-size progress.
  • Start progress tracking only after validating HTTP status and Content-Type and wrap body reads in a reader that reports incremental bytes.
  • Add unit tests covering known-size, unknown-size, validation-failure, and progress accounting behavior in fetch.
internal/fetch/fetch.go
internal/fetch/fetch_test.go
Thread progress options through config loading APIs and remote mixin downloads, respecting cache hits.
  • Introduce LoadOption and loadOptions with WithProgress, and add LoadWithContext to carry context and progress into config loading.
  • Update LoadRemote, ensureRemoteConfig, and RemoteMixin.download to accept/propagate fetch.ProgressObserver and call fetch.Download with the right SourceKind.
  • Extend Config to carry download context and progress observer, and ensure new mixin configs inherit these settings.
  • Add tests to verify progress is reported only on cache misses for remote configs and mixins and that SourceKind is set correctly.
internal/config/load.go
internal/config/load_test.go
internal/config/config/config.go
internal/config/config/mixin.go
Wire interactive download progress into the CLI for interactive stderr and document terminology and behavior.
  • In cli.Main, construct a downloadprogress.Observer when stderr is interactive and pass it via loader.LoadOption to both local and remote config loaders.
  • Document remote mixin and remote config download progress behavior in the config reference and refine shared terminology in CONTEXT.md.
  • Extend the changelog entry to mention the new interactive download progress feature and adjust go.mod/go.sum to depend directly on bubbles.
internal/cli/cli.go
docs/docs/config.md
CONTEXT.md
docs/docs/changelog.md
go.mod
go.sum
Implement a Bubble Tea/bubbles-based progress UI for known-size downloads with terminal-width-aware bars and safe labeling.
  • Add downloadprogress.Observer that decides between animated and manual trackers based on Content-Length and TTY detection, with configurable width, colors, throttle, and final pause.
  • Implement manualTracker for unknown-size or non-TTY cases and animatedTracker using a Bubble Tea program and bubbles progress.Model for known-size downloads.
  • Handle terminal width detection, label truncation, byte formatting, URL redaction, and safe label derivation from URLs.
  • Add focused tests for Observer behavior, byte formatting, label derivation, throttling, and Bubble Tea progress model rendering.
internal/downloadprogress/progress.go
internal/downloadprogress/progress_test.go

Assessment against linked issues

Issue Objective Addressed Explanation
#360 Implement a progress-reporting mechanism (progress bar or equivalent) for HTTP downloads, supporting both known-size and unknown-size responses.
#360 Use the progress mechanism for downloading remote configs and remote mixins in the CLI, showing progress to users when appropriate and documenting the behavior.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 4 issues, and left some high level feedback:

  • The recordingProgress helper type is duplicated in both internal/fetch/fetch_test.go and internal/config/load_test.go; consider moving a shared test helper into a common test file/package to avoid repetition and keep progress test utilities in one place.
  • In readAll, you always use a plain bytes.Buffer; since Download already knows resp.ContentLength, you could pre-allocate the buffer capacity (e.g., via buf.Grow(int(resp.ContentLength))) to avoid repeated allocations for larger downloads, including when a ProgressTracker is used.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `recordingProgress` helper type is duplicated in both `internal/fetch/fetch_test.go` and `internal/config/load_test.go`; consider moving a shared test helper into a common test file/package to avoid repetition and keep progress test utilities in one place.
- In `readAll`, you always use a plain `bytes.Buffer`; since `Download` already knows `resp.ContentLength`, you could pre-allocate the buffer capacity (e.g., via `buf.Grow(int(resp.ContentLength))`) to avoid repeated allocations for larger downloads, including when a `ProgressTracker` is used.

## Individual Comments

### Comment 1
<location path="internal/fetch/fetch.go" line_range="25-26" />
<code_context>
-	Timeout: 5 * 60 * time.Second,
+var httpClient = newHTTPClient()
+
+func newHTTPClient() *http.Client {
+	transport := http.DefaultTransport.(*http.Transport).Clone()
+	transport.DisableCompression = true
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Guard the type assertion on http.DefaultTransport to avoid unexpected panics if the default transport is overridden.

`http.DefaultTransport.(*http.Transport).Clone()` will panic if `DefaultTransport` has been replaced with a non-`*http.Transport` `RoundTripper`. Please guard the type assertion (e.g., with an `ok` check or type switch) and fall back to using `http.DefaultTransport` unchanged when it isn’t a `*http.Transport`, while preserving the current behavior when it is.
</issue_to_address>

### Comment 2
<location path="internal/downloadprogress/progress.go" line_range="506-515" />
<code_context>
+	return rawURL[:idx]
+}
+
+func truncateMiddle(s string, width int) string {
+	if width <= 0 {
+		return ""
+	}
+
+	if lipgloss.Width(s) <= width {
+		return s
+	}
+
+	if width <= 3 {
+		return strings.Repeat(".", width)
+	}
+
+	runes := []rune(s)
+	keep := width - 3
+	left := keep / 2
+	right := keep - left
+
+	if left+right >= len(runes) {
+		return s
+	}
+
+	return string(runes[:left]) + "..." + string(runes[len(runes)-right:])
+}
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** truncateMiddle mixes rune-based slicing with lipgloss.Width, which can mis-handle wide or combining characters.

This logic decides whether to truncate using `lipgloss.Width(s)` but then truncates by indexing `[]rune(s)`. For strings with wide/combining characters where `lipgloss.Width``len([]rune(s))`, you can end up with off-by-one visual widths or split grapheme clusters.

Consider either:
- Using a lipgloss-provided truncation helper if one exists, or
- Making the measurement and slicing use the same width metric (even plain rune length), so the truncation decision and the actual slice are consistent.

Also, clamping `keep` to `len(runes)` before computing `left/right` would reduce the risk of awkward splits.

Suggested implementation:

```golang
func truncateMiddle(s string, width int) string {
	if width <= 0 {
		return ""
	}

	runes := []rune(s)
	if len(runes) <= width {
		return s
	}

	if width <= 3 {
		return strings.Repeat(".", width)
	}

	keep := width - 3
	if keep > len(runes) {
		keep = len(runes)
	}

	left := keep / 2
	right := keep - left

	if left+right >= len(runes) {
		return s
	}

	return string(runes[:left]) + "..." + string(runes[len(runes)-right:])
}

```

If `lipgloss` is only used in `truncateMiddle`, you should also remove its import from this file to avoid an unused import error. If it's used elsewhere, no import changes are needed.
</issue_to_address>

### Comment 3
<location path="docs/docs/config.md" line_range="328-332" />
<code_context>

 It is possible to specify mixin as url. Lets will download it and load it as a mixin.
 File will be stored in `.lets/mixins` directory.
+When stderr is an interactive terminal, lets shows download progress for remote mixin downloads. Cache hits do not show progress.
</code_context>
<issue_to_address>
**suggestion (typo):** Capitalize URL and consider adding missing articles in the mixin URL sentence.

For example, you could write: “It is possible to specify a mixin as a URL. lets will download it and load it as a mixin.”

```suggestion
It is possible to specify a mixin as a URL. lets will download it and load it as a mixin.
The file will be stored in the `.lets/mixins` directory.
When stderr is an interactive terminal, lets shows download progress for remote mixin downloads. Cache hits do not show progress.

By default, the mixin filename will be the SHA256 hash of the URL.
```
</issue_to_address>

### Comment 4
<location path="docs/docs/config.md" line_range="332" />
<code_context>
 File will be stored in `.lets/mixins` directory.
+When stderr is an interactive terminal, lets shows download progress for remote mixin downloads. Cache hits do not show progress.

 By default mixin filename will be sha256 hash of url.

</code_context>
<issue_to_address>
**suggestion (typo):** Capitalize URL and add article for readability.

Rephrase to: "By default, the mixin filename will be the SHA256 hash of the URL."

```suggestion
By default, the mixin filename will be the SHA256 hash of the URL.
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread internal/fetch/fetch.go
Comment on lines +25 to +26
func newHTTPClient() *http.Client {
transport := http.DefaultTransport.(*http.Transport).Clone()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Guard the type assertion on http.DefaultTransport to avoid unexpected panics if the default transport is overridden.

http.DefaultTransport.(*http.Transport).Clone() will panic if DefaultTransport has been replaced with a non-*http.Transport RoundTripper. Please guard the type assertion (e.g., with an ok check or type switch) and fall back to using http.DefaultTransport unchanged when it isn’t a *http.Transport, while preserving the current behavior when it is.

Comment on lines +506 to +515
func truncateMiddle(s string, width int) string {
if width <= 0 {
return ""
}

if lipgloss.Width(s) <= width {
return s
}

if width <= 3 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): truncateMiddle mixes rune-based slicing with lipgloss.Width, which can mis-handle wide or combining characters.

This logic decides whether to truncate using lipgloss.Width(s) but then truncates by indexing []rune(s). For strings with wide/combining characters where lipgloss.Widthlen([]rune(s)), you can end up with off-by-one visual widths or split grapheme clusters.

Consider either:

  • Using a lipgloss-provided truncation helper if one exists, or
  • Making the measurement and slicing use the same width metric (even plain rune length), so the truncation decision and the actual slice are consistent.

Also, clamping keep to len(runes) before computing left/right would reduce the risk of awkward splits.

Suggested implementation:

func truncateMiddle(s string, width int) string {
	if width <= 0 {
		return ""
	}

	runes := []rune(s)
	if len(runes) <= width {
		return s
	}

	if width <= 3 {
		return strings.Repeat(".", width)
	}

	keep := width - 3
	if keep > len(runes) {
		keep = len(runes)
	}

	left := keep / 2
	right := keep - left

	if left+right >= len(runes) {
		return s
	}

	return string(runes[:left]) + "..." + string(runes[len(runes)-right:])
}

If lipgloss is only used in truncateMiddle, you should also remove its import from this file to avoid an unused import error. If it's used elsewhere, no import changes are needed.

Comment thread docs/docs/config.md
Comment on lines 328 to 332
It is possible to specify mixin as url. Lets will download it and load it as a mixin.
File will be stored in `.lets/mixins` directory.
When stderr is an interactive terminal, lets shows download progress for remote mixin downloads. Cache hits do not show progress.

By default mixin filename will be sha256 hash of url.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (typo): Capitalize URL and consider adding missing articles in the mixin URL sentence.

For example, you could write: “It is possible to specify a mixin as a URL. lets will download it and load it as a mixin.”

Suggested change
It is possible to specify mixin as url. Lets will download it and load it as a mixin.
File will be stored in `.lets/mixins` directory.
When stderr is an interactive terminal, lets shows download progress for remote mixin downloads. Cache hits do not show progress.
By default mixin filename will be sha256 hash of url.
It is possible to specify a mixin as a URL. lets will download it and load it as a mixin.
The file will be stored in the `.lets/mixins` directory.
When stderr is an interactive terminal, lets shows download progress for remote mixin downloads. Cache hits do not show progress.
By default, the mixin filename will be the SHA256 hash of the URL.

Comment thread docs/docs/config.md
File will be stored in `.lets/mixins` directory.
When stderr is an interactive terminal, lets shows download progress for remote mixin downloads. Cache hits do not show progress.

By default mixin filename will be sha256 hash of url.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (typo): Capitalize URL and add article for readability.

Rephrase to: "By default, the mixin filename will be the SHA256 hash of the URL."

Suggested change
By default mixin filename will be sha256 hash of url.
By default, the mixin filename will be the SHA256 hash of the URL.

@kindermax kindermax force-pushed the add-download-progress branch from 6c93616 to 096d449 Compare June 14, 2026 15:00
@kindermax kindermax merged commit 34dc51d into master Jun 14, 2026
5 checks passed
@kindermax kindermax deleted the add-download-progress branch June 14, 2026 15:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add progress bar for downloading remote mixins or remote configs

1 participant