diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bbcc34..1f67716 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## [0.2.2] - 2026-08-18 + +### Fixed +- Current download/upload speed always displaying `0 B/s` — the spring animation in `animate.Spring` was numerically unstable at the UI tick interval (130ms), since `1 - damping*dt` evaluated to a negative damping factor. Velocity flipped sign and grew every tick, driving the animated value deeply negative, where `FormatBpsExt` clamped it to `0 B/s`. Damping now uses an exponential factor (`exp(-damping*dt)`), keeping the spring stable and convergent at any step size. The sparkline and peak values were unaffected because they read raw history samples directly. + +### Changed +- VERSION bumped to 0.2.2. + ## [0.2.1] - 2026-07-23 ### Added diff --git a/VERSION b/VERSION index 7dff5b8..ee1372d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.1 \ No newline at end of file +0.2.2 diff --git a/cmd/flow/main.go b/cmd/flow/main.go index eb06e7e..40629ca 100644 --- a/cmd/flow/main.go +++ b/cmd/flow/main.go @@ -16,7 +16,7 @@ import ( "github.com/programmersd21/flow/internal/ui" ) -var version = "0.2.1" +var version = "0.2.2" func main() { flagTiny := flag.Bool("tiny", false, "single-line mode for tmux/status bars") diff --git a/internal/animate/ease.go b/internal/animate/ease.go index 3c05537..b558773 100644 --- a/internal/animate/ease.go +++ b/internal/animate/ease.go @@ -32,7 +32,7 @@ func Clamp01(t float64) float64 { func Spring(current, target float64, velocity *float64, dt float64) float64 { force := stiffness * (target - current) *velocity += force * dt - *velocity *= 1.0 - damping*dt + *velocity *= math.Exp(-damping * dt) if *velocity < 0.0001 && *velocity > -0.0001 { *velocity = 0 } diff --git a/internal/animate/ease_test.go b/internal/animate/ease_test.go index 3429a2f..6cd77df 100644 --- a/internal/animate/ease_test.go +++ b/internal/animate/ease_test.go @@ -48,3 +48,22 @@ func TestSpringConverges(t *testing.T) { t.Errorf("Spring did not converge: %f (want ~100)", val) } } + +func TestSpringStableAtUITickInterval(t *testing.T) { + const target = 34603008.0 + var vel float64 + val := 0.0 + minVal := val + for i := 0; i < 1000; i++ { + val = Spring(val, target, &vel, 0.13) + if val < minVal { + minVal = val + } + } + if math.Abs(val-target) > target*0.01 { + t.Errorf("Spring did not converge at dt=0.13: %f (want ~%f)", val, target) + } + if minVal < 0 { + t.Errorf("Spring oscillated negative at dt=0.13 (min %f) — value would render as 0 B/s", minVal) + } +}