Use the system pager to paginate list results

pull/6/head
Mickaël Menu 3 years ago
parent e113707a62
commit b86a74d1e9
No known key found for this signature in database
GPG Key ID: 53D73664CD359895

@ -9,6 +9,8 @@ import (
"github.com/mickael-menu/zk/core/zk"
"github.com/mickael-menu/zk/util/errors"
"github.com/mickael-menu/zk/util/opt"
"github.com/mickael-menu/zk/util/pager"
"github.com/mickael-menu/zk/util/strings"
"github.com/tj/go-naturaldate"
)
@ -26,6 +28,7 @@ type List struct {
ModifiedAfter string `help:"Show only the notes modified after the given date" placeholder:"<date>"`
Exclude []string `help:"Excludes notes matching the given file path pattern from the list" short:"x" placeholder:"<glob>"`
Sort []string `help:"Sort the notes by the given criterion" short:"s" placeholder:"<term>"`
NoPager bool `help:"Do not pipe zk output into a pager" short:"P"`
}
func (cmd *List) Run(container *Container) error {
@ -44,8 +47,10 @@ func (cmd *List) Run(container *Container) error {
return err
}
logger := container.Logger
return db.WithTransaction(func(tx sqlite.Transaction) error {
notes := sqlite.NewNoteDAO(tx, container.Logger)
notes := sqlite.NewNoteDAO(tx, logger)
deps := note.ListDeps{
BasePath: zk.Path,
@ -53,9 +58,20 @@ func (cmd *List) Run(container *Container) error {
Templates: container.TemplateLoader(zk.Config.Lang),
}
count, err := note.List(*opts, deps, printNote)
p := pager.PassthroughPager
if !cmd.NoPager {
p, err = pager.New(logger)
if err != nil {
return err
}
}
count, err := note.List(*opts, deps, p.WriteString)
p.Close()
if err == nil {
fmt.Printf("\nFound %d result(s)\n", count)
fmt.Printf("\nFound %d %s\n", count, strings.Pluralize("result", count))
}
return err
@ -177,11 +193,6 @@ func relPaths(zk *zk.Zk, paths []string) ([]string, bool) {
return relPaths, len(relPaths) > 0
}
func printNote(note string) error {
_, err := fmt.Println(note)
return err
}
func parseDate(date string) (time.Time, error) {
// FIXME: support years
return naturaldate.Parse(date, time.Now().UTC(), naturaldate.WithDirection(naturaldate.Past))

@ -0,0 +1,125 @@
package pager
import (
"fmt"
"io"
"os"
"os/exec"
"strings"
"sync"
"github.com/kballard/go-shellquote"
"github.com/mickael-menu/zk/util"
"github.com/mickael-menu/zk/util/errors"
"github.com/mickael-menu/zk/util/opt"
osutil "github.com/mickael-menu/zk/util/os"
)
// Pager writes text to a TTY using the user's pager.
type Pager struct {
io.WriteCloser
done chan bool
isCloseable bool
closeOnce sync.Once
}
// PassthroughPager is a Pager writing the content directly to stdout without
// pagination.
var PassthroughPager = &Pager{
WriteCloser: os.Stdout,
isCloseable: false,
}
// New creates a pager.Pager to be used to write a paginated text to the TTY.
func New(logger util.Logger) (*Pager, error) {
wrap := errors.Wrapper("failed to paginate the output, try again with --no-pager or fix your PAGER environment variable")
pagerCmd := locatePager()
if pagerCmd.IsNull() {
return PassthroughPager, nil
}
args, err := shellquote.Split(pagerCmd.String())
if err != nil {
return nil, wrap(err)
}
cmd := exec.Command(args[0], args[1:]...)
r, w, err := os.Pipe()
if err != nil {
return nil, wrap(err)
}
cmd.Stdin = r
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
done := make(chan bool)
pager := Pager{
WriteCloser: w,
done: done,
isCloseable: true,
closeOnce: sync.Once{},
}
go func() {
defer close(done)
err := cmd.Run()
if err != nil {
logger.Err(wrap(err))
os.Exit(1)
}
}()
return &pager, nil
}
// Close terminates the pager, waiting for the process to be finished before returning.
//
// We make sure Close is called only once, since we don't know how it is
// implemented in underlying writers.
func (p *Pager) Close() error {
if !p.isCloseable {
return nil
}
var err error
p.closeOnce.Do(func() {
err = p.WriteCloser.Close()
})
<-p.done
return err
}
// WriteString sends the given text to the pager, ending with a newline.
func (p *Pager) WriteString(text string) error {
_, err := fmt.Fprintln(p, text)
return err
}
func locatePager() opt.String {
return osutil.GetOptEnv("ZK_PAGER").
Or(osutil.GetOptEnv("PAGER")).
Or(locateDefaultPager())
}
var defaultPagers = []string{
"less -FIRX", "more -R",
}
func locateDefaultPager() opt.String {
for _, pager := range defaultPagers {
parts, err := shellquote.Split(pager)
if err != nil {
continue
}
pager, err := exec.LookPath(parts[0])
parts[0] = pager
if err == nil {
return opt.NewNotEmptyString(strings.Join(parts, " "))
}
}
return opt.NullString
}

@ -16,3 +16,12 @@ func Prepend(text string, prefix string) string {
return strings.Join(append([]string{""}, lines...), prefix)
}
// Pluralize adds an `s` at the end of word if the count is more than 1.
func Pluralize(word string, count int) string {
if word == "" || (count >= -1 && count <= 1) {
return word
} else {
return word + "s"
}
}

@ -17,3 +17,18 @@ func TestPrepend(t *testing.T) {
test("One line\nTwo lines\nThree lines", "> ", "> One line\n> Two lines\n> Three lines")
test("Newline\n", "> ", "> Newline\n")
}
func TestPluralize(t *testing.T) {
test := func(word string, count int, expected string) {
assert.Equal(t, Pluralize(word, count), expected)
}
test("", 1, "")
test("", 2, "")
test("word", -2, "words")
test("word", -1, "word")
test("word", 0, "word")
test("word", 1, "word")
test("word", 2, "words")
test("word", 1000, "words")
}

Loading…
Cancel
Save