// /futriis/internal/cli/prompt.go // Пакет cli реализует интерактивное приглашение с поддержкой истории команд package cli import ( "bufio" "fmt" "os" "futriis/pkg/utils" "github.com/mattn/go-runewidth" "golang.org/x/term" ) // Prompt представляет интерактивное приглашение type Prompt struct { history *History buffer []rune pos int } // NewPrompt создаёт новое приглашение func NewPrompt() *Prompt { return &Prompt{ history: NewHistory(100), buffer: make([]rune, 0), pos: 0, } } // ReadLine читает строку с поддержкой истории и редактирования func (p *Prompt) ReadLine() (string, error) { oldState, err := term.MakeRaw(int(os.Stdin.Fd())) if err != nil { return p.readSimple() } defer term.Restore(int(os.Stdin.Fd()), oldState) // Очищаем буфер p.buffer = make([]rune, 0) p.pos = 0 // Показываем приглашение fmt.Print(utils.GetPrompt()) reader := bufio.NewReader(os.Stdin) for { r, _, err := reader.ReadRune() if err != nil { return "", err } switch r { case 3: // Ctrl+C return "", nil case 4: // Ctrl+D if len(p.buffer) == 0 { return "exit", nil } case 13, 10: // Enter fmt.Println() cmd := string(p.buffer) p.history.Add(cmd) p.history.Reset() return cmd, nil case 127: // Backspace if p.pos > 0 { // Удаляем символ перед курсором p.buffer = append(p.buffer[:p.pos-1], p.buffer[p.pos:]...) p.pos-- p.refreshLine() } case 27: // Escape sequence (стрелки) // Читаем следующие два символа r2, _, _ := reader.ReadRune() r3, _, _ := reader.ReadRune() if r2 == '[' { switch r3 { case 'A': // Up arrow prev := p.history.GetPrevious() if prev != "" { p.buffer = []rune(prev) p.pos = len(p.buffer) p.refreshLine() } case 'B': // Down arrow next := p.history.GetNext() p.buffer = []rune(next) p.pos = len(p.buffer) p.refreshLine() case 'C': // Right arrow if p.pos < len(p.buffer) { p.pos++ p.refreshLine() } case 'D': // Left arrow if p.pos > 0 { p.pos-- p.refreshLine() } } } default: // Добавляем символ (поддержка Unicode, включая русский) if r >= 32 { // Печатные символы // Вставляем символ в позицию курсора if p.pos == len(p.buffer) { p.buffer = append(p.buffer, r) } else { p.buffer = append(p.buffer[:p.pos], append([]rune{r}, p.buffer[p.pos:]...)...) } p.pos++ p.refreshLine() } } } } // refreshLine обновляет текущую строку с правильным позиционированием курсора func (p *Prompt) refreshLine() { // Сохраняем позицию fmt.Print("\r\033[K") // Возврат в начало строки и очистка // Печатаем приглашение и текущий буфер promptStr := utils.ColorPromptCode + "futriis:~> " + utils.ColorReset fmt.Print(promptStr + string(p.buffer)) // Вычисляем ширину приглашения (без ANSI кодов) promptWidth := runewidth.StringWidth("futriis:~> ") // Вычисляем позицию курсора (учитываем ширину каждого символа) cursorPos := promptWidth for i := 0; i < p.pos; i++ { cursorPos += runewidth.RuneWidth(p.buffer[i]) } // Возвращаем курсор на позицию fmt.Printf("\033[%dG", cursorPos+1) } // readSimple читает строку без специальной обработки (fallback) func (p *Prompt) readSimple() (string, error) { fmt.Print(utils.GetPrompt()) reader := bufio.NewReader(os.Stdin) return reader.ReadString('\n') }