diff --git a/CHANGELOG.md b/CHANGELOG.md index bcfa680..b6f55cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +## [0.2.4] - 2026-08-20 + +### Added +- In-app version display — the running version (e.g. `v0.2.4`) now appears as a small accent-colored pill, centered under the hero `flow` logo and next to the title in compact/mini modes (and via `flow --version`). +- The active-interface dot now glows: it flashes bright on each sample in compact and mini modes. + +### Changed +- Much more vibrant, cheerful colors — the default theme's download/upload gradients, borders, and graph colors were brightened and saturated for a lively look. +- All text is now prominent against any background (black or transparent): the dim/muted text ramp was brightened across every built-in theme so secondary labels, flags, and hints always stay clearly readable. +- The hero mode now combines the rainbow ASCII `flow` logo with a centered version pill and the `"Calm your network. See it breathe."` tagline. +- Status and latency indicators now use theme-consistent colors instead of hardcoded green/amber/red, so they always match the active theme. The accent color highlights elevated latency or a down link. +- Removed the green interface-status dot from the footer for a cleaner, less cluttered look. The interface name and state (paused / down only / up only / refresh interval) remain. +- VERSION bumped to 0.2.4. + ## [0.2.3] - 2026-08-19 ### Fixed diff --git a/Makefile b/Makefile index 1390c72..64fff05 100644 --- a/Makefile +++ b/Makefile @@ -59,7 +59,7 @@ clean: ## demo: generate assets/demo.gif with VHS demo: build - vhs flow.tape + vhs demo.tape @if command -v gifsicle >/dev/null 2>&1; then \ echo "optimizing with gifsicle..."; \ gifsicle -O3 --colors 256 --lossy=80 -o assets/demo.gif assets/demo.gif; \ diff --git a/README.md b/README.md index ea8929c..14d0592 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,8 @@ Pre-built binaries for Linux, macOS, and Windows (amd64/arm64) are on the [relea - Session peak tracking and daily totals, persisted across restarts - Streaming JSON output (`--json-stream`) for pipelines - Four responsive display modes, switching on both width and height +- Vibrant, cheerful theme out of the box -- colorful direction gradients, borders, and a glowing activity dot (also via `flow --version`) +- Current version shown in-app as an accent pill under the hero logo / next to the title, and via `flow --version` - Zero required configuration - Linux, macOS, and Windows -- no elevated privileges required diff --git a/VERSION b/VERSION index 7179039..abd4105 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.3 +0.2.4 diff --git a/assets/compact_mode.png b/assets/compact_mode.png index 888ca32..e61a436 100644 Binary files a/assets/compact_mode.png and b/assets/compact_mode.png differ diff --git a/assets/demo.gif b/assets/demo.gif index b71361a..855f0cb 100644 Binary files a/assets/demo.gif and b/assets/demo.gif differ diff --git a/assets/mini_mode.png b/assets/mini_mode.png index 1f2a637..4b3066a 100644 Binary files a/assets/mini_mode.png and b/assets/mini_mode.png differ diff --git a/assets/normal_mode.png b/assets/normal_mode.png index dd978cb..4e52cdc 100644 Binary files a/assets/normal_mode.png and b/assets/normal_mode.png differ diff --git a/cmd/flow/main.go b/cmd/flow/main.go index b2af636..70c5060 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.3" +var version = "0.2.4" func main() { flagTiny := flag.Bool("tiny", false, "single-line mode for tmux/status bars") @@ -112,6 +112,8 @@ func main() { initialIface = "auto" } + ui.SetVersion(version) + model := ui.New(cfg, smp, ifaces, initialIface, cancel, forced) opts := []tea.ProgramOption{tea.WithAltScreen()} diff --git a/demo.tape b/demo.tape index 9054c75..7906d11 100644 --- a/demo.tape +++ b/demo.tape @@ -4,7 +4,7 @@ Require make Require curl Set Shell "bash" -Set FontFamily "JetBrains Mono" +Set FontFamily "JetBrainsMono Nerd Font Mono" Set FontSize 16 Set Theme { "name": "Flow", diff --git a/internal/theme/theme.go b/internal/theme/theme.go index 1b75878..d03d608 100644 --- a/internal/theme/theme.go +++ b/internal/theme/theme.go @@ -36,8 +36,8 @@ type ThemeInfo struct { var themes = []Theme{ { Name: "default", - TextDim: "#64748b", - TextMuted: "#94a3b8", + TextDim: "#94a3b8", + TextMuted: "#c3cedb", TextSoft: "#cbd5e1", TextBase: "#e2e8f0", TextBright: "#f8fafc", @@ -45,23 +45,23 @@ var themes = []Theme{ Border: "#334155", // Neutral slate-700 Accent: "#6366f1", // Indigo DownloadStops: [5][3]uint8{ - {0x3b, 0x82, 0xf6}, // Vibrant Blue (#3B82F6) - {0x63, 0x66, 0xf1}, // Indigo (#6366F1) - {0x06, 0xb6, 0xd4}, // Bright Cyan (#06B6D4) - {0x00, 0xf5, 0xd4}, // Glowing Mint-Cyan + {0x26, 0x5f, 0xff}, // Strong Blue (#265FFF) + {0x22, 0x8b, 0xfa}, // Vivid Azure (#228BFA) + {0x08, 0xcf, 0xf6}, // Bright Cyan (#08CFF6) + {0x2d, 0xff, 0xd6}, // Neon Mint-Green (#2DFFD6) {0xff, 0xff, 0xff}, // Pure White }, UploadStops: [5][3]uint8{ - {0x10, 0xb9, 0x81}, // Emerald (#10B981) - {0x22, 0xc5, 0x5e}, // Green (#22C55E) - {0x84, 0xcc, 0x16}, // Lime (#84CC16) - {0xa3, 0xe6, 0x35}, // Bright Lime + {0x07, 0xc1, 0x6e}, // Vivid Emerald (#07C16E) + {0x2f, 0xd3, 0x54}, // Bright Green (#2FD354) + {0xa3, 0xe6, 0x35}, // Lime (#A3E635) + {0xdd, 0xff, 0x4f}, // Neon Lime (#DDFF4F) {0xff, 0xff, 0xff}, // Pure White }, - DownloadBorderStart: [3]uint8{0x25, 0x63, 0xeb}, + DownloadBorderStart: [3]uint8{0x1d, 0x4e, 0xd8}, DownloadBorderEnd: [3]uint8{0x06, 0xb6, 0xd4}, - UploadBorderStart: [3]uint8{0x05, 0x96, 0x69}, - UploadBorderEnd: [3]uint8{0x84, 0xcc, 0x16}, + UploadBorderStart: [3]uint8{0x04, 0x78, 0x57}, + UploadBorderEnd: [3]uint8{0xa3, 0xe6, 0x35}, LogoStops: [4][3]uint8{ {0xd9, 0x46, 0xef}, // Fuchsia {0x8b, 0x5c, 0xf6}, // Purple @@ -71,8 +71,8 @@ var themes = []Theme{ }, { Name: "nord", - TextDim: "#4c566a", // Polar Night Gray - TextMuted: "#434c5e", // Darker Gray + TextDim: "#9fb3cc", // Polar Sky Gray + TextMuted: "#c6d3e2", // Light Frost Gray TextSoft: "#d8dee9", // Snow Storm Soft TextBase: "#e5e9f0", // Snow Storm Base TextBright: "#eceff4", // Snow Storm Bright @@ -106,8 +106,8 @@ var themes = []Theme{ }, { Name: "dracula", - TextDim: "#6272a4", // Comment Purple - TextMuted: "#6272a4", + TextDim: "#8be9fd", // Cyan + TextMuted: "#bd93f9", // Purple TextSoft: "#f8f8f2", // Foreground TextBase: "#f8f8f2", TextBright: "#f1fa8c", // Yellow Accent @@ -141,8 +141,8 @@ var themes = []Theme{ }, { Name: "gruvbox", - TextDim: "#7c6f64", // Gray - TextMuted: "#a89984", // Light Gray + TextDim: "#b5a28c", // Soft Gray-Brown + TextMuted: "#d4c5a8", // Light Sand TextSoft: "#ebdbb2", // Cream TextBase: "#ebdbb2", TextBright: "#fabd2f", // Yellow @@ -176,8 +176,8 @@ var themes = []Theme{ }, { Name: "forest", - TextDim: "#3f6212", // Dark Moss Green - TextMuted: "#4ade80", // Light Green + TextDim: "#34d399", // Emerald + TextMuted: "#6ee7b7", // Soft Green TextSoft: "#86efac", // Soft Green TextBase: "#dcfce7", // Very Soft Green TextBright: "#f0fdf4", // Bright leaf @@ -211,8 +211,8 @@ var themes = []Theme{ }, { Name: "monochrome", - TextDim: "#525252", // Neutral Gray - TextMuted: "#737373", // Lighter Gray + TextDim: "#808080", // Neutral Gray + TextMuted: "#989898", // Light Gray TextSoft: "#a3a3a3", // Soft Gray TextBase: "#e5e5e5", // Off-white TextBright: "#ffffff", // Pure White @@ -246,8 +246,8 @@ var themes = []Theme{ }, { Name: "catppuccin", - TextDim: "#585b70", - TextMuted: "#7f849c", + TextDim: "#8b93ad", + TextMuted: "#a5aec7", TextSoft: "#a6adc8", TextBase: "#cdd6f4", TextBright: "#f5e0dc", @@ -281,8 +281,8 @@ var themes = []Theme{ }, { Name: "tokyo-night", - TextDim: "#565f89", - TextMuted: "#737aa2", + TextDim: "#7f8bb8", + TextMuted: "#8f9dcd", TextSoft: "#9aa5ce", TextBase: "#a9b1d6", TextBright: "#c0caf5", @@ -383,7 +383,7 @@ func GetTextDimColor() string { func Title() lipgloss.Style { return lipgloss.NewStyle(). - Foreground(lipgloss.Color(activeTheme.TextSoft)). + Foreground(lipgloss.Color(activeTheme.TextBright)). Bold(true) } @@ -401,9 +401,12 @@ func Dim() lipgloss.Style { func LogoDotColor(pulse float64) lipgloss.Style { pulse = animate.Clamp01(pulse) - r1, g1, b1 := hexToRGB(activeTheme.TextDim) - r2, g2, b2 := hexToRGB(activeTheme.Accent) - r, g, b := animate.ColorLerp(r1, g1, b1, r2, g2, b2, pulse) + ra, ga, ba := hexToRGB(activeTheme.Accent) + // Peak color: brighten the accent toward white so the dot flashes a neon glow on each sample. + br := uint8(float64(ra)*0.30 + 255*0.70) + bg := uint8(float64(ga)*0.30 + 255*0.70) + bb := uint8(float64(ba)*0.30 + 255*0.70) + r, g, b := animate.ColorLerp(ra, ga, ba, br, bg, bb, pulse) return lipgloss.NewStyle().Foreground(lipgloss.Color(fmt.Sprintf("#%02x%02x%02x", r, g, b))) } @@ -489,6 +492,13 @@ func ValuePrimary(intensity float64, download bool) lipgloss.Style { return st.Bold(true) } +func DirArrow(download bool) string { + if download { + return "↓" + } + return "↑" +} + var logoSrc = []string{ "███████╗██╗ ██████╗ ██╗ ██╗", "██╔════╝██║ ██╔═══██╗██║ ██║", @@ -547,13 +557,6 @@ func fourStopLogoGradient(position, brightness float64) (uint8, uint8, uint8) { return r, g, b } -func DirArrow(download bool) string { - if download { - return "↓" - } - return "↑" -} - const logoSubtitle = "Calm your network. See it breathe." func LogoSubtitle(width int) string { @@ -569,6 +572,7 @@ func LogoSubtitle(width int) string { if rightPad < 0 { rightPad = 0 } + return strings.Repeat(" ", pad) + styled + strings.Repeat(" ", rightPad) } diff --git a/internal/ui/model.go b/internal/ui/model.go index 1d46440..39f37d7 100644 --- a/internal/ui/model.go +++ b/internal/ui/model.go @@ -33,6 +33,17 @@ type ifaceDetailMsg struct { err error } +// appVersion is set from the main package via SetVersion so the UI can +// surface it in the title. Defaults to "dev" for local builds. +var appVersion = "dev" + +// SetVersion records the build version for display inside the UI. +func SetVersion(v string) { + if v != "" { + appVersion = v + } +} + type DisplayFilter int const ( diff --git a/internal/ui/views.go b/internal/ui/views.go index 08d851d..d4b0f83 100644 --- a/internal/ui/views.go +++ b/internal/ui/views.go @@ -132,10 +132,24 @@ func lineCount(lines []string) int { return strings.Count(strings.Join(lines, "\n"), "\n") + 1 } +// versionBadge renders the running version as a small accent-colored pill. +func versionBadge() string { + return lipgloss.NewStyle(). + Foreground(lipgloss.Color("#ffffff")). + Background(lipgloss.Color(theme.GetAccentColor())). + Padding(0, 1). + Render("v" + strings.TrimPrefix(appVersion, "v")) +} + +func centerText(s string, width int) string { + return lipgloss.NewStyle().Width(width).Align(lipgloss.Center).Render(s) +} + func TitleRow(pulse float64) string { - return fmt.Sprintf("%s %s", + logo := fmt.Sprintf("%s %s", theme.LogoDotColor(pulse).Render("●"), theme.Title().Render("flow")) + return logo + " " + versionBadge() } func renderTiny(m Model) string { @@ -157,17 +171,13 @@ func renderTiny(m Model) string { func renderStatsLine(m Model) string { if m.pingLatency > 0 { ms := m.pingLatency.Seconds() * 1000 - var color string - switch { - case ms < 30: - color = "#10b981" - case ms < 100: - color = "#f59e0b" - default: - color = "#ef4444" + // Use theme-consistent styling: neutral when healthy, accent when high. + style := theme.Soft() + if ms >= 100 { + style = theme.Accent() } - pingIcon := lipgloss.NewStyle().Foreground(lipgloss.Color(color)).Render("↔") - pingVal := lipgloss.NewStyle().Foreground(lipgloss.Color(color)).Bold(true).Render(fmt.Sprintf("%.0fms", ms)) + pingIcon := style.Render("↔") + pingVal := style.Bold(true).Render(fmt.Sprintf("%.0fms", ms)) return theme.Muted().Render("ping ") + pingIcon + " " + pingVal } return "" @@ -280,13 +290,13 @@ func renderIfaceDetails(m Model) string { for _, addr := range d.Addrs { block = append(block, " "+theme.Muted().Render("ip ")+theme.Soft().Render(addr)) } - statusColor := "#10b981" + statusStyle := theme.Soft() statusText := "up" if !d.IsUp { - statusColor = "#ef4444" + statusStyle = theme.Accent() statusText = "down" } - block = append(block, " "+theme.Muted().Render("link ")+lipgloss.NewStyle().Foreground(lipgloss.Color(statusColor)).Bold(true).Render(statusText)) + block = append(block, " "+theme.Muted().Render("link ")+statusStyle.Bold(true).Render(statusText)) if d.Mtu > 0 { block = append(block, " "+theme.Muted().Render("mtu ")+theme.Soft().Render(fmt.Sprintf("%d", d.Mtu))) } @@ -492,17 +502,23 @@ func dashboardContentLines(m Model, mode ViewMode) []string { switch mode { case ViewHero: - if termH >= 28 { - logo := theme.LogoColored(contentW) - lines = append(lines, logo...) - lines = append(lines, GapRow) - lines = append(lines, theme.LogoSubtitle(contentW)) - lines = append(lines, GapRow) + if termH >= 30 { + if logo := theme.LogoColored(contentW); logo != nil { + lines = append(lines, logo...) + lines = append(lines, GapRow) + lines = append(lines, centerText(versionBadge(), contentW)) + lines = append(lines, GapRow) + lines = append(lines, theme.LogoSubtitle(contentW)) + lines = append(lines, GapRow) + } else { + lines = append(lines, TitleRow(m.samplePulse)) + lines = append(lines, GapRow) + } } else { lines = append(lines, TitleRow(m.samplePulse)) lines = append(lines, GapRow) } - case ViewCompact: + case ViewCompact, ViewMini: lines = append(lines, TitleRow(m.samplePulse)) lines = append(lines, GapRow) } @@ -547,11 +563,7 @@ func dashboardContentLines(m Model, mode ViewMode) []string { } lines = append(lines, GapRow) - dotColor := "#10b981" - if m.paused { - dotColor = "#ef4444" - } - ifaceStr := fmt.Sprintf("%s %s", lipgloss.NewStyle().Foreground(lipgloss.Color(dotColor)).Render("●"), theme.Muted().Render(m.ifaceName)) + ifaceStr := theme.Muted().Render(m.ifaceName) if m.paused { ifaceStr += theme.Dim().Render(" paused") }