Compare commits

...

69 commits

Author SHA1 Message Date
lestrrat 4368da0fbf
Merge pull request #843 from peco/dependabot/go_modules/github.com/stretchr/testify-1.12.1
Some checks failed
CI / Go ${{ matrix.go }} test (1.25) (push) Has been cancelled
CI / Check generated code (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
lint / lint (push) Has been cancelled
build(deps): bump github.com/stretchr/testify from 1.11.1 to 1.12.1
2026-09-05 10:24:55 +09:00
dependabot[bot] d473d30d3a
build(deps): bump github.com/stretchr/testify from 1.11.1 to 1.12.1
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.11.1 to 1.12.1.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.11.1...v1.12.1)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-version: 1.12.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-05 01:21:22 +00:00
lestrrat ea8d6f064d
Merge pull request #842 from peco/dependabot/go_modules/github.com/mattn/go-runewidth-0.0.28
build(deps): bump github.com/mattn/go-runewidth from 0.0.27 to 0.0.28
2026-09-05 10:20:16 +09:00
lestrrat 578b59851e
Merge pull request #844 from peco/dependabot/github_actions/github/codeql-action-4.37.9
build(deps): bump github/codeql-action from 4.37.3 to 4.37.9
2026-09-05 10:19:51 +09:00
dependabot[bot] beaf90d759
build(deps): bump github/codeql-action from 4.37.3 to 4.37.9
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.37.3 to 4.37.9.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v4.37.3...v4.37.9)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.37.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-29 00:53:11 +00:00
dependabot[bot] ed3e39d9a2
build(deps): bump github.com/mattn/go-runewidth from 0.0.27 to 0.0.28
Bumps [github.com/mattn/go-runewidth](https://github.com/mattn/go-runewidth) from 0.0.27 to 0.0.28.
- [Commits](https://github.com/mattn/go-runewidth/compare/v0.0.27...v0.0.28)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-runewidth
  dependency-version: 0.0.28
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-22 10:02:31 +00:00
lestrrat 2e25a62fcd
Merge pull request #838 from peco/dependabot/github_actions/actions/checkout-7.0.1
Some checks failed
CI / Go ${{ matrix.go }} test (1.25) (push) Has been cancelled
CI / Check generated code (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
lint / lint (push) Has been cancelled
build(deps): bump actions/checkout from 7.0.0 to 7.0.1
2026-08-21 17:23:12 +09:00
dependabot[bot] e7ffb77baa
build(deps): bump actions/checkout from 7.0.0 to 7.0.1
Bumps [actions/checkout](https://github.com/actions/checkout) from 7.0.0 to 7.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](9c091bb21b...3d3c42e5aa)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-21 08:18:13 +00:00
lestrrat ed3929e8c4
Merge pull request #840 from peco/dependabot/go_modules/github.com/mattn/go-runewidth-0.0.27
build(deps): bump github.com/mattn/go-runewidth from 0.0.24 to 0.0.27
2026-08-21 17:18:09 +09:00
lestrrat 3752f7fc38
Merge pull request #839 from peco/dependabot/github_actions/github/codeql-action-4.37.3
build(deps): bump github/codeql-action from 4 to 4.37.3
2026-08-21 17:17:54 +09:00
lestrrat 40bcfa25d1
Merge pull request #835 from peco/dependabot/github_actions/actions/setup-go-7.0.0
build(deps): bump actions/setup-go from 6.5.0 to 7.0.0
2026-08-21 17:16:28 +09:00
lestrrat f730b08b5d
Merge pull request #825 from peco/update-manpage
docs: update man page
2026-08-21 17:15:36 +09:00
dependabot[bot] e947b755d7
build(deps): bump github.com/mattn/go-runewidth from 0.0.24 to 0.0.27
Bumps [github.com/mattn/go-runewidth](https://github.com/mattn/go-runewidth) from 0.0.24 to 0.0.27.
- [Commits](https://github.com/mattn/go-runewidth/compare/v0.0.24...v0.0.27)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-runewidth
  dependency-version: 0.0.27
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-01 10:02:37 +00:00
dependabot[bot] a45e60b285
build(deps): bump github/codeql-action from 4 to 4.37.3
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4 to 4.37.3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v4...v4.37.3)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.37.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-01 00:53:52 +00:00
dependabot[bot] a4afa02128
build(deps): bump actions/setup-go from 6.5.0 to 7.0.0
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 6.5.0 to 7.0.0.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](924ae3a1cd...b7ad1dad31)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-18 00:52:13 +00:00
lestrrat c3f424413e
Merge pull request #834 from peco/dependabot/github_actions/golangci/golangci-lint-action-9.3.0
build(deps): bump golangci/golangci-lint-action from 9.2.1 to 9.3.0
2026-07-12 19:46:16 +09:00
dependabot[bot] 51ff07275e
build(deps): bump golangci/golangci-lint-action from 9.2.1 to 9.3.0
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 9.2.1 to 9.3.0.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](82606bf257...ba0d7d2ec0)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-version: 9.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-12 10:44:52 +00:00
lestrrat 49d9793ea1
Merge pull request #833 from peco/dependabot/github_actions/goreleaser/goreleaser-action-7.2.3
build(deps): bump goreleaser/goreleaser-action from 7.2.2 to 7.2.3
2026-07-12 19:44:14 +09:00
dependabot[bot] 7966368c66
build(deps): bump goreleaser/goreleaser-action from 7.2.2 to 7.2.3
Bumps [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) from 7.2.2 to 7.2.3.
- [Release notes](https://github.com/goreleaser/goreleaser-action/releases)
- [Commits](5daf1e915a...f06c13b6b1)

---
updated-dependencies:
- dependency-name: goreleaser/goreleaser-action
  dependency-version: 7.2.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-12 10:42:50 +00:00
lestrrat baf8773274
Merge pull request #832 from peco/dependabot/github_actions/actions/setup-go-6.5.0
build(deps): bump actions/setup-go from 6.4.0 to 6.5.0
2026-07-12 19:41:25 +09:00
dependabot[bot] f4fbedefa2
build(deps): bump actions/setup-go from 6.4.0 to 6.5.0
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 6.4.0 to 6.5.0.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](4a3601121d...924ae3a1cd)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: 6.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-12 10:39:46 +00:00
lestrrat d9c865b45f
Merge pull request #831 from peco/dependabot/github_actions/actions/cache-6.1.0
build(deps): bump actions/cache from 5.0.5 to 6.1.0
2026-07-12 19:38:55 +09:00
dependabot[bot] 78dc0ea123
build(deps): bump actions/cache from 5.0.5 to 6.1.0
Bumps [actions/cache](https://github.com/actions/cache) from 5.0.5 to 6.1.0.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](27d5ce7f10...55cc834586)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-12 10:37:21 +00:00
lestrrat 55667855d4
Merge pull request #829 from peco/dependabot/github_actions/actions/checkout-7.0.0
build(deps): bump actions/checkout from 6.0.2 to 7.0.0
2026-07-12 19:35:46 +09:00
dependabot[bot] 08e3c514de
build(deps): bump actions/checkout from 6.0.2 to 7.0.0
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.2 to 7.0.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](de0fac2e45...9c091bb21b)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-20 00:52:16 +00:00
lestrrat 8762bd7da0 docs: regenerate man page from README 2026-06-05 05:10:47 +00:00
lestrrat 41a8422b22
Merge pull request #822 from peco/feat-follow-mode
Some checks failed
CI / Go ${{ matrix.go }} test (1.25) (push) Has been cancelled
CI / Check generated code (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
lint / lint (push) Has been cancelled
Update man page / update-manpage (push) Has been cancelled
add follow mode for streaming input
2026-06-05 14:10:24 +09:00
Daisuke Maki a66446d2e2 add follow mode for streaming input 2026-06-04 11:16:23 +09:00
lestrrat 5f6ec1b21c
Merge pull request #821 from peco/perf-source-buffer-cap
amortize capped Source.Append to O(1)
2026-06-04 11:15:14 +09:00
Daisuke Maki 643ad7881d modernize benchmark loop to b.Loop()
CI's golangci-lint modernize check flags `for range b.N`; switch to
`b.Loop()`, which also handles timer reset internally.
2026-06-04 11:13:46 +09:00
Daisuke Maki 886f02bdee fix data race in Source.linesInRange under in-place compaction
In-place compaction in Append overwrites and then clears positions in the
backing array. linesInRange returned a slice aliased into that array and
released the read lock before the caller iterated it, so a concurrent
compaction could nil out the caller's view (panic on DisplayString()) and
trip the race detector. Return a copy.

Add a -race regression test that pins the contract.
2026-06-04 10:14:55 +09:00
Daisuke Maki ac77b9f2ff amortize capped Source.Append to O(1) 2026-06-04 07:54:04 +09:00
lestrrat 7e657cfd9f
Merge pull request #818 from peco/dependabot/go_modules/github.com/mattn/go-runewidth-0.0.24
build(deps): bump github.com/mattn/go-runewidth from 0.0.23 to 0.0.24
2026-05-31 18:06:49 +09:00
lestrrat 35ea2ba494
Merge pull request #819 from peco/dependabot/go_modules/github.com/gdamore/tcell/v2-2.13.10
build(deps): bump github.com/gdamore/tcell/v2 from 2.13.9 to 2.13.10
2026-05-31 18:06:33 +09:00
dependabot[bot] b819e73236
build(deps): bump github.com/gdamore/tcell/v2 from 2.13.9 to 2.13.10
Bumps [github.com/gdamore/tcell/v2](https://github.com/gdamore/tcell) from 2.13.9 to 2.13.10.
- [Release notes](https://github.com/gdamore/tcell/releases)
- [Changelog](https://github.com/gdamore/tcell/blob/main/CHANGESv3.md)
- [Commits](https://github.com/gdamore/tcell/compare/v2.13.9...v2.13.10)

---
updated-dependencies:
- dependency-name: github.com/gdamore/tcell/v2
  dependency-version: 2.13.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-30 10:02:29 +00:00
dependabot[bot] 70243267cd
build(deps): bump github.com/mattn/go-runewidth from 0.0.23 to 0.0.24
Bumps [github.com/mattn/go-runewidth](https://github.com/mattn/go-runewidth) from 0.0.23 to 0.0.24.
- [Commits](https://github.com/mattn/go-runewidth/compare/v0.0.23...v0.0.24)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-runewidth
  dependency-version: 0.0.24
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-30 10:02:24 +00:00
lestrrat 4008f2e7bd
Merge pull request #817 from peco/dependabot/github_actions/goreleaser/goreleaser-action-7.2.2
build(deps): bump goreleaser/goreleaser-action from 7.2.1 to 7.2.2
2026-05-27 10:23:53 +09:00
lestrrat adc039f3a8
Merge pull request #816 from peco/dependabot/github_actions/golangci/golangci-lint-action-9.2.1
build(deps): bump golangci/golangci-lint-action from 9.2.0 to 9.2.1
2026-05-27 10:23:40 +09:00
dependabot[bot] 003bf6b456
build(deps): bump goreleaser/goreleaser-action from 7.2.1 to 7.2.2
Bumps [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) from 7.2.1 to 7.2.2.
- [Release notes](https://github.com/goreleaser/goreleaser-action/releases)
- [Commits](1a80836c5c...5daf1e915a)

---
updated-dependencies:
- dependency-name: goreleaser/goreleaser-action
  dependency-version: 7.2.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-23 00:52:14 +00:00
dependabot[bot] 45c4bf09ac
build(deps): bump golangci/golangci-lint-action from 9.2.0 to 9.2.1
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 9.2.0 to 9.2.1.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](1e7e51e771...82606bf257)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-version: 9.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-23 00:52:10 +00:00
lestrrat 78c41943e8
Merge pull request #815 from peco/dependabot/github_actions/goreleaser/goreleaser-action-7.2.1
build(deps): bump goreleaser/goreleaser-action from 7.1.0 to 7.2.1
2026-05-11 09:10:48 +09:00
dependabot[bot] 68be2eeb7f
build(deps): bump goreleaser/goreleaser-action from 7.1.0 to 7.2.1
Bumps [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) from 7.1.0 to 7.2.1.
- [Release notes](https://github.com/goreleaser/goreleaser-action/releases)
- [Commits](e24998b8b6...1a80836c5c)

---
updated-dependencies:
- dependency-name: goreleaser/goreleaser-action
  dependency-version: 7.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-02 03:26:37 +00:00
lestrrat 69c9ca8cd7
Merge pull request #812 from peco/dependabot/github_actions/actions/cache-5.0.5
build(deps): bump actions/cache from 5.0.4 to 5.0.5
2026-04-25 21:59:29 +09:00
lestrrat b461fd28cc
Merge pull request #814 from peco/dependabot/go_modules/github.com/gdamore/tcell/v2-2.13.9
build(deps): bump github.com/gdamore/tcell/v2 from 2.13.8 to 2.13.9
2026-04-25 21:59:20 +09:00
dependabot[bot] 3df3d9735f
build(deps): bump github.com/gdamore/tcell/v2 from 2.13.8 to 2.13.9
Bumps [github.com/gdamore/tcell/v2](https://github.com/gdamore/tcell) from 2.13.8 to 2.13.9.
- [Release notes](https://github.com/gdamore/tcell/releases)
- [Changelog](https://github.com/gdamore/tcell/blob/main/CHANGESv3.md)
- [Commits](https://github.com/gdamore/tcell/compare/v2.13.8...v2.13.9)

---
updated-dependencies:
- dependency-name: github.com/gdamore/tcell/v2
  dependency-version: 2.13.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-25 10:02:29 +00:00
lestrrat 45dae94e47
Merge pull request #813 from peco/dependabot/github_actions/goreleaser/goreleaser-action-7.1.0
build(deps): bump goreleaser/goreleaser-action from 7.0.0 to 7.1.0
2026-04-25 16:21:16 +09:00
dependabot[bot] 8c7dd793d9
build(deps): bump goreleaser/goreleaser-action from 7.0.0 to 7.1.0
Bumps [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) from 7.0.0 to 7.1.0.
- [Release notes](https://github.com/goreleaser/goreleaser-action/releases)
- [Commits](ec59f474b9...e24998b8b6)

---
updated-dependencies:
- dependency-name: goreleaser/goreleaser-action
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-25 00:52:29 +00:00
dependabot[bot] d842d4124e
build(deps): bump actions/cache from 5.0.4 to 5.0.5
Bumps [actions/cache](https://github.com/actions/cache) from 5.0.4 to 5.0.5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](668228422a...27d5ce7f10)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: 5.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-18 00:52:13 +00:00
lestrrat cb1c747fe6
Merge pull request #811 from peco/dependabot/go_modules/github.com/mattn/go-runewidth-0.0.23
build(deps): bump github.com/mattn/go-runewidth from 0.0.21 to 0.0.23
2026-04-11 19:04:06 +09:00
lestrrat 0188a6534c
Merge pull request #810 from peco/dependabot/github_actions/peter-evans/create-pull-request-8.1.1
build(deps): bump peter-evans/create-pull-request from 8.1.0 to 8.1.1
2026-04-11 19:03:11 +09:00
lestrrat 8c9aca356b
Merge pull request #808 from peco/dependabot/github_actions/actions/setup-go-6.4.0
build(deps): bump actions/setup-go from 6.3.0 to 6.4.0
2026-04-11 19:03:00 +09:00
dependabot[bot] dcd727a5b2
build(deps): bump github.com/mattn/go-runewidth from 0.0.21 to 0.0.23
Bumps [github.com/mattn/go-runewidth](https://github.com/mattn/go-runewidth) from 0.0.21 to 0.0.23.
- [Commits](https://github.com/mattn/go-runewidth/compare/v0.0.21...v0.0.23)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-runewidth
  dependency-version: 0.0.23
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-11 10:02:25 +00:00
dependabot[bot] 8e925b8870
build(deps): bump peter-evans/create-pull-request from 8.1.0 to 8.1.1
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 8.1.0 to 8.1.1.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](c0f553fe54...5f6978faf0)

---
updated-dependencies:
- dependency-name: peter-evans/create-pull-request
  dependency-version: 8.1.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-11 00:52:20 +00:00
dependabot[bot] 93de8d0469
build(deps): bump actions/setup-go from 6.3.0 to 6.4.0
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 6.3.0 to 6.4.0.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](4b73464bb3...4a3601121d)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: 6.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-04 00:52:14 +00:00
lestrrat 778ae4368e
Merge pull request #807 from peco/dependabot/github_actions/actions/cache-5.0.4
build(deps): bump actions/cache from 5.0.3 to 5.0.4
2026-03-24 17:13:52 +09:00
dependabot[bot] c2d3f453bf
build(deps): bump actions/cache from 5.0.3 to 5.0.4
Bumps [actions/cache](https://github.com/actions/cache) from 5.0.3 to 5.0.4.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](cdf6c1fa76...668228422a)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: 5.0.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-21 00:52:19 +00:00
lestrrat db69779cf1
Merge pull request #806 from peco/docs-claude-init
add CLAUDE.md and .claude/docs for agent context
2026-03-08 10:47:52 +09:00
Daisuke Maki 3efedc3128 fix filterbench deps and exit code defaults in docs 2026-03-08 10:38:41 +09:00
Daisuke Maki 082d12b1f4 fix more doc inaccuracies from copilot review 2026-03-08 10:28:35 +09:00
Daisuke Maki df830dc80d fix inaccuracies in agent docs from copilot review 2026-03-08 09:38:22 +09:00
lestrrat 16d700eb28
Merge pull request #805 from peco/dependabot/go_modules/github.com/mattn/go-runewidth-0.0.21
build(deps): bump github.com/mattn/go-runewidth from 0.0.20 to 0.0.21
2026-03-08 09:17:30 +09:00
Daisuke Maki 1fbf855884 add CLAUDE.md and .claude/docs for agent context 2026-03-08 09:09:36 +09:00
dependabot[bot] 6bc81c893f
build(deps): bump github.com/mattn/go-runewidth from 0.0.20 to 0.0.21
Bumps [github.com/mattn/go-runewidth](https://github.com/mattn/go-runewidth) from 0.0.20 to 0.0.21.
- [Commits](https://github.com/mattn/go-runewidth/compare/v0.0.20...v0.0.21)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-runewidth
  dependency-version: 0.0.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-07 10:02:25 +00:00
lestrrat b1734bbc34
Merge pull request #804 from masawada/fix-ctrl-space-selection
fix: Ctrl+Space (ToggleSelection) not working after tcell migration
2026-03-02 12:32:41 +09:00
Masayoshi Wada 42c39e38c8 fix: Ctrl+Space (ToggleSelection) not working after tcell migration
tcellEventToEvent did not convert Ctrl+Space to KeyCtrlSpace for
CSI u terminals or traditional terminals (tcell emits KeyCtrlSpace
which fell outside the KeyCtrlA..KeyCtrlZ range).
2026-03-01 20:42:04 +09:00
lestrrat 1eab243c5e
Merge pull request #803 from peco/dependabot/github_actions/goreleaser/goreleaser-action-7.0.0
build(deps): bump goreleaser/goreleaser-action from 6.4.0 to 7.0.0
2026-02-28 22:13:23 +09:00
lestrrat d232db1e32
Merge pull request #802 from peco/dependabot/github_actions/actions/setup-go-6.3.0
build(deps): bump actions/setup-go from 6.2.0 to 6.3.0
2026-02-28 22:13:11 +09:00
dependabot[bot] 14ed0ef01e
build(deps): bump goreleaser/goreleaser-action from 6.4.0 to 7.0.0
Bumps [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) from 6.4.0 to 7.0.0.
- [Release notes](https://github.com/goreleaser/goreleaser-action/releases)
- [Commits](e435ccd777...ec59f474b9)

---
updated-dependencies:
- dependency-name: goreleaser/goreleaser-action
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-28 00:52:15 +00:00
dependabot[bot] 3ed0b3015e
build(deps): bump actions/setup-go from 6.2.0 to 6.3.0
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 6.2.0 to 6.3.0.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](7a3fe6cf4c...4b73464bb3)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: 6.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-28 00:52:10 +00:00
30 changed files with 1272 additions and 69 deletions

50
.claude/docs/cli.md Normal file
View file

@ -0,0 +1,50 @@
<!-- Agent-consumed file. Keep terse, unambiguous, machine-parseable. -->
# CLI
## Entry Point
`cmd/peco/peco.go` — parses flags, creates `peco.New()`, calls `Run(ctx)`
## Flags (CLIOptions)
| Flag | Type | Description |
|------|------|-------------|
| `--help` | bool | Show help |
| `--version` | bool | Show version |
| `--query` | string | Initial query string |
| `--rcfile` | string | Config file path |
| `--buffer-size` | int | Max lines to read (0=unlimited) |
| `--null` | bool | Use NUL as line separator |
| `--initial-index` | int | Initial cursor position |
| `--initial-filter` | string | Initial filter name |
| `--prompt` | string | Prompt string |
| `--layout` | string | Layout type: top-down, bottom-up, top-down-query-bottom |
| `--select-1` | bool | Auto-select if single match |
| `--exit-zero` | bool | Exit immediately with status 1 if input is empty |
| `--select-all` | bool | Select all lines and exit immediately |
| `--on-cancel` | string | Cancel behavior: success/error |
| `--selection-prefix` | string | Prefix for selected lines |
| `--exec` | string | Command to execute with selection |
| `--print-query` | bool | Print query as first output line |
| `--color` | ColorMode | Color mode: auto, none |
| `--height` | string | Terminal height spec |
## Exit Codes
- 0 — success (lines selected)
- 0 — cancel (default, `--on-cancel success`)
- 1 — cancel with `--on-cancel error`
- Custom — from `--exec` command exit status
## Input
- Reads from stdin by default
- Positional arg → read from file
- Supports streaming (infinite) input
## Output
- Selected lines to stdout, one per line
- With `--print-query`: query string as first line
- With `--exec`: pipes selection to command

View file

@ -0,0 +1,57 @@
<!-- Agent-consumed file. Keep terse, unambiguous, machine-parseable. -->
# Internal Dependency Graph
```
cmd/peco → peco (root), internal/util
cmd/filterbench → peco (root), filter, line, pipeline
peco (root) → config, filter, hub, line, pipeline, query, selection, sig
→ internal/ansi, internal/keyseq, internal/util, internal/buffer
config → internal/util
filter → line, pipeline, internal/util
selection → line
line → internal/ansi
pipeline → line
internal/buffer → line
```
## Layer Grouping
### Leaf (no internal deps)
- `hub` — message bus, no imports
- `query` — query text/caret, no imports
- `sig` — signal handling, no imports
- `internal/ansi` — ANSI parser, no imports
- `internal/keyseq` — key matching, no imports
- `internal/util` — platform utils, no imports
### Core
- `line` → internal/ansi
- `pipeline` → line
- `internal/buffer` → line
### Processing
- `config` → internal/util
- `filter` → line, pipeline, internal/util
- `selection` → line
### Application
- `peco` (root) → all above
- `cmd/peco` → peco, internal/util
- `cmd/filterbench` → peco (root), filter, line, pipeline
## External Dependencies
- `github.com/gdamore/tcell/v2` — terminal UI (screen.go)
- `github.com/goccy/go-yaml` — config parsing
- `github.com/google/btree` — ordered selection storage
- `github.com/jessevdk/go-flags` — CLI flag parsing
- `github.com/lestrrat-go/pdebug` — debug logging
- `github.com/mattn/go-runewidth` — Unicode width calculation
- `github.com/stretchr/testify` — test assertions

102
.claude/docs/internals.md Normal file
View file

@ -0,0 +1,102 @@
<!-- Agent-consumed file. Keep terse, unambiguous, machine-parseable. -->
# Internals
## Concurrency Model
Three main goroutines coordinated via context cancellation:
1. **Input loop** (`input.go`) — reads terminal events, resolves key sequences via Keymap, dispatches actions
2. **View loop** (`view.go`) — renders screen in response to hub messages (draw, paging, status)
3. **Filter loop** (`filter.go`) — executes query against line buffer when query changes
Communication → **Hub** (`hub/`), central message bus with typed generic channels.
## Data Flow
```
stdin/file → Source → MemoryBuffer
User keystroke → Input → Hub.SendQuery() → Filter loop
Filter.Apply() → FilteredBuffer → Hub.SendDraw() → View loop
View → Layout → Screen (tcell) → terminal
```
## Hub Message Types
| Channel | Payload | Sender | Receiver |
|---------|---------|--------|----------|
| `QueryCh` | `string` | Input (action) | Filter loop |
| `DrawCh` | `*DrawOptions` | Filter, actions | View loop |
| `PagingCh` | `PagingRequest` | Input (action) | View loop |
| `StatusMsgCh` | `StatusMsg` | Various | View loop |
Hub supports **batch mode** — multiple sends within `Batch()` callback are processed together.
## Buffer Architecture
- `MemoryBuffer` — stores all input lines, grows as Source reads
- `FilteredBuffer` — wraps any Buffer with index range (page slice)
- `Source` — implements `pipeline.Source`, reads input lines into MemoryBuffer
- `ContextBuffer` — adds surrounding context lines for zoom view
- `CurrentLineBuffer()` returns raw MemoryBuffer (no filter) or FilteredBuffer (after filter)
## Filter Pipeline
1. Query change arrives via Hub
2. Filter loop creates pipeline: `MemoryBufferSource → filter.Apply → MemoryBuffer`
3. Filter.Apply runs in parallel chunks (if `SupportsParallel()`)
4. Results collected into new MemoryBuffer → set as CurrentLineBuffer
5. Hub.SendDraw() triggers View redraw
## Screen Abstraction
- `Screen` interface wraps terminal operations
- `TcellScreen` — production impl using tcell/v2
- `InlineScreen` — wraps TcellScreen for height-limited display
- `SimScreen` — test mock with event injection
## Layout System
- `BasicLayout` composes: `UserPrompt` + `ListArea` + `StatusBar`
- Layout variants registered via `RegisterLayout(name, LayoutBuilder)`
- Built-in: `top-down` (default), `bottom-up`, `top-down-query-bottom`
- `AnchorSettings` controls vertical positioning (top/bottom anchor)
## Key Sequence Resolution
- `internal/keyseq.Keyseq` uses AhoCorasick matcher
- Supports multi-key sequences (e.g., C-x,C-c)
- Longest-match-wins semantics
- `InMiddleOfChain()` indicates partial match in progress
## Selection Model
- `selection.Set` uses `google/btree` for ordered storage by line ID
- Supports: single select, multi-select (toggle), range select, select-all
- Sticky selection — persists across query changes (configurable)
## Action System
- ~40 built-in actions in `action.go`
- Actions implement `Action` interface: `Execute(ctx, *Peco, Event)`
- `ActionFunc` — function adapter with `Register()` for key binding
- Combined actions — multiple actions bound to single key sequence
- `Keymap.LookupAction(Event) → Action` — resolve event to action
## State Objects
| State | Purpose |
|-------|---------|
| `Location` | Current page, line number, offset, per-page count |
| `SingleKeyJumpState` | Single-key jump mode toggle and prefix map |
| `ZoomState` | Zoom view buffer and line reference |
| `FrozenState` | Frozen source buffer for suspend/resume |
| `QueryExecState` | Query execution delay timer |
## Object Pools
- `line.GetMatched/ReleaseMatched` — pool for Matched line wrappers
- `internal/buffer.GetLineListBuf/ReleaseLineListBuf` — pool for line slices

189
.claude/docs/packages.md Normal file
View file

@ -0,0 +1,189 @@
<!-- Agent-consumed file. Keep terse, unambiguous, machine-parseable. -->
# Package Map
## peco (root)
Interactive filtering tool core. Holds global state, goroutine loops, UI components.
- **New() → *Peco** — create new instance
- **(*Peco).Setup() → error** — initialize from config/options
- **(*Peco).Run(ctx) → error** — main event loop
- **(*Peco).ApplyConfig(CLIOptions) → error** — apply CLI flags to config
- **(*Peco).SetupSource(ctx) → (*Source, error)** — initialize input source
- **(*Peco).PrintResults()** — output selected lines to stdout
- **(*Peco).CurrentLineBuffer() → Buffer** — active line buffer (raw or filtered)
- **(*Peco).ExecQuery(ctx, func()) → bool** — execute filter with debounce
- Key types: `Peco`, `Buffer`, `FilteredBuffer`, `MemoryBuffer`, `Source`, `Screen`, `Layout`, `Action`, `Keymap`, `Event`, `CLIOptions`, `Location`, `PageCrop`
- Key interfaces: `MessageHub`, `Screen`, `Layout`, `Action`, `ActionMap`, `Buffer`, `Keyseq`, `ConfigReader`
- Screen impls: `TcellScreen` (production), `InlineScreen` (height-limited), `SimScreen` (tests)
- Layout impls: `BasicLayout` with builders: `DefaultLayout`, `BottomUpLayout`, `TopDownQueryBottomLayout`
- Files: `peco.go`, `action.go`, `buffer.go`, `event.go`, `filter.go`, `input.go`, `keymap.go`, `layout.go`, `layout_any.go`, `layout_windows.go`, `options.go`, `page.go`, `screen.go`, `screen_inline.go`, `source.go`, `state.go`, `view.go`, `vertical_anchor_gen.go`
- Imports: config, filter, hub, line, pipeline, query, selection, sig, internal/ansi, internal/buffer, internal/keyseq, internal/util
## cmd/peco
CLI entry point.
- Parses flags via `go-flags`, creates `peco.New()`, calls `Run(ctx)`
- Files: `peco.go`
- Imports: peco (root), internal/util
## cmd/filterbench
Benchmark tool for filter performance.
- Files: `main.go`
- Imports: peco (root), filter, line, pipeline
## config/
Configuration loading and types.
- **Config** — main config struct (Keymap, Action, Style, Layout, CustomFilter, SingleKeyJump, Height, etc.)
- **(*Config).Init() → error** — set defaults
- **(*Config).ReadFilename(string) → error** — load YAML config file
- **LocateRcfile(Locator) → (string, error)** — find config file path
- Key types: `Config`, `StyleSet`, `Style`, `Attribute`, `OnCancelBehavior`, `ColorMode`, `CustomFilterConfig`, `SingleKeyJumpConfig`, `HeightSpec`
- Color constants: `ColorDefault`, `ColorBlack`..`ColorWhite`, `AttrBold`, `AttrUnderline`, `AttrReverse`, `AttrTrueColor`
- Layout constants: `LayoutTypeTopDown`, `LayoutTypeBottomUp`, `LayoutTypeTopDownQueryBottom`
- Files: `config.go`, `style.go`, `height.go`, `layout.go`
- Imports: internal/util
## filter/
Filter algorithm implementations.
- **Filter** interface — `Apply(ctx, []line.Line, ChanOutput) → error`, `BufSize() → int`, `NewContext(ctx, string) → ctx`, `String() → string`, `SupportsParallel() → bool`
- **Collector** interface (optional) — `ApplyCollect(ctx, []line.Line) → ([]line.Line, error)`
- **Set** — filter collection with rotation: `Add(Filter)`, `Rotate()`, `Current() → Filter`, `SetCurrentByName(string) → error`
- Implementations: `NewIgnoreCase()`, `NewCaseSensitive()`, `NewSmartCase()`, `NewRegexp()`, `NewIRegexp()`, `NewFuzzy(sortLongest bool)`, `NewExternalCmd(name, cmd string, args []string, threshold int, idgen IDGenerator, enableSep bool)`
- Files: `filter.go`, `base.go`, `regexp.go`, `fuzzy.go`, `external.go`, `set.go`
- Imports: line, pipeline, internal/util
## hub/
Central message bus for goroutine communication.
- **New(bufsize int) → *Hub** — create hub with channel buffer size
- **(*Hub).SendDraw(ctx, *DrawOptions)** — trigger screen redraw
- **(*Hub).SendQuery(ctx, string)** — send query change
- **(*Hub).SendPaging(ctx, PagingRequest)** — send paging command
- **(*Hub).SendStatusMsg(ctx, string, time.Duration)** — show status message
- **(*Hub).Batch(ctx, func(ctx))** — batch multiple sends atomically
- Channel accessors: `DrawCh()`, `PagingCh()`, `QueryCh()`, `StatusMsgCh()`
- Key types: `Hub`, `Payload[T]`, `DrawOptions`, `PagingRequest`, `PagingRequestType`, `StatusMsg`
- Paging types: `ToLineAbove`, `ToLineBelow`, `ToScrollPageDown`, `ToScrollPageUp`, `ToScrollLeft`, `ToScrollRight`, `ToScrollFirstItem`, `ToScrollLastItem`, `ToLineInPage`
- Files: `hub.go`, `draw.go`, `paging.go`, `paging_request_type_gen.go`
- Imports: (none internal)
## line/
Line data types for display and selection.
- **Line** interface — `ID() → uint64`, `Buffer() → string`, `DisplayString() → string`, `Output() → string`, `IsDirty() → bool`, `SetDirty(bool)`, implements `btree.Item`
- **NewRaw(id uint64, s string, enableSep bool, stripANSI bool) → *Raw** — create raw line
- **NewMatched(Line, [][]int) → *Matched** — wrap line with match indices
- **GetMatched(Line, [][]int) → *Matched** — pooled allocation
- **ReleaseMatched(*Matched)** — return to pool
- **IDGenerator** interface — `Next() → uint64`
- Files: `raw.go`, `matched.go`
- Imports: internal/ansi, btree
## pipeline/
Generic source→acceptor→destination pipeline.
- **New() → *Pipeline** — create pipeline
- **(*Pipeline).SetSource(Source)** — set data source
- **(*Pipeline).Add(Acceptor)** — add processing stage
- **(*Pipeline).SetDestination(Destination)** — set terminal stage
- **(*Pipeline).Run(ctx) → error** — execute pipeline
- Key interfaces: `Source` (`Start`, `Reset`), `Acceptor` (`Accept`), `Destination` (`Accept`, `Reset`, `Done`), `Suspender` (optional `Suspend`/`Resume`)
- **ChanOutput** (chan line.Line) — `Send(ctx, line.Line) → error`, `OutCh() → <-chan line.Line`
- **NewQueryContext(ctx, string) → ctx** / **QueryFromContext(ctx) → string** — pass query through context
- Files: `pipeline.go`
- Imports: line
## query/
Query text and caret management.
- **Text** — query string with save/restore: `Set(string)`, `Reset()`, `SaveQuery()`, `RestoreSavedQuery()`, `DeleteRange(int, int)`, `InsertAt(rune, int)`, `String()`, `Len()`, `RuneSlice()`, `RuneAt(int)`
- **Caret** — cursor position: `Pos() → int`, `SetPos(int)`, `Move(int)`
- Files: `query.go`
- Imports: (none internal)
## selection/
Ordered selection storage using btree.
- **New() → *Set** — create selection set
- **(*Set).Add(line.Line)** — add to selection
- **(*Set).Remove(line.Line)** — remove from selection
- **(*Set).Has(line.Line) → bool** — check membership
- **(*Set).Len() → int** — count selected
- **(*Set).Ascend(func(line.Line) bool)** — iterate in order
- **(*Set).Copy(dst *Set)** — copy all items
- **RangeStart** — range selection start marker: `Valid()`, `Value()`, `SetValue(int)`, `Reset()`
- Files: `selection.go`
- Imports: line, btree
## sig/
OS signal handling.
- **New(handler ReceivedHandler, sigs ...os.Signal) → *Handler**
- **(*Handler).Loop(ctx, func()) → error** — signal listening loop
- **ReceivedHandler** interface — `Handle(os.Signal)`
- Files: `sig.go`
- Imports: (none internal)
## internal/ansi
ANSI escape sequence parser.
- **Parse(string) → ParseResult** — strip ANSI, extract color spans
- **ExtractSegment([]AttrSpan, start, end int) → []AttrSpan** — slice attr spans for substring
- Key types: `ParseResult` (`Stripped string`, `Attrs []AttrSpan`), `AttrSpan` (`Fg, Bg Attribute`, `Length int`)
- Files: `parser.go`
- Imports: (none internal)
## internal/buffer
Line list buffer pool.
- **GetLineListBuf() → []line.Line** — get from pool
- **ReleaseLineListBuf([]line.Line)** — return to pool
- Files: `line.go`
- Imports: line
## internal/keyseq
Key sequence matching (multi-key bindings).
- **New() → *Keyseq** — create matcher (uses AhoCorasick internally)
- **(*Keyseq).Add(KeyList, any)** — register key sequence → action
- **(*Keyseq).Compile() → error** — build matcher
- **(*Keyseq).AcceptKey(Key) → (any, error)** — feed key, get action if matched
- **ToKeyList(string) → (KeyList, error)** — parse "C-x,C-c" → KeyList
- **KeyEventToString(KeyType, rune, ModifierKey) → (string, error)** — event → name
- Key types: `Key`, `KeyList`, `KeyType`, `ModifierKey`
- Matcher impls: Trie, TernarySearch, AhoCorasick (AhoCorasick used by default)
- Files: `keyseq.go`, `keys.go`, `trie.go`, `ternary.go`, `ahocorasick.go`
- Imports: (none internal)
## internal/util
Platform utilities.
- **IsTty(io.Reader) → bool** — check if reader is terminal
- **Homedir() → (string, error)** — user home directory
- **Shell(ctx, string) → *exec.Cmd** — create shell command
- **StripANSISequence(string) → string** — remove ANSI escapes (deprecated, use internal/ansi)
- **IsCollectResultsError(error) → bool** — check for collect-results sentinel
- **IsIgnorableError(error) → bool** — check for ignorable errors
- **GetExitStatus(error) → (int, bool)** — extract exit code
- Platform files: `tty_posix.go`, `tty_bsd.go`, `tty_windows.go`, `shell_unix.go`, `shell_windows.go`, `homedir_posix.go`, `homedir_darwin.go`, `homedir_windows.go`
- Files: `util.go`
- Imports: (none internal)

58
.claude/docs/testing.md Normal file
View file

@ -0,0 +1,58 @@
<!-- Agent-consumed file. Keep terse, unambiguous, machine-parseable. -->
# Testing
## Commands
```bash
make test # go test -v -race ./...
go test -v -run TestFoo ./... # single test, all packages
go test -v -run TestFoo ./filter/ # single test, specific package
go test -race -coverprofile=coverage.out ./... # coverage
```
## Test Package Convention
- Tests use same package name (not `_test` suffix) — white-box testing
- Exception: some packages use `_test` suffix for external testing
## Key Test Helpers
- `newPeco() → *Peco` — creates test instance with SimScreen, default config
- `NewDummyScreen() → *SimScreen` — mock terminal; supports `SendEvent(Event)` for input injection
- SimScreen has fixed size, no-op rendering, collects events
## Test Patterns
- Table-driven tests with `t.Run()` subtests
- Regression tests for GitHub issues in `issues_test.go`
- Filter tests in `filter/filter_test.go`, `filter/base_test.go`
- Hub tests in `hub/hub_test.go`
- Key sequence tests in `internal/keyseq/trie_test.go`, `ahocorasick_test.go`, `ternary_test.go`
- Pipeline tests in `pipeline/pipeline_test.go`
- Selection tests in `selection/selection_test.go`
- Query tests in `query/query_test.go`
## Benchmark Tests
- `filter/bench_test.go` — filter algorithm benchmarks
- `hub/bench_test.go` — hub message passing benchmarks
- `line/bench_test.go` — line allocation benchmarks
- `internal/ansi/bench_test.go` — ANSI parsing benchmarks
- `internal/util/bench_test.go` — utility benchmarks
- `cmd/filterbench/` — standalone filter benchmark CLI
## No Test Data Directory
- No `testdata/` or golden files
- Tests use inline data and programmatic setup
## Build Tags
- Platform-specific files: `_posix.go`, `_bsd.go`, `_windows.go`, `_darwin.go`
- No custom build tags for testing
## Code Generation
- `go generate ./...` — runs `stringer` for enum types
- Generated files: `vertical_anchor_gen.go`, `hub/paging_request_type_gen.go`

View file

@ -10,9 +10,9 @@ jobs:
name: Go ${{ matrix.go }} test
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Cache Go modules
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/go/pkg/mod
@ -21,14 +21,14 @@ jobs:
restore-keys: |
${{ runner.os }}-go-
- name: Setup Go
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: ${{ matrix.go }}
check-latest: true
- name: Test
run: make test
- name: Install GoReleaser
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3
with:
install-only: true
- name: Build
@ -39,9 +39,9 @@ jobs:
name: Check generated code
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Cache Go modules
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/go/pkg/mod
@ -50,7 +50,7 @@ jobs:
restore-keys: |
${{ runner.os }}-go-
- name: Setup Go
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
check-latest: true

View file

@ -24,14 +24,14 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
uses: github/codeql-action/init@v4.37.9
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v4
uses: github/codeql-action/autobuild@v4.37.9
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
uses: github/codeql-action/analyze@v4.37.9
with:
category: "/language:${{matrix.language}}"

View file

@ -5,11 +5,11 @@ jobs:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: "go.mod"
- uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0
- uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0
with:
version: v2.8.0
- name: Run go vet

View file

@ -9,13 +9,13 @@ jobs:
update-manpage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install pandoc
run: sudo apt-get update && sudo apt-get install -y pandoc
- name: Generate man page
run: bash contrib/man/generate.sh
- name: Create Pull Request
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
commit-message: "docs: regenerate man page from README"
title: "docs: update man page"

View file

@ -13,17 +13,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3
with:
version: "~> v2"
args: release --clean

1
AGENTS.md Symbolic link
View file

@ -0,0 +1 @@
CLAUDE.md

104
CLAUDE.md Normal file
View file

@ -0,0 +1,104 @@
<!-- Agent-consumed file. Keep terse, unambiguous, machine-parseable. -->
# CLAUDE.md
peco is an interactive filtering tool for the terminal, written in Go.
## Pre-Read Rules
Read the linked doc BEFORE working in that area. No exceptions.
| Trigger | Doc |
|---------|-----|
| Working with any package API or adding imports | `.claude/docs/packages.md` |
| Modifying cross-package dependencies | `.claude/docs/dependencies.md` |
| Writing or running tests | `.claude/docs/testing.md` |
| Modifying CLI flags, entry point, or output | `.claude/docs/cli.md` |
| Modifying concurrency, hub, buffers, filters, layout, screen, actions | `.claude/docs/internals.md` |
## Build & Test Commands
```bash
make # Build binary via goreleaser (default target)
make build # Build binary to dist/peco_<os>_<arch>/peco
make test # Run all tests: go test -v -race ./...
make deps # Download Go module dependencies
make clean # Remove build artifacts
```
Run a single test:
```bash
go test -v -run TestFunctionName ./...
go test -v -run TestFunctionName ./filter/ # for a specific package
```
The entry point is `cmd/peco/peco.go`.
## Architecture
### Concurrency Model
peco runs three main goroutines coordinated via context cancellation:
- **Input loop** (`input.go`) — reads tcell key events, resolves key sequences via Keymap, dispatches actions
- **View loop** (`view.go`) — renders screen in response to draw/paging/status messages
- **Filter loop** (`filter.go`) — executes queries against the line buffer when query text changes
These goroutines communicate through the **Hub** (`hub/`), a central message bus with typed channels: `QueryCh`, `DrawCh`, `PagingCh`, `StatusMsgCh`.
### Data Flow
1. **Source** (`source.go`) reads input lines (stdin or file), implements `pipeline.Source`
2. User keystrokes trigger actions that modify the query
3. Query changes are sent to the Filter loop via Hub
4. **Filter** applies the active filter algorithm to produce matched lines
5. Results flow through the **Pipeline** (`pipeline/`) as `Source → Acceptor → Destination`
6. **View** receives draw messages and delegates to **Layout** (`layout.go`) which composes `UserPrompt`, `ListArea`, and `StatusBar`
7. **Screen** (`screen.go`) wraps tcell/v2 for terminal cell rendering
### Key Interfaces
- **`Buffer`** — line storage (`LineAt`, `Size`); implemented by `MemoryBuffer`, `FilteredBuffer`, `Source`
- **`Filter`** (in `filter/`) — `Apply(ctx, []line.Line, ChanOutput)` for each filter algorithm (IgnoreCase, CaseSensitive, SmartCase, Regexp, IRegexp, Fuzzy, ExternalCmd)
- **`Line`** (`line/`) — represents a single line with `ID`, `Buffer`, `DisplayString`, `Output`
- **`Screen`** — terminal abstraction (`Init`, `SetCell`, `Flush`, `PollEvent`); `SimScreen` used in tests
- **`Layout`** — screen composition (`DrawScreen`, `DrawPrompt`, `MovePage`)
- **`Action`** — user actions bound to keys (`action.go`); ~40 built-in actions, supports combined action sequences
### Selection
Uses `google/btree` for ordered selection storage. Supports multi-select, range mode, and sticky selection (persists across query changes).
### Key Sequence Resolution
`internal/keyseq/` implements Trie, TernarySearch, and AhoCorasick for matching multi-key sequences to actions (longest-match-wins).
### Platform-Specific Code
Platform-specific behavior lives in `layout_any.go` / `layout_windows.go` (handling the `extraOffset` layout constant) and in files suffixed `_posix.go` / `_windows.go` under `internal/util/` (TTY detection, shell integration, home directory resolution).
### Code Generation
Uses `go:generate` with `stringer` for enum string representations.
## Testing Patterns
- `newPeco()` helper creates a test instance with `SimScreen` (mock terminal)
- `NewDummyScreen()` returns a `SimScreen` that supports event injection for simulating user input
- Table-driven tests with `t.Run()` subtests are the common pattern
- Regression tests for specific GitHub issues in `issues_test.go`
## Cache Maintenance
These docs cache repository state. Still read source before modifying code.
1. When your changes affect a doc below, update it in the same commit.
2. If you notice any doc is wrong or stale — even on an unrelated task — fix it immediately.
| Doc | Update trigger |
|-----|----------------|
| `packages.md` | Add/remove/rename exported functions, types, or packages |
| `dependencies.md` | Add/remove internal package imports |
| `testing.md` | Change test infrastructure, helpers, or test commands |
| `cli.md` | Add/remove CLI flags, change exit codes or output format |
| `internals.md` | Change concurrency model, hub channels, buffer types, layout system, action system |

View file

@ -437,6 +437,26 @@ Without `--height`, peco uses the full terminal screen (default behavior, unchan
**Note:** In inline mode, peco sets the environment variable `TCELL_ALTSCREEN=disable` to prevent tcell from using the alternate screen buffer, and restores the original value on exit. If peco is killed abnormally (e.g. `SIGKILL`), you may need to unset this variable manually: `unset TCELL_ALTSCREEN`.
### -f, --follow
When specified, peco follows streaming input, automatically scrolling to keep the newest lines visible at the bottom of the list, just like `tail -f`. This is useful for live log streams:
```
journalctl -f -n 1000 | peco --follow --layout top-down-query-bottom
```
Moving the cursor manually (e.g. with the arrow keys) turns follow mode off so you can scroll back through the history. The `ToggleFollow` action turns it back on; bind it to a key in your configuration file, for example:
```json
{
"Keymap": {
"C-f": "peco.ToggleFollow"
}
}
```
Follow mode can also be enabled by default with the `Follow` configuration variable. The `--follow` command line option takes precedence over the configuration file.
# Configuration File
peco by default consults a few locations for the config files.
@ -737,6 +757,7 @@ Some keys just... don't map correctly / too easily for various reasons. Here, we
| peco.SelectVisible | Selects the all visible line, and save it |
| peco.ToggleQuery | Toggle list between filtered by query and not filtered |
| peco.ToggleRangeMode | Start selecting by range, or append selecting range to selections |
| peco.ToggleFollow | Toggle follow mode (auto-scroll to the newest lines, like tail -f) |
| peco.ToggleSelectMode | (DEPRECATED) Alias to ToggleRangeMode |
| peco.ToggleSelection | Selects the current line, and saves it |
| peco.ToggleSelectionAndSelectNext | Selects the current line, saves it, and proceeds to the next line |

View file

@ -172,6 +172,8 @@ func init() {
ActionFunc(doFreezeResults).Register("FreezeResults")
ActionFunc(doUnfreezeResults).Register("UnfreezeResults")
ActionFunc(doToggleFollow).Register("ToggleFollow")
ActionFunc(doZoomIn).Register("ZoomIn")
ActionFunc(doZoomOut).Register("ZoomOut")
@ -966,6 +968,27 @@ func doUnfreezeResults(ctx context.Context, state *Peco, _ Event) {
state.Hub().SendDrawPrompt(ctx)
}
// doToggleFollow turns follow mode on or off. In follow mode peco auto-scrolls
// to keep the newest input lines visible, like "tail -f". Enabling it redraws
// so the viewport jumps to the tail immediately.
func doToggleFollow(ctx context.Context, state *Peco, _ Event) {
if pdebug.Enabled {
g := pdebug.Marker("doToggleFollow")
defer g.End()
}
follow := state.Follow()
enabled := !follow.Enabled()
follow.Set(enabled)
if enabled {
state.Hub().SendStatusMsg(ctx, "Follow mode on", 0)
state.Hub().SendDraw(ctx, &hub.DrawOptions{DisableCache: true})
return
}
state.Hub().SendStatusMsg(ctx, "Follow mode off", 0)
}
// doZoomIn expands the view to show context lines around matched lines by building
// a ContextBuffer from the current filter results and the original source.
func doZoomIn(ctx context.Context, state *Peco, _ Event) {

View file

@ -52,13 +52,23 @@ type ContextLine struct {
// NewFilteredBuffer creates a FilteredBuffer containing one page of lines from
// the source buffer, computing the maximum column width for horizontal scrolling.
func NewFilteredBuffer(src Buffer, page, perPage int) *FilteredBuffer {
return newFilteredBufferRange(src, perPage*(page-1), perPage)
}
// newFilteredBufferRange creates a FilteredBuffer holding up to count lines
// from the source buffer starting at the given index. This is the shared core
// behind both page-aligned cropping (NewFilteredBuffer) and the sliding-window
// crop used by follow mode (WindowCrop), where start is not page-aligned.
func newFilteredBufferRange(src Buffer, start, count int) *FilteredBuffer {
fb := FilteredBuffer{
src: src,
}
start := perPage * (page - 1)
if start < 0 {
start = 0
}
// if for whatever reason we wanted a page that goes over the
// if for whatever reason we wanted a range that goes over the
// capacity of the original buffer, we don't need to do any more
// calculations. bail out
if start > src.Size() {
@ -66,7 +76,7 @@ func NewFilteredBuffer(src Buffer, page, perPage int) *FilteredBuffer {
}
// Copy over the selections that are applicable to this filtered buffer.
end := min(start+perPage, src.Size())
end := min(start+count, src.Size())
selection := make([]int, 0, end-start)
lines := src.linesInRange(start, end)

View file

@ -96,6 +96,11 @@ type Config struct {
// Height specifies the display height in lines or percentage (e.g. "10", "50%").
// When set, peco renders inline without using the alternate screen buffer.
Height string `json:"Height" yaml:"Height"`
// Follow enables follow mode at startup: peco auto-scrolls to keep the
// newest input lines visible, like "tail -f". The --follow CLI flag
// overrides this value.
Follow bool `json:"Follow" yaml:"Follow"`
}
// SingleKeyJumpConfig holds configuration for single key jump mode.

View file

@ -741,6 +741,38 @@ behavior, unchanged).
alternate screen buffer, and restores the original value on exit.
If peco is killed abnormally (e.g.\ \f[V]SIGKILL\f[R]), you may need to
unset this variable manually: \f[V]unset TCELL_ALTSCREEN\f[R].
.SS -f, \[en]follow
.PP
When specified, peco follows streaming input, automatically scrolling to
keep the newest lines visible at the bottom of the list, just like
\f[V]tail -f\f[R].
This is useful for live log streams:
.IP
.nf
\f[C]
journalctl -f -n 1000 | peco --follow --layout top-down-query-bottom
\f[R]
.fi
.PP
Moving the cursor manually (e.g.\ with the arrow keys) turns follow mode
off so you can scroll back through the history.
The \f[V]ToggleFollow\f[R] action turns it back on; bind it to a key in
your configuration file, for example:
.IP
.nf
\f[C]
{
\[dq]Keymap\[dq]: {
\[dq]C-f\[dq]: \[dq]peco.ToggleFollow\[dq]
}
}
\f[R]
.fi
.PP
Follow mode can also be enabled by default with the \f[V]Follow\f[R]
configuration variable.
The \f[V]--follow\f[R] command line option takes precedence over the
configuration file.
.SH Configuration File
.PP
peco by default consults a few locations for the config files.
@ -1399,6 +1431,11 @@ T}@T{
Start selecting by range, or append selecting range to selections
T}
T{
peco.ToggleFollow
T}@T{
Toggle follow mode (auto-scroll to the newest lines, like tail -f)
T}
T{
peco.ToggleSelectMode
T}@T{
(DEPRECATED) Alias to ToggleRangeMode

194
follow_test.go Normal file
View file

@ -0,0 +1,194 @@
package peco
import (
"fmt"
"strings"
"testing"
"github.com/peco/peco/filter"
"github.com/peco/peco/hub"
"github.com/peco/peco/line"
"github.com/stretchr/testify/require"
)
// makeFollowBuffer builds a MemoryBuffer with n lines labelled "lineN".
func makeFollowBuffer(n int) *MemoryBuffer {
mb := NewMemoryBuffer(0)
for i := range n {
mb.lines = append(mb.lines, line.NewRaw(uint64(i), fmt.Sprintf("line%d", i), false, false))
}
return mb
}
// readScreenRow returns the text rendered on row y of the simulation screen,
// trailing blanks trimmed.
func readScreenRow(t *testing.T, s *SimScreen, y int) string {
t.Helper()
cells, w, _ := s.screen.GetContents()
var b strings.Builder
for x := range w {
c := cells[y*w+x]
if len(c.Runes) == 0 || c.Runes[0] == 0 {
b.WriteRune(' ')
continue
}
b.WriteRune(c.Runes[0])
}
return strings.TrimRight(b.String(), " ")
}
func newFollowState(t *testing.T, buf Buffer) (*Peco, *SimScreen) {
t.Helper()
screen := NewDummyScreen()
state := New()
state.screen = screen
state.Filters().Add(filter.NewIgnoreCase())
state.currentLineBuffer = buf
return state, screen
}
func TestWindowCrop(t *testing.T) {
buf := makeFollowBuffer(100)
t.Run("tail window", func(t *testing.T) {
fb := WindowCrop{offset: 90, perPage: 10}.Crop(buf)
require.Equal(t, 10, fb.Size())
first, err := fb.LineAt(0)
require.NoError(t, err)
require.Equal(t, "line90", first.DisplayString())
last, err := fb.LineAt(9)
require.NoError(t, err)
require.Equal(t, "line99", last.DisplayString())
})
t.Run("partial window smaller than perPage", func(t *testing.T) {
small := makeFollowBuffer(5)
fb := WindowCrop{offset: 0, perPage: 10}.Crop(small)
require.Equal(t, 5, fb.Size())
})
t.Run("negative offset clamps to zero", func(t *testing.T) {
fb := WindowCrop{offset: -5, perPage: 3}.Crop(buf)
require.Equal(t, 3, fb.Size())
first, err := fb.LineAt(0)
require.NoError(t, err)
require.Equal(t, "line0", first.DisplayString())
})
t.Run("offset past end yields empty", func(t *testing.T) {
fb := WindowCrop{offset: 200, perPage: 10}.Crop(buf)
require.Equal(t, 0, fb.Size())
})
}
// TestGHIssue820_FollowKeepsNewestVisible verifies that with follow mode on,
// DrawScreen pins the viewport to the tail of the buffer so the newest line
// is rendered on the last visible row (like tail -f), regardless of page
// alignment.
func TestGHIssue820_FollowKeepsNewestVisible(t *testing.T) {
buf := makeFollowBuffer(50)
state, screen := newFollowState(t, buf)
state.Follow().Set(true)
// top-down-query-bottom is the layout from the issue: list anchored to
// the top (row 0), query at the bottom.
layout, err := TopDownQueryBottomLayout(state)
require.NoError(t, err)
layout.DrawScreen(state, nil)
loc := state.Location()
perPage := layout.linesPerPage()
require.Equal(t, 49, loc.LineNumber(), "cursor should be pinned to the newest line")
require.Equal(t, 50, loc.Total())
require.Equal(t, max(50-perPage, 0), loc.Offset(),
"offset should be a sliding window over the tail, not page-aligned")
// The list area is anchored at row 0; the newest line sits on the last
// visible row.
lastRow := perPage - 1
require.Equal(t, "line49", readScreenRow(t, screen, lastRow),
"newest line should be on the bottom row of the list area")
}
// TestFollowDefaultShowsHead verifies that without follow mode the viewport
// stays at the head of the buffer (unchanged behavior).
func TestFollowDefaultShowsHead(t *testing.T) {
buf := makeFollowBuffer(50)
state, screen := newFollowState(t, buf)
require.False(t, state.IsFollowing())
layout, err := TopDownQueryBottomLayout(state)
require.NoError(t, err)
layout.DrawScreen(state, nil)
loc := state.Location()
require.Equal(t, 0, loc.LineNumber())
require.Equal(t, 0, loc.Offset())
require.Equal(t, "line0", readScreenRow(t, screen, 0),
"oldest line should be on the top row when not following")
}
// TestFollowDisabledByManualScroll verifies that any manual vertical
// navigation turns follow mode off.
func TestFollowDisabledByManualScroll(t *testing.T) {
buf := makeFollowBuffer(50)
state, _ := newFollowState(t, buf)
state.Follow().Set(true)
layout, err := TopDownQueryBottomLayout(state)
require.NoError(t, err)
// Pin to the tail first.
layout.DrawScreen(state, nil)
require.True(t, state.IsFollowing())
moved := layout.MovePage(state, hub.ToLineAbove)
require.True(t, moved)
require.False(t, state.IsFollowing(),
"manual vertical scroll should disable follow mode")
}
// TestToggleFollowAction verifies the ToggleFollow action flips follow state.
func TestToggleFollowAction(t *testing.T) {
state := New()
state.hub = nullHub{}
require.False(t, state.IsFollowing())
doToggleFollow(t.Context(), state, Event{})
require.True(t, state.IsFollowing(), "follow should be on after first toggle")
doToggleFollow(t.Context(), state, Event{})
require.False(t, state.IsFollowing(), "follow should be off after second toggle")
}
// TestFollowConfigAndFlag verifies that --follow and the Follow config field
// both enable follow mode, with the CLI flag taking precedence.
func TestFollowConfigAndFlag(t *testing.T) {
t.Run("CLI flag", func(t *testing.T) {
p := newPeco()
require.NoError(t, p.ApplyConfig(CLIOptions{OptFollow: true}))
require.True(t, p.IsFollowing())
})
t.Run("config field", func(t *testing.T) {
p := newPeco()
p.config.Follow = true
require.NoError(t, p.ApplyConfig(CLIOptions{}))
require.True(t, p.IsFollowing())
})
t.Run("default off", func(t *testing.T) {
p := newPeco()
require.NoError(t, p.ApplyConfig(CLIOptions{}))
require.False(t, p.IsFollowing())
})
}
// TestToggleFollowRegistered verifies the action is wired into the registry so
// it can be bound from a keymap.
func TestToggleFollowRegistered(t *testing.T) {
_, ok := nameToActions["peco.ToggleFollow"]
require.True(t, ok, "peco.ToggleFollow should be registered")
}

9
go.mod
View file

@ -3,13 +3,13 @@ module github.com/peco/peco
go 1.25.0
require (
github.com/gdamore/tcell/v2 v2.13.8
github.com/gdamore/tcell/v2 v2.13.10
github.com/goccy/go-yaml v1.19.2
github.com/google/btree v1.1.3
github.com/jessevdk/go-flags v1.6.1
github.com/lestrrat-go/pdebug v0.0.0-20180220043849-39f9a71bcabe
github.com/mattn/go-runewidth v0.0.20
github.com/stretchr/testify v1.11.1
github.com/mattn/go-runewidth v0.0.28
github.com/stretchr/testify v1.12.1
)
require (
@ -17,10 +17,9 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gdamore/encoding v1.0.1 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/term v0.37.0 // indirect
golang.org/x/text v0.31.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

20
go.sum
View file

@ -4,8 +4,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
github.com/gdamore/tcell/v2 v2.13.8 h1:Mys/Kl5wfC/GcC5Cx4C2BIQH9dbnhnkPgS9/wF3RlfU=
github.com/gdamore/tcell/v2 v2.13.8/go.mod h1:+Wfe208WDdB7INEtCsNrAN6O2m+wsTPk1RAovjaILlo=
github.com/gdamore/tcell/v2 v2.13.10 h1:Afs3JKt83HnhuUKdZ3MnxUgOqQRWftj5JyDqv1LLynA=
github.com/gdamore/tcell/v2 v2.13.10/go.mod h1:+Wfe208WDdB7INEtCsNrAN6O2m+wsTPk1RAovjaILlo=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
@ -16,15 +16,15 @@ github.com/lestrrat-go/pdebug v0.0.0-20180220043849-39f9a71bcabe h1:S7XSBlgc/eI2
github.com/lestrrat-go/pdebug v0.0.0-20180220043849-39f9a71bcabe/go.mod h1:zvUY6gZZVL2nu7NM+/3b51Z/hxyFZCZxV0hvfZ3NJlg=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/mattn/go-runewidth v0.0.28 h1:rPyg2ybwEKPebvpzVWe1gKBkH8EQFkxO4Y0hjBeLaBU=
github.com/mattn/go-runewidth v0.0.28/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@ -61,7 +61,3 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -328,6 +328,9 @@ func (u UserPrompt) Draw(state *Peco) {
loc := state.Location()
pmsg := fmt.Sprintf("%s [%d (%d/%d)]", state.Filters().Current().String(), loc.Total(), loc.Page(), loc.MaxPage())
if state.IsFollowing() {
pmsg = "FOLLOW " + pmsg
}
u.screen.Print(PrintArgs{
X: width - runewidth.StringWidth(pmsg),
Y: location,
@ -581,16 +584,26 @@ func (l *ListArea) Draw(state *Peco, parent Layout, perPage int, options *hub.Dr
loc := state.Location()
linebuf := state.CurrentLineBuffer()
following := state.IsFollowing()
if options != nil && options.RunningQuery {
// While following, the viewport is pinned to the tail of the buffer
// (see calculateFollowPage), so the running-query page adjustment must
// not move it.
if !following && options != nil && options.RunningQuery {
adjustPageForRunningQuery(loc, linebuf, parent, state)
}
pf := loc.PageCrop()
if pdebug.Enabled {
pdebug.Printf("Cropping linebuf which contains %d lines at page %d (%d entries per page)", linebuf.Size(), pf.currentPage, pf.perPage)
var buf *FilteredBuffer
if following {
// Sliding window over the tail so the newest line is on the last row.
buf = WindowCrop{offset: loc.Offset(), perPage: loc.PerPage()}.Crop(linebuf)
} else {
pf := loc.PageCrop()
if pdebug.Enabled {
pdebug.Printf("Cropping linebuf which contains %d lines at page %d (%d entries per page)", linebuf.Size(), pf.currentPage, pf.perPage)
}
buf = pf.Crop(linebuf)
}
buf := pf.Crop(linebuf)
bufsiz := buf.Size()
// This protects us from losing the selected line in case our selected
@ -910,6 +923,39 @@ func (l *BasicLayout) CalculatePage(state *Peco, perPage int) error {
return nil
}
// calculateFollowPage pins the viewport to the tail of the buffer for follow
// mode. Unlike CalculatePage, the offset is a sliding window (Size-perPage)
// rather than a page boundary, so the newest line always lands on the last
// visible row. The cursor is pinned to the newest line.
func (l *BasicLayout) calculateFollowPage(state *Peco, perPage int) error {
if pdebug.Enabled {
g := pdebug.Marker("BasicLayout.calculateFollowPage %d", perPage)
defer g.End()
}
buf := state.CurrentLineBuffer()
loc := state.Location()
total := buf.Size()
loc.SetPerPage(perPage)
loc.SetTotal(total)
if total == 0 {
loc.SetOffset(0)
loc.SetLineNumber(0)
loc.SetPage(1)
loc.SetMaxPage(1)
// wait for targets
return errors.New("no targets or query. nothing to do")
}
loc.SetOffset(max(total-perPage, 0))
loc.SetLineNumber(total - 1)
loc.SetMaxPage((total + perPage - 1) / perPage)
loc.SetPage(loc.MaxPage())
return nil
}
// DrawPrompt draws the prompt to the terminal
func (l *BasicLayout) DrawPrompt(state *Peco) {
l.prompt.Draw(state)
@ -924,7 +970,11 @@ func (l *BasicLayout) DrawScreen(state *Peco, options *hub.DrawOptions) {
perPage := l.linesPerPage()
if err := l.CalculatePage(state, perPage); err != nil {
calculate := l.CalculatePage
if state.IsFollowing() {
calculate = l.calculateFollowPage
}
if err := calculate(state, perPage); err != nil {
return
}
@ -1097,6 +1147,10 @@ func updateRangeSelection(state *Peco, buf Buffer, loc *Location, lineBefore, lc
// verticalScroll moves the cursor position vertically
func verticalScroll(state *Peco, l *BasicLayout, p hub.PagingRequest) bool {
// Manual vertical navigation cancels follow mode so the user can scroll
// back through history. Re-enable it with the ToggleFollow action.
state.Follow().Set(false)
loc := state.Location()
lineBefore := loc.LineNumber()

View file

@ -33,6 +33,7 @@ type CLIOptions struct {
OptPrintQuery bool `long:"print-query" description:"print out the current query as first line of output"`
OptColor config.ColorMode `long:"color" description:"color mode: 'auto' (default, parse ANSI codes) or 'none' (disable)" default:"auto"`
OptHeight string `long:"height" description:"display height in lines or percentage (e.g. '10', '50%')"`
OptFollow bool `long:"follow" short:"f" description:"follow streaming input, auto-scrolling to keep the newest lines\nvisible (like 'tail -f'). Manual cursor movement turns this off"`
}
// parse parses command-line arguments and validates the resulting options.

14
page.go
View file

@ -120,3 +120,17 @@ func (l *Location) PageCrop() PageCrop {
func (pf PageCrop) Crop(in Buffer) *FilteredBuffer {
return NewFilteredBuffer(in, pf.currentPage, pf.perPage)
}
// WindowCrop crops a fixed-size window of lines starting at an explicit
// offset rather than a page boundary. Follow mode uses it to show the tail
// of the buffer (the newest lines) regardless of page alignment.
type WindowCrop struct {
offset int
perPage int
}
// Crop returns a new Buffer containing up to perPage lines starting at the
// window's offset.
func (wc WindowCrop) Crop(in Buffer) *FilteredBuffer {
return newFilteredBufferRange(in, wc.offset, wc.perPage)
}

14
peco.go
View file

@ -96,6 +96,8 @@ type Peco struct {
frozen FrozenState
follow FollowState
zoom ZoomState
// cancelFunc is called for Exit()
@ -291,6 +293,16 @@ func (p *Peco) Frozen() *FrozenState {
return &p.frozen
}
// Follow returns the follow-mode state.
func (p *Peco) Follow() *FollowState {
return &p.follow
}
// IsFollowing reports whether follow mode is currently active.
func (p *Peco) IsFollowing() bool {
return p.follow.Enabled()
}
func (p *Peco) Zoom() *ZoomState {
return &p.zoom
}
@ -794,6 +806,8 @@ func (p *Peco) ApplyConfig(opts CLIOptions) error {
p.selectOneAndExit = opts.OptSelect1
p.exitZeroAndExit = opts.OptExitZero
p.selectAllAndExit = opts.OptSelectAll
// Follow mode: CLI flag overrides config.
p.follow.Set(opts.OptFollow || p.config.Follow)
p.printQuery = opts.OptPrintQuery
p.initialQuery = opts.OptQuery
p.initialFilter = opts.OptInitialFilter

View file

@ -110,8 +110,20 @@ func tcellEventToEvent(tev tcell.Event) Event {
}
}
// Special case: space must be sent as KeySpace with Ch=0
// to match the convention expected by doAcceptChar
// Ctrl+Space via CSI u: tcell delivers KeyRune with
// rune=' ' and ModCtrl. Convert to KeyCtrlSpace (0x00).
if r == ' ' && mod&keyseq.ModCtrl != 0 {
mod &^= keyseq.ModCtrl
return Event{
Type: EventKey,
Key: keyseq.KeyCtrlSpace,
Ch: 0,
Mod: mod,
}
}
// Plain space must be sent as KeySpace with Ch=0
// to match the convention expected by doAcceptChar.
if r == ' ' {
return Event{
Type: EventKey,
@ -138,17 +150,17 @@ func tcellEventToEvent(tev tcell.Event) Event {
}
}
// Ctrl+letter keys: tcell.KeyCtrlA(65)..KeyCtrlZ(90).
// Ctrl keys: tcell.KeyCtrlSpace(64)..KeyCtrlZ(90).
// On terminals with enhanced keyboard protocols (CSI u /
// fixterms), tcell normalizes Ctrl+letter to these constants
// with ModCtrl set. Peco's keyseq system encodes the ctrl
// nature in the key value (0x01-0x1A), not in the modifier,
// nature in the key value (0x00-0x1A), not in the modifier,
// so strip the redundant ModCtrl. (issue #715)
if key >= tcell.KeyCtrlA && key <= tcell.KeyCtrlZ {
if key >= tcell.KeyCtrlSpace && key <= tcell.KeyCtrlZ {
mod &^= keyseq.ModCtrl
return Event{
Type: EventKey,
Key: keyseq.KeyType(key - tcell.KeyCtrlA + 1),
Key: keyseq.KeyType(key - tcell.KeyCtrlSpace),
Ch: 0,
Mod: mod,
}

View file

@ -538,6 +538,62 @@ func TestTcellEventToEventCtrlKeysStripModCtrl(t *testing.T) {
}
}
// TestTcellEventToEventCtrlSpace verifies that Ctrl+Space is correctly
// converted to KeyCtrlSpace (0x00) regardless of how the terminal reports it.
//
// Traditional terminals send NUL (0x00); tcell's input handler
// delivers this as KeyCtrlSpace(64) with ModCtrl.
// The KeyCtrlSpace..KeyCtrlZ path handles this.
//
// CSI u / enhanced terminals report Ctrl+Space as KeyRune with rune=' '
// and ModCtrl. This must also produce Key=KeyCtrlSpace, Mod=ModNone
// so that the ToggleSelectionAndSelectNext action fires correctly.
func TestTcellEventToEventCtrlSpace(t *testing.T) {
t.Parallel()
tests := []struct {
name string
tcellEv *tcell.EventKey
wantKey keyseq.KeyType
wantCh rune
wantMod keyseq.ModifierKey
}{
{
// Traditional terminal: NUL byte (0x00).
// tcell's input handler calls
// NewEventKey(KeyCtrlSpace+Key(r), 0, ModCtrl)
// which yields key=KeyCtrlSpace(64), mod=ModCtrl.
// The KeyCtrlSpace..KeyCtrlZ path strips ModCtrl and
// maps to keyseq.KeyCtrlSpace(0x00).
name: "traditional terminal: NUL byte via KeyCtrlSpace+ModCtrl",
tcellEv: tcell.NewEventKey(tcell.KeyCtrlSpace, 0, tcell.ModCtrl),
wantKey: keyseq.KeyCtrlSpace,
wantCh: 0,
wantMod: keyseq.ModNone,
},
{
// CSI u / enhanced terminal: Ctrl+Space reported as
// KeyRune with rune=' ' and ModCtrl.
name: "CSI u terminal: Ctrl+Space as rune with ModCtrl",
tcellEv: tcell.NewEventKey(tcell.KeyRune, ' ', tcell.ModCtrl),
wantKey: keyseq.KeyCtrlSpace,
wantCh: 0,
wantMod: keyseq.ModNone,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := tcellEventToEvent(tt.tcellEv)
require.Equal(t, EventKey, got.Type, "event type")
require.Equal(t, tt.wantKey, got.Key, "key")
require.Equal(t, tt.wantCh, got.Ch, "ch")
require.Equal(t, tt.wantMod, got.Mod, "modifier")
})
}
}
// TestTcellEventToEventCtrlWithAltPreservesAlt verifies that Ctrl+Alt
// combinations strip ModCtrl but preserve ModAlt.
func TestTcellEventToEventCtrlWithAltPreservesAlt(t *testing.T) {

View file

@ -26,11 +26,16 @@ type Source struct {
inClosed bool
isInfinite bool
lines []line.Line
name string
mutex sync.RWMutex
ready chan struct{}
setupDone chan struct{}
setupOnce sync.Once
// start is the index of the oldest live line within lines. When a
// capacity is set, Append advances start instead of reallocating on
// every line; the dead prefix lines[:start] is reclaimed in bulk by a
// periodic compaction. The live window is always lines[start:].
start int
name string
mutex sync.RWMutex
ready chan struct{}
setupDone chan struct{}
setupOnce sync.Once
}
// drawRefreshInterval is the interval at which the screen is redrawn while
@ -225,7 +230,12 @@ func (s *Source) Start(ctx context.Context, out pipeline.ChanOutput) {
if !resume {
// no fancy resume handling needed. Send individual lines.
for _, l := range s.lines {
// setupDone is closed, so the buffer is stable; snapshot the live
// window (lines[start:]) under the lock and iterate it.
s.mutex.RLock()
live := s.lines[s.start:]
s.mutex.RUnlock()
for _, l := range live {
select {
case <-ctx.Done():
if pdebug.Enabled {
@ -314,40 +324,65 @@ func (s *Source) SetupDone() <-chan struct{} {
return s.setupDone
}
// linesInRange returns a slice of lines between start and end indices from the buffer.
func (s *Source) linesInRange(start, end int) []line.Line {
// linesInRange returns the lines between from and to (indices into the live
// window) from the buffer. The returned slice is a copy: Append may compact
// the backing array in place after the lock is released, which would
// otherwise overwrite or clear the slice the caller is still iterating.
func (s *Source) linesInRange(from, to int) []line.Line {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.lines[start:end]
src := s.lines[s.start+from : s.start+to]
out := make([]line.Line, len(src))
copy(out, src)
return out
}
// LineAt returns the line at the given index from the buffer.
// LineAt returns the line at the given index (within the live window) from the buffer.
func (s *Source) LineAt(n int) (line.Line, error) {
s.mutex.RLock()
defer s.mutex.RUnlock()
return bufferLineAt(s.lines, n)
return bufferLineAt(s.lines[s.start:], n)
}
// Size returns the number of lines currently in the buffer.
func (s *Source) Size() int {
s.mutex.RLock()
defer s.mutex.RUnlock()
return len(s.lines)
return len(s.lines) - s.start
}
// 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.
//
// Discarding is amortized O(1): rather than reallocating and copying the
// whole window on every line once saturated (which is O(capacity) per
// Append), we advance the logical start index and only compact — copying
// the live window to the front and releasing the discarded lines — once the
// dead prefix has grown to a full window. Compaction therefore happens once
// every capacity appends, so the per-line cost is O(1) amortized while peak
// memory stays bounded at ~2*capacity lines.
func (s *Source) Append(l line.Line) {
s.mutex.Lock()
defer s.mutex.Unlock()
s.lines = append(s.lines, l)
if s.capacity > 0 && len(s.lines) > s.capacity {
diff := len(s.lines) - s.capacity
if s.capacity <= 0 {
return
}
// Copy to a new slice to allow GC of discarded lines
newLines := make([]line.Line, s.capacity)
copy(newLines, s.lines[diff:])
s.lines = newLines
// Drop the oldest line logically once we are over capacity. len-start
// is the live size, so this keeps Size() pinned at exactly capacity.
if len(s.lines)-s.start > s.capacity {
s.start++
}
// Once the dead prefix is as large as the live window (len == 2*capacity),
// compact: slide the live window to the front, clear the freed tail so the
// discarded lines can be GC'd, and reset start.
if s.start >= s.capacity {
n := copy(s.lines, s.lines[s.start:])
clear(s.lines[n:])
s.lines = s.lines[:n]
s.start = 0
}
}

View file

@ -0,0 +1,29 @@
package peco
import (
"strconv"
"strings"
"testing"
"github.com/peco/peco/line"
)
// BenchmarkSourceAppendSaturated measures Append cost on a capacity-bounded
// source that is already saturated, which is the steady state for a long-lived
// streaming input (e.g. `tail -f | peco -b N`).
func BenchmarkSourceAppendSaturated(b *testing.B) {
const capacity = 10000
ig := newIDGen()
go ig.Run(b.Context())
s := NewSource("-", strings.NewReader(""), false, ig, capacity, false, false)
// Pre-fill to capacity so every benchmarked Append discards an old line.
for i := range capacity {
s.Append(line.NewRaw(uint64(i), strconv.Itoa(i), false, false))
}
l := line.NewRaw(uint64(capacity), "payload", false, false)
for b.Loop() {
s.Append(l)
}
}

123
source_capacity_test.go Normal file
View file

@ -0,0 +1,123 @@
package peco
import (
"fmt"
"strings"
"sync"
"testing"
"github.com/peco/peco/line"
"github.com/stretchr/testify/require"
)
// TestSourceCapacity verifies that a capacity-bounded Source keeps exactly the
// most recent `capacity` lines as new lines stream in, across many compaction
// boundaries, and that the backing storage stays bounded (amortized O(1)
// trimming rather than a full reallocation per append).
func TestSourceCapacity(t *testing.T) {
const capacity = 4
ig := newIDGen()
go ig.Run(t.Context())
s := NewSource("-", strings.NewReader(""), false, ig, capacity, false, false)
const total = 25 // crosses several capacity-sized compaction windows
for i := range total {
s.Append(line.NewRaw(uint64(i), fmt.Sprintf("line%d", i), false, false))
want := min(i+1, capacity)
require.Equal(t, want, s.Size(), "Size must stay pinned at capacity once saturated")
// The live window must be exactly the most-recently-appended lines.
oldest := (i + 1) - s.Size()
for j := range s.Size() {
l, err := s.LineAt(j)
require.NoError(t, err, "LineAt(%d) at append %d", j, i)
require.Equal(t, fmt.Sprintf("line%d", oldest+j), l.DisplayString())
}
// Backing storage must not grow without bound.
require.LessOrEqual(t, len(s.lines), 2*capacity,
"backing window should stay bounded at ~2*capacity")
}
// linesInRange over the live window still returns a correct contiguous slice
// after many compactions.
rng := s.linesInRange(0, capacity)
require.Len(t, rng, capacity)
require.Equal(t, fmt.Sprintf("line%d", total-capacity), rng[0].DisplayString())
require.Equal(t, fmt.Sprintf("line%d", total-1), rng[capacity-1].DisplayString())
// Out-of-range access is rejected.
_, err := s.LineAt(capacity)
require.Error(t, err)
}
// TestSourceUnlimitedRetainsAll verifies that the default (capacity 0) keeps
// every appended line — the start-offset machinery must not kick in.
func TestSourceUnlimitedRetainsAll(t *testing.T) {
ig := newIDGen()
go ig.Run(t.Context())
s := NewSource("-", strings.NewReader(""), false, ig, 0, false, false)
const total = 50
for i := range total {
s.Append(line.NewRaw(uint64(i), fmt.Sprintf("line%d", i), false, false))
}
require.Equal(t, total, s.Size())
require.Equal(t, 0, s.start, "start must stay 0 when capacity is unlimited")
first, err := s.LineAt(0)
require.NoError(t, err)
require.Equal(t, "line0", first.DisplayString())
last, err := s.LineAt(total - 1)
require.NoError(t, err)
require.Equal(t, fmt.Sprintf("line%d", total-1), last.DisplayString())
}
// TestSourceLinesInRangeConcurrentAppend verifies that the slice returned by
// linesInRange remains stable for the caller even when Append concurrently
// compacts the backing array. Run under -race, this catches any future
// regression where linesInRange returns a slice aliased into the live storage.
func TestSourceLinesInRangeConcurrentAppend(t *testing.T) {
const capacity = 32
ig := newIDGen()
go ig.Run(t.Context())
s := NewSource("-", strings.NewReader(""), false, ig, capacity, false, false)
for i := range capacity {
s.Append(line.NewRaw(uint64(i), fmt.Sprintf("seed%d", i), false, false))
}
stop := make(chan struct{})
var wg sync.WaitGroup
wg.Go(func() {
i := uint64(capacity)
for {
select {
case <-stop:
return
default:
}
s.Append(line.NewRaw(i, fmt.Sprintf("hot%d", i), false, false))
i++
}
})
// Repeatedly grab a window and read every element. If linesInRange
// returned a slice aliased into s.lines, a concurrent compaction would
// either nil out entries (post-clear) or rewrite them, producing a race
// detector hit and/or a nil DisplayString() deref.
for range 2000 {
rng := s.linesInRange(0, capacity)
require.Len(t, rng, capacity)
for _, l := range rng {
require.NotNil(t, l)
_ = l.DisplayString()
}
}
close(stop)
wg.Wait()
}

View file

@ -2,6 +2,7 @@ package peco
import (
"sync"
"sync/atomic"
"time"
)
@ -75,6 +76,24 @@ func (z *ZoomState) Clear() {
z.lineNo = 0
}
// FollowState tracks whether follow mode is active. In follow mode peco
// auto-scrolls to keep the newest input lines visible, like "tail -f".
// It is toggled at runtime: manual cursor movement turns it off, and the
// ToggleFollow action turns it back on.
type FollowState struct {
enabled atomic.Bool
}
// Enabled reports whether follow mode is currently active.
func (f *FollowState) Enabled() bool {
return f.enabled.Load()
}
// Set turns follow mode on or off.
func (f *FollowState) Set(enabled bool) {
f.enabled.Store(enabled)
}
// FrozenState holds a snapshot of filter results when the user
// "freezes" the current results to filter on top of them.
type FrozenState struct {