195 lines
4.2 KiB
Go
195 lines
4.2 KiB
Go
/*
|
||||
|
|
* 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
|
|||
|
|
}
|