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 /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, " · ") }