Skip to content

Commit eb92b7e

Browse files
committed
feat: Introduce semantic coloring for metric sparklines and refactor system information display into separate rows.
1 parent 8a55caf commit eb92b7e

3 files changed

Lines changed: 147 additions & 66 deletions

File tree

internal/monitor/monitor.go

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ type MetricHistory struct {
3030
}
3131

3232
type SystemInfo struct {
33-
Snapshot string
34-
Disk string
35-
Net string
33+
Uptime string
34+
Disk string
35+
Net string
3636
}
3737

3838
const HistoryLength = 30
@@ -87,17 +87,13 @@ func SampleMetrics() MetricsSample {
8787

8888
func SampleSystem() SystemInfo {
8989
var info SystemInfo
90-
load, _ := getLoadAvg()
91-
cpu, _ := getCPUUsage()
92-
mem, _ := getMemUsage()
93-
uptime := getUptimeShort()
94-
info.Snapshot = fmt.Sprintf("Snapshot: CPU %0.0f%% MEM %0.0f%% LOAD %0.2f UPTIME %s", cpu, mem, load, uptime)
90+
info.Uptime = "UPTIME: " + getUptimeShort()
9591

9692
if disk := getDiskSummary(); disk != "" {
97-
info.Disk = "Disk: " + disk
93+
info.Disk = "DISK: " + disk
9894
}
9995
if net := getNetSummary(); net != "" {
100-
info.Net = "Net: " + net
96+
info.Net = "NET: " + net
10197
}
10298
return info
10399
}

internal/theme/theme.go

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,19 +48,25 @@ type Styles struct {
4848
Info lipgloss.Style
4949
ContentBox lipgloss.Style
5050
Overflow lipgloss.Style
51-
Accent lipgloss.Color
52-
AccentDark lipgloss.Color
53-
Ink lipgloss.Color
54-
Muted lipgloss.Color
55-
Background lipgloss.Color
51+
// Semantic colors for metrics
52+
Green lipgloss.Style
53+
Yellow lipgloss.Style
54+
Red lipgloss.Style
55+
Processing lipgloss.Style
56+
57+
Accent lipgloss.Color
58+
AccentDark lipgloss.Color
59+
Ink lipgloss.Color
60+
Muted lipgloss.Color
61+
Background lipgloss.Color
5662
}
5763

5864
func BuildStyles(index int) Styles {
5965
if index < 0 || index >= len(Themes) {
6066
index = 0
6167
}
6268
t := Themes[index]
63-
69+
6470
s := Styles{}
6571
s.Accent = lipgloss.Color(t.Accent)
6672
s.AccentDark = lipgloss.Color(t.AccentDark)
@@ -81,5 +87,11 @@ func BuildStyles(index int) Styles {
8187
Padding(0, 1)
8288
s.Overflow = lipgloss.NewStyle().Foreground(s.Muted).Background(s.Background).Padding(0, 1)
8389

90+
// Semantic colors
91+
s.Green = lipgloss.NewStyle().Foreground(lipgloss.Color("#4ade80")).Background(s.AccentDark)
92+
s.Yellow = lipgloss.NewStyle().Foreground(lipgloss.Color("#facc15")).Background(s.AccentDark)
93+
s.Red = lipgloss.NewStyle().Foreground(lipgloss.Color("#f87171")).Background(s.AccentDark)
94+
s.Processing = lipgloss.NewStyle().Foreground(s.Muted).Background(s.AccentDark)
95+
8496
return s
8597
}

internal/ui/model.go

Lines changed: 123 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,6 @@ func NewModel() Model {
5959
vp := viewport.New(0, 0)
6060
vp.SetContent("Loading...")
6161

62-
// config.Load() now returns (Config, []Tab), we only need []Tab here
63-
// but the signature of Load changed in previous step, so we need to adapt
6462
_, tabs := config.Load()
6563

6664
return Model{
@@ -145,21 +143,17 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
145143

146144
func (m Model) View() string {
147145
header := m.renderTabs(m.tabs, m.active, m.width)
148-
summary := m.renderSummary(m.metrics, m.width)
149-
snapshot := m.renderInfoLine(m.system.Snapshot, m.width)
150-
disk := m.renderInfoLine(m.system.Disk, m.width)
151-
net := m.renderInfoLine(m.system.Net, m.width)
146+
metricsRow := m.renderMetricsRow(m.metrics, m.width)
147+
systemRow := m.renderSystemRow(m.system, m.width)
152148
title := m.renderContentTitle(m.tabs[m.active].Title, m.width)
153149
content := m.styles.ContentBox.Width(m.width).Render(m.viewport.View())
154150
footer := m.renderFooter(m.statusLine, spinnerFrames[m.spinnerIdx], m.width)
155151

156152
return lipgloss.JoinVertical(
157153
lipgloss.Left,
158154
header,
159-
summary,
160-
snapshot,
161-
disk,
162-
net,
155+
metricsRow,
156+
systemRow,
163157
title,
164158
content,
165159
footer,
@@ -213,7 +207,101 @@ func runCommandCmd(t config.Tab) tea.Cmd {
213207
}
214208
}
215209

216-
// Rendering helpers (unchanged)
210+
// Rendering helpers
211+
212+
// renderMetricsRow renders the top row of sparklines and current values
213+
func (m Model) renderMetricsRow(history monitor.MetricHistory, width int) string {
214+
if width <= 0 {
215+
return ""
216+
}
217+
218+
// Helper to render a single metric block with color
219+
renderBlock := func(label string, valStr string, data []float64, min, max float64, isPercent bool) string {
220+
// Determine color based on latest value
221+
var color lipgloss.Style
222+
if len(data) > 0 {
223+
last := data[len(data)-1]
224+
// Normalize value for color mapping
225+
param := last
226+
if !isPercent {
227+
// efficient approximation for load/net: reasonable max
228+
// load: max 4.0 (green), 8.0 (yellow), >8.0 (red)
229+
// net: max 1MB/s (green), 10MB/s (yellow), >10MB/s (red)
230+
// This is heuristic, percent is easier
231+
if max > 0 {
232+
param = (last / max) * 100
233+
}
234+
}
235+
236+
if param < 50 {
237+
color = m.styles.Green
238+
} else if param < 80 {
239+
color = m.styles.Yellow
240+
} else {
241+
color = m.styles.Red
242+
}
243+
} else {
244+
color = m.styles.Processing
245+
}
246+
247+
sl := sparkline(data, min, max)
248+
// Colorize the sparkline and the value
249+
return fmt.Sprintf("%s %s %s", label, color.Render(valStr), color.Render(sl))
250+
}
251+
252+
var blocks []string
253+
254+
// CPU
255+
if len(history.CPU) > 0 {
256+
val := history.CPU[len(history.CPU)-1]
257+
blocks = append(blocks, renderBlock("CPU", fmt.Sprintf("%0.0f%%", val), history.CPU, 0, 100, true))
258+
}
259+
260+
// MEM
261+
if len(history.Mem) > 0 {
262+
val := history.Mem[len(history.Mem)-1]
263+
blocks = append(blocks, renderBlock("MEM", fmt.Sprintf("%0.0f%%", val), history.Mem, 0, 100, true))
264+
}
265+
266+
// LOAD (heuristic color: <1.0 green, <high yellow, >high red)
267+
if len(history.Load) > 0 {
268+
val := history.Load[len(history.Load)-1]
269+
max := maxFloat(history.Load)
270+
if max < 2.0 {
271+
max = 2.0
272+
} // Minimum scale for load
273+
274+
// Custom logic for load color
275+
var color lipgloss.Style
276+
if val < 1.0 {
277+
color = m.styles.Green
278+
} else if val < 4.0 {
279+
color = m.styles.Yellow
280+
} else {
281+
color = m.styles.Red
282+
}
283+
284+
sl := sparkline(history.Load, 0, max)
285+
blocks = append(blocks, fmt.Sprintf("LOAD %s %s", color.Render(fmt.Sprintf("%0.2f", val)), color.Render(sl)))
286+
}
287+
288+
// NET
289+
if len(history.Net) > 0 {
290+
val := history.Net[len(history.Net)-1]
291+
max := maxFloat(history.Net)
292+
if max < 1 {
293+
max = 1
294+
}
295+
blocks = append(blocks, renderBlock("NET", monitor.FormatRate(val), history.Net, 0, max, false))
296+
}
297+
298+
if len(blocks) == 0 {
299+
return m.styles.Summary.Width(width).Render("Waiting for metrics...")
300+
}
301+
302+
row := strings.Join(blocks, " ")
303+
return m.styles.Summary.Width(width).Render(row)
304+
}
217305

218306
func (m Model) renderTabs(tabs []config.Tab, active, width int) string {
219307
if width <= 0 {
@@ -308,53 +396,28 @@ func (m Model) renderTabs(tabs []config.Tab, active, width int) string {
308396
return m.styles.Header.Width(width).Render(row)
309397
}
310398

311-
func (m Model) renderFooter(status, spinner string, width int) string {
312-
help := "q:quit tab/shift+tab:next/prev up/down/pgup/pgdn:scroll t:theme"
313-
if status != "" {
314-
help = spinner + " " + status + " | " + help
315-
} else if spinner != "" {
316-
help = spinner + " " + help
399+
func (m Model) renderSystemRow(info monitor.SystemInfo, width int) string {
400+
if width <= 0 {
401+
return ""
317402
}
318-
return m.styles.Footer.Width(width).Render(help)
319-
}
320403

321-
func (m Model) renderSummary(history monitor.MetricHistory, width int) string {
322-
parts := make([]string, 0, 4)
323-
if len(history.Load) > 0 {
324-
max := maxFloat(history.Load)
325-
if max < 1 {
326-
max = 1
327-
}
328-
parts = append(parts, fmt.Sprintf("LOAD %s %0.2f", sparkline(history.Load, 0, max), history.Load[len(history.Load)-1]))
404+
var parts []string
405+
if info.Disk != "" {
406+
parts = append(parts, info.Disk)
329407
}
330-
if len(history.CPU) > 0 {
331-
parts = append(parts, fmt.Sprintf("CPU %s %0.0f%%", sparkline(history.CPU, 0, 100), history.CPU[len(history.CPU)-1]))
332-
}
333-
if len(history.Mem) > 0 {
334-
parts = append(parts, fmt.Sprintf("MEM %s %0.0f%%", sparkline(history.Mem, 0, 100), history.Mem[len(history.Mem)-1]))
335-
}
336-
if len(history.Net) > 0 {
337-
max := maxFloat(history.Net)
338-
if max < 1 {
339-
max = 1
340-
}
341-
parts = append(parts, fmt.Sprintf("NET %s %s", sparkline(history.Net, 0, max), monitor.FormatRate(history.Net[len(history.Net)-1])))
408+
if info.Net != "" {
409+
parts = append(parts, info.Net)
342410
}
343-
row := strings.Join(parts, " | ")
344-
if row == "" {
345-
row = "METRICS unavailable (missing commands)"
411+
if info.Uptime != "" {
412+
parts = append(parts, info.Uptime)
346413
}
347-
return m.styles.Summary.Width(width).Render(row)
348-
}
349414

350-
func (m Model) renderInfoLine(text string, width int) string {
351-
if width <= 0 {
415+
if len(parts) == 0 {
352416
return ""
353417
}
354-
if strings.TrimSpace(text) == "" {
355-
text = " "
356-
}
357-
return m.styles.Info.Width(width).Render(text)
418+
419+
row := strings.Join(parts, " ")
420+
return m.styles.Info.Width(width).Render(row)
358421
}
359422

360423
func (m Model) renderContentTitle(title string, width int) string {
@@ -365,6 +428,16 @@ func (m Model) renderContentTitle(title string, width int) string {
365428
return m.styles.Summary.Width(width).Render(label)
366429
}
367430

431+
func (m Model) renderFooter(status, spinner string, width int) string {
432+
help := "q:quit tab/shift+tab:next/prev up/down/pgup/pgdn:scroll t:theme"
433+
if status != "" {
434+
help = spinner + " " + status + " | " + help
435+
} else if spinner != "" {
436+
help = spinner + " " + help
437+
}
438+
return m.styles.Footer.Width(width).Render(help)
439+
}
440+
368441
func sparkline(values []float64, min, max float64) string {
369442
if len(values) == 0 {
370443
return ""

0 commit comments

Comments
 (0)