Skip to content

Commit d1ee856

Browse files
Release v0.2.0: spring animation, ping target, JSON stream, stats persistence, custom themes
- Add spring-physics value animation for smooth throughput transitions - Make ping target configurable via --ping flag and ping_target config (default 1.1.1.1) - Add --json-stream continuous JSON Lines output for real-time pipelines - Persist daily traffic totals to ~/.config/flow/stats.json across restarts - Support custom theme .toml files in ~/.config/flow/themes/ - Add make check target (fmt, vet, lint, test) - Add ROADMAP.md and expand test coverage (animate, history persist, ping) - Bump VERSION to 0.2.0 and update CHANGELOG/README
1 parent 95fd660 commit d1ee856

17 files changed

Lines changed: 533 additions & 50 deletions

File tree

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
## [0.2.0] - 2026-07-15
2+
3+
### Added
4+
- Spring-smoothed value animation — Throughput values now glide smoothly toward their targets using spring physics instead of snapping, matching the architecture documented in README.
5+
- Configurable ping target — Customize the latency check host via `ping_target` in config or `--ping` CLI flag (default: 1.1.1.1).
6+
- Streaming JSON output (`--json-stream`) — Continuous JSON Lines output to stdout for piping into other tools, one sample per line.
7+
- Daily totals persistence — Today's traffic totals are saved to `~/.config/flow/stats.json` on quit and restored on next launch, so totals survive process restarts.
8+
- Custom theme files — Place `.toml` files in `~/.config/flow/themes/` to define user themes with custom colors and gradients, automatically picked up at startup.
9+
- ROADWAY.md — Published project roadmap covering v0.2.x through v0.5.x.
10+
- Expanded test coverage — New test suites for animate package (spring physics, easing) and history persistence (save/load round-trip).
11+
12+
### Changed
13+
- VERSION bumped to 0.2.0.
14+
- Makefile — Added `make check` target for format, vet, lint, and test in one command.
15+
116
## [0.1.7] - 2026-07-10
217

318
### Added

Makefile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,13 @@ demo: build
6868
echo "install gifsicle for further optimization: brew install gifsicle"; \
6969
fi
7070

71+
## check: format, vet, lint, and test
72+
check:
73+
go fmt ./...
74+
go vet ./...
75+
golangci-lint run ./...
76+
go test ./...
77+
7178
## help: print this message
7279
help:
7380
@sed -n 's/^## //p' $(MAKEFILE_LIST) 2>/dev/null || findstr /B "##" Makefile

README.md

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -142,10 +142,11 @@ flow adjusts its display according to terminal width and height.
142142

143143
## Features
144144

145-
- Live latency (ping) indicator with color-coded ↔ display
145+
- Live latency (ping) indicator with color-coded ↔ display and configurable target
146+
- Spring-smoothed throughput animation for premium visual feel
146147
- Network processes panel — press `n` to view active processes sorted by connection count
147148
- Interface details overlay — press `I` (capital i) to view IP addresses, MAC, link status, MTU
148-
- Theme selector with 8 built-in themes — press `t` to browse and select
149+
- Theme selector with 8 built-in themes plus custom user themes — press `t` to browse and select
149150
- Reset confirmation — press `r` twice to confirm, preventing accidental data loss
150151
- Real-time download and upload throughput
151152
- Interpolated display values using spring-based animation
@@ -155,7 +156,9 @@ flow adjusts its display according to terminal width and height.
155156
- Minimalist, high-end unicode today statistics and keybinding footer
156157
- Directional indicators for traffic trend
157158
- Automatic unit scaling from B/s to GB/s
158-
- Session peak tracking and daily traffic totals
159+
- Session peak tracking and daily traffic totals (persisted across restarts)
160+
- Streaming JSON output (`--json-stream`) for real-time data pipelines
161+
- Custom theme files in `~/.config/flow/themes/`
159162
- Four display modes with automatic responsive switching on both width and height resize
160163
- No required configuration; optional TOML configuration file
161164
- Non-interactive output modes for use in scripts
@@ -195,8 +198,10 @@ flow --tiny # single-line mode for status bars
195198
flow --mini # graphs-only mode, no headers/footers
196199
flow --compact # compact layout with title row and waveforms
197200
flow --json # single JSON output, then exit
201+
flow --json-stream # continuous JSON lines output
198202
flow --once # single plain-text output, then exit
199203
flow --interface wlan0 # specify network interface
204+
flow --ping 8.8.8.8 # ping target for latency measurement
200205
flow --refresh 500ms # adjust sampling interval (default 100ms)
201206
flow --bits # display in bits/sec instead of bytes/sec
202207
flow --no-color
@@ -260,13 +265,14 @@ A configuration file is created automatically on first run:
260265
The `XDG_CONFIG_HOME` environment variable is respected on Linux if set.
261266

