Skip to content
Open
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
6 changes: 4 additions & 2 deletions cmd/riptide/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,10 @@ func main() {
t := theme.Get(themeName)

lipgloss.SetHasDarkBackground(true)
fmt.Fprint(os.Stdout, "\x1b]11;"+t.HexBG()+"\a")
fmt.Fprint(os.Stdout, "\x1b]10;"+t.HexFG()+"\a")
if !t.IsTransparent() {
fmt.Fprint(os.Stdout, "\x1b]11;"+t.HexBG()+"\a")
fmt.Fprint(os.Stdout, "\x1b]10;"+t.HexFG()+"\a")
}
defer func() {
fmt.Fprint(os.Stdout, "\x1b]111\a\x1b]110\a")
}()
Expand Down
74 changes: 71 additions & 3 deletions internal/theme/theme.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ var DefaultTheme = Theme{
// All is every built-in palette, default first.
var All = []Theme{
DefaultTheme,
SystemTheme,
oceanTheme,
midnightTheme,
sunsetTheme,
Expand Down Expand Up @@ -150,16 +151,41 @@ func PaintScreen(t Theme, width, height int, content string) string {
if height <= 0 {
height = 24
}

if !t.IsTransparent() {
lines := strings.Split(content, "\n")
bg := lipgloss.NewStyle().Background(t.AppBg)
for i, line := range lines {
lines[i] = bg.Render(line)
}
content = strings.Join(lines, "\n")

return lipgloss.Place(
width, height,
lipgloss.Center, lipgloss.Center,
content,
lipgloss.WithWhitespaceBackground(t.AppBg),
)
}
return lipgloss.Place(
width, height,
lipgloss.Center, lipgloss.Center,
content,
lipgloss.WithWhitespaceBackground(t.AppBg),
)
}

// Hex returns the AppBg as a #rrggbb string for OSC sequences.
// IsTransparent returns true when the theme uses a transparent canvas so
// callers can skip background fills / OSC sequences.
func (t Theme) IsTransparent() bool {
return t.AppBg == ""
}

// HexBG returns the AppBg as a #rrggbb string for OSC sequences.
// Returns empty string for transparent themes (caller should skip OSC).
func (t Theme) HexBG() string {
if t.IsTransparent() {
return ""
}
s := string(t.AppBg)
if s == "" {
return "#191a1b"
Expand All @@ -168,14 +194,56 @@ func (t Theme) HexBG() string {
}

// HexFG returns a concrete foreground for OSC sequences.
// Returns empty string for transparent themes (caller should skip OSC).
func (t Theme) HexFG() string {
if t.IsTransparent() {
return ""
}
if t.AccentDL != "" {
// Prefer a soft off-white derived from foreground dark branch.
return "#e8eaed"
}
return "#e8eaed"
}

// SystemTheme uses the terminal's native color palette with transparent
// background. IsTransparent() identifies it by Name=="system". Accent colors
// reference ANSI palette numbers (0-15) so they inherit whatever colors the
// terminal emulator has configured. Background fills are left empty for
// transparency so the terminal canvas shows through.
var SystemTheme = Theme{
Name: "system",
Display: "System",
Tagline: "Terminal native colors",

AppBg: lipgloss.Color(""),

Foreground: lipgloss.AdaptiveColor{},
Muted: lipgloss.AdaptiveColor{Light: "8", Dark: "8"},
Border: lipgloss.AdaptiveColor{Light: "7", Dark: "8"},
Download: lipgloss.AdaptiveColor{Light: "6", Dark: "6"},
Upload: lipgloss.AdaptiveColor{Light: "3", Dark: "3"},
Latency: lipgloss.AdaptiveColor{Light: "5", Dark: "5"},
Highlight: lipgloss.AdaptiveColor{Light: "2", Dark: "2"},

GraphDownBottom: lipgloss.Color("6"),
GraphDownTop: lipgloss.Color("6"),
GraphUpBottom: lipgloss.Color("3"),
GraphUpTop: lipgloss.Color("3"),

MenuAccentFill: lipgloss.AdaptiveColor{},

MenuIdleFill: lipgloss.Color(""),
MenuSelectDL: lipgloss.Color(""),
MenuSelectUL: lipgloss.Color(""),
MenuSelectExit: lipgloss.Color(""),
MenuSelectSet: lipgloss.Color(""),

AccentDL: lipgloss.Color("6"),
AccentUL: lipgloss.Color("3"),
AccentLat: lipgloss.Color("5"),
AccentHL: lipgloss.Color("2"),
}

// --- Built-in palettes ---------------------------------------------------

var oceanTheme = Theme{
Expand Down
16 changes: 15 additions & 1 deletion internal/ui/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,13 @@ func (a *App) enter(s screenID) tea.Cmd {
func (a *App) applyTheme(name string) {
t := apptheme.Get(name)
a.theme = t
fmt.Fprint(os.Stdout, "\x1b]11;"+t.HexBG()+"\a")
if t.IsTransparent() {
fmt.Fprint(os.Stdout, "\x1b]111\a\x1b]110\a")
} else {
fmt.Fprint(os.Stdout, "\x1b]11;"+t.HexBG()+"\a")
fmt.Fprint(os.Stdout, "\x1b]10;"+t.HexFG()+"\a")
}

if a.menu != nil {
a.menu.applyTheme(t)
}
Expand All @@ -117,18 +123,26 @@ func (a *App) applyTheme(name string) {
a.test.theme = t
if a.test.dlGraph != nil {
a.test.dlGraph.bottom, a.test.dlGraph.top = t.GraphDownBottom, t.GraphDownTop
a.test.dlGraph.bg = t.AppBg
a.test.dlGraph.grid = gridColor(t, t.GraphDownBottom)
}
if a.test.ulGraph != nil {
a.test.ulGraph.bottom, a.test.ulGraph.top = t.GraphUpBottom, t.GraphUpTop
a.test.ulGraph.bg = t.AppBg
a.test.ulGraph.grid = gridColor(t, t.GraphUpBottom)
}
}
if a.monitor != nil {
a.monitor.theme = t
if a.monitor.dlGraph != nil {
a.monitor.dlGraph.bottom, a.monitor.dlGraph.top = t.GraphDownBottom, t.GraphDownTop
a.monitor.dlGraph.bg = t.AppBg
a.monitor.dlGraph.grid = gridColor(t, t.GraphDownBottom)
}
if a.monitor.ulGraph != nil {
a.monitor.ulGraph.bottom, a.monitor.ulGraph.top = t.GraphUpBottom, t.GraphUpTop
a.monitor.ulGraph.bg = t.AppBg
a.monitor.ulGraph.grid = gridColor(t, t.GraphUpBottom)
}
}
}
Expand Down
50 changes: 39 additions & 11 deletions internal/ui/card.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,17 @@ type cardState struct {
err error
}

// gridColor returns the color used for graph grid/peak lines.
// For transparent themes (terminal native palette) it returns ANSI 8 (gray)
// since hex-based dimming cannot target ANSI palette numbers. For all other
// themes it returns a faint blend of bottom blended toward the canvas.
func gridColor(theme apptheme.Theme, bottom lipgloss.Color) lipgloss.Color {
if theme.IsTransparent() {
return lipgloss.Color("8")
}
return dimColor(bottom, 0.28, theme.AppBg)
}

// newCardState builds the shared state for one run: spinner, channels, context,
// and the two history graphs.
func newCardState(theme apptheme.Theme, compact bool) *cardState {
Expand All @@ -142,8 +153,8 @@ func newCardState(theme apptheme.Theme, compact bool) *cardState {
events: make(chan tea.Msg, 64),
spinner: s,
phase: engine.PhaseInit,
dlGraph: newGraph(40, graphHeight, theme.GraphDownBottom, theme.GraphDownTop),
ulGraph: newGraph(40, graphHeight, theme.GraphUpBottom, theme.GraphUpTop),
dlGraph: newGraph(40, graphHeight, theme.GraphDownBottom, theme.GraphDownTop, theme.AppBg, gridColor(theme, theme.GraphDownBottom)),
ulGraph: newGraph(40, graphHeight, theme.GraphUpBottom, theme.GraphUpTop, theme.AppBg, gridColor(theme, theme.GraphUpBottom)),
}
}

Expand Down Expand Up @@ -485,22 +496,33 @@ var logoStops = [4][3]uint8{

// renderHeader draws the RIPTIDE wordmark the same way flow draws FLOW:
// pre-baked ANSI Shadow art + per-row 4-stop gradient + muted tagline.
func renderHeader(tagline string) string {
func renderHeader(theme apptheme.Theme, tagline string) string {
n := len(logoSrc)
lines := make([]string, n)
for i, line := range logoSrc {
rowT := 0.0
if n > 1 {
rowT = float64(i) / float64(n-1)
var color lipgloss.Color
if theme.IsTransparent() {
color = theme.AccentDL
} else {
rowT := 0.0
if n > 1 {
rowT = float64(i) / float64(n-1)
}
r, g, b := logoGradient(rowT)
color = lipgloss.Color(fmt.Sprintf("#%02x%02x%02x", r, g, b))
}
r, g, b := logoGradient(rowT)
color := lipgloss.Color(fmt.Sprintf("#%02x%02x%02x", r, g, b))
lines[i] = lipgloss.NewStyle().Foreground(color).Bold(true).Render(line)
}
logo := lipgloss.JoinVertical(lipgloss.Left, lines...)

var tagColor lipgloss.Color
if theme.IsTransparent() {
tagColor = theme.AccentHL
} else {
tagColor = lipgloss.Color("#94a3b8")
}
tag := lipgloss.NewStyle().
Foreground(lipgloss.Color("#94a3b8")).
Foreground(tagColor).
Render(tagline)

return lipgloss.JoinVertical(lipgloss.Center, logo, "", tag)
Expand Down Expand Up @@ -532,9 +554,15 @@ func lerpU8(a, b uint8, t float64) uint8 {

// renderCompactHeader draws a minimal header: just the tagline text without the
// large pixel-art logo. Used when --compact mode is active or toggled with 't'.
func renderCompactHeader(tagline string) string {
func renderCompactHeader(theme apptheme.Theme, tagline string) string {
var color lipgloss.Color
if theme.IsTransparent() {
color = theme.AccentDL
} else {
color = lipgloss.Color("#56d364")
}
tag := lipgloss.NewStyle().
Foreground(lipgloss.Color("#56d364")).
Foreground(color).
Bold(true).
Render(tagline)
return tag
Expand Down
4 changes: 2 additions & 2 deletions internal/ui/menu.go
Original file line number Diff line number Diff line change
Expand Up @@ -488,9 +488,9 @@ func (m *menuModel) View() string {

var header string
if m.compact {
header = renderCompactHeader("Choose how you'd like to measure your connection")
header = renderCompactHeader(m.theme, "Choose how you'd like to measure your connection")
} else {
header = renderHeader("Choose how you'd like to measure your connection")
header = renderHeader(m.theme, "Choose how you'd like to measure your connection")
}

rule := lipgloss.NewStyle().Foreground(m.theme.Border).Render(strings.Repeat("─", 36))
Expand Down
4 changes: 2 additions & 2 deletions internal/ui/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -428,9 +428,9 @@ func (m *model) View() string {

var header string
if m.compact {
header = renderCompactHeader("Wonder how speedy your internet is?")
header = renderCompactHeader(m.theme, "Wonder how speedy your internet is?")
} else {
header = renderHeader("Wonder how speedy your internet is?")
header = renderHeader(m.theme, "Wonder how speedy your internet is?")
}

var main string
Expand Down
4 changes: 2 additions & 2 deletions internal/ui/monitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,9 +269,9 @@ func (m *monitorModel) View() string {

var header string
if m.compact {
header = renderCompactHeader("Watching your connection in real time")
header = renderCompactHeader(m.theme, "Watching your connection in real time")
} else {
header = renderHeader("Watching your connection in real time")
header = renderHeader(m.theme, "Watching your connection in real time")
}

stack := lipgloss.JoinVertical(lipgloss.Center, header, "", card)
Expand Down
2 changes: 1 addition & 1 deletion internal/ui/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -601,7 +601,7 @@ func (m *settingsModel) View() string {
}

// Always compact title — full logo makes the page too tall.
header := renderCompactHeader("Settings · Tune riptide to your taste")
header := renderCompactHeader(m.theme, "Settings · Tune riptide to your taste")

search := m.viewSearch(cardW)

Expand Down
Loading