mirror of
https://github.com/usestrix/strix.git
synced 2026-08-24 20:02:39 +02:00
feat(tui): replace Textual with a Go/Bubble Tea interface (#941)
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Filesystem: apply_patch + view_image (filesystem_renderer.py)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const (
|
||||
addFilePfx = "*** Add File: "
|
||||
deleteFilePfx = "*** Delete File: "
|
||||
updateFilePfx = "*** Update File: "
|
||||
beginPatch = "*** Begin Patch"
|
||||
endPatch = "*** End Patch"
|
||||
)
|
||||
|
||||
type patchOp struct {
|
||||
kind string
|
||||
path string
|
||||
old []string
|
||||
new []string
|
||||
}
|
||||
|
||||
func extractPatchText(args map[string]any) string {
|
||||
if raw, ok := args["patch"].(string); ok {
|
||||
return raw
|
||||
}
|
||||
if raw, ok := args["patch"].(map[string]any); ok {
|
||||
if inner, ok := raw["patch"].(string); ok {
|
||||
return inner
|
||||
}
|
||||
}
|
||||
if fb, ok := args["input"].(string); ok {
|
||||
return fb
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func parsePatchOperations(patch string) []patchOp {
|
||||
var ops []patchOp
|
||||
var cur *patchOp
|
||||
flush := func() {
|
||||
if cur != nil && cur.kind != "" {
|
||||
ops = append(ops, *cur)
|
||||
}
|
||||
cur = nil
|
||||
}
|
||||
for _, line := range strings.Split(patch, "\n") {
|
||||
switch {
|
||||
case line == beginPatch || line == endPatch:
|
||||
continue
|
||||
case strings.HasPrefix(line, addFilePfx):
|
||||
flush()
|
||||
cur = &patchOp{kind: "add", path: strings.TrimSpace(line[len(addFilePfx):])}
|
||||
case strings.HasPrefix(line, updateFilePfx):
|
||||
flush()
|
||||
cur = &patchOp{kind: "update", path: strings.TrimSpace(line[len(updateFilePfx):])}
|
||||
case strings.HasPrefix(line, deleteFilePfx):
|
||||
flush()
|
||||
cur = &patchOp{kind: "delete", path: strings.TrimSpace(line[len(deleteFilePfx):])}
|
||||
case cur != nil && cur.kind == "update":
|
||||
if strings.HasPrefix(line, "@@") {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(line, "-") && !strings.HasPrefix(line, "---") {
|
||||
cur.old = append(cur.old, line[1:])
|
||||
} else if strings.HasPrefix(line, "+") && !strings.HasPrefix(line, "+++") {
|
||||
cur.new = append(cur.new, line[1:])
|
||||
}
|
||||
case cur != nil && cur.kind == "add":
|
||||
if strings.HasPrefix(line, "+") {
|
||||
cur.new = append(cur.new, line[1:])
|
||||
} else if strings.TrimSpace(line) != "" {
|
||||
cur.new = append(cur.new, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
flush()
|
||||
return ops
|
||||
}
|
||||
|
||||
var opLabel = map[string]string{"add": "create", "update": "edit", "delete": "delete"}
|
||||
|
||||
func renderPatchOperation(b *strings.Builder, op patchOp) {
|
||||
label := opLabel[op.kind]
|
||||
if label == "" {
|
||||
label = "file"
|
||||
}
|
||||
b.WriteString(Col(Emerald).Render("◇ ") + Dim().Render(label))
|
||||
if op.path != "" {
|
||||
p := op.path
|
||||
if len(p) > 60 {
|
||||
p = p[len(p)-60:]
|
||||
}
|
||||
b.WriteString(" " + Dim().Render(p))
|
||||
}
|
||||
lang := languageForPath(op.path)
|
||||
if op.kind == "update" {
|
||||
for _, line := range highlightLines(op.old, lang) {
|
||||
b.WriteString("\n" + Col(Red).Render("-") + " " + line)
|
||||
}
|
||||
for _, line := range highlightLines(op.new, lang) {
|
||||
b.WriteString("\n" + Col(Green).Render("+") + " " + line)
|
||||
}
|
||||
} else if op.kind == "add" && len(op.new) > 0 {
|
||||
b.WriteString("\n" + HighlightCode(strings.Join(op.new, "\n"), lang))
|
||||
}
|
||||
}
|
||||
|
||||
func highlightLines(lines []string, lang string) []string {
|
||||
if len(lines) == 0 || lang == "" {
|
||||
return lines
|
||||
}
|
||||
return strings.Split(HighlightCode(strings.Join(lines, "\n"), lang), "\n")
|
||||
}
|
||||
|
||||
func renderApplyPatch(args map[string]any, result any, status string) string {
|
||||
ops := parsePatchOperations(extractPatchText(args))
|
||||
var b strings.Builder
|
||||
if len(ops) == 0 {
|
||||
b.WriteString(Col(Emerald).Render("◇ ") + Dim().Render("patch"))
|
||||
if s, ok := result.(string); ok && strings.TrimSpace(s) != "" {
|
||||
b.WriteString("\n " + Dim().Render(strings.TrimSpace(s)))
|
||||
} else if result == nil {
|
||||
b.WriteString(" " + Dim().Render("Processing..."))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
for i, op := range ops {
|
||||
if i > 0 {
|
||||
b.WriteString("\n")
|
||||
}
|
||||
renderPatchOperation(&b, op)
|
||||
}
|
||||
if status == "failed" {
|
||||
if s, ok := result.(string); ok && strings.TrimSpace(s) != "" {
|
||||
b.WriteString("\n " + Col(Red).Render(strings.TrimSpace(s)))
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
Reference in New Issue
Block a user