262267
```toml
263-
refresh = "100ms" # sampling interval
264-
history = 60 # seconds of retained sparkline history
265-
theme = "default"
266-
unit = "auto" # auto, kb, mb, or gb
267-
interface = "auto" # auto, or a specific interface name (e.g. eth0, wlan0)
268-
no_color = false
269-
bits = false # display throughput in bits/sec
268+
refresh = "100ms" # sampling interval
269+
history = 60 # seconds of retained sparkline history
270+
theme = "default"
271+
unit = "auto" # auto, kb, mb, or gb
272+
interface = "auto" # auto, or a specific interface name (e.g. eth0, wlan0)
273+
no_color = false
274+
bits = false # display throughput in bits/sec
275+
ping_target = "1.1.1.1" # host for latency measurement
270276
```
271277

272278
<details>
@@ -290,6 +296,24 @@ All elements are centered on both axes. Panel border color changes according to
290296

291297
</details>
292298

299+
### Custom themes
300+
301+
Place `.toml` files in `~/.config/flow/themes/` to define user themes:
302+
303+
```toml
304+
name = "my-theme"
305+
text_dim = "#64748b"
306+
text_muted = "#94a3b8"
307+
text_soft = "#cbd5e1"
308+
text_base = "#e2e8f0"
309+
text_bright = "#f8fafc"
310+
text_pure = "#ffffff"
311+
border = "#334155"
312+
accent = "#6366f1"
313+
download = ["#3b82f6", "#6366f1", "#06b6d4", "#00f5d4", "#ffffff"]
314+
upload = ["#10b981", "#22c55e", "#84cc16", "#a3e635", "#ffffff"]
315+
```
316+
293317
## Architecture
294318

295319
flow runs two independent loops connected by a channel.

ROADMAP.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Roadmap
2+
3+
## v0.2.x: Polish & Practicality
4+
5+
- Spring-smoothed value animation
6+
- Configurable ping target
7+
- Streaming JSON output (`--json-stream`)
8+
- Daily totals persisted between sessions
9+
- Custom user themes
10+
- Data export (`--json-stream` for piping)
11+
12+
## v0.3.x: Deeper Insight
13+
14+
- Per-connection process bandwidth (platform-permitting)
15+
- Historical sparklines (multi-hour view)
16+
- Configurable notification thresholds
17+
- Network interface discovery on startup
18+
19+
## v0.4.x: Integration
20+
21+
- Web dashboard companion (optional HTTP server)
22+
- Remote monitoring over SSH
23+
- Plugin system for custom data sources
24+
25+
## v0.5.x: Scale
26+
27+
- Multiple interface dashboard
28+
- Alert/webhook integration
29+
- Session replay and export

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.1.7
1+
0.2.0

