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"
}
|