mirror of
https://github.com/ollama/ollama.git
synced 2026-04-18 09:54:18 +02:00
514 lines
11 KiB
Go
514 lines
11 KiB
Go
package config
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
|
|
"golang.org/x/term"
|
|
)
|
|
|
|
// ANSI escape sequences for terminal formatting.
|
|
const (
|
|
ansiHideCursor = "\033[?25l"
|
|
ansiShowCursor = "\033[?25h"
|
|
ansiBold = "\033[1m"
|
|
ansiReset = "\033[0m"
|
|
ansiGray = "\033[37m"
|
|
ansiGreen = "\033[32m"
|
|
ansiClearDown = "\033[J"
|
|
)
|
|
|
|
const maxDisplayedItems = 10
|
|
|
|
var errCancelled = errors.New("cancelled")
|
|
|
|
type selectItem struct {
|
|
Name string
|
|
Description string
|
|
}
|
|
|
|
type inputEvent int
|
|
|
|
const (
|
|
eventNone inputEvent = iota
|
|
eventEnter
|
|
eventEscape
|
|
eventUp
|
|
eventDown
|
|
eventTab
|
|
eventBackspace
|
|
eventChar
|
|
)
|
|
|
|
type selectState struct {
|
|
items []selectItem
|
|
filter string
|
|
selected int
|
|
scrollOffset int
|
|
}
|
|
|
|
func newSelectState(items []selectItem) *selectState {
|
|
return &selectState{items: items}
|
|
}
|
|
|
|
func (s *selectState) filtered() []selectItem {
|
|
return filterItems(s.items, s.filter)
|
|
}
|
|
|
|
func (s *selectState) handleInput(event inputEvent, char byte) (done bool, result string, err error) {
|
|
filtered := s.filtered()
|
|
|
|
switch event {
|
|
case eventEnter:
|
|
if len(filtered) > 0 && s.selected < len(filtered) {
|
|
return true, filtered[s.selected].Name, nil
|
|
}
|
|
case eventEscape:
|
|
return true, "", errCancelled
|
|
case eventBackspace:
|
|
if len(s.filter) > 0 {
|
|
s.filter = s.filter[:len(s.filter)-1]
|
|
s.selected = 0
|
|
s.scrollOffset = 0
|
|
}
|
|
case eventUp:
|
|
if s.selected > 0 {
|
|
s.selected--
|
|
if s.selected < s.scrollOffset {
|
|
s.scrollOffset = s.selected
|
|
}
|
|
}
|
|
case eventDown:
|
|
if s.selected < len(filtered)-1 {
|
|
s.selected++
|
|
if s.selected >= s.scrollOffset+maxDisplayedItems {
|
|
s.scrollOffset = s.selected - maxDisplayedItems + 1
|
|
}
|
|
}
|
|
case eventChar:
|
|
s.filter += string(char)
|
|
s.selected = 0
|
|
s.scrollOffset = 0
|
|
}
|
|
|
|
return false, "", nil
|
|
}
|
|
|
|
type multiSelectState struct {
|
|
items []selectItem
|
|
itemIndex map[string]int
|
|
filter string
|
|
highlighted int
|
|
scrollOffset int
|
|
checked map[int]bool
|
|
checkOrder []int
|
|
focusOnButton bool
|
|
}
|
|
|
|
func newMultiSelectState(items []selectItem, preChecked []string) *multiSelectState {
|
|
s := &multiSelectState{
|
|
items: items,
|
|
itemIndex: make(map[string]int, len(items)),
|
|
checked: make(map[int]bool),
|
|
}
|
|
|
|
for i, item := range items {
|
|
s.itemIndex[item.Name] = i
|
|
}
|
|
|
|
for _, name := range preChecked {
|
|
if idx, ok := s.itemIndex[name]; ok {
|
|
s.checked[idx] = true
|
|
s.checkOrder = append(s.checkOrder, idx)
|
|
}
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
func (s *multiSelectState) filtered() []selectItem {
|
|
return filterItems(s.items, s.filter)
|
|
}
|
|
|
|
func (s *multiSelectState) toggleItem() {
|
|
filtered := s.filtered()
|
|
if len(filtered) == 0 || s.highlighted >= len(filtered) {
|
|
return
|
|
}
|
|
|
|
item := filtered[s.highlighted]
|
|
origIdx := s.itemIndex[item.Name]
|
|
|
|
if s.checked[origIdx] {
|
|
delete(s.checked, origIdx)
|
|
for i, idx := range s.checkOrder {
|
|
if idx == origIdx {
|
|
s.checkOrder = append(s.checkOrder[:i], s.checkOrder[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
} else {
|
|
s.checked[origIdx] = true
|
|
s.checkOrder = append(s.checkOrder, origIdx)
|
|
}
|
|
}
|
|
|
|
func (s *multiSelectState) handleInput(event inputEvent, char byte) (done bool, result []string, err error) {
|
|
filtered := s.filtered()
|
|
|
|
switch event {
|
|
case eventEnter:
|
|
if s.focusOnButton && len(s.checkOrder) > 0 {
|
|
var res []string
|
|
for _, idx := range s.checkOrder {
|
|
res = append(res, s.items[idx].Name)
|
|
}
|
|
return true, res, nil
|
|
} else if !s.focusOnButton {
|
|
s.toggleItem()
|
|
}
|
|
case eventTab:
|
|
if len(s.checkOrder) > 0 {
|
|
s.focusOnButton = !s.focusOnButton
|
|
}
|
|
case eventEscape:
|
|
return true, nil, errCancelled
|
|
case eventBackspace:
|
|
if len(s.filter) > 0 {
|
|
s.filter = s.filter[:len(s.filter)-1]
|
|
s.highlighted = 0
|
|
s.scrollOffset = 0
|
|
s.focusOnButton = false
|
|
}
|
|
case eventUp:
|
|
if s.focusOnButton {
|
|
s.focusOnButton = false
|
|
} else if s.highlighted > 0 {
|
|
s.highlighted--
|
|
if s.highlighted < s.scrollOffset {
|
|
s.scrollOffset = s.highlighted
|
|
}
|
|
}
|
|
case eventDown:
|
|
if s.focusOnButton {
|
|
s.focusOnButton = false
|
|
} else if s.highlighted < len(filtered)-1 {
|
|
s.highlighted++
|
|
if s.highlighted >= s.scrollOffset+maxDisplayedItems {
|
|
s.scrollOffset = s.highlighted - maxDisplayedItems + 1
|
|
}
|
|
}
|
|
case eventChar:
|
|
s.filter += string(char)
|
|
s.highlighted = 0
|
|
s.scrollOffset = 0
|
|
s.focusOnButton = false
|
|
}
|
|
|
|
return false, nil, nil
|
|
}
|
|
|
|
func (s *multiSelectState) selectedCount() int {
|
|
return len(s.checkOrder)
|
|
}
|
|
|
|
// Terminal I/O handling
|
|
|
|
type terminalState struct {
|
|
fd int
|
|
oldState *term.State
|
|
}
|
|
|
|
func enterRawMode() (*terminalState, error) {
|
|
fd := int(os.Stdin.Fd())
|
|
oldState, err := term.MakeRaw(fd)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fmt.Fprint(os.Stderr, ansiHideCursor)
|
|
return &terminalState{fd: fd, oldState: oldState}, nil
|
|
}
|
|
|
|
func (t *terminalState) restore() {
|
|
fmt.Fprint(os.Stderr, ansiShowCursor)
|
|
term.Restore(t.fd, t.oldState)
|
|
}
|
|
|
|
func clearLines(n int) {
|
|
if n > 0 {
|
|
fmt.Fprintf(os.Stderr, "\033[%dA", n)
|
|
fmt.Fprint(os.Stderr, ansiClearDown)
|
|
}
|
|
}
|
|
|
|
func parseInput(r io.Reader) (inputEvent, byte, error) {
|
|
buf := make([]byte, 3)
|
|
n, err := r.Read(buf)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
|
|
switch {
|
|
case n == 1 && buf[0] == 13:
|
|
return eventEnter, 0, nil
|
|
case n == 1 && (buf[0] == 3 || buf[0] == 27):
|
|
return eventEscape, 0, nil
|
|
case n == 1 && buf[0] == 9:
|
|
return eventTab, 0, nil
|
|
case n == 1 && buf[0] == 127:
|
|
return eventBackspace, 0, nil
|
|
case n == 3 && buf[0] == 27 && buf[1] == 91 && buf[2] == 65:
|
|
return eventUp, 0, nil
|
|
case n == 3 && buf[0] == 27 && buf[1] == 91 && buf[2] == 66:
|
|
return eventDown, 0, nil
|
|
case n == 1 && buf[0] >= 32 && buf[0] < 127:
|
|
return eventChar, buf[0], nil
|
|
}
|
|
|
|
return eventNone, 0, nil
|
|
}
|
|
|
|
// Rendering
|
|
|
|
func renderSelect(w io.Writer, prompt string, s *selectState) int {
|
|
filtered := s.filtered()
|
|
|
|
if s.filter == "" {
|
|
fmt.Fprintf(w, "%s %sType to filter...%s\r\n", prompt, ansiGray, ansiReset)
|
|
} else {
|
|
fmt.Fprintf(w, "%s %s\r\n", prompt, s.filter)
|
|
}
|
|
lineCount := 1
|
|
|
|
if len(filtered) == 0 {
|
|
fmt.Fprintf(w, " %s(no matches)%s\r\n", ansiGray, ansiReset)
|
|
lineCount++
|
|
} else {
|
|
displayCount := min(len(filtered), maxDisplayedItems)
|
|
|
|
for i := range displayCount {
|
|
idx := s.scrollOffset + i
|
|
if idx >= len(filtered) {
|
|
break
|
|
}
|
|
item := filtered[idx]
|
|
prefix := " "
|
|
if idx == s.selected {
|
|
prefix = " " + ansiBold + "> "
|
|
}
|
|
if item.Description != "" {
|
|
fmt.Fprintf(w, "%s%s%s %s- %s%s\r\n", prefix, item.Name, ansiReset, ansiGray, item.Description, ansiReset)
|
|
} else {
|
|
fmt.Fprintf(w, "%s%s%s\r\n", prefix, item.Name, ansiReset)
|
|
}
|
|
lineCount++
|
|
}
|
|
|
|
if remaining := len(filtered) - s.scrollOffset - displayCount; remaining > 0 {
|
|
fmt.Fprintf(w, " %s... and %d more%s\r\n", ansiGray, remaining, ansiReset)
|
|
lineCount++
|
|
}
|
|
}
|
|
|
|
return lineCount
|
|
}
|
|
|
|
func renderMultiSelect(w io.Writer, prompt string, s *multiSelectState) int {
|
|
filtered := s.filtered()
|
|
|
|
if s.filter == "" {
|
|
fmt.Fprintf(w, "%s %sType to filter...%s\r\n", prompt, ansiGray, ansiReset)
|
|
} else {
|
|
fmt.Fprintf(w, "%s %s\r\n", prompt, s.filter)
|
|
}
|
|
lineCount := 1
|
|
|
|
if len(filtered) == 0 {
|
|
fmt.Fprintf(w, " %s(no matches)%s\r\n", ansiGray, ansiReset)
|
|
lineCount++
|
|
} else {
|
|
displayCount := min(len(filtered), maxDisplayedItems)
|
|
|
|
for i := range displayCount {
|
|
idx := s.scrollOffset + i
|
|
if idx >= len(filtered) {
|
|
break
|
|
}
|
|
item := filtered[idx]
|
|
origIdx := s.itemIndex[item.Name]
|
|
|
|
checkbox := "[ ]"
|
|
if s.checked[origIdx] {
|
|
checkbox = "[x]"
|
|
}
|
|
|
|
prefix := " "
|
|
suffix := ""
|
|
if idx == s.highlighted && !s.focusOnButton {
|
|
prefix = "> "
|
|
}
|
|
if len(s.checkOrder) > 0 && s.checkOrder[0] == origIdx {
|
|
suffix = " " + ansiGray + "(default)" + ansiReset
|
|
}
|
|
|
|
desc := ""
|
|
if item.Description != "" {
|
|
desc = " " + ansiGray + "- " + item.Description + ansiReset
|
|
}
|
|
|
|
if idx == s.highlighted && !s.focusOnButton {
|
|
fmt.Fprintf(w, " %s%s %s %s%s%s%s\r\n", ansiBold, prefix, checkbox, item.Name, ansiReset, desc, suffix)
|
|
} else {
|
|
fmt.Fprintf(w, " %s %s %s%s%s\r\n", prefix, checkbox, item.Name, desc, suffix)
|
|
}
|
|
lineCount++
|
|
}
|
|
|
|
if remaining := len(filtered) - s.scrollOffset - displayCount; remaining > 0 {
|
|
fmt.Fprintf(w, " %s... and %d more%s\r\n", ansiGray, remaining, ansiReset)
|
|
lineCount++
|
|
}
|
|
}
|
|
|
|
fmt.Fprintf(w, "\r\n")
|
|
lineCount++
|
|
count := s.selectedCount()
|
|
switch {
|
|
case count == 0:
|
|
fmt.Fprintf(w, " %sSelect at least one model.%s\r\n", ansiGray, ansiReset)
|
|
case s.focusOnButton:
|
|
fmt.Fprintf(w, " %s> [ Continue ]%s %s(%d selected)%s\r\n", ansiBold, ansiReset, ansiGray, count, ansiReset)
|
|
default:
|
|
fmt.Fprintf(w, " %s[ Continue ] (%d selected) - press Tab%s\r\n", ansiGray, count, ansiReset)
|
|
}
|
|
lineCount++
|
|
|
|
return lineCount
|
|
}
|
|
|
|
// selectPrompt prompts the user to select a single item from a list.
|
|
func selectPrompt(prompt string, items []selectItem) (string, error) {
|
|
if len(items) == 0 {
|
|
return "", fmt.Errorf("no items to select from")
|
|
}
|
|
|
|
ts, err := enterRawMode()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer ts.restore()
|
|
|
|
state := newSelectState(items)
|
|
var lastLineCount int
|
|
|
|
render := func() {
|
|
clearLines(lastLineCount)
|
|
lastLineCount = renderSelect(os.Stderr, prompt, state)
|
|
}
|
|
|
|
render()
|
|
|
|
for {
|
|
event, char, err := parseInput(os.Stdin)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
done, result, err := state.handleInput(event, char)
|
|
if done {
|
|
clearLines(lastLineCount)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
render()
|
|
}
|
|
}
|
|
|
|
// multiSelectPrompt prompts the user to select multiple items from a list.
|
|
func multiSelectPrompt(prompt string, items []selectItem, preChecked []string) ([]string, error) {
|
|
if len(items) == 0 {
|
|
return nil, fmt.Errorf("no items to select from")
|
|
}
|
|
|
|
ts, err := enterRawMode()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer ts.restore()
|
|
|
|
state := newMultiSelectState(items, preChecked)
|
|
var lastLineCount int
|
|
|
|
render := func() {
|
|
clearLines(lastLineCount)
|
|
lastLineCount = renderMultiSelect(os.Stderr, prompt, state)
|
|
}
|
|
|
|
render()
|
|
|
|
for {
|
|
event, char, err := parseInput(os.Stdin)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
done, result, err := state.handleInput(event, char)
|
|
if done {
|
|
clearLines(lastLineCount)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
render()
|
|
}
|
|
}
|
|
|
|
func confirmPrompt(prompt string) (bool, error) {
|
|
fd := int(os.Stdin.Fd())
|
|
oldState, err := term.MakeRaw(fd)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer term.Restore(fd, oldState)
|
|
|
|
fmt.Fprintf(os.Stderr, "%s (\033[1my\033[0m/n) ", prompt)
|
|
|
|
buf := make([]byte, 1)
|
|
for {
|
|
if _, err := os.Stdin.Read(buf); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
switch buf[0] {
|
|
case 'Y', 'y', 13:
|
|
fmt.Fprintf(os.Stderr, "yes\r\n")
|
|
return true, nil
|
|
case 'N', 'n', 27, 3:
|
|
fmt.Fprintf(os.Stderr, "no\r\n")
|
|
return false, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func filterItems(items []selectItem, filter string) []selectItem {
|
|
if filter == "" {
|
|
return items
|
|
}
|
|
var result []selectItem
|
|
filterLower := strings.ToLower(filter)
|
|
for _, item := range items {
|
|
if strings.Contains(strings.ToLower(item.Name), filterLower) {
|
|
result = append(result, item)
|
|
}
|
|
}
|
|
return result
|
|
}
|