Methodically add function comments

This commit is contained in:
Daisuke Maki 2026-02-20 10:55:49 +09:00
parent 93fcda4a08
commit fc18c0d3de
35 changed files with 265 additions and 1 deletions

View file

@ -52,6 +52,7 @@ func (a ActionFunc) Execute(ctx context.Context, state *Peco, e Event) {
a(ctx, state, e) 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) { func (a ActionFunc) registerKeySequence(k keyseq.KeyList) {
defaultKeyBinding[k.String()] = a defaultKeyBinding[k.String()] = a
} }
@ -74,6 +75,7 @@ func (a ActionFunc) RegisterKeySequence(name string, k keyseq.KeyList) {
a.registerKeySequence(k) 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 { func wrapDeprecated(fn func(context.Context, *Peco, Event), oldName, newName string) ActionFunc {
return ActionFunc(func(ctx context.Context, state *Peco, e Event) { return ActionFunc(func(ctx context.Context, state *Peco, e Event) {
state.Hub().SendStatusMsg(ctx, fmt.Sprintf("%s is deprecated. Use %s", oldName, newName), 0) 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()) 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) { func doRotateFilter(ctx context.Context, state *Peco, _ Event) {
if pdebug.Enabled { if pdebug.Enabled {
g := pdebug.Marker("doRotateFilter") g := pdebug.Marker("doRotateFilter")
@ -243,6 +246,7 @@ func doRotateFilter(ctx context.Context, state *Peco, _ Event) {
execQueryAndDraw(ctx, state) 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) { func doBackToInitialFilter(ctx context.Context, state *Peco, _ Event) {
if pdebug.Enabled { if pdebug.Enabled {
g := pdebug.Marker("doBackToInitialFilter") g := pdebug.Marker("doBackToInitialFilter")
@ -255,6 +259,7 @@ func doBackToInitialFilter(ctx context.Context, state *Peco, _ Event) {
execQueryAndDraw(ctx, state) execQueryAndDraw(ctx, state)
} }
// doToggleSelection toggles the selection state of the line at the current cursor position.
func doToggleSelection(_ context.Context, state *Peco, _ Event) { func doToggleSelection(_ context.Context, state *Peco, _ Event) {
if pdebug.Enabled { if pdebug.Enabled {
g := pdebug.Marker("doToggleSelection") g := pdebug.Marker("doToggleSelection")
@ -274,6 +279,7 @@ func doToggleSelection(_ context.Context, state *Peco, _ Event) {
selection.Add(l) selection.Add(l)
} }
// doToggleRangeMode enables or disables range selection mode, anchoring or clearing the range start.
func doToggleRangeMode(_ context.Context, state *Peco, _ Event) { func doToggleRangeMode(_ context.Context, state *Peco, _ Event) {
if pdebug.Enabled { if pdebug.Enabled {
g := pdebug.Marker("doToggleRangeMode") 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) { func doCancelRangeMode(_ context.Context, state *Peco, _ Event) {
state.SelectionRangeStart().Reset() state.SelectionRangeStart().Reset()
} }
// doSelectNone deselects all currently selected lines and redraws the screen.
func doSelectNone(ctx context.Context, state *Peco, _ Event) { func doSelectNone(ctx context.Context, state *Peco, _ Event) {
state.Selection().Reset() state.Selection().Reset()
state.Hub().SendDraw(ctx, &hub.DrawOptions{DisableCache: true}) 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) { func doSelectAll(ctx context.Context, state *Peco, _ Event) {
selection := state.Selection() selection := state.Selection()
b := state.CurrentLineBuffer() b := state.CurrentLineBuffer()
@ -314,6 +323,7 @@ func doSelectAll(ctx context.Context, state *Peco, _ Event) {
state.Hub().SendDraw(ctx, nil) 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) { func doSelectVisible(ctx context.Context, state *Peco, _ Event) {
if pdebug.Enabled { if pdebug.Enabled {
g := pdebug.Marker("doSelectVisible") g := pdebug.Marker("doSelectVisible")
@ -343,6 +353,8 @@ func (err collectResultsError) Error() string {
func (err collectResultsError) CollectResults() bool { func (err collectResultsError) CollectResults() bool {
return true 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) { func doFinish(ctx context.Context, state *Peco, _ Event) {
if pdebug.Enabled { if pdebug.Enabled {
g := pdebug.Marker("doFinish") 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) { func doCancel(ctx context.Context, state *Peco, e Event) {
km := state.Keymap() km := state.Keymap()
@ -445,6 +459,8 @@ func batchAction(ctx context.Context, state *Peco, fn func(context.Context)) {
state.Hub().Batch(ctx, fn) 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) { func doToggleSelectionAndSelectNext(ctx context.Context, state *Peco, e Event) {
batchAction(ctx, state, func(ctx context.Context) { batchAction(ctx, state, func(ctx context.Context) {
doToggleSelection(ctx, state, e) 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) { func doInvertSelection(ctx context.Context, state *Peco, _ Event) {
if pdebug.Enabled { if pdebug.Enabled {
g := pdebug.Marker("doInvertSelection") g := pdebug.Marker("doInvertSelection")
@ -481,6 +498,8 @@ func doInvertSelection(ctx context.Context, state *Peco, _ Event) {
state.Hub().SendDraw(ctx, nil) 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) { func doDeleteBackwardWord(ctx context.Context, state *Peco, _ Event) {
if pdebug.Enabled { if pdebug.Enabled {
g := pdebug.Marker("doDeleteBackwardWord") g := pdebug.Marker("doDeleteBackwardWord")
@ -521,6 +540,7 @@ func doDeleteBackwardWord(ctx context.Context, state *Peco, _ Event) {
execQueryAndDraw(ctx, state) 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) { func doForwardWord(ctx context.Context, state *Peco, _ Event) {
if state.Caret().Pos() >= state.Query().Len() { if state.Caret().Pos() >= state.Query().Len() {
return return
@ -548,6 +568,7 @@ func doForwardWord(ctx context.Context, state *Peco, _ Event) {
c.SetPos(q.Len()) 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) { func doBackwardWord(ctx context.Context, state *Peco, _ Event) {
c := state.Caret() c := state.Caret()
q := state.Query() q := state.Query()
@ -594,6 +615,7 @@ func doBackwardWord(ctx context.Context, state *Peco, _ Event) {
c.SetPos(0) c.SetPos(0)
} }
// doForwardChar moves the cursor one character forward in the query.
func doForwardChar(ctx context.Context, state *Peco, _ Event) { func doForwardChar(ctx context.Context, state *Peco, _ Event) {
c := state.Caret() c := state.Caret()
if c.Pos() >= state.Query().Len() { if c.Pos() >= state.Query().Len() {
@ -603,6 +625,7 @@ func doForwardChar(ctx context.Context, state *Peco, _ Event) {
state.Hub().SendDrawPrompt(ctx) state.Hub().SendDrawPrompt(ctx)
} }
// doBackwardChar moves the cursor one character backward in the query.
func doBackwardChar(ctx context.Context, state *Peco, _ Event) { func doBackwardChar(ctx context.Context, state *Peco, _ Event) {
c := state.Caret() c := state.Caret()
if c.Pos() <= 0 { if c.Pos() <= 0 {
@ -612,6 +635,7 @@ func doBackwardChar(ctx context.Context, state *Peco, _ Event) {
state.Hub().SendDrawPrompt(ctx) 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) { func doDeleteForwardWord(ctx context.Context, state *Peco, _ Event) {
c := state.Caret() c := state.Caret()
q := state.Query() q := state.Query()
@ -645,16 +669,20 @@ func doDeleteForwardWord(ctx context.Context, state *Peco, _ Event) {
execQueryAndDraw(ctx, state) execQueryAndDraw(ctx, state)
} }
// doBeginningOfLine moves the cursor to the beginning of the query line.
func doBeginningOfLine(ctx context.Context, state *Peco, _ Event) { func doBeginningOfLine(ctx context.Context, state *Peco, _ Event) {
state.Caret().SetPos(0) state.Caret().SetPos(0)
state.Hub().SendDrawPrompt(ctx) state.Hub().SendDrawPrompt(ctx)
} }
// doEndOfLine moves the cursor to the end of the query line.
func doEndOfLine(ctx context.Context, state *Peco, _ Event) { func doEndOfLine(ctx context.Context, state *Peco, _ Event) {
state.Caret().SetPos(state.Query().Len()) state.Caret().SetPos(state.Query().Len())
state.Hub().SendDrawPrompt(ctx) 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) { func doEndOfFile(ctx context.Context, state *Peco, e Event) {
if state.Query().Len() > 0 { if state.Query().Len() > 0 {
doDeleteForwardChar(ctx, state, e) 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) { func doKillBeginningOfLine(ctx context.Context, state *Peco, _ Event) {
q := state.Query() q := state.Query()
q.DeleteRange(0, state.Caret().Pos()) q.DeleteRange(0, state.Caret().Pos())
@ -670,6 +699,7 @@ func doKillBeginningOfLine(ctx context.Context, state *Peco, _ Event) {
execQueryAndDraw(ctx, state) execQueryAndDraw(ctx, state)
} }
// doKillEndOfLine deletes all text from the cursor to the end of the query.
func doKillEndOfLine(ctx context.Context, state *Peco, _ Event) { func doKillEndOfLine(ctx context.Context, state *Peco, _ Event) {
if state.Query().Len() <= state.Caret().Pos() { if state.Query().Len() <= state.Caret().Pos() {
return return
@ -680,11 +710,13 @@ func doKillEndOfLine(ctx context.Context, state *Peco, _ Event) {
execQueryAndDraw(ctx, state) execQueryAndDraw(ctx, state)
} }
// doDeleteAll clears the entire query string and re-runs the query.
func doDeleteAll(ctx context.Context, state *Peco, _ Event) { func doDeleteAll(ctx context.Context, state *Peco, _ Event) {
state.Query().Reset() state.Query().Reset()
state.ExecQuery(ctx, state.selectOneCallback()) 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) { func doDeleteForwardChar(ctx context.Context, state *Peco, _ Event) {
q := state.Query() q := state.Query()
c := state.Caret() c := state.Caret()
@ -698,6 +730,7 @@ func doDeleteForwardChar(ctx context.Context, state *Peco, _ Event) {
execQueryAndDraw(ctx, state) execQueryAndDraw(ctx, state)
} }
// doDeleteBackwardChar deletes the character immediately before the cursor in the query.
func doDeleteBackwardChar(ctx context.Context, state *Peco, _ Event) { func doDeleteBackwardChar(ctx context.Context, state *Peco, _ Event) {
if pdebug.Enabled { if pdebug.Enabled {
g := pdebug.Marker("doDeleteBackwardChar") g := pdebug.Marker("doDeleteBackwardChar")
@ -734,10 +767,12 @@ func doDeleteBackwardChar(ctx context.Context, state *Peco, _ Event) {
execQueryAndDraw(ctx, state) execQueryAndDraw(ctx, state)
} }
// doRefreshScreen forces a full screen redraw with cache disabled and synchronous rendering.
func doRefreshScreen(ctx context.Context, state *Peco, _ Event) { func doRefreshScreen(ctx context.Context, state *Peco, _ Event) {
state.Hub().SendDraw(ctx, &hub.DrawOptions{DisableCache: true, ForceSync: true}) 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) { func doToggleQuery(ctx context.Context, state *Peco, _ Event) {
if pdebug.Enabled { if pdebug.Enabled {
g := pdebug.Marker("doToggleQuery") g := pdebug.Marker("doToggleQuery")
@ -754,10 +789,12 @@ func doToggleQuery(ctx context.Context, state *Peco, _ Event) {
execQueryAndDraw(ctx, state) execQueryAndDraw(ctx, state)
} }
// doKonamiCommand is an easter egg triggered by the Konami code key sequence.
func doKonamiCommand(ctx context.Context, state *Peco, _ Event) { func doKonamiCommand(ctx context.Context, state *Peco, _ Event) {
state.Hub().SendStatusMsg(ctx, "All your filters are belongs to us", 0) 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) { func doToggleSingleKeyJump(ctx context.Context, state *Peco, _ Event) {
if pdebug.Enabled { if pdebug.Enabled {
g := pdebug.Marker("doToggleSingleKeyJump") g := pdebug.Marker("doToggleSingleKeyJump")
@ -766,6 +803,8 @@ func doToggleSingleKeyJump(ctx context.Context, state *Peco, _ Event) {
state.ToggleSingleKeyJumpMode(ctx) 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) { func doToggleViewAround(ctx context.Context, state *Peco, e Event) {
if pdebug.Enabled { if pdebug.Enabled {
g := pdebug.Marker("doToggleViewAround") 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) { func doGoToNextSelection(ctx context.Context, state *Peco, _ Event) {
if pdebug.Enabled { if pdebug.Enabled {
g := pdebug.Marker("doGoToNextSelection") g := pdebug.Marker("doGoToNextSelection")
@ -793,6 +833,7 @@ func doGoToNextSelection(ctx context.Context, state *Peco, _ Event) {
doGoToAdjacentSelection(ctx, state, true) 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) { func doGoToPreviousSelection(ctx context.Context, state *Peco, _ Event) {
if pdebug.Enabled { if pdebug.Enabled {
g := pdebug.Marker("doGoToPreviousSelection") 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) { func doFreezeResults(ctx context.Context, state *Peco, _ Event) {
if pdebug.Enabled { if pdebug.Enabled {
g := pdebug.Marker("doFreezeResults") g := pdebug.Marker("doFreezeResults")
@ -908,6 +951,7 @@ func doFreezeResults(ctx context.Context, state *Peco, _ Event) {
state.Hub().SendDrawPrompt(ctx) 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) { func doUnfreezeResults(ctx context.Context, state *Peco, _ Event) {
if pdebug.Enabled { if pdebug.Enabled {
g := pdebug.Marker("doUnfreezeResults") g := pdebug.Marker("doUnfreezeResults")
@ -926,6 +970,8 @@ func doUnfreezeResults(ctx context.Context, state *Peco, _ Event) {
state.Hub().SendDrawPrompt(ctx) 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) { func doZoomIn(ctx context.Context, state *Peco, _ Event) {
if pdebug.Enabled { if pdebug.Enabled {
g := pdebug.Marker("doZoomIn") g := pdebug.Marker("doZoomIn")
@ -974,6 +1020,7 @@ func doZoomIn(ctx context.Context, state *Peco, _ Event) {
state.Hub().SendDraw(ctx, &hub.DrawOptions{DisableCache: true}) 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) { func doZoomOut(ctx context.Context, state *Peco, _ Event) {
if pdebug.Enabled { if pdebug.Enabled {
g := pdebug.Marker("doZoomOut") g := pdebug.Marker("doZoomOut")
@ -996,6 +1043,7 @@ func doZoomOut(ctx context.Context, state *Peco, _ Event) {
state.Hub().SendDraw(ctx, &hub.DrawOptions{DisableCache: true}) 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) { func doSingleKeyJump(ctx context.Context, state *Peco, e Event) {
if pdebug.Enabled { if pdebug.Enabled {
g := pdebug.Marker("doSingleKeyJump %c", e.Ch) 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 { func makeCombinedAction(actions ...Action) ActionFunc {
return ActionFunc(func(ctx context.Context, state *Peco, e Event) { return ActionFunc(func(ctx context.Context, state *Peco, e Event) {
batchAction(ctx, state, func(ctx context.Context) { batchAction(ctx, state, func(ctx context.Context) {

View file

@ -49,6 +49,8 @@ type ContextLine struct {
line.Line 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 { func NewFilteredBuffer(src Buffer, page, perPage int) *FilteredBuffer {
fb := FilteredBuffer{ fb := FilteredBuffer{
src: src, src: src,
@ -118,12 +120,15 @@ func NewMemoryBuffer(capacity int) *MemoryBuffer {
return mb return mb
} }
// Size returns the number of lines currently held in the buffer, thread-safe.
func (mb *MemoryBuffer) Size() int { func (mb *MemoryBuffer) Size() int {
mb.mutex.RLock() mb.mutex.RLock()
defer mb.mutex.RUnlock() defer mb.mutex.RUnlock()
return len(mb.lines) 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() { func (mb *MemoryBuffer) Reset() {
mb.mutex.Lock() mb.mutex.Lock()
defer mb.mutex.Unlock() 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{} { func (mb *MemoryBuffer) Done() <-chan struct{} {
mb.mutex.RLock() mb.mutex.RLock()
defer mb.mutex.RUnlock() defer mb.mutex.RUnlock()
return mb.done 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) { func (mb *MemoryBuffer) Accept(ctx context.Context, in <-chan line.Line, _ pipeline.ChanOutput) {
if pdebug.Enabled { if pdebug.Enabled {
g := pdebug.Marker("MemoryBuffer.Accept") g := pdebug.Marker("MemoryBuffer.Accept")
@ -226,18 +235,22 @@ func (mb *MemoryBuffer) AppendLine(l line.Line) {
mb.mutex.Unlock() mb.mutex.Unlock()
} }
// LineAt returns the line at the given index, thread-safe.
func (mb *MemoryBuffer) LineAt(n int) (line.Line, error) { func (mb *MemoryBuffer) LineAt(n int) (line.Line, error) {
mb.mutex.RLock() mb.mutex.RLock()
defer mb.mutex.RUnlock() defer mb.mutex.RUnlock()
return bufferLineAt(mb.lines, n) 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 { func (mb *MemoryBuffer) linesInRange(start, end int) []line.Line {
mb.mutex.RLock() mb.mutex.RLock()
defer mb.mutex.RUnlock() defer mb.mutex.RUnlock()
return mb.lines[start:end] 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) { func bufferLineAt(lines []line.Line, n int) (line.Line, error) {
if s := len(lines); s <= 0 || n >= s { if s := len(lines); s <= 0 || n >= s {
return nil, errors.New("empty buffer") return nil, errors.New("empty buffer")
@ -372,6 +385,7 @@ func (cb *ContextBuffer) LineAt(i int) (line.Line, error) {
return cb.entries[i], nil 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 { func (cb *ContextBuffer) linesInRange(start, end int) []line.Line {
return cb.entries[start:end] return cb.entries[start:end]
} }

View file

@ -8,22 +8,27 @@ type Caret struct {
pos int pos int
} }
// Pos returns the current caret position, thread-safe.
func (c *Caret) Pos() int { func (c *Caret) Pos() int {
c.mutex.Lock() c.mutex.Lock()
defer c.mutex.Unlock() defer c.mutex.Unlock()
return c.pos 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) { func (c *Caret) setPosNL(p int) {
c.pos = p c.pos = p
} }
// SetPos sets the caret position, thread-safe.
func (c *Caret) SetPos(p int) { func (c *Caret) SetPos(p int) {
c.mutex.Lock() c.mutex.Lock()
defer c.mutex.Unlock() defer c.mutex.Unlock()
c.setPosNL(p) c.setPosNL(p)
} }
// Move moves the caret by the given delta, thread-safe.
func (c *Caret) Move(diff int) { func (c *Caret) Move(diff int) {
c.mutex.Lock() c.mutex.Lock()
defer c.mutex.Unlock() defer c.mutex.Unlock()

View file

@ -21,6 +21,8 @@ const (
OnCancelError OnCancelBehavior = "error" 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 { func (o *OnCancelBehavior) UnmarshalText(b []byte) error {
switch s := string(b); s { switch s := string(b); s {
case "", "success": case "", "success":
@ -224,6 +226,8 @@ func NewStyleSet() *StyleSet {
return ss 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() { func (ss *StyleSet) Init() {
ss.Basic.fg = ColorDefault ss.Basic.fg = ColorDefault
ss.Basic.bg = ColorDefault ss.Basic.bg = ColorDefault
@ -259,6 +263,8 @@ func (s *Style) UnmarshalYAML(unmarshal func(any) error) error {
return stringsToStyle(s, raw) 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 { func stringsToStyle(style *Style, raw []string) error {
style.fg = ColorDefault style.fg = ColorDefault
style.bg = ColorDefault style.bg = ColorDefault
@ -312,6 +318,8 @@ type configLocateFunc func(string) (string, error)
var configFilenames = []string{"config.json", "config.yaml", "config.yml"} 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) { func locateRcfileIn(dir string) (string, error) {
for _, basename := range configFilenames { for _, basename := range configFilenames {
file := filepath.Join(dir, basename) file := filepath.Join(dir, basename)

View file

@ -32,6 +32,8 @@ type filterProcessor struct {
onError func(error) 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 { func newFilterProcessor(f filter.Filter, q string, bufSize int, onError func(error)) *filterProcessor {
return &filterProcessor{ return &filterProcessor{
filter: f, 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) { func (fp *filterProcessor) Accept(ctx context.Context, in <-chan line.Line, out pipeline.ChanOutput) {
acceptAndFilter(ctx, fp.filter, fp.bufSize, fp.onError, in, out) 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(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) { 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 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) { 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) flush := make(chan []line.Line)
flushDone := make(chan struct{}) 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 }) 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) { 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) flush := make(chan orderedChunk)
flushDone := make(chan struct{}) 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 { func NewFilter(state *Peco) *Filter {
return &Filter{ return &Filter{
state: state, state: state,

View file

@ -14,6 +14,7 @@ type baseFilter struct {
applyFn func(ctx context.Context, lines []line.Line, emit func(line.Line)) error 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 { func (b *baseFilter) NewContext(ctx context.Context, query string) context.Context {
return newContext(ctx, query) return newContext(ctx, query)
} }
@ -22,12 +23,15 @@ func (b *baseFilter) BufSize() int {
return 0 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 { func (b *baseFilter) Apply(ctx context.Context, lines []line.Line, out pipeline.ChanOutput) error {
return b.applyFn(ctx, lines, func(l line.Line) { return b.applyFn(ctx, lines, func(l line.Line) {
_ = out.Send(ctx, l) _ = 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) { func (b *baseFilter) ApplyCollect(ctx context.Context, lines []line.Line) ([]line.Line, error) {
result := make([]line.Line, 0, len(lines)/2) result := make([]line.Line, 0, len(lines)/2)
err := b.applyFn(ctx, lines, func(l line.Line) { err := b.applyFn(ctx, lines, func(l line.Line) {

View file

@ -56,6 +56,7 @@ func (ecf ExternalCmd) BufSize() int {
return ecf.thresholdBufsiz 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 { func (ecf *ExternalCmd) NewContext(ctx context.Context, query string) context.Context {
return newContext(ctx, query) return newContext(ctx, query)
} }
@ -68,6 +69,8 @@ func (ecf ExternalCmd) String() string {
return ecf.name 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) { func (ecf *ExternalCmd) Apply(ctx context.Context, buf []line.Line, out pipeline.ChanOutput) (err error) {
var readerPanicErr error var readerPanicErr error

View file

@ -80,15 +80,19 @@ func (m byMatchStart) Less(i, j int) bool {
return false return false
} }
// matchContains reports whether match range a fully contains match range b.
func matchContains(a []int, b []int) bool { func matchContains(a []int, b []int) bool {
return a[0] <= b[0] && a[1] >= b[1] return a[0] <= b[0] && a[1] >= b[1]
} }
// matchOverlaps reports whether two match ranges overlap.
func matchOverlaps(a []int, b []int) bool { func matchOverlaps(a []int, b []int) bool {
return a[0] <= b[0] && a[1] >= b[0] || return a[0] <= b[0] && a[1] >= b[0] ||
a[0] <= b[1] && a[1] >= b[1] 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 { func mergeMatches(a []int, b []int) []int {
ret := make([]int, 2) ret := make([]int, 2)

View file

@ -46,6 +46,8 @@ func (ff Fuzzy) String() string {
return "Fuzzy" 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 { func (ff *Fuzzy) applyInternal(ctx context.Context, lines []line.Line, emit func(line.Line)) error {
originalQuery := pipeline.QueryFromContext(ctx) originalQuery := pipeline.QueryFromContext(ctx)
@ -183,11 +185,15 @@ LINE:
return nil 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) { func popRune(s string) (string, rune, int) {
r, n := utf8.DecodeRuneInString(s) r, n := utf8.DecodeRuneInString(s)
return s[n:], r, n 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 { func less(s []fuzzyMatchedItem) func(i, j int) bool {
return func(i, j int) bool { return func(i, j int) bool {
if s[i].longest != s[j].longest { if s[i].longest != s[j].longest {
@ -210,6 +216,8 @@ type fuzzyMatchedItem struct {
earliest int 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 { func newFuzzyMatchedItem(line line.Line, matches [][]int) fuzzyMatchedItem {
longest := 0 longest := 0
count := 0 count := 0

View file

@ -55,6 +55,8 @@ func (r regexpFlagFunc) flags(s string) []string {
return r(s) 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) { func regexpFor(q string, flags []string, quotemeta bool) (*regexp.Regexp, error) {
reTxt := q reTxt := q
if quotemeta { if quotemeta {
@ -141,6 +143,8 @@ func NewIRegexp() *Regexp {
const maxRegexpCacheSize = 100 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) { func (f *regexpQueryFactory) Compile(s string, flags regexpFlags, quotemeta bool) (positive, negative []*regexp.Regexp, err error) {
f.mutex.Lock() f.mutex.Lock()
defer f.mutex.Unlock() defer f.mutex.Unlock()
@ -190,6 +194,8 @@ func (f *regexpQueryFactory) Compile(s string, flags regexpFlags, quotemeta bool
return posRxs, negRxs, nil 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 { func (rf *Regexp) applyInternal(ctx context.Context, lines []line.Line, emit func(line.Line)) error {
query := pipeline.QueryFromContext(ctx) query := pipeline.QueryFromContext(ctx)
posRegexps, negRegexps, err := rf.factory.Compile(query, rf.flags, rf.quotemeta) posRegexps, negRegexps, err := rf.factory.Compile(query, rf.flags, rf.quotemeta)
@ -272,10 +278,12 @@ func (rf *Regexp) String() string {
return rf.name return rf.name
} }
// NewIgnoreCase creates a case-insensitive literal string filter.
func NewIgnoreCase() *Regexp { func NewIgnoreCase() *Regexp {
return newRegexpFilter("IgnoreCase", ignoreCaseFlags, true) return newRegexpFilter("IgnoreCase", ignoreCaseFlags, true)
} }
// NewCaseSensitive creates a case-sensitive literal string filter.
func NewCaseSensitive() *Regexp { func NewCaseSensitive() *Regexp {
return newRegexpFilter("CaseSensitive", defaultFlags, true) return newRegexpFilter("CaseSensitive", defaultFlags, true)
} }

View file

@ -19,24 +19,28 @@ type Set struct {
mutex sync.Mutex mutex sync.Mutex
} }
// Reset sets the active filter back to the first one in the set.
func (fs *Set) Reset() { func (fs *Set) Reset() {
fs.mutex.Lock() fs.mutex.Lock()
defer fs.mutex.Unlock() defer fs.mutex.Unlock()
fs.current = 0 fs.current = 0
} }
// Size returns the number of filters in the set.
func (fs *Set) Size() int { func (fs *Set) Size() int {
fs.mutex.Lock() fs.mutex.Lock()
defer fs.mutex.Unlock() defer fs.mutex.Unlock()
return len(fs.filters) return len(fs.filters)
} }
// Add appends a new filter to the set.
func (fs *Set) Add(lf Filter) { func (fs *Set) Add(lf Filter) {
fs.mutex.Lock() fs.mutex.Lock()
defer fs.mutex.Unlock() defer fs.mutex.Unlock()
fs.filters = append(fs.filters, lf) fs.filters = append(fs.filters, lf)
} }
// Rotate cycles to the next filter in the set, wrapping around to the first.
func (fs *Set) Rotate() { func (fs *Set) Rotate() {
fs.mutex.Lock() fs.mutex.Lock()
defer fs.mutex.Unlock() 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 { func (fs *Set) SetCurrentByName(name string) error {
fs.mutex.Lock() fs.mutex.Lock()
defer fs.mutex.Unlock() defer fs.mutex.Unlock()

View file

@ -113,6 +113,7 @@ var doneChPool = sync.Pool{
}, },
} }
// waitDone blocks until the receiver signals completion by calling Done.
func (p *Payload[T]) waitDone() { func (p *Payload[T]) waitDone() {
// Save the channel reference before blocking. This read is safe because // Save the channel reference before blocking. This read is safe because
// p.done was set by send() on this same goroutine before the payload // 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) doneChPool.Put(ch)
} }
// isBatchCtx reports whether the context was created by a Batch call.
func isBatchCtx(ctx context.Context) bool { func isBatchCtx(ctx context.Context) bool {
var isBatchMode bool var isBatchMode bool
v := ctx.Value(batchPayloadKey{}) v := ctx.Value(batchPayloadKey{})
@ -223,6 +225,7 @@ func (r statusMsgReq) Delay() time.Duration {
return r.delay return r.delay
} }
// newStatusMsgReq creates a StatusMsg with the given message text and display duration.
func newStatusMsgReq(s string, d time.Duration) *statusMsgReq { func newStatusMsgReq(s string, d time.Duration) *statusMsgReq {
return &statusMsgReq{ return &statusMsgReq{
msg: s, msg: s,

View file

@ -13,6 +13,7 @@ type Input struct {
state *Peco state *Peco
} }
// NewInput creates and returns a new Input instance for handling keyboard events.
func NewInput(state *Peco, am ActionMap, src chan Event) *Input { func NewInput(state *Peco, am ActionMap, src chan Event) *Input {
return &Input{ return &Input{
actions: am, 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 { func (i *Input) Loop(ctx context.Context, cancel func()) error {
defer cancel() 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 { func (i *Input) handleInputEvent(ctx context.Context, ev Event) error {
if pdebug.Enabled { if pdebug.Enabled {
g := pdebug.Marker("event received from user: %#v", ev) g := pdebug.Marker("event received from user: %#v", ev)

View file

@ -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) { func ReleaseLineListBuf(l []line.Line) {
if l == nil { if l == nil {
return return
@ -22,6 +23,7 @@ func ReleaseLineListBuf(l []line.Line) {
lineListPool.Put(l) //nolint:staticcheck // SA6002: converting to pointer-based pool breaks tests 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 { func GetLineListBuf() []line.Line {
l, _ := lineListPool.Get().([]line.Line) l, _ := lineListPool.Get().([]line.Line)
return l return l

View file

@ -20,16 +20,19 @@ func (n *nodeData) Value() any {
return n.value return n.value
} }
// NewMatcher creates a new Aho-Corasick matcher for multi-pattern key sequence matching.
func NewMatcher() *Matcher { func NewMatcher() *Matcher {
return &Matcher{ return &Matcher{
NewTernaryTrie(), NewTernaryTrie(),
} }
} }
// Clear removes all patterns from the matcher, resetting it to empty state.
func (m *Matcher) Clear() { func (m *Matcher) Clear() {
m.Root().RemoveAll() m.Root().RemoveAll()
} }
// Add inserts a key sequence pattern with an associated value into the matcher.
func (m *Matcher) Add(pattern KeyList, v any) { func (m *Matcher) Add(pattern KeyList, v any) {
m.Put(pattern, &nodeData{ m.Put(pattern, &nodeData{
pattern: &pattern, 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 { func (m *Matcher) Compile() error {
m.Balance() m.Balance()
root, _ := m.Root().(*TernaryNode) root, _ := m.Root().(*TernaryNode)
@ -54,6 +58,7 @@ func (m *Matcher) Compile() error {
return nil return nil
} }
// fillFailure recursively computes the failure link for curr based on its parent's failure chain.
func fillFailure(curr, root, parent *TernaryNode) { func fillFailure(curr, root, parent *TernaryNode) {
data := getNodeData(curr) data := getNodeData(curr)
if data == nil { if data == nil {
@ -69,12 +74,14 @@ func fillFailure(curr, root, parent *TernaryNode) {
data.failure = fnode 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 { func (m *Matcher) Match(text KeyList) <-chan Match {
ch := make(chan Match, 1) ch := make(chan Match, 1)
go m.startMatch(text, ch) go m.startMatch(text, ch)
return 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) { func (m *Matcher) startMatch(text KeyList, ch chan<- Match) {
defer close(ch) defer close(ch)
root, _ := m.Root().(*TernaryNode) 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 { func getNextNode(node, root *TernaryNode, r Key) *TernaryNode {
for { for {
next, _ := node.Get(r).(*TernaryNode) 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) { func fireAll(curr, root *TernaryNode, ch chan<- Match, idx int) {
for curr != root { for curr != root {
data := getNodeData(curr) 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 { func getNodeData(node *TernaryNode) *nodeData {
d, _ := node.Value().(*nodeData) d, _ := node.Value().(*nodeData)
return d return d
} }
// getNodeFailure returns the failure link for node, falling back to root if none is set.
func getNodeFailure(node, root *TernaryNode) *TernaryNode { func getNodeFailure(node, root *TernaryNode) *TernaryNode {
next := getNodeData(node).failure next := getNodeData(node).failure
if next == nil { if next == nil {

View file

@ -104,6 +104,7 @@ const (
var stringToKey = map[string]KeyType{} var stringToKey = map[string]KeyType{}
var keyToString = map[KeyType]string{} 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) { func mapkey(n string, k KeyType) {
stringToKey[n] = k stringToKey[n] = k
keyToString[k] = n keyToString[k] = n
@ -193,6 +194,7 @@ func init() {
mapkey("C-8", KeyCtrl8) 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) { func ToKeyList(ksk string) (KeyList, error) {
list := KeyList{} list := KeyList{}
for term := range strings.SplitSeq(ksk, ",") { for term := range strings.SplitSeq(ksk, ",") {
@ -241,6 +243,7 @@ func KeyEventToString(key KeyType, ch rune, mod ModifierKey) (string, error) {
return s, nil 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) { func ToKey(key string) (k KeyType, modifier ModifierKey, ch rune, err error) {
modifier = ModNone modifier = ModNone

View file

@ -25,6 +25,7 @@ type Key struct {
Ch rune Ch rune
} }
// String returns a comma-separated string representation of the key list.
func (kl KeyList) String() string { func (kl KeyList) String() string {
list := make([]string, len(kl)) list := make([]string, len(kl))
for i := range kl { for i := range kl {
@ -33,6 +34,7 @@ func (kl KeyList) String() string {
return strings.Join(list, ",") return strings.Join(list, ",")
} }
// String returns the modifier key as a dash-separated string (e.g. "C-S-M").
func (m ModifierKey) String() string { func (m ModifierKey) String() string {
var parts []string var parts []string
if m&ModCtrl != 0 { if m&ModCtrl != 0 {
@ -47,6 +49,7 @@ func (m ModifierKey) String() string {
return strings.Join(parts, "-") return strings.Join(parts, "-")
} }
// String returns a human-readable representation of the key, including any modifiers.
func (k Key) String() string { func (k Key) String() string {
var s string var s string
if m := k.Modifier.String(); m != "" { if m := k.Modifier.String(); m != "" {
@ -62,6 +65,7 @@ func (k Key) String() string {
return s return s
} }
// NewKeyFromKey creates a Key from a KeyType with no modifier and no rune.
func NewKeyFromKey(k KeyType) Key { func NewKeyFromKey(k KeyType) Key {
return Key{ return Key{
Modifier: 0, Modifier: 0,
@ -73,6 +77,7 @@ func NewKeyFromKey(k KeyType) Key {
// KeyList is just the list of keys // KeyList is just the list of keys
type KeyList []Key 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 { func (k Key) Compare(x Key) int {
if k.Modifier < x.Modifier { if k.Modifier < x.Modifier {
return -1 return -1
@ -95,6 +100,7 @@ func (k Key) Compare(x Key) int {
return 0 return 0
} }
// Equals reports whether kl and x contain the same keys in the same order.
func (kl KeyList) Equals(x KeyList) bool { func (kl KeyList) Equals(x KeyList) bool {
if len(kl) != len(x) { if len(kl) != len(x) {
return false return false
@ -119,6 +125,7 @@ type Keyseq struct {
mutex sync.Mutex mutex sync.Mutex
} }
// New creates a new Keyseq matcher for resolving multi-key bindings.
func New() *Keyseq { func New() *Keyseq {
return &Keyseq{ return &Keyseq{
Matcher: NewMatcher(), 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 { func (k *Keyseq) InMiddleOfChain() bool {
return k.current != nil && k.current != k.Matcher 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() { func (k *Keyseq) CancelChain() {
k.mutex.Lock() k.mutex.Lock()
defer k.mutex.Unlock() defer k.mutex.Unlock()
@ -148,6 +157,8 @@ func (k *Keyseq) Current() keyseqMatcher {
return k.current 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) { func (k *Keyseq) AcceptKey(key Key) (any, error) {
// XXX should we return Action instead of interface{}? // XXX should we return Action instead of interface{}?
k.mutex.Lock() k.mutex.Lock()

View file

@ -4,6 +4,7 @@ type TernaryTrie struct {
root TernaryNode root TernaryNode
} }
// NewTernaryTrie creates a new empty ternary search trie.
func NewTernaryTrie() *TernaryTrie { func NewTernaryTrie() *TernaryTrie {
return &TernaryTrie{} return &TernaryTrie{}
} }
@ -24,6 +25,7 @@ func (t *TernaryTrie) Put(k KeyList, v any) Node {
return Put(t, k, v) return Put(t, k, v)
} }
// Size returns the total number of nodes in the trie.
func (t *TernaryTrie) Size() int { func (t *TernaryTrie) Size() int {
count := 0 count := 0
EachDepth(t, func(Node) bool { EachDepth(t, func(Node) bool {
@ -33,6 +35,7 @@ func (t *TernaryTrie) Size() int {
return count return count
} }
// Balance rebalances all sibling lists in the trie for optimal search performance.
func (t *TernaryTrie) Balance() { func (t *TernaryTrie) Balance() {
EachDepth(t, func(n Node) bool { EachDepth(t, func(n Node) bool {
tn, _ := n.(*TernaryNode) tn, _ := n.(*TernaryNode)
@ -49,14 +52,17 @@ type TernaryNode struct {
value any value any
} }
// NewTernaryNode creates a new ternary trie node with the given key label.
func NewTernaryNode(l Key) *TernaryNode { func NewTernaryNode(l Key) *TernaryNode {
return &TernaryNode{label: l} return &TernaryNode{label: l}
} }
// GetList looks up a child node matching the first key in the list.
func (n *TernaryNode) GetList(k KeyList) Node { func (n *TernaryNode) GetList(k KeyList) Node {
return n.Get(k[0]) 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 { func (n *TernaryNode) Get(k Key) Node {
curr := n.firstChild curr := n.firstChild
for curr != nil { for curr != nil {
@ -72,6 +78,7 @@ func (n *TernaryNode) Get(k Key) Node {
return nil 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) { func (n *TernaryNode) Dig(k Key) (node Node, isnew bool) {
curr := n.firstChild curr := n.firstChild
if curr == nil { if curr == nil {
@ -106,6 +113,7 @@ func (n *TernaryNode) HasChildren() bool {
return n.firstChild != nil return n.firstChild != nil
} }
// Size returns the number of direct children of this node.
func (n *TernaryNode) Size() int { func (n *TernaryNode) Size() int {
if n.firstChild == nil { if n.firstChild == nil {
return 0 return 0
@ -118,6 +126,7 @@ func (n *TernaryNode) Size() int {
return count 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) { func (n *TernaryNode) Each(proc func(Node) bool) {
var f func(*TernaryNode) bool var f func(*TernaryNode) bool
f = func(n *TernaryNode) bool { f = func(n *TernaryNode) bool {
@ -131,6 +140,7 @@ func (n *TernaryNode) Each(proc func(Node) bool) {
f(n.firstChild) f(n.firstChild)
} }
// RemoveAll removes all children from this node.
func (n *TernaryNode) RemoveAll() { func (n *TernaryNode) RemoveAll() {
n.firstChild = nil n.firstChild = nil
} }
@ -147,6 +157,7 @@ func (n *TernaryNode) SetValue(v any) {
n.value = v n.value = v
} }
// children collects all direct child nodes into a sorted slice.
func (n *TernaryNode) children() []*TernaryNode { func (n *TernaryNode) children() []*TernaryNode {
children := make([]*TernaryNode, n.Size()) children := make([]*TernaryNode, n.Size())
if n.firstChild == nil { if n.firstChild == nil {
@ -162,6 +173,7 @@ func (n *TernaryNode) children() []*TernaryNode {
return children return children
} }
// Balance rebalances the children of this node into a balanced binary search tree.
func (n *TernaryNode) Balance() { func (n *TernaryNode) Balance() {
if n.firstChild == nil { if n.firstChild == nil {
return return
@ -174,6 +186,7 @@ func (n *TernaryNode) Balance() {
n.firstChild = balance(children, 0, len(children)) 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 { func balance(nodes []*TernaryNode, s, e int) *TernaryNode {
count := e - s count := e - s
if count <= 0 { if count <= 0 {

View file

@ -12,10 +12,12 @@ type Trie interface {
Size() int Size() int
} }
// NewTrie creates a new empty Trie for storing key sequences.
func NewTrie() Trie { func NewTrie() Trie {
return NewTernaryTrie() 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 { func Get(t Trie, k KeyList) Node {
if t == nil { if t == nil {
return nil return nil
@ -30,6 +32,7 @@ func Get(t Trie, k KeyList) Node {
return n 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 { func Put(t Trie, k KeyList, v any) Node {
if t == nil { if t == nil {
return nil return nil
@ -42,6 +45,7 @@ func Put(t Trie, k KeyList, v any) Node {
return n return n
} }
// EachDepth iterates over trie nodes in depth-first order, calling proc for each node.
func EachDepth(t Trie, proc func(Node) bool) { func EachDepth(t Trie, proc func(Node) bool) {
if t == nil { if t == nil {
return return
@ -55,6 +59,7 @@ func EachDepth(t Trie, proc func(Node) bool) {
r.Each(f) 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) { func EachWidth(t Trie, proc func(Node) bool) {
if t == nil { if t == nil {
return return
@ -92,6 +97,7 @@ type Node interface {
SetValue(v any) SetValue(v any)
} }
// Children returns all child nodes of n as a slice.
func Children(n Node) []Node { func Children(n Node) []Node {
children := make([]Node, n.Size()) children := make([]Node, n.Size())
idx := 0 idx := 0

View file

@ -7,6 +7,7 @@ import (
"os" "os"
) )
// Homedir returns the current user's home directory from the HOME environment variable.
func Homedir() (string, error) { func Homedir() (string, error) {
home := os.Getenv("HOME") home := os.Getenv("HOME")
if home == "" { if home == "" {

View file

@ -7,6 +7,7 @@ import (
"os/exec" "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 { func Shell(ctx context.Context, cmd ...string) *exec.Cmd {
const shellpath = `/bin/sh` const shellpath = `/bin/sh`
const shellopt = `-c` const shellopt = `-c`

View file

@ -10,6 +10,7 @@ type fder interface {
Fd() uintptr Fd() uintptr
} }
// CaseInsensitiveIndexFunc returns a function that matches runes equal to r, ignoring case.
func CaseInsensitiveIndexFunc(r rune) func(rune) bool { func CaseInsensitiveIndexFunc(r rune) func(rune) bool {
lr := unicode.ToUpper(r) lr := unicode.ToUpper(r)
return func(v rune) bool { 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 { func ContainsUpper(query string) bool {
for _, c := range query { for _, c := range query {
if unicode.IsUpper(c) { if unicode.IsUpper(c) {
@ -46,6 +48,7 @@ type exitStatuser interface {
ExitStatus() int ExitStatus() int
} }
// IsIgnorableError checks whether err implements the Ignorable interface and returns true.
func IsIgnorableError(err error) bool { func IsIgnorableError(err error) bool {
for e := err; e != nil; e = errors.Unwrap(e) { for e := err; e != nil; e = errors.Unwrap(e) {
if v, ok := e.(ignorable); ok { if v, ok := e.(ignorable); ok {
@ -55,6 +58,7 @@ func IsIgnorableError(err error) bool {
return false return false
} }
// IsCollectResultsError checks whether err signals that results should be collected.
func IsCollectResultsError(err error) bool { func IsCollectResultsError(err error) bool {
for e := err; e != nil; e = errors.Unwrap(e) { for e := err; e != nil; e = errors.Unwrap(e) {
if v, ok := e.(collectResults); ok { if v, ok := e.(collectResults); ok {
@ -64,6 +68,7 @@ func IsCollectResultsError(err error) bool {
return false return false
} }
// GetExitStatus extracts the exit status code from an error, returning 1 and false if not found.
func GetExitStatus(err error) (int, bool) { func GetExitStatus(err error) (int, bool) {
for e := err; e != nil; e = errors.Unwrap(e) { for e := err; e != nil; e = errors.Unwrap(e) {
if ese, ok := e.(exitStatuser); ok { if ese, ok := e.(exitStatuser); ok {

View file

@ -43,6 +43,7 @@ func (km Keymap) Sequence() Keyseq {
return km.seq 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) { func (km Keymap) ExecuteAction(ctx context.Context, state *Peco, ev Event) (err error) {
if pdebug.Enabled { if pdebug.Enabled {
g := pdebug.Marker("Keymap.ExecuteAction %v", ev).BindError(&err) 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 { func wrapRememberSequence(a Action) Action {
return ActionFunc(func(ctx context.Context, state *Peco, ev Event) { return ActionFunc(func(ctx context.Context, state *Peco, ev Event) {
if s, err := keyseq.KeyEventToString(ev.Key, ev.Ch, ev.Mod); err == nil { 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 { func wrapClearSequence(a Action) Action {
return ActionFunc(func(ctx context.Context, state *Peco, ev Event) { return ActionFunc(func(ctx context.Context, state *Peco, ev Event) {
seq := state.Inputseq() seq := state.Inputseq()
@ -121,6 +124,7 @@ func wrapClearSequence(a Action) Action {
const maxResolveActionDepth = 100 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) { func (km Keymap) resolveActionName(name string, depth int) (Action, error) {
if depth >= maxResolveActionDepth { if depth >= maxResolveActionDepth {
return nil, fmt.Errorf("could not resolve %s: deep recursion", name) return nil, fmt.Errorf("could not resolve %s: deep recursion", name)

View file

@ -358,6 +358,7 @@ func newScreenStatusBar(screen Screen, anchor VerticalAnchor, anchorOffset int,
}, nil }, nil
} }
// stopTimer stops and drains the clear timer, preventing stale events from firing.
func (s *screenStatusBar) stopTimer() { func (s *screenStatusBar) stopTimer() {
s.timerMutex.Lock() s.timerMutex.Lock()
defer s.timerMutex.Unlock() 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) { func (s *screenStatusBar) setClearTimer(t *time.Timer) {
s.timerMutex.Lock() s.timerMutex.Lock()
defer s.timerMutex.Unlock() defer s.timerMutex.Unlock()
@ -469,6 +471,7 @@ func (l *ListArea) SetDirty(dirty bool) {
l.dirty = dirty l.dirty = dirty
} }
// selectionContains reports whether the line at index n is in the current selection.
func selectionContains(state *Peco, n int) bool { func selectionContains(state *Peco, n int) bool {
if l, err := state.CurrentLineBuffer().LineAt(n); err == nil { if l, err := state.CurrentLineBuffer().LineAt(n); err == nil {
return state.Selection().Has(l) 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 { func (l *BasicLayout) linesPerPage() int {
_, height := l.screen.Size() _, height := l.screen.Size()

View file

@ -34,6 +34,7 @@ type CLIOptions struct {
OptHeight string `long:"height" description:"display height in lines or percentage (e.g. '10', '50%')"` 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) { func (options *CLIOptions) parse(s []string) ([]string, error) {
p := flags.NewParser(options, flags.PrintErrors) p := flags.NewParser(options, flags.PrintErrors)
args, err := p.ParseArgs(s) args, err := p.ParseArgs(s)
@ -49,6 +50,7 @@ func (options *CLIOptions) parse(s []string) ([]string, error) {
return args, nil return args, nil
} }
// Validate checks the parsed CLI options for correctness (e.g., layout type).
func (options CLIOptions) Validate() error { func (options CLIOptions) Validate() error {
if options.OptLayout != "" { if options.OptLayout != "" {
if !IsValidLayoutType(LayoutType(options.OptLayout)) { if !IsValidLayoutType(LayoutType(options.OptLayout)) {
@ -58,6 +60,7 @@ func (options CLIOptions) Validate() error {
return nil return nil
} }
// help generates formatted help text from struct field tags.
func (options CLIOptions) help() []byte { func (options CLIOptions) help() []byte {
buf := bytes.Buffer{} buf := bytes.Buffer{}

View file

@ -105,6 +105,7 @@ func (l *Location) MaxPage() int {
return l.maxPage return l.maxPage
} }
// PageCrop returns a PageCrop snapshot of the current page and perPage values under a read lock.
func (l *Location) PageCrop() PageCrop { func (l *Location) PageCrop() PageCrop {
l.mutex.RLock() l.mutex.RLock()
defer l.mutex.RUnlock() defer l.mutex.RUnlock()

39
peco.go
View file

@ -411,6 +411,8 @@ func (p *Peco) Setup() (err error) {
return nil 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() { func (p *Peco) selectOneAndExitIfPossible() {
// If we have only one line, we just want to bail out // If we have only one line, we just want to bail out
// printing that one line as the result. // 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() { func (p *Peco) selectOneCallback() func() {
if p.selectOneAndExit { if p.selectOneAndExit {
return p.selectOneAndExitIfPossible return p.selectOneAndExitIfPossible
@ -438,12 +442,16 @@ func (p *Peco) selectOneCallback() func() {
return nil 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() { func (p *Peco) exitZeroIfPossible() {
if p.CurrentLineBuffer().Size() == 0 { if p.CurrentLineBuffer().Size() == 0 {
p.Exit(setExitStatus(makeIgnorable(errors.New("no input, exiting")), 1)) 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() { func (p *Peco) selectAllAndExitIfPossible() {
b := p.CurrentLineBuffer() b := p.CurrentLineBuffer()
selection := p.Selection() 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) { func (p *Peco) Run(ctx context.Context) (err error) {
if pdebug.Enabled { if pdebug.Enabled {
g := pdebug.Marker("Peco.Run").BindError(&err) g := pdebug.Marker("Peco.Run").BindError(&err)
@ -624,6 +634,8 @@ func (p *Peco) Run(ctx context.Context) (err error) {
return p.Err() 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 { func (p *Peco) parseCommandLine(opts *CLIOptions, args *[]string, argv []string) error {
remaining, err := opts.parse(argv) remaining, err := opts.parse(argv)
if err != nil { if err != nil {
@ -653,6 +665,8 @@ func (p *Peco) parseCommandLine(opts *CLIOptions, args *[]string, argv []string)
return nil 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) { func (p *Peco) SetupSource(ctx context.Context) (s *Source, err error) {
if pdebug.Enabled { if pdebug.Enabled {
g := pdebug.Marker("Peco.SetupSource").BindError(&err) 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 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 { func readConfig(cfg *Config, filename string) error {
if filename != "" { if filename != "" {
if err := cfg.ReadFilename(filename); err != nil { if err := cfg.ReadFilename(filename); err != nil {
@ -711,6 +727,8 @@ func readConfig(cfg *Config, filename string) error {
return nil 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 { func (p *Peco) ApplyConfig(opts CLIOptions) error {
// If layoutType is not set and is set in the config, set it // If layoutType is not set and is set in the config, set it
if p.layoutType == "" { if p.layoutType == "" {
@ -810,6 +828,8 @@ func (p *Peco) ApplyConfig(opts CLIOptions) error {
return nil return nil
} }
// populateInitialFilter sets the initial active filter based on the
// --initial-filter flag or the InitialFilter config value.
func (p *Peco) populateInitialFilter() error { func (p *Peco) populateInitialFilter() error {
if v := p.initialFilter; len(v) > 0 { if v := p.initialFilter; len(v) > 0 {
if err := p.filters.SetCurrentByName(v); err != nil { if err := p.filters.SetCurrentByName(v); err != nil {
@ -819,6 +839,8 @@ func (p *Peco) populateInitialFilter() error {
return nil 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 func (p *Peco) populateSingleKeyJump() error { //nolint:unparam
p.singleKeyJump.showPrefix = p.config.SingleKeyJump.ShowPrefix p.singleKeyJump.showPrefix = p.config.SingleKeyJump.ShowPrefix
@ -836,6 +858,8 @@ func (p *Peco) populateSingleKeyJump() error { //nolint:unparam
return nil 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() { func (p *Peco) populateFilters() {
p.filters.Add(filter.NewIgnoreCase()) p.filters.Add(filter.NewIgnoreCase())
p.filters.Add(filter.NewCaseSensitive()) 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 { func (p *Peco) populateKeymap() error {
// Create a new keymap object // Create a new keymap object
k := NewKeymap(p.config.Keymap, p.config.Action) k := NewKeymap(p.config.Keymap, p.config.Action)
@ -862,17 +888,22 @@ func (p *Peco) populateKeymap() error {
return nil return nil
} }
// populateStyles applies the style settings from config to the Peco StyleSet.
func (p *Peco) populateStyles() error { //nolint:unparam func (p *Peco) populateStyles() error { //nolint:unparam
p.styles = p.config.Style p.styles = p.config.Style
return nil 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 { func (p *Peco) CurrentLineBuffer() Buffer {
p.mutex.Lock() p.mutex.Lock()
defer p.mutex.Unlock() defer p.mutex.Unlock()
return p.currentLineBuffer 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) { func (p *Peco) SetCurrentLineBuffer(ctx context.Context, b Buffer) {
p.mutex.Lock() p.mutex.Lock()
defer p.mutex.Unlock() defer p.mutex.Unlock()
@ -884,6 +915,8 @@ func (p *Peco) SetCurrentLineBuffer(ctx context.Context, b Buffer) {
go p.Hub().SendDraw(ctx, nil) 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) { func (p *Peco) ResetCurrentLineBuffer(ctx context.Context) {
if fs := p.Frozen().Source(); fs != nil { if fs := p.Frozen().Source(); fs != nil {
p.SetCurrentLineBuffer(ctx, fs) 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()) { func (p *Peco) sendQuery(ctx context.Context, q string, nextFunc func()) {
if pdebug.Enabled { if pdebug.Enabled {
g := pdebug.Marker("sending query to filter goroutine (q=%v, isInfinite=%t)", q, p.source.IsInfinite()) 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 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() { func (p *Peco) PrintResults() {
if pdebug.Enabled { if pdebug.Enabled {
g := pdebug.Marker("Peco.PrintResults") g := pdebug.Marker("Peco.PrintResults")

View file

@ -174,6 +174,7 @@ func (p *Pipeline) Run(ctx context.Context) (err error) {
return nil return nil
} }
// Done returns a channel that is closed when the pipeline completes.
func (p *Pipeline) Done() <-chan struct{} { func (p *Pipeline) Done() <-chan struct{} {
p.mutex.Lock() p.mutex.Lock()
defer p.mutex.Unlock() defer p.mutex.Unlock()

View file

@ -36,6 +36,7 @@ func (q *Query) SaveQuery() {
q.query = []rune(nil) 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) { func (q *Query) DeleteRange(start, end int) {
q.mutex.Lock() q.mutex.Lock()
defer q.mutex.Unlock() defer q.mutex.Unlock()
@ -88,6 +89,7 @@ func (q *Query) RuneAt(where int) rune {
return q.query[where] return q.query[where]
} }
// InsertAt inserts a rune at the specified position in the query.
func (q *Query) InsertAt(ch rune, where int) { func (q *Query) InsertAt(ch rune, where int) {
q.mutex.Lock() q.mutex.Lock()
defer q.mutex.Unlock() defer q.mutex.Unlock()

View file

@ -261,6 +261,7 @@ func (t *TcellScreen) Init(_ *Config) error {
return nil return nil
} }
// NewTcellScreen creates a new TcellScreen with initialized channels and default error output.
func NewTcellScreen() *TcellScreen { func NewTcellScreen() *TcellScreen {
return &TcellScreen{ return &TcellScreen{
suspendCh: make(chan struct{}), suspendCh: make(chan struct{}),
@ -414,6 +415,7 @@ func (t *TcellScreen) PollEvent(ctx context.Context, cfg *Config) chan Event {
return evCh return evCh
} }
// Suspend signals the event polling goroutine to suspend the screen.
func (t *TcellScreen) Suspend() { func (t *TcellScreen) Suspend() {
select { select {
case t.suspendCh <- struct{}{}: 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 { func (t *TcellScreen) Resume(ctx context.Context) error {
// Resume must be a block operation, because we can't safely proceed // Resume must be a block operation, because we can't safely proceed
// without actually knowing that the screen has been re-initialized. // 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) 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 { func screenPrint(t Screen, args PrintArgs) int {
var written int var written int

View file

@ -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 { func (s *InlineScreen) Init(_ *Config) error {
// Save and override TCELL_ALTSCREEN to prevent alternate screen buffer // Save and override TCELL_ALTSCREEN to prevent alternate screen buffer
s.savedAltscreen = os.Getenv("TCELL_ALTSCREEN") s.savedAltscreen = os.Getenv("TCELL_ALTSCREEN")
@ -171,6 +172,7 @@ func (s *InlineScreen) Size() (int, int) {
return w, s.height 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 { func (s *InlineScreen) PollEvent(ctx context.Context, _ *Config) chan Event {
evCh := make(chan Event) evCh := make(chan Event)

View file

@ -36,6 +36,7 @@ func (s *Selection) Add(l line.Line) {
s.tree.ReplaceOrInsert(l) s.tree.ReplaceOrInsert(l)
} }
// Copy copies all selected lines from s into dst.
func (s *Selection) Copy(dst *Selection) { func (s *Selection) Copy(dst *Selection) {
s.Ascend(func(it btree.Item) bool { s.Ascend(func(it btree.Item) bool {
l, ok := it.(line.Line) l, ok := it.(line.Line)
@ -54,24 +55,28 @@ func (s *Selection) Remove(l line.Line) {
s.tree.Delete(l) s.tree.Delete(l)
} }
// Reset clears all selected indices from the selection.
func (s *Selection) Reset() { func (s *Selection) Reset() {
s.mutex.Lock() s.mutex.Lock()
defer s.mutex.Unlock() defer s.mutex.Unlock()
s.tree = btree.New(32) s.tree = btree.New(32)
} }
// Has reports whether the given line is in the selection.
func (s *Selection) Has(x line.Line) bool { func (s *Selection) Has(x line.Line) bool {
s.mutex.RLock() s.mutex.RLock()
defer s.mutex.RUnlock() defer s.mutex.RUnlock()
return s.tree.Has(x) return s.tree.Has(x)
} }
// Len returns the number of selected lines.
func (s *Selection) Len() int { func (s *Selection) Len() int {
s.mutex.RLock() s.mutex.RLock()
defer s.mutex.RUnlock() defer s.mutex.RUnlock()
return s.tree.Len() return s.tree.Len()
} }
// Ascend iterates over selected lines in ascending order, calling i for each.
func (s *Selection) Ascend(i btree.ItemIterator) { func (s *Selection) Ascend(i btree.ItemIterator) {
s.mutex.RLock() s.mutex.RLock()
defer s.mutex.RUnlock() defer s.mutex.RUnlock()

View file

@ -14,6 +14,7 @@ type ReceivedHandler interface {
type ReceivedHandlerFunc func(os.Signal) type ReceivedHandlerFunc func(os.Signal)
// Handle calls the underlying function with the received signal.
func (s ReceivedHandlerFunc) Handle(sig os.Signal) { func (s ReceivedHandlerFunc) Handle(sig os.Signal) {
s(sig) s(sig)
} }
@ -23,6 +24,7 @@ type Handler struct {
sigCh chan os.Signal 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 { func New(h ReceivedHandler, sigs ...os.Signal) *Handler {
if len(sigs) == 0 { if len(sigs) == 0 {
sigs = append(sigs, syscall.SIGTERM, syscall.SIGINT, syscall.SIGHUP) 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 { func (h *Handler) Loop(ctx context.Context, cancel func()) error {
defer cancel() defer cancel()
defer signal.Stop(h.sigCh) defer signal.Stop(h.sigCh)

View file

@ -62,10 +62,13 @@ func NewSource(name string, in io.Reader, isInfinite bool, idgen line.IDGenerato
return s return s
} }
// Name returns the display name of this source.
func (s *Source) Name() string { func (s *Source) Name() string {
return s.name 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 { func (s *Source) IsInfinite() bool {
return s.isInfinite && !s.inClosed 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) { func (s *Source) Start(ctx context.Context, out pipeline.ChanOutput) {
var sent int var sent int
// I should be the only one running this method until I bail out // 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 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 { func (s *Source) linesInRange(start, end int) []line.Line {
s.mutex.RLock() s.mutex.RLock()
defer s.mutex.RUnlock() defer s.mutex.RUnlock()
return s.lines[start:end] 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) { func (s *Source) LineAt(n int) (line.Line, error) {
s.mutex.RLock() s.mutex.RLock()
defer s.mutex.RUnlock() defer s.mutex.RUnlock()
return bufferLineAt(s.lines, n) return bufferLineAt(s.lines, n)
} }
// Size returns the number of lines currently in the buffer.
func (s *Source) Size() int { func (s *Source) Size() int {
s.mutex.RLock() s.mutex.RLock()
defer s.mutex.RUnlock() defer s.mutex.RUnlock()
return len(s.lines) 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) { func (s *Source) Append(l line.Line) {
s.mutex.Lock() s.mutex.Lock()
defer s.mutex.Unlock() defer s.mutex.Unlock()

View file

@ -13,6 +13,7 @@ type View struct {
state *Peco state *Peco
} }
// NewView creates a new View with the given state and its configured layout.
func NewView(state *Peco) (*View, error) { func NewView(state *Peco) (*View, error) {
layout, err := NewLayout(LayoutType(state.LayoutType()), state) layout, err := NewLayout(LayoutType(state.LayoutType()), state)
if err != nil { if err != nil {
@ -24,6 +25,8 @@ func NewView(state *Peco) (*View, error) {
}, nil }, 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 { func (v *View) Loop(ctx context.Context, cancel func()) error {
defer cancel() 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]) { func (v *View) printStatus(p *hub.Payload[hub.StatusMsg]) {
defer p.Done() defer p.Done()
r := p.Data() r := p.Data()
v.layout.PrintStatus(r.Message(), r.Delay()) 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]) { func (v *View) purgeDisplayCache(p *hub.Payload[*hub.DrawOptions]) {
defer p.Done() defer p.Done()
v.layout.PurgeDisplayCache() 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) { func (v *View) drawScreen(p *hub.Payload[*hub.DrawOptions], options *hub.DrawOptions) {
defer p.Done() defer p.Done()
v.layout.DrawScreen(v.state, options) v.layout.DrawScreen(v.state, options)
} }
// drawPrompt renders the query prompt line with cursor position.
func (v *View) drawPrompt(p *hub.Payload[*hub.DrawOptions]) { func (v *View) drawPrompt(p *hub.Payload[*hub.DrawOptions]) {
defer p.Done() defer p.Done()
v.layout.DrawPrompt(v.state) 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]) { func (v *View) movePage(p *hub.Payload[hub.PagingRequest]) {
defer p.Done() defer p.Done()