Files
futriix/internal/repl/pager.go
T

196 lines
4.2 KiB
Go
Raw Permalink 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.
/*
* 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
}