95 lines
2.5 KiB
Go
95 lines
2.5 KiB
Go
// Copyright (c) 2017-2026 Lethean (https://lt.hn)
|
|
//
|
|
// Licensed under the European Union Public Licence (EUPL) version 1.2.
|
|
// SPDX-License-Identifier: EUPL-1.2
|
|
|
|
package tui
|
|
|
|
import (
|
|
"time"
|
|
|
|
"dappco.re/go/core"
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
|
|
cli "dappco.re/go/core/cli/pkg/cli"
|
|
)
|
|
|
|
// Compile-time check: StatusModel implements cli.FrameModel.
|
|
var _ cli.FrameModel = (*StatusModel)(nil)
|
|
|
|
// StatusModel displays chain sync status in the header region.
|
|
type StatusModel struct {
|
|
node *Node
|
|
status NodeStatusMsg
|
|
}
|
|
|
|
// NewStatusModel creates a StatusModel backed by the given Node.
|
|
func NewStatusModel(n *Node) *StatusModel {
|
|
return &StatusModel{node: n}
|
|
}
|
|
|
|
// Init returns a command that reads the current chain status immediately.
|
|
func (m *StatusModel) Init() tea.Cmd {
|
|
return m.node.WaitForStatus()
|
|
}
|
|
|
|
// Update handles incoming messages. On NodeStatusMsg it stores the snapshot
|
|
// and schedules the next tick; all other messages are ignored.
|
|
func (m *StatusModel) Update(msg tea.Msg) (cli.FrameModel, tea.Cmd) {
|
|
switch msg := msg.(type) {
|
|
case NodeStatusMsg:
|
|
m.status = msg
|
|
return m, m.node.Tick()
|
|
default:
|
|
return m, nil
|
|
}
|
|
}
|
|
|
|
// View renders a single-line status bar. When height is zero the model has
|
|
// not yet received a status snapshot, so it shows a placeholder.
|
|
func (m *StatusModel) View(width, height int) string {
|
|
var line string
|
|
if height == 0 {
|
|
line = " height 0 | syncing..."
|
|
} else {
|
|
s := m.status
|
|
line = core.Sprintf(" height %d | sync %.1f%% | diff %s | %d peers | tip %s",
|
|
s.Height, s.SyncPct, formatDifficulty(s.Difficulty), s.PeerCount, formatAge(s.TipTime))
|
|
}
|
|
if len(line) > width && width > 0 {
|
|
line = line[:width]
|
|
}
|
|
return line
|
|
}
|
|
|
|
// formatAge returns a human-readable duration since t.
|
|
func formatAge(t time.Time) string {
|
|
if t.IsZero() {
|
|
return "\u2014"
|
|
}
|
|
d := time.Since(t)
|
|
switch {
|
|
case d < time.Minute:
|
|
return core.Sprintf("%ds ago", int(d.Seconds()))
|
|
case d < time.Hour:
|
|
return core.Sprintf("%dm ago", int(d.Minutes()))
|
|
case d < 24*time.Hour:
|
|
return core.Sprintf("%dh ago", int(d.Hours()))
|
|
default:
|
|
return core.Sprintf("%dd ago", int(d.Hours()/24))
|
|
}
|
|
}
|
|
|
|
// formatDifficulty returns a compact difficulty string with SI suffix.
|
|
func formatDifficulty(d uint64) string {
|
|
switch {
|
|
case d >= 1_000_000_000:
|
|
return core.Sprintf("%.1fG", float64(d)/1_000_000_000)
|
|
case d >= 1_000_000:
|
|
return core.Sprintf("%.1fM", float64(d)/1_000_000)
|
|
case d >= 1_000:
|
|
return core.Sprintf("%.1fK", float64(d)/1_000)
|
|
default:
|
|
return core.Sprintf("%d", d)
|
|
}
|
|
}
|