okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

nekomimi.petfeat: multiple images, collapsed pastes, and an attachment strip in the prompt6e82811

main
4.4 KiB153 linesraw
1package tui
2
3import (
4	"albedo/cli/internal/daemon"
5	"regexp"
6	"strconv"
7	"strings"
8)
9
10const (
11	// MaxPromptImages is how many images one prompt may attach.
12	MaxPromptImages = 10
13	// A paste longer than either collapses into a marker.
14	collapseLines = 10
15	collapseChars = 1000
16)
17
18var (
19	imageMarker = regexp.MustCompile(`\[Image #(\d+)\]`)
20	pasteMarker = regexp.MustCompile(`\[Paste #(\d+)(?:, [^\]\n]*)?\]`)
21	// anyMarker is what one backspace or delete removes whole.
22	anyMarker = regexp.MustCompile(`\[(?:Image|Paste) #\d+(?:, [^\]\n]*)?\]`)
23)
24
25// markerKind says how one kind of attachment is marked in the prompt.
26type markerKind[T any] interface {
27	pattern() *regexp.Regexp
28	marker(n int, item T) string
29}
30
31// pastedImage is an image in the composer and, when the terminal shows
32// them, its thumbnail.
33type pastedImage struct {
34	daemon.ImageAttachment
35	thumb *thumbnail
36}
37
38// unseen wraps images restored from a sent prompt, whose thumbnails the
39// terminal may no longer hold.
40func unseen(images []daemon.ImageAttachment) []pastedImage {
41	pasted := make([]pastedImage, len(images))
42	for i, image := range images {
43		pasted[i] = pastedImage{ImageAttachment: image}
44	}
45	return pasted
46}
47
48type imageKind struct{}
49
50func (imageKind) pattern() *regexp.Regexp { return imageMarker }
51
52func (imageKind) marker(n int, _ pastedImage) string {
53	return "[Image #" + strconv.Itoa(n) + "]"
54}
55
56type pasteKind struct{}
57
58func (pasteKind) pattern() *regexp.Regexp { return pasteMarker }
59
60func (pasteKind) marker(n int, text string) string {
61	if lines := strings.Count(text, "\n") + 1; lines > collapseLines {
62		return "[Paste #" + strconv.Itoa(n) + ", +" + strconv.Itoa(lines) + " lines]"
63	}
64	return "[Paste #" + strconv.Itoa(n) + ", " + strconv.Itoa(len([]rune(text))) + " chars]"
65}
66
67// attachments holds what was pasted into the composer under the number its
68// marker carries. Deleting a marker drops its item from the prompt.
69type attachments[T any, K markerKind[T]] struct {
70	byNumber map[int]T
71	next     int
72	kind     K
73}
74
75type (
76	promptImages = attachments[pastedImage, imageKind]
77	promptPastes = attachments[string, pasteKind]
78)
79
80// collapses reports whether pasted text is long enough to become a marker.
81func collapses(text string) bool {
82	return strings.Count(text, "\n") >= collapseLines || len([]rune(text)) > collapseChars
83}
84
85// add keeps item and returns the marker to insert at the cursor.
86func (a *attachments[T, K]) add(item T) string {
87	if a.byNumber == nil {
88		a.byNumber = make(map[int]T)
89	}
90	a.next++
91	a.byNumber[a.next] = item
92	return a.kind.marker(a.next, item)
93}
94
95// restore replaces the items with ones numbered 1..n, as a resolved prompt
96// marks them.
97func (a *attachments[T, K]) restore(items []T) {
98	a.byNumber, a.next = nil, 0
99	for _, item := range items {
100		a.add(item)
101	}
102}
103
104// referenced returns the numbers of the items text still marks, in reading
105// order, each once.
106func (a attachments[T, K]) referenced(text string) []int {
107	var numbers []int
108	seen := make(map[int]bool)
109	for _, match := range a.kind.pattern().FindAllStringSubmatch(text, -1) {
110		n, _ := strconv.Atoi(match[1])
111		if _, ok := a.byNumber[n]; ok && !seen[n] {
112			seen[n] = true
113			numbers = append(numbers, n)
114		}
115	}
116	return numbers
117}
118
119// resolve numbers the marked items 1..n in reading order and returns the
120// text with its markers renumbered to match, and the items in that order.
121// Markers for numbers never pasted stay as typed.
122func (a attachments[T, K]) resolve(text string) (string, []T) {
123	numbers := a.referenced(text)
124	renumbered := make(map[int]int, len(numbers))
125	items := make([]T, len(numbers))
126	for i, n := range numbers {
127		renumbered[n] = i + 1
128		items[i] = a.byNumber[n]
129	}
130	pattern := a.kind.pattern()
131	text = pattern.ReplaceAllStringFunc(text, func(marker string) string {
132		n, _ := strconv.Atoi(pattern.FindStringSubmatch(marker)[1])
133		if to, ok := renumbered[n]; ok {
134			return a.kind.marker(to, a.byNumber[n])
135		}
136		return marker
137	})
138	return text, items
139}
140
141// markerSpan returns the rune span of the marker in line that a backspace at
142// col (backward) or a delete at col (forward) would cut into.
143func markerSpan(line []rune, col int, backward bool) (int, int, bool) {
144	text := string(line)
145	for _, span := range anyMarker.FindAllStringIndex(text, -1) {
146		start := len([]rune(text[:span[0]]))
147		end := start + len([]rune(text[span[0]:span[1]]))
148		if backward && start < col && col <= end || !backward && start <= col && col < end {
149			return start, end, true
150		}
151	}
152	return 0, 0, false
153}