okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
6e82811
main
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. 12MaxPromptImages = 10 13// A paste longer than either collapses into a marker. 14collapseLines = 10 15collapseChars = 1000 16) 17 18var ( 19imageMarker = regexp .MustCompile (`\[Image #(\d+)\]` ) 20pasteMarker = regexp .MustCompile (`\[Paste #(\d+)(?:, [^\]\n]*)?\]` ) 21// anyMarker is what one backspace or delete removes whole. 22anyMarker = 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 { 27pattern ()* regexp.Regexp 28marker (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 35thumb * 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 { 41pasted := make ([]pastedImage ,len (images )) 42for i ,image := range images { 43pasted [i ]= pastedImage {ImageAttachment :image } 44 } 45return pasted 46} 47 48type imageKind struct {} 49 50func (imageKind )pattern ()* regexp.Regexp {return imageMarker } 51 52func (imageKind )marker (n int ,_ pastedImage )string { 53return "[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 { 61if lines := strings .Count (text ,"\n" )+ 1 ;lines > collapseLines { 62return "[Paste #" + strconv .Itoa (n )+ ", +" + strconv .Itoa (lines )+ " lines]" 63 } 64return "[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 { 70byNumber map [int ]T 71next int 72kind K 73} 74 75type ( 76promptImages = attachments [pastedImage ,imageKind ] 77promptPastes = attachments [string ,pasteKind ] 78) 79 80// collapses reports whether pasted text is long enough to become a marker. 81func collapses (text string )bool { 82return 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 { 87if a .byNumber == nil { 88a .byNumber = make (map [int ]T ) 89 } 90a .next ++ 91a .byNumber [a .next ]= item 92return 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 ) { 98a .byNumber ,a .next = nil ,0 99for _ ,item := range items { 100a .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 { 107var numbers []int 108seen := make (map [int ]bool ) 109for _ ,match := range a .kind .pattern ().FindAllStringSubmatch (text ,- 1 ) { 110n ,_ := strconv .Atoi (match [1 ]) 111if _ ,ok := a .byNumber [n ];ok && ! seen [n ] { 112seen [n ]= true 113numbers = append (numbers ,n ) 114 } 115 } 116return 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 ) { 123numbers := a .referenced (text ) 124renumbered := make (map [int ]int ,len (numbers )) 125items := make ([]T ,len (numbers )) 126for i ,n := range numbers { 127renumbered [n ]= i + 1 128items [i ]= a .byNumber [n ] 129 } 130pattern := a .kind .pattern () 131text = pattern .ReplaceAllStringFunc (text ,func (marker string )string { 132n ,_ := strconv .Atoi (pattern .FindStringSubmatch (marker )[1 ]) 133if to ,ok := renumbered [n ];ok { 134return a .kind .marker (to ,a .byNumber [n ]) 135 } 136return marker 137 }) 138return 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 ) { 144text := string (line ) 145for _ ,span := range anyMarker .FindAllStringIndex (text ,- 1 ) { 146start := len ([]rune (text [:span [0 ]])) 147end := start + len ([]rune (text [span [0 ]:span [1 ]])) 148if backward && start < col && col <= end || ! backward && start <= col && col < end { 149return start ,end ,true 150 } 151 } 152return 0 ,0 ,false 153}