From b3e74de390d30b839eb37764b9be0e897d0e9acb Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 21 Jan 2024 14:47:56 +0530 Subject: [PATCH] More work on pager kitten --- kittens/pager/file_input.go | 103 ++++++++++++++++++++++++++++-------- kittens/pager/main.go | 12 +++-- kittens/pager/main.py | 5 ++ 3 files changed, 96 insertions(+), 24 deletions(-) diff --git a/kittens/pager/file_input.go b/kittens/pager/file_input.go index e48742627..f438452aa 100644 --- a/kittens/pager/file_input.go +++ b/kittens/pager/file_input.go @@ -9,23 +9,41 @@ import ( "io" "os" "strings" + "time" "golang.org/x/sys/unix" ) var _ = fmt.Print -func read_input(input_file *os.File, input_file_name string, input_channel chan<- input_line_struct) { +func wait_for_file_to_grow(file_name string, limit int64) (err error) { + // TODO: Use the fsnotify package to avoid this poll + for { + time.Sleep(time.Second) + s, err := os.Stat(file_name) + if err != nil { + return err + } + if s.Size() > limit { + break + } + } + return +} + +func read_input(input_file *os.File, input_file_name string, input_channel chan<- input_line_struct, follow bool, count_carriage_returns bool) { const buf_capacity = 8192 var buf_array [buf_capacity]byte output_buf := strings.Builder{} output_buf.Grow(buf_capacity) var err error var n int + var total_read int64 + var num_carriage_returns int defer func() { _ = input_file.Close() - last := input_line_struct{line: output_buf.String(), err: err} + last := input_line_struct{line: output_buf.String(), err: err, num_carriage_returns: num_carriage_returns} if errors.Is(err, io.EOF) { last.err = nil } @@ -35,30 +53,73 @@ func read_input(input_file *os.File, input_file_name string, input_channel chan< close(input_channel) }() - process_chunk := func(chunk []byte) { - for len(chunk) > 0 { - idx := bytes.IndexByte(chunk, '\n') - switch idx { - case -1: - _, _ = output_buf.Write(chunk) - chunk = nil - default: - _, _ = output_buf.Write(chunk[idx:]) - chunk = chunk[idx+1:] - input_channel <- input_line_struct{line: output_buf.String()} - output_buf.Reset() - output_buf.Grow(buf_capacity) + var process_chunk func([]byte) + + if count_carriage_returns { + process_chunk = func(chunk []byte) { + for _, ch := range chunk { + switch ch { + case '\r': + num_carriage_returns += 1 + default: + _ = output_buf.WriteByte(ch) + case '\n': + input_channel <- input_line_struct{line: output_buf.String(), num_carriage_returns: num_carriage_returns, is_a_complete_line: true} + num_carriage_returns = 0 + output_buf.Reset() + output_buf.Grow(buf_capacity) + } + } + } + } else { + process_chunk = func(chunk []byte) { + for len(chunk) > 0 { + idx := bytes.IndexByte(chunk, '\n') + switch idx { + case -1: + _, _ = output_buf.Write(chunk) + chunk = nil + default: + _, _ = output_buf.Write(chunk[idx:]) + chunk = chunk[idx+1:] + input_channel <- input_line_struct{line: output_buf.String(), is_a_complete_line: true} + output_buf.Reset() + output_buf.Grow(buf_capacity) + } } } } - for err != nil { - n, err = input_file.Read(buf_array[:]) - if n > 0 { - process_chunk(buf_array[:n]) + for { + for err != nil { + n, err = input_file.Read(buf_array[:]) + if n > 0 { + total_read += int64(n) + process_chunk(buf_array[:n]) + } + if err == unix.EAGAIN || err == unix.EINTR { + err = nil + } } - if err == unix.EAGAIN || err == unix.EINTR { - err = nil + if !follow { + break + } + if errors.Is(err, io.EOF) { + input_file.Close() + if err = wait_for_file_to_grow(input_file_name, total_read); err != nil { + break + } + if input_file, err = os.Open(input_file_name); err != nil { + break + } + var off int64 + if off, err = input_file.Seek(total_read, io.SeekStart); err != nil { + break + } + if off != total_read { + err = fmt.Errorf("Failed to seek in %s to: %d", input_file_name, off) + break + } } } } diff --git a/kittens/pager/main.go b/kittens/pager/main.go index 504a43c25..fcac9190a 100644 --- a/kittens/pager/main.go +++ b/kittens/pager/main.go @@ -25,8 +25,10 @@ var debugprintln = tty.DebugPrintln var _ = debugprintln type input_line_struct struct { - line string - err error + line string + num_carriage_returns int + is_a_complete_line bool + err error } type global_state_struct struct { @@ -59,7 +61,11 @@ func main(_ *cli.Command, opts_ *Options, args []string) (rc int, err error) { } global_state.input_file_name = args[0] } - go read_input(input_file, global_state.input_file_name, input_channel) + follow := global_state.opts.Follow + if follow && global_state.input_file_name == "/dev/stdin" { + follow = false + } + go read_input(input_file, global_state.input_file_name, input_channel, follow, global_state.opts.Role == "scrollback") return } diff --git a/kittens/pager/main.py b/kittens/pager/main.py index 5541fdaae..55d783e18 100644 --- a/kittens/pager/main.py +++ b/kittens/pager/main.py @@ -12,6 +12,11 @@ default=pager choices=pager,scrollback The role the pager is used for. The default is a standard less like pager. + + +--follow +type=bool-set +Follow changes in the specified file, automatically scrolling if currently on the last line. '''.format help_text = '''\