mirror of
https://github.com/peco/peco.git
synced 2026-09-10 07:16:29 -04:00
Methodically add function comments
This commit is contained in:
parent
93fcda4a08
commit
fc18c0d3de
49
action.go
49
action.go
|
|
@ -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) {
|
||||||
|
|
|
||||||
14
buffer.go
14
buffer.go
|
|
@ -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]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
5
caret.go
5
caret.go
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
10
filter.go
10
filter.go
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
3
input.go
3
input.go
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 == "" {
|
||||||
|
|
|
||||||
|
|
@ -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`
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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{}
|
||||||
|
|
||||||
|
|
|
||||||
1
page.go
1
page.go
|
|
@ -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
39
peco.go
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
2
query.go
2
query.go
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
11
source.go
11
source.go
|
|
@ -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()
|
||||||
|
|
|
||||||
9
view.go
9
view.go
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue