Initial commit

This commit is contained in:
Mester Gábor
2026-03-19 07:12:03 +01:00
commit 2dd6519168
25 changed files with 3645 additions and 0 deletions
+176
View File
@@ -0,0 +1,176 @@
package tui
import (
"fmt"
"os"
"path/filepath"
"strings"
"greq/internal/storage"
"github.com/charmbracelet/lipgloss"
)
type diffKind int
const (
diffEqual diffKind = iota
diffAdded // present in new, absent in old (+)
diffRemoved // present in old, absent in new (-)
)
type diffLine struct {
kind diffKind
content string
}
// computeDiff performs a line-level LCS diff between oldStr and newStr.
// Lines are capped at 500 each to keep the algorithm O(n²) bounded.
func computeDiff(oldStr, newStr string) []diffLine {
a := strings.Split(oldStr, "\n")
b := strings.Split(newStr, "\n")
if len(a) > 500 {
a = a[:500]
}
if len(b) > 500 {
b = b[:500]
}
return lcsBacktrack(a, b, lcsTable(a, b))
}
func lcsTable(a, b []string) [][]int {
m, n := len(a), len(b)
dp := make([][]int, m+1)
for i := range dp {
dp[i] = make([]int, n+1)
}
for i := 1; i <= m; i++ {
for j := 1; j <= n; j++ {
if a[i-1] == b[j-1] {
dp[i][j] = dp[i-1][j-1] + 1
} else if dp[i-1][j] >= dp[i][j-1] {
dp[i][j] = dp[i-1][j]
} else {
dp[i][j] = dp[i][j-1]
}
}
}
return dp
}
func lcsBacktrack(a, b []string, dp [][]int) []diffLine {
var result []diffLine
i, j := len(a), len(b)
for i > 0 || j > 0 {
switch {
case i > 0 && j > 0 && a[i-1] == b[j-1]:
result = append(result, diffLine{diffEqual, a[i-1]})
i--
j--
case j > 0 && (i == 0 || dp[i][j-1] >= dp[i-1][j]):
result = append(result, diffLine{diffAdded, b[j-1]})
j--
default:
result = append(result, diffLine{diffRemoved, a[i-1]})
i--
}
}
// Reverse
for l, r := 0, len(result)-1; l < r; l, r = l+1, r-1 {
result[l], result[r] = result[r], result[l]
}
return result
}
// renderDiff formats diff lines with ANSI colours for the viewport.
// Equal lines are dimmed; added lines are green (+); removed lines are red (-).
func renderDiff(lines []diffLine, width int) string {
if len(lines) == 0 {
return hintStyle.Render(" No differences found.")
}
addStyle := lipgloss.NewStyle().Foreground(colorGreen)
remStyle := lipgloss.NewStyle().Foreground(colorRed)
eqStyle := lipgloss.NewStyle().Foreground(colorGray)
maxLine := width - 3
if maxLine < 1 {
maxLine = 1
}
var sb strings.Builder
for _, l := range lines {
content := l.content
if len(content) > maxLine {
content = content[:maxLine-1] + "…"
}
switch l.kind {
case diffAdded:
sb.WriteString(addStyle.Render("+ "+content) + "\n")
case diffRemoved:
sb.WriteString(remStyle.Render("- "+content) + "\n")
case diffEqual:
sb.WriteString(eqStyle.Render(" "+content) + "\n")
}
}
return sb.String()
}
// diffSummary returns a compact "+N -M" or "identical" summary.
func diffSummary(lines []diffLine) string {
var added, removed int
for _, l := range lines {
switch l.kind {
case diffAdded:
added++
case diffRemoved:
removed++
}
}
if added == 0 && removed == 0 {
return "identical"
}
var parts []string
if added > 0 {
parts = append(parts, fmt.Sprintf("+%d", added))
}
if removed > 0 {
parts = append(parts, fmt.Sprintf("-%d", removed))
}
return strings.Join(parts, " ")
}
// ---------------------------------------------------------------------------
// Snapshot storage (.greq/snapshots/<slug>.json)
// ---------------------------------------------------------------------------
func snapshotPath(name string) string {
slug := strings.ToLower(strings.Map(func(r rune) rune {
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
return r
}
return '_'
}, name))
return filepath.Join(storage.RootDir, "snapshots", slug+".json")
}
func saveSnapshot(name, body string) error {
p := snapshotPath(name)
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
return err
}
return os.WriteFile(p, []byte(body), 0o644)
}
func loadSnapshot(name string) (string, error) {
data, err := os.ReadFile(snapshotPath(name))
if err != nil {
return "", err
}
return string(data), nil
}
func snapshotExists(name string) bool {
_, err := os.Stat(snapshotPath(name))
return err == nil
}