futriis/internal/cli/prompt.go
2026-02-27 22:04:04 +03:00

205 lines
5.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// /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
}