mirror of
https://github.com/peco/peco.git
synced 2026-09-10 07:16:29 -04:00
add follow mode for streaming input
This commit is contained in:
parent
5f6ec1b21c
commit
a66446d2e2
21
README.md
21
README.md
|
|
@ -437,6 +437,26 @@ Without `--height`, peco uses the full terminal screen (default behavior, unchan
|
|||
|
||||
**Note:** In inline mode, peco sets the environment variable `TCELL_ALTSCREEN=disable` to prevent tcell from using the alternate screen buffer, and restores the original value on exit. If peco is killed abnormally (e.g. `SIGKILL`), you may need to unset this variable manually: `unset TCELL_ALTSCREEN`.
|
||||
|
||||
### -f, --follow
|
||||
|
||||
When specified, peco follows streaming input, automatically scrolling to keep the newest lines visible at the bottom of the list, just like `tail -f`. This is useful for live log streams:
|
||||
|
||||
```
|
||||
journalctl -f -n 1000 | peco --follow --layout top-down-query-bottom
|
||||
```
|
||||
|
||||
Moving the cursor manually (e.g. with the arrow keys) turns follow mode off so you can scroll back through the history. The `ToggleFollow` action turns it back on; bind it to a key in your configuration file, for example:
|
||||
|
||||
```json
|
||||
{
|
||||
"Keymap": {
|
||||
"C-f": "peco.ToggleFollow"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Follow mode can also be enabled by default with the `Follow` configuration variable. The `--follow` command line option takes precedence over the configuration file.
|
||||
|
||||
# Configuration File
|
||||
|
||||
peco by default consults a few locations for the config files.
|
||||
|
|
@ -737,6 +757,7 @@ Some keys just... don't map correctly / too easily for various reasons. Here, we
|
|||
| peco.SelectVisible | Selects the all visible line, and save it |
|
||||
| peco.ToggleQuery | Toggle list between filtered by query and not filtered |
|
||||
| peco.ToggleRangeMode | Start selecting by range, or append selecting range to selections |
|
||||
| peco.ToggleFollow | Toggle follow mode (auto-scroll to the newest lines, like tail -f) |
|
||||
| peco.ToggleSelectMode | (DEPRECATED) Alias to ToggleRangeMode |
|
||||
| peco.ToggleSelection | Selects the current line, and saves it |
|
||||
| peco.ToggleSelectionAndSelectNext | Selects the current line, saves it, and proceeds to the next line |
|
||||
|
|
|
|||
23
action.go
23
action.go
|
|
@ -172,6 +172,8 @@ func init() {
|
|||
ActionFunc(doFreezeResults).Register("FreezeResults")
|
||||
ActionFunc(doUnfreezeResults).Register("UnfreezeResults")
|
||||
|
||||
ActionFunc(doToggleFollow).Register("ToggleFollow")
|
||||
|
||||
ActionFunc(doZoomIn).Register("ZoomIn")
|
||||
ActionFunc(doZoomOut).Register("ZoomOut")
|
||||
|
||||
|
|
@ -966,6 +968,27 @@ func doUnfreezeResults(ctx context.Context, state *Peco, _ Event) {
|
|||
state.Hub().SendDrawPrompt(ctx)
|
||||
}
|
||||
|
||||
// doToggleFollow turns follow mode on or off. In follow mode peco auto-scrolls
|
||||
// to keep the newest input lines visible, like "tail -f". Enabling it redraws
|
||||
// so the viewport jumps to the tail immediately.
|
||||
func doToggleFollow(ctx context.Context, state *Peco, _ Event) {
|
||||
if pdebug.Enabled {
|
||||
g := pdebug.Marker("doToggleFollow")
|
||||
defer g.End()
|
||||
}
|
||||
|
||||
follow := state.Follow()
|
||||
enabled := !follow.Enabled()
|
||||
follow.Set(enabled)
|
||||
|
||||
if enabled {
|
||||
state.Hub().SendStatusMsg(ctx, "Follow mode on", 0)
|
||||
state.Hub().SendDraw(ctx, &hub.DrawOptions{DisableCache: true})
|
||||
return
|
||||
}
|
||||
state.Hub().SendStatusMsg(ctx, "Follow mode off", 0)
|
||||
}
|
||||
|
||||
// doZoomIn expands the view to show context lines around matched lines by building
|
||||
// a ContextBuffer from the current filter results and the original source.
|
||||
func doZoomIn(ctx context.Context, state *Peco, _ Event) {
|
||||
|
|
|
|||
16
buffer.go
16
buffer.go
|
|
@ -52,13 +52,23 @@ type ContextLine struct {
|
|||
// NewFilteredBuffer creates a FilteredBuffer containing one page of lines from
|
||||
// the source buffer, computing the maximum column width for horizontal scrolling.
|
||||
func NewFilteredBuffer(src Buffer, page, perPage int) *FilteredBuffer {
|
||||
return newFilteredBufferRange(src, perPage*(page-1), perPage)
|
||||
}
|
||||
|
||||
// newFilteredBufferRange creates a FilteredBuffer holding up to count lines
|
||||
// from the source buffer starting at the given index. This is the shared core
|
||||
// behind both page-aligned cropping (NewFilteredBuffer) and the sliding-window
|
||||
// crop used by follow mode (WindowCrop), where start is not page-aligned.
|
||||
func newFilteredBufferRange(src Buffer, start, count int) *FilteredBuffer {
|
||||
fb := FilteredBuffer{
|
||||
src: src,
|
||||
}
|
||||
|
||||
start := perPage * (page - 1)
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
|
||||
// if for whatever reason we wanted a page that goes over the
|
||||
// if for whatever reason we wanted a range that goes over the
|
||||
// capacity of the original buffer, we don't need to do any more
|
||||
// calculations. bail out
|
||||
if start > src.Size() {
|
||||
|
|
@ -66,7 +76,7 @@ func NewFilteredBuffer(src Buffer, page, perPage int) *FilteredBuffer {
|
|||
}
|
||||
|
||||
// Copy over the selections that are applicable to this filtered buffer.
|
||||
end := min(start+perPage, src.Size())
|
||||
end := min(start+count, src.Size())
|
||||
selection := make([]int, 0, end-start)
|
||||
|
||||
lines := src.linesInRange(start, end)
|
||||
|
|
|
|||
|
|
@ -96,6 +96,11 @@ type Config struct {
|
|||
// Height specifies the display height in lines or percentage (e.g. "10", "50%").
|
||||
// When set, peco renders inline without using the alternate screen buffer.
|
||||
Height string `json:"Height" yaml:"Height"`
|
||||
|
||||
// Follow enables follow mode at startup: peco auto-scrolls to keep the
|
||||
// newest input lines visible, like "tail -f". The --follow CLI flag
|
||||
// overrides this value.
|
||||
Follow bool `json:"Follow" yaml:"Follow"`
|
||||
}
|
||||
|
||||
// SingleKeyJumpConfig holds configuration for single key jump mode.
|
||||
|
|
|
|||
194
follow_test.go
Normal file
194
follow_test.go
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
package peco
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/peco/peco/filter"
|
||||
"github.com/peco/peco/hub"
|
||||
"github.com/peco/peco/line"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// makeFollowBuffer builds a MemoryBuffer with n lines labelled "lineN".
|
||||
func makeFollowBuffer(n int) *MemoryBuffer {
|
||||
mb := NewMemoryBuffer(0)
|
||||
for i := range n {
|
||||
mb.lines = append(mb.lines, line.NewRaw(uint64(i), fmt.Sprintf("line%d", i), false, false))
|
||||
}
|
||||
return mb
|
||||
}
|
||||
|
||||
// readScreenRow returns the text rendered on row y of the simulation screen,
|
||||
// trailing blanks trimmed.
|
||||
func readScreenRow(t *testing.T, s *SimScreen, y int) string {
|
||||
t.Helper()
|
||||
cells, w, _ := s.screen.GetContents()
|
||||
var b strings.Builder
|
||||
for x := range w {
|
||||
c := cells[y*w+x]
|
||||
if len(c.Runes) == 0 || c.Runes[0] == 0 {
|
||||
b.WriteRune(' ')
|
||||
continue
|
||||
}
|
||||
b.WriteRune(c.Runes[0])
|
||||
}
|
||||
return strings.TrimRight(b.String(), " ")
|
||||
}
|
||||
|
||||
func newFollowState(t *testing.T, buf Buffer) (*Peco, *SimScreen) {
|
||||
t.Helper()
|
||||
screen := NewDummyScreen()
|
||||
state := New()
|
||||
state.screen = screen
|
||||
state.Filters().Add(filter.NewIgnoreCase())
|
||||
state.currentLineBuffer = buf
|
||||
return state, screen
|
||||
}
|
||||
|
||||
func TestWindowCrop(t *testing.T) {
|
||||
buf := makeFollowBuffer(100)
|
||||
|
||||
t.Run("tail window", func(t *testing.T) {
|
||||
fb := WindowCrop{offset: 90, perPage: 10}.Crop(buf)
|
||||
require.Equal(t, 10, fb.Size())
|
||||
first, err := fb.LineAt(0)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "line90", first.DisplayString())
|
||||
last, err := fb.LineAt(9)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "line99", last.DisplayString())
|
||||
})
|
||||
|
||||
t.Run("partial window smaller than perPage", func(t *testing.T) {
|
||||
small := makeFollowBuffer(5)
|
||||
fb := WindowCrop{offset: 0, perPage: 10}.Crop(small)
|
||||
require.Equal(t, 5, fb.Size())
|
||||
})
|
||||
|
||||
t.Run("negative offset clamps to zero", func(t *testing.T) {
|
||||
fb := WindowCrop{offset: -5, perPage: 3}.Crop(buf)
|
||||
require.Equal(t, 3, fb.Size())
|
||||
first, err := fb.LineAt(0)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "line0", first.DisplayString())
|
||||
})
|
||||
|
||||
t.Run("offset past end yields empty", func(t *testing.T) {
|
||||
fb := WindowCrop{offset: 200, perPage: 10}.Crop(buf)
|
||||
require.Equal(t, 0, fb.Size())
|
||||
})
|
||||
}
|
||||
|
||||
// TestGHIssue820_FollowKeepsNewestVisible verifies that with follow mode on,
|
||||
// DrawScreen pins the viewport to the tail of the buffer so the newest line
|
||||
// is rendered on the last visible row (like tail -f), regardless of page
|
||||
// alignment.
|
||||
func TestGHIssue820_FollowKeepsNewestVisible(t *testing.T) {
|
||||
buf := makeFollowBuffer(50)
|
||||
state, screen := newFollowState(t, buf)
|
||||
state.Follow().Set(true)
|
||||
|
||||
// top-down-query-bottom is the layout from the issue: list anchored to
|
||||
// the top (row 0), query at the bottom.
|
||||
layout, err := TopDownQueryBottomLayout(state)
|
||||
require.NoError(t, err)
|
||||
|
||||
layout.DrawScreen(state, nil)
|
||||
|
||||
loc := state.Location()
|
||||
perPage := layout.linesPerPage()
|
||||
|
||||
require.Equal(t, 49, loc.LineNumber(), "cursor should be pinned to the newest line")
|
||||
require.Equal(t, 50, loc.Total())
|
||||
require.Equal(t, max(50-perPage, 0), loc.Offset(),
|
||||
"offset should be a sliding window over the tail, not page-aligned")
|
||||
|
||||
// The list area is anchored at row 0; the newest line sits on the last
|
||||
// visible row.
|
||||
lastRow := perPage - 1
|
||||
require.Equal(t, "line49", readScreenRow(t, screen, lastRow),
|
||||
"newest line should be on the bottom row of the list area")
|
||||
}
|
||||
|
||||
// TestFollowDefaultShowsHead verifies that without follow mode the viewport
|
||||
// stays at the head of the buffer (unchanged behavior).
|
||||
func TestFollowDefaultShowsHead(t *testing.T) {
|
||||
buf := makeFollowBuffer(50)
|
||||
state, screen := newFollowState(t, buf)
|
||||
require.False(t, state.IsFollowing())
|
||||
|
||||
layout, err := TopDownQueryBottomLayout(state)
|
||||
require.NoError(t, err)
|
||||
|
||||
layout.DrawScreen(state, nil)
|
||||
|
||||
loc := state.Location()
|
||||
require.Equal(t, 0, loc.LineNumber())
|
||||
require.Equal(t, 0, loc.Offset())
|
||||
require.Equal(t, "line0", readScreenRow(t, screen, 0),
|
||||
"oldest line should be on the top row when not following")
|
||||
}
|
||||
|
||||
// TestFollowDisabledByManualScroll verifies that any manual vertical
|
||||
// navigation turns follow mode off.
|
||||
func TestFollowDisabledByManualScroll(t *testing.T) {
|
||||
buf := makeFollowBuffer(50)
|
||||
state, _ := newFollowState(t, buf)
|
||||
state.Follow().Set(true)
|
||||
|
||||
layout, err := TopDownQueryBottomLayout(state)
|
||||
require.NoError(t, err)
|
||||
// Pin to the tail first.
|
||||
layout.DrawScreen(state, nil)
|
||||
require.True(t, state.IsFollowing())
|
||||
|
||||
moved := layout.MovePage(state, hub.ToLineAbove)
|
||||
require.True(t, moved)
|
||||
require.False(t, state.IsFollowing(),
|
||||
"manual vertical scroll should disable follow mode")
|
||||
}
|
||||
|
||||
// TestToggleFollowAction verifies the ToggleFollow action flips follow state.
|
||||
func TestToggleFollowAction(t *testing.T) {
|
||||
state := New()
|
||||
state.hub = nullHub{}
|
||||
require.False(t, state.IsFollowing())
|
||||
|
||||
doToggleFollow(t.Context(), state, Event{})
|
||||
require.True(t, state.IsFollowing(), "follow should be on after first toggle")
|
||||
|
||||
doToggleFollow(t.Context(), state, Event{})
|
||||
require.False(t, state.IsFollowing(), "follow should be off after second toggle")
|
||||
}
|
||||
|
||||
// TestFollowConfigAndFlag verifies that --follow and the Follow config field
|
||||
// both enable follow mode, with the CLI flag taking precedence.
|
||||
func TestFollowConfigAndFlag(t *testing.T) {
|
||||
t.Run("CLI flag", func(t *testing.T) {
|
||||
p := newPeco()
|
||||
require.NoError(t, p.ApplyConfig(CLIOptions{OptFollow: true}))
|
||||
require.True(t, p.IsFollowing())
|
||||
})
|
||||
|
||||
t.Run("config field", func(t *testing.T) {
|
||||
p := newPeco()
|
||||
p.config.Follow = true
|
||||
require.NoError(t, p.ApplyConfig(CLIOptions{}))
|
||||
require.True(t, p.IsFollowing())
|
||||
})
|
||||
|
||||
t.Run("default off", func(t *testing.T) {
|
||||
p := newPeco()
|
||||
require.NoError(t, p.ApplyConfig(CLIOptions{}))
|
||||
require.False(t, p.IsFollowing())
|
||||
})
|
||||
}
|
||||
|
||||
// TestToggleFollowRegistered verifies the action is wired into the registry so
|
||||
// it can be bound from a keymap.
|
||||
func TestToggleFollowRegistered(t *testing.T) {
|
||||
_, ok := nameToActions["peco.ToggleFollow"]
|
||||
require.True(t, ok, "peco.ToggleFollow should be registered")
|
||||
}
|
||||
66
layout.go
66
layout.go
|
|
@ -328,6 +328,9 @@ func (u UserPrompt) Draw(state *Peco) {
|
|||
|
||||
loc := state.Location()
|
||||
pmsg := fmt.Sprintf("%s [%d (%d/%d)]", state.Filters().Current().String(), loc.Total(), loc.Page(), loc.MaxPage())
|
||||
if state.IsFollowing() {
|
||||
pmsg = "FOLLOW " + pmsg
|
||||
}
|
||||
u.screen.Print(PrintArgs{
|
||||
X: width - runewidth.StringWidth(pmsg),
|
||||
Y: location,
|
||||
|
|
@ -581,16 +584,26 @@ func (l *ListArea) Draw(state *Peco, parent Layout, perPage int, options *hub.Dr
|
|||
|
||||
loc := state.Location()
|
||||
linebuf := state.CurrentLineBuffer()
|
||||
following := state.IsFollowing()
|
||||
|
||||
if options != nil && options.RunningQuery {
|
||||
// While following, the viewport is pinned to the tail of the buffer
|
||||
// (see calculateFollowPage), so the running-query page adjustment must
|
||||
// not move it.
|
||||
if !following && options != nil && options.RunningQuery {
|
||||
adjustPageForRunningQuery(loc, linebuf, parent, state)
|
||||
}
|
||||
|
||||
pf := loc.PageCrop()
|
||||
if pdebug.Enabled {
|
||||
pdebug.Printf("Cropping linebuf which contains %d lines at page %d (%d entries per page)", linebuf.Size(), pf.currentPage, pf.perPage)
|
||||
var buf *FilteredBuffer
|
||||
if following {
|
||||
// Sliding window over the tail so the newest line is on the last row.
|
||||
buf = WindowCrop{offset: loc.Offset(), perPage: loc.PerPage()}.Crop(linebuf)
|
||||
} else {
|
||||
pf := loc.PageCrop()
|
||||
if pdebug.Enabled {
|
||||
pdebug.Printf("Cropping linebuf which contains %d lines at page %d (%d entries per page)", linebuf.Size(), pf.currentPage, pf.perPage)
|
||||
}
|
||||
buf = pf.Crop(linebuf)
|
||||
}
|
||||
buf := pf.Crop(linebuf)
|
||||
bufsiz := buf.Size()
|
||||
|
||||
// This protects us from losing the selected line in case our selected
|
||||
|
|
@ -910,6 +923,39 @@ func (l *BasicLayout) CalculatePage(state *Peco, perPage int) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// calculateFollowPage pins the viewport to the tail of the buffer for follow
|
||||
// mode. Unlike CalculatePage, the offset is a sliding window (Size-perPage)
|
||||
// rather than a page boundary, so the newest line always lands on the last
|
||||
// visible row. The cursor is pinned to the newest line.
|
||||
func (l *BasicLayout) calculateFollowPage(state *Peco, perPage int) error {
|
||||
if pdebug.Enabled {
|
||||
g := pdebug.Marker("BasicLayout.calculateFollowPage %d", perPage)
|
||||
defer g.End()
|
||||
}
|
||||
buf := state.CurrentLineBuffer()
|
||||
loc := state.Location()
|
||||
|
||||
total := buf.Size()
|
||||
loc.SetPerPage(perPage)
|
||||
loc.SetTotal(total)
|
||||
|
||||
if total == 0 {
|
||||
loc.SetOffset(0)
|
||||
loc.SetLineNumber(0)
|
||||
loc.SetPage(1)
|
||||
loc.SetMaxPage(1)
|
||||
// wait for targets
|
||||
return errors.New("no targets or query. nothing to do")
|
||||
}
|
||||
|
||||
loc.SetOffset(max(total-perPage, 0))
|
||||
loc.SetLineNumber(total - 1)
|
||||
loc.SetMaxPage((total + perPage - 1) / perPage)
|
||||
loc.SetPage(loc.MaxPage())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DrawPrompt draws the prompt to the terminal
|
||||
func (l *BasicLayout) DrawPrompt(state *Peco) {
|
||||
l.prompt.Draw(state)
|
||||
|
|
@ -924,7 +970,11 @@ func (l *BasicLayout) DrawScreen(state *Peco, options *hub.DrawOptions) {
|
|||
|
||||
perPage := l.linesPerPage()
|
||||
|
||||
if err := l.CalculatePage(state, perPage); err != nil {
|
||||
calculate := l.CalculatePage
|
||||
if state.IsFollowing() {
|
||||
calculate = l.calculateFollowPage
|
||||
}
|
||||
if err := calculate(state, perPage); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -1097,6 +1147,10 @@ func updateRangeSelection(state *Peco, buf Buffer, loc *Location, lineBefore, lc
|
|||
|
||||
// verticalScroll moves the cursor position vertically
|
||||
func verticalScroll(state *Peco, l *BasicLayout, p hub.PagingRequest) bool {
|
||||
// Manual vertical navigation cancels follow mode so the user can scroll
|
||||
// back through history. Re-enable it with the ToggleFollow action.
|
||||
state.Follow().Set(false)
|
||||
|
||||
loc := state.Location()
|
||||
lineBefore := loc.LineNumber()
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ type CLIOptions struct {
|
|||
OptPrintQuery bool `long:"print-query" description:"print out the current query as first line of output"`
|
||||
OptColor config.ColorMode `long:"color" description:"color mode: 'auto' (default, parse ANSI codes) or 'none' (disable)" default:"auto"`
|
||||
OptHeight string `long:"height" description:"display height in lines or percentage (e.g. '10', '50%')"`
|
||||
OptFollow bool `long:"follow" short:"f" description:"follow streaming input, auto-scrolling to keep the newest lines\nvisible (like 'tail -f'). Manual cursor movement turns this off"`
|
||||
}
|
||||
|
||||
// parse parses command-line arguments and validates the resulting options.
|
||||
|
|
|
|||
14
page.go
14
page.go
|
|
@ -120,3 +120,17 @@ func (l *Location) PageCrop() PageCrop {
|
|||
func (pf PageCrop) Crop(in Buffer) *FilteredBuffer {
|
||||
return NewFilteredBuffer(in, pf.currentPage, pf.perPage)
|
||||
}
|
||||
|
||||
// WindowCrop crops a fixed-size window of lines starting at an explicit
|
||||
// offset rather than a page boundary. Follow mode uses it to show the tail
|
||||
// of the buffer (the newest lines) regardless of page alignment.
|
||||
type WindowCrop struct {
|
||||
offset int
|
||||
perPage int
|
||||
}
|
||||
|
||||
// Crop returns a new Buffer containing up to perPage lines starting at the
|
||||
// window's offset.
|
||||
func (wc WindowCrop) Crop(in Buffer) *FilteredBuffer {
|
||||
return newFilteredBufferRange(in, wc.offset, wc.perPage)
|
||||
}
|
||||
|
|
|
|||
14
peco.go
14
peco.go
|
|
@ -96,6 +96,8 @@ type Peco struct {
|
|||
|
||||
frozen FrozenState
|
||||
|
||||
follow FollowState
|
||||
|
||||
zoom ZoomState
|
||||
|
||||
// cancelFunc is called for Exit()
|
||||
|
|
@ -291,6 +293,16 @@ func (p *Peco) Frozen() *FrozenState {
|
|||
return &p.frozen
|
||||
}
|
||||
|
||||
// Follow returns the follow-mode state.
|
||||
func (p *Peco) Follow() *FollowState {
|
||||
return &p.follow
|
||||
}
|
||||
|
||||
// IsFollowing reports whether follow mode is currently active.
|
||||
func (p *Peco) IsFollowing() bool {
|
||||
return p.follow.Enabled()
|
||||
}
|
||||
|
||||
func (p *Peco) Zoom() *ZoomState {
|
||||
return &p.zoom
|
||||
}
|
||||
|
|
@ -794,6 +806,8 @@ func (p *Peco) ApplyConfig(opts CLIOptions) error {
|
|||
p.selectOneAndExit = opts.OptSelect1
|
||||
p.exitZeroAndExit = opts.OptExitZero
|
||||
p.selectAllAndExit = opts.OptSelectAll
|
||||
// Follow mode: CLI flag overrides config.
|
||||
p.follow.Set(opts.OptFollow || p.config.Follow)
|
||||
p.printQuery = opts.OptPrintQuery
|
||||
p.initialQuery = opts.OptQuery
|
||||
p.initialFilter = opts.OptInitialFilter
|
||||
|
|
|
|||
19
state.go
19
state.go
|
|
@ -2,6 +2,7 @@ package peco
|
|||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
|
@ -75,6 +76,24 @@ func (z *ZoomState) Clear() {
|
|||
z.lineNo = 0
|
||||
}
|
||||
|
||||
// FollowState tracks whether follow mode is active. In follow mode peco
|
||||
// auto-scrolls to keep the newest input lines visible, like "tail -f".
|
||||
// It is toggled at runtime: manual cursor movement turns it off, and the
|
||||
// ToggleFollow action turns it back on.
|
||||
type FollowState struct {
|
||||
enabled atomic.Bool
|
||||
}
|
||||
|
||||
// Enabled reports whether follow mode is currently active.
|
||||
func (f *FollowState) Enabled() bool {
|
||||
return f.enabled.Load()
|
||||
}
|
||||
|
||||
// Set turns follow mode on or off.
|
||||
func (f *FollowState) Set(enabled bool) {
|
||||
f.enabled.Store(enabled)
|
||||
}
|
||||
|
||||
// FrozenState holds a snapshot of filter results when the user
|
||||
// "freezes" the current results to filter on top of them.
|
||||
type FrozenState struct {
|
||||
|
|
|
|||
Loading…
Reference in a new issue