Files
futriix/internal/repl/pager.go
T

195 lines
4.2 KiB
Go
Raw Normal View History

2026-09-19 22:31:37 +00:00
/*
* Copyright 2026 Safronov Grigorii
*
* Licensed under the CDDL, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* https://opensource.org/licenses/CDDL-1.0
*/
// Файл: internal/repl/pager.go
// Назначение: Пейджер для постраничного вывода длинных результатов.
// Использует внешний pager (less/more) если доступен, иначе встроенный.
// Совместимо с Linux и OpenIndiana.
package repl
import (
"bufio"
"fmt"
"io"
"os"
"os/exec"
"strings"
)
// Pager предоставляет постраничный вывод.
type Pager struct {
enabled bool
threshold int
external string
lines int
}
// NewPager создаёт пейджер.
func NewPager(enabled bool, threshold int) *Pager {
p := &Pager{
enabled: enabled,
threshold: threshold,
lines: 24,
}
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 возвращает, включён ли пейджер.
func (p *Pager) Enabled() bool {
return p != nil && p.enabled
}
// SetEnabled включает/выключает пейджер.
func (p *Pager) SetEnabled(v bool) {
if p != nil {
p.enabled = v
}
}
// SetThreshold устанавливает порог строк.
func (p *Pager) SetThreshold(n int) {
if p != nil && n > 0 {
p.threshold = n
}
}
// Page выводит текст через пейджер, если он длиннее порога.
func (p *Pager) Page(text string) error {
if p == nil || !p.enabled {
fmt.Print(text)
return nil
}
lineCount := strings.Count(text, "\n")
if lineCount < p.threshold {
fmt.Print(text)
return nil
}
if p.external != "" {
if err := p.pageExternal(text); err == nil {
return nil
}
// fallback на встроенный
}
return p.pageBuiltin(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")
}
cmd := exec.Command(p.external, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
stdin, err := cmd.StdinPipe()
if err != nil {
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
}