Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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; \
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.2.3
0.2.4
Binary file modified assets/compact_mode.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/mini_mode.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/normal_mode.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion cmd/flow/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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()}
Expand Down
2 changes: 1 addition & 1 deletion demo.tape
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
80 changes: 42 additions & 38 deletions internal/theme/theme.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,32 +36,32 @@ type ThemeInfo struct {
var themes = []Theme{
{
Name: "default",
TextDim: "#64748b",
TextMuted: "#94a3b8",
TextDim: "#94a3b8",
TextMuted: "#c3cedb",
TextSoft: "#cbd5e1",
TextBase: "#e2e8f0",
TextBright: "#f8fafc",
TextPure: "#ffffff",
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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -246,8 +246,8 @@ var themes = []Theme{
},
{
Name: "catppuccin",
TextDim: "#585b70",
TextMuted: "#7f849c",
TextDim: "#8b93ad",
TextMuted: "#a5aec7",
TextSoft: "#a6adc8",
TextBase: "#cdd6f4",
TextBright: "#f5e0dc",
Expand Down Expand Up @@ -281,8 +281,8 @@ var themes = []Theme{
},
{
Name: "tokyo-night",
TextDim: "#565f89",
TextMuted: "#737aa2",
TextDim: "#7f8bb8",
TextMuted: "#8f9dcd",
TextSoft: "#9aa5ce",
TextBase: "#a9b1d6",
TextBright: "#c0caf5",
Expand Down Expand Up @@ -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)
}

Expand All @@ -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)))
}

Expand Down Expand Up @@ -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{
"███████╗██╗ ██████╗ ██╗ ██╗",
"██╔════╝██║ ██╔═══██╗██║ ██║",
Expand Down Expand Up @@ -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 {
Expand All @@ -569,6 +572,7 @@ func LogoSubtitle(width int) string {
if rightPad < 0 {
rightPad = 0
}

return strings.Repeat(" ", pad) + styled + strings.Repeat(" ", rightPad)
}

Expand Down
11 changes: 11 additions & 0 deletions internal/ui/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
64 changes: 38 additions & 26 deletions internal/ui/views.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 ""
Expand Down Expand Up @@ -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)))
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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")
}
Expand Down
Loading