aboutsummaryrefslogtreecommitdiff
path: root/internal/cli/pager.go
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-07-24 09:30:26 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-07-24 09:30:26 +0200
commit9321fd3490b1739e4c84fd229a2cfef244ffdc29 (patch)
treedc747f19d74f06e8bc4741864d007e0cd593e5eb /internal/cli/pager.go
parentb93de95dd24ff2a1d3126e6d7d00cd77530a03bf (diff)
downloadlectio-9321fd3490b1739e4c84fd229a2cfef244ffdc29.tar.gz
lectio-9321fd3490b1739e4c84fd229a2cfef244ffdc29.zip
cli: pager for reading output (-P/--pager, --no-pager, config pager, $PAGER fallback); v0.3.0
Diffstat (limited to 'internal/cli/pager.go')
-rw-r--r--internal/cli/pager.go83
1 files changed, 83 insertions, 0 deletions
diff --git a/internal/cli/pager.go b/internal/cli/pager.go
new file mode 100644
index 0000000..2c7365a
--- /dev/null
+++ b/internal/cli/pager.go
@@ -0,0 +1,83 @@
+package cli
+
+import (
+ "io"
+ "os"
+ "os/exec"
+ "strings"
+
+ "github.com/lukaszkasprzak/lectio/internal/config"
+)
+
+// pagerRequested decides whether the reading-output tail should be paged,
+// given -P/--pager, --no-pager, and cfg.Pager. --no-pager always wins (an
+// explicit "never page" beats everything); otherwise -P forces paging on;
+// otherwise a non-empty cfg.Pager turns paging on by default. Pure and
+// side-effect free -- the actual TTY gate lives in isTerminalWriter.
+func pagerRequested(pagerFlag, noPager bool, cfg config.Config) bool {
+ if noPager {
+ return false
+ }
+ if pagerFlag {
+ return true
+ }
+ return cfg.Pager != ""
+}
+
+// pagerCommand resolves the pager argv to run: cfg.Pager wins, then $PAGER,
+// then the "less -R" built-in default. The resolved command string is split
+// on whitespace via strings.Fields into an argv slice (so "less -R" or a
+// $PAGER value with flags works without a shell).
+func pagerCommand(cfg config.Config) []string {
+ cmd := cfg.Pager
+ if cmd == "" {
+ cmd = os.Getenv("PAGER")
+ }
+ if cmd == "" {
+ cmd = "less -R"
+ }
+ return strings.Fields(cmd)
+}
+
+// isTerminalWriter reports whether w is a real terminal -- only then is
+// paging worth doing. Reuses ttyWidth (termwidth_unix.go/termwidth_other.go)
+// via the *os.File it already knows how to probe; anything else (notably
+// the bytes.Buffer every test uses for stdout) is never a terminal, so
+// paging never triggers in tests, and `lectio | cat` (stdout redirected to
+// a pipe, not a *os.File terminal) writes directly too.
+func isTerminalWriter(w io.Writer) bool {
+ if f, ok := w.(*os.File); ok {
+ return ttyWidth(f) > 0
+ }
+ return false
+}
+
+// startPager launches argv[0] as a pager subprocess wired to the real
+// terminal (stdout/stderr passed through), returning a WriteCloser the
+// caller renders reading output into. ok=false (with out, finish both nil)
+// on any setup failure -- an empty argv, a StdinPipe error, or the pager
+// binary not being found/startable -- so the caller can fall back to
+// writing directly to stdout without crashing.
+func startPager(argv []string, stdout, stderr io.Writer) (out io.WriteCloser, finish func(), ok bool) {
+ if len(argv) == 0 {
+ return nil, nil, false
+ }
+
+ cmd := exec.Command(argv[0], argv[1:]...)
+ cmd.Stdout = stdout
+ cmd.Stderr = stderr
+
+ w, err := cmd.StdinPipe()
+ if err != nil {
+ return nil, nil, false
+ }
+ if err := cmd.Start(); err != nil {
+ return nil, nil, false
+ }
+
+ finish = func() {
+ w.Close()
+ _ = cmd.Wait()
+ }
+ return w, finish, true
+}