cmd/flow/main.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@ func main() {
2323
flagMini := flag.Bool("mini", false, "graphs-only mini mode")
2424
flagCompact := flag.Bool("compact", false, "numbers-only compact mode")
2525
flagJSON := flag.Bool("json", false, "one-shot JSON snapshot, then exit")
26+
flagJSONStream := flag.Bool("json-stream", false, "continuous JSON lines output")
2627
flagOnce := flag.Bool("once", false, "one-shot plain-text snapshot, then exit")
2728
flagIface := flag.String("interface", "", "force a specific network interface")
2829
flagRefresh := flag.Duration("refresh", 0, "sampling interval (e.g. 250ms)")
2930
flagNoColor := flag.Bool("no-color", false, "disable ANSI color output")
3031
flagBits := flag.Bool("bits", false, "display throughput in bits/sec instead of bytes/sec")
32+
flagPing := flag.String("ping", "", "ping target host (default: 1.1.1.1)")
3133
flagVersion := flag.Bool("version", false, "print version and exit")
3234

3335
flag.Usage = func() {
@@ -63,12 +65,20 @@ func main() {
6365
if *flagBits {
6466
cfg.Bits = true
6567
}
68+
if *flagPing != "" {
69+
cfg.PingTarget = *flagPing
70+
}
6671

6772
col := collector.New(cfg.Interface)
6873

6974
refresh := cfg.RefreshDuration()
7075
smp := sampler.New(col, refresh)
7176

77+
if *flagJSONStream {
78+
runJSONStream(smp, refresh, cfg.Bits)
79+
return
80+
}
81+
7282
if *flagJSON || *flagOnce {
7383
runOnce(col, smp, refresh, *flagJSON, cfg.Bits)
7484
return
@@ -190,6 +200,34 @@ func runOnce(col *collector.Collector, smp *sampler.Sampler, refresh time.Durati
190200
)
191201
}
192202

203+
func runJSONStream(smp *sampler.Sampler, refresh time.Duration, bits bool) {
204+
ctx, cancel := context.WithCancel(context.Background())
205+
defer cancel()
206+
207+
go smp.Run(ctx)
208+
209+
enc := json.NewEncoder(os.Stdout)
210+
s1 := <-smp.Out
211+
if s1.Err != nil {
212+
fmt.Fprintf(os.Stderr, "flow: %v\n", s1.Err)
213+
os.Exit(1)
214+
}
215+
for s := range smp.Out {
216+
if s.Err != nil {
217+
fmt.Fprintf(os.Stderr, "flow: %v\n", s.Err)
218+
os.Exit(1)
219+
}
220+
_ = enc.Encode(map[string]interface{}{
221+
"interface": s.Interface,
222+
"download_bps": s.DownBps,
223+
"upload_bps": s.UpBps,
224+
"download_human": ui.FormatBpsExt(s.DownBps, ui.UnitAuto, bits),
225+
"upload_human": ui.FormatBpsExt(s.UpBps, ui.UnitAuto, bits),
226+
"timestamp": s.At.Format(time.RFC3339Nano),
227+
})
228+
}
229+
}
230+
193231
func autoUnitExt(bps float64, bits bool) string {
194232
if bits {
195233
bps = bps * 8

internal/animate/ease.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ package animate
22

33
import "math"
44

5+
const (
6+
stiffness = 180.0
7+
damping = 12.0
8+
)
9+
510
func Lerp(a, b, t float64) float64 {
611
return a + (b-a)*t
712
}
@@ -23,3 +28,13 @@ func Clamp01(t float64) float64 {
2328
}
2429
return t
2530
}
31+
32+
func Spring(current, target float64, velocity *float64, dt float64) float64 {
33+
force := stiffness * (target - current)
34+
*velocity += force * dt
35+
*velocity *= 1.0 - damping*dt
36+
if *velocity < 0.0001 && *velocity > -0.0001 {
37+
*velocity = 0
38+
}
39+
return current + *velocity*dt
40+
}

internal/animate/ease_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package animate
2+
3+
import (
4+
"math"
5+
"testing"
6+
)
7+
8+
func TestClamp01(t *testing.T) {
9+
if got := Clamp01(-0.5); got != 0 {
10+
t.Errorf("Clamp01(-0.5) = %f; want 0", got)
11+
}
12+
if got := Clamp01(1.5); got != 1 {
13+
t.Errorf("Clamp01(1.5) = %f; want 1", got)
14+
}
15+
if got := Clamp01(0.5); got != 0.5 {
16+
t.Errorf("Clamp01(0.5) = %f; want 0.5", got)
17+
}
18+
}
19+
20+
func TestLerp(t *testing.T) {
21+
if got := Lerp(0, 100, 0.5); got != 50 {
22+
t.Errorf("Lerp(0, 100, 0.5) = %f; want 50", got)
23+
}
24+
}
25+
26+
func TestSpring(t *testing.T) {
27+
var vel float64
28+
val := Spring(0, 100, &vel, 0.016)
29+
if val <= 0 {
30+
t.Errorf("Spring(0, 100) = %f; want > 0 after one step", val)
31+
}
32+
}
33+
34+
func TestColorLerp(t *testing.T) {
35+
r, g, b := ColorLerp(0, 0, 0, 255, 255, 255, 0.5)
36+
if r != 128 || g != 128 || b != 128 {
37+
t.Errorf("ColorLerp(black, white, 0.5) = (%d,%d,%d); want (128,128,128)", r, g, b)
38+
}
39+
}
40+
41+
func TestSpringConverges(t *testing.T) {
42+
var vel float64
43+
val := 0.0
44+
for i := 0; i < 1000; i++ {
45+
val = Spring(val, 100, &vel, 0.016)
46+
}
47+
if math.Abs(val-100) > 1 {
48+
t.Errorf("Spring did not converge: %f (want ~100)", val)
49+
}
50+
}

internal/config/config.go

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,26 @@ import (
1313
)
1414

1515
type Config struct {
16-
Refresh duration `toml:"refresh"`
17-
History int `toml:"history"` // seconds of sparkline history
18-
Theme string `toml:"theme"`
19-
Unit string `toml:"unit"` // auto | kb | mb | gb
20-
Interface string `toml:"interface"` // auto | specific name
21-
NoColor bool `toml:"no_color"`
22-
Bits bool `toml:"bits"`
16+
Refresh duration `toml:"refresh"`
17+
History int `toml:"history"`
18+
Theme string `toml:"theme"`
19+
Unit string `toml:"unit"`
20+
Interface string `toml:"interface"`
21+
NoColor bool `toml:"no_color"`
22+
Bits bool `toml:"bits"`
23+
PingTarget string `toml:"ping_target"`
2324
}
2425

2526
func Defaults() Config {
2627
return Config{
27-
Refresh: duration{100 * time.Millisecond},
28-
History: 60,
29-
Theme: "default",
30-
Unit: "auto",
31-
Interface: "auto",
32-
NoColor: false,
33-
Bits: false,
28+
Refresh: duration{100 * time.Millisecond},
29+
History: 60,
30+
Theme: "default",
31+
Unit: "auto",
32+
Interface: "auto",
33+
NoColor: false,
34+
Bits: false,
35+
PingTarget: "1.1.1.1",
3436
}
3537
}
3638

@@ -78,6 +80,7 @@ func Save(cfg Config) error {
7880
cfg.Interface,
7981
boolStr(cfg.NoColor),
8082
boolStr(cfg.Bits),
83+
cfg.PingTarget,
8184
)
8285
return err
8386
}
@@ -117,20 +120,22 @@ func writeDefaults(path string, cfg Config) error {
117120
cfg.Interface,
118121
boolStr(cfg.NoColor),
119122
boolStr(cfg.Bits),
123+
cfg.PingTarget,
120124
)
121125
return err
122126
}
123127

124128
const defaultTOML = `# flow configuration
125129
# https://github.com/programmersd21/flow
126130
127-
refresh = "%s" # sampling interval (e.g. "100ms", "250ms", "1s")
128-
history = %d # seconds of sparkline history retained
129-
theme = "%s"
130-
unit = "%s" # auto | kb | mb | gb
131-
interface = "%s" # auto or interface name (e.g. "eth0", "wlan0")
132-
no_color = %s
133-
bits = %s # display throughput in bits/sec instead of bytes/sec
131+
refresh = "%s" # sampling interval (e.g. "100ms", "250ms", "1s")
132+
history = %d # seconds of sparkline history
133+
theme = "%s"
134+
unit = "%s" # auto | kb | mb | gb
135+
interface = "%s" # auto or interface name (e.g. "eth0", "wlan0")
136+
no_color = %s
137+
bits = %s # display in bits/sec instead of bytes/sec
138+
ping_target = "%s" # host for latency measurement
134139
`
135140

136141
func boolStr(b bool) string {

0 commit comments

Comments
 (0)