Download
138 lines 3.8 KiBGoModified 2026-10-31 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
package tui

import (
	"context"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"strconv"
	"strings"
	"time"

	"charm.land/lipgloss/v2"
	"github.com/charmbracelet/x/ansi"

	"blinker/internal/policy"
)

// wrap fits text to a width, breaking at spaces where it can.
func wrap(s string, width int) string {
	return ansi.Wrap(s, max(width, 10), "")
}

// styleLines styles each line by itself: styling a block pads every line
// to the widest one, which the terminal may then wrap.
func styleLines(style lipgloss.Style, s string) string {
	lines := strings.Split(s, "\n")
	for i, l := range lines {
		lines[i] = style.Render(l)
	}
	return strings.Join(lines, "\n")
}

// result shows a tool's output under its summary line.
func result(s string, style lipgloss.Style) string {
	lines := strings.Split(s, "\n")
	for i, l := range lines {
		prefix := "    "
		if i == 0 {
			prefix = "  ⎿ "
		}
		lines[i] = prefix + style.Render(l)
	}
	return strings.Join(lines, "\n")
}

// indent prefixes every line.
func indent(s, prefix string) string {
	return prefix + strings.ReplaceAll(s, "\n", "\n"+prefix)
}

// clipLines keeps the first n lines.
func clipLines(s string, n int) string {
	lines := strings.Split(s, "\n")
	if len(lines) <= n {
		return s
	}
	return strings.Join(lines[:n], "\n") + fmt.Sprintf("\n… %d more lines", len(lines)-n)
}

// tailLines keeps the last n lines.
func tailLines(s string, n int) string {
	lines := strings.Split(s, "\n")
	if len(lines) <= n {
		return s
	}
	return strings.Join(lines[len(lines)-n:], "\n")
}

// truncate cuts a line to a width.
func truncate(s string, width int) string {
	return ansi.Truncate(s, max(width, 1), "…")
}

func firstLine(s string) string {
	line, _, more := strings.Cut(strings.TrimSpace(s), "\n")
	if more {
		line += " …"
	}
	return line
}

// externalDiff renders a unified diff with the user's diff_command, argv,
// for a terminal width columns wide, keeping at most limit lines (0 for
// all). Only colors are kept from what it prints, and its lines are wrapped
// to the width, so nothing is cut off.
func externalDiff(argv []string, d string, limit, width int) (string, error) {
	w := strconv.Itoa(width)
	args := make([]string, len(argv)-1)
	for i, a := range argv[1:] {
		args[i] = strings.ReplaceAll(a, "{width}", w)
	}
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	cmd := exec.CommandContext(ctx, argv[0], args...)
	cmd.Stdin = strings.NewReader(d)
	// The user's program, but reading what the model wrote: it gets none
	// of what no sandboxed command may have, the API keys among them.
	for _, kv := range os.Environ() {
		if name, _, _ := strings.Cut(kv, "="); !policy.EnvFloored(name) {
			cmd.Env = append(cmd.Env, kv)
		}
	}
	cmd.Env = append(cmd.Env, "COLUMNS="+w)
	out, err := cmd.Output()
	// Exit status 1 means only that there are differences, as for diff(1)
	// and delta.
	var exit *exec.ExitError
	if errors.As(err, &exit) && exit.ExitCode() == 1 {
		err = nil
	}
	if err != nil {
		if exit != nil && len(exit.Stderr) > 0 {
			return "", fmt.Errorf("%s: %w: %s", argv[0], err, firstLine(string(exit.Stderr)))
		}
		return "", fmt.Errorf("%s: %w", argv[0], err)
	}
	s := strings.TrimRight(SanitizeColors(string(out)), "\n")
	if strings.TrimSpace(ansi.Strip(s)) == "" {
		return "", fmt.Errorf("%s printed nothing", argv[0])
	}
	lines := strings.Split(ansi.Hardwrap(s, max(width, 10), true), "\n")
	more := ""
	if limit > 0 && len(lines) > limit {
		more = "\n" + dimStyle.Render(fmt.Sprintf("    … %d more lines", len(lines)-limit))
		lines = lines[:limit]
	}
	// Reset the colors at the end, in case the output leaves one on.
	return strings.Join(lines, "\n") + "\x1b[0m" + more, nil
}

// menuRow is a line of a menu, marked if it is the one selected.
func menuRow(line string, selected bool) string {
	if selected {
		return selStyle.Render("› "+line) + "\n"
	}
	return "  " + line + "\n"
}