Go 140 lines
package tui
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/ajhahnde/eeco/internal/config"
"github.com/ajhahnde/eeco/internal/hooks"
"github.com/ajhahnde/eeco/internal/queue"
)
// hooksDigest is the compact, live hook-wiring state ("pre-commit:on
// session:off"), recomputed per render so a toggle shows immediately.
func hooksDigest(cfg *config.Config) string {
return hooks.ShortState(cfg)
}
// memoryCount returns the number of fact files under <workspace>/memory,
// excluding the regenerated index and the attic. It is strictly
// read-only and never creates the directory: a missing store is zero.
func memoryCount(cfg *config.Config) int {
if cfg == nil {
return 0
}
ents, err := os.ReadDir(filepath.Join(cfg.Workspace, "memory"))
if err != nil {
return 0
}
n := 0
for _, e := range ents {
if e.IsDir() {
continue
}
name := e.Name()
if name == "MEMORY.md" || !strings.HasSuffix(name, ".md") {
continue
}
n++
}
return n
}
// queueCount returns the number of unresolved queue items. A missing
// queue file is reported as zero (queue.Count already handles this).
func queueCount(cfg *config.Config) int {
if cfg == nil {
return 0
}
n, err := queue.Count(filepath.Join(cfg.Workspace, "state"))
if err != nil {
return 0
}
return n
}
// gateText renders the resolved gate chain — its steps joined by
// " && " — or a neutral placeholder when the profile has none.
func gateText(cfg *config.Config) string {
if cfg == nil || len(cfg.Gate) == 0 {
return "(none)"
}
return strings.Join(config.GateSteps(cfg.Gate), " && ")
}
// OneScreen is the plain, non-interactive status digest. It is what
// `eeco` prints when stdout is not a terminal (piped or CI): one screen,
// exit 0, no interactive loop. It reads only; it changes nothing.
func OneScreen(cfg *config.Config, version string) string {
var b strings.Builder
fmt.Fprintf(&b, "eeco %s\n", version)
fmt.Fprintf(&b, " repo %s\n", cfg.RepoRoot)
fmt.Fprintf(&b, " profile %s\n", cfg.Profile)
fmt.Fprintf(&b, " gate %s\n", gateText(cfg))
fmt.Fprintf(&b, " automation %s\n", cfg.Automation)
fmt.Fprintf(&b, " memory %d fact(s)\n", memoryCount(cfg))
fmt.Fprintf(&b, " queue %d open\n", queueCount(cfg))
fmt.Fprintf(&b, " hooks %s\n", hooksDigest(cfg))
if config.IsInitialized(cfg) {
fmt.Fprintf(&b, " workspace %s/ (initialised)\n", cfg.WorkspaceName)
} else {
fmt.Fprintf(&b, " workspace %s/ (missing — run `eeco init`)\n", cfg.WorkspaceName)
}
if hint, ok := doctorHintLine(cfg); ok {
fmt.Fprintln(&b, hint)
}
return b.String()
}
// doctorHintLine returns the fresh-workspace nudge and true iff the
// workspace is initialised but has no observable activity yet — no
// memory facts, no queue items, no scaffolded user workflows. The
// hint suppresses itself as soon as any of those exist; no marker
// file is required.
func doctorHintLine(cfg *config.Config) (string, bool) {
if cfg == nil || !config.IsInitialized(cfg) {
return "", false
}
if memoryCount(cfg) > 0 || queueCount(cfg) > 0 {
return "", false
}
ents, _ := os.ReadDir(filepath.Join(cfg.Workspace, "workflows"))
for _, e := range ents {
if e.IsDir() {
return "", false
}
}
return " hint run `eeco doctor` for a workspace health check", true
}
// barLine is the compact, always-current digest rendered above the
// input line in the interactive control center: one line, dot-separated,
// recomputed on each render so counts stay live. lastRun is the headline
// of the most recent workflow run this session; it is omitted entirely
// until the first run lands so the bar carries no placeholder noise.
// The version string is intentionally elided — it already prints once
// in the home block on session start.
func barLine(cfg *config.Config, version, lastRun string) string {
_ = version
ws := "no workspace"
if config.IsInitialized(cfg) {
ws = cfg.WorkspaceName + "/"
}
fields := []string{
filepath.Base(cfg.RepoRoot),
string(cfg.Profile),
ws,
"gate:" + gateText(cfg),
"auto:" + string(cfg.Automation),
fmt.Sprintf("mem:%d", memoryCount(cfg)),
fmt.Sprintf("q:%d", queueCount(cfg)),
hooksDigest(cfg),
}
if lastRun != "" {
fields = append(fields, "run:"+lastRun)
}
return strings.Join(fields, " · ")
}