203 lines
5.5 KiB
Go
203 lines
5.5 KiB
Go
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))
|
||
}
|