ajhahn.de
← eeco
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, " · ")
}