// /futriis/internal/cli/prompt.go // Пакет cli реализует интерактивное приглашение командной строки с поддержкой истории. // Обеспечивает редактирование строки ввода, навигацию по истории стрелками, // поддержку Unicode (включая кириллицу) и управление курсором терминала. // Использует raw режим терминала для обработки специальных клавиш. package cli import ( "bufio" "fmt" "os" "futriis/pkg/utils" "github.com/mattn/go-runewidth" "golang.org/x/term" ) // ANSI escape sequences для управления курсором const ( ansiHideCursor = "\033[?25l" ansiShowCursor = "\033[?25h" ansiClearLine = "\033[2K" ansiCarriageReturn = "\r" ) // 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) // Скрываем курсор в начале строки fmt.Print(ansiHideCursor) // Очищаем буфер p.buffer = make([]rune, 0) p.pos = 0 // Показываем приглашение promptStr := utils.ColorPrompt + "futriis:~> " + utils.ColorReset fmt.Print(promptStr) // Показываем курсор только после приглашения (в месте ввода) fmt.Print(ansiShowCursor) 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.Print(ansiHideCursor) fmt.Println() cmd := string(p.buffer) if cmd != "" { p.history.Add(cmd) } p.history.Reset() // Логируем команду if logger := utils.GetLogger(); logger != nil && cmd != "" { logger.Log("CMD", cmd) } 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(ansiHideCursor) // Возврат в начало строки и очистка fmt.Print(ansiCarriageReturn + ansiClearLine) // Печатаем приглашение promptStr := utils.ColorPrompt + "futriis:~> " + utils.ColorReset fmt.Print(promptStr) // Печатаем текущий буфер if len(p.buffer) > 0 { fmt.Print(string(p.buffer)) } // Вычисляем ширину приглашения 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) fmt.Print(ansiShowCursor) } // readSimple читает строку без специальной обработки (fallback) func (p *Prompt) readSimple() (string, error) { fmt.Print(utils.GetPrompt()) reader := bufio.NewReader(os.Stdin) cmd, err := reader.ReadString('\n') if err == nil { cmd = cmd[:len(cmd)-1] // Убираем \n // Логируем команду if logger := utils.GetLogger(); logger != nil && cmd != "" { logger.Log("CMD", cmd) } } return cmd, err }