Files
tia-love/internal/app/home.go
T

203 lines
5.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package app
import (
"fmt"
"runtime"
"strings"
"time"
"charm.land/lipgloss/v2"
"gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/i18n"
"gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/ui/components"
"gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/ui/theme"
)
// homeBody — dashboard beranda (F-027). Tata letak terinspirasi mirador:
// grid panel dengan judul di border; baris atas Jam · Docker · Host,
// baris bawah Kontainer · Aktivitas.
func (m Model) homeBody() string {
W := max(m.width, 60)
bodyH := m.bodyHeight()
row1H := 9
row2H := bodyH - row1H
if row2H < 5 {
row1H = 6
row2H = max(bodyH-row1H, 3)
}
w1 := int(float64(W) * 0.36)
w2 := int(float64(W) * 0.30)
w3 := W - w1 - w2
row1 := lipgloss.JoinHorizontal(lipgloss.Top,
m.clockPanel(w1, row1H),
m.dockerPanel(w2, row1H),
m.hostPanel(w3, row1H),
)
k1 := int(float64(W) * 0.55)
k2 := W - k1
row2 := lipgloss.JoinHorizontal(lipgloss.Top,
panel(i18n.T("section.containers"), i18n.Tf("fmt.countTotal", len(m.containers)),
m.miniContainerLines(row2H-2), k1, row2H, false),
panel(i18n.T("section.activity"), fmt.Sprintf("(%d)", len(m.activity)),
m.activityLines(k2-4, row2H-2), k2, row2H, false),
)
return row1 + "\n" + row2
}
func (m Model) clockPanel(w, h int) string {
now := time.Now()
rows := components.CenterBlock(components.RenderClock(now), w-4)
for i, r := range rows {
rows[i] = theme.Style(theme.Current().Accent).Bold(true).Render(r)
}
tanggal := fmt.Sprintf("%d %s %d", now.Day(), i18n.MonthName(now), now.Year())
lines := append(rows, "", dim(" "+i18n.WeekdayName(now)+", "+tanggal+" · "+now.Format("MST")))
if len(lines) > h-2 {
lines = lines[:h-2]
}
return panel(i18n.T("section.clock"), "", lines, w, h, false)
}
// engineTitle — judul panel runtime (Docker/Podman) sesuai hasil deteksi.
func (m Model) engineTitle() string {
if m.engineName == "podman" {
return "Podman"
}
return "Docker"
}
func (m Model) dockerPanel(w, h int) string {
var lines []string
switch {
case m.engineErr != nil:
lines = append(lines, red(i18n.T("error.fail")), "", dim(truncate(m.engineErr.Error(), w-6)))
case m.engine.ServerVersion == "":
lines = append(lines, dim(i18n.T("state.loading")))
default:
lines = append(lines,
fieldW(i18n.T("field.engine"), 9)+dim(m.engine.ServerVersion),
fieldW(i18n.T("field.api"), 9)+dim(m.engine.APIVersion),
fieldW(i18n.T("section.containers"), 9)+dim(i18n.Tf("fmt.dockerContainers", m.engine.Running, m.engine.Containers)),
fieldW(i18n.T("field.image"), 9)+dim(fmt.Sprintf("%d", m.engine.Images)),
"",
styled(theme.Current().Success, i18n.T("state.connected")),
)
}
return panel(m.engineTitle(), i18n.T("host.local"), lines, w, h, false)
}
func (m Model) hostPanel(w, h int) string {
s := m.sys
cpus := m.engine.CPUs
if cpus == 0 {
cpus = runtime.NumCPU()
}
pct := 0
if s.MemTotal > 0 {
pct = int((s.MemTotal - s.MemAvail) * 100 / s.MemTotal)
}
lines := []string{
fieldW(i18n.T("field.host"), 9) + dim(m.hostname),
fieldW(i18n.T("field.cpu"), 9) + dim(i18n.Tf("fmt.coreLoad", cpus, s.Load1)),
fieldW(i18n.T("field.ram"), 9) + dim(i18n.Tf("fmt.ramUsage", pct)) + pctBar(float64(pct), 10) +
dim(fmt.Sprintf(" %s/%s", humanKBs(s.MemTotal-s.MemAvail), humanKBs(s.MemTotal))),
fieldW(i18n.T("field.uptime"), 9) + dim(humanDuration(s.Uptime)),
}
return panel(i18n.T("section.host"), "", lines, w, h, false)
}
func (m Model) miniContainerLines(maxLines int) []string {
if m.loading && len(m.containers) == 0 {
return []string{dim(i18n.T("state.loading"))}
}
if len(m.containers) == 0 {
return []string{dim(i18n.T("hint.miniNoContainers"))}
}
max := maxLines
if len(m.containers) > max {
max--
}
lines := make([]string, 0, maxLines)
for i := 0; i < len(m.containers) && i < max; i++ {
c := m.containers[i]
glyph, color := stateStyle(c.State)
lines = append(lines, styled(color, glyph)+" "+text(truncate(c.Name, 24))+" "+dim(truncate(c.Image, 32)))
}
if extra := len(m.containers) - len(lines); extra > 0 {
lines = append(lines, dim(i18n.Tf("fmt.listMore", extra)))
}
return lines
}
// --- small formatting helpers ---
func humanKB(kb uint64) string {
f := float64(kb) * 1024
units := []string{"B", "KB", "MB", "GB", "TB"}
i := 0
for f >= 1024 && i < len(units)-1 {
f /= 1024
i++
}
if i <= 1 {
return fmt.Sprintf("%.0f %s", f, units[i])
}
return fmt.Sprintf("%.1f %s", f, units[i])
}
func humanKBs(kb uint64) string {
return strings.Replace(humanKB(kb), " ", "", 1)
}
// humanBytes — ukuran byte → "1.4 GB" (untuk image).
func humanBytes(n int64) string {
f := float64(n)
units := []string{"B", "KB", "MB", "GB", "TB"}
i := 0
for f >= 1024 && i < len(units)-1 {
f /= 1024
i++
}
if i <= 1 {
return fmt.Sprintf("%.0f %s", f, units[i])
}
return fmt.Sprintf("%.1f %s", f, units[i])
}
func humanDuration(sec float64) string {
d := time.Duration(sec) * time.Second
days := int(d.Hours()) / 24
hours := int(d.Hours()) % 24
mins := int(d.Minutes()) % 60
switch {
case days > 0:
return i18n.Tf("duration.days", days, hours)
case hours > 0:
return i18n.Tf("duration.hours", hours, mins)
default:
return i18n.Tf("duration.minutes", mins)
}
}
// pctBar — bar persentase; warna ikut beban (hijau <60% · kuning 60–85% · merah >85%).
func pctBar(pct float64, w int) string {
fill := int(pct * float64(w) / 100)
if fill > w {
fill = w
}
th := theme.Current()
color := th.Success
switch {
case pct >= 85:
color = th.Danger
case pct >= 60:
color = th.Warning
}
return styled(color, strings.Repeat("█", fill)) + dim(strings.Repeat("░", w-fill))
}