futriis/internal/cli/prompt.go

205 lines
5.5 KiB
Go
Raw Normal View History

2026-02-27 22:04:04 +03:00
// /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
}