mirror of
https://github.com/theniceboy/.config.git
synced 2026-04-24 14:27:58 +08:00
808 lines
22 KiB
Go
808 lines
22 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"math"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
statusRightModuleCPU = "cpu"
|
|
statusRightModuleNetwork = "network"
|
|
statusRightModuleMemory = "memory"
|
|
statusRightModuleMemoryTotals = "memory_totals"
|
|
statusRightModuleAgent = "agent"
|
|
statusRightModuleTodoPreview = "todo_preview"
|
|
statusRightModuleTodos = "todos"
|
|
statusRightModuleFlashMoe = "flash_moe"
|
|
statusRightModuleHost = "host"
|
|
)
|
|
|
|
const (
|
|
statusIconCPU = ""
|
|
statusIconNetwork = ""
|
|
statusIconMemory = ""
|
|
statusIconWindow = ""
|
|
statusIconSession = ""
|
|
statusIconTotal = ""
|
|
statusIconAgent = ""
|
|
statusIconTodos = ""
|
|
statusIconFlashMoe = ""
|
|
)
|
|
|
|
func statusRightModules() []string {
|
|
return []string{
|
|
statusRightModuleCPU,
|
|
statusRightModuleNetwork,
|
|
statusRightModuleMemory,
|
|
statusRightModuleMemoryTotals,
|
|
statusRightModuleAgent,
|
|
statusRightModuleTodos,
|
|
statusRightModuleTodoPreview,
|
|
statusRightModuleFlashMoe,
|
|
statusRightModuleHost,
|
|
}
|
|
}
|
|
|
|
var cpuUsagePattern = regexp.MustCompile(`CPU usage:\s*([0-9.]+)% user,\s*([0-9.]+)% sys,`)
|
|
|
|
var statusCommandOutput = func(name string, args ...string) ([]byte, error) {
|
|
cmd := exec.Command(name, args...)
|
|
return cmd.Output()
|
|
}
|
|
|
|
var statusCommandStart = func(name string, args ...string) error {
|
|
cmd := exec.Command(name, args...)
|
|
if err := cmd.Start(); err != nil {
|
|
return err
|
|
}
|
|
if cmd.Process != nil {
|
|
_ = cmd.Process.Release()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var statusNow = time.Now
|
|
var statusHostname = os.Hostname
|
|
var statusDetectCurrentAgentFromTmux = detectCurrentAgentFromTmux
|
|
var statusLoadRegistry = loadRegistry
|
|
var statusMemoryCachePath = func() string { return "/tmp/tmux-mem-usage.json" }
|
|
var statusMemoryCacheRefreshScript = func() string {
|
|
return filepath.Join(os.Getenv("HOME"), ".config", "tmux", "tmux-status", "mem_usage_cache.py")
|
|
}
|
|
var statusTodoFilePath = func() string {
|
|
return filepath.Join(os.Getenv("HOME"), ".cache", "agent", "todos.json")
|
|
}
|
|
var statusFlashMoeMetricsPath = func() string {
|
|
return filepath.Join(os.Getenv("HOME"), ".flash-moe", "tmux_metrics")
|
|
}
|
|
var statusNetworkRateCachePath = func() string {
|
|
return "/tmp/agent-tmux-network-rate.json"
|
|
}
|
|
|
|
type tmuxRightStatusArgs struct {
|
|
Width int
|
|
StatusBG string
|
|
SessionName string
|
|
WindowIndex string
|
|
PaneID string
|
|
WindowID string
|
|
}
|
|
|
|
type statusSegment struct {
|
|
FG string
|
|
BG string
|
|
Text string
|
|
Bold bool
|
|
}
|
|
|
|
type statusMemoryCache struct {
|
|
Pane map[string]string `json:"pane"`
|
|
Window map[string]string `json:"window"`
|
|
Session map[string]string `json:"session"`
|
|
Total string `json:"total"`
|
|
}
|
|
|
|
type statusTodoCache struct {
|
|
Global []statusTodoItem `json:"global"`
|
|
Sessions map[string][]statusTodoItem `json:"sessions"`
|
|
Windows map[string][]statusTodoItem `json:"windows"`
|
|
}
|
|
|
|
type statusTodoItem struct {
|
|
Title string `json:"title"`
|
|
Done bool `json:"done"`
|
|
}
|
|
|
|
type statusNetworkCounter struct {
|
|
InBytes uint64
|
|
OutBytes uint64
|
|
}
|
|
|
|
type statusNetworkRateCache struct {
|
|
Interface string `json:"interface"`
|
|
InBytes uint64 `json:"in_bytes"`
|
|
OutBytes uint64 `json:"out_bytes"`
|
|
SampledAt int64 `json:"sampled_at_unix_ms"`
|
|
}
|
|
|
|
func runTmuxRightStatus(args []string) error {
|
|
fs := flag.NewFlagSet("agent tmux right-status", flag.ContinueOnError)
|
|
fs.SetOutput(os.Stderr)
|
|
if err := fs.Parse(args); err != nil {
|
|
return err
|
|
}
|
|
values := fs.Args()
|
|
parsed := tmuxRightStatusArgs{}
|
|
if len(values) > 0 {
|
|
parsed.Width, _ = strconv.Atoi(strings.TrimSpace(values[0]))
|
|
}
|
|
if len(values) > 1 {
|
|
parsed.StatusBG = strings.TrimSpace(values[1])
|
|
}
|
|
if len(values) > 2 {
|
|
parsed.SessionName = strings.TrimSpace(values[2])
|
|
}
|
|
if len(values) > 3 {
|
|
parsed.WindowIndex = strings.TrimSpace(values[3])
|
|
}
|
|
if len(values) > 4 {
|
|
parsed.PaneID = strings.TrimSpace(values[4])
|
|
}
|
|
if len(values) > 5 {
|
|
parsed.WindowID = strings.TrimSpace(values[5])
|
|
}
|
|
if parsed.StatusBG == "" || parsed.StatusBG == "default" {
|
|
parsed.StatusBG = "black"
|
|
}
|
|
if parsed.Width > 0 && parsed.Width < statusRightMinimumWidth() {
|
|
return nil
|
|
}
|
|
fmt.Print(renderTmuxRightStatus(parsed))
|
|
return nil
|
|
}
|
|
|
|
func renderTmuxRightStatus(args tmuxRightStatusArgs) string {
|
|
segments := make([]statusSegment, 0, 6)
|
|
if statusRightModuleEnabled(statusRightModuleCPU) {
|
|
if label := loadCPUStatusLabel(); label != "" {
|
|
segments = append(segments, statusSegment{FG: "#1d1f21", BG: "#d08770", Text: label, Bold: true})
|
|
}
|
|
}
|
|
if statusRightModuleEnabled(statusRightModuleNetwork) {
|
|
if label := loadNetworkStatusLabel(); label != "" {
|
|
segments = append(segments, statusSegment{FG: "#1d1f21", BG: "#8fbcbb", Text: label, Bold: true})
|
|
}
|
|
}
|
|
if statusRightModuleEnabled(statusRightModuleMemory) {
|
|
if label := loadMemoryStatusLabel(args.PaneID); label != "" {
|
|
segments = append(segments, statusSegment{FG: "#eceff4", BG: "#5e81ac", Text: label})
|
|
}
|
|
}
|
|
if statusRightModuleEnabled(statusRightModuleMemoryTotals) {
|
|
segments = append(segments, loadMemoryTotalsStatusSegments(args)...)
|
|
}
|
|
if statusRightModuleEnabled(statusRightModuleAgent) {
|
|
if label := loadAgentStatusLabel(args.WindowID); label != "" {
|
|
segments = append(segments, statusSegment{FG: "#1d1f21", BG: "#81a1c1", Text: label, Bold: true})
|
|
}
|
|
}
|
|
if statusRightModuleEnabled(statusRightModuleTodos) {
|
|
if label := loadTodosStatusLabel(args.WindowID); label != "" {
|
|
segments = append(segments, statusSegment{FG: "#1d1f21", BG: "#cc6666", Text: label, Bold: true})
|
|
}
|
|
}
|
|
if statusRightModuleEnabled(statusRightModuleFlashMoe) {
|
|
if segment, ok := loadFlashMoeStatusSegment(); ok {
|
|
segments = append(segments, segment)
|
|
}
|
|
}
|
|
if statusRightModuleEnabled(statusRightModuleHost) {
|
|
if label := loadHostStatusLabel(); label != "" {
|
|
segments = append(segments, statusSegment{FG: "#1d1f21", BG: statusThemeColor(), Text: label})
|
|
}
|
|
}
|
|
return formatRightStatusSegments(args.StatusBG, segments)
|
|
}
|
|
|
|
func formatRightStatusSegments(statusBG string, segments []statusSegment) string {
|
|
if len(segments) == 0 {
|
|
return ""
|
|
}
|
|
separator := ""
|
|
rightCap := "█"
|
|
prevBG := statusBG
|
|
var builder strings.Builder
|
|
for _, segment := range segments {
|
|
builder.WriteString(fmt.Sprintf("#[fg=%s,bg=%s]%s#[fg=%s,bg=%s", segment.BG, prevBG, separator, segment.FG, segment.BG))
|
|
if segment.Bold {
|
|
builder.WriteString(",bold")
|
|
}
|
|
builder.WriteString("]")
|
|
builder.WriteString(segment.Text)
|
|
prevBG = segment.BG
|
|
}
|
|
builder.WriteString(fmt.Sprintf(" #[fg=%s,bg=%s]%s", prevBG, statusBG, rightCap))
|
|
return builder.String()
|
|
}
|
|
|
|
func statusRightMinimumWidth() int {
|
|
value := strings.TrimSpace(os.Getenv("TMUX_RIGHT_MIN_WIDTH"))
|
|
if value == "" {
|
|
return 90
|
|
}
|
|
parsed, err := strconv.Atoi(value)
|
|
if err != nil || parsed < 1 {
|
|
return 90
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
func statusThemeColor() string {
|
|
value := strings.TrimSpace(os.Getenv("TMUX_THEME_COLOR"))
|
|
if value == "" {
|
|
return "#b294bb"
|
|
}
|
|
return value
|
|
}
|
|
|
|
func loadCPUStatusLabel() string {
|
|
output, err := statusCommandOutput("top", "-l", "1", "-n", "0")
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
total, ok := parseCPUUsageTotal(string(output))
|
|
if !ok {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf(" %s %s ", statusIconCPU, formatUsagePercent(total))
|
|
}
|
|
|
|
func parseCPUUsageTotal(output string) (float64, bool) {
|
|
matches := cpuUsagePattern.FindStringSubmatch(output)
|
|
if len(matches) != 3 {
|
|
return 0, false
|
|
}
|
|
user, err := strconv.ParseFloat(matches[1], 64)
|
|
if err != nil {
|
|
return 0, false
|
|
}
|
|
system, err := strconv.ParseFloat(matches[2], 64)
|
|
if err != nil {
|
|
return 0, false
|
|
}
|
|
total := math.Max(0, user+system)
|
|
if total > 100 {
|
|
total = 100
|
|
}
|
|
return total, true
|
|
}
|
|
|
|
func formatUsagePercent(value float64) string {
|
|
if value < 10 && math.Abs(value-math.Round(value)) > 0.05 {
|
|
return fmt.Sprintf("%.1f%%", value)
|
|
}
|
|
return fmt.Sprintf("%.0f%%", value)
|
|
}
|
|
|
|
func loadNetworkStatusLabel() string {
|
|
preferred := loadPrimaryNetworkInterface()
|
|
output, err := statusCommandOutput("netstat", "-ibn")
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
counters := parseNetworkCounters(string(output))
|
|
iface, current, ok := pickNetworkCounter(counters, preferred)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
now := statusNow().UnixMilli()
|
|
previous, _ := loadNetworkRateCache()
|
|
rate := " ↓-- ↑-- "
|
|
if previous.Interface == iface && previous.SampledAt > 0 && now > previous.SampledAt && current.InBytes >= previous.InBytes && current.OutBytes >= previous.OutBytes {
|
|
seconds := float64(now-previous.SampledAt) / 1000
|
|
if seconds >= 0.25 {
|
|
down := float64(current.InBytes-previous.InBytes) / seconds
|
|
up := float64(current.OutBytes-previous.OutBytes) / seconds
|
|
rate = fmt.Sprintf(" %s ↓%s ↑%s ", statusIconNetwork, formatByteRate(down), formatByteRate(up))
|
|
}
|
|
}
|
|
_ = saveNetworkRateCache(statusNetworkRateCache{
|
|
Interface: iface,
|
|
InBytes: current.InBytes,
|
|
OutBytes: current.OutBytes,
|
|
SampledAt: now,
|
|
})
|
|
if rate == " ↓-- ↑-- " {
|
|
return fmt.Sprintf(" %s ↓-- ↑-- ", statusIconNetwork)
|
|
}
|
|
return rate
|
|
}
|
|
|
|
func loadPrimaryNetworkInterface() string {
|
|
output, err := statusCommandOutput("route", "-n", "get", "default")
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
scanner := bufio.NewScanner(strings.NewReader(string(output)))
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if !strings.HasPrefix(line, "interface:") {
|
|
continue
|
|
}
|
|
return strings.TrimSpace(strings.TrimPrefix(line, "interface:"))
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func parseNetworkCounters(output string) map[string]statusNetworkCounter {
|
|
counters := make(map[string]statusNetworkCounter)
|
|
scanner := bufio.NewScanner(strings.NewReader(output))
|
|
for scanner.Scan() {
|
|
fields := strings.Fields(scanner.Text())
|
|
if len(fields) < 10 {
|
|
continue
|
|
}
|
|
name := strings.TrimSuffix(strings.TrimSpace(fields[0]), "*")
|
|
if name == "" || strings.EqualFold(name, "Name") {
|
|
continue
|
|
}
|
|
if _, exists := counters[name]; exists {
|
|
continue
|
|
}
|
|
inBytes, err := strconv.ParseUint(fields[6], 10, 64)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
outBytes, err := strconv.ParseUint(fields[9], 10, 64)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
counters[name] = statusNetworkCounter{InBytes: inBytes, OutBytes: outBytes}
|
|
}
|
|
return counters
|
|
}
|
|
|
|
func pickNetworkCounter(counters map[string]statusNetworkCounter, preferred string) (string, statusNetworkCounter, bool) {
|
|
preferred = strings.TrimSpace(preferred)
|
|
if preferred != "" {
|
|
if counter, ok := counters[preferred]; ok {
|
|
return preferred, counter, true
|
|
}
|
|
}
|
|
var bestName string
|
|
var bestCounter statusNetworkCounter
|
|
var bestTotal uint64
|
|
for name, counter := range counters {
|
|
if ignoreNetworkInterface(name) {
|
|
continue
|
|
}
|
|
total := counter.InBytes + counter.OutBytes
|
|
if total <= bestTotal {
|
|
continue
|
|
}
|
|
bestName = name
|
|
bestCounter = counter
|
|
bestTotal = total
|
|
}
|
|
if bestName == "" {
|
|
return "", statusNetworkCounter{}, false
|
|
}
|
|
return bestName, bestCounter, true
|
|
}
|
|
|
|
func ignoreNetworkInterface(name string) bool {
|
|
for _, prefix := range []string{"lo", "awdl", "llw", "gif", "stf", "anpi", "ap", "bridge", "pktap"} {
|
|
if strings.HasPrefix(name, prefix) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func formatByteRate(bytesPerSecond float64) string {
|
|
if bytesPerSecond < 0 {
|
|
bytesPerSecond = 0
|
|
}
|
|
units := []string{"B", "K", "M", "G"}
|
|
value := bytesPerSecond
|
|
unit := units[0]
|
|
for _, candidate := range units[1:] {
|
|
if value < 1024 {
|
|
break
|
|
}
|
|
value /= 1024
|
|
unit = candidate
|
|
}
|
|
if value < 10 && unit != "B" {
|
|
return fmt.Sprintf("%.1f%s", value, unit)
|
|
}
|
|
return fmt.Sprintf("%.0f%s", value, unit)
|
|
}
|
|
|
|
func loadNetworkRateCache() (statusNetworkRateCache, error) {
|
|
data, err := os.ReadFile(statusNetworkRateCachePath())
|
|
if err != nil {
|
|
return statusNetworkRateCache{}, err
|
|
}
|
|
var cache statusNetworkRateCache
|
|
if err := json.Unmarshal(data, &cache); err != nil {
|
|
return statusNetworkRateCache{}, err
|
|
}
|
|
return cache, nil
|
|
}
|
|
|
|
func saveNetworkRateCache(cache statusNetworkRateCache) error {
|
|
data, err := json.Marshal(cache)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
path := statusNetworkRateCachePath()
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
return err
|
|
}
|
|
tmpPath := path + ".tmp"
|
|
if err := os.WriteFile(tmpPath, data, 0o644); err != nil {
|
|
return err
|
|
}
|
|
return os.Rename(tmpPath, path)
|
|
}
|
|
|
|
func loadMemoryStatusLabel(paneID string) string {
|
|
paneID = strings.TrimSpace(paneID)
|
|
if paneID == "" {
|
|
return ""
|
|
}
|
|
cache, ok := loadMemoryStatusCache()
|
|
if !ok {
|
|
return ""
|
|
}
|
|
value := strings.TrimSpace(cache.Pane[paneID])
|
|
if value == "" {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf(" %s %s ", statusIconMemory, value)
|
|
}
|
|
|
|
func loadMemoryTotalsStatusSegments(args tmuxRightStatusArgs) []statusSegment {
|
|
cache, ok := loadMemoryStatusCache()
|
|
if !ok {
|
|
return nil
|
|
}
|
|
segments := make([]statusSegment, 0, 3)
|
|
windowKey := strings.TrimSpace(args.SessionName)
|
|
if windowKey != "" && strings.TrimSpace(args.WindowIndex) != "" {
|
|
windowKey = windowKey + ":" + strings.TrimSpace(args.WindowIndex)
|
|
}
|
|
if value := strings.TrimSpace(cache.Window[windowKey]); value != "" {
|
|
segments = append(segments, statusSegment{FG: "#eceff4", BG: "#4c566a", Text: fmt.Sprintf(" %s %s ", statusIconWindow, value)})
|
|
}
|
|
if value := strings.TrimSpace(cache.Session[strings.TrimSpace(args.SessionName)]); value != "" {
|
|
segments = append(segments, statusSegment{FG: "#eceff4", BG: "#434c5e", Text: fmt.Sprintf(" %s %s ", statusIconSession, value)})
|
|
}
|
|
if value := strings.TrimSpace(cache.Total); value != "" {
|
|
segments = append(segments, statusSegment{FG: "#eceff4", BG: "#3b4252", Text: fmt.Sprintf(" %s %s ", statusIconTotal, value)})
|
|
}
|
|
return segments
|
|
}
|
|
|
|
func loadMemoryStatusCache() (statusMemoryCache, bool) {
|
|
refreshMemoryUsageCache()
|
|
data, err := os.ReadFile(statusMemoryCachePath())
|
|
if err != nil {
|
|
return statusMemoryCache{}, false
|
|
}
|
|
var cache statusMemoryCache
|
|
if err := json.Unmarshal(data, &cache); err != nil {
|
|
return statusMemoryCache{}, false
|
|
}
|
|
return cache, true
|
|
}
|
|
|
|
func loadAgentStatusLabel(windowID string) string {
|
|
windowID = strings.TrimSpace(windowID)
|
|
if windowID == "" {
|
|
return ""
|
|
}
|
|
ref, err := statusDetectCurrentAgentFromTmux(windowID)
|
|
if err != nil || strings.TrimSpace(ref.ID) == "" {
|
|
return ""
|
|
}
|
|
reg, err := statusLoadRegistry()
|
|
if err != nil || reg == nil {
|
|
return ""
|
|
}
|
|
record := reg.Agents[strings.TrimSpace(ref.ID)]
|
|
if record == nil {
|
|
return ""
|
|
}
|
|
device := strings.TrimSpace(record.Device)
|
|
if device == "" {
|
|
device = "no device"
|
|
}
|
|
return fmt.Sprintf(" %s %s ", statusIconAgent, device)
|
|
}
|
|
|
|
func refreshMemoryUsageCache() {
|
|
script := statusMemoryCacheRefreshScript()
|
|
if strings.TrimSpace(script) == "" || !fileExists(script) {
|
|
return
|
|
}
|
|
_ = statusCommandStart("python3", script)
|
|
}
|
|
|
|
func loadTodosStatusLabel(windowID string) string {
|
|
items, ok := statusTodoItemsForWindow(windowID)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
count := 0
|
|
for _, item := range items {
|
|
if !item.Done {
|
|
count++
|
|
}
|
|
}
|
|
if count == 0 {
|
|
return ""
|
|
}
|
|
if !statusRightModuleEnabled(statusRightModuleTodoPreview) {
|
|
return fmt.Sprintf(" %s %d ", statusIconTodos, count)
|
|
}
|
|
title := firstOpenStatusTodoTitle(items)
|
|
if title != "" {
|
|
return fmt.Sprintf(" %s %d %s ", statusIconTodos, count, truncate(title, statusTodoMaxChars()))
|
|
}
|
|
return fmt.Sprintf(" %s %d ", statusIconTodos, count)
|
|
}
|
|
|
|
func loadStatusTodoCache() (statusTodoCache, bool) {
|
|
data, err := os.ReadFile(statusTodoFilePath())
|
|
if err != nil {
|
|
return statusTodoCache{}, false
|
|
}
|
|
var cache statusTodoCache
|
|
if err := json.Unmarshal(data, &cache); err != nil {
|
|
return statusTodoCache{}, false
|
|
}
|
|
if cache.Global == nil {
|
|
cache.Global = []statusTodoItem{}
|
|
}
|
|
if cache.Sessions == nil {
|
|
cache.Sessions = map[string][]statusTodoItem{}
|
|
}
|
|
if cache.Windows == nil {
|
|
cache.Windows = map[string][]statusTodoItem{}
|
|
}
|
|
return cache, true
|
|
}
|
|
|
|
func statusTodoItemsForWindow(windowID string) ([]statusTodoItem, bool) {
|
|
windowID = strings.TrimSpace(windowID)
|
|
if windowID == "" {
|
|
return nil, false
|
|
}
|
|
cache, ok := loadStatusTodoCache()
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
return cache.Windows[windowID], true
|
|
}
|
|
|
|
func firstOpenStatusTodoTitle(items []statusTodoItem) string {
|
|
for _, item := range items {
|
|
if item.Done {
|
|
continue
|
|
}
|
|
title := strings.Join(strings.Fields(strings.TrimSpace(item.Title)), " ")
|
|
if title != "" {
|
|
return title
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func statusTodoMaxChars() int {
|
|
value := strings.TrimSpace(os.Getenv("TMUX_TODO_MAX_CHARS"))
|
|
if value == "" {
|
|
return 32
|
|
}
|
|
parsed, err := strconv.Atoi(value)
|
|
if err != nil || parsed < 8 {
|
|
return 32
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
func loadFlashMoeStatusSegment() (statusSegment, bool) {
|
|
metricsPath := statusFlashMoeMetricsPath()
|
|
if strings.TrimSpace(metricsPath) == "" {
|
|
return statusSegment{}, false
|
|
}
|
|
data, err := os.ReadFile(metricsPath)
|
|
if err != nil {
|
|
return statusSegment{}, false
|
|
}
|
|
values := map[string]string{}
|
|
scanner := bufio.NewScanner(strings.NewReader(string(data)))
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if line == "" {
|
|
continue
|
|
}
|
|
key, value, ok := strings.Cut(line, "=")
|
|
if !ok {
|
|
continue
|
|
}
|
|
values[strings.TrimSpace(key)] = strings.TrimSpace(value)
|
|
}
|
|
phase := values["phase"]
|
|
if updated := strings.TrimSpace(values["updated_ms"]); updated != "" {
|
|
if updatedMS, err := strconv.ParseInt(updated, 10, 64); err == nil {
|
|
ageMS := statusNow().UnixMilli() - updatedMS
|
|
if ageMS > 10000 && (phase == "gen" || phase == "prefill") {
|
|
phase = "idle"
|
|
}
|
|
}
|
|
}
|
|
switch phase {
|
|
case "prefill":
|
|
promptTokens := strings.TrimSpace(values["prompt_tokens"])
|
|
label := fmt.Sprintf(" %s prefill ", statusIconFlashMoe)
|
|
if promptTokens != "" && promptTokens != "0" {
|
|
label = fmt.Sprintf(" %s prefill:%s ", statusIconFlashMoe, promptTokens)
|
|
}
|
|
return statusSegment{FG: "#1d1f21", BG: "#ebcb8b", Text: label, Bold: true}, true
|
|
case "gen":
|
|
tokS := strings.TrimSpace(values["tok_s"])
|
|
label := fmt.Sprintf(" %s gen ", statusIconFlashMoe)
|
|
if tokS != "" && tokS != "0.00" {
|
|
label = fmt.Sprintf(" %s %s tok/s ", statusIconFlashMoe, tokS)
|
|
}
|
|
return statusSegment{FG: "#1d1f21", BG: "#a3be8c", Text: label, Bold: true}, true
|
|
default:
|
|
return statusSegment{}, false
|
|
}
|
|
}
|
|
|
|
func loadHostStatusLabel() string {
|
|
host, err := statusHostname()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
host = strings.TrimSpace(host)
|
|
if host == "" {
|
|
return ""
|
|
}
|
|
if short, _, ok := strings.Cut(host, "."); ok {
|
|
host = short
|
|
}
|
|
return fmt.Sprintf(" %s", host)
|
|
}
|
|
|
|
func defaultStatusRightModuleEnabled(module string) bool {
|
|
switch module {
|
|
case statusRightModuleCPU, statusRightModuleNetwork, statusRightModuleMemory, statusRightModuleAgent, statusRightModuleTodoPreview, statusRightModuleTodos, statusRightModuleFlashMoe, statusRightModuleHost:
|
|
return true
|
|
case statusRightModuleMemoryTotals:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func isValidStatusRightModule(module string) bool {
|
|
switch module {
|
|
case statusRightModuleCPU, statusRightModuleNetwork, statusRightModuleMemory, statusRightModuleMemoryTotals, statusRightModuleAgent, statusRightModuleTodoPreview, statusRightModuleTodos, statusRightModuleFlashMoe, statusRightModuleHost:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func statusRightModuleEnabled(module string) bool {
|
|
if !isValidStatusRightModule(module) {
|
|
return false
|
|
}
|
|
cfg := loadAppConfig()
|
|
if cfg.StatusRight == nil {
|
|
return defaultStatusRightModuleEnabled(module)
|
|
}
|
|
return cfg.StatusRight.moduleEnabled(module)
|
|
}
|
|
|
|
func (cfg statusRightConfig) moduleEnabled(module string) bool {
|
|
switch module {
|
|
case statusRightModuleCPU:
|
|
return derefBool(cfg.CPU, defaultStatusRightModuleEnabled(module))
|
|
case statusRightModuleNetwork:
|
|
return derefBool(cfg.Network, defaultStatusRightModuleEnabled(module))
|
|
case statusRightModuleMemory:
|
|
return derefBool(cfg.Memory, defaultStatusRightModuleEnabled(module))
|
|
case statusRightModuleMemoryTotals:
|
|
return derefBool(cfg.MemoryTotals, defaultStatusRightModuleEnabled(module))
|
|
case statusRightModuleAgent:
|
|
return derefBool(cfg.Agent, defaultStatusRightModuleEnabled(module))
|
|
case statusRightModuleTodoPreview:
|
|
return derefBool(cfg.TodoPreview, defaultStatusRightModuleEnabled(module))
|
|
case statusRightModuleTodos:
|
|
return derefBool(cfg.Todos, defaultStatusRightModuleEnabled(module))
|
|
case statusRightModuleFlashMoe:
|
|
return derefBool(cfg.FlashMoe, defaultStatusRightModuleEnabled(module))
|
|
case statusRightModuleHost:
|
|
return derefBool(cfg.Host, defaultStatusRightModuleEnabled(module))
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func toggleStatusRightModule(module string) error {
|
|
if !isValidStatusRightModule(module) {
|
|
return fmt.Errorf("unknown status-right module: %s", module)
|
|
}
|
|
enabled := !statusRightModuleEnabled(module)
|
|
return updateAppConfig(func(cfg *appConfig) {
|
|
if cfg.StatusRight == nil {
|
|
cfg.StatusRight = &statusRightConfig{}
|
|
}
|
|
cfg.StatusRight.setModuleEnabled(module, enabled)
|
|
if cfg.StatusRight.isDefault() {
|
|
cfg.StatusRight = nil
|
|
}
|
|
})
|
|
}
|
|
|
|
func (cfg *statusRightConfig) setModuleEnabled(module string, enabled bool) {
|
|
value := boolPtr(enabled)
|
|
if enabled == defaultStatusRightModuleEnabled(module) {
|
|
value = nil
|
|
}
|
|
switch module {
|
|
case statusRightModuleCPU:
|
|
cfg.CPU = value
|
|
case statusRightModuleNetwork:
|
|
cfg.Network = value
|
|
case statusRightModuleMemory:
|
|
cfg.Memory = value
|
|
case statusRightModuleMemoryTotals:
|
|
cfg.MemoryTotals = value
|
|
case statusRightModuleAgent:
|
|
cfg.Agent = value
|
|
case statusRightModuleTodoPreview:
|
|
cfg.TodoPreview = value
|
|
case statusRightModuleTodos:
|
|
cfg.Todos = value
|
|
case statusRightModuleFlashMoe:
|
|
cfg.FlashMoe = value
|
|
case statusRightModuleHost:
|
|
cfg.Host = value
|
|
}
|
|
}
|
|
|
|
func (cfg *statusRightConfig) isDefault() bool {
|
|
if cfg == nil {
|
|
return true
|
|
}
|
|
return cfg.CPU == nil && cfg.Network == nil && cfg.Memory == nil && cfg.MemoryTotals == nil && cfg.Agent == nil && cfg.TodoPreview == nil && cfg.Todos == nil && cfg.FlashMoe == nil && cfg.Host == nil
|
|
}
|
|
|
|
func derefBool(value *bool, fallback bool) bool {
|
|
if value == nil {
|
|
return fallback
|
|
}
|
|
return *value
|
|
}
|
|
|
|
func boolPtr(value bool) *bool {
|
|
ptr := new(bool)
|
|
*ptr = value
|
|
return ptr
|
|
}
|