diff --git a/action.go b/action.go index a6ab7d4..989bd6e 100644 --- a/action.go +++ b/action.go @@ -52,6 +52,7 @@ func (a ActionFunc) Execute(ctx context.Context, state *Peco, e Event) { a(ctx, state, e) } +// registerKeySequence registers a key sequence in the default key binding map for action dispatch. func (a ActionFunc) registerKeySequence(k keyseq.KeyList) { defaultKeyBinding[k.String()] = a } @@ -74,6 +75,7 @@ func (a ActionFunc) RegisterKeySequence(name string, k keyseq.KeyList) { a.registerKeySequence(k) } +// wrapDeprecated wraps an action function to emit a deprecation warning before executing it. func wrapDeprecated(fn func(context.Context, *Peco, Event), oldName, newName string) ActionFunc { return ActionFunc(func(ctx context.Context, state *Peco, e Event) { state.Hub().SendStatusMsg(ctx, fmt.Sprintf("%s is deprecated. Use %s", oldName, newName), 0) @@ -231,6 +233,7 @@ func doAcceptChar(ctx context.Context, state *Peco, e Event) { state.ExecQuery(ctx, state.selectOneCallback()) } +// doRotateFilter cycles to the next filter in the configured filter set and re-runs the query. func doRotateFilter(ctx context.Context, state *Peco, _ Event) { if pdebug.Enabled { g := pdebug.Marker("doRotateFilter") @@ -243,6 +246,7 @@ func doRotateFilter(ctx context.Context, state *Peco, _ Event) { execQueryAndDraw(ctx, state) } +// doBackToInitialFilter resets the filter back to the one configured at startup and re-runs the query. func doBackToInitialFilter(ctx context.Context, state *Peco, _ Event) { if pdebug.Enabled { g := pdebug.Marker("doBackToInitialFilter") @@ -255,6 +259,7 @@ func doBackToInitialFilter(ctx context.Context, state *Peco, _ Event) { execQueryAndDraw(ctx, state) } +// doToggleSelection toggles the selection state of the line at the current cursor position. func doToggleSelection(_ context.Context, state *Peco, _ Event) { if pdebug.Enabled { g := pdebug.Marker("doToggleSelection") @@ -274,6 +279,7 @@ func doToggleSelection(_ context.Context, state *Peco, _ Event) { selection.Add(l) } +// doToggleRangeMode enables or disables range selection mode, anchoring or clearing the range start. func doToggleRangeMode(_ context.Context, state *Peco, _ Event) { if pdebug.Enabled { g := pdebug.Marker("doToggleRangeMode") @@ -292,15 +298,18 @@ func doToggleRangeMode(_ context.Context, state *Peco, _ Event) { } } +// doCancelRangeMode exits range selection mode without modifying the current selections. func doCancelRangeMode(_ context.Context, state *Peco, _ Event) { state.SelectionRangeStart().Reset() } +// doSelectNone deselects all currently selected lines and redraws the screen. func doSelectNone(ctx context.Context, state *Peco, _ Event) { state.Selection().Reset() state.Hub().SendDraw(ctx, &hub.DrawOptions{DisableCache: true}) } +// doSelectAll selects every line in the current line buffer. func doSelectAll(ctx context.Context, state *Peco, _ Event) { selection := state.Selection() b := state.CurrentLineBuffer() @@ -314,6 +323,7 @@ func doSelectAll(ctx context.Context, state *Peco, _ Event) { state.Hub().SendDraw(ctx, nil) } +// doSelectVisible selects all lines currently visible on screen within the page crop. func doSelectVisible(ctx context.Context, state *Peco, _ Event) { if pdebug.Enabled { g := pdebug.Marker("doSelectVisible") @@ -343,6 +353,8 @@ func (err collectResultsError) Error() string { func (err collectResultsError) CollectResults() bool { return true } +// doFinish completes the peco session. If execOnFinish is set, it runs the configured +// command with the selected lines as stdin; otherwise it exits with a collect-results signal. func doFinish(ctx context.Context, state *Peco, _ Event) { if pdebug.Enabled { g := pdebug.Marker("doFinish") @@ -418,6 +430,8 @@ func doFinish(ctx context.Context, state *Peco, _ Event) { } } +// doCancel cancels the current operation: a pending key sequence, range mode, or the entire +// peco session. The exit status depends on the OnCancel configuration. func doCancel(ctx context.Context, state *Peco, e Event) { km := state.Keymap() @@ -445,6 +459,8 @@ func batchAction(ctx context.Context, state *Peco, fn func(context.Context)) { state.Hub().Batch(ctx, fn) } +// doToggleSelectionAndSelectNext toggles the selection of the current line, then moves +// the cursor to the next line (direction depends on layout orientation). func doToggleSelectionAndSelectNext(ctx context.Context, state *Peco, e Event) { batchAction(ctx, state, func(ctx context.Context) { doToggleSelection(ctx, state, e) @@ -456,6 +472,7 @@ func doToggleSelectionAndSelectNext(ctx context.Context, state *Peco, e Event) { }) } +// doInvertSelection inverts the selection state of every line in the current buffer. func doInvertSelection(ctx context.Context, state *Peco, _ Event) { if pdebug.Enabled { g := pdebug.Marker("doInvertSelection") @@ -481,6 +498,8 @@ func doInvertSelection(ctx context.Context, state *Peco, _ Event) { state.Hub().SendDraw(ctx, nil) } +// doDeleteBackwardWord deletes the word before the cursor in the query, handling +// whitespace boundaries, then re-runs the query. func doDeleteBackwardWord(ctx context.Context, state *Peco, _ Event) { if pdebug.Enabled { g := pdebug.Marker("doDeleteBackwardWord") @@ -521,6 +540,7 @@ func doDeleteBackwardWord(ctx context.Context, state *Peco, _ Event) { execQueryAndDraw(ctx, state) } +// doForwardWord moves the cursor forward to the beginning of the next word in the query. func doForwardWord(ctx context.Context, state *Peco, _ Event) { if state.Caret().Pos() >= state.Query().Len() { return @@ -548,6 +568,7 @@ func doForwardWord(ctx context.Context, state *Peco, _ Event) { c.SetPos(q.Len()) } +// doBackwardWord moves the cursor backward to the beginning of the previous word in the query. func doBackwardWord(ctx context.Context, state *Peco, _ Event) { c := state.Caret() q := state.Query() @@ -594,6 +615,7 @@ func doBackwardWord(ctx context.Context, state *Peco, _ Event) { c.SetPos(0) } +// doForwardChar moves the cursor one character forward in the query. func doForwardChar(ctx context.Context, state *Peco, _ Event) { c := state.Caret() if c.Pos() >= state.Query().Len() { @@ -603,6 +625,7 @@ func doForwardChar(ctx context.Context, state *Peco, _ Event) { state.Hub().SendDrawPrompt(ctx) } +// doBackwardChar moves the cursor one character backward in the query. func doBackwardChar(ctx context.Context, state *Peco, _ Event) { c := state.Caret() if c.Pos() <= 0 { @@ -612,6 +635,7 @@ func doBackwardChar(ctx context.Context, state *Peco, _ Event) { state.Hub().SendDrawPrompt(ctx) } +// doDeleteForwardWord deletes the word (or whitespace run) after the cursor in the query. func doDeleteForwardWord(ctx context.Context, state *Peco, _ Event) { c := state.Caret() q := state.Query() @@ -645,16 +669,20 @@ func doDeleteForwardWord(ctx context.Context, state *Peco, _ Event) { execQueryAndDraw(ctx, state) } +// doBeginningOfLine moves the cursor to the beginning of the query line. func doBeginningOfLine(ctx context.Context, state *Peco, _ Event) { state.Caret().SetPos(0) state.Hub().SendDrawPrompt(ctx) } +// doEndOfLine moves the cursor to the end of the query line. func doEndOfLine(ctx context.Context, state *Peco, _ Event) { state.Caret().SetPos(state.Query().Len()) state.Hub().SendDrawPrompt(ctx) } +// doEndOfFile deletes the character at the cursor if the query is non-empty, or cancels +// the session if the query is empty (similar to Ctrl-D behavior in a shell). func doEndOfFile(ctx context.Context, state *Peco, e Event) { if state.Query().Len() > 0 { doDeleteForwardChar(ctx, state, e) @@ -663,6 +691,7 @@ func doEndOfFile(ctx context.Context, state *Peco, e Event) { } } +// doKillBeginningOfLine deletes all text from the cursor to the beginning of the query. func doKillBeginningOfLine(ctx context.Context, state *Peco, _ Event) { q := state.Query() q.DeleteRange(0, state.Caret().Pos()) @@ -670,6 +699,7 @@ func doKillBeginningOfLine(ctx context.Context, state *Peco, _ Event) { execQueryAndDraw(ctx, state) } +// doKillEndOfLine deletes all text from the cursor to the end of the query. func doKillEndOfLine(ctx context.Context, state *Peco, _ Event) { if state.Query().Len() <= state.Caret().Pos() { return @@ -680,11 +710,13 @@ func doKillEndOfLine(ctx context.Context, state *Peco, _ Event) { execQueryAndDraw(ctx, state) } +// doDeleteAll clears the entire query string and re-runs the query. func doDeleteAll(ctx context.Context, state *Peco, _ Event) { state.Query().Reset() state.ExecQuery(ctx, state.selectOneCallback()) } +// doDeleteForwardChar deletes the character at the current cursor position in the query. func doDeleteForwardChar(ctx context.Context, state *Peco, _ Event) { q := state.Query() c := state.Caret() @@ -698,6 +730,7 @@ func doDeleteForwardChar(ctx context.Context, state *Peco, _ Event) { execQueryAndDraw(ctx, state) } +// doDeleteBackwardChar deletes the character immediately before the cursor in the query. func doDeleteBackwardChar(ctx context.Context, state *Peco, _ Event) { if pdebug.Enabled { g := pdebug.Marker("doDeleteBackwardChar") @@ -734,10 +767,12 @@ func doDeleteBackwardChar(ctx context.Context, state *Peco, _ Event) { execQueryAndDraw(ctx, state) } +// doRefreshScreen forces a full screen redraw with cache disabled and synchronous rendering. func doRefreshScreen(ctx context.Context, state *Peco, _ Event) { state.Hub().SendDraw(ctx, &hub.DrawOptions{DisableCache: true, ForceSync: true}) } +// doToggleQuery swaps between the current query and the last saved query, then re-runs the filter. func doToggleQuery(ctx context.Context, state *Peco, _ Event) { if pdebug.Enabled { g := pdebug.Marker("doToggleQuery") @@ -754,10 +789,12 @@ func doToggleQuery(ctx context.Context, state *Peco, _ Event) { execQueryAndDraw(ctx, state) } +// doKonamiCommand is an easter egg triggered by the Konami code key sequence. func doKonamiCommand(ctx context.Context, state *Peco, _ Event) { state.Hub().SendStatusMsg(ctx, "All your filters are belongs to us", 0) } +// doToggleSingleKeyJump toggles single-key-jump mode on or off. func doToggleSingleKeyJump(ctx context.Context, state *Peco, _ Event) { if pdebug.Enabled { g := pdebug.Marker("doToggleSingleKeyJump") @@ -766,6 +803,8 @@ func doToggleSingleKeyJump(ctx context.Context, state *Peco, _ Event) { state.ToggleSingleKeyJumpMode(ctx) } +// doToggleViewAround clears the query and jumps to the current line's position in the +// unfiltered source, effectively toggling between filtered and context views. func doToggleViewAround(ctx context.Context, state *Peco, e Event) { if pdebug.Enabled { g := pdebug.Marker("doToggleViewAround") @@ -785,6 +824,7 @@ func doToggleViewAround(ctx context.Context, state *Peco, e Event) { } } +// doGoToNextSelection moves the cursor to the next selected line, wrapping around if needed. func doGoToNextSelection(ctx context.Context, state *Peco, _ Event) { if pdebug.Enabled { g := pdebug.Marker("doGoToNextSelection") @@ -793,6 +833,7 @@ func doGoToNextSelection(ctx context.Context, state *Peco, _ Event) { doGoToAdjacentSelection(ctx, state, true) } +// doGoToPreviousSelection moves the cursor to the previous selected line, wrapping around if needed. func doGoToPreviousSelection(ctx context.Context, state *Peco, _ Event) { if pdebug.Enabled { g := pdebug.Marker("doGoToPreviousSelection") @@ -881,6 +922,8 @@ func resetQueryState(state *Peco) { } } +// doFreezeResults snapshots the current result set into a frozen buffer, preventing +// further filtering until unfrozen. func doFreezeResults(ctx context.Context, state *Peco, _ Event) { if pdebug.Enabled { g := pdebug.Marker("doFreezeResults") @@ -908,6 +951,7 @@ func doFreezeResults(ctx context.Context, state *Peco, _ Event) { state.Hub().SendDrawPrompt(ctx) } +// doUnfreezeResults restores live filtering by clearing the frozen buffer and resetting the query. func doUnfreezeResults(ctx context.Context, state *Peco, _ Event) { if pdebug.Enabled { g := pdebug.Marker("doUnfreezeResults") @@ -926,6 +970,8 @@ func doUnfreezeResults(ctx context.Context, state *Peco, _ Event) { state.Hub().SendDrawPrompt(ctx) } +// 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) { if pdebug.Enabled { g := pdebug.Marker("doZoomIn") @@ -974,6 +1020,7 @@ func doZoomIn(ctx context.Context, state *Peco, _ Event) { state.Hub().SendDraw(ctx, &hub.DrawOptions{DisableCache: true}) } +// doZoomOut restores the pre-zoom line buffer, collapsing the expanded context view. func doZoomOut(ctx context.Context, state *Peco, _ Event) { if pdebug.Enabled { g := pdebug.Marker("doZoomOut") @@ -996,6 +1043,7 @@ func doZoomOut(ctx context.Context, state *Peco, _ Event) { state.Hub().SendDraw(ctx, &hub.DrawOptions{DisableCache: true}) } +// doSingleKeyJump looks up the line index for the pressed key and jumps to it, then finishes. func doSingleKeyJump(ctx context.Context, state *Peco, e Event) { if pdebug.Enabled { g := pdebug.Marker("doSingleKeyJump %c", e.Ch) @@ -1013,6 +1061,7 @@ func doSingleKeyJump(ctx context.Context, state *Peco, e Event) { }) } +// makeCombinedAction creates a composite action that executes multiple actions sequentially in a batch. func makeCombinedAction(actions ...Action) ActionFunc { return ActionFunc(func(ctx context.Context, state *Peco, e Event) { batchAction(ctx, state, func(ctx context.Context) { diff --git a/buffer.go b/buffer.go index f94b65d..04c2738 100644 --- a/buffer.go +++ b/buffer.go @@ -49,6 +49,8 @@ type ContextLine struct { line.Line } +// 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 { fb := FilteredBuffer{ src: src, @@ -118,12 +120,15 @@ func NewMemoryBuffer(capacity int) *MemoryBuffer { return mb } +// Size returns the number of lines currently held in the buffer, thread-safe. func (mb *MemoryBuffer) Size() int { mb.mutex.RLock() defer mb.mutex.RUnlock() return len(mb.lines) } +// Reset clears the buffer, reinitializing the done channel and lines slice +// so the buffer can be reused for a new pipeline run. func (mb *MemoryBuffer) Reset() { mb.mutex.Lock() defer mb.mutex.Unlock() @@ -148,12 +153,16 @@ func (mb *MemoryBuffer) MarkComplete() { }) } +// Done returns a channel that is closed when the buffer has been fully populated. func (mb *MemoryBuffer) Done() <-chan struct{} { mb.mutex.RLock() defer mb.mutex.RUnlock() return mb.done } +// Accept receives lines from a pipeline input channel and appends them to the +// buffer in batches. It marks the buffer complete when the channel closes or +// the context is cancelled. func (mb *MemoryBuffer) Accept(ctx context.Context, in <-chan line.Line, _ pipeline.ChanOutput) { if pdebug.Enabled { g := pdebug.Marker("MemoryBuffer.Accept") @@ -226,18 +235,22 @@ func (mb *MemoryBuffer) AppendLine(l line.Line) { mb.mutex.Unlock() } +// LineAt returns the line at the given index, thread-safe. func (mb *MemoryBuffer) LineAt(n int) (line.Line, error) { mb.mutex.RLock() defer mb.mutex.RUnlock() return bufferLineAt(mb.lines, n) } +// linesInRange returns a slice of lines between start and end indices, thread-safe. func (mb *MemoryBuffer) linesInRange(start, end int) []line.Line { mb.mutex.RLock() defer mb.mutex.RUnlock() return mb.lines[start:end] } +// bufferLineAt is a shared helper that retrieves a line by index from a raw +// line slice, returning an error if the index is out of bounds. func bufferLineAt(lines []line.Line, n int) (line.Line, error) { if s := len(lines); s <= 0 || n >= s { return nil, errors.New("empty buffer") @@ -372,6 +385,7 @@ func (cb *ContextBuffer) LineAt(i int) (line.Line, error) { return cb.entries[i], nil } +// linesInRange returns a slice of entries between start and end indices. func (cb *ContextBuffer) linesInRange(start, end int) []line.Line { return cb.entries[start:end] } diff --git a/caret.go b/caret.go index 631b1e1..b4c14c5 100644 --- a/caret.go +++ b/caret.go @@ -8,22 +8,27 @@ type Caret struct { pos int } +// Pos returns the current caret position, thread-safe. func (c *Caret) Pos() int { c.mutex.Lock() defer c.mutex.Unlock() return c.pos } +// setPosNL sets the caret position without acquiring the mutex. +// The caller must already hold the lock. func (c *Caret) setPosNL(p int) { c.pos = p } +// SetPos sets the caret position, thread-safe. func (c *Caret) SetPos(p int) { c.mutex.Lock() defer c.mutex.Unlock() c.setPosNL(p) } +// Move moves the caret by the given delta, thread-safe. func (c *Caret) Move(diff int) { c.mutex.Lock() defer c.mutex.Unlock() diff --git a/config.go b/config.go index 0f717d0..1080869 100644 --- a/config.go +++ b/config.go @@ -21,6 +21,8 @@ const ( OnCancelError OnCancelBehavior = "error" ) +// UnmarshalText parses a text value into an OnCancelBehavior, accepting +// "success" (or empty) and "error" as valid values. func (o *OnCancelBehavior) UnmarshalText(b []byte) error { switch s := string(b); s { case "", "success": @@ -224,6 +226,8 @@ func NewStyleSet() *StyleSet { return ss } +// Init initializes the StyleSet with default foreground and background colors +// for each UI element (basic, query, matched, selected, prompt, context, etc.). func (ss *StyleSet) Init() { ss.Basic.fg = ColorDefault ss.Basic.bg = ColorDefault @@ -259,6 +263,8 @@ func (s *Style) UnmarshalYAML(unmarshal func(any) error) error { return stringsToStyle(s, raw) } +// stringsToStyle parses an array of color and attribute strings (e.g. "red", +// "on_blue", "bold", "#ff00ff") into a Style's foreground and background Attributes. func stringsToStyle(style *Style, raw []string) error { style.fg = ColorDefault style.bg = ColorDefault @@ -312,6 +318,8 @@ type configLocateFunc func(string) (string, error) var configFilenames = []string{"config.json", "config.yaml", "config.yml"} +// locateRcfileIn searches the given directory for a config file with one of +// the known filenames (config.json, config.yaml, config.yml). func locateRcfileIn(dir string) (string, error) { for _, basename := range configFilenames { file := filepath.Join(dir, basename) diff --git a/filter.go b/filter.go index aaba12b..0caf6c5 100644 --- a/filter.go +++ b/filter.go @@ -32,6 +32,8 @@ type filterProcessor struct { onError func(error) } +// newFilterProcessor creates a filterProcessor that runs the given filter +// against a query, managing result buffering and error reporting. func newFilterProcessor(f filter.Filter, q string, bufSize int, onError func(error)) *filterProcessor { return &filterProcessor{ filter: f, @@ -41,6 +43,7 @@ func newFilterProcessor(f filter.Filter, q string, bufSize int, onError func(err } } +// Accept receives lines from the pipeline, applies the filter, and forwards matches. func (fp *filterProcessor) Accept(ctx context.Context, in <-chan line.Line, out pipeline.ChanOutput) { acceptAndFilter(ctx, fp.filter, fp.bufSize, fp.onError, in, out) } @@ -235,6 +238,8 @@ func AcceptAndFilter(ctx context.Context, f filter.Filter, configBufSize int, in acceptAndFilter(ctx, f, configBufSize, nil, in, out) } +// acceptAndFilter is the core filtering loop: it reads lines from in, batches +// them, applies the filter function, and buffers matches for output. func acceptAndFilter(ctx context.Context, f filter.Filter, configBufSize int, onError func(error), in <-chan line.Line, out pipeline.ChanOutput) { useParallel := f.SupportsParallel() && runtime.GOMAXPROCS(0) > 1 @@ -255,6 +260,8 @@ func acceptAndFilter(ctx context.Context, f filter.Filter, configBufSize int, on } } +// acceptAndFilterSerial runs the filter in a single goroutine, used as the +// fallback when the filter does not support parallel execution. func acceptAndFilterSerial(ctx context.Context, f filter.Filter, bufsiz int, buf []line.Line, onError func(error), in <-chan line.Line, out pipeline.ChanOutput) { flush := make(chan []line.Line) flushDone := make(chan struct{}) @@ -265,6 +272,8 @@ func acceptAndFilterSerial(ctx context.Context, f filter.Filter, bufsiz int, buf batchAndFlush(ctx, bufsiz, buf, in, flush, func(b []line.Line) []line.Line { return b }) } +// acceptAndFilterParallel distributes filter work across multiple goroutines, +// tagging each batch with a sequence number for ordered result merging. func acceptAndFilterParallel(ctx context.Context, f filter.Filter, bufsiz int, buf []line.Line, onError func(error), in <-chan line.Line, out pipeline.ChanOutput) { flush := make(chan orderedChunk) flushDone := make(chan struct{}) @@ -324,6 +333,7 @@ func batchAndFlush[T any](ctx context.Context, bufsiz int, buf []line.Line, in < } } +// NewFilter creates a new Filter bound to the given Peco state. func NewFilter(state *Peco) *Filter { return &Filter{ state: state, diff --git a/filter/base.go b/filter/base.go index 6b26f47..76ceedb 100644 --- a/filter/base.go +++ b/filter/base.go @@ -14,6 +14,7 @@ type baseFilter struct { applyFn func(ctx context.Context, lines []line.Line, emit func(line.Line)) error } +// NewContext returns a context initialized with the given query for pipeline use. func (b *baseFilter) NewContext(ctx context.Context, query string) context.Context { return newContext(ctx, query) } @@ -22,12 +23,15 @@ func (b *baseFilter) BufSize() int { return 0 } +// Apply runs the filter's matching logic on lines, sending matches to out. func (b *baseFilter) Apply(ctx context.Context, lines []line.Line, out pipeline.ChanOutput) error { return b.applyFn(ctx, lines, func(l line.Line) { _ = out.Send(ctx, l) }) } +// ApplyCollect runs the filter and returns matched lines directly as a slice, +// bypassing channel-based output for better performance in parallel paths. func (b *baseFilter) ApplyCollect(ctx context.Context, lines []line.Line) ([]line.Line, error) { result := make([]line.Line, 0, len(lines)/2) err := b.applyFn(ctx, lines, func(l line.Line) { diff --git a/filter/external.go b/filter/external.go index 4dffc67..ba8fdda 100644 --- a/filter/external.go +++ b/filter/external.go @@ -56,6 +56,7 @@ func (ecf ExternalCmd) BufSize() int { return ecf.thresholdBufsiz } +// NewContext returns a context initialized with the given query for pipeline use. func (ecf *ExternalCmd) NewContext(ctx context.Context, query string) context.Context { return newContext(ctx, query) } @@ -68,6 +69,8 @@ func (ecf ExternalCmd) String() string { return ecf.name } +// Apply pipes the buffered lines through the external command and sends +// matching output lines to out. func (ecf *ExternalCmd) Apply(ctx context.Context, buf []line.Line, out pipeline.ChanOutput) (err error) { var readerPanicErr error diff --git a/filter/filter.go b/filter/filter.go index cdbfcd3..ff1c4ab 100644 --- a/filter/filter.go +++ b/filter/filter.go @@ -80,15 +80,19 @@ func (m byMatchStart) Less(i, j int) bool { return false } +// matchContains reports whether match range a fully contains match range b. func matchContains(a []int, b []int) bool { return a[0] <= b[0] && a[1] >= b[1] } +// matchOverlaps reports whether two match ranges overlap. func matchOverlaps(a []int, b []int) bool { return a[0] <= b[0] && a[1] >= b[0] || a[0] <= b[1] && a[1] >= b[1] } +// mergeMatches combines two overlapping match ranges into a single range +// spanning both. func mergeMatches(a []int, b []int) []int { ret := make([]int, 2) diff --git a/filter/fuzzy.go b/filter/fuzzy.go index 8e3d1bc..3bdfb26 100644 --- a/filter/fuzzy.go +++ b/filter/fuzzy.go @@ -46,6 +46,8 @@ func (ff Fuzzy) String() string { return "Fuzzy" } +// applyInternal performs fuzzy matching on each line, emitting matches with +// their character-level match indices for highlighting. func (ff *Fuzzy) applyInternal(ctx context.Context, lines []line.Line, emit func(line.Line)) error { originalQuery := pipeline.QueryFromContext(ctx) @@ -183,11 +185,15 @@ LINE: return nil } +// popRune decodes and removes the first rune from s, returning the remainder, +// the decoded rune, and its byte width. func popRune(s string) (string, rune, int) { r, n := utf8.DecodeRuneInString(s) return s[n:], r, n } +// less returns a comparison function that orders fuzzy matches by longest +// contiguous match, earliest position, then shortest line length. func less(s []fuzzyMatchedItem) func(i, j int) bool { return func(i, j int) bool { if s[i].longest != s[j].longest { @@ -210,6 +216,8 @@ type fuzzyMatchedItem struct { earliest int } +// newFuzzyMatchedItem creates a fuzzyMatchedItem, computing the longest +// contiguous match length and earliest match position from the given indices. func newFuzzyMatchedItem(line line.Line, matches [][]int) fuzzyMatchedItem { longest := 0 count := 0 diff --git a/filter/regexp.go b/filter/regexp.go index ca827a1..bc7a319 100644 --- a/filter/regexp.go +++ b/filter/regexp.go @@ -55,6 +55,8 @@ func (r regexpFlagFunc) flags(s string) []string { return r(s) } +// regexpFor compiles q into a regexp, optionally quoting meta characters +// and prepending inline flags (e.g. case-insensitive). func regexpFor(q string, flags []string, quotemeta bool) (*regexp.Regexp, error) { reTxt := q if quotemeta { @@ -141,6 +143,8 @@ func NewIRegexp() *Regexp { const maxRegexpCacheSize = 100 +// Compile parses the query string into positive and negative regexp slices, +// caching compiled results for reuse within the expiry threshold. func (f *regexpQueryFactory) Compile(s string, flags regexpFlags, quotemeta bool) (positive, negative []*regexp.Regexp, err error) { f.mutex.Lock() defer f.mutex.Unlock() @@ -190,6 +194,8 @@ func (f *regexpQueryFactory) Compile(s string, flags regexpFlags, quotemeta bool return posRxs, negRxs, nil } +// applyInternal matches each line against the compiled positive and negative +// regexps, deduplicating overlapping match ranges before emitting results. func (rf *Regexp) applyInternal(ctx context.Context, lines []line.Line, emit func(line.Line)) error { query := pipeline.QueryFromContext(ctx) posRegexps, negRegexps, err := rf.factory.Compile(query, rf.flags, rf.quotemeta) @@ -272,10 +278,12 @@ func (rf *Regexp) String() string { return rf.name } +// NewIgnoreCase creates a case-insensitive literal string filter. func NewIgnoreCase() *Regexp { return newRegexpFilter("IgnoreCase", ignoreCaseFlags, true) } +// NewCaseSensitive creates a case-sensitive literal string filter. func NewCaseSensitive() *Regexp { return newRegexpFilter("CaseSensitive", defaultFlags, true) } diff --git a/filter/set.go b/filter/set.go index 51ceab9..c6eb955 100644 --- a/filter/set.go +++ b/filter/set.go @@ -19,24 +19,28 @@ type Set struct { mutex sync.Mutex } +// Reset sets the active filter back to the first one in the set. func (fs *Set) Reset() { fs.mutex.Lock() defer fs.mutex.Unlock() fs.current = 0 } +// Size returns the number of filters in the set. func (fs *Set) Size() int { fs.mutex.Lock() defer fs.mutex.Unlock() return len(fs.filters) } +// Add appends a new filter to the set. func (fs *Set) Add(lf Filter) { fs.mutex.Lock() defer fs.mutex.Unlock() fs.filters = append(fs.filters, lf) } +// Rotate cycles to the next filter in the set, wrapping around to the first. func (fs *Set) Rotate() { fs.mutex.Lock() defer fs.mutex.Unlock() @@ -49,6 +53,8 @@ func (fs *Set) Rotate() { } } +// SetCurrentByName switches the active filter to the one matching the given +// name, returning ErrFilterNotFound if no filter matches. func (fs *Set) SetCurrentByName(name string) error { fs.mutex.Lock() defer fs.mutex.Unlock() diff --git a/hub/hub.go b/hub/hub.go index 0fdc0bc..a94d1d7 100644 --- a/hub/hub.go +++ b/hub/hub.go @@ -113,6 +113,7 @@ var doneChPool = sync.Pool{ }, } +// waitDone blocks until the receiver signals completion by calling Done. func (p *Payload[T]) waitDone() { // Save the channel reference before blocking. This read is safe because // p.done was set by send() on this same goroutine before the payload @@ -129,6 +130,7 @@ func (p *Payload[T]) waitDone() { doneChPool.Put(ch) } +// isBatchCtx reports whether the context was created by a Batch call. func isBatchCtx(ctx context.Context) bool { var isBatchMode bool v := ctx.Value(batchPayloadKey{}) @@ -223,6 +225,7 @@ func (r statusMsgReq) Delay() time.Duration { return r.delay } +// newStatusMsgReq creates a StatusMsg with the given message text and display duration. func newStatusMsgReq(s string, d time.Duration) *statusMsgReq { return &statusMsgReq{ msg: s, diff --git a/input.go b/input.go index cca6922..1e9dc4a 100644 --- a/input.go +++ b/input.go @@ -13,6 +13,7 @@ type Input struct { state *Peco } +// NewInput creates and returns a new Input instance for handling keyboard events. func NewInput(state *Peco, am ActionMap, src chan Event) *Input { return &Input{ actions: am, @@ -21,6 +22,7 @@ func NewInput(state *Peco, am ActionMap, src chan Event) *Input { } } +// Loop runs the main input event loop, reading terminal events and dispatching them. func (i *Input) Loop(ctx context.Context, cancel func()) error { defer cancel() @@ -39,6 +41,7 @@ func (i *Input) Loop(ctx context.Context, cancel func()) error { } } +// handleInputEvent processes a single terminal input event, mapping it to an action. func (i *Input) handleInputEvent(ctx context.Context, ev Event) error { if pdebug.Enabled { g := pdebug.Marker("event received from user: %#v", ev) diff --git a/internal/buffer/line.go b/internal/buffer/line.go index fa3a206..3414fc2 100644 --- a/internal/buffer/line.go +++ b/internal/buffer/line.go @@ -14,6 +14,7 @@ var lineListPool = sync.Pool{ }, } +// ReleaseLineListBuf returns a line list buffer to the sync.Pool for reuse. func ReleaseLineListBuf(l []line.Line) { if l == nil { return @@ -22,6 +23,7 @@ func ReleaseLineListBuf(l []line.Line) { lineListPool.Put(l) //nolint:staticcheck // SA6002: converting to pointer-based pool breaks tests } +// GetLineListBuf retrieves a line list buffer from the sync.Pool, allocating if needed. func GetLineListBuf() []line.Line { l, _ := lineListPool.Get().([]line.Line) return l diff --git a/internal/keyseq/ahocorasick.go b/internal/keyseq/ahocorasick.go index b54a451..2c56895 100644 --- a/internal/keyseq/ahocorasick.go +++ b/internal/keyseq/ahocorasick.go @@ -20,16 +20,19 @@ func (n *nodeData) Value() any { return n.value } +// NewMatcher creates a new Aho-Corasick matcher for multi-pattern key sequence matching. func NewMatcher() *Matcher { return &Matcher{ NewTernaryTrie(), } } +// Clear removes all patterns from the matcher, resetting it to empty state. func (m *Matcher) Clear() { m.Root().RemoveAll() } +// Add inserts a key sequence pattern with an associated value into the matcher. func (m *Matcher) Add(pattern KeyList, v any) { m.Put(pattern, &nodeData{ pattern: &pattern, @@ -37,6 +40,7 @@ func (m *Matcher) Add(pattern KeyList, v any) { }) } +// Compile builds the failure links needed for Aho-Corasick matching after all patterns are added. func (m *Matcher) Compile() error { m.Balance() root, _ := m.Root().(*TernaryNode) @@ -54,6 +58,7 @@ func (m *Matcher) Compile() error { return nil } +// fillFailure recursively computes the failure link for curr based on its parent's failure chain. func fillFailure(curr, root, parent *TernaryNode) { data := getNodeData(curr) if data == nil { @@ -69,12 +74,14 @@ func fillFailure(curr, root, parent *TernaryNode) { data.failure = fnode } +// Match tests a key sequence against all compiled patterns and returns matches on a channel. func (m *Matcher) Match(text KeyList) <-chan Match { ch := make(chan Match, 1) go m.startMatch(text, ch) return ch } +// startMatch begins a new match attempt from the root of the Aho-Corasick automaton. func (m *Matcher) startMatch(text KeyList, ch chan<- Match) { defer close(ch) root, _ := m.Root().(*TernaryNode) @@ -88,6 +95,7 @@ func (m *Matcher) startMatch(text KeyList, ch chan<- Match) { } } +// getNextNode follows failure links from node until it finds a child matching r, or returns root. func getNextNode(node, root *TernaryNode, r Key) *TernaryNode { for { next, _ := node.Get(r).(*TernaryNode) @@ -100,6 +108,7 @@ func getNextNode(node, root *TernaryNode, r Key) *TernaryNode { } } +// fireAll emits all pattern matches found at curr by walking the failure chain back to root. func fireAll(curr, root *TernaryNode, ch chan<- Match, idx int) { for curr != root { data := getNodeData(curr) @@ -114,11 +123,13 @@ func fireAll(curr, root *TernaryNode, ch chan<- Match, idx int) { } } +// getNodeData extracts the nodeData stored in a TernaryNode's value. func getNodeData(node *TernaryNode) *nodeData { d, _ := node.Value().(*nodeData) return d } +// getNodeFailure returns the failure link for node, falling back to root if none is set. func getNodeFailure(node, root *TernaryNode) *TernaryNode { next := getNodeData(node).failure if next == nil { diff --git a/internal/keyseq/keys.go b/internal/keyseq/keys.go index cf9819b..0bdea03 100644 --- a/internal/keyseq/keys.go +++ b/internal/keyseq/keys.go @@ -104,6 +104,7 @@ const ( var stringToKey = map[string]KeyType{} var keyToString = map[KeyType]string{} +// mapkey registers a bidirectional mapping between a key name string and its KeyType constant. func mapkey(n string, k KeyType) { stringToKey[n] = k keyToString[k] = n @@ -193,6 +194,7 @@ func init() { mapkey("C-8", KeyCtrl8) } +// ToKeyList parses a comma-separated key binding string (e.g. "C-x,C-c") into a list of KeySeq values. func ToKeyList(ksk string) (KeyList, error) { list := KeyList{} for term := range strings.SplitSeq(ksk, ",") { @@ -241,6 +243,7 @@ func KeyEventToString(key KeyType, ch rune, mod ModifierKey) (string, error) { return s, nil } +// ToKey parses a single key name string into its KeyType, modifier, and rune components. func ToKey(key string) (k KeyType, modifier ModifierKey, ch rune, err error) { modifier = ModNone diff --git a/internal/keyseq/keyseq.go b/internal/keyseq/keyseq.go index f1939c9..10f44a5 100644 --- a/internal/keyseq/keyseq.go +++ b/internal/keyseq/keyseq.go @@ -25,6 +25,7 @@ type Key struct { Ch rune } +// String returns a comma-separated string representation of the key list. func (kl KeyList) String() string { list := make([]string, len(kl)) for i := range kl { @@ -33,6 +34,7 @@ func (kl KeyList) String() string { return strings.Join(list, ",") } +// String returns the modifier key as a dash-separated string (e.g. "C-S-M"). func (m ModifierKey) String() string { var parts []string if m&ModCtrl != 0 { @@ -47,6 +49,7 @@ func (m ModifierKey) String() string { return strings.Join(parts, "-") } +// String returns a human-readable representation of the key, including any modifiers. func (k Key) String() string { var s string if m := k.Modifier.String(); m != "" { @@ -62,6 +65,7 @@ func (k Key) String() string { return s } +// NewKeyFromKey creates a Key from a KeyType with no modifier and no rune. func NewKeyFromKey(k KeyType) Key { return Key{ Modifier: 0, @@ -73,6 +77,7 @@ func NewKeyFromKey(k KeyType) Key { // KeyList is just the list of keys type KeyList []Key +// Compare returns -1, 0, or 1 comparing k and x by modifier, key type, and character. func (k Key) Compare(x Key) int { if k.Modifier < x.Modifier { return -1 @@ -95,6 +100,7 @@ func (k Key) Compare(x Key) int { return 0 } +// Equals reports whether kl and x contain the same keys in the same order. func (kl KeyList) Equals(x KeyList) bool { if len(kl) != len(x) { return false @@ -119,6 +125,7 @@ type Keyseq struct { mutex sync.Mutex } +// New creates a new Keyseq matcher for resolving multi-key bindings. func New() *Keyseq { return &Keyseq{ Matcher: NewMatcher(), @@ -126,10 +133,12 @@ func New() *Keyseq { } } +// InMiddleOfChain reports whether the matcher is partway through a multi-key sequence. func (k *Keyseq) InMiddleOfChain() bool { return k.current != nil && k.current != k.Matcher } +// CancelChain resets the matcher to the root, abandoning any in-progress key sequence. func (k *Keyseq) CancelChain() { k.mutex.Lock() defer k.mutex.Unlock() @@ -148,6 +157,8 @@ func (k *Keyseq) Current() keyseqMatcher { return k.current } +// AcceptKey advances the key sequence matcher with the given key, returning the bound action +// if a complete sequence is matched, or ErrInSequence if more keys are expected. func (k *Keyseq) AcceptKey(key Key) (any, error) { // XXX should we return Action instead of interface{}? k.mutex.Lock() diff --git a/internal/keyseq/ternary.go b/internal/keyseq/ternary.go index a617c93..0002424 100644 --- a/internal/keyseq/ternary.go +++ b/internal/keyseq/ternary.go @@ -4,6 +4,7 @@ type TernaryTrie struct { root TernaryNode } +// NewTernaryTrie creates a new empty ternary search trie. func NewTernaryTrie() *TernaryTrie { return &TernaryTrie{} } @@ -24,6 +25,7 @@ func (t *TernaryTrie) Put(k KeyList, v any) Node { return Put(t, k, v) } +// Size returns the total number of nodes in the trie. func (t *TernaryTrie) Size() int { count := 0 EachDepth(t, func(Node) bool { @@ -33,6 +35,7 @@ func (t *TernaryTrie) Size() int { return count } +// Balance rebalances all sibling lists in the trie for optimal search performance. func (t *TernaryTrie) Balance() { EachDepth(t, func(n Node) bool { tn, _ := n.(*TernaryNode) @@ -49,14 +52,17 @@ type TernaryNode struct { value any } +// NewTernaryNode creates a new ternary trie node with the given key label. func NewTernaryNode(l Key) *TernaryNode { return &TernaryNode{label: l} } +// GetList looks up a child node matching the first key in the list. func (n *TernaryNode) GetList(k KeyList) Node { return n.Get(k[0]) } +// Get searches the children of this node for a child matching key k. func (n *TernaryNode) Get(k Key) Node { curr := n.firstChild for curr != nil { @@ -72,6 +78,7 @@ func (n *TernaryNode) Get(k Key) Node { return nil } +// Dig finds or creates a child node for the given key, returning the node and whether it was newly created. func (n *TernaryNode) Dig(k Key) (node Node, isnew bool) { curr := n.firstChild if curr == nil { @@ -106,6 +113,7 @@ func (n *TernaryNode) HasChildren() bool { return n.firstChild != nil } +// Size returns the number of direct children of this node. func (n *TernaryNode) Size() int { if n.firstChild == nil { return 0 @@ -118,6 +126,7 @@ func (n *TernaryNode) Size() int { return count } +// Each calls proc for every child node in sorted order, stopping early if proc returns false. func (n *TernaryNode) Each(proc func(Node) bool) { var f func(*TernaryNode) bool f = func(n *TernaryNode) bool { @@ -131,6 +140,7 @@ func (n *TernaryNode) Each(proc func(Node) bool) { f(n.firstChild) } +// RemoveAll removes all children from this node. func (n *TernaryNode) RemoveAll() { n.firstChild = nil } @@ -147,6 +157,7 @@ func (n *TernaryNode) SetValue(v any) { n.value = v } +// children collects all direct child nodes into a sorted slice. func (n *TernaryNode) children() []*TernaryNode { children := make([]*TernaryNode, n.Size()) if n.firstChild == nil { @@ -162,6 +173,7 @@ func (n *TernaryNode) children() []*TernaryNode { return children } +// Balance rebalances the children of this node into a balanced binary search tree. func (n *TernaryNode) Balance() { if n.firstChild == nil { return @@ -174,6 +186,7 @@ func (n *TernaryNode) Balance() { n.firstChild = balance(children, 0, len(children)) } +// balance recursively builds a balanced binary tree from a sorted slice of nodes. func balance(nodes []*TernaryNode, s, e int) *TernaryNode { count := e - s if count <= 0 { diff --git a/internal/keyseq/trie.go b/internal/keyseq/trie.go index 149ad00..17c1fc4 100644 --- a/internal/keyseq/trie.go +++ b/internal/keyseq/trie.go @@ -12,10 +12,12 @@ type Trie interface { Size() int } +// NewTrie creates a new empty Trie for storing key sequences. func NewTrie() Trie { return NewTernaryTrie() } +// Get looks up a value by key sequence path in the trie, returning nil if not found. func Get(t Trie, k KeyList) Node { if t == nil { return nil @@ -30,6 +32,7 @@ func Get(t Trie, k KeyList) Node { return n } +// Put inserts a value at the given key sequence path in the trie, creating nodes as needed. func Put(t Trie, k KeyList, v any) Node { if t == nil { return nil @@ -42,6 +45,7 @@ func Put(t Trie, k KeyList, v any) Node { return n } +// EachDepth iterates over trie nodes in depth-first order, calling proc for each node. func EachDepth(t Trie, proc func(Node) bool) { if t == nil { return @@ -55,6 +59,7 @@ func EachDepth(t Trie, proc func(Node) bool) { r.Each(f) } +// EachWidth iterates over trie nodes in breadth-first order, calling proc for each node. func EachWidth(t Trie, proc func(Node) bool) { if t == nil { return @@ -92,6 +97,7 @@ type Node interface { SetValue(v any) } +// Children returns all child nodes of n as a slice. func Children(n Node) []Node { children := make([]Node, n.Size()) idx := 0 diff --git a/internal/util/homedir_posix.go b/internal/util/homedir_posix.go index d52c60a..277e2cb 100644 --- a/internal/util/homedir_posix.go +++ b/internal/util/homedir_posix.go @@ -7,6 +7,7 @@ import ( "os" ) +// Homedir returns the current user's home directory from the HOME environment variable. func Homedir() (string, error) { home := os.Getenv("HOME") if home == "" { diff --git a/internal/util/shell_unix.go b/internal/util/shell_unix.go index d47d171..273eb16 100644 --- a/internal/util/shell_unix.go +++ b/internal/util/shell_unix.go @@ -7,6 +7,7 @@ import ( "os/exec" ) +// Shell creates an exec.Cmd that runs the given command strings via /bin/sh -c. func Shell(ctx context.Context, cmd ...string) *exec.Cmd { const shellpath = `/bin/sh` const shellopt = `-c` diff --git a/internal/util/util.go b/internal/util/util.go index 8a8fb38..7dbd805 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -10,6 +10,7 @@ type fder interface { Fd() uintptr } +// CaseInsensitiveIndexFunc returns a function that matches runes equal to r, ignoring case. func CaseInsensitiveIndexFunc(r rune) func(rune) bool { lr := unicode.ToUpper(r) return func(v rune) bool { @@ -17,6 +18,7 @@ func CaseInsensitiveIndexFunc(r rune) func(rune) bool { } } +// ContainsUpper reports whether the string contains any uppercase letter. func ContainsUpper(query string) bool { for _, c := range query { if unicode.IsUpper(c) { @@ -46,6 +48,7 @@ type exitStatuser interface { ExitStatus() int } +// IsIgnorableError checks whether err implements the Ignorable interface and returns true. func IsIgnorableError(err error) bool { for e := err; e != nil; e = errors.Unwrap(e) { if v, ok := e.(ignorable); ok { @@ -55,6 +58,7 @@ func IsIgnorableError(err error) bool { return false } +// IsCollectResultsError checks whether err signals that results should be collected. func IsCollectResultsError(err error) bool { for e := err; e != nil; e = errors.Unwrap(e) { if v, ok := e.(collectResults); ok { @@ -64,6 +68,7 @@ func IsCollectResultsError(err error) bool { return false } +// GetExitStatus extracts the exit status code from an error, returning 1 and false if not found. func GetExitStatus(err error) (int, bool) { for e := err; e != nil; e = errors.Unwrap(e) { if ese, ok := e.(exitStatuser); ok { diff --git a/keymap.go b/keymap.go index edb3797..fe47168 100644 --- a/keymap.go +++ b/keymap.go @@ -43,6 +43,7 @@ func (km Keymap) Sequence() Keyseq { return km.seq } +// ExecuteAction looks up and executes the action(s) bound to the given key event. func (km Keymap) ExecuteAction(ctx context.Context, state *Peco, ev Event) (err error) { if pdebug.Enabled { g := pdebug.Marker("Keymap.ExecuteAction %v", ev).BindError(&err) @@ -91,6 +92,7 @@ func (km Keymap) LookupAction(ev Event) Action { } } +// wrapRememberSequence wraps an action to record the key press as part of a multi-key sequence. func wrapRememberSequence(a Action) Action { return ActionFunc(func(ctx context.Context, state *Peco, ev Event) { if s, err := keyseq.KeyEventToString(ev.Key, ev.Ch, ev.Mod); err == nil { @@ -102,6 +104,7 @@ func wrapRememberSequence(a Action) Action { }) } +// wrapClearSequence wraps an action to clear the accumulated key sequence after execution. func wrapClearSequence(a Action) Action { return ActionFunc(func(ctx context.Context, state *Peco, ev Event) { seq := state.Inputseq() @@ -121,6 +124,7 @@ func wrapClearSequence(a Action) Action { const maxResolveActionDepth = 100 +// resolveActionName maps a string action name from config to the corresponding action function. func (km Keymap) resolveActionName(name string, depth int) (Action, error) { if depth >= maxResolveActionDepth { return nil, fmt.Errorf("could not resolve %s: deep recursion", name) diff --git a/layout.go b/layout.go index 21be2e8..90d247c 100644 --- a/layout.go +++ b/layout.go @@ -358,6 +358,7 @@ func newScreenStatusBar(screen Screen, anchor VerticalAnchor, anchorOffset int, }, nil } +// stopTimer stops and drains the clear timer, preventing stale events from firing. func (s *screenStatusBar) stopTimer() { s.timerMutex.Lock() defer s.timerMutex.Unlock() @@ -367,6 +368,7 @@ func (s *screenStatusBar) stopTimer() { } } +// setClearTimer sets or resets the timer that will clear the status message after a delay. func (s *screenStatusBar) setClearTimer(t *time.Timer) { s.timerMutex.Lock() defer s.timerMutex.Unlock() @@ -469,6 +471,7 @@ func (l *ListArea) SetDirty(dirty bool) { l.dirty = dirty } +// selectionContains reports whether the line at index n is in the current selection. func selectionContains(state *Peco, n int) bool { if l, err := state.CurrentLineBuffer().LineAt(n); err == nil { return state.Selection().Has(l) @@ -946,6 +949,7 @@ func (l *BasicLayout) DrawScreen(state *Peco, options *hub.DrawOptions) { } } +// linesPerPage calculates the number of visible lines per page based on terminal height. func (l *BasicLayout) linesPerPage() int { _, height := l.screen.Size() diff --git a/options.go b/options.go index 08425aa..732e8d9 100644 --- a/options.go +++ b/options.go @@ -34,6 +34,7 @@ type CLIOptions struct { OptHeight string `long:"height" description:"display height in lines or percentage (e.g. '10', '50%')"` } +// parse parses command-line arguments and validates the resulting options. func (options *CLIOptions) parse(s []string) ([]string, error) { p := flags.NewParser(options, flags.PrintErrors) args, err := p.ParseArgs(s) @@ -49,6 +50,7 @@ func (options *CLIOptions) parse(s []string) ([]string, error) { return args, nil } +// Validate checks the parsed CLI options for correctness (e.g., layout type). func (options CLIOptions) Validate() error { if options.OptLayout != "" { if !IsValidLayoutType(LayoutType(options.OptLayout)) { @@ -58,6 +60,7 @@ func (options CLIOptions) Validate() error { return nil } +// help generates formatted help text from struct field tags. func (options CLIOptions) help() []byte { buf := bytes.Buffer{} diff --git a/page.go b/page.go index bd8ba9b..1ad4670 100644 --- a/page.go +++ b/page.go @@ -105,6 +105,7 @@ func (l *Location) MaxPage() int { return l.maxPage } +// PageCrop returns a PageCrop snapshot of the current page and perPage values under a read lock. func (l *Location) PageCrop() PageCrop { l.mutex.RLock() defer l.mutex.RUnlock() diff --git a/peco.go b/peco.go index 89b967d..faefa05 100644 --- a/peco.go +++ b/peco.go @@ -411,6 +411,8 @@ func (p *Peco) Setup() (err error) { return nil } +// selectOneAndExitIfPossible selects the first line and exits if there is +// exactly one line in the buffer and --select-1 mode is active. func (p *Peco) selectOneAndExitIfPossible() { // If we have only one line, we just want to bail out // printing that one line as the result. @@ -431,6 +433,8 @@ func (p *Peco) selectOneAndExitIfPossible() { } } +// selectOneCallback returns a callback for pipeline completion that triggers +// selectOneAndExitIfPossible if --select-1 mode is enabled, or nil otherwise. func (p *Peco) selectOneCallback() func() { if p.selectOneAndExit { return p.selectOneAndExitIfPossible @@ -438,12 +442,16 @@ func (p *Peco) selectOneCallback() func() { return nil } +// exitZeroIfPossible exits immediately with status 1 if the current line +// buffer is empty and --exit-0 mode is enabled. func (p *Peco) exitZeroIfPossible() { if p.CurrentLineBuffer().Size() == 0 { p.Exit(setExitStatus(makeIgnorable(errors.New("no input, exiting")), 1)) } } +// selectAllAndExitIfPossible adds all lines in the current buffer to the +// selection and exits when --select-all mode is enabled. func (p *Peco) selectAllAndExitIfPossible() { b := p.CurrentLineBuffer() selection := p.Selection() @@ -538,6 +546,8 @@ func (p *Peco) setupInitialQuery(ctx context.Context) { } } +// Run is the main entry point that sets up the TUI, starts the input source +// and pipeline components, and blocks until the context is canceled. func (p *Peco) Run(ctx context.Context) (err error) { if pdebug.Enabled { g := pdebug.Marker("Peco.Run").BindError(&err) @@ -624,6 +634,8 @@ func (p *Peco) Run(ctx context.Context) (err error) { return p.Err() } +// parseCommandLine parses CLI arguments from argv into the CLIOptions struct +// and stores any remaining positional arguments in args. func (p *Peco) parseCommandLine(opts *CLIOptions, args *[]string, argv []string) error { remaining, err := opts.parse(argv) if err != nil { @@ -653,6 +665,8 @@ func (p *Peco) parseCommandLine(opts *CLIOptions, args *[]string, argv []string) return nil } +// SetupSource configures the input source (stdin or a file specified as a +// positional argument) and starts reading lines into the Source buffer. func (p *Peco) SetupSource(ctx context.Context) (s *Source, err error) { if pdebug.Enabled { g := pdebug.Marker("Peco.SetupSource").BindError(&err) @@ -701,6 +715,8 @@ func (p *Peco) SetupSource(ctx context.Context) (s *Source, err error) { return src, nil } +// readConfig loads the configuration from the given filename into cfg. +// If filename is empty, no file is read and nil is returned. func readConfig(cfg *Config, filename string) error { if filename != "" { if err := cfg.ReadFilename(filename); err != nil { @@ -711,6 +727,8 @@ func readConfig(cfg *Config, filename string) error { return nil } +// ApplyConfig applies the loaded Config and CLI options to the Peco instance, +// setting up layout, styles, keymap, filters, and all other runtime parameters. func (p *Peco) ApplyConfig(opts CLIOptions) error { // If layoutType is not set and is set in the config, set it if p.layoutType == "" { @@ -810,6 +828,8 @@ func (p *Peco) ApplyConfig(opts CLIOptions) error { return nil } +// populateInitialFilter sets the initial active filter based on the +// --initial-filter flag or the InitialFilter config value. func (p *Peco) populateInitialFilter() error { if v := p.initialFilter; len(v) > 0 { if err := p.filters.SetCurrentByName(v); err != nil { @@ -819,6 +839,8 @@ func (p *Peco) populateInitialFilter() error { return nil } +// populateSingleKeyJump configures the single-key-jump mode by building +// the prefix-to-index mapping from the config. func (p *Peco) populateSingleKeyJump() error { //nolint:unparam p.singleKeyJump.showPrefix = p.config.SingleKeyJump.ShowPrefix @@ -836,6 +858,8 @@ func (p *Peco) populateSingleKeyJump() error { //nolint:unparam return nil } +// populateFilters registers the built-in filter set (IgnoreCase, CaseSensitive, +// SmartCase, Regexp, Fuzzy, etc.) and any custom external filters from config. func (p *Peco) populateFilters() { p.filters.Add(filter.NewIgnoreCase()) p.filters.Add(filter.NewCaseSensitive()) @@ -850,6 +874,8 @@ func (p *Peco) populateFilters() { } } +// populateKeymap creates a new Keymap from the config and applies the +// key-to-action bindings. func (p *Peco) populateKeymap() error { // Create a new keymap object k := NewKeymap(p.config.Keymap, p.config.Action) @@ -862,17 +888,22 @@ func (p *Peco) populateKeymap() error { return nil } +// populateStyles applies the style settings from config to the Peco StyleSet. func (p *Peco) populateStyles() error { //nolint:unparam p.styles = p.config.Style return nil } +// CurrentLineBuffer returns the current active line buffer, which is either +// the filtered result set or the original source buffer. func (p *Peco) CurrentLineBuffer() Buffer { p.mutex.Lock() defer p.mutex.Unlock() return p.currentLineBuffer } +// SetCurrentLineBuffer replaces the current line buffer with b and triggers +// a redraw of the screen. func (p *Peco) SetCurrentLineBuffer(ctx context.Context, b Buffer) { p.mutex.Lock() defer p.mutex.Unlock() @@ -884,6 +915,8 @@ func (p *Peco) SetCurrentLineBuffer(ctx context.Context, b Buffer) { go p.Hub().SendDraw(ctx, nil) } +// ResetCurrentLineBuffer clears the current line buffer, reverting it to the +// source buffer (or the frozen source if zoom/freeze is active). func (p *Peco) ResetCurrentLineBuffer(ctx context.Context) { if fs := p.Frozen().Source(); fs != nil { p.SetCurrentLineBuffer(ctx, fs) @@ -892,6 +925,9 @@ func (p *Peco) ResetCurrentLineBuffer(ctx context.Context) { } } +// sendQuery sends the query string q to the hub for filter processing. For +// finite sources it uses batch mode; for infinite/streaming sources it sends +// immediately and schedules nextFunc via waitAndCall. func (p *Peco) sendQuery(ctx context.Context, q string, nextFunc func()) { if pdebug.Enabled { g := pdebug.Marker("sending query to filter goroutine (q=%v, isInfinite=%t)", q, p.source.IsInfinite()) @@ -1031,6 +1067,9 @@ func (p *Peco) ExecQuery(ctx context.Context, nextFunc func()) bool { return true } +// PrintResults writes the selected lines (or the current line if none are +// selected) to the configured output writer. If --print-query is set, the +// query string is printed first. func (p *Peco) PrintResults() { if pdebug.Enabled { g := pdebug.Marker("Peco.PrintResults") diff --git a/pipeline/pipeline.go b/pipeline/pipeline.go index 23539e1..5a47a94 100644 --- a/pipeline/pipeline.go +++ b/pipeline/pipeline.go @@ -174,6 +174,7 @@ func (p *Pipeline) Run(ctx context.Context) (err error) { return nil } +// Done returns a channel that is closed when the pipeline completes. func (p *Pipeline) Done() <-chan struct{} { p.mutex.Lock() defer p.mutex.Unlock() diff --git a/query.go b/query.go index d14d8d4..df5f60c 100644 --- a/query.go +++ b/query.go @@ -36,6 +36,7 @@ func (q *Query) SaveQuery() { q.query = []rune(nil) } +// DeleteRange deletes runes in the range [start, end) from the query with boundary validation. func (q *Query) DeleteRange(start, end int) { q.mutex.Lock() defer q.mutex.Unlock() @@ -88,6 +89,7 @@ func (q *Query) RuneAt(where int) rune { return q.query[where] } +// InsertAt inserts a rune at the specified position in the query. func (q *Query) InsertAt(ch rune, where int) { q.mutex.Lock() defer q.mutex.Unlock() diff --git a/screen.go b/screen.go index f63e1bc..27b344e 100644 --- a/screen.go +++ b/screen.go @@ -261,6 +261,7 @@ func (t *TcellScreen) Init(_ *Config) error { return nil } +// NewTcellScreen creates a new TcellScreen with initialized channels and default error output. func NewTcellScreen() *TcellScreen { return &TcellScreen{ suspendCh: make(chan struct{}), @@ -414,6 +415,7 @@ func (t *TcellScreen) PollEvent(ctx context.Context, cfg *Config) chan Event { return evCh } +// Suspend signals the event polling goroutine to suspend the screen. func (t *TcellScreen) Suspend() { select { case t.suspendCh <- struct{}{}: @@ -421,6 +423,7 @@ func (t *TcellScreen) Suspend() { } } +// Resume sends a resume request and waits for screen re-initialization to complete. func (t *TcellScreen) Resume(ctx context.Context) error { // Resume must be a block operation, because we can't safely proceed // without actually knowing that the screen has been re-initialized. @@ -481,6 +484,7 @@ func (t *TcellScreen) Print(args PrintArgs) int { return screenPrint(t, args) } +// screenPrint writes a string to the screen with tab expansion, ANSI color support, and optional line fill. func screenPrint(t Screen, args PrintArgs) int { var written int diff --git a/screen_inline.go b/screen_inline.go index 3daccbc..153571c 100644 --- a/screen_inline.go +++ b/screen_inline.go @@ -37,6 +37,7 @@ func NewInlineScreen(spec HeightSpec) *InlineScreen { } } +// Init initializes the tcell screen for inline mode, disabling the alternate screen buffer. func (s *InlineScreen) Init(_ *Config) error { // Save and override TCELL_ALTSCREEN to prevent alternate screen buffer s.savedAltscreen = os.Getenv("TCELL_ALTSCREEN") @@ -171,6 +172,7 @@ func (s *InlineScreen) Size() (int, int) { return w, s.height } +// PollEvent creates an event channel and polls for terminal events with special resize handling. func (s *InlineScreen) PollEvent(ctx context.Context, _ *Config) chan Event { evCh := make(chan Event) diff --git a/selection.go b/selection.go index 6b679b2..7286369 100644 --- a/selection.go +++ b/selection.go @@ -36,6 +36,7 @@ func (s *Selection) Add(l line.Line) { s.tree.ReplaceOrInsert(l) } +// Copy copies all selected lines from s into dst. func (s *Selection) Copy(dst *Selection) { s.Ascend(func(it btree.Item) bool { l, ok := it.(line.Line) @@ -54,24 +55,28 @@ func (s *Selection) Remove(l line.Line) { s.tree.Delete(l) } +// Reset clears all selected indices from the selection. func (s *Selection) Reset() { s.mutex.Lock() defer s.mutex.Unlock() s.tree = btree.New(32) } +// Has reports whether the given line is in the selection. func (s *Selection) Has(x line.Line) bool { s.mutex.RLock() defer s.mutex.RUnlock() return s.tree.Has(x) } +// Len returns the number of selected lines. func (s *Selection) Len() int { s.mutex.RLock() defer s.mutex.RUnlock() return s.tree.Len() } +// Ascend iterates over selected lines in ascending order, calling i for each. func (s *Selection) Ascend(i btree.ItemIterator) { s.mutex.RLock() defer s.mutex.RUnlock() diff --git a/sig/sig.go b/sig/sig.go index edc0669..0167615 100644 --- a/sig/sig.go +++ b/sig/sig.go @@ -14,6 +14,7 @@ type ReceivedHandler interface { type ReceivedHandlerFunc func(os.Signal) +// Handle calls the underlying function with the received signal. func (s ReceivedHandlerFunc) Handle(sig os.Signal) { s(sig) } @@ -23,6 +24,7 @@ type Handler struct { sigCh chan os.Signal } +// New creates a new signal handler that forwards the specified signals (default: SIGTERM, SIGINT, SIGHUP) to h. func New(h ReceivedHandler, sigs ...os.Signal) *Handler { if len(sigs) == 0 { sigs = append(sigs, syscall.SIGTERM, syscall.SIGINT, syscall.SIGHUP) @@ -37,6 +39,7 @@ func New(h ReceivedHandler, sigs ...os.Signal) *Handler { } } +// Loop listens for OS signals and invokes the handler when one is received, then returns. func (h *Handler) Loop(ctx context.Context, cancel func()) error { defer cancel() defer signal.Stop(h.sigCh) diff --git a/source.go b/source.go index fe7fa68..6432779 100644 --- a/source.go +++ b/source.go @@ -62,10 +62,13 @@ func NewSource(name string, in io.Reader, isInfinite bool, idgen line.IDGenerato return s } +// Name returns the display name of this source. func (s *Source) Name() string { return s.name } +// IsInfinite reports whether the source is an infinite stream (e.g. tail -f) +// that has not yet been closed. func (s *Source) IsInfinite() bool { return s.isInfinite && !s.inClosed } @@ -197,7 +200,8 @@ func (s *Source) Setup(ctx context.Context, state *Peco) { }) } -// Start starts +// Start begins sending buffered lines into the pipeline output channel. +// If input is still being read, it resumes from where the last send left off. func (s *Source) Start(ctx context.Context, out pipeline.ChanOutput) { var sent int // I should be the only one running this method until I bail out @@ -306,24 +310,29 @@ func (s *Source) SetupDone() <-chan struct{} { return s.setupDone } +// linesInRange returns a slice of lines between start and end indices from the buffer. func (s *Source) linesInRange(start, end int) []line.Line { s.mutex.RLock() defer s.mutex.RUnlock() return s.lines[start:end] } +// LineAt returns the line at the given index from the buffer. func (s *Source) LineAt(n int) (line.Line, error) { s.mutex.RLock() defer s.mutex.RUnlock() return bufferLineAt(s.lines, n) } +// Size returns the number of lines currently in the buffer. func (s *Source) Size() int { s.mutex.RLock() defer s.mutex.RUnlock() return len(s.lines) } +// Append adds a new line to the source buffer. If a capacity is set and +// exceeded, the oldest lines are discarded to maintain the limit. func (s *Source) Append(l line.Line) { s.mutex.Lock() defer s.mutex.Unlock() diff --git a/view.go b/view.go index 5e70bf1..f6d67d4 100644 --- a/view.go +++ b/view.go @@ -13,6 +13,7 @@ type View struct { state *Peco } +// NewView creates a new View with the given state and its configured layout. func NewView(state *Peco) (*View, error) { layout, err := NewLayout(LayoutType(state.LayoutType()), state) if err != nil { @@ -24,6 +25,8 @@ func NewView(state *Peco) (*View, error) { }, nil } +// Loop runs the main view loop, listening for draw, paging, and status message +// events from the hub and dispatching them to the appropriate handlers. func (v *View) Loop(ctx context.Context, cancel func()) error { defer cancel() @@ -50,30 +53,36 @@ func (v *View) Loop(ctx context.Context, cancel func()) error { } } +// printStatus renders a status message on the screen's status bar. func (v *View) printStatus(p *hub.Payload[hub.StatusMsg]) { defer p.Done() r := p.Data() v.layout.PrintStatus(r.Message(), r.Delay()) } +// purgeDisplayCache clears the cached display state, forcing a full redraw on +// the next draw cycle. func (v *View) purgeDisplayCache(p *hub.Payload[*hub.DrawOptions]) { defer p.Done() v.layout.PurgeDisplayCache() } +// drawScreen renders the current state (prompt, list, status) to the terminal screen. func (v *View) drawScreen(p *hub.Payload[*hub.DrawOptions], options *hub.DrawOptions) { defer p.Done() v.layout.DrawScreen(v.state, options) } +// drawPrompt renders the query prompt line with cursor position. func (v *View) drawPrompt(p *hub.Payload[*hub.DrawOptions]) { defer p.Done() v.layout.DrawPrompt(v.state) } +// movePage handles paging events such as scroll up/down and jump to top/bottom. func (v *View) movePage(p *hub.Payload[hub.PagingRequest]) { defer p.Done()