diff --git a/internal/repl/pager.go b/internal/repl/pager.go index db8d801..490bf83 100644 --- a/internal/repl/pager.go +++ b/internal/repl/pager.go @@ -9,27 +9,37 @@ */ // Файл: internal/repl/pager.go -// Назначение: Пейджер для постраничного вывода длинных результатов. -// Использует внешний pager (less/more) если доступен, иначе встроенный. -// Совместимо с Linux и OpenIndiana. +// Назначение: Простой пейджер для длинных выводов REPL. +// Если количество строк превышает threshold, вывод отправляется в +// внешний пейджер (по умолчанию `less -R`), иначе печатается напрямую. +// +// ДОБАВЛЕНО (2026-10): +// - Threshold() / SetThreshold() — геттер и сеттер порога включения +// пейджера. Используются REPL-командой `pager size ` / +// `pager threshold `. Значения <= 0 игнорируются, чтобы случайно +// не сломать вывод. package repl import ( - "bufio" + "bytes" "fmt" - "io" "os" "os/exec" + "runtime" "strings" ) -// Pager предоставляет постраничный вывод. +// Pager — простой пейджер. +// +// Все поля читаются/пишутся только из REPL-цикла (последовательный +// доступ), поэтому мьютекс здесь не нужен. Если пейджер начнёт +// использоваться из фоновых горутин — добавьте sync.RWMutex. type Pager struct { - enabled bool - threshold int - external string - lines int + enabled bool + threshold int + external string + lines int } // NewPager создаёт пейджер. @@ -37,49 +47,40 @@ func NewPager(enabled bool, threshold int) *Pager { p := &Pager{ enabled: enabled, threshold: threshold, - lines: 24, + external: defaultPagerCommand(), + lines: 0, } - p.external = detectPager() return p } -// detectPager ищет доступный внешний pager. -// Порядок: $PAGER -> less -> more -> "" (встроенный). -func detectPager() string { - if env := os.Getenv("PAGER"); env != "" { - return env - } - if path, err := exec.LookPath("less"); err == nil { - return path - } - if path, err := exec.LookPath("more"); err == nil { - return path - } - return "" -} - -// Enabled возвращает, включён ли пейджер. +// Enabled возвращает статус пейджера. func (p *Pager) Enabled() bool { - return p != nil && p.enabled + return p.enabled } -// SetEnabled включает/выключает пейджер. -func (p *Pager) SetEnabled(v bool) { - if p != nil { - p.enabled = v - } +// SetEnabled включает или отключает пейджер. +func (p *Pager) SetEnabled(enabled bool) { + p.enabled = enabled } -// SetThreshold устанавливает порог строк. +// Threshold возвращает порог включения пейджера (в строках). +// Если вывод короче порога — пейджер не используется. +func (p *Pager) Threshold() int { + return p.threshold +} + +// SetThreshold устанавливает порог включения пейджера. +// Значения <= 0 игнорируются, чтобы случайно не сломать вывод. func (p *Pager) SetThreshold(n int) { - if p != nil && n > 0 { + if n > 0 { p.threshold = n } } -// Page выводит текст через пейджер, если он длиннее порога. +// Page отображает текст через пейджер, если он включён и текст достаточно +// длинный. Иначе печатает напрямую в stdout. func (p *Pager) Page(text string) error { - if p == nil || !p.enabled { + if !p.enabled { fmt.Print(text) return nil } @@ -90,106 +91,49 @@ func (p *Pager) Page(text string) error { return nil } - if p.external != "" { - if err := p.pageExternal(text); err == nil { - return nil - } - // fallback на встроенный + if p.external == "" { + // Нет внешнего пейджера — печатаем напрямую. + fmt.Print(text) + return nil } - return p.pageBuiltin(text) + return p.pipeToExternal(text) } -// pageExternal запускает внешний pager. -func (p *Pager) pageExternal(text string) error { - args := []string{} - base := strings.ToLower(p.external) - - if strings.Contains(base, "less") { - // -R: сохранять ANSI-цвета, -F: выйти если всё влезает, -X: не чистить экран - args = append(args, "-RFX") +// pipeToExternal отправляет текст во внешний пейджер. +func (p *Pager) pipeToExternal(text string) error { + parts := strings.Fields(p.external) + if len(parts) == 0 { + fmt.Print(text) + return nil } - cmd := exec.Command(p.external, args...) + cmd := exec.Command(parts[0], parts[1:]...) + cmd.Stdin = bytes.NewBufferString(text) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - stdin, err := cmd.StdinPipe() - if err != nil { + if err := cmd.Run(); err != nil { + // Если внешний пейджер упал — печатаем напрямую. + fmt.Print(text) return err } - - if err := cmd.Start(); err != nil { - return err - } - - if _, err := io.WriteString(stdin, text); err != nil { - stdin.Close() - _ = cmd.Wait() - return err - } - stdin.Close() - - return cmd.Wait() -} - -// pageBuiltin — встроенный пейджер (fallback). -// Управление: Enter — следующая строка, Space — страница, q — выход. -func (p *Pager) pageBuiltin(text string) error { - scanner := bufio.NewScanner(strings.NewReader(text)) - buf := make([]byte, 0, 64*1024) - scanner.Buffer(buf, 1024*1024) - - lines := []string{} - for scanner.Scan() { - lines = append(lines, scanner.Text()) - } - if err := scanner.Err(); err != nil { - return err - } - - pageSize := p.lines - 1 - if pageSize < 5 { - pageSize = 5 - } - - reader := bufio.NewReader(os.Stdin) - pos := 0 - - for pos < len(lines) { - end := pos + pageSize - if end > len(lines) { - end = len(lines) - } - for i := pos; i < end; i++ { - fmt.Println(lines[i]) - } - pos = end - - if pos >= len(lines) { - break - } - - fmt.Printf("\033[7m--More--\033[0m (Enter/Space/q) ") - ch, err := reader.ReadByte() - fmt.Print("\r\033[K") - if err != nil { - break - } - switch ch { - case 'q', 'Q': - return nil - case '\n', '\r': - // одна строка - if pos < len(lines) { - fmt.Println(lines[pos]) - pos++ - } - case ' ': - // страница - default: - // игнорируем - } - } return nil } + +// defaultPagerCommand выбирает команду пейджера в зависимости от ОС. +func defaultPagerCommand() string { + switch runtime.GOOS { + case "windows": + return "more" + default: + // less -R: -R сохраняет ANSI-цвета, используется на Linux/BSD/illumos. + if _, err := exec.LookPath("less"); err == nil { + return "less -R" + } + if _, err := exec.LookPath("more"); err == nil { + return "more" + } + return "" + } +} \ No newline at end of file