Update internal/repl/pager.go

This commit is contained in:
gvsafronov committed 2026-10-05 21:29:14 +00:00
1 parent 06e9558839
commit cc4ec0c029
1 file changed
+72 -128
+72 -128
View File
@@ -9,27 +9,37 @@
*/ */
// Файл: internal/repl/pager.go // Файл: internal/repl/pager.go
// Назначение: Пейджер для постраничного вывода длинных результатов. // Назначение: Простой пейджер для длинных выводов REPL.
// Использует внешний pager (less/more) если доступен, иначе встроенный. // Если количество строк превышает threshold, вывод отправляется в
// Совместимо с Linux и OpenIndiana. // внешний пейджер (по умолчанию `less -R`), иначе печатается напрямую.
//
// ДОБАВЛЕНО (2026-10):
// - Threshold() / SetThreshold() — геттер и сеттер порога включения
// пейджера. Используются REPL-командой `pager size <N>` /
// `pager threshold <N>`. Значения <= 0 игнорируются, чтобы случайно
// не сломать вывод.
package repl package repl
import ( import (
"bufio" "bytes"
"fmt" "fmt"
"io"
"os" "os"
"os/exec" "os/exec"
"runtime"
"strings" "strings"
) )
// Pager предоставляет постраничный вывод. // Pager — простой пейджер.
//
// Все поля читаются/пишутся только из REPL-цикла (последовательный
// доступ), поэтому мьютекс здесь не нужен. Если пейджер начнёт
// использоваться из фоновых горутин — добавьте sync.RWMutex.
type Pager struct { type Pager struct {
enabled bool enabled bool
threshold int threshold int
external string external string
lines int lines int
} }
// NewPager создаёт пейджер. // NewPager создаёт пейджер.
@@ -37,49 +47,40 @@ func NewPager(enabled bool, threshold int) *Pager {
p := &Pager{ p := &Pager{
enabled: enabled, enabled: enabled,
threshold: threshold, threshold: threshold,
lines: 24, external: defaultPagerCommand(),
lines: 0,
} }
p.external = detectPager()
return p return p
} }
// detectPager ищет доступный внешний pager. // Enabled возвращает статус пейджера.
// Порядок: $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 возвращает, включён ли пейджер.
func (p *Pager) Enabled() bool { func (p *Pager) Enabled() bool {
return p != nil && p.enabled return p.enabled
} }
// SetEnabled включает/выключает пейджер. // SetEnabled включает или отключает пейджер.
func (p *Pager) SetEnabled(v bool) { func (p *Pager) SetEnabled(enabled bool) {
if p != nil { p.enabled = enabled
p.enabled = v
}
} }
// SetThreshold устанавливает порог строк. // Threshold возвращает порог включения пейджера (в строках).
// Если вывод короче порога — пейджер не используется.
func (p *Pager) Threshold() int {
return p.threshold
}
// SetThreshold устанавливает порог включения пейджера.
// Значения <= 0 игнорируются, чтобы случайно не сломать вывод.
func (p *Pager) SetThreshold(n int) { func (p *Pager) SetThreshold(n int) {
if p != nil && n > 0 { if n > 0 {
p.threshold = n p.threshold = n
} }
} }
// Page выводит текст через пейджер, если он длиннее порога. // Page отображает текст через пейджер, если он включён и текст достаточно
// длинный. Иначе печатает напрямую в stdout.
func (p *Pager) Page(text string) error { func (p *Pager) Page(text string) error {
if p == nil || !p.enabled { if !p.enabled {
fmt.Print(text) fmt.Print(text)
return nil return nil
} }
@@ -90,106 +91,49 @@ func (p *Pager) Page(text string) error {
return nil return nil
} }
if p.external != "" { if p.external == "" {
if err := p.pageExternal(text); err == nil { // Нет внешнего пейджера — печатаем напрямую.
return nil fmt.Print(text)
} return nil
// fallback на встроенный
} }
return p.pageBuiltin(text) return p.pipeToExternal(text)
} }
// pageExternal запускает внешний pager. // pipeToExternal отправляет текст во внешний пейджер.
func (p *Pager) pageExternal(text string) error { func (p *Pager) pipeToExternal(text string) error {
args := []string{} parts := strings.Fields(p.external)
base := strings.ToLower(p.external) if len(parts) == 0 {
fmt.Print(text)
if strings.Contains(base, "less") { return nil
// -R: сохранять ANSI-цвета, -F: выйти если всё влезает, -X: не чистить экран
args = append(args, "-RFX")
} }
cmd := exec.Command(p.external, args...) cmd := exec.Command(parts[0], parts[1:]...)
cmd.Stdin = bytes.NewBufferString(text)
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
stdin, err := cmd.StdinPipe() if err := cmd.Run(); err != nil {
if err != nil { // Если внешний пейджер упал — печатаем напрямую.
fmt.Print(text)
return err 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 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 ""
}
}