Compare commits

...

1560 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
Daisuke Maki 6ab3ff6d5d Actually update version number 2026-02-24 11:47:27 +09:00
lestrrat acdfd7ddba
Merge pull request #799 from peco/fix-release-workflow
ci: fix goreleaser dirty-state error by removing release-notes step
2026-02-24 10:54:02 +09:00
lestrrat a980ec02e4
Merge pull request #798 from peco/update-manpage
docs: update man page
2026-02-24 10:52:53 +09:00
Daisuke Maki efe1c5254f ci: fix goreleaser dirty-state error by removing release-notes step 2026-02-24 10:52:03 +09:00
Daisuke Maki e5d7c9ede6 Update Changes 2026-02-24 10:45:16 +09:00
lestrrat ce63a3fc81 docs: regenerate man page from README 2026-02-24 01:42:35 +00:00
lestrrat 70845827cd
Merge pull request #797 from peco/update-changes-v060
docs: fix v0.6.0 Changes and README gaps
2026-02-24 10:42:07 +09:00
Daisuke Maki dcad6fbf1b docs: fix v0.6.0 Changes and README gaps 2026-02-24 10:39:48 +09:00
lestrrat cf2ea47a52
Merge pull request #795 from peco/update-manpage
docs: update man page
2026-02-24 06:36:59 +09:00
lestrrat 600e20302d
Merge pull request #796 from peco/chore-default-build-target
Make build the default Makefile target
2026-02-24 06:36:05 +09:00
Daisuke Maki 2a39bf9315 Make build the default Makefile target 2026-02-23 22:26:18 +09:00
lestrrat 8f128cce45 docs: regenerate man page from README 2026-02-23 13:16:39 +00:00
lestrrat 1808f304ca
Merge pull request #794 from nekketsuuu/nekketsuuu-docs-make-build
docs fix: Build results will be at ./dist
2026-02-23 22:16:12 +09:00
nekketsuuu 8cd3fdfc62
docs fix: Build results will be at ./dist
The default output directory of goreleaser is `./dist`: https://goreleaser.com/customization/dist/

Also, the current default rule of Makefile is `check-goreleaser`, not `build`.
2026-02-22 23:30:07 +09:00
lestrrat e1453e217c
Merge pull request #793 from peco/update-manpage
docs: update man page
2026-02-22 11:45:43 +09:00
lestrrat 05d06f3a48
Merge pull request #791 from peco/docs-changelog
Tweak
2026-02-22 11:45:11 +09:00
lestrrat fdf0c9040a docs: regenerate man page from README 2026-02-22 00:07:02 +00:00
lestrrat 9a70e05aa9
Merge pull request #792 from peco/docs-readme-review-fixes
tweaks to README
2026-02-22 09:06:38 +09:00
Daisuke Maki 28039b1bd9 tweaks to README 2026-02-22 09:01:04 +09:00
Daisuke Maki f0ea966553 Tweak 2026-02-22 08:14:10 +09:00
lestrrat ba5c351e63
Merge pull request #790 from peco/docs-changelog
Update Changes
2026-02-22 06:44:39 +09:00
Daisuke Maki e37246c18e Update Changes 2026-02-22 06:42:32 +09:00
lestrrat b65eb6e7b1
Merge pull request #787 from peco/fix-matched-pool
fix: pool Matched structs with sync.Pool to reduce GC pressure
2026-02-22 00:33:31 +09:00
Daisuke Maki bf8595be01 merge master, resolve bench_test conflicts 2026-02-22 00:31:46 +09:00
Daisuke Maki 6be528ba24 fix: lint issues in matched pool 2026-02-22 00:29:33 +09:00
lestrrat 7a63934041
Merge pull request #789 from peco/fix-merge-matches-alloc
fix: in-place mergeMatches to avoid per-merge heap allocation
2026-02-22 00:29:32 +09:00
lestrrat bf02727ebd
Merge pull request #788 from peco/fix-displaystring-alloc
fix: avoid redundant displayString storage and alloc in DisplayString
2026-02-22 00:29:04 +09:00
lestrrat 43becc50fa
Merge pull request #786 from peco/fix-strip-ansi-alloc
fix: fast-path StripANSISequence to skip regexp when no ESC byte
2026-02-22 00:28:28 +09:00
Daisuke Maki a5c51bcc4b fix: pool Matched structs with sync.Pool to reduce GC pressure
Benchmark (10k lines x 3 cycles):
  pooled vs unpooled: 107KiB vs 562KiB (-81%), 2 vs 10k allocs
  end-to-end filter: 10.71MiB -> 10.14MiB (-5%), 150.6k -> 121.5k allocs (-19%)
2026-02-22 00:26:22 +09:00
Daisuke Maki b4c5d2e928 fix: avoid redundant displayString storage and alloc in DisplayString
Benchmark (10k lines, enableANSI=false):
  1244us -> 331us (-73%), 2541KiB -> 781KiB (-69%), 40k -> 10k allocs (-75%)
2026-02-22 00:26:11 +09:00
Daisuke Maki 4b6baa3751 fix: in-place mergeMatches to avoid per-merge heap allocation
Benchmark (10k lines, overlapping matches):
  47.4ms -> 42.8ms (-10%), 27.3MiB -> 21.3MiB (-22%), 861k -> 471k allocs (-45%)
2026-02-22 00:26:06 +09:00
Daisuke Maki 4b1dfa1c4d fix: fast-path StripANSISequence to skip regexp when no ESC byte
Benchmark (10k lines):
  plain text: 1023us -> 37us (-96%), 1.7MiB -> 0B (-100%), 30k -> 0 allocs
  95% plain:  1064us -> 162us (-85%), 1720KiB -> 63KiB (-96%), 30.5k -> 2k allocs
2026-02-22 00:26:02 +09:00
lestrrat a79df901f7
Merge pull request #784 from peco/fix-regexp-match-alloc
fix: reuse match/dedup slices across regexp filter loop iterations
2026-02-21 22:29:10 +09:00
Daisuke Maki 6ea14d47b9 fix: reuse match/dedup slices across regexp filter loop iterations
Move slice declarations outside loop and reset with [:0] each iteration.

| Metric     | Before  | After   | Delta  |
|------------|---------|---------|--------|
| allocs/op  | 2209    | 1813    | -396   |
| B/op       | 176836  | 147849  | -28987 |
| ns/op      | ~352k   | ~317k   | -10%   |
2026-02-21 22:28:36 +09:00
lestrrat a91172b458
Merge pull request #783 from peco/fix-closure-alloc
fix: eliminate closure allocation in fuzzy filter
2026-02-21 22:26:45 +09:00
Daisuke Maki 7588d33015 fix: eliminate closure allocation in fuzzy filter
Replace strings.IndexFunc with closure with direct CaseInsensitiveIndex.

| Metric     | Before  | After   | Delta  |
|------------|---------|---------|--------|
| allocs/op  | 3018    | 2018    | -1000  |
| B/op       | 123016  | 107014  | -16002 |
| ns/op      | ~113k   | ~88k    | -22%   |
2026-02-21 22:25:06 +09:00
lestrrat a19fa681e2
Merge pull request #785 from peco/fix-hub-batch-ctx
fix: merge two context keys into one in Hub.Batch
2026-02-21 22:24:19 +09:00
lestrrat 35e1681805
Merge pull request #782 from peco/fix-fuzzy-match-alloc
fix: pre-allocate flat backing array for fuzzy match pairs
2026-02-21 22:23:14 +09:00
lestrrat c0d97e2a49
Merge pull request #781 from peco/fix-ansi-split-alloc
fix: use stack-allocated array for SGR parameter parsing
2026-02-21 22:22:40 +09:00
lestrrat 06c4370c85
Merge pull request #780 from peco/test-ansi-color-boundary
Add tests for matching over color boundaries (me being paranoid)
2026-02-21 22:21:41 +09:00
Daisuke Maki e2bc390c68 fix: merge two context keys into one in Hub.Batch
Reduce context.WithValue calls from 2 to 1 per Batch invocation.

| Metric     | Before | After | Delta |
|------------|--------|-------|-------|
| allocs/op  | 3      | 2     | -1    |
| B/op       | 128    | 80    | -48   |
| ns/op      | ~372   | ~352  | -5%   |
2026-02-21 22:20:25 +09:00
Daisuke Maki 3c27da49ac fix: use stack-allocated array for SGR parameter parsing
Replace strings.Split with fixed [8]string array to avoid heap allocation.

| Metric     | Before | After | Delta |
|------------|--------|-------|-------|
| allocs/op  | 1      | 0     | -1    |
| B/op       | 48-80  | 0     | -100% |
| ns/op      | ~45    | ~20   | -55%  |
2026-02-21 22:20:21 +09:00
Daisuke Maki 580e062548 fix: pre-allocate flat backing array for fuzzy match pairs
Use a single backing []int with subslicing instead of per-match allocations.

| Metric     | Before  | After   | Delta  |
|------------|---------|---------|--------|
| allocs/op  | 3018    | 2018    | -1000  |
| B/op       | 123016  | 108616  | -14400 |
2026-02-21 22:20:19 +09:00
Daisuke Maki 8569cbd5bf appease linter 2026-02-21 22:18:04 +09:00
Daisuke Maki 40ac45f94d Add tests for matching over color boundaries (me being paranoid) 2026-02-21 21:30:18 +09:00
lestrrat ec3f2cb98b
Merge pull request #779 from peco/update-manpage
docs: update man page
2026-02-21 21:23:27 +09:00
lestrrat 5c39fff7ad docs: regenerate man page from README 2026-02-21 11:35:56 +00:00
lestrrat 113c0aee16
Merge pull request #778 from peco/docs-changelog-readme
Update docs
2026-02-21 20:35:33 +09:00
lestrrat 1a8dda7f4f
Merge pull request #777 from peco/dependabot/go_modules/github.com/mattn/go-runewidth-0.0.20
Bump github.com/mattn/go-runewidth from 0.0.19 to 0.0.20
2026-02-21 20:33:31 +09:00
Daisuke Maki 17536e1410 Update docs 2026-02-21 20:33:02 +09:00
dependabot[bot] e01669b48d
Bump github.com/mattn/go-runewidth from 0.0.19 to 0.0.20
Bumps [github.com/mattn/go-runewidth](https://github.com/mattn/go-runewidth) from 0.0.19 to 0.0.20.
- [Commits](https://github.com/mattn/go-runewidth/compare/v0.0.19...v0.0.20)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-21 10:02:26 +00:00
lestrrat 6869d4dcfa
Merge pull request #776 from peco/feat-ansi-default
make color support default, change option name to color
2026-02-21 18:40:43 +09:00
Daisuke Maki 14530444b1 appease linter 2026-02-21 16:04:30 +09:00
Daisuke Maki f689006279 change cli option / config name validate better 2026-02-21 15:59:42 +09:00
Daisuke Maki 9ab2b5af84 make color support default, change option name to color 2026-02-21 15:43:34 +09:00
lestrrat f05b49ceef
Merge pull request #775 from peco/dependabot/github_actions/peter-evans/create-pull-request-8.1.0
Bump peter-evans/create-pull-request from 7.0.11 to 8.1.0
2026-02-21 15:24:05 +09:00
dependabot[bot] 1bfa7d2467
Bump peter-evans/create-pull-request from 7.0.11 to 8.1.0
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 7.0.11 to 8.1.0.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](22a9089034...c0f553fe54)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-21 00:52:16 +00:00
lestrrat 76a48722da
Merge pull request #774 from peco/fix-filteredbuffer-errmsg
fix FilteredBuffer.LineAt error message
2026-02-21 08:04:25 +09:00
lestrrat 82a4d605b7
Merge pull request #773 from peco/fix-hub-pool-comment
add comment explaining pool type assertion safety
2026-02-21 08:04:06 +09:00
lestrrat 673f8b0688
Merge pull request #772 from peco/fix-source-race
fix data race on Source.inClosed
2026-02-21 08:03:54 +09:00
lestrrat cc2d63bb15
Merge pull request #771 from peco/fix-test-require
convert t.Errorf/t.Fatal to testify/require
2026-02-21 08:03:03 +09:00
Daisuke Maki 8c4db9e026 convert t.Errorf/t.Fatal to testify/require 2026-02-21 07:58:26 +09:00
Daisuke Maki 44d5885a2f fix data race on Source.inClosed 2026-02-21 07:58:24 +09:00
Daisuke Maki e024c7a01e add comment explaining pool type assertion safety 2026-02-21 07:58:14 +09:00
Daisuke Maki 38ce584c21 fix FilteredBuffer.LineAt error message 2026-02-21 07:58:11 +09:00
lestrrat 4e58dad08a
Merge pull request #770 from peco/fix-filtered-buffer-bounds
add negative index check to FilteredBuffer.LineAt
2026-02-20 22:52:56 +09:00
lestrrat f90f5cd0c2
Merge pull request #769 from peco/fix-hub-test-require
use require instead of t.Errorf in hub tests
2026-02-20 22:52:41 +09:00
lestrrat 982d0d30cd
Merge pull request #768 from peco/fix-peco-test-require
use t.Fatal instead of t.Errorf in peco tests
2026-02-20 22:52:26 +09:00
lestrrat ba6d3a7b31
Merge pull request #767 from peco/fix-keyseq-named-returns
remove named return values from Dig and ToKey
2026-02-20 22:51:46 +09:00
Daisuke Maki 6d97fcaa0f remove named return values from Dig and ToKey 2026-02-20 22:48:08 +09:00
Daisuke Maki cef9453b46 use t.Fatal instead of t.Errorf in peco tests 2026-02-20 22:47:54 +09:00
Daisuke Maki c404f05d43 use require instead of t.Errorf in hub tests 2026-02-20 22:47:52 +09:00
Daisuke Maki 3bd446705d add negative index check to FilteredBuffer.LineAt 2026-02-20 22:43:56 +09:00
lestrrat 76f0724099
Merge pull request #764 from peco/fix-named-returns
remove named return values from Compile and MovePage
2026-02-20 22:31:52 +09:00
lestrrat 464fc30b3d
Merge pull request #766 from peco/fix-external-send-error
propagate out.Send error in external filter
2026-02-20 22:31:31 +09:00
lestrrat b5b1f523be
Merge pull request #765 from peco/fix-regexp-submatch
use FindAllStringIndex instead of FindAllStringSubmatchIndex
2026-02-20 22:31:17 +09:00
lestrrat 3c54a192a7
Merge pull request #763 from peco/fix-getlist-panic
guard GetList against empty KeyList
2026-02-20 22:31:04 +09:00
lestrrat 2914b94aaa
Merge pull request #762 from peco/fix-regexp-shadow
rename shadowed loop variable in regexp filter
2026-02-20 22:30:44 +09:00
lestrrat fbb737c72d
Merge pull request #761 from peco/fix-regexp-time-style
standardize on time.Since in regexp cache eviction
2026-02-20 22:30:30 +09:00
lestrrat 727e85afed
Merge pull request #760 from peco/fix-layout-test-require
use require instead of t.Errorf in layout tests
2026-02-20 22:30:15 +09:00
lestrrat 98f51cbbf5
Merge pull request #759 from peco/fix-resultch-race
fix data race on resultCh in PrintResults
2026-02-20 22:29:51 +09:00
Daisuke Maki 5fa4303b52 standardize on time.Since in regexp cache eviction 2026-02-20 22:27:23 +09:00
Daisuke Maki 0354f3071a rename shadowed loop variable in regexp filter 2026-02-20 22:27:20 +09:00
Daisuke Maki 3dec918f69 use require instead of t.Errorf in layout tests 2026-02-20 22:27:06 +09:00
Daisuke Maki 4f7978201b guard GetList against empty KeyList 2026-02-20 22:27:04 +09:00
Daisuke Maki e32edbda88 remove named return values from Compile and MovePage 2026-02-20 22:27:01 +09:00
Daisuke Maki 2f20cddcf6 use FindAllStringIndex instead of FindAllStringSubmatchIndex 2026-02-20 22:26:47 +09:00
Daisuke Maki deb12acf59 propagate out.Send error in external filter 2026-02-20 22:26:44 +09:00
Daisuke Maki 720f1b0be8 fix data race on resultCh in PrintResults 2026-02-20 22:26:41 +09:00
lestrrat e4e92ad5ac
Merge pull request #754 from peco/fix-selection-copy
prevent deadlock in Selection.Copy
2026-02-20 21:09:56 +09:00
lestrrat acbeb2e59a
Merge pull request #758 from peco/fix-loop-error-handling
log errors
2026-02-20 21:09:43 +09:00
lestrrat e0fe6b7375
Merge pull request #757 from peco/fix-query-bounds
Add bounds validation
2026-02-20 21:09:30 +09:00
lestrrat b00e80a94e
Merge pull request #756 from peco/fix-regexp-cache
use LRU eviction
2026-02-20 21:09:17 +09:00
lestrrat 2b6d042f93
Merge pull request #755 from peco/fix-fuzzy-redundant
use strings.IndexRun/IndexFunc
2026-02-20 21:09:02 +09:00
Daisuke Maki 174b0f1b30 prevent deadlock in Selection.Copy 2026-02-20 21:06:46 +09:00
lestrrat 01956a4c51
Merge pull request #753 from peco/fix-ignored-errors
very minor tidying
2026-02-20 21:03:38 +09:00
lestrrat e01c17be99
Merge pull request #752 from peco/fix-external-filter-errors
propagate cancellation and distinguish EOF from real I/O errors
2026-02-20 21:03:01 +09:00
lestrrat 8a2b5334de
Merge pull request #751 from peco/test-missing-coverage
increase test coverage
2026-02-20 21:02:35 +09:00
lestrrat c55bb30529
Merge pull request #750 from peco/chore-ci-workflows
tweak ci
2026-02-20 21:01:44 +09:00
Daisuke Maki 6e169570fe log errors 2026-02-20 21:00:02 +09:00
Daisuke Maki 136b4a80e9 Add bounds validation 2026-02-20 20:58:00 +09:00
Daisuke Maki 70975b098e use LRU eviction 2026-02-20 20:57:21 +09:00
Daisuke Maki 5fc35a050e use strings.IndexRun/IndexFunc 2026-02-20 20:56:09 +09:00
Daisuke Maki bb8a6310ea very minor tidying 2026-02-20 20:54:18 +09:00
Daisuke Maki ed55e27368 propagate cancellation and distinguish EOF from real I/O errors 2026-02-20 20:51:22 +09:00
Daisuke Maki d10fe55d8e 1.25 is fine 2026-02-20 20:43:07 +09:00
Daisuke Maki 43e5cf1ddc tweak ci 2026-02-20 20:38:37 +09:00
Daisuke Maki 0ccff01ddd increase test coverage 2026-02-20 20:37:44 +09:00
lestrrat 717dfca53b
Merge pull request #749 from peco/refactor-extract-config
extract config package
2026-02-20 18:44:31 +09:00
Daisuke Maki 11754d722b extract config package 2026-02-20 18:42:24 +09:00
lestrrat 14748ed33b
Merge pull request #748 from peco/fix-selection-bindings
bind GoToNextSelection/GoToPreviousSelection to Alt+j/Alt+k
2026-02-20 17:34:16 +09:00
lestrrat 09aacb6f50
Merge pull request #746 from peco/docs-custommatcher-migration
document CustomMatcher to CustomFilter migration format
2026-02-20 17:34:03 +09:00
Daisuke Maki d8d8e43b97 bind GoToNextSelection/GoToPreviousSelection to Alt+j/Alt+k
Resolves the Ctrl+K conflict with KillEndOfLine (#575, #579) by
giving both actions their own Alt-key defaults. Documents the
GoToPreviousSelection malformed name fix from v0.5.x.
2026-02-20 17:06:22 +09:00
Daisuke Maki 1baeb547b4 document CustomMatcher to CustomFilter migration format 2026-02-20 17:01:11 +09:00
lestrrat 7b54585c95
Merge pull request #745 from peco/extract-query-package
Extract query package
2026-02-20 15:18:36 +09:00
Daisuke Maki ebaf55dafa appease linter 2026-02-20 15:15:53 +09:00
Daisuke Maki 2a8d21774a extract query package 2026-02-20 15:13:44 +09:00
lestrrat 01ede393bb
Merge pull request #744 from peco/extract-selection-package
extract Selection object to its own package
2026-02-20 15:07:32 +09:00
Daisuke Maki bdf9b33e6d extract Selection object to its own package 2026-02-20 14:56:29 +09:00
lestrrat 37a9b23c61
Merge pull request #743 from peco/replace-applyfn-with-interface
change applyFn to interface
2026-02-20 14:08:01 +09:00
Daisuke Maki 8a9dfefb3a change applyFn to interface 2026-02-20 14:05:22 +09:00
lestrrat 19327037c9
Merge pull request #742 from peco/replace-callbacks-with-interfaces
Replace raw closure callbacks with interfaces
2026-02-20 13:44:51 +09:00
Daisuke Maki 88b5b7a39c appease linter 2026-02-20 13:42:55 +09:00
Daisuke Maki 1fea5cd5a0 Replace raw closure callbacks with interfaces 2026-02-20 13:35:31 +09:00
lestrrat 4d178a4675
Merge pull request #738 from peco/update-manpage
docs: update man page
2026-02-20 11:01:29 +09:00
lestrrat 0fe5e98477
Merge pull request #741 from peco/add-function-comments
Methodically add function comments
2026-02-20 11:01:14 +09:00
Daisuke Maki d8d010eafc run gofmt -w 2026-02-20 10:58:04 +09:00
lestrrat e06fae240a
Merge pull request #740 from peco/remove-hub-sub-interfaces
simplify
2026-02-20 10:56:24 +09:00
Daisuke Maki fc18c0d3de Methodically add function comments 2026-02-20 10:55:49 +09:00
Daisuke Maki 892882f69c simplify 2026-02-20 10:35:40 +09:00
lestrrat 93fcda4a08
Merge pull request #739 from peco/standardize-tests-use-require
standardize on using testify/require
2026-02-20 10:25:22 +09:00
Daisuke Maki 8b74d9421a standardize on using testify/require 2026-02-20 10:22:58 +09:00
lestrrat f918f68134 docs: regenerate man page from README 2026-02-20 01:09:23 +00:00
lestrrat ac64686192
Merge pull request #737 from peco/docs-negative-terms-breaking
docs: document negative-term query breaking change for v0.6.0
2026-02-20 10:08:57 +09:00
Daisuke Maki 21ceb9908a docs: document negative-term query breaking change for v0.6.0 2026-02-20 08:30:02 +09:00
lestrrat 7199f09552
Merge pull request #735 from peco/fix-mouse-support
fix: restore mouse support lost in tcell migration
2026-02-19 21:57:53 +09:00
lestrrat a74bdfe4d0
Merge pull request #736 from peco/update-manpage
docs: update man page
2026-02-19 21:57:30 +09:00
lestrrat 63473702b4 docs: regenerate man page from README 2026-02-19 12:56:57 +00:00
lestrrat 6f5b949656
Merge pull request #734 from peco/docs-shift-tab
docs: document Shift+Tab to Tab mapping after tcell migration
2026-02-19 21:56:36 +09:00
lestrrat 796f6b2d2b
Merge pull request #733 from peco/docs-tcell-altscreen
docs: document TCELL_ALTSCREEN env var in --height section
2026-02-19 21:56:22 +09:00
lestrrat bf6e2a78f2
Merge pull request #732 from peco/docs-layout-option
docs: add top-down-query-bottom to --layout option docs
2026-02-19 21:56:07 +09:00
lestrrat 2c9c5c0a41
Merge pull request #731 from peco/docs-missing-actions
docs: add ViewAround, GoToNextSelection, GoToPreviousSelection to act…
2026-02-19 21:55:56 +09:00
lestrrat e809133abe
Merge pull request #730 from peco/docs-exit0-selectall
docs: document --exit-0 and --select-all CLI flags
2026-02-19 21:55:45 +09:00
lestrrat 275bfe2f80
Merge pull request #729 from peco/docs-hex-colors
docs: document default, hex (#RRGGBB) color formats
2026-02-19 21:55:36 +09:00
Daisuke Maki 80f7560eb2 chore: fix gofmt trailing newline in screen_test.go 2026-02-19 21:55:00 +09:00
Daisuke Maki a4e1fcb1dc fix: restore mouse support lost in tcell migration 2026-02-19 21:51:51 +09:00
Daisuke Maki 62842229bf docs: document TCELL_ALTSCREEN env var in --height section 2026-02-19 21:43:39 +09:00
Daisuke Maki e5f12cb7f3 docs: document Shift+Tab to Tab mapping after tcell migration 2026-02-19 21:43:33 +09:00
Daisuke Maki 661c4fb5f6 docs: document default, hex (#RRGGBB) color formats 2026-02-19 21:34:07 +09:00
Daisuke Maki 041557f913 docs: add ViewAround, GoToNextSelection, GoToPreviousSelection to action table 2026-02-19 21:33:57 +09:00
Daisuke Maki 2ab4ac0567 docs: document --exit-0 and --select-all CLI flags 2026-02-19 21:32:49 +09:00
Daisuke Maki d3d4b313c3 docs: add top-down-query-bottom to --layout option docs 2026-02-19 21:32:15 +09:00
lestrrat 7e319f0b6d
Merge pull request #728 from peco/fix-esc-timer-redundancy
fix: remove redundant Esc timer, trust tcell disambiguation
2026-02-19 20:04:21 +09:00
lestrrat 8a74cc4a70
Merge pull request #727 from peco/fix-frozen-filter-cache
fix: invalidate filter cache on freeze/unfreeze
2026-02-19 20:03:36 +09:00
lestrrat 64027d9a2b
Merge pull request #726 from peco/fix-select1-streaming
fix: --select-1 triggers on interactive queries and respects context for streaming sources
2026-02-19 20:02:42 +09:00
Daisuke Maki f8694463a2 fix: remove redundant Esc timer, trust tcell disambiguation 2026-02-19 20:01:58 +09:00
Daisuke Maki a24479624d fix: invalidate filter cache on freeze/unfreeze 2026-02-19 19:59:37 +09:00
Daisuke Maki ea2ce9e909 fix: --select-1 triggers on interactive queries and respects context for streaming sources 2026-02-19 19:51:57 +09:00
lestrrat d9e46440b4
Merge pull request #724 from peco/chore-remove-use256color
Remove dead Use256Color config field
2026-02-19 19:46:48 +09:00
lestrrat e207eaa72f
Merge pull request #725 from peco/update-manpage
docs: update man page
2026-02-19 19:46:31 +09:00
lestrrat 83f154459a docs: regenerate man page from README 2026-02-19 10:45:52 +00:00
lestrrat 67de06c1e1
Merge pull request #723 from peco/docs-smartcase-negative-limitation
docs: note SmartCase + negative terms limitation
2026-02-19 19:45:30 +09:00
Daisuke Maki aa8bec3184 Remove dead Use256Color config field
After the tcell migration, 256-color support is automatic via
terminfo detection. The field was stored but never read by any
rendering code.
2026-02-19 19:44:19 +09:00
Daisuke Maki a84879b78e docs: note SmartCase + negative terms limitation 2026-02-19 19:29:11 +09:00
lestrrat 2238188798
Merge pull request #721 from peco/update-manpage
docs: update man page
2026-02-19 19:16:04 +09:00
lestrrat 9b0b83a302 docs: regenerate man page from README 2026-02-19 09:41:42 +00:00
lestrrat 76f0525a1a
Merge pull request #722 from peco/fix-external-filter-parallel
Fix external filter parallel
2026-02-19 18:41:16 +09:00
Daisuke Maki ebe017f5c1 docs: clarify external filters run sequentially unlike built-in filters 2026-02-19 18:37:46 +09:00
Daisuke Maki 8212acecae fix: external filters should not run in parallel 2026-02-19 18:15:01 +09:00
lestrrat 4d61e9c718
Merge pull request #720 from peco/goreleaser
use goreleaser
2026-02-19 16:20:24 +09:00
Daisuke Maki a28679d2df use make build/goreleaser 2026-02-19 15:18:46 +09:00
Daisuke Maki bbfff0f835 use goreleaser 2026-02-19 13:24:24 +09:00
lestrrat 8fc5813265
Merge pull request #719 from peco/feat-query-cursor-style
Update Changes
2026-02-19 11:36:59 +09:00
Daisuke Maki 8e84cfc875 Update Changes 2026-02-19 11:35:01 +09:00
lestrrat 8b35079480
Merge pull request #718 from peco/update-manpage
docs: update man page
2026-02-19 11:32:11 +09:00
lestrrat 40b743b04f docs: regenerate man page from README 2026-02-19 00:59:13 +00:00
lestrrat 4a76ffdaa6
Merge pull request #717 from peco/feat-query-cursor-style
Feat query cursor style
2026-02-19 09:58:49 +09:00
Daisuke Maki 7e206f13a7 Merge master into feat-query-cursor-style 2026-02-19 09:55:31 +09:00
Daisuke Maki 7041a265db Add QueryCursor config option 2026-02-19 09:48:14 +09:00
lestrrat fef7922a36
Merge pull request #716 from peco/fix-ctrl-c-exit
Fix #715. meta char handling
2026-02-19 08:41:35 +09:00
Daisuke Maki bae7ac32f2 Fix #715. meta char handling
tcell handles ctrl meta characters slightly differently than we expected
2026-02-19 08:38:28 +09:00
lestrrat 74ffb86a87
Merge pull request #714 from peco/docs-update-changes
tweak
2026-02-18 23:04:20 +09:00
Daisuke Maki cfd508a785 tweak 2026-02-18 23:00:19 +09:00
lestrrat a29d50cd0b
Merge pull request #713 from peco/docs-update-changes
Update Changes
2026-02-18 22:21:43 +09:00
Daisuke Maki 740db75767 Update Changes 2026-02-18 22:16:52 +09:00
lestrrat a1a3596848
Merge pull request #712 from peco/refactor-remove-interface-files
Do away with interface.go files
2026-02-18 22:00:15 +09:00
Daisuke Maki 95e13933c2 Do away with interface.go files 2026-02-18 21:12:19 +09:00
lestrrat be9d23d9ac
Merge pull request #707 from peco/fix-fuzzy-slice-literals
Use var declarations for appended-to slices
2026-02-18 16:03:33 +09:00
lestrrat 3df337e1f4
Merge pull request #708 from peco/fix-draw-ticker-constant
Extract drawRefreshInterval constant
2026-02-18 16:03:19 +09:00
lestrrat c5bf382061
Merge pull request #709 from peco/fix-esc-timeout-constant
Extract escKeyTimeout constant
2026-02-18 16:03:07 +09:00
lestrrat a24814affe
Merge pull request #711 from peco/fix-typo-source-test
Fix "lien" typo in test assertion message
2026-02-18 16:02:55 +09:00
lestrrat a01c747f02
Merge pull request #710 from peco/fix-len-check-style
Use idiomatic empty string check
2026-02-18 16:02:41 +09:00
lestrrat ebd05de222
Merge pull request #706 from peco/fix-keyseq-cleanup
Remove dead prevInputTime field and fix "rest" typo
2026-02-18 16:02:25 +09:00
Daisuke Maki c60329eca9 Use var declarations for appended-to slices 2026-02-18 15:57:20 +09:00
Daisuke Maki c8748a736e Extract drawRefreshInterval constant 2026-02-18 15:57:20 +09:00
Daisuke Maki cf77e13382 Extract escKeyTimeout constant 2026-02-18 15:57:20 +09:00
Daisuke Maki 37be8451e6 Use idiomatic empty string check 2026-02-18 15:57:19 +09:00
Daisuke Maki 3a4730167a Fix "lien" typo in test assertion message 2026-02-18 15:56:31 +09:00
Daisuke Maki 27cda7fc36 Remove dead prevInputTime field and fix "rest" typo 2026-02-18 15:56:19 +09:00
lestrrat 8f7038b5c2
Merge pull request #705 from peco/fix-tty-windows-redundant-err
Remove redundant err != nil in Windows syscall wrappers
2026-02-18 15:31:49 +09:00
lestrrat 52cb231084
Merge pull request #704 from peco/fix-simscreen-bare-recover
Log panics in SimScreen PollEvent instead of swallowing
2026-02-18 15:31:32 +09:00
lestrrat e20b3f4f34
Merge pull request #703 from peco/fix-pipeline-test-assertions
Add assertion to TestPipeline
2026-02-18 15:31:07 +09:00
lestrrat 2f74f1c7f0
Merge pull request #702 from peco/fix-pin-stringer-ci
Pin stringer to v0.42.0 in CI
2026-02-18 15:30:43 +09:00
Daisuke Maki 381ce7ca5b Pin stringer to v0.42.0 in CI 2026-02-18 15:28:14 +09:00
Daisuke Maki 34e539b7da Add assertion to TestPipeline 2026-02-18 15:28:06 +09:00
Daisuke Maki 7bbd53e775 Log panics in SimScreen PollEvent instead of swallowing 2026-02-18 15:27:46 +09:00
Daisuke Maki 66bf406024 Remove redundant err != nil in Windows syscall wrappers 2026-02-18 15:27:42 +09:00
lestrrat 51e6af6d36
Merge pull request #695 from peco/fix-config-test-coverage
Expand TestApplyConfig to cover all config options
2026-02-18 14:58:18 +09:00
Daisuke Maki 09f5ed6972 Expand TestApplyConfig to cover all config options 2026-02-18 14:55:35 +09:00
lestrrat 92724b8dc5
Merge pull request #697 from peco/fix-magic-numbers-prompt
Extract queryStartX variable in UserPrompt.Draw
2026-02-18 14:51:51 +09:00
lestrrat e09776384f
Merge pull request #696 from peco/fix-filter-dedup-cancellation
Extract checkCancelled helper for filter loops
2026-02-18 14:51:40 +09:00
lestrrat 441feb59a9
Merge pull request #698 from peco/fix-assert-to-require
Replace testify/assert with testify/require
2026-02-18 14:49:14 +09:00
lestrrat 6fdcd40b78
Merge pull request #699 from peco/fix-filter-dedup-exclusion
Extract isExcluded helper for negative term check
2026-02-18 14:48:45 +09:00
lestrrat 554a196a8b
Merge pull request #700 from peco/fix-filter-constructors
Extract newRegexpFilter helper for filter constructors
2026-02-18 14:48:25 +09:00
lestrrat dfbae22734
Merge pull request #694 from peco/fix-setdirty-add-pattern
Extract selectLine helper for SetDirty+Add pattern
2026-02-18 14:48:09 +09:00
Daisuke Maki b4141eca2a Extract queryStartX variable in UserPrompt.Draw 2026-02-18 14:46:08 +09:00
Daisuke Maki d1e5b18232 Replace testify/assert with testify/require 2026-02-18 14:46:06 +09:00
Daisuke Maki 1e90260188 Extract checkCancelled helper for filter loops 2026-02-18 14:46:05 +09:00
Daisuke Maki 0d55ffa4ae Extract isExcluded helper for negative term check 2026-02-18 14:46:04 +09:00
Daisuke Maki 44b78c5dd8 Extract newRegexpFilter helper for filter constructors 2026-02-18 14:46:03 +09:00
Daisuke Maki a547170580 Extract selectLine helper for SetDirty+Add pattern 2026-02-18 14:46:02 +09:00
lestrrat 6fe3e4516d
Merge pull request #701 from peco/fix-staticcheck-sa4016
Remove no-op bitwise OR with zero in test
2026-02-18 14:42:21 +09:00
Daisuke Maki de2d3465cb Remove no-op bitwise OR with zero in test 2026-02-18 14:40:17 +09:00
lestrrat 90f9c5ceca
Merge pull request #692 from peco/fix-ansi-parser-errors
Handle strconv.Atoi errors in ANSI SGR parser
2026-02-18 11:31:56 +09:00
lestrrat 22fff45193
Merge pull request #691 from peco/fix-test-time-sleep
Replace time.Sleep with require.Never/Eventually
2026-02-18 11:31:45 +09:00
lestrrat fa5cc8aa9d
Merge pull request #690 from peco/fix-hub-batch-locking
Replace shouldLock with context-based re-entrancy
2026-02-18 11:31:30 +09:00
lestrrat ef6c4839e3
Merge pull request #689 from peco/refactor-list-area-draw
Extract helpers from ListArea.Draw
2026-02-18 11:31:18 +09:00
lestrrat b96fe3ae01
Merge pull request #688 from peco/refactor-vertical-scroll
Extract helpers from verticalScroll
2026-02-18 11:30:56 +09:00
lestrrat 1fd9332873
Merge pull request #693 from peco/fix-enable-staticcheck-govet
Re-enable staticcheck and govet linters
2026-02-18 11:29:34 +09:00
Daisuke Maki 965feb2b8c Extract helpers from verticalScroll 2026-02-18 11:25:50 +09:00
Daisuke Maki 9425a01e3e Extract helpers from ListArea.Draw 2026-02-18 11:25:47 +09:00
Daisuke Maki 57d6761a9f Replace shouldLock with context-based re-entrancy 2026-02-18 11:25:42 +09:00
Daisuke Maki 0e71932a1d Replace time.Sleep with require.Never/Eventually 2026-02-18 11:25:35 +09:00
Daisuke Maki 0b4f9d0adf Handle strconv.Atoi errors in ANSI SGR parser 2026-02-18 11:25:30 +09:00
Daisuke Maki 6dfecd61c8 Re-enable staticcheck and govet linters 2026-02-18 11:25:24 +09:00
lestrrat 6058cbee57
Merge pull request #687 from peco/fix-dead-code
Remove dead code
2026-02-18 09:53:08 +09:00
lestrrat 22439d1b69
Merge pull request #686 from peco/fix-error-handling-minor
tweak handling
2026-02-18 09:52:52 +09:00
lestrrat 0c49ba8856
Merge pull request #685 from peco/fix-api-cleanup
remove error, avoid magic string
2026-02-18 09:52:35 +09:00
lestrrat 2acd138f66
Merge pull request #684 from peco/fix-code-style
style, minor tweaks
2026-02-18 09:52:13 +09:00
lestrrat 022c7ba4f4
Merge pull request #683 from peco/fix-test-ci
some modernization, remove debug prints, pin go 1.25
2026-02-18 09:51:56 +09:00
Daisuke Maki 6a89f1e352 some modernization, remove debug prints, pin go 1.25 2026-02-18 09:47:25 +09:00
Daisuke Maki e1c62ab701 style, minor tweaks 2026-02-18 09:46:54 +09:00
Daisuke Maki 8910d5bf17 remove error, avoid magic string 2026-02-18 09:46:27 +09:00
Daisuke Maki dfe029958c tweak handling 2026-02-18 09:45:26 +09:00
Daisuke Maki 1f218ae97e Remove dead code 2026-02-18 09:44:47 +09:00
lestrrat ced740f32d
Merge pull request #682 from peco/fix-filter-apply-error-handling
Report filter Apply errors to status bar
2026-02-18 09:37:45 +09:00
Daisuke Maki 73d5d370a4 appease linter 2026-02-18 09:35:29 +09:00
lestrrat c4e02ac1ce
Merge pull request #681 from peco/fix-screen-init-resume-error
Propagate screen Init error on resume
2026-02-18 09:35:10 +09:00
Daisuke Maki fade21e143 appease linter 2026-02-18 09:33:35 +09:00
Daisuke Maki e7db38fa07 Report filter Apply errors to status bar 2026-02-18 09:30:06 +09:00
Daisuke Maki 515e13fbf3 Propagate screen Init error on resume 2026-02-18 09:30:05 +09:00
lestrrat 3564a975b5
Merge pull request #680 from peco/fix-scanner-err-check
Check scanner.Err() after scan loop
2026-02-18 09:29:08 +09:00
Daisuke Maki a77cad279a Appease linter 2026-02-18 09:27:34 +09:00
Daisuke Maki 8d5a0f672c Check scanner.Err() after scan loop 2026-02-18 09:20:04 +09:00
lestrrat 77f66d5150
Merge pull request #679 from peco/fix-cancel-func-data-race
Fix data race on cancelFunc and err fields
2026-02-18 09:18:30 +09:00
Daisuke Maki b7e04a549a Fix data race on cancelFunc and err fields 2026-02-18 09:15:55 +09:00
lestrrat c73fa58511
Merge pull request #678 from peco/golangci-lint
Check for far wider range of things in lint
2026-02-18 08:38:47 +09:00
Daisuke Maki 464c1dc72a revert goroutine error wrapping 2026-02-18 08:36:39 +09:00
Daisuke Maki c877ae72d9 appease linter 2026-02-18 08:30:04 +09:00
Daisuke Maki e3005e7a14 Update lint version 2026-02-18 08:21:02 +09:00
Daisuke Maki 46331edcfd Check for far wider range of things in lint 2026-02-18 07:49:40 +09:00
lestrrat 6435d29b6d
Merge pull request #677 from peco/refactor-typed-context-key
convert missing context key refactoring
2026-02-17 21:42:33 +09:00
Daisuke Maki e61e114ded convert missing context key refactoring 2026-02-17 21:40:04 +09:00
lestrrat bcfd3bff83
Merge pull request #676 from peco/refactor-dedup-batch-loop
Batch flush refactoring
2026-02-17 21:35:39 +09:00
lestrrat d4744778bb
Merge pull request #675 from peco/refactor-remove-unnecessary-sort
remove unnecessary sort
2026-02-17 21:34:55 +09:00
lestrrat 761dd460a9
Merge pull request #674 from peco/fix-unguarded-pdebug-filter
guard debug print
2026-02-17 21:33:05 +09:00
lestrrat 78aff3a21b
Merge pull request #673 from peco/fix-options-write-error
explicitly ignore errors
2026-02-17 21:32:51 +09:00
lestrrat 970b3e545f
Merge pull request #672 from peco/fix-tab-rendering-off-by-one
fix off-by-one drawing tabs
2026-02-17 21:32:27 +09:00
Daisuke Maki 3869c3bd25 Batch flush refactoring 2026-02-17 21:24:40 +09:00
Daisuke Maki 28d16a4b1a remove unnecessary sort 2026-02-17 20:25:37 +09:00
Daisuke Maki c69a935ea8 explicitly ignore errors 2026-02-17 20:20:28 +09:00
Daisuke Maki ded5d7f305 guard debug print 2026-02-17 20:17:52 +09:00
Daisuke Maki 681d034bc4 fix off-by-one drawing tabs 2026-02-17 20:09:27 +09:00
lestrrat a2959c038a
Merge pull request #671 from peco/refactor-extract-run-helpers
refactor
2026-02-17 19:35:59 +09:00
Daisuke Maki 32765acb02 refactor 2026-02-17 19:34:01 +09:00
lestrrat ce6dac6800
Merge pull request #670 from peco/fix-viewarround-typo
Fix typo
2026-02-17 19:30:46 +09:00
lestrrat 32f69ee127
Merge pull request #669 from peco/fix-unguarded-pdebug
fix unguarded debug prints
2026-02-17 19:30:29 +09:00
lestrrat e34cd07e65
Merge pull request #668 from peco/fix-caret-naming
style: fix naming
2026-02-17 19:29:42 +09:00
lestrrat 2b7c9190f0
Merge pull request #667 from peco/fix-urnknown-typo
fix typo
2026-02-17 19:28:49 +09:00
lestrrat b0c340aca1
Merge pull request #666 from peco/fix-duplicate-attr-reverse
remove duplicate specification
2026-02-17 19:28:04 +09:00
lestrrat f6f3111df6
Merge pull request #665 from peco/fix-inline-buffer-size
simplify
2026-02-17 19:27:39 +09:00
lestrrat 45ba1585c0
Merge pull request #664 from peco/remove-filterquery
remove filter query struct
2026-02-17 19:27:17 +09:00
lestrrat b67e306752
Merge pull request #663 from peco/remove-cli-struct
remove unused struct
2026-02-17 19:24:34 +09:00
lestrrat a734850952
Merge pull request #662 from peco/remove-filter-outch
Remove unused outch
2026-02-17 19:24:06 +09:00
Daisuke Maki ec9ebb4f02 Fix typo 2026-02-17 19:22:00 +09:00
Daisuke Maki 4f795dc6cb fix unguarded debug prints 2026-02-17 19:16:39 +09:00
Daisuke Maki 1bb115f22b fix typo 2026-02-17 19:14:41 +09:00
Daisuke Maki 7f2815efab style: fix naming 2026-02-17 19:14:04 +09:00
Daisuke Maki 55c3397a6d remove duplicate specification 2026-02-17 19:10:32 +09:00
Daisuke Maki 4922d24a68 simplify 2026-02-17 19:05:51 +09:00
Daisuke Maki f3f29cf45d remove filter query struct 2026-02-17 19:04:52 +09:00
lestrrat 44780a2518
Merge pull request #661 from peco/remove-hub-issync
remove unused
2026-02-17 19:02:13 +09:00
Daisuke Maki aacc46f593 remove unused field 2026-02-17 19:01:41 +09:00
Daisuke Maki af981da581 remove unused struct 2026-02-17 19:01:11 +09:00
Daisuke Maki 939f75ee85 Remove unused outch 2026-02-17 18:58:18 +09:00
Daisuke Maki 055b7c3bbc remove unused 2026-02-17 18:54:39 +09:00
lestrrat b9702a35b8
Merge pull request #660 from peco/fix-select-remove-nil
avoid possibility of selection.Remove(nil)
2026-02-17 18:53:52 +09:00
lestrrat 02012db634
Merge pull request #659 from peco/fix-inline-pollevent-panic-swallow
don't just swallow the panic from inline tcell polling
2026-02-17 18:52:27 +09:00
Daisuke Maki bb004da328 avoid possibility of selection.Remove(nil) 2026-02-17 18:51:03 +09:00
lestrrat 01a3faccb5
Merge pull request #658 from peco/fix-hub-batch-panic-swallow
Avoid hub.Batch swalloing panics
2026-02-17 18:17:21 +09:00
Daisuke Maki 60d90d2ae1 don't just swallow the panic from inline tcell polling 2026-02-17 18:15:57 +09:00
Daisuke Maki 4ad0bc31c6 Avoid hub.Batch swalloing panics 2026-02-17 18:11:47 +09:00
lestrrat 5fe676efb6
Merge pull request #657 from peco/chore-generate-ci
Check for generated files
2026-02-17 18:06:05 +09:00
Daisuke Maki c3c4116dc0 Check for generated files 2026-02-17 18:03:46 +09:00
lestrrat 6bce9be413
Merge pull request #656 from peco/fix-global-action-registry
Fix global action registry
2026-02-17 14:53:25 +09:00
lestrrat 6be6200c0d
Merge pull request #655 from peco/decouple-layout-from-peco
remove direct access to peco from layout
2026-02-17 14:46:35 +09:00
Daisuke Maki 6037ac2ee5 Avoid action-at-a-distance when we don't need to 2026-02-17 14:44:49 +09:00
Daisuke Maki 65643706c7 remove direct access to peco from layout 2026-02-17 14:44:18 +09:00
Daisuke Maki 6c8ecd04fd remove direct access to peco from layout 2026-02-17 13:51:42 +09:00
lestrrat 7907224eda
Merge pull request #654 from peco/refactor-peco-god-object
Refactor Peco object
2026-02-17 13:46:52 +09:00
Daisuke Maki e2ddf46710 consolidate state objects into one file 2026-02-17 13:38:05 +09:00
Daisuke Maki de0b3bf338 Refactor query exec state 2026-02-17 13:31:10 +09:00
Daisuke Maki 4e0f004cb3 refactor frozen state 2026-02-17 13:28:21 +09:00
Daisuke Maki a813e7198e Refactor ZoomState 2026-02-17 13:25:21 +09:00
Daisuke Maki 24df34ddda refactor single key jump into its own state object 2026-02-17 13:20:10 +09:00
lestrrat 3f2817153b
Merge pull request #653 from peco/use-typed-struct-context-keys
Use saner struct key approach
2026-02-17 13:17:08 +09:00
Daisuke Maki 28d7583555 Use saner struct key approach 2026-02-17 13:13:28 +09:00
lestrrat 4ebbe37d8c
Merge pull request #650 from peco/complete-test-todos
more tests
2026-02-17 12:44:00 +09:00
Daisuke Maki 35f920b3f1 should do the trick 2026-02-17 12:39:28 +09:00
Daisuke Maki a861e13008 see if this helps 2026-02-17 12:36:20 +09:00
Daisuke Maki 7e43c9e1c9 tweak 2026-02-17 12:36:20 +09:00
Daisuke Maki 5c13375911 tweak caching 2026-02-17 12:36:20 +09:00
Daisuke Maki 4746c32231 more tests 2026-02-17 12:36:20 +09:00
lestrrat efac590a5c
Merge pull request #651 from peco/remove-matchindexer
Remove unused interface
2026-02-17 12:04:49 +09:00
lestrrat 0c55a69065
Merge pull request #652 from peco/document-buffer-sealing
doc tweak
2026-02-17 12:03:14 +09:00
lestrrat 785f8e264d
Merge pull request #649 from peco/extraoffset-to-const
Move variable to platform specific const
2026-02-17 12:02:57 +09:00
Daisuke Maki c373a303a8 Remove unused interface 2026-02-17 11:59:10 +09:00
Daisuke Maki e4de1c5504 doc tweak 2026-02-17 11:58:53 +09:00
Daisuke Maki 3a14f1f89b Move variable to platform specific const 2026-02-17 11:58:32 +09:00
lestrrat 20ad174000
Merge pull request #648 from peco/update-manpage
docs: update man page
2026-02-17 11:40:56 +09:00
lestrrat d0489cc5bd docs: regenerate man page from README 2026-02-17 02:27:49 +00:00
lestrrat 0986318381
Merge pull request #646 from peco/fix-skip-read-config
refactor how we skip config reads in tests
2026-02-17 11:27:42 +09:00
lestrrat aa89a3bc42
Merge pull request #647 from peco/remove-deprecated-config-fields
remove unused config entries
2026-02-17 11:27:28 +09:00
Daisuke Maki 8b87099a71 remove unused config entries 2026-02-17 11:25:22 +09:00
lestrrat e5d29e85be
Merge pull request #645 from peco/fix-filter-work-goroutine-wait
Wait for worker
2026-02-17 11:24:32 +09:00
Daisuke Maki 9d6d8a8531 refactor how we skip config reads in tests 2026-02-17 11:22:36 +09:00
Daisuke Maki e4da6d08b1 Wait for worker 2026-02-17 11:13:31 +09:00
lestrrat 3ba1cbdb01
Merge pull request #643 from peco/fix-input-alt-key-race
Handle esc in the input loop as well
2026-02-17 11:10:29 +09:00
lestrrat 84484a3213
Merge pull request #644 from peco/fix-hub-payload-waitdone
make this operation safer. (not necessarily a problem)
2026-02-17 11:10:12 +09:00
lestrrat 0aac27f2a5
Merge pull request #642 from peco/fix-context-background-propagation
properly propagate ctx
2026-02-17 11:07:10 +09:00
Daisuke Maki dfe6f1e775 make this operation safer. (not necessarily a problem) 2026-02-17 11:06:33 +09:00
Daisuke Maki 873d6c4236 Handle esc in the input loop as well 2026-02-17 11:04:13 +09:00
Daisuke Maki 02f7140f27 properly propagate ctx 2026-02-17 10:17:26 +09:00
lestrrat c76990e3f0
Merge pull request #641 from peco/fix-query-exec-timer-race
be more paranoid
2026-02-17 09:57:48 +09:00
lestrrat 382199aa53
Merge pull request #640 from peco/hub-remove-ctx-from-send
Refactor context usage around hub.send
2026-02-17 09:57:32 +09:00
Daisuke Maki fd89c1aefa be more paranoid 2026-02-17 09:55:12 +09:00
Daisuke Maki 7cd4b18817 Refactor context usage around hub.send 2026-02-17 09:53:29 +09:00
lestrrat 70123a95ae
Merge pull request #638 from peco/fix-screen-pollevent-panic-swallow
log pollevent errors
2026-02-17 07:51:19 +09:00
lestrrat 4285ef3779
Merge pull request #639 from peco/fix-external-filter-panic-swallow
don't throw away panics in external filters
2026-02-17 07:50:11 +09:00
Daisuke Maki 2f0d21d047 move the close outside 2026-02-17 07:49:27 +09:00
Daisuke Maki 8f0204320c don't throw away panics in external filters 2026-02-17 07:39:31 +09:00
Daisuke Maki cfc3736e8e log pollevent errors 2026-02-17 07:38:56 +09:00
lestrrat 2bd6fa1504
Merge pull request #637 from peco/dedup-test-setup
refactor test setup
2026-02-17 07:38:30 +09:00
Daisuke Maki fdf90a2121 refactor test setup 2026-02-17 07:36:21 +09:00
lestrrat 62e667cce8
Merge pull request #636 from peco/split-action-interface
Simplify Action interface
2026-02-17 07:30:52 +09:00
Daisuke Maki 27c8f08bea Simplify Action interface 2026-02-17 07:28:31 +09:00
lestrrat 79bc52f006
Merge pull request #635 from peco/split-messagehub
consolidate SendStatusMsg
2026-02-17 07:20:05 +09:00
lestrrat 90807c9142
Merge pull request #632 from peco/makefile-cleanup
cleanup Makefile
2026-02-17 07:18:11 +09:00
Daisuke Maki ffc3e85bd0 consolidate SendStatusMsg 2026-02-17 07:17:45 +09:00
lestrrat b8d9a859f7
Merge pull request #634 from peco/check-write-errors
Check for Write() errors
2026-02-17 07:16:00 +09:00
Daisuke Maki d0aadb6525 tweaks 2026-02-17 07:15:21 +09:00
lestrrat 48c7133f41
Merge pull request #633 from peco/replace-test-sleeps
use require.Eventually
2026-02-17 07:13:57 +09:00
lestrrat 06534679f1
Merge pull request #631 from peco/dedup-query-exec-draw
refactor query -> exec draw
2026-02-17 07:13:22 +09:00
lestrrat aff5a0d521
Merge pull request #630 from peco/fix-query-comments
fix comments
2026-02-17 07:13:03 +09:00
Daisuke Maki 446d0d332b Check for Write() errors 2026-02-17 07:11:18 +09:00
lestrrat c543201da9
Merge pull request #629 from peco/use-builtin-max
Use max()
2026-02-17 07:10:32 +09:00
Daisuke Maki 310e11700f use require.Eventually 2026-02-17 07:10:16 +09:00
Daisuke Maki 92a2b97712 cleanup Makefile 2026-02-17 07:09:12 +09:00
lestrrat e3472a53a9
Merge pull request #628 from peco/consolidate-postinit
remove PostInit
2026-02-17 07:08:54 +09:00
Daisuke Maki cd26822d4c refactor query -> exec draw 2026-02-17 07:08:20 +09:00
Daisuke Maki 576519b02f fix comments 2026-02-17 07:06:50 +09:00
Daisuke Maki a74e621fe0 Use max() 2026-02-17 07:05:24 +09:00
Daisuke Maki 6ecb5c777d remove PostInit 2026-02-17 07:04:32 +09:00
lestrrat 57a10a87a2
Merge pull request #627 from peco/add-test-coverage
Add test coverage
2026-02-17 07:03:02 +09:00
Daisuke Maki 4c439c71a8 Add test coverage 2026-02-17 07:00:32 +09:00
lestrrat e1164500e6
Merge pull request #626 from peco/remove-layout-panics
Don't panic for invalid values
2026-02-17 00:08:48 +09:00
lestrrat 51396c3c12
Merge pull request #625 from peco/dedup-paging-actions
Refactor paging related actions
2026-02-17 00:06:55 +09:00
Daisuke Maki 772c3ef5f1 Don't panic for invalid values 2026-02-17 00:06:20 +09:00
Daisuke Maki 4cd07c8dd3 Refactor paging related actions 2026-02-17 00:04:51 +09:00
lestrrat 5e9de3365b
Merge pull request #624 from peco/update-manpage
docs: update man page
2026-02-16 23:55:26 +09:00
lestrrat 470179cd60
Merge pull request #623 from peco/typed-pipeline-channels
use types in pipelines
2026-02-16 23:54:26 +09:00
lestrrat ac7821dce4 docs: regenerate man page from README 2026-02-16 14:53:04 +00:00
lestrrat 550d56c9a4
Merge pull request #622 from peco/rename-termbox
Replace Termbox (name references) with Tcell
2026-02-16 23:52:43 +09:00
Daisuke Maki f5f6262d82 use types in pipelines 2026-02-16 23:51:36 +09:00
Daisuke Maki a1d7596b1a Replace Termbox (name references) with Tcell 2026-02-16 23:50:19 +09:00
lestrrat dc53fb8909
Merge pull request #621 from peco/fix-input-modifier-race
Fix possible input modifier detection race
2026-02-16 23:11:50 +09:00
Daisuke Maki b775ac5dea Fix possible input modifier detection race 2026-02-16 23:08:47 +09:00
lestrrat c2c812db24
Merge pull request #620 from peco/fix-signal-stop
Fix possible race in signal handler
2026-02-16 23:04:00 +09:00
lestrrat 74adf55448
Merge pull request #619 from peco/fix-7.1-suspend-goroutine-leak
Fix possible goroutine leak during suspend
2026-02-16 23:01:43 +09:00
Daisuke Maki 78d7e4a5d8 Fix possible race in signal handler 2026-02-16 23:01:18 +09:00
lestrrat 4541ad2962
Merge pull request #618 from peco/dedup-batch-action
refactor batchAction
2026-02-16 22:55:49 +09:00
Daisuke Maki cdb31988d8 Fix possible goroutine leak during suspend 2026-02-16 22:55:20 +09:00
Daisuke Maki 3a85ae62e0 refactor batchAction 2026-02-16 22:52:18 +09:00
lestrrat deee60fad0
Merge pull request #617 from peco/fix-filter-duplication
Refactor filter
2026-02-16 22:45:24 +09:00
lestrrat b152854e6c
Merge pull request #616 from peco/dedup-freeze-unfreeze
slight refactor around freeze/unfreeze
2026-02-16 22:44:15 +09:00
lestrrat 5a88606205
Merge pull request #615 from peco/typed-hub-channels
Change Hub structure to take typed payloads
2026-02-16 22:43:44 +09:00
Daisuke Maki 7f62fe18b3 Refactor filter 2026-02-16 22:43:11 +09:00
Daisuke Maki 19f9bd1be9 slight refactor around freeze/unfreeze 2026-02-16 22:41:44 +09:00
Daisuke Maki 12a55491ef Change Hub structure to take typed payloads 2026-02-16 22:40:34 +09:00
lestrrat 53a69a609f
Merge pull request #614 from peco/dedup-selection-nav
refactor navigation code
2026-02-16 22:36:21 +09:00
Daisuke Maki d40ea73946 refactor navigation code 2026-02-16 22:32:25 +09:00
lestrrat fe611dde03
Merge pull request #613 from peco/typed-on-cancel
Use types for OnCancel values
2026-02-16 22:22:09 +09:00
Daisuke Maki 952884009c Use types for OnCancel values 2026-02-16 22:17:35 +09:00
lestrrat eb3da9c70f
Merge pull request #612 from peco/remove-unused-state-interface
Remove unused interface
2026-02-16 22:14:06 +09:00
Daisuke Maki 5bd247e9a6 Remove unused interface 2026-02-16 22:09:59 +09:00
lestrrat 2debeb5b28
Merge pull request #611 from peco/remove-pkg-errors
Do away with pkg/errors
2026-02-16 22:09:00 +09:00
Daisuke Maki 91339e96f9 Do away with pkg/errors 2026-02-16 22:06:21 +09:00
lestrrat b7f50d8a33
Merge pull request #610 from peco/fix-gomaxprocs
This is so 2010
2026-02-16 21:58:51 +09:00
Daisuke Maki 5f20cf3b7f This is so 2010 2026-02-16 21:56:04 +09:00
lestrrat 4c1065f284
Merge pull request #609 from peco/fix-external-filter-goroutine-leak
Wait for reader goroutine
2026-02-16 21:47:39 +09:00
lestrrat f0c35f429b
Merge pull request #608 from peco/fix-query-exec-timer-leak
stop the query exec timer
2026-02-16 21:47:25 +09:00
Daisuke Maki eb17420fee stop the query exec timer 2026-02-16 21:44:59 +09:00
lestrrat 6de9f7e58f
Merge pull request #607 from peco/fix-resume-deadlock
Remove the possibility of Resume() deadlocking
2026-02-16 21:42:52 +09:00
Daisuke Maki 090f44859a Wait for reader goroutine 2026-02-16 21:42:14 +09:00
Daisuke Maki 8710f98b1f Remove the possibility of Resume() deadlocking 2026-02-16 21:35:56 +09:00
lestrrat 82bc046253
Merge pull request #606 from peco/fix-frozen-done-channel
Create MarkComplete() method to avoid future us from deadlocking
2026-02-16 21:33:27 +09:00
Daisuke Maki d691144f96 Create MarkComplete() method to avoid future us from deadlocking 2026-02-16 21:26:27 +09:00
lestrrat 0fb619016d
Merge pull request #605 from peco/update-manpage
docs: update man page
2026-02-16 21:24:38 +09:00
lestrrat 731c3972b7
Merge branch 'master' into update-manpage 2026-02-16 21:22:55 +09:00
lestrrat d298cca584 docs: regenerate man page from README 2026-02-16 12:21:46 +00:00
lestrrat e0de14a560
Merge pull request #603 from peco/update-manpage
docs: update man page
2026-02-16 21:21:42 +09:00
lestrrat 8ff71a8c69
Merge pull request #604 from peco/remove-toc
We don't need a TOC in the age of AIs
2026-02-16 21:21:26 +09:00
Daisuke Maki 909635d588 We don't need a TOC in the age of AIs 2026-02-16 21:14:27 +09:00
lestrrat 4b49f65118 docs: regenerate man page from README 2026-02-16 12:00:38 +00:00
lestrrat dc696bc92f
Merge pull request #602 from peco/gh-398
Add ANSI color support
2026-02-16 21:00:18 +09:00
Daisuke Maki d8f557e108 Merge branch 'master' into gh-398 2026-02-16 20:57:37 +09:00
Daisuke Maki 49f7153598 Add ANSI color support 2026-02-16 20:06:53 +09:00
lestrrat e82a4a67e2
Merge pull request #601 from peco/update-manpage
docs: update man page
2026-02-16 19:58:04 +09:00
lestrrat 3aa22bd477 docs: regenerate man page from README 2026-02-16 10:56:30 +00:00
lestrrat 087900d58f
Merge pull request #600 from peco/gh-289
Implement zoom in/out
2026-02-16 19:56:10 +09:00
Daisuke Maki d4ef70813f Implement zoom in/out 2026-02-16 19:49:49 +09:00
lestrrat c33d0f50cb
Merge pull request #599 from peco/update-manpage
docs: update man page
2026-02-16 19:30:25 +09:00
lestrrat ba97c8394e docs: regenerate man page from README 2026-02-16 10:28:28 +00:00
lestrrat e97970e021
Merge pull request #598 from peco/gh-471
Implement --height for inline display
2026-02-16 19:28:09 +09:00
Daisuke Maki 479226adc2 Appease linter 2026-02-16 19:24:59 +09:00
Daisuke Maki ee131131c7 Implement --height for inline display 2026-02-16 19:17:09 +09:00
lestrrat 84017db910
Merge pull request #597 from peco/update-manpage
docs: update man page
2026-02-16 19:10:06 +09:00
lestrrat 6ec2528651 docs: regenerate man page from README 2026-02-16 09:13:49 +00:00
lestrrat e366f13607
Merge pull request #596 from peco/gh-424
Implement freeze/unfreeze
2026-02-16 18:13:26 +09:00
Daisuke Maki b3264ffd9a see if this fixes flaky tests 2026-02-16 18:11:15 +09:00
Daisuke Maki 68d17afc7c Implement freeze/unfreeze 2026-02-16 15:43:38 +09:00
lestrrat 757abc8b66
Merge pull request #595 from peco/gh-445
Add new layout, refactr layout lookup/registration
2026-02-16 15:10:22 +09:00
Daisuke Maki 4f1d149ceb Add new layout, refactr layout lookup/registration 2026-02-16 15:05:01 +09:00
lestrrat 35a014be56
Merge pull request #593 from peco/update-manpage
docs: update man page
2026-02-16 14:57:15 +09:00
lestrrat 97e97dffe1 docs: regenerate man page from README 2026-02-16 05:50:23 +00:00
lestrrat aae60b5083
Merge pull request #594 from peco/gh-480
Explicitly document horizontal scrolling
2026-02-16 14:50:04 +09:00
Daisuke Maki 7b9a0b2021 Explicitly document horizontal scrolling 2026-02-16 14:46:46 +09:00
lestrrat 637c5b3e07
Merge pull request #592 from peco/gh-434
Negative filters
2026-02-16 14:37:20 +09:00
Daisuke Maki 8ac38b0de6 appease linter 2026-02-16 14:35:30 +09:00
Daisuke Maki 87ce6ab7cb Negative filters 2026-02-16 14:31:16 +09:00
lestrrat 2e2c39bb8a
Merge pull request #591 from peco/update-manpage
docs: update man page
2026-02-16 11:51:45 +09:00
lestrrat f6fed6226f docs: regenerate man page from README 2026-02-16 02:49:46 +00:00
lestrrat 4b209aaf84
Merge pull request #559 from curry798/patch-1
docs: add x-cmd method to install peco
2026-02-16 11:49:27 +09:00
lestrrat fb07df89c0
Merge branch 'master' into patch-1 2026-02-16 11:49:19 +09:00
lestrrat b0b422152b
Merge pull request #590 from peco/gh-454
Performance optimizations for large buffers
2026-02-16 11:29:59 +09:00
Daisuke Maki 2c591ff8ed Batch sending source -> filter 2026-02-16 10:58:18 +09:00
Daisuke Maki 17549703ae pre-allocate buffer lines 2026-02-16 10:24:58 +09:00
Daisuke Maki fbba2f8dc3 batch append 2026-02-16 10:07:00 +09:00
Daisuke Maki 835f4698c2 Use ctx.Done() to check for Send() success/failure 2026-02-16 09:59:59 +09:00
Daisuke Maki 1a76db2e67 Skip processing in chunks for parallel workers 2026-02-16 09:27:33 +09:00
Daisuke Maki 97f408b99e First pass optimizing for large buffers 2026-02-16 09:00:07 +09:00
lestrrat e54c80ebbe
Merge pull request #589 from peco/gh-543
Fix #543
2026-02-15 19:45:08 +09:00
Daisuke Maki ca4a52ba53 Fix #543 2026-02-15 19:42:00 +09:00
lestrrat 1921422bf8
Merge pull request #588 from peco/gh-455
Fix #455
2026-02-15 18:21:58 +09:00
Daisuke Maki a35076da6f Fix #455 2026-02-15 18:13:54 +09:00
lestrrat 46ecbd1e57
Merge pull request #587 from peco/gh-467
Add man page. fixes #467
2026-02-15 16:56:01 +09:00
Daisuke Maki 31f2ecd178 Add man page. fixes #467 2026-02-15 16:47:48 +09:00
lestrrat 7715ff482f
Merge pull request #586 from peco/gh-294
fix #294
2026-02-15 15:42:44 +09:00
Daisuke Maki 56c81db24a gh-294 2026-02-15 15:35:21 +09:00
lestrrat ab653b3a53
Merge pull request #585 from peco/gh-511
Fix #511
2026-02-15 14:22:02 +09:00
Daisuke Maki 17b0aba8e8 Fix #511 2026-02-15 14:19:08 +09:00
lestrrat 8ecd1711e0
Merge pull request #584 from peco/gh-428
fix #428
2026-02-15 14:10:22 +09:00
Daisuke Maki aa1335ff70 fix #428 2026-02-15 14:07:29 +09:00
lestrrat 7c8c95b391
Merge pull request #583 from peco/gh-460
Fix #460
2026-02-15 13:18:35 +09:00
Daisuke Maki 214f2faeb1 Fix #460 2026-02-15 13:15:27 +09:00
lestrrat 59108b523e
Merge pull request #582 from peco/exit-0
fix #538
2026-02-15 09:24:53 +09:00
Daisuke Maki b560fcbbe7 fix #538 2026-02-15 09:11:18 +09:00
lestrrat 4ed16af465
Merge pull request #581 from peco/fix-ctrl-shift-modifiers
Fix #529
2026-02-15 08:48:57 +09:00
Daisuke Maki 0ef5cdb83d Fix #529 2026-02-15 08:44:21 +09:00
lestrrat 29d968e359
Merge pull request #580 from peco/gh-557
Fix #557
2026-02-15 08:16:14 +09:00
Daisuke Maki 3a7556f264 Fix #557 2026-02-15 08:14:33 +09:00
lestrrat c2f53b273d
Merge pull request #579 from peco/gh-575
Fix bindings, fix #575
2026-02-15 07:50:11 +09:00
Daisuke Maki f2c6c895b8 Fix bindings, fix #575 2026-02-15 07:46:14 +09:00
lestrrat 46f3bb7fe5
Merge pull request #578 from peco/gh-574
Fix #574
2026-02-15 07:39:19 +09:00
Daisuke Maki 0fb46c5d1e Fix #574 2026-02-15 07:37:26 +09:00
lestrrat 496c035741
Merge pull request #577 from peco/fix-null-external-filter
Fix #312
2026-02-14 23:01:48 +09:00
Daisuke Maki 1effee57e9 tweak 2026-02-14 22:55:48 +09:00
Daisuke Maki e228d8d366 tweak 2026-02-14 22:47:42 +09:00
Daisuke Maki be3d6cff25 Fix #312 2026-02-14 21:54:48 +09:00
lestrrat f701816be7
Merge pull request #573 from mattn/fix-bug
Remove leading tab from doGoToPreviousSelection function name
2026-02-14 19:24:59 +09:00
lestrrat 2253af3961
Merge pull request #576 from peco/yaml-config
Add YAML support
2026-02-14 19:24:23 +09:00
Daisuke Maki ed9a2aa56c Add YAML support 2026-02-14 19:20:25 +09:00
Yasuhiro Matsumoto 0d84ed1846
Remove leading tab from doGoToPreviousSelection function name 2026-02-14 17:37:13 +09:00
Yasuhiro Matsumoto c1ffd8b128
go fmt ./... 2026-02-14 17:36:53 +09:00
lestrrat 3723b65a5f
Merge pull request #572 from peco/memory-fixes
optimizations around memory usage
2026-02-14 17:13:49 +09:00
Daisuke Maki e4f2b4837d optimizations around memory usage 2026-02-14 15:47:37 +09:00
lestrrat 95f9dba8ad
Merge pull request #571 from peco/dependabot/go_modules/github.com/google/btree-1.1.3
Bump github.com/google/btree from 1.1.2 to 1.1.3
2026-02-14 15:27:04 +09:00
dependabot[bot] 3621c464d4
Bump github.com/google/btree from 1.1.2 to 1.1.3
Bumps [github.com/google/btree](https://github.com/google/btree) from 1.1.2 to 1.1.3.
- [Commits](https://github.com/google/btree/compare/v1.1.2...v1.1.3)

---
updated-dependencies:
- dependency-name: github.com/google/btree
  dependency-version: 1.1.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-14 06:25:06 +00:00
lestrrat d7fde6ac27
Merge pull request #570 from peco/dependabot/go_modules/github.com/jessevdk/go-flags-1.6.1
Bump github.com/jessevdk/go-flags from 1.5.0 to 1.6.1
2026-02-14 15:24:20 +09:00
dependabot[bot] 72dc9483ca
Bump github.com/jessevdk/go-flags from 1.5.0 to 1.6.1
Bumps [github.com/jessevdk/go-flags](https://github.com/jessevdk/go-flags) from 1.5.0 to 1.6.1.
- [Release notes](https://github.com/jessevdk/go-flags/releases)
- [Commits](https://github.com/jessevdk/go-flags/compare/v1.5.0...v1.6.1)

---
updated-dependencies:
- dependency-name: github.com/jessevdk/go-flags
  dependency-version: 1.6.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-14 00:27:11 +00:00
lestrrat 6caeefc551
Merge pull request #569 from peco/dependabot/go_modules/github.com/stretchr/testify-1.11.1
Bump github.com/stretchr/testify from 1.8.4 to 1.11.1
2026-02-14 09:26:28 +09:00
lestrrat e53e2c6213
Merge pull request #568 from peco/dependabot/go_modules/github.com/mattn/go-runewidth-0.0.19
Bump github.com/mattn/go-runewidth from 0.0.15 to 0.0.19
2026-02-14 09:26:19 +09:00
dependabot[bot] c3d9ee0527
Bump github.com/stretchr/testify from 1.8.4 to 1.11.1
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.8.4 to 1.11.1.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.8.4...v1.11.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-14 00:23:44 +00:00
dependabot[bot] 247683e347
Bump github.com/mattn/go-runewidth from 0.0.15 to 0.0.19
Bumps [github.com/mattn/go-runewidth](https://github.com/mattn/go-runewidth) from 0.0.15 to 0.0.19.
- [Commits](https://github.com/mattn/go-runewidth/compare/v0.0.15...v0.0.19)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-14 00:23:40 +00:00
lestrrat 1fc9c00698
Merge pull request #567 from peco/migrate-tcell
tcell migration
2026-02-14 09:23:07 +09:00
Daisuke Maki 0531cfd20a Properly reset screen 2026-02-14 09:13:18 +09:00
Daisuke Maki db63d9f380 Add linting fixes 2026-02-14 07:48:23 +09:00
Daisuke Maki 8356007755 Updae workflows 2026-02-13 20:07:44 +09:00
Daisuke Maki 7d42de7860 actual migration 2026-02-13 19:48:39 +09:00
Daisuke Maki e9c5582ba1 Initial pass 2026-02-13 12:35:06 +09:00
lestrrat 288862215c
Merge pull request #565 from ElementalWarrior/fix-makefile
Fix the makefile (dead glide refs, fix .phony)
2025-11-10 08:31:18 +09:00
James McDonnell 0c4112ee16 Add missing newline at the end of .gitignore 2025-11-09 15:02:10 -08:00
lestrrat 7b17e88d96
Merge branch 'master' into fix-makefile 2025-11-10 08:00:42 +09:00
lestrrat fa33145d71
Merge pull request #564 from ElementalWarrior/iregexp
Add case insensitive regex as a filter option
2025-11-10 08:00:30 +09:00
lestrrat 1ed60d275f
Merge branch 'master' into iregexp 2025-11-10 07:58:46 +09:00
lestrrat 5e01c7ce2f
Merge pull request #563 from ElementalWarrior/fix-query-toggle
Fix toggle query (C-t)
2025-11-10 07:58:07 +09:00
James McDonnell ccf33a8e97 Update readme to just use make 2025-11-09 14:45:52 -08:00
James McDonnell 5d9f2a78de Fix the makefile (dead glide refs, fix .phony)
Some of these variables were only referenced in old glide code,
which was partially removed.

Also the .PHONY line needed updating, for example, calling Building releases/peco_linux_amd64/peco
more than once would not output a new binary.

The release folder should be excluded from git, you don't want to commit
build artefacts.
2025-11-09 14:45:11 -08:00
James McDonnell 6d0ff86f90 Add case insensitive regex as a filter option
Regexp is the only way to do boolean OR searches.

Having the option to not have to type  is really convenient
2025-11-09 14:39:57 -08:00
James McDonnell 23707858fe Installdeps was removed from Makefile 2025-11-09 11:32:53 -08:00
James McDonnell 82faeab193 Fix toggle query
This logic makes no sense. It would just immediately undo the line preceeding
2025-11-09 11:32:36 -08:00
lestrrat a3f06f11be
Merge pull request #561 from eunos-1128/feature/readme-conda-installaion
Add installation instructions for the conda-format package
2025-06-22 05:56:28 +09:00
UENO, M. 1543952823
Add installation instructions for the conda-format package 2025-06-21 15:37:50 +09:00
curry798 b17182d9d2 docs: add x-cmd method to install peco 2024-10-10 15:35:23 +08:00
curry798 3a76bd164b docs: add x-cmd method to install tokei 2024-09-25 17:24:08 +08:00
lestrrat 51f38bf44c
Merge pull request #550 from peco/dependabot/go_modules/github.com/pkg/errors-0.9.1
Bump github.com/pkg/errors from 0.0.0-20161029093637-248dadf4e906 to 0.9.1
2023-09-17 07:43:47 +09:00
dependabot[bot] 330bd08291
Bump github.com/pkg/errors
Bumps [github.com/pkg/errors](https://github.com/pkg/errors) from 0.0.0-20161029093637-248dadf4e906 to 0.9.1.
- [Release notes](https://github.com/pkg/errors/releases)
- [Commits](https://github.com/pkg/errors/commits/v0.9.1)

---
updated-dependencies:
- dependency-name: github.com/pkg/errors
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-16 22:40:20 +00:00
lestrrat 06617b8768
Merge pull request #549 from peco/dependabot/go_modules/github.com/google/btree-1.1.2
Bump github.com/google/btree from 0.0.0-20161213163243-0c3044bc8bad to 1.1.2
2023-09-17 07:39:46 +09:00
dependabot[bot] 0fcedc33d9
Bump github.com/google/btree
Bumps [github.com/google/btree](https://github.com/google/btree) from 0.0.0-20161213163243-0c3044bc8bad to 1.1.2.
- [Commits](https://github.com/google/btree/commits/v1.1.2)

---
updated-dependencies:
- dependency-name: github.com/google/btree
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-16 22:32:30 +00:00
lestrrat 41a1b95a91
Merge pull request #548 from peco/dependabot/go_modules/github.com/jessevdk/go-flags-1.5.0
Bump github.com/jessevdk/go-flags from 1.1.0 to 1.5.0
2023-09-17 07:31:57 +09:00
lestrrat fd875d9739
Merge branch 'master' into dependabot/go_modules/github.com/jessevdk/go-flags-1.5.0 2023-09-17 07:27:36 +09:00
lestrrat 427ee9eb89
Merge pull request #547 from peco/dependabot/github_actions/actions/checkout-4
Bump actions/checkout from 2 to 4
2023-09-17 07:27:22 +09:00
lestrrat 73738a251b
Merge branch 'master' into dependabot/github_actions/actions/checkout-4 2023-09-17 07:20:22 +09:00
dependabot[bot] 49aa736533
Bump github.com/jessevdk/go-flags from 1.1.0 to 1.5.0
Bumps [github.com/jessevdk/go-flags](https://github.com/jessevdk/go-flags) from 1.1.0 to 1.5.0.
- [Release notes](https://github.com/jessevdk/go-flags/releases)
- [Commits](https://github.com/jessevdk/go-flags/compare/v1.1...v1.5.0)

---
updated-dependencies:
- dependency-name: github.com/jessevdk/go-flags
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-16 22:19:29 +00:00
lestrrat 2ae9bd4c59
Merge pull request #546 from peco/dependabot/go_modules/github.com/stretchr/testify-1.8.4
Bump github.com/stretchr/testify from 0.0.0-20161117074351-18a02ba4a312 to 1.8.4
2023-09-17 07:18:55 +09:00
dependabot[bot] d91744a24b
Bump actions/checkout from 2 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-16 22:16:03 +00:00
lestrrat 16e8a9f97d
Merge branch 'master' into dependabot/go_modules/github.com/stretchr/testify-1.8.4 2023-09-17 07:15:53 +09:00
lestrrat 6468ffad6e
Merge pull request #545 from peco/dependabot/github_actions/actions/setup-go-4
Bump actions/setup-go from 2 to 4
2023-09-17 07:15:30 +09:00
dependabot[bot] 8f1e90e10e
Bump github.com/stretchr/testify
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 0.0.0-20161117074351-18a02ba4a312 to 1.8.4.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/commits/v1.8.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-16 22:00:49 +00:00
dependabot[bot] 819ab79a8a
Bump actions/setup-go from 2 to 4
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 2 to 4.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v2...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-16 22:00:46 +00:00
lestrrat aa4c20d911
Merge pull request #544 from RincewindsHat/update_termbox_dependency
Dependency Update and automatic update checks
2023-09-17 07:00:29 +09:00
Lorenz Kästle f8145bb325 Add dependabot config 2023-09-15 14:48:16 +02:00
rincewind 2779a03cdd Update termbox-go dependency 2023-09-15 14:44:07 +02:00
Daisuke Maki 46703dc3aa Update Changes; version++ 2023-03-21 16:31:07 +09:00
lestrrat 7352294fa4 docs: update toc 2023-03-08 22:22:23 +00:00
lestrrat dd0d8ae00f
Merge pull request #539 from puhitaku/puhitaku/fuzzylongest
Add FuzzyLongest filter
2023-03-09 07:22:04 +09:00
Takumi Sueda 06a80bf8a9 Describe FuzzyLongestSort flag in the document 2023-03-08 23:46:19 +09:00
Takumi Sueda 8c3265f159 Add FuzzyLongest filter 2023-03-08 23:33:08 +09:00
Takumi Sueda 1e185a785f Add FuzzyLongestSort flag 2023-03-08 23:33:08 +09:00
Takumi Sueda 3af95d6d07 Bump Go version to 1.20.x 2023-03-08 23:32:32 +09:00
lestrrat ce38fa40be
Merge pull request #536 from cha147/patch-1
fix typos in readme
2022-07-14 07:30:04 +09:00
cha147 a7853fe85b fix typos in readme 2022-07-13 15:12:04 -07:00
lestrrat d4b3c115e9
Merge pull request #528 from peco/switch-github-actions
Switch to github actions from travis-ci
2021-07-30 12:30:04 +09:00
Shohei YOSHIDA 9c8756c26e Switch to github actions from travis-ci
Travis-CI is no longer available
2021-07-30 11:55:54 +09:00
Shohei YOSHIDA a1678931a9
Merge pull request #527 from shirayu/fix_typos
Fix typos
2021-07-26 12:27:08 +09:00
Yuta Hayashibe dc559668f3 Fix typos 2021-07-24 23:32:15 +09:00
lestrrat f39f746023 docs: update toc 2021-07-19 03:42:24 +00:00
lestrrat d350aae268
Merge pull request #526 from grawlinson/update_readme
docs: update reference to Arch Linux package
2021-07-19 12:42:03 +09:00
George Rawlinson 366ed21d52
docs: update reference to Arch Linux package 2021-07-19 02:38:50 +00:00
Daisuke Maki 27cc910092 Update Changes; v0.5.10 2021-06-08 18:15:34 +09:00
Daisuke Maki 21d3f70e05 Fix Makefile 2021-06-08 18:15:29 +09:00
Daisuke Maki bf09debbe1 Update Changes 2021-06-08 15:22:36 +09:00
Daisuke Maki 2550b108b1 do away with 386 2021-06-08 15:19:53 +09:00
Daisuke Maki ea18d1890f add go.sum 2021-06-07 22:48:32 +09:00
lestrrat af816b11c3
Merge pull request #520 from skaji/darwin-arm64
Add darwin-arm64
2021-04-30 12:06:22 +09:00
Shoichi Kaji 19f296dfa4 Use go1.16.x in travis ci 2021-04-30 09:52:06 +09:00
Shoichi Kaji cc161e0c4b Add darwin-arm64 2021-04-29 21:46:46 +09:00
lestrrat d8b6fb5601
Merge pull request #515 from neuenmuller/doctoc-official
switch back doctoc to official release
2021-03-15 18:48:25 +09:00
Sarisia d237add077 switch back doctoc to official release 2020-12-07 22:41:52 +09:00
lestrrat 4045ebad41
Merge pull request #514 from dthadi3/ppc64le
Travis-ci: added support for ppc64le
2020-12-03 07:53:22 +09:00
Devendranath Thadi 80cc79651d Travis-ci: added support for ppc64le 2020-12-02 11:23:36 +00:00
lestrrat 9e70017663
Merge pull request #513 from peco/syohex/fix-double-cursor-issue
Fix double cursor issue
2020-10-23 12:33:57 +09:00
Shohei YOSHIDA 1ee7eade47 Fix double cursor issue 2020-10-23 11:59:56 +09:00
lestrrat 8417a33e75 docs: update toc 2020-08-14 06:47:22 +00:00
lestrrat ed54c2d02d
Update README.md 2020-08-14 15:47:01 +09:00
lestrrat 21baa08c54
Merge pull request #509 from neuenmuller/toc-push
Use SSH deploy for TOC update
2020-08-14 15:25:11 +09:00
sarisia 7dce295f0e use ssh deploy 2020-08-12 15:42:07 +09:00
lestrrat 1b7d631c07
Merge pull request #508 from neuenmuller/fix-toc
Fix TOC in README.md
2020-08-12 13:18:52 +09:00
sarisia 653e368e2f add TOC section to README 2020-08-12 11:36:20 +09:00
sarisia 3c5fc2c915 use patched toc-generator in workflow 2020-08-12 11:36:01 +09:00
lestrrat e8c3231ea8
Merge pull request #506 from peco/release/v0.5.8
This be v0.5.8
2020-07-18 20:25:23 +09:00
Daisuke Maki e82bb34767 This be v0.5.8 2020-07-18 20:12:52 +09:00
lestrrat 8d38aa104c
Merge pull request #505 from narugit/feature/support-256colors
Support 256color
2020-07-18 20:09:30 +09:00
narugit e00862063c Add contributor (apply 256color) 2020-06-06 21:44:08 +09:00
narugit 5832d4aff3 Modify test to apply 256color feature 2020-06-06 21:44:00 +09:00
narugit a7cd43ff22 Apply 256color configuration 2020-06-06 21:38:11 +09:00
narugit a14f3abf35 Add configuration for 256color 2020-06-06 21:38:11 +09:00
narugit dc845d6028 Update README for 256color 2020-06-06 21:37:56 +09:00
Daisuke Maki 9c4464680c for whatever reason, I couldn't resolve #503, so here it is 2020-05-28 07:37:02 +09:00
Daisuke Maki 458682b9db Remove darwin-386, as it is no longer supported 2020-05-28 07:35:51 +09:00
lestrrat c1fffe19e8
Merge pull request #504 from imbsky/toc-gen
Use GitHub Actions to keep our ToC up to date
2020-05-28 07:31:54 +09:00
lestrrat 358871840d
Merge branch 'master' into toc-gen 2020-05-28 07:31:45 +09:00
lestrrat d2378d0d4c
Merge pull request #502 from imbsky/macos
Correct the name of macOS
2020-05-28 07:28:08 +09:00
Sora Morimoto fe2b4b56f8 Add toc.yml 2020-05-28 04:03:06 +09:00
Sora Morimoto 822a38601a Remove table of contents section from README 2020-05-28 04:03:06 +09:00
lestrrat ffc06167f8
Merge pull request #500 from pniedzwiedzinski/patch-1
Add AUR installation instruction
2020-05-27 06:25:34 +09:00
Sora Morimoto 1f0b384376 Correct the name of macOS 2020-05-27 05:35:03 +09:00
Patryk Niedźwiedziński 45e7fc6452
Add AUR installation instruction 2020-03-21 19:09:22 +01:00
lestrrat 9a06ebeedf
Merge pull request #496 from peco/release/v0.5.7
This be v0.5.7
2020-01-10 12:53:38 +09:00
Daisuke Maki 1b1d197911 This be v0.5.7 2020-01-10 12:45:00 +09:00
lestrrat 05485f5ce5
Merge pull request #495 from peco/topic/issue-494
Handle batch mode differently when the source is an infinite stream
2020-01-10 12:28:10 +09:00
Daisuke Maki b749b6191b some cleanups 2020-01-10 12:19:44 +09:00
Daisuke Maki 71c4c48efe properly set the requested value 2020-01-10 11:57:28 +09:00
Daisuke Maki 305a208ca2 remove non-applicable comments/commented sections/debug statements 2020-01-10 11:55:12 +09:00
Daisuke Maki 143638649c Add a new flag to pass to NewSource to detect infinite source
my previous detection for continueous input was completely wrong.
Now we just assume that anything coming in from Stdin could be infinite.
Additionally, in case we think it's infinite but we were actually able
to close the Reader stream, we consider it non-infinite after that
point.
2020-01-10 09:28:39 +09:00
Daisuke Maki 43ee9051f8 Fix blocking issue cased by #491
... by punting the problem when the input is an infinite stream.
This commit fixes the blocking, but the subsequent queries don't seem
to be properly running

refs #494
2020-01-09 20:22:18 +09:00
Daisuke Maki 6b42ace9d0 This be v0.5.6 2020-01-07 09:38:23 +09:00
Daisuke Maki 5aeaf9b2c0 This be v0.5.5 2020-01-02 08:26:10 +09:00
lestrrat 01cc2b3c6b
Merge pull request #492 from peco/topic/update-go-mod
Update go.mod (use new versioning, same termbox-go version)
2020-01-02 08:23:53 +09:00
Daisuke Maki 24136f4ffa Update go.mod (use new versioning, same termbox-go version) 2020-01-02 08:14:17 +09:00
lestrrat e7faada179
Merge pull request #491 from peco/topic/issue-490
Properly execute selectOneAndExit logic when an initial query is specified
2020-01-02 08:12:54 +09:00
Daisuke Maki 4d0335dbd5 Properly exec query even without --select-1 2020-01-01 16:08:33 +09:00
Daisuke Maki 55cdd5825f fix tests to more accurately reproduce user typing 2020-01-01 15:58:36 +09:00
Daisuke Maki 4ad576bc6e Properly execute selectOneAndExit logic when an initial query is specified 2020-01-01 15:27:28 +09:00
lestrrat 6bce15df53
Merge pull request #489 from shigemk2/remove_glide_readme
Delete description about glide because glide.yaml is already deleted
2019-12-24 11:02:41 +09:00
shigemk2 73fa36a33f Delete description about glide because glide.yaml is already deleted 2019-12-24 10:55:30 +09:00
Daisuke Maki 9a22355560 This be v0.5.4 2019-12-19 10:48:54 +09:00
lestrrat c4ad0a7ba4
Merge pull request #488 from peco/martinbrylski-master
fix minor go tip problem, and merge martinbrylski's fix
2019-12-19 10:06:37 +09:00
Daisuke Maki ff7b71a8b9 Update go version while we're at it 2019-12-19 09:46:07 +09:00
Daisuke Maki 927fba1ce1 redact pseudo-version for go 1.13~
https://tip.golang.org/doc/go1.13#version-validation
2019-12-19 09:38:30 +09:00
Martin Brylski 6972baf397 updated termbox. solves issue with tmux-256color TERM definition 2019-12-18 10:52:56 +01:00
lestrrat 82a3beea29
Merge pull request #486 from friederbluemle/fix-spelling
Fix GitHub spelling
2019-11-06 09:34:33 +09:00
Frieder Bluemle 2b7b521460
Fix GitHub spelling 2019-11-05 12:37:32 -08:00
lestrrat d289bb620a
Update README.md 2019-10-25 13:54:10 +09:00
lestrrat 742d55319e
Update FUNDING.yml 2019-10-25 13:49:51 +09:00
lestrrat a2f99967f2
Create FUNDING.yml 2019-10-25 13:48:51 +09:00
lestrrat 250632a9f4
Merge pull request #479 from tkainrad/patch-1
Add installation instructions for debian and ubuntu based distributions
2019-10-23 15:49:35 +09:00
lestrrat 4b06b9bd51
Merge branch 'master' into patch-1 2019-10-23 15:49:06 +09:00
lestrrat e6c113ee8f
Merge pull request #484 from peco/hisamura333-add-readme-comment
Hisamura333 add readme comment
2019-10-23 15:47:11 +09:00
Daisuke Maki c30492cb37 tweak 2019-10-23 15:41:36 +09:00
lestrrat 0a08098528
Merge branch 'master' into add-readme-comment 2019-10-23 15:40:54 +09:00
lestrrat 7753939751
Merge pull request #474 from lpmi-13/typofix
fix simple typos and formatting
2019-10-23 15:40:37 +09:00
lestrrat 9a4b60a41f
Merge branch 'master' into typofix 2019-10-23 15:34:34 +09:00
lestrrat 0f22620f6c
Merge pull request #482 from aviaviavi/master
APT and Scarf installation instructions
2019-09-21 16:44:30 +09:00
aviaviavi 8b90384f70 APT and Scarf installation 2019-09-20 13:34:23 -07:00
Thomas Kainrad 69a009ac0f
Add installation instructions for debian and ubuntu based distributions
As peco is available in the default Debian/Ubuntu repositories, I suggest to include the respective installation instructions here, so people do not think they have to build manually.
2019-06-11 19:05:52 +02:00
lestrrat 967233e253
Merge pull request #477 from watsonso/patch-1
action.go : fix typo
2019-03-15 09:54:38 +09:00
鈴木康文 6d1bce6bce action : fix typo 2019-03-15 09:44:04 +09:00
lestrrat 231cc2c1fe
Merge pull request #475 from peco/topic/mod
go module
2019-02-27 15:05:38 +09:00
Daisuke Maki 38fcb2ef1e set env var 2019-02-27 14:59:12 +09:00
Daisuke Maki e823aea441 set env var 2019-02-27 14:55:54 +09:00
Daisuke Maki 24e9b2c7b5 remove reference to glide 2019-02-27 14:53:31 +09:00
Daisuke Maki d8570b225e Use go.mod 2019-02-26 12:58:53 +09:00
lpmi-13 a243d95ff7 fix simple typos and formatting 2019-01-08 21:58:13 +00:00
Taiki HISAMURA 82edaa0f42 Add comment in Select Multiple Lines 2018-12-06 11:58:37 +09:00
lestrrat c93f2e424e
Merge pull request #465 from peco/topic/issue-462
Clarify building instructions
2018-10-25 23:28:48 +09:00
lestrrat 7e59e48412
Merge branch 'master' into topic/issue-462 2018-10-25 22:59:30 +09:00
Daisuke Maki eb778350a5 typo 2018-10-25 22:57:41 +09:00
Daisuke Maki 2255f2b54b apparently this fixes issues with pt! 2018-10-25 18:12:14 +09:00
lestrrat 6762b04883
Merge pull request #457 from taynaud/master
Add a few commands to use peco more like a pager
2018-10-25 16:56:57 +09:00
lestrrat 5027605f27
Merge branch 'master' into master 2018-10-25 16:50:15 +09:00
lestrrat dc15605aee
Merge pull request #463 from peco/topic/issue-456
Update dependency for pdebug
2018-10-25 16:46:46 +09:00
Daisuke Maki a53ba91609 enable ignore for "peco", but disable for "cmd/peco" 2018-10-25 16:14:37 +09:00
Daisuke Maki 00707e8727 Aaaarg, pt didn't find the last remaining reference to pdebug!
pt 2.2.0 used the line "peco" from .gitignore, and decided to
skip matching against the directory "cmd/peco". This made me
think that I changed all occurrances of github.com/lestrrat/go-pdebug
to github.com/lestrrat-go/pdebug, but apparently there was one
that was missing.
2018-10-25 15:59:57 +09:00
Daisuke Maki 053dde7342 see if totally renaming things will appease travis CI 2018-10-25 10:04:42 +09:00
Daisuke Maki a30b40c08f Remove old go versions 2018-10-25 10:00:27 +09:00
Daisuke Maki 7b62d0196d see if clearing the cache will appease travis CI 2018-10-25 09:59:54 +09:00
Daisuke Maki d02bb4122c Workaround glide being stubborn wrt lestrrat/go-pdebug -> lestrrat-go/pdebug 2018-10-25 09:50:16 +09:00
Daisuke Maki e6cbd7b122 Update README
fixes #462
2018-10-25 09:18:30 +09:00
lestrrat 0233a5f2a1
Merge pull request #461 from HaraldNordgren/go_versions
Bump Travis versions
2018-10-20 19:06:48 +09:00
Harald Nordgren 8c92244428 Bump Travis versions 2018-10-20 12:01:40 +02:00
Thomas Aynaud 601a7c7adc Add goToPreviousSelection 2018-06-22 15:17:31 +02:00
Thomas Aynaud 1cb1b137ac Add doGoToNextSelection action 2018-06-22 15:17:22 +02:00
Thomas Aynaud 90b5fa5d1c Add doToggleViewArround
Reset the query but do not move. This allows to select a line using peco
filters and view around, for instance for a log analysis
2018-06-22 15:16:25 +02:00
Thomas Aynaud 281db7f5e3 Go fmt 2018-06-18 13:50:20 +02:00
Daisuke Maki 9542898d4c Update changes 2018-03-31 05:27:26 +09:00
lestrrat 9385f4ec68
Merge pull request #451 from peco/topic/set-cursor
Call SetCursor when drawing the prompt
2018-03-31 05:23:53 +09:00
Daisuke Maki b60d067747 Call SetCursor when drawing the prompt 2018-03-30 10:07:42 +09:00
Daisuke Maki 814932819a version++; this be peco 0.5.3 2018-03-12 07:16:56 +09:00
lestrrat 48c72760de
Merge pull request #448 from peco/topic/go1.10
update travis to test with go1.10
2018-03-12 07:15:59 +09:00
Daisuke Maki 695ef530e9 test with go1.10 2018-03-12 07:06:44 +09:00
lestrrat 5c847fd6a9
Merge pull request #447 from sachaos/feature/update-termbox-go
Update termbox-go revision
2018-03-12 06:54:12 +09:00
Takumasa Sakao 06c6f59708 Update termbox-go revision
https://github.com/nsf/termbox-go/issues/185
2018-03-10 22:33:52 +09:00
lestrrat b80edba1de
Merge pull request #444 from bamchoh/patch-1
Correct a few links for CustomFilter/CustomMatcher
2018-02-26 20:14:42 +09:00
bamchoh 798152335b
Revise links for CustomFilter/CustomMatcher
1. Delete a deadlink of CustomMatcher
2. Correct a link to CustomFilter wiki page
2018-02-07 16:54:27 +09:00
Daisuke Maki c335455888 Update Changes 2017-12-27 10:37:33 +09:00
lestrrat 387272fa8e
Merge pull request #440 from peco/topic/print-query
Implement --print-query
2017-12-27 10:35:19 +09:00
lestrrat 7a70f43e5d
Merge branch 'master' into topic/print-query 2017-12-27 08:44:09 +09:00
lestrrat cc1aac9e9f
Merge pull request #441 from peco/topic/rename-GH439
Rename ScrollTop/ScrollBottom to ScrollFirstItem/ScrollLastItem
2017-12-27 08:43:45 +09:00
Daisuke Maki 972ecec7a2 Actually generate the stringer files
Previously I had manually created a stringer.go file from the output of
stringer command, because I had problems running it properly on my
environment for various reasons.

Taking a fresh look, this is no longer a problem, so I'm opting to
actually generate it manually.

Adds ScrollFirstItem and ScrollLastItem constants
2017-12-27 07:26:18 +09:00
Daisuke Maki a47c6ca159 Add description 2017-12-27 07:15:32 +09:00
Daisuke Maki d213c89cab Rename to Top -> FirstItem, Bottom -> LastItem 2017-12-27 07:14:11 +09:00
Daisuke Maki 4341024b1d Add README 2017-12-27 07:00:36 +09:00
Anders Eurenius 3058ce77f4 Add scroll to top and bottom 2017-12-26 15:28:00 +01:00
Daisuke Maki 2b0093b828 Test print-query 2017-12-26 21:58:25 +09:00
Daisuke Maki d25a14bf40 peco --print-query prints out the query as first line 2017-12-26 21:16:34 +09:00
Daisuke Maki b9a1bbec36 Merge branch 'nicolas-raoul-patch-1' 2017-12-26 07:22:50 +09:00
Daisuke Maki 3629a8cf99 Merge branch 'patch-1' of https://github.com/nicolas-raoul/peco into nicolas-raoul-patch-1 2017-12-26 07:22:24 +09:00
lestrrat 75f471e6f0
Merge pull request #437 from peco/topic/v0.5.2
version++; this be v0.5.2
2017-12-08 10:20:46 +09:00
Daisuke Maki bdb30f5fb7 version++; this be v0.5.2 2017-12-08 10:13:17 +09:00
lestrrat 47e1fb3c3b
Merge pull request #436 from peco/topic/glide-tweak
add --update-vendored flag
2017-12-08 10:02:22 +09:00
Daisuke Maki 6ee4fb7a0a add --update-vendored flag 2017-12-08 09:52:55 +09:00
lestrrat 3566702e4b
Merge pull request #435 from peco/topic/print-go-version
Print out the go version used to build the app
2017-12-08 09:52:08 +09:00
Daisuke Maki 149bf78df5 Print out the go version used to build the app 2017-12-08 09:42:55 +09:00
Nicolas Raoul a523adfc13
Fixed typos in filter name
Using `-initial-filter RegExp` would result in "specified filter was not found".
2017-11-20 16:12:20 +09:00
lestrrat 4498edd44f
Merge pull request #432 from Jagua/patch-1
Fix document to use new api "peco.RotateFilter"
2017-11-08 08:55:48 +09:00
Jagua bbc1646dfb
Fix document to use new api "peco.RotateFilter"
fix to use new api "peco.RotateFilter" instead of deprecated "peco.RotateMatcher" in Default Keymap section.
2017-11-07 23:53:55 +09:00
lestrrat 1403c233d2 Merge pull request #430 from dvrkps/patch-1
travis: update go versions
2017-10-04 06:57:29 +09:00
Davor Kapsa b47401a470 travis: update go versions 2017-09-27 12:57:35 +02:00
lestrrat 7b7749376f Merge pull request #426 from peco/topic/issue-432
Update glide version
2017-06-21 15:09:30 +09:00
Daisuke Maki 297ea2af36 oops 2017-06-21 15:03:37 +09:00
Daisuke Maki f1c639ef92 Update glide version 2017-06-21 15:03:37 +09:00
lestrrat 0329ae4a87 Merge pull request #418 from mash/faq-bracketed-paste-mode
Added FAQ entry about seeing bracket paste mode's escape sequences
2017-03-27 07:26:33 +09:00
Masakazu Ohtsuka e63b3a43a0 Added FAQ entry about seeing bracket paste mode's escape sequences 2017-03-27 00:10:10 +09:00
lestrrat 6157554008 Merge pull request #416 from zchee/makefile-glide
Makefile: remove unnecessary glide binary check
2017-03-22 12:19:38 +09:00
Koichi Shiraishi b35ae0fa92
Makefile: remove unnecessary glide binary check
Signed-off-by: Koichi Shiraishi <zchee.io@gmail.com>
2017-03-22 12:12:08 +09:00
Syohei YOSHIDA 81fb06c4f7 Merge pull request #415 from k0kubun/v051-changes-typo
Fix typo in Changes
2017-03-17 14:38:22 +09:00
Takashi Kokubun 1ab4a3576b Fix typo in Changes 2017-03-17 14:03:10 +09:00
lestrrat 492fad3a62 Merge pull request #414 from peco/topic/v0.5.1
version++; this be v0.5.1
2017-03-17 12:51:00 +09:00
Daisuke Maki 5b1bc48a30 version++; this be v0.5.1 2017-03-17 12:41:58 +09:00
lestrrat 96cf88f129 Merge pull request #412 from peco/topic/pr-409-redux
Only calculate the column size when necessary
2017-03-17 12:17:35 +09:00
Daisuke Maki e8bef7854e Update Changes 2017-03-17 12:12:40 +09:00
lestrrat 73487ee4f0 Merge pull request #413 from k0kubun/pr-412-consider-width
Reduce right scroll limit with a screen width
2017-03-17 12:11:06 +09:00
Takashi Kokubun 405a729ac0 Reduce right scroll limit with a screen width
Column: 0 can show `width` columns.
Thus if lines have `num` columns, we should limit scroll to Column: `num - width`.

Otherwise screen is blacked out when we unlimitedly do ScrollRight.
2017-03-17 10:43:21 +09:00
Daisuke Maki 98936082f0 Only calculate the column size when necessary
This trims down the calculation required by #409 to only those lines
that will appear on screen, and still controls how far right we can
scroll to
2017-03-17 09:42:33 +09:00
lestrrat fa9f5d593a Merge pull request #411 from peco/topic/issue-410
Make Resume a blocking operation
2017-03-17 00:03:08 +09:00
Daisuke Maki 3308108729 Baaah, fix glide check while we're at it 2017-03-16 23:58:54 +09:00
Daisuke Maki 94a0271706 Update changes 2017-03-16 23:44:00 +09:00
Daisuke Maki 8069f8eb2f Make Resume a blocking operation
This is required because we need to make sure we know termbox
has been initialized before attempting to make another drawing
operation.

Notably, without proper termbox initialization, screen.Size() returns a
bogus value, and we miss the line location where we left off to execute
the external command specified by --exec
2017-03-16 23:16:12 +09:00
lestrrat 91c93e2271 Merge pull request #408 from peco/topic/glide
Fix glide handling
2017-03-15 18:38:51 +09:00
Daisuke Maki ffc148bc9e fix glide path 2017-03-15 18:35:09 +09:00
Daisuke Maki ccdf75e101 Fix glide handling 2017-03-15 18:30:46 +09:00
lestrrat 138af2c22e Merge pull request #407 from peco/topic/build-arm
Build arm binaries, too
2017-03-15 18:21:17 +09:00
Daisuke Maki f92895fe26 Build arm binaries, too 2017-03-15 15:52:05 +09:00
lestrrat 873c8a3fbc Merge pull request #406 from peco/topic/tweaks
Topic/tweaks
2017-03-15 09:10:58 +09:00
Daisuke Maki aaa77cd3c8 remove unused structs and fields 2017-03-15 09:01:01 +09:00
Daisuke Maki 1e14eea871 make sure to use WithCancel (and be pedantic) 2017-03-15 09:01:01 +09:00
lestrrat 531849df65 Merge pull request #403 from peco/topic/env-exec
Add some environment variables
2017-03-15 09:00:14 +09:00
Daisuke Maki f3e9250486 Add some environment variables 2017-03-06 17:06:50 +09:00
lestrrat 439ea20938 Merge pull request #402 from peco/topic/remove-tty
Topic/remove tty
2017-03-06 17:02:55 +09:00
Daisuke Maki 924b1041c8 Update Changes 2017-03-06 09:50:58 +09:00
Daisuke Maki c37f3a3ae8 Remove tty, as previously noted 2017-03-06 09:42:26 +09:00
lestrrat 5a94918fad Merge pull request #400 from peco/topic/v0.5.0
version++; this be v0.5.0
2017-03-06 09:03:36 +09:00
Daisuke Maki 8e70ee0072 version++; this be v0.5.0 2017-03-06 08:56:25 +09:00
lestrrat 97cf18213c Merge pull request #399 from peco/topic/timing
Tweak timing for when to initialize the screen and other components
2017-03-04 15:06:50 +09:00
Daisuke Maki ab72a5521f Tweak timing for when to initialize the screen and other components
We should wait to initialize the screen until we have something
coming in from the source: otherwise multiple peograms chained via
pipes would compete for the same terminal resource, and things may
go southward.

But then there are components that rely on the screen being intialized,
so we let them wait as well.
2017-03-03 23:23:07 +09:00
lestrrat 766fdd73c0 Merge pull request #394 from peco/topic/command
[WIP] Remove ExecuteCommand, and add --exec
2017-03-03 20:28:31 +09:00
Daisuke Maki b3e1903818 Update changes 2017-03-03 16:22:09 +09:00
Daisuke Maki ab74eda9e5 Update Changes 2017-03-03 16:20:32 +09:00
Daisuke Maki 2abd71514d Update README 2017-03-03 16:18:33 +09:00
Daisuke Maki 2aafac08c1 remove debug statement 2017-03-03 16:14:00 +09:00
Daisuke Maki 75d0d54da6 Fix test 2017-03-03 15:52:42 +09:00
Daisuke Maki 20fe945b21 add shell 2017-03-03 15:48:28 +09:00
Daisuke Maki 8ca4f61967 Remove ExecuteCommand, hello --exec
--exec is a new, and far better tool to execute external commands
from peco results. It is invoked when `Finish` action is called,
and pipes selected line(s) to the specified command. If there were
saved lines, every saved line plus the currently selected line is
piped to the command. Otherwise, only the currently selected line
is piped.

When the command exits, execution goes back to peco, where you can
keep doing incremental searches, and again execute external commands.

When you are done, you are expected to exit out of peco using
the `Cancel` action
2017-03-03 15:40:09 +09:00
Daisuke Maki eb3dcf5306 fix README 2017-03-03 15:04:46 +09:00
Daisuke Maki e670560421 Change ExecuteCommand in a non-backcompatible way
Data is channeled through Stdin of the command, so we
don't do any silly interpolation
2017-03-03 15:03:11 +09:00
Daisuke Maki 482fa7fc6d Change default scan buffer size to 256kb
...and make it configurable
2017-03-03 14:21:38 +09:00
Daisuke Maki a78051dcbe Properly suspend termbox before executing external command
This makes sure that termbox does not inadvertantly steal our
commands while executing external commands, and also makes sure
we explicitly reset the screen.

Also, make sure we use a temporary selection.
2017-03-03 13:53:41 +09:00
lestrrat ecd2b35aff Merge pull request #392 from peco/topic/v0.4.9
version++; this be v0.4.9
2017-03-01 10:34:01 +09:00
Daisuke Maki 936910b202 version++; this be v0.4.9 2017-03-01 06:51:48 +09:00
Daisuke Maki 3022ba26ed Update Changes 2017-03-01 06:45:20 +09:00
lestrrat ee8ffa11bb Merge pull request #391 from peco/topic/issue-388
remove unnecessary check for selectionPrefix
2017-03-01 06:44:10 +09:00
Daisuke Maki bc7e5ea1f6 remove unnecessary check for selectionPrefix 2017-02-28 23:30:06 +09:00
lestrrat ed63779ea9 Merge pull request #390 from peco/topic/issue-389
Resume reading from source when we are done reading
2017-02-28 23:26:15 +09:00
Daisuke Maki 5a83582996 Update changes 2017-02-28 23:17:39 +09:00
Daisuke Maki c59137c2bd Resume reading from source when we are done reading
In order to serve the user filtered results as fast as possible,
we start reading from Source as soon as we have acquired a few lines
from STDIN or file.

So in case of #389, we started to process the source while we
are still reading from STDIN. But that means our buffer is not
quite ready with all of the input from STDIN.

Meanwhile, the filtering thinks it's done when we reach end of input
mark prematurely, and it never resumed reading. This is why we have
less lines than we should.

Now, in order to fix this, we need to process as much as we have, and
then come back to see if we have more. We do this by looping until
the setupDone channel is closed, and we have exhaused our buffer.

This fixes #389, but it also shows that the processing is a little
different when we run the filter for the first, and subsuquent
executions. Apparently the next execution takes a little bit longer
to *draw* (note: I have not checked if it's the filtering that's slow,
or that simply if the draw timings are off)
2017-02-28 23:09:36 +09:00
lestrrat ddec47bd0b Merge pull request #387 from peco/topic/v0.4.8
version++; this be v0.4.8
2017-02-27 12:07:25 +09:00
Daisuke Maki 8866cff58d version++; this be v0.4.8 2017-02-27 12:01:56 +09:00
lestrrat ce93a80fde Merge pull request #386 from peco/topic/go1.8
Use go 1.8
2017-02-27 11:52:11 +09:00
Daisuke Maki 7a558401de Use go 1.8 2017-02-27 11:46:25 +09:00
lestrrat e584efd745 Merge pull request #385 from peco/topic/issue-384
Implement --selection-prefix
2017-02-27 11:44:59 +09:00
Daisuke Maki a03e36f2ba s/select-prefix/selection-prefix/g 2017-02-27 11:41:34 +09:00
Daisuke Maki 2a903429e4 s/SelectPrefix/SelectionPrefix/g 2017-02-27 11:38:01 +09:00
Daisuke Maki b94d764f51 goimports 2017-02-26 18:13:43 +09:00
Daisuke Maki 7b9a3d1e54 Update Changes 2017-02-26 16:30:01 +09:00
Daisuke Maki 6cef077bc5 Add tests and some docs 2017-02-26 16:27:25 +09:00
Daisuke Maki ff10112452 PoC implementation for #384
* missing docs
* missing tests
2017-02-26 08:27:11 +09:00
lestrrat 6654b913c4 Update README.md 2017-02-23 16:11:16 +09:00
lestrrat 9673e19128 Merge pull request #383 from Joshua-Snapp/add_readme_toc
Add table of contents to README
2017-02-21 05:57:31 +09:00
Joshua Snapp 41ccdfe932 Add table of contents to the README.md file.
This readme is long and comprehensive. Let's help people know what it
encompasses and provide links to get to the section they're interested
in.
2017-02-20 13:10:28 -05:00
Joshua Snapp 870e6c5767 Changed to standard markdown notation for all level 1 headers.
I figured why not be consistent through the document. Also, it'll make it
easier to use the vim-markdown-toc plugin to generate the table of
contents.

Source for GitHub markdown:
https://guides.github.com/features/mastering-markdown/
2017-02-20 13:01:01 -05:00
lestrrat b63b588464 Merge pull request #382 from peco/topic/on-cancel
Topic/on cancel
2017-02-10 07:19:09 +09:00
Daisuke Maki eab12e589b Update Changes 2017-02-10 07:15:37 +09:00
Daisuke Maki 98a6321aab Update README 2017-02-10 07:13:51 +09:00
Daisuke Maki c7f40e4346 surpress error message when user canceled 2017-02-10 07:06:34 +09:00
Daisuke Maki 6216146848 Actually implement it 2017-02-09 20:17:38 +09:00
Daisuke Maki c5a069a15d Add new option to the command line 2017-02-09 19:10:25 +09:00
Daisuke Maki 6d5a2065a2 spelling 2017-01-09 19:07:34 +09:00
lestrrat 825adb2768 Merge pull request #380 from peco/topic/travis
Topic/travis
2016-12-21 13:55:21 +09:00
Daisuke Maki 1279628768 fix tests 2016-12-21 13:38:16 +09:00
Daisuke Maki 8d83b77447 use proper path 2016-12-21 13:36:36 +09:00
Daisuke Maki a379d18f4a cache directories 2016-12-21 13:32:26 +09:00
lestrrat e30265c15a Merge pull request #379 from peco/topic/v0.4.7
version++; this be v0.4.7
2016-12-17 07:35:42 +09:00
Daisuke Maki 2ac754b1fc version++; this be v0.4.7 2016-12-17 07:29:28 +09:00
lestrrat 003fe3127f Merge pull request #378 from peco/topic/issue-376
Fix --select-1
2016-12-17 07:25:09 +09:00
Daisuke Maki 9f1366c934 early return 2016-12-17 07:17:41 +09:00
Daisuke Maki d8c7102efd fix the test that should have caught the problem 2016-12-17 07:13:09 +09:00
Daisuke Maki 08582ac4a8 fix #376 2016-12-17 06:48:16 +09:00
lestrrat 6ec0a8d6b5 Merge pull request #377 from peco/topic/deadcode
Topic/deadcode
2016-12-17 06:47:30 +09:00
Daisuke Maki aa928308ca wrong type 2016-12-15 09:48:28 +09:00
Daisuke Maki eccb83e040 remove dead code detected by deadcode utility 2016-12-15 09:45:56 +09:00
lestrrat 0ed0d2b943 Merge pull request #374 from peco/topic/glide-up
ran glide up to update deps
2016-12-14 18:42:28 +09:00
Daisuke Maki 521c88abd6 ran glide up to update deps 2016-12-14 18:30:10 +09:00
lestrrat e44bb17f75 Merge pull request #373 from peco/topic/remove-append
Remove unused Append()
2016-12-14 18:27:41 +09:00
Daisuke Maki b500e2f628 Remove unused Append() 2016-12-14 18:23:59 +09:00
lestrrat bac1323fe0 Merge pull request #372 from peco/topic/tweaks
Topic/tweaks
2016-12-14 17:38:07 +09:00
Daisuke Maki 949166a67c bail out of the correct loop 2016-12-14 17:05:11 +09:00
Daisuke Maki 47be5c7c85 Integrate ExternalCommand into the new format 2016-12-14 14:59:38 +09:00
Daisuke Maki 80c7cde206 what mattn wishes is my command 2016-12-14 12:19:28 +09:00
Daisuke Maki 945cdf0b08 remove golang.org/x/net/context 2016-12-14 11:07:13 +09:00
Daisuke Maki a98498e3e8 oops, remove reference to golang.org/x/net/context 2016-12-14 11:06:44 +09:00
Daisuke Maki 61b5d22fb8 Move stuff around for better code organization 2016-12-14 11:00:22 +09:00
Daisuke Maki b1ea3c36b1 separate interface 2016-12-14 08:54:59 +09:00
Daisuke Maki c7eff12a67 oops, I shipped v0.4.6 w/ wrong issue ID
... but I don't think this merits a redo of the release. will
be updated for next version
2016-12-14 07:05:35 +09:00
lestrrat fd87529bec Merge pull request #371 from peco/topic/v0.4.6
version++; this be v0.4.6
2016-12-14 06:56:23 +09:00
Daisuke Maki 9fb60e6ac3 version++; this be v0.4.6 2016-12-14 06:50:45 +09:00
lestrrat cb95badd13 Merge pull request #370 from peco/topic/pr-369
Cleanup for PR 369
2016-12-14 06:35:14 +09:00
Daisuke Maki 58b60cd7e3 tweak comments 2016-12-13 10:13:10 +09:00
Daisuke Maki 45398301ea Apply mattn's review 2016-12-13 10:06:12 +09:00
Daisuke Maki 32a6786021 Update comment 2016-12-13 07:33:57 +09:00
Daisuke Maki d659c93bdc Update changes 2016-12-13 07:24:50 +09:00
Daisuke Maki 668a498ed8 slight refactor 2016-12-13 07:21:51 +09:00
Daisuke Maki 0ea80d7d90 remove unused field and params 2016-12-13 07:15:46 +09:00
Daisuke Maki 3476752a12 remove unused fields 2016-12-13 07:13:10 +09:00
Daisuke Maki 2dbfd78205 Rework #369
* --fuzzy-filter seems unnecessary
* regexp.Regexp is overkill. Implement in terms of Index*()
2016-12-12 17:12:07 +09:00
Eric F bfb3ae2fc1 Fix english in README fuzzy filter description. 2016-12-11 14:00:44 +11:00
Eric F 99f30cad6c Add more details to README 2016-12-11 13:56:50 +11:00
Eric F c00f0d2975 Implement #242: Add case sensitivity test cases 2016-12-08 23:35:09 +11:00
Eric F 1fb6e3b1b7 Implement #242: Use switch case
Replace if statement with switch when validating the FuzzyFilter option.
2016-12-08 23:22:01 +11:00
Eric F 21179c2c38 Implement #242: Made the fuzzy filter configurable 2016-12-08 17:20:55 +11:00
Eric F 4c4b4535ed Implement #242: fuzzy finder filter
- The fuzzy finder is implemented as a new filter, which
re-uses the RegexpFilter.
- A new queryTransformer field has been added to the RegexpFilter
struct to allow changing the user query before applying the filter.
- The new queryTransformer is used for the fuzzy filter to change
the user query into a regexp.
- Also added a test in filter_test.go
2016-11-28 00:00:28 +11:00
Daisuke Maki f5dc16fe62 Update Changes 2016-11-14 20:40:27 +09:00
lestrrat 22837a443d Merge pull request #368 from peco/topic/issue-367
surpress resetting the buffer when the source is slow
2016-11-14 20:38:52 +09:00
Daisuke Maki a6e7a042c6 oops 2016-11-14 20:32:01 +09:00
Daisuke Maki e432146891 Test for previous fix 2016-11-14 20:27:53 +09:00
Daisuke Maki 296aceffbb surpress resetting the buffer when the source is slow
fixes #367
2016-11-14 16:57:27 +09:00
lestrrat fbeaa7d329 Merge pull request #366 from znz/fix-syntax-error
Fix syntax error when /bin/sh is dash
2016-11-01 08:04:07 +09:00
Kazuhiro NISHIYAMA 3acd28bf60 Fix syntax error when /bin/sh is dash
dash says `/bin/sh: 1: Syntax error: Bad fd number`
2016-10-31 22:15:54 +09:00
lestrrat c2f781cb40 Merge pull request #365 from peco/topic/v0.4.5
version++; this be v0.4.5
2016-10-30 18:48:33 +09:00
Daisuke Maki 12fed927a1 version++; this be v0.4.5 2016-10-30 18:40:00 +09:00
lestrrat 9f536a6f4a Merge pull request #364 from peco/topic/issue-363
seems like we failed to bail out of the for
2016-10-30 18:38:54 +09:00
Daisuke Maki 0a025f1b8b test --select-1 2016-10-30 18:31:53 +09:00
Daisuke Maki 837714dd75 Update changes 2016-10-30 18:22:30 +09:00
Daisuke Maki ef5ed5f8eb seems like we failed to bail out of the for
instead, we seem to be just bailing out of the select
2016-10-30 10:55:45 +09:00
lestrrat 470141f3f0 Merge pull request #362 from peco/readme-patch
Update README.md
2016-10-25 12:25:26 +09:00
lestrrat 11836ff1a8 Update README.md 2016-10-25 11:37:55 +09:00
lestrrat 3ee675edcf Merge pull request #361 from peco/topic/v0.4.4
version++; this be v0.4.4
2016-10-23 15:41:46 +09:00
Daisuke Maki 0d4fbc0187 version++; this be v0.4.4 2016-10-23 15:34:00 +09:00
lestrrat 97bb22b6e1 Merge pull request #360 from peco/topic/issue-359
[WIP] topic/issue-359
2016-10-23 15:31:58 +09:00
Daisuke Maki 8acd3b1250 Update changes 2016-10-23 15:25:47 +09:00
Daisuke Maki 72b05ececd See if this avoids the panic described in https://github.com/peco/peco/issues/359#issuecomment-255512262 2016-10-22 16:43:58 +09:00
Daisuke Maki 5ad53b9f30 We won't use go1.6 2016-10-22 10:51:59 +09:00
Daisuke Maki c8a43049f3 Use an interface for peco.Hub()
This allows us to plug a dummy hub.Hub instance, and avoid nasty
code for testing
2016-10-22 10:50:55 +09:00
Daisuke Maki 82a9d29697 Move the fail-safe notify sync.Once call to a defer, so we don't have to remember calling it 2016-10-22 10:34:05 +09:00
Daisuke Maki bf00034e15 Pass ctx in the test, too 2016-10-22 10:31:22 +09:00
Daisuke Maki e1f82e731a Use ctx when setting up the source 2016-10-22 10:28:03 +09:00
Daisuke Maki 138f34ba14 I heard rumors that upgrading termbox-go fixes the problem 2016-10-21 17:33:53 +09:00
lestrrat e95bcd4651 Merge pull request #358 from tSU-RooT/fix-01
Fix some lower lines are not drawn when moved from less lines page
2016-10-17 14:53:00 +09:00
tSU-RooT 43d4f27119 Shrink display cache 2016-10-17 14:23:08 +09:00
lestrrat e40c0acde1 Merge pull request #357 from peco/topic/issue-356
Send a draw request when the query is empty
2016-10-11 18:59:56 +09:00
Daisuke Maki 4a563d7f3a Send a draw request when the query is empty 2016-10-11 16:04:09 +09:00
lestrrat c29ba5a856 Merge pull request #355 from peco/topic/issue-354
Fix InvertSelection command
2016-10-06 07:48:07 +09:00
Syohei YOSHIDA 72746ab553 Fix InvertSelection command 2016-10-06 01:29:04 +09:00
lestrrat 93068da71f Merge pull request #353 from peco/topic/v0.4.3
version++; this be v0.4.3
2016-10-05 15:01:25 +09:00
Daisuke Maki d4f7705129 version++; this be v0.4.3 2016-10-05 14:51:43 +09:00
Daisuke Maki 1aceada5ae Update Changes 2016-10-05 14:46:06 +09:00
lestrrat d6a5ab3dd4 Merge pull request #351 from peco/topic/issue-346
Make sure to re-draw the screen using DisableCache: true
2016-10-05 14:43:39 +09:00
Daisuke Maki f25f6e288c Do the same for refresh screen 2016-10-05 14:25:15 +09:00
Daisuke Maki 715d5b2a65 Make sure to re-draw the screen using DisableCache: true 2016-10-05 14:25:15 +09:00
lestrrat 1c6f10a020 Merge pull request #352 from peco/topic/skip-rcfile-on-test
Skip reading from rcfiles when running tests
2016-10-05 14:24:41 +09:00
Daisuke Maki 8b384d914e Skip reading from rcfiles when running tests 2016-10-05 14:19:00 +09:00
lestrrat 9ef6bb29a1 Merge pull request #350 from peco/alt-text
Add more useful alt text
2016-10-05 13:33:32 +09:00
lestrrat 948a92a4db Add more useful alt text
fixes #349
2016-10-05 13:28:29 +09:00
Daisuke Maki e3d8928df5 Update Changes 2016-10-01 08:10:07 +09:00
lestrrat f1a3549dfa Merge pull request #348 from peco/topic/issue-345
Add notion of "top-level" actions
2016-10-01 08:07:57 +09:00
Daisuke Maki 987e844ba2 Fix the test 2016-10-01 08:02:23 +09:00
Daisuke Maki 25d942b08b Add notion of "top-level" actions
This is used to by-pass locking the hub.Batch operation when
we have nested action calls that use batching internally.

fixes #345
2016-09-30 18:20:25 +09:00
lestrrat 08c244ef16 Merge pull request #344 from peco/topic/pr-343
Fix for InitialFilter from config files not taking effect
2016-09-25 04:46:52 -05:00
Daisuke Maki 8a162fc2b5 Add changes so we don't forget 2016-09-25 04:40:54 -05:00
Daisuke Maki b57305c0ea do the same for prompt 2016-09-25 04:40:32 -05:00
Daisuke Maki bf689d4255 Tweak things 2016-09-25 04:26:37 -05:00
rhysd 9bea557510 Fix that 'InitialFilter' configuration does not affect 2016-09-25 04:11:43 -05:00
lestrrat e859398f0f Merge pull request #342 from peco/topic/faq
Add FAQ entry
2016-09-05 13:12:59 +09:00
Daisuke Maki 559e3aa2fb Add FAQ entry 2016-09-05 13:05:14 +09:00
lestrrat 94c0dc650a Merge pull request #340 from peco/topic/opts
Test and fix options where necessary
2016-09-03 20:05:59 +09:00
Daisuke Maki 2c735b9f29 Test, and fix options
I should've done this a long time ago, but what's done is done.
There was one more omission between v0.3.x and v0.4.x, which is
buffer-size :/

At least I'm glad I got this out of the way.
2016-09-03 19:54:52 +09:00
Daisuke Maki 67eb241d83 --tty is gone
I looked through the git history, but it looks like this option was
never supported. It was just copied over from percol, when I started
this out. Juuuuust in case somebody might be specifying it (god knows)
I'm leaving it here with a warning
2016-09-03 18:49:50 +09:00
lestrrat da8d11008d Merge pull request #339 from peco/topic/msys2-cigwyn-faq
tweak FAQ
2016-09-03 18:34:15 +09:00
Daisuke Maki 9f1c8add77 tweak FAQ 2016-09-03 18:28:46 +09:00
lestrrat 396b821eb8 Merge pull request #338 from peco/prompt
Fix prompt setting issue
2016-09-03 18:23:58 +09:00
Daisuke Maki 812b65f92c Add a test for this 2016-09-03 18:17:21 +09:00
Syohei YOSHIDA a40af8432a Fix prompt setting issue 2016-09-03 02:57:46 +09:00
lestrrat 287f180b4b Merge pull request #333 from peco/topic/v0.4.2
version++; this be v0.4.2
2016-08-23 09:29:38 +09:00
Daisuke Maki 54bafeffa9 version++; this be v0.4.2 2016-08-23 06:40:28 +09:00
lestrrat 58cd0c9d81 Merge pull request #332 from peco/topic/issue-331
Topic/issue 331
2016-08-23 06:36:32 +09:00
Daisuke Maki 52dfaa6189 Add a very very rudimentary test 2016-08-22 18:04:48 +09:00
Daisuke Maki 821af182ab reinstate single key jump
the caching was more agressive than before, and we forgot to
initialize the structure correctly

TODO: Write tests to catch this

fixes #331
2016-08-22 17:56:32 +09:00
lestrrat 183a48b98d Merge pull request #330 from peco/topic/v0.4.1
version++; this be v0.4.1
2016-08-21 03:26:04 +09:00
Daisuke Maki 8d3daa3108 version++; this be v0.4.1 2016-08-21 03:21:11 +09:00
lestrrat e73acf3488 Merge pull request #329 from peco/missing-field
Clone idgen field too
2016-08-21 03:19:07 +09:00
Syohei YOSHIDA aa4065aa62 Clone idgen field too 2016-08-21 02:32:19 +09:00
lestrrat 0bad067fb9 Merge pull request #327 from peco/topic/v0.4.0
Topic/v0.4.0
2016-08-19 19:09:52 +09:00
Daisuke Maki 32f0db1708 version++; this be v0.4.0 2016-08-19 19:04:25 +09:00
Daisuke Maki 76d55c2147 Fix makefile 2016-08-19 19:04:25 +09:00
lestrrat 1a098f25a0 Merge pull request #326 from peco/topic/issue-320
Topic/issue 320
2016-08-18 19:42:54 +09:00
Daisuke Maki 7851980a11 Remove unprotected debug statement 2016-08-18 18:19:48 +09:00
Daisuke Maki 0623bdb817 Test with go1.7 2016-08-18 13:38:21 +09:00
Daisuke Maki 1340799367 pipe output to /dev/null 2016-08-18 13:26:14 +09:00
Daisuke Maki 87b4ad48a0 fix to work with new pipeline 2016-08-18 11:04:40 +09:00
Daisuke Maki e0d2a50377 Change pipeline interface.
Remove Producer/ProcNode, just pass channels around so we don't
have to worry about Accept needing to be sync'ed
2016-08-18 11:04:40 +09:00
Daisuke Maki 02c90c13d4 remove unncessary lock 2016-08-18 11:04:40 +09:00
lestrrat e8759162c8 Merge pull request #325 from peco/topic/channel-for-runes
Use channel to receive list of runes insteard of raw array access
2016-07-05 16:08:34 +09:00
Daisuke Maki 79556d5ad1 Use channel to receive list of runes insteard of raw array access 2016-07-05 16:03:13 +09:00
lestrrat 622671ef4d Merge pull request #324 from peco/topic/review-locking
Localize locking so that the one can Accept while doing other things
2016-07-03 19:39:03 +09:00
Daisuke Maki f578cfc850 Various tweaks to remove locking and interdependency
* Source does not depend on MemoryBuffer
* Remove a bunch of mutexes
* Change them to RWMutex where applicable
2016-07-02 16:19:17 +09:00
Daisuke Maki 36daafbb18 Change Runes() to return a copy of the underlying runes 2016-07-02 14:28:19 +09:00
Daisuke Maki 497573e89a The "filter" itself does not need locking, just the regexp compilation 2016-07-01 16:45:08 +09:00
Daisuke Maki 19ff36e276 Localize locking so that the one can Accept while doing other things 2016-07-01 15:38:38 +09:00
lestrrat 405da08b8f Merge pull request #323 from peco/topic/refactor-idgen
refactor idgen so that it exits gracefully
2016-07-01 15:30:31 +09:00
Daisuke Maki 7f70d73996 pass the id generator instead of state 2016-07-01 15:26:56 +09:00
Daisuke Maki b14cd4955b Make sure to initial buffer.start 2016-07-01 14:41:35 +09:00
Daisuke Maki 07a22aa466 refactor idgen so that it exits gracefully 2016-07-01 14:13:27 +09:00
lestrrat 69b057ec47 Merge pull request #322 from peco/topic/issue-321
Move handling collecting results out of the context
2016-07-01 14:09:59 +09:00
Daisuke Maki 9018aa45c6 Rename function 2016-07-01 13:24:20 +09:00
Daisuke Maki d8e5bcb349 Move handling collecting results out of the context
This avoids possibly running a race condition during doFinish,
as doFinish onyl notifies that we're exiting now. Collecting the
data is now handled in the main function, after all of the
sub-goroutines are done.

fixes #321 (but only for peco > 0.3.6)
2016-07-01 13:19:02 +09:00
lestrrat 1c04e4ec94 Merge pull request #319 from peco/topic/issue-318
Investigate drawing problems
2016-06-30 19:37:30 +09:00
Daisuke Maki bdcd74b2eb Remove the big lock around Source.Setup
This was causing deadlocks when requests to draw the screen were
received while setting up the source
2016-06-30 13:44:48 +09:00
Daisuke Maki 7499972766 streamline how Done() gets called 2016-06-30 13:43:59 +09:00
Daisuke Maki 94b85ea149 Run SetupSource after view/input/filter loops start
In order to start drawing while reading a big bufer, we need to
reverse the order of setting up these components

refs #318
2016-06-30 13:11:51 +09:00
lestrrat 41cf51ccf6 Merge pull request #317 from peco/topic/cleanup
cleanup some code
2016-06-30 13:05:19 +09:00
Daisuke Maki ae98d7825d cleanup some code 2016-06-30 12:59:57 +09:00
lestrrat 858c2e327e Merge pull request #315 from peco/topic/cleanup
Topic/cleanup
2016-06-23 18:20:11 +09:00
Daisuke Maki 7d9e87304a remove direct access to .current 2016-06-23 05:15:39 -04:00
Daisuke Maki aaebbf9d05 protect selection race 2016-06-23 05:14:09 -04:00
Daisuke Maki 3b98bf9bde protect regexpfilter, one more filter set 2016-06-23 05:07:42 -04:00
Daisuke Maki 47ea4f3067 protect done() 2016-06-23 05:02:30 -04:00
Daisuke Maki 215572ff58 use accessors 2016-06-23 05:00:48 -04:00
Daisuke Maki 9cbe49c72d protect peco from race 2016-06-23 04:56:09 -04:00
Daisuke Maki a6589820d0 protect caret race 2016-06-23 04:34:31 -04:00
Daisuke Maki 5d265d60e9 protect memorybuffer race 2016-06-23 04:32:09 -04:00
Daisuke Maki f4ea32e0e3 protect query race 2016-06-23 04:22:48 -04:00
Daisuke Maki ca7a273d98 protect filterset race 2016-06-23 04:14:44 -04:00
Daisuke Maki 464919f631 Remove unused field/method 2016-06-23 03:40:55 -04:00
lestrrat 877a1254ff Merge pull request #314 from peco/topic/cleanup
Topic/cleanup
2016-06-23 16:06:04 +09:00
Daisuke Maki eada702831 gofmt 2016-06-23 02:59:56 -04:00
Daisuke Maki e261219f4b one less reflect in the world 2016-06-23 02:55:12 -04:00
Daisuke Maki e32b419df9 Fix styles 2016-06-23 02:50:08 -04:00
Daisuke Maki 8d89338eb2 Fix Ignorable error detection/test 2016-06-23 02:24:29 -04:00
Daisuke Maki f09391251d comments + gofmt 2016-06-21 23:25:14 -04:00
lestrrat 0ec4029c40 Merge pull request #313 from peco/topic/filter-buffer-tweak
Tweak draw timing, and buffer
2016-06-22 05:45:47 +09:00
Daisuke Maki 68e614ed53 Make sure to reset the state source 2016-06-21 16:02:59 -04:00
Daisuke Maki b8d45ca2fd Draw while waiting 2016-06-21 15:41:24 -04:00
Daisuke Maki 7e18037f43 pass the buffers via channels so that we get to wait 2016-06-21 15:33:52 -04:00
Daisuke Maki c048ef4d03 tweak 2016-06-21 11:49:35 -04:00
Daisuke Maki b04024c411 Tweak draw timing, and buffer 2016-06-21 08:30:14 -04:00
Daisuke Maki a46071dc05 docs 2016-06-20 16:41:10 -04:00
Daisuke Maki f3ac1361c9 we don't expect pipelines to be re-used 2016-06-20 16:40:52 -04:00
Daisuke Maki 6b31c60d97 fix link 2016-06-20 16:05:37 -04:00
Daisuke Maki 432629f140 Update issue/contributing templates 2016-06-20 16:04:44 -04:00
lestrrat d45b3c61bb Merge pull request #311 from peco/topic/fix-custom-filter
Topic/fix custom filter
2016-06-20 21:08:57 +09:00
Daisuke Maki 614cf1691b properly use the default buffer threshold 2016-06-20 08:03:10 -04:00
Daisuke Maki b5490a0982 listen to the correct channel 2016-06-20 20:26:50 +09:00
Daisuke Maki 1bacd36dce Add filter from config 2016-06-20 20:13:12 +09:00
lestrrat 3e4e8df881 Merge pull request #304 from peco/topic/unspaghetti
[WIP] Trying to untangle some of the mess
2016-06-20 15:19:33 +09:00
Daisuke Maki e1e64b83dc reinstate --initial-filter 2016-06-18 17:40:33 +09:00
Daisuke Maki 8c400c1d81 move caret position when --query is specified 2016-06-18 17:40:33 +09:00
Daisuke Maki f62e125f1e separate Options and Peco 2016-06-18 17:40:33 +09:00
Daisuke Maki 187658c717 reinstante --query 2016-06-18 17:40:33 +09:00
Daisuke Maki a67fe2cda7 reinstate --select-1 2016-06-18 17:40:33 +09:00
Daisuke Maki ca5fc95ae5 apply the equivalent of e0f624c 2016-06-18 17:40:33 +09:00
Daisuke Maki b85690181f oops 2016-06-18 17:40:33 +09:00
Daisuke Maki bf161fb19a Make it possible to swap a buffer instead of os.Stdout 2016-06-18 17:40:33 +09:00
Daisuke Maki f036f025a0 Oops, use Output() 2016-06-18 17:40:33 +09:00
Daisuke Maki e4124d14de newline 2016-06-18 17:40:33 +09:00
Daisuke Maki 71e367bda0 Fix handling of single esc/alt 2016-06-18 17:40:33 +09:00
Daisuke Maki 1af475f0a3 remove unused methods 2016-06-18 17:40:33 +09:00
Daisuke Maki 215d2ad73d Apply initial index and layout from command line 2016-06-18 17:40:33 +09:00
Daisuke Maki 6cdf3346dd Properly set enableSep 2016-06-18 17:40:33 +09:00
Daisuke Maki 7e0d5599d8 Send one more Draw() call after filtering is done 2016-06-18 17:40:33 +09:00
Daisuke Maki 2a81e71247 import 2016-06-18 17:40:33 +09:00
Daisuke Maki 41083de5ca move type declarations 2016-06-18 17:40:33 +09:00
Daisuke Maki c010abbb4e Enable this test, too 2016-06-18 17:40:33 +09:00
Daisuke Maki dfedb3f9d1 Remove unused code 2016-06-18 17:40:33 +09:00
Daisuke Maki fcc6eb3dd9 Only close Close for non-stdin
fixes #308
2016-06-18 17:40:33 +09:00
Daisuke Maki 185340534c oops
b6d15338ec..385191cc6f (r67460505)
2016-06-18 17:40:33 +09:00
Daisuke Maki 01ccb5211a fix --version
fixes #307
2016-06-18 17:40:33 +09:00
Daisuke Maki 6511b31674 Locate rcfile
fixes #306
2016-06-18 17:40:33 +09:00
Daisuke Maki 1785e73cd5 Test that -h/--help does what's intended 2016-06-18 17:40:33 +09:00
Daisuke Maki cd1c759a91 Properly respect -h/--help
fixes #305
2016-06-18 17:40:33 +09:00
Daisuke Maki f7351adfe8 go tool vet 2016-06-18 17:40:33 +09:00
Daisuke Maki 8f93144a19 use interface to detect error type 2016-06-18 17:40:33 +09:00
Daisuke Maki c12cd97d29 Use pdebug instead of trace 2016-06-18 17:40:33 +09:00
Daisuke Maki 39eb717c6e Wrap errors 2016-06-18 17:40:33 +09:00
Daisuke Maki 39031fd1f1 gofmt stuff 2016-06-18 17:40:33 +09:00
Daisuke Maki bfaf856727 remove unreachable code 2016-06-18 17:40:33 +09:00
Daisuke Maki 22d942b220 remove unused code 2016-06-18 17:40:33 +09:00
Daisuke Maki 52d4e90c92 remove unused code 2016-06-18 17:40:33 +09:00
Daisuke Maki 10b2b57854 Fix locking 2016-06-18 17:40:33 +09:00
Daisuke Maki fb76393f3b Upon checking, this doesn't test anything 2016-06-18 17:40:33 +09:00
Daisuke Maki 78d8d6a35c fmt 2016-06-18 17:40:33 +09:00
Daisuke Maki 75b3bd33d8 Cleanup 2016-06-18 17:40:33 +09:00
Daisuke Maki 37e85afcd4 Remove unused code 2016-06-18 17:40:33 +09:00
Daisuke Maki 4169593599 Remove bogus code 2016-06-18 17:40:33 +09:00
Daisuke Maki d5381eeee4 Cleanup 2016-06-18 17:40:33 +09:00
Daisuke Maki ad19096c23 remove an unused error, and move stuff around 2016-06-18 17:40:33 +09:00
Daisuke Maki 3f2be2c877 Reader is no longer used 2016-06-18 17:40:33 +09:00
Daisuke Maki e3c36875fa eradicate fmt.Errorf 2016-06-18 17:40:33 +09:00
Daisuke Maki c24f6f0fe0 Remove unused code/types
note: ExternalCmdFilter should be re-checked
2016-06-18 17:40:33 +09:00
Daisuke Maki 4df1abb45b Set peco.Argv 2016-06-18 17:40:33 +09:00
Daisuke Maki 8669c17406 Fix errors usage, and supply default peco.Argv 2016-06-18 17:40:33 +09:00
Daisuke Maki 9ef1da7a71 Ah, finally realized that Input was looking at a stale version of peco.Keymap 2016-06-18 17:40:33 +09:00
Daisuke Maki 45343ab1cc Wait for key event to be delivered 2016-06-18 17:40:33 +09:00
Daisuke Maki 509685f77a Actually invoke SetCell from dummyScreen.Print 2016-06-18 17:40:33 +09:00
Daisuke Maki 3e9476676e Make screen a member of Peco struct, not a global 2016-06-18 17:40:33 +09:00
Daisuke Maki 098b88c90d more fixes 2016-06-18 17:40:33 +09:00
Daisuke Maki 7a239e4592 Fix tests, and things that change with it 2016-06-18 17:40:33 +09:00
Daisuke Maki 3c615080ba Cleanup some code 2016-06-18 17:40:33 +09:00
Daisuke Maki c7750ac50a get rid of debug prints 2016-06-18 17:40:33 +09:00
Daisuke Maki 5269d44dff Hmm, 500 ms ... ? what was I thinking 2016-06-18 17:40:33 +09:00
Daisuke Maki 83bffcdc5f use SetQuery 2016-06-18 17:40:33 +09:00
Daisuke Maki b11549010c properly initialize with a queryExecDelay
Otherwise we would be *always* running queries, which consumes all of
our resources.
2016-06-18 17:40:33 +09:00
Daisuke Maki a78e298fc6 Remove unused stuff 2016-06-18 17:40:33 +09:00
Daisuke Maki 497029fbcc use a pointer 2016-06-18 17:40:33 +09:00
Daisuke Maki 503f008d14 use Config{}/Init 2016-06-18 17:40:33 +09:00
Daisuke Maki 0a3adce117 Update deps 2016-06-18 17:40:33 +09:00
Daisuke Maki 77099ef426 collect results from selection buffer 2016-06-18 17:40:33 +09:00
Daisuke Maki c19b144883 remove debug print 2016-06-18 17:40:33 +09:00
Daisuke Maki d996d054ca starting to plugin the new pipeline stuff 2016-06-18 17:40:33 +09:00
Daisuke Maki a8d042d93c highlighting is back 2016-06-18 17:39:02 +09:00
Daisuke Maki 39d36c41b8 back to being able to draw the screen 2016-06-18 17:39:02 +09:00
Daisuke Maki 7064a04efa fix updater loop 2016-06-18 17:39:02 +09:00
Daisuke Maki a1781ee023 fix initial kick reading from source 2016-06-18 17:39:02 +09:00
Daisuke Maki b1c55013e9 look at args[1], not args[0] 2016-06-18 17:39:02 +09:00
Daisuke Maki c15f9d2964 Use runtime.Caller to identify the file 2016-06-18 17:39:02 +09:00
Daisuke Maki 417be36cfb More refactoring around current position/location state 2016-06-18 17:39:02 +09:00
Daisuke Maki 406cfe3bd7 Trying to get rid of "everything is a Ctx" 2016-06-18 17:39:02 +09:00
Daisuke Maki d4715dd9bf Organize the pipeline package 2016-06-18 17:39:02 +09:00
Daisuke Maki 7af0422f14 Add testify 2016-06-18 17:39:02 +09:00
Daisuke Maki 39dfde1bfe spewing not necessary for now 2016-06-18 17:39:02 +09:00
Daisuke Maki 4e47502086 Add deps 2016-06-18 17:39:02 +09:00
Daisuke Maki e8c8ed19e7 Trying to untangle some of the mess 2016-06-18 17:39:02 +09:00
lestrrat 6a093bb4bf Merge pull request #309 from dgryski/staticcheck
fix bug in byMatchStart.Less()
2016-06-18 07:43:17 +09:00
Damian Gryski e0f624c537 fix bug in byMatchStart.Less()
Foud by https://github.com/dominikh/go-staticcheck

filter.go:84:10: identical expressions on the left and right side of the '<' operator
2016-06-18 00:18:03 +02:00
lestrrat ff7739dd7d Merge pull request #303 from peco/topic/v0.3.6
version++; this be v0.3.6
2016-05-11 10:24:34 +09:00
Daisuke Maki 894ffac027 version++; this be v0.3.6 2016-05-11 10:20:46 +09:00
lestrrat 11367c118f Merge pull request #302 from peco/topic/makefile
Update Makefile to work with uploading
2016-05-11 10:11:14 +09:00
Daisuke Maki ee54a81ca5 Update Makefile to work with uploading 2016-05-11 10:06:55 +09:00
lestrrat 142e02b100 Merge pull request #301 from peco/topic/makefile
Topic/makefile
2016-05-11 06:28:58 +09:00
Daisuke Maki fa2c186f23 Add clean target 2016-05-10 21:07:53 +09:00
Daisuke Maki 67b86e3b21 Update Makefile to generate darwin files 2016-05-10 21:07:53 +09:00
lestrrat d337de61b2 Merge pull request #300 from peco/topic/update-readme
Update README
2016-05-10 18:11:32 +09:00
Daisuke Maki 6c131ecb48 Update README 2016-05-10 18:05:14 +09:00
lestrrat 37ae3d8e6d Merge pull request #296 from peco/topic/fix-makefile
fix makefile
2016-04-27 18:51:12 +09:00
Daisuke Maki 5762e4e3f5 Avoid instsalling glide if it already exists 2016-04-27 18:45:06 +09:00
Daisuke Maki 4537850b83 Acutally specify the target file, so a real executable is generated 2016-04-27 18:39:37 +09:00
lestrrat c55fa0b365 Merge pull request #295 from peco/topic/glide
Convert to using glide
2016-04-27 15:26:27 +09:00
Daisuke Maki 606f9245ef Remove unused files 2016-04-27 15:14:51 +09:00
Daisuke Maki 315dbb9c4b Use lastword instead of "4th word" 2016-04-27 15:11:14 +09:00
Daisuke Maki ffdb665aa4 Change destination directory 2016-04-27 15:05:56 +09:00
Daisuke Maki c645aaa14c Echo for debugging 2016-04-27 15:02:47 +09:00
Daisuke Maki 64a3cd2437 Use glide 2016-04-27 14:59:24 +09:00
lestrrat 1ccb24df9f Merge pull request #291 from peco/topic/issue-290
Use `<^>v` to represent `←↑→↓`
2016-03-23 12:06:35 +09:00
Daisuke Maki 05ad9ca805 Use <^>v to represent ←↑→↓ 2016-03-23 11:13:38 +09:00
Daisuke Maki 4fe5ea7345 Add CONTRIBUTING.md 2016-02-18 09:09:32 +09:00
Daisuke Maki 9724ce6d58 Run tests on go1.6 2016-02-18 09:09:32 +09:00
lestrrat f611475ce0 Merge pull request #286 from peco/windows-extension
Set '.exe' extension to executable file for Windows platform
2015-11-16 11:29:47 +09:00
Syohei YOSHIDA 36a5e2a4d3 Set '.exe' extension to executable file for Windows platform 2015-11-16 10:19:26 +09:00
lestrrat 8344e86ec9 Merge pull request #284 from peco/default-layout
fix default layout
2015-11-12 10:05:09 +09:00
Yasuhiro Matsumoto c4a47a02da This project is not waterfall driven development. 2015-11-11 19:50:42 +09:00
Yasuhiro Matsumoto afbf55afc5 fix test 2015-11-09 13:19:56 +09:00
Yasuhiro Matsumoto a9a999cc56 fix default layout
Close #283
2015-11-09 13:08:51 +09:00
lestrrat c56ae510ad Merge pull request #271 from peco/execute-command
[RFC] Execute command
2015-11-05 06:54:28 +09:00
Daisuke Maki 92937c3101 version++; this be v0.3.5 2015-10-30 09:01:07 +09:00
Daisuke Maki 84aa894be0 Update deps 2015-10-30 08:56:35 +09:00
Daisuke Maki c1bf3c346d Update README 2015-10-30 08:48:58 +09:00
Daisuke Maki 7533af0043 Update help message 2015-10-30 08:48:24 +09:00
lestrrat 1ec2ee3b89 Merge pull request #278 from peco/select1
implement --select-1
2015-10-29 17:43:24 +09:00
Yasuhiro Matsumoto ec118d1088 implement --select-1 2015-10-29 17:33:30 +09:00
lestrrat fb8238770c Merge pull request #276 from peco/gzip
lied about gzip
2015-10-06 06:36:56 +09:00
Yasuhiro Matsumoto ffaca4007d lied about gzip. #275 2015-10-06 01:02:38 +09:00
lestrrat 562340528c Merge pull request #274 from peco/fix-munus-current-line
Don't change line if there is no output
2015-10-03 18:08:57 +09:00
Syohei YOSHIDA c3eeb5c8af Don't change line if there is no output 2015-10-03 10:13:19 +09:00
Yasuhiro Matsumoto 1949759916 $FILE should be a file contained line string. $LINE should be the line it-self. 2015-10-01 22:54:52 +09:00
Yasuhiro Matsumoto 76c145c86c handle stdin/stdout/stderr if the command is not async. 2015-10-01 19:12:20 +09:00
Yasuhiro Matsumoto 9d1afd4bc1 update doc 2015-10-01 19:06:15 +09:00
Yasuhiro Matsumoto 4962dd6028 add peco.ExecuteCommand 2015-10-01 19:04:25 +09:00
Daisuke Maki 912e858a10 version++; this be v0.3.4 2015-09-29 14:21:36 +09:00
Daisuke Maki 97728cbc21 run gofmt
refs #269
2015-09-29 14:21:15 +09:00
lestrrat 023b519467 Merge pull request #266 from peco/topic/issue-261-take2
Implement hit-a-hint
2015-09-29 14:15:10 +09:00
Daisuke Maki 4bde862ac6 Update Changes 2015-09-29 13:38:36 +09:00
Daisuke Maki 5f38de0d5f Allow tweaking SingleKeyJump mode
- Setting SingleKeyJump.ShowPrefix to true will make the
  hint markers to be show ALWAYS
- Create a config struct for SingleKeyJump so we can
  extend it later
2015-09-29 13:27:02 +09:00
Daisuke Maki 5d8cc56786 Do not enable SingleKeyJump by default 2015-09-29 13:08:11 +09:00
Daisuke Maki 4d706f7374 Populate things at the right locations
see https://github.com/peco/peco/pull/266#issuecomment-143935402
2015-09-29 12:55:12 +09:00
Daisuke Maki 2f7edfb9db Apply https://github.com/peco/peco/pull/266#issuecomment-143934464 2015-09-29 12:39:52 +09:00
lestrrat b149089c0f Merge pull request #268 from peco/ignore-build
ignore .build
2015-09-29 12:30:04 +09:00
Daisuke Maki d07dfc94d4 Make sure to print the prefix even when there's a match 2015-09-29 12:29:24 +09:00
Yasuhiro Matsumoto b5e11fe7c6 ignore .build 2015-09-29 12:23:50 +09:00
Daisuke Maki 62fa53e053 Implement hit-a-hint
- "@" toggles "SingleKeyJump" mode
- list of hot keys are displayed to the left
- selecting the key allows you to jump to the line, and
  peco exits by selecting that line
2015-09-29 12:02:39 +09:00
lestrrat 897bcd39b0 Merge pull request #267 from peco/build-windows
windows hack for build/make.go
2015-09-29 12:01:48 +09:00
Yasuhiro Matsumoto 9dff126bec windows hack for build/make.go 2015-09-29 11:51:49 +09:00
lestrrat b528cd7bca Merge pull request #263 from peco/topic/the-great-go1.5-refactor
Let us begin the great refactor
2015-09-29 10:10:19 +09:00
Daisuke Maki 05bb4d771d wrong path 2015-09-29 09:59:36 +09:00
Daisuke Maki 6e867c78c6 change directories because we use internal packages 2015-09-29 09:55:01 +09:00
Daisuke Maki 513cbe77e9 change where we symlink 2015-09-29 09:52:57 +09:00
Daisuke Maki f727919361 use fmt.Printf instead of println 2015-09-29 09:49:40 +09:00
Daisuke Maki faed898c6c see if this works 2015-09-29 09:48:14 +09:00
Daisuke Maki 5c28501217 Set install step 2015-09-29 09:46:51 +09:00
Daisuke Maki b94e455e12 use the build script to test on travis 2015-09-29 09:45:42 +09:00
Daisuke Maki 3a7062d0ac only build post go1.5 2015-09-29 09:41:04 +09:00
Daisuke Maki f3d87a4b77 Redo the build script for go 1.5
- Also, run the build step in travis-ci to check for potential
  hooplas for other os/arches
2015-09-29 09:39:48 +09:00
Daisuke Maki 95c840dcb0 argh, forgot to fix package name 2015-09-28 22:27:55 +09:00
Daisuke Maki 41b3826136 move tests 2015-09-28 22:12:31 +09:00
Daisuke Maki d0eeecee2e refactor screen so we can test 2015-09-28 22:08:21 +09:00
Daisuke Maki 586c36a0e7 Let us being the great refactor
* Move internal stuff to internal
* Properly use conditional compilation to detect Windows
* Move type declarations to one location (except for a few places still)
2015-09-28 21:39:43 +09:00
lestrrat 6b3b872212 Merge pull request #265 from peco/topic/issue-264
Compare against linbuf, not cropped buffer
2015-09-28 21:37:45 +09:00
Daisuke Maki cbec2ff082 Compare against linbuf, not cropped buffer
fixes #264
2015-09-28 21:31:54 +09:00
lestrrat 350a9d69c6 Merge pull request #262 from peco/topic/issue-260
Topic/issue 260
2015-09-28 18:15:43 +09:00
Daisuke Maki 64e7e6f791 Fix another similar bug while we're at it
refs https://github.com/peco/peco/issues/260#issuecomment-143670990
2015-09-28 18:04:14 +09:00
Daisuke Maki f2b92f5758 Fix tests
refs #260
2015-09-28 17:00:00 +09:00
Daisuke Maki 5beaeb6328 Only do the page re-calculation if we have a running query
refs #260
2015-09-28 16:52:08 +09:00
Daisuke Maki fe1be27ea7 Change location when running query
Move to the closest location on the final page if our filtered buffer
is less than our location when the query was run
2015-09-28 14:36:50 +09:00
lestrrat d101e12bd4 Merge pull request #259 from peco/fix-fmt-specifier
Correct format specifiers
2015-09-15 13:31:10 +09:00
Syohei YOSHIDA 5996542fe4 Correct format specifiers 2015-09-15 13:24:05 +09:00
Syohei YOSHIDA 62c95562ef Merge pull request #258 from peco/update-travis
Add version 1.5
2015-09-02 19:06:00 +09:00
Syohei YOSHIDA f6226d1311 Add version 1.5 2015-09-02 18:43:18 +09:00
Syohei YOSHIDA ec38f98067 Merge pull request #257 from peco/fix-not-highlighting-issue
Fix not highlighting issue
2015-08-20 14:55:11 +09:00
Syohei YOSHIDA be5ba39a3f Fix not highlighting issue 2015-08-20 14:11:44 +09:00
Daisuke Maki 258501dc09 version++; this be v0.3.3 2015-07-11 21:02:26 +09:00
Daisuke Maki 1970c6faef remove useless temp variable 2015-07-11 21:00:45 +09:00
lestrrat 32ea8979a0 Merge pull request #254 from peco/topic/travis-container
set sudo: false in travis.yml
2015-07-11 20:55:15 +09:00
Daisuke Maki b988f27433 set sudo: false in travis.yml 2015-07-11 19:57:03 +09:00
lestrrat 6a4f594568 Merge pull request #253 from peco/topic/issue-252
Implement BackToInitialFilter
2015-07-11 18:36:37 +09:00
Daisuke Maki 03e0c99432 Implement BackToInitialFilter
fixes #252
It is by default NOT mapped to any key.
2015-07-11 16:58:01 +09:00
lestrrat f21eae1d7a Merge pull request #251 from peco/revert-250-update_readme
Revert "Update README.md"
2015-07-09 18:47:13 +09:00
lestrrat ce476ccb1b Revert "Update README.md" 2015-07-10 03:46:35 +09:00
lestrrat b192da6fad Merge pull request #250 from coodix/update_readme
Update README.md
2015-07-09 18:16:51 +09:00
iisakov d3f3aaa280 Update README.md 2015-07-09 12:03:01 +03:00
lestrrat 3178676272 Merge pull request #249 from Sixeight/feature/horizontal-scroll
Add ScrollLeft/ScrollRight action for horizontal scroll.
2015-05-20 10:37:43 +09:00
Tomohiro Nishimura 5c37d64b41 Fix ListArea's dirty is now encapsulated 2015-05-18 22:46:48 +09:00
Tomohiro Nishimura ea6724c57d Fix tab width calculation 2015-05-18 22:04:17 +09:00
Tomohiro Nishimura f30c38224c Remove wasted loop 2015-05-18 02:40:17 +09:00
Tomohiro Nishimura 8fb7f6eb16 Improve performance for horizontal scroll 2015-05-18 02:40:17 +09:00
Tomohiro Nishimura f2c5cf2af7 Fix comment 2015-05-18 02:40:17 +09:00
Tomohiro Nishimura bd87550ae5 Add documentation for ScrollLeft/Right 2015-05-18 02:40:17 +09:00
Tomohiro Nishimura b60d54fbc9 Fix horizontal scroll bug around line cache 2015-05-17 22:57:40 +09:00
Tomohiro Nishimura fa02d52f66 Add ScrollLeft and ScrollRight action for horizontal scroll 2015-05-17 22:46:38 +09:00
lestrrat 7816985f07 Merge pull request #247 from peco/better-bsd-support
Better support *BSD distributions
2015-05-08 18:26:44 +09:00
Syohei YOSHIDA 90e5498667 Better support *BSD distributions 2015-05-08 14:31:35 +09:00
Daisuke Maki 8afaf128af Apply mattn's fix from #246
fixes #246
2015-05-08 12:03:58 +09:00
lestrrat 62ed896220 Merge pull request #243 from peco/correct-misspellings
Correct misspellings in documents
2015-04-23 08:33:17 +09:00
Syohei YOSHIDA 1e9e1886a2 Correct misspellings in documents 2015-04-22 19:16:46 +09:00
Daisuke Maki d36c492e50 version++; this be v0.3.2 2015-03-24 18:02:01 +09:00
Daisuke Maki e9a5186526 Add Sixeight 2015-03-24 18:01:43 +09:00
lestrrat 003e2b9feb Merge pull request #241 from Sixeight/fix/send-query-twice
Fix send query twice when QueryExecutionDelay is 0
2015-03-24 17:58:26 +09:00
Tomohiro Nishimura 55a4f7f6c5 Fix send query twice when QueryExecutionDelay is 0 2015-03-24 17:49:23 +09:00
Daisuke Maki 2e77ad9d95 version++; this be v0.3.1 2015-03-24 09:13:58 +09:00
lestrrat c0fa721551 Merge pull request #240 from peco/fix-regexp-filter-issue
Fix Regexp filter issue
2015-03-24 09:11:33 +09:00
Syohei YOSHIDA 7f54c7fc0a Fix Regexp filter issue
- Register 'Regexp' filter
- Set initial values of RegexpFilter
2015-03-23 23:49:26 +09:00
Daisuke Maki 700b77b5ba version++; this be v0.3.0 2015-03-20 17:25:24 +09:00
Daisuke Maki 2e469bfeb4 Slight code refactoring 2015-03-20 17:24:46 +09:00
Daisuke Maki 9b43eb3fbb Remove looping element from CalculatePage 2015-03-19 14:16:14 +09:00
Daisuke Maki 5c593f8ebd Update README 2015-03-19 07:26:46 +09:00
Daisuke Maki 26c92bb01b Fix cancelation 2015-03-18 18:43:40 +09:00
lestrrat a3fb52de7c Merge pull request #237 from peco/topic/pipeline-rewrite
Topic/pipeline rewrite
2015-03-17 16:34:52 +09:00
Daisuke Maki cf07fbeb6b Fix doSelectVisible 2015-03-17 16:30:15 +09:00
Daisuke Maki 31fd0b89ea Update deps 2015-03-17 15:39:37 +09:00
Daisuke Maki 27547220e8 A few more screen redrawing glitches 2015-03-17 15:39:37 +09:00
Daisuke Maki b6be30704b Properly redraw upon InvertSelection 2015-03-17 15:39:37 +09:00
Daisuke Maki c24da22065 Apply runewidth to prefixLen 2015-03-17 15:39:37 +09:00
Daisuke Maki afabacac4e Lint 2015-03-17 15:39:37 +09:00
Daisuke Maki 7b7f373fd8 Update Changes 2015-03-17 15:39:37 +09:00
Daisuke Maki 3fe24360cd remove debug prints 2015-03-17 15:39:36 +09:00
Daisuke Maki 20269a268d Pretty much revert the previous change
Just use StringWidth when printing the query
2015-03-17 15:39:36 +09:00
Daisuke Maki 023fba7fab Properly move the cursor the number of RUNES 2015-03-17 15:39:36 +09:00
Daisuke Maki 89805d696e Properly color the entire query section 2015-03-17 15:39:36 +09:00
Daisuke Maki 16c08c6d56 Reserve one line at the bottom for windows
This allows the IM to be displayed there
2015-03-17 15:39:36 +09:00
Daisuke Maki 9d2a16c75b Make sure to protect access to execQueryTimer 2015-03-17 15:39:36 +09:00
Daisuke Maki 64fd5b4e84 Update README to reflect latest changes 2015-03-17 15:39:36 +09:00
Daisuke Maki e9da5a7cfe Replace $QUERY like how the document says
Also, if Args is omitted, a default value of []string{"$QUERY"} will be
used
2015-03-17 15:37:53 +09:00
Daisuke Maki 384598864f Avoid a certain death via infinite loop
Checking for QueryLen here would be stupid
2015-03-17 15:37:53 +09:00
Daisuke Maki c37cee0a99 Add a QueryExecutionDelay config
This allows you to set an arbitrary delay between query execution.
For example, when you type fast, by default queries will be run
against every character being typed in. By setting this to a higher
number, peco will batch these up into one query execution instruction.

The unit of this parameter is milliseconds.

Personally, I like 500
2015-03-17 15:37:53 +09:00
Daisuke Maki 9f53e6fc06 call Flush() after UserPrompt.Draw() was called
should fix
http://twitter.com/nobeans/status/576357248378609664
http://twitter.com/nobeans/status/576360941371072512
2015-03-17 15:37:53 +09:00
Daisuke Maki 64fc4d12fd some linting 2015-03-17 15:37:53 +09:00
Daisuke Maki 580fc48264 Tweaks to reader 2015-03-17 15:37:52 +09:00
Daisuke Maki 105e067172 Properly re-draw when query is empty 2015-03-17 15:37:52 +09:00
Daisuke Maki 18f6c847d9 Remove unused type 2015-03-17 15:37:52 +09:00
Daisuke Maki bcba884007 rename confusing variable 2015-03-17 15:37:52 +09:00
Daisuke Maki af4a4c8116 Remove more silly accessors 2015-03-17 15:37:52 +09:00
Daisuke Maki 08653e2afe Note and docs about BufferThreshold 2015-03-17 15:37:52 +09:00
Daisuke Maki 1ee6c37ed8 Remove some dead code 2015-03-17 15:37:52 +09:00
Daisuke Maki 7ee40fe3b0 Conditionally bake in debug code 2015-03-17 15:37:52 +09:00
Daisuke Maki 35397b95ec Mark lines that the cursor was on, and the new location as dirty 2015-03-17 15:37:52 +09:00
Daisuke Maki 960051a38c remove btree 2015-03-17 15:37:52 +09:00
Daisuke Maki ec82b5865d remove one more occurance of +1 in the line offset 2015-03-17 15:37:51 +09:00
Daisuke Maki db56e1859c Make things 0-base 2015-03-17 15:37:51 +09:00
Daisuke Maki ca7dc1b6a5 We don't need this globally unique ID stuff
This wasn't a distributed system, oops.
2015-03-17 15:37:51 +09:00
Daisuke Maki d7c6aad237 typo 2015-03-17 15:37:51 +09:00
Daisuke Maki 3b8334eb52 Me so stupid. Thanks to @nobeans 2015-03-17 15:37:51 +09:00
Daisuke Maki 85dea5ae68 tweak README 2015-03-17 15:37:51 +09:00
Daisuke Maki b848c6c0a2 Implement sticky selection
By default this feature is disabled, keeping backward compatibiity.
This should fix #230
2015-03-17 15:37:51 +09:00
Daisuke Maki 521df7fa01 Re-instate the missing order/dedupe matches bit 2015-03-17 15:37:51 +09:00
Daisuke Maki 100742b9d9 Properly kick DrawPrompt when the cursor position changes 2015-03-17 15:37:51 +09:00
Daisuke Maki 541886d965 tweak timing 2015-03-17 15:37:50 +09:00
Daisuke Maki 8e18b66713 Add a dirty flag to lines
this should solve issues for dangling caches
2015-03-17 15:37:50 +09:00
Daisuke Maki b40b377813 Reinstate enableSep for custom filter 2015-03-17 15:37:50 +09:00
Daisuke Maki 933cc2fcea Remove InputReader
Upon realizing that I had written this but haven't used it in actual
peco code other than testing, I tried it: It didn't work. Well, at least
it didn't work out of the box.

I could sense a loooooong debugging session ahead, and decided it's not
worth it at this point in time. we can always re-instate it, as we have
the history of it in git
2015-03-17 15:37:50 +09:00
Daisuke Maki a18faef700 remove unused parameter from SendDraw 2015-03-17 15:37:50 +09:00
Daisuke Maki 761b700aab add proper newline 2015-03-17 15:37:50 +09:00
Daisuke Maki 737bf68f7f Remove unused method Filter() 2015-03-17 15:37:50 +09:00
Daisuke Maki a22d76e331 Remove unused method 2015-03-17 15:37:50 +09:00
Daisuke Maki 4ec0f93f81 Rename struct for something more fitting 2015-03-17 15:37:50 +09:00
Daisuke Maki 6de39a305c Fix paging, remove unusued functions 2015-03-17 15:37:50 +09:00
Daisuke Maki 851a3405ce Remove more references to matcher 2015-03-17 15:37:50 +09:00
Daisuke Maki fe5ae10e94 Sayonara matchers 2015-03-17 15:37:49 +09:00
Daisuke Maki f323966c77 Implement custom matcher using the new streaming filter 2015-03-17 15:37:49 +09:00
Daisuke Maki 9178b578fa Re-enable hilighting 2015-03-17 15:37:49 +09:00
Daisuke Maki 8518eeb8db Add SmartCaseFilter 2015-03-17 15:37:49 +09:00
Daisuke Maki 85902a097a grrr, more fixing 2015-03-17 15:37:49 +09:00
Daisuke Maki 233acbc1c5 oops, forgot to use gorename and this is what I get 2015-03-17 15:37:49 +09:00
Daisuke Maki 94851bae92 Oh boy, what a mistake 2015-03-17 15:37:49 +09:00
Daisuke Maki 4157d44a0f Fire status msg clearing at the correct timing 2015-03-17 15:37:49 +09:00
Daisuke Maki d0dde77c50 This offset bit just seems wrong 2015-03-17 15:37:49 +09:00
Daisuke Maki b6faf2db28 rename and move 2015-03-17 15:37:49 +09:00
Daisuke Maki 104079e78c s/Name/String/, and gorename also did a gofmt 2015-03-17 15:37:48 +09:00
Daisuke Maki e8915c03cb More refactoring for testing 2015-03-17 15:37:48 +09:00
Daisuke Maki 6d04e2422b Rename 2015-03-17 15:37:48 +09:00
Daisuke Maki f12128e74c tweak message 2015-03-17 15:37:48 +09:00
Daisuke Maki 7ccde3a693 use a real channel 2015-03-17 15:37:48 +09:00
Daisuke Maki 6fc11b7568 Add small test 2015-03-17 15:37:48 +09:00
Daisuke Maki f8ec9e2042 Ugh, don't declare a new variable 2015-03-17 15:37:48 +09:00
Daisuke Maki 99c4e9b6d0 Move CLI stuff to cli.go (in anticipation of future testing work) 2015-03-17 15:37:48 +09:00
Daisuke Maki 4af9fc5b43 lestrrat's work in progress 2015-03-17 15:37:48 +09:00
lestrrat 63de38ba21 Merge pull request #235 from Sixeight/fix/typo-documentation
There is no F-1 key
2015-03-04 21:36:38 +09:00
Tomohiro Nishimura 6e39fa08ea Fix typo there is no F-1 key 2015-03-04 21:04:19 +09:00
lestrrat 4ef731dc96 Merge pull request #232 from Sixeight/patch/fix-documentation
Updata documentation around KeyMap and Available Actions
2015-03-04 20:57:10 +09:00
lestrrat ad9bba52e7 Merge pull request #233 from Sixeight/fix/function-key-mapping
Fix function key mapping
2015-03-04 20:56:18 +09:00
Tomohiro Nishimura f014430c23 Add F1 to available keys section at documentation 2015-03-03 15:59:04 +09:00
Tomohiro Nishimura 54ff2d01a1 Add test for converting with not termbox key character 2015-03-03 15:46:09 +09:00
Tomohiro Nishimura a0f13ce8ea Fix function key mapping 2015-03-03 15:25:40 +09:00
Tomohiro Nishimura b6e65611ce Add some test for keyseq/keys.go 2015-03-03 15:25:17 +09:00
Tomohiro Nishimura 0e36d641d7 Update documentation around available actions 2015-03-02 15:28:44 +09:00
Tomohiro Nishimura 2e0deabcc7 Update documentation around keymap 2015-03-02 15:09:42 +09:00
Tomohiro Nishimura c84d4b1c46 Fix typo 2015-03-01 05:33:45 +09:00
Daisuke Maki f0c506536a version++; this is v0.2.12 2015-02-27 06:55:20 +09:00
lestrrat 733f9f00ae Merge pull request #229 from peco/topic/fix-match-normalization
Fix match normalization
2015-02-26 20:05:11 +09:00
Daisuke Maki 504b234a6e Make the comparisons for mergeMatches stricter 2015-02-26 19:58:30 +09:00
Daisuke Maki 7ad8185fc8 Fix match normalization
This change fixes #228 by correctly deduping the matches generated
by RegexpMatcher. Before, we were not correctly handling the case
when the result of matched patterns would overlap with eachother.
2015-02-26 19:20:47 +09:00
lestrrat 9b452e2be2 Merge pull request #227 from peco/topic/fix-tab-drawing-count
Align computation about how many chars were written to terminal
2015-02-26 17:28:33 +09:00
Daisuke Maki c38e0a35e9 Align computation about how many chars were written to terminal
previously, we used runewidth.StringWidth() in the outer loop calling
printScreen(), but withing printScreen() we had code that expanded
a single character to up to 4 chars ("\t" == "    "), which cause our
drawing to go nanners after that point

This change make printScreen return the number of chars that were
written to the terminal, so we can use that value to advance the
drawing position in the calling routine

Fixes #226
2015-02-26 14:30:48 +09:00
Daisuke Maki 6f4d8559e2 Tweak README 2015-02-13 10:33:23 +09:00
lestrrat 951a089358 Merge pull request #225 from kozo2/patch-1
Update README.md
2015-02-13 10:30:34 +09:00
Kozo Nishida 167daae10d Update README.md
No need to tap now
2015-02-13 06:28:08 +09:00
lestrrat a9b73fab23 Merge pull request #221 from peco/bufread-custom-matcher
Read per lines using bufio.Reader
2015-01-21 15:27:20 +09:00
mattn dc3c8f062a Close pipe 2015-01-21 09:48:00 +09:00
mattn 1df0926a47 Read per lines using bufio.Reader 2015-01-21 09:44:49 +09:00
lestrrat 892f828463 Merge pull request #219 from jedipunkz/master
fixed a reverse problem of SelectNext and SelectPrevious keys.
2015-01-08 08:36:51 +09:00
jedipunkz df6a41568b fixed a reverse order problem of SelectNext and SelectPrevious key. 2015-01-07 17:30:27 +09:00
mattn 9a94d9b942 Merge pull request #218 from yuya-takeyama/typo_on_konami_command
Fix typo
2014-12-26 15:49:53 +09:00
Yuya Takeyama 1c14ebfca5 Fix typo 2014-12-26 06:18:10 +00:00
Daisuke Maki 8866e66c6e remove 1.1 2014-12-16 12:40:41 +09:00
Daisuke Maki b754398e5e Fix build script 2014-12-16 12:17:52 +09:00
Daisuke Maki 0ad82671a0 version++; this is v0.2.11 2014-12-16 07:41:05 +09:00
Daisuke Maki cbd5fd7487 This is a temporary fix to automate shipping via docker 2014-12-16 07:40:32 +09:00
lestrrat 7a9e00712f Merge pull request #214 from peco/topic/issue-212
Topic/issue 212
2014-12-11 13:04:37 +09:00
Daisuke Maki c44aa5794c Make sure we have the correct default values for layoutType
1. the peco command's --layout should have a default value
2. NewCtx(...) should not set the value of layoutType if the value is ""

fixes #212
2014-12-11 11:29:08 +09:00
Daisuke Maki c2e45a1637 Test for --layout being empty
This is the actual cause for #212
2014-12-11 11:28:08 +09:00
Daisuke Maki 82451d844c Add tests for #212
So far I can't reproduce what @syohex said on
https://github.com/peco/peco/issues/212#issuecomment-66556379 :(
2014-12-11 11:08:14 +09:00
lestrrat cc846e1654 Merge pull request #211 from yoshi-taka/typos
fixed typos
2014-11-29 22:46:20 +09:00
yoshi-taka 007f8bc8ab fixed typos 2014-11-29 22:02:36 +09:00
Daisuke Maki 9968a271ca slight addition to .gitignore 2014-11-16 17:42:12 +09:00
Daisuke Maki 9d61c6186c Fix off-by-one
see http://twitter.com/masutaka/status/532864814809616384
2014-11-13 21:10:50 +09:00
lestrrat 0a04cd20bc Merge pull request #210 from peco/toggle-query
Implement peco.ToggleQuery as Ctrl-t
2014-11-12 18:04:48 +09:00
Daisuke Maki b8063c31f7 Implement peco.ToggleQuery as Ctrl-t
refs #202

Currently the only thing it does is to turn the query off when
there is one, and back on. If there's no query specified, it doesn't
do anything.

Caveats on this incarnation:
* Turning a query "on" means the query is re-executed. If the filter
  being applied is time consuming, it might be annoying
* Selections are not remembered. If you have selected lines while
  the query is on, it won't be re-instated when you turn it off and
  then on again
2014-11-07 07:30:05 +09:00
Daisuke Maki 74e0e70a55 Comment so I remember 2014-11-07 07:21:38 +09:00
lestrrat 883a41f44d Merge pull request #209 from peco/tab
Owner drawing tab
2014-11-06 16:21:33 +09:00
mattn 4f770b2ec1 Owner drawing tab 2014-11-06 15:56:04 +09:00
Daisuke Maki 43b5513dbf Add docs 2014-11-05 15:16:20 +09:00
lestrrat cd7227ee45 Merge pull request #208 from peco/display-total-entries
Move maxPage into PageInfo, and display total entries in the prompt, too
2014-11-05 15:15:08 +09:00
Daisuke Maki 7a08381490 Move maxPage into PageInfo, and display total entries in the prompt, too 2014-11-05 15:10:49 +09:00
lestrrat e81da20b2a Merge pull request #207 from peco/selection-as-bit-vector
Change selection to use big.Int{} instead of []int
2014-11-05 14:58:29 +09:00
Daisuke Maki 6f5bc5388e do away with make([]struct{}, count) 2014-11-05 08:44:45 +09:00
Daisuke Maki 35cbc82ab0 Argh, stray code got into previous commits 2014-11-05 08:42:03 +09:00
Daisuke Maki 946dfce289 fix selection inversion operation 2014-11-04 07:03:03 +09:00
Daisuke Maki f9429fbd76 Change selection to use big.Int{} instead of []int
We flip bits instead of creating arrays now
2014-11-04 06:52:28 +09:00
lestrrat 8365365dc9 Merge pull request #206 from peco/invert-selection
Implement selection inversion
2014-11-03 18:53:25 +09:00
Daisuke Maki 43d64dda8e Add note to changes so we don't forget 2014-11-03 18:06:03 +09:00
Daisuke Maki dfb015fd4c Implement selction inversion
refs #204
2014-11-03 14:46:52 +09:00
lestrrat f69b174269 Merge pull request #205 from peco/rename-match-to-line
Rename Match to Line, NoMatch to RawLine and DidMatch to MatchedLine
2014-11-03 07:53:26 +09:00
Daisuke Maki b7bd37bdb5 Rename Match to Line, NoMatch to RawLine and DidMatch to MatchedLine
More refactoring so that it's clear what each component is doing
2014-11-03 07:45:49 +09:00
Daisuke Maki 0a6cae3f75 Remove unused field 2014-11-02 14:53:55 +09:00
lestrrat 46bb851352 Merge pull request #203 from peco/use-channel-instead-of-array
Simple change to avoid using an array
2014-11-01 21:16:20 +09:00
Daisuke Maki 5e28de0a64 Simple change to avoid using an array
Use a channel instead of an array -- no reason, really, but all those
seemingly duplicate []Match declarations were starting to annoy me
2014-11-01 21:05:31 +09:00
lestrrat ba5c88a62e Merge pull request #201 from mineo/patch-1
The configuration file is not in $XDG_CONFIG_HOME directly
2014-10-27 06:16:54 +09:00
Wieland Hoffmann e799d54f70 The configuration file is not in $XDG_CONFIG_HOME directly 2014-10-26 19:58:37 +01:00
lestrrat f657c2024c Merge pull request #200 from peco/fix-race
Fix a bunch of race conditions
2014-10-15 09:51:56 +09:00
Daisuke Maki 2963853cef Update changes to describe the fix 2014-10-14 18:16:33 +09:00
Daisuke Maki 4594f2d26d newMutex, sync.Locker 2014-10-14 16:44:33 +09:00
Daisuke Maki 38992a4cc0 more race fixes 2014-10-14 14:37:59 +09:00
Daisuke Maki 98add93d64 more stuff 2014-10-14 14:17:11 +09:00
Daisuke Maki d2c601512f protect termbox stuff 2014-10-14 13:14:01 +09:00
Daisuke Maki bd7ceaf6f5 more fixes against matcher 2014-10-08 21:11:10 +09:00
Daisuke Maki f6c4e30b28 fix test 2014-10-08 21:09:25 +09:00
Daisuke Maki 2f1f4cbb63 Fix a bunch of race conditions 2014-10-08 20:50:57 +09:00
Daisuke Maki 4952013023 oops, forgot to delete these 2014-09-24 20:26:07 +09:00
Daisuke Maki a6d046394c version++; this is v0.2.10 2014-09-24 19:11:35 +09:00
Daisuke Maki 4b21d6151f Update Changes 2014-09-24 16:31:21 +09:00
lestrrat 7e503f2ea2 Merge pull request #198 from peco/update-termbox
Update termbox-go
2014-09-18 12:55:51 +09:00
mattn f574db6a24 Update termbox-go 2014-09-18 12:32:59 +09:00
mattn b449f75073 Merge pull request #197 from peco/remove-workaround-for-stdout
Remove workaround for stdout.
2014-09-18 12:16:34 +09:00
mattn 0f3ae23697 Remove workaround for stdout. 2014-09-18 11:37:02 +09:00
Daisuke Maki 2d18439167 Update Changes before I forget 2014-09-13 16:13:18 +09:00
Daisuke Maki af5155175e Remove godep from README 2014-09-12 12:31:21 +09:00
Daisuke Maki 5e61b70367 Tweak 2014-09-12 12:31:07 +09:00
lestrrat dcf16a3ea9 Merge pull request #195 from peco/topic/releng
Topic/releng
2014-09-12 12:03:10 +09:00
Daisuke Maki 382028ee15 get pwd once 2014-09-12 11:58:49 +09:00
Daisuke Maki 65816da4f3 run linking for peco dir 2014-09-12 11:56:49 +09:00
Daisuke Maki cf50014649 separate the build steps so we can reuse 2014-09-12 11:52:44 +09:00
Daisuke Maki ae79e89bd7 oops 2014-09-12 11:44:01 +09:00
Daisuke Maki 9f5e082a20 Apply mattn's critiques 2014-09-12 11:41:19 +09:00
Daisuke Maki 3b08aa5e1d Ignore dynamically checkedout files 2014-09-12 09:46:18 +09:00
Daisuke Maki 07bc7b35f3 Oh yeah, we no longer need this 2014-09-12 09:45:27 +09:00
Daisuke Maki a9f9e4f962 grr 2014-09-12 09:36:15 +09:00
Daisuke Maki 813b47e1ef See if these patterns work 2014-09-12 09:34:52 +09:00
Daisuke Maki d1cda09ce7 Exclude some dirs 2014-09-12 09:22:46 +09:00
Daisuke Maki bf45b8e370 Switch how we build stuff for wercker 2014-09-12 09:09:28 +09:00
lestrrat 7dc6eac830 Merge pull request #193 from philsnow/select_all_and_finish
Add another motivating example of combined commands the readme
2014-09-10 14:00:27 +09:00
Philip Snowberger d17b81458d Add another motivating example of combined commands the readme 2014-09-09 21:49:30 -07:00
lestrrat c63dd712a8 Merge pull request #192 from peco/topic/fix_clear_status_race
Fix a race condition that existed between clear status and print status
2014-09-08 22:02:24 +09:00
Daisuke Maki 4c078d6505 Fix a race condition that existed between clear status and print status
For whatever reason I was thinking at the time, I had split the channels
to receive request to print to and clear the status message. This
basically means that, even if you meant to print X and then clear it
in 500 milliseconds, the clear request could actually arrive BEFORE
the print message.

This change basically puts all the status related request into one
channel, so that all requests can come sequentially
2014-09-08 14:41:06 +09:00
Daisuke Maki 98b19505e5 version++; this is v0.2.9 2014-09-08 10:26:53 +09:00
Daisuke Maki 2aba11373c Tweak AUTHORS 2014-09-08 10:23:26 +09:00
Daisuke Maki 068cd85dac oops, gofmt 2014-09-08 10:14:06 +09:00
Daisuke Maki ac19ae07e0 Tweaky tweaky, mainly just to scratch my itch 2014-09-08 10:12:28 +09:00
Daisuke Maki 959cef0d67 s/if/iff/ 2014-09-08 10:00:13 +09:00
Daisuke Maki 55a785606f Use constants instead of string literals 2014-09-08 09:59:42 +09:00
Daisuke Maki 7f1af3bd8f Tweak English 2014-09-08 09:58:37 +09:00
lestrrat c2a5176351 Merge pull request #191 from rhysd/add/matchers/smart-case
Added 'SmartCase' matcher to default matchers
2014-09-08 09:56:35 +09:00
lestrrat 1fc7dcb4ea Merge pull request #190 from peco/topic/refresh_screen
Tweaks to make it easier to work with big buffers/slow matchers
2014-09-07 18:31:46 +09:00
rhysd 0802c8e575 Describe more about SmartCase matcher 2014-09-07 16:13:19 +09:00
rhysd 17a4436494 Update README for SmartCase matcher 2014-09-07 02:01:49 +09:00
Daisuke Maki 31cb92f13d Change the keybinding to Ctrl-i to Ctrl-l 2014-09-06 20:35:23 +09:00
Daisuke Maki 4b7f66fcbc Draw prompt before running the query
This basically makes it easier for the users to see what exactly they
are waiting for. Previously we ran the filter, then re-drew the screen.
But this could take a while if the buffer is really big or the matcher
is slow, and therefore you would be stuck with a really old version of
your query in the screen for a while.

This change basically forces a redraw of the prompt alone before running
the query, which was something I meant to do for a long time, and now
possible because I split up the drawing in the layout object to
Prompt/Status/Lines sections
2014-09-06 18:25:14 +09:00
Daisuke Maki 95a5aebed9 Implement refresh screen via Ctrl-i
Is this the correct way to fix http://twitter.com/Linda_pp/status/508175851850973184?
2014-09-06 18:10:30 +09:00
rhysd ec262335af Reduce duplicates in SmartCaseMatcher implementation
by improving RegexpMatcher's API.  Now it has a function to get a flag
for regexp instead of a flag itself.
2014-09-06 02:00:12 +09:00
rhysd 2340bb9364 Implement SmartCase matcher anyway
It requires some refactorings.  There are many duplicates yet.
2014-09-06 02:00:00 +09:00
lestrrat 4b026b877b Merge pull request #189 from peco/improve-verification-for-custom-matcher
Improve verification for custom matcher
2014-09-05 12:50:18 +09:00
Syohei YOSHIDA a054fa439c Improve error message 2014-09-05 12:47:58 +09:00
Syohei YOSHIDA b281e725a7 Improve verification for custom matcher
This change also avoids run time error(index out of range).
2014-09-05 11:22:27 +09:00
Syohei YOSHIDA fd86537235 Merge pull request #187 from sona-tar/checkMatcher
Return Error, if Invalid Matcher
2014-09-03 10:15:54 +09:00
sona_tar 81c34e5a2a exec gofmt 2014-09-03 05:55:09 +09:00
sona_tar 57a546c8f2 Refacotr about checking bool type 2014-09-03 05:44:49 +09:00
sona_tar 0446c97734 Fix initial-matcher error message 2014-09-03 00:11:02 +09:00
sona_tar f380195c2e Return Error, if Invalid Matcher 2014-09-02 23:24:46 +09:00
Daisuke Maki a2fdcbab3e Note about ToggleRangeMode needing to be assigned explicitly
closes #186
2014-09-02 09:02:02 +09:00
lestrrat d34e4755cd Merge pull request #185 from peco/testing
Start adding tests for actions
2014-09-02 08:36:29 +09:00
Daisuke Maki f90771c6cd Add tests for DeleteForwardWord 2014-09-01 20:15:35 +09:00
Daisuke Maki 2aef061145 Add tests and make deletebackwardword more consistent 2014-09-01 19:32:58 +09:00
Daisuke Maki fc6469a46d Test DeleteBackwardChar 2014-09-01 18:28:41 +09:00
Daisuke Maki f03f9f0e62 More testing DeleteForwardChar 2014-09-01 18:24:09 +09:00
Daisuke Maki 9f20cf2d44 Start adding tests for actions 2014-09-01 18:12:32 +09:00
Daisuke Maki 541446a87c version++; this is v0.2.8 2014-09-01 15:10:10 +09:00
lestrrat 4dd46b4db1 Merge pull request #184 from peco/fix_delete_forward_word
Fix doDeleteForwardWord
2014-09-01 15:07:31 +09:00
Daisuke Maki 4b4fd8f62c Properly set caret position 2014-09-01 14:48:46 +09:00
Daisuke Maki d0877a78ef Fix doDeleteBackwardWord 2014-09-01 14:15:33 +09:00
Daisuke Maki fb7b2e5f6d Dynamically change the check function depending on rune at current position 2014-09-01 13:40:56 +09:00
Daisuke Maki 3f38934228 Fix doDeleteForwardWord 2014-09-01 12:53:44 +09:00
lestrrat 9054e2778e Merge pull request #183 from peco/fix_delete_forward_char
Fix delete forward char
2014-09-01 11:32:45 +09:00
Daisuke Maki f2c65aa650 fix DoDeleteForwardChar 2014-09-01 11:15:48 +09:00
Daisuke Maki e057cd404d Fix regression for caret drawing 2014-09-01 11:15:16 +09:00
Daisuke Maki 54497c83f8 version++; this is v0.2.7 2014-09-01 10:18:24 +09:00
lestrrat a5b032f149 Merge pull request #182 from peco/fix-deletebackwardchar
SetQuery updates caret position. So should use stored value.
2014-09-01 10:13:33 +09:00
mattn 686dd9447e SetQuery updates caret position. So should use stored value. 2014-09-01 09:13:23 +09:00
Syohei YOSHIDA 7f6809eade Merge pull request #181 from rhysd/patch-1
Fixed some default keymaps in README.
2014-08-31 23:46:26 +09:00
rhysd 297db59dc8 Fix some default keymaps 2014-08-31 23:38:06 +09:00
Daisuke Maki b1f78317b0 version++; this is v0.2.6 2014-08-31 07:24:53 +09:00
lestrrat 9fcafb70b6 Merge pull request #177 from peco/testing
merge testing branch
2014-08-31 07:18:16 +09:00
lestrrat 357132f648 Merge pull request #179 from sona-tar/fix/readme
Fix README.md
2014-08-30 23:31:57 +09:00
sona_tar bec150fe15 Update README.md 2014-08-30 21:54:54 +09:00
sona_tar cf14224f00 Fix README.md 2014-08-30 21:54:51 +09:00
Daisuke Maki 11dfa1b426 Refactor query and caretPos 2014-08-27 15:26:48 +09:00
Daisuke Maki 3b2b1fe07a Because I love @mattn 2014-08-27 14:17:13 +09:00
Daisuke Maki 3daa824e8e Refactor exitStatus 2014-08-27 14:13:40 +09:00
Daisuke Maki 3a16a2b25e Slightly refactor matcher rotation 2014-08-27 14:10:52 +09:00
Daisuke Maki 576c731b4f Add tests for reader 2014-08-27 13:56:53 +09:00
Daisuke Maki 05e065d026 Try very hard to keep data == nil when it's nil 2014-08-27 11:37:35 +09:00
Daisuke Maki ddec4d4901 oops, the other way around 2014-08-27 11:28:52 +09:00
Daisuke Maki 5f22bda676 Add tests for hub 2014-08-27 11:27:37 +09:00
Daisuke Maki 44783b4270 Not such good tests, but putting them here as placeholders for now 2014-08-26 12:52:26 +09:00
Daisuke Maki 4379a963c2 Add tests for NoMatch struct 2014-08-26 12:32:34 +09:00
Daisuke Maki 46f5714cf5 more tests 2014-08-22 15:30:53 +09:00
Daisuke Maki daa134e159 Tweak to make it possible to test status bar et al 2014-08-22 15:14:56 +09:00
Daisuke Maki b7605254a7 Apply conversion 2014-08-22 14:27:23 +09:00
Daisuke Maki 63e997f9a7 test printScreen 2014-08-22 14:25:24 +09:00
Daisuke Maki cd515a369f Start adding test for layout 2014-08-22 12:25:17 +09:00
Daisuke Maki a62bc068b2 Rip out termbox functions from main code so we can assigh a dummy later
in tests
2014-08-21 21:12:48 +09:00
mattn bb1ea393a1 Cosmetic change 2014-08-21 20:39:16 +09:00
Daisuke Maki 1e02a23bbb FINALLY do away with the redandunt specification of option description 2014-08-21 20:31:45 +09:00
lestrrat e11b2b8ab5 Merge pull request #175 from kei-s/fix-prompt-option
Fix to show prompt by command line option
2014-08-21 10:09:26 +09:00
kei-s c7c95f8cbd Remove unused field 2014-08-20 20:08:51 +09:00
kei-s 9dfe97008a Fix to show prompt by command line option 2014-08-20 20:05:05 +09:00
Daisuke Maki f78ebb76e9 lint code 2014-08-19 20:37:57 +09:00
Daisuke Maki 6362fbc763 version++; this is v0.2.5 2014-08-18 12:08:11 +09:00
Daisuke Maki 3c97d00109 Tweak README 2014-08-18 10:01:54 +09:00
Daisuke Maki d871329c0d note version number 2014-08-18 09:59:50 +09:00
lestrrat a651890426 Update README.md 2014-08-18 06:38:39 +09:00
lestrrat f6b6928262 Merge pull request #172 from peco/selectable-layout
Selectable layout
2014-08-18 06:34:25 +09:00
Daisuke Maki 3ccdc35c40 Remove hardcoded height - 2 and use linesPerPage() 2014-08-17 18:19:53 +09:00
Daisuke Maki 36549186c6 Add notes to Changes file 2014-08-17 18:16:30 +09:00
Daisuke Maki 23a4381156 Fix README
As proposed by https://github.com/peco/peco/pull/172#commitcomment-7414889
2014-08-17 18:06:52 +09:00
Daisuke Maki 27cfbfebde Use batch mode just in case 2014-08-17 18:02:09 +09:00
Daisuke Maki 91eefefa24 Fix ToggleSelectionAndSelectNext
This action was doing the right thing, but the changes in the layout
was looking at the wrong line number
2014-08-17 18:01:29 +09:00
Daisuke Maki 0aa077bfa2 Jesus, what am I, a CS 101 student? 2014-08-17 17:51:25 +09:00
Daisuke Maki 74f4c46ce8 Implement ScrollPageUp/ScrollPageDown
Based on comment at https://github.com/peco/peco/pull/172#issuecomment-52393224
2014-08-17 08:20:16 +09:00
lestrrat 908f724c01 Update README.md 2014-08-16 21:50:14 +09:00
Daisuke Maki f9ee817fd5 Allow setting layout from config file
...So you don't have to type it all the time!
2014-08-16 21:13:32 +09:00
Daisuke Maki 04b1427f93 Fix moving around with arrows 2014-08-16 18:47:22 +09:00
Daisuke Maki b189d0f695 Fix the logic to check if a line is selected 2014-08-16 18:27:23 +09:00
Daisuke Maki 7ed340e6ad Make layout selectable from command line
Note: bottom-up is still broken
2014-08-16 18:17:39 +09:00
Daisuke Maki 789ee803c3 Change everything to use anchors via AnchorSettings 2014-08-16 18:07:19 +09:00
Daisuke Maki ebdbbf937a Make UserPrompt calculate its veritical position dynamically 2014-08-16 17:00:49 +09:00
Daisuke Maki ca0d6f32a5 Make it possible to dynamically calculate y-offset for ListArea 2014-08-16 16:46:06 +09:00
Daisuke Maki 006228e0a5 Rip out list area from layout 2014-08-16 16:33:42 +09:00
Daisuke Maki 2b8a2aab5a rip out page calculation 2014-08-16 15:51:23 +09:00
Daisuke Maki 89beeeb3ba Fix a long-standing regression for PageInfo
Because we were this struct as a value, the value inside Ctx never got updated :/
2014-08-16 15:49:54 +09:00
Daisuke Maki 068c982864 Rip screen drawing out of View
View handles communication and control of elements in the Layout.
Layout does the actual drawing.
2014-08-16 07:33:31 +09:00
Daisuke Maki 5d7f648535 version++; this is v0.2.4 2014-08-13 15:09:20 +09:00
lestrrat 65614ad0f0 Update README.md 2014-08-13 15:06:03 +09:00
Daisuke Maki 5006a121ea Deprecate 'Matcher' in config file, use 'InitialMatcher'
This aligns the option name with the command line option name in the
peco binary.
2014-08-13 14:55:04 +09:00
lestrrat c8d8a56ca8 Merge pull request #170 from peco/initial-matcher
Initial matcher
2014-08-08 15:24:27 +09:00
mattn 67b59846f1 Add deprecated in doc 2014-08-08 14:43:12 +09:00
mattn 70f0c2f06a Initial matcher 2014-08-08 12:14:34 +09:00
Daisuke Maki b8e0c8f37d version++; this is v0.2.3 2014-07-28 16:00:15 +09:00
Daisuke Maki 1a87672c27 Note new changes 2014-07-25 20:34:29 +09:00
lestrrat 0569260286 Merge pull request #168 from youz/fix_bgcolor_mixing
fix bgcolor mixing
2014-07-25 20:29:29 +09:00
Yousuke Ushiki 3be9b10682 add test for color mixing 2014-07-25 12:52:44 +09:00
Daisuke Maki 3d6dea1750 remove mention of devel, it just doesn't work with github 2014-07-25 05:37:55 +09:00
Daisuke Maki 8b558d0f0d typo 2014-07-25 05:37:36 +09:00
Daisuke Maki d0bd1a1095 separate author/contributor
We have an increasing number of contributors, but the main authors
still remain as these three, so move that out to a separate list
2014-07-25 05:35:12 +09:00
lestrrat 41c7214ac8 Merge pull request #169 from peco/pr-167
Pr 167
2014-07-25 05:04:43 +09:00
Daisuke Maki d1545ba530 Note changes 2014-07-25 05:02:29 +09:00
youz cfa44a38b6 fix bgcolor mixing 2014-07-25 00:02:57 +09:00
Daisuke Maki 2271f0c75a rename 2014-07-24 21:03:01 +09:00
Daisuke Maki 374f85ebde remove all escape sequences
refs https://github.com/peco/peco/pull/167#issuecomment-49998149
2014-07-24 20:55:25 +09:00
Daisuke Maki b22cb7b8f6 Tweak so that we save the control sequence in Match.Buffer()
Using #167 as a basis, we save the original control sequence, and
only strip it when we display to peco. This also has the added
benefit that Match.Line() is used for matching, so we can effectively
match the line w/o the control sequence
2014-07-24 20:36:17 +09:00
Bawaaaaah 3d581cedce rename function EsapeAnsiChar to ansiStripsSequences add some comment 2014-07-24 11:10:37 +02:00
Bawaaaaah ad0fe09a7e add unitary test for ascii escape and pass the regex to a global var 2014-07-24 10:39:57 +02:00
lestrrat fb8106b6c3 Update README.md
Add Chocolatey Nuget
2014-07-24 05:10:42 +09:00
Bawaaaaah 06817b99f4 Fix bug with ansi escape sequence 2014-07-23 15:35:52 +02:00
Daisuke Maki 73473f0201 golint 2014-07-23 18:12:50 +09:00
Daisuke Maki 056e41ecf3 golint 2014-07-23 18:05:37 +09:00
Daisuke Maki 13f85d3884 version++; this is v0.2.2 2014-07-23 16:37:07 +09:00
Daisuke Maki c1ebadfdf0 Update Changes and README before I forget 2014-07-23 15:24:53 +09:00
Daisuke Maki 5e569b53c9 this better work 2014-07-23 15:22:23 +09:00
Daisuke Maki 07df513ee7 remove colon 2014-07-23 15:21:03 +09:00
Daisuke Maki 0d27258400 see if this works 2014-07-23 15:17:40 +09:00
Daisuke Maki acfe27f6ee update wercker.yml 2014-07-23 15:16:00 +09:00
lestrrat 33839db360 Merge pull request #165 from uobikiemukot/master
Support bold background attribute
2014-07-23 12:49:06 +09:00
uobikiemukot 4f4988134e add test case ("on_blink" and "reverse" attribute) 2014-07-22 21:49:38 +09:00
uobikiemukot b12be7ad2d update "Attributes" description (add comment for "on_bold" attribute) 2014-07-22 21:49:30 +09:00
uobikiemukot 13aefc81c7 support "on_bold" attribute (termbox bg: AttrBold) 2014-07-22 21:49:09 +09:00
Daisuke Maki afd06fbfd2 ran go vet, fixed issues 2014-07-18 20:38:38 +09:00
lestrrat dc9265e137 Update README.md 2014-07-17 11:43:02 +09:00
lestrrat f76039eb49 Update README.md
Add animated gif for windows, courtesey of @taichi (https://gist.github.com/taichi/26814518d8b00352693b)
2014-07-17 11:37:06 +09:00
lestrrat 3c311fb50f Update README.md 2014-07-16 22:43:30 +09:00
lestrrat be8c3ea012 Update README.md 2014-07-16 07:39:38 +09:00
lestrrat 7997d8e831 Update README.md 2014-07-16 07:39:15 +09:00
Daisuke Maki 61cfee607b Update deps 2014-07-15 17:23:41 +09:00
Daisuke Maki 22fc3a48e7 version++; this is v0.2.1 2014-07-15 15:03:00 +09:00
lestrrat b3ed4c1852 Merge pull request #163 from peco/show_keyseq
This change shows the current key sequence being handled
2014-07-15 14:59:10 +09:00
Daisuke Maki 529944a74a Truncate statues message from the left, not from the right 2014-07-15 13:17:49 +09:00
Daisuke Maki 065ca648c4 This change show current key sequence being handled 2014-07-15 09:23:03 +09:00
lestrrat 6135199f81 Merge pull request #162 from peco/issue-161
Document workaround for Shift+Tab
2014-07-14 15:27:17 +09:00
Daisuke Maki 2a42cdb02c Document workaround for Shift+Tab
Doesn't fix it, but at least shows a work around to close #161
2014-07-14 15:17:02 +09:00
Daisuke Maki 534a31e704 avoid building tty_posix.go on freebsd 2014-07-13 06:47:54 +09:00
Daisuke Maki e2a40b2df9 reword 2014-07-12 17:36:39 +09:00
lestrrat 1c8b6b9375 Merge pull request #157 from peco/cosmetic
Cosmetic
2014-07-12 17:31:58 +09:00
Daisuke Maki 319c23fd3b golint: docs 2014-07-12 16:55:44 +09:00
Daisuke Maki 73fd8fb6c0 golint: rename Newmatchstring to newMatchString 2014-07-12 16:45:32 +09:00
Daisuke Maki 0fb3f4749d golint: rename MatchString to matchString 2014-07-12 16:44:23 +09:00
Daisuke Maki 620e324c5e golint 2014-07-12 16:41:30 +09:00
Daisuke Maki d2c422250e golint 2014-07-12 16:32:09 +09:00
Daisuke Maki 8c1bcab562 more golint 2014-07-12 16:22:08 +09:00
Daisuke Maki ea41547c16 golint: docs 2014-07-12 16:11:42 +09:00
Daisuke Maki c2e61912f2 rename so that we have the same naming policy 2014-07-12 16:06:55 +09:00
lestrrat c263953477 Update README.md 2014-07-12 15:47:46 +09:00
Daisuke Maki fc1f9088bf log changes 2014-07-12 05:17:39 +09:00
lestrrat f1aff5ce79 Merge pull request #155 from peco/combined_action
Combined action
2014-07-12 05:16:01 +09:00
Daisuke Maki 2be415c70c Add docs 2014-07-12 05:09:52 +09:00
Daisuke Maki da74cec637 Convert paging to HubReq 2014-07-11 17:43:13 +09:00
Daisuke Maki b9e23bc87a Use the Batch() function to synchrously execute everything 2014-07-11 17:16:31 +09:00
Daisuke Maki c6e9d97f1f Change to use HubReq to communicate 2014-07-11 17:14:03 +09:00
Daisuke Maki 8efd69240f Refactor channels to a hub struct 2014-07-11 14:57:27 +09:00
Daisuke Maki 8fb86c037a Move to a named struct 2014-07-11 14:52:07 +09:00
Daisuke Maki d0bbb443e1 start refactoring ctx.go 2014-07-11 14:42:07 +09:00
Daisuke Maki 7d3310fce5 Create combined actions.
This change still doesn't work, as data modification happens at
different goroutines and some things just don't make sense then
2014-07-11 14:39:31 +09:00
lestrrat 32481b13ea Merge pull request #151 from peco/devel
Documentation fixes
2014-07-07 18:07:15 +09:00
lestrrat 6381287366 Update README.md
Add demo about range selection
2014-07-07 18:02:35 +09:00
Daisuke Maki 2aae0e11b3 his name is not in the git log, but it's pretty much his code, so he deserves to be here 2014-07-07 17:59:56 +09:00
Daisuke Maki ed84974b0d his name is not in the git log, but it's pretty much his code, so he deserves to be here 2014-07-07 14:26:07 +09:00
Daisuke Maki f7bcbba81f Chnage hacking docs 2014-07-07 12:52:40 +09:00
Daisuke Maki 9f1caccc82 oops, update AUTHORS 2014-07-07 12:16:57 +09:00
139 changed files with 23491 additions and 3024 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`

15
.github/CONTRIBUTING.md vendored Normal file
View file

@ -0,0 +1,15 @@
# CONTRIBUTING
:+1::tada: Thank you for taking the time to contribute! :tada::+1:
## Reporting Issues/Filing Pulling Requests
* Please follow the [Issue Template](./ISSUE_TEMPLATE.md)
* Please always include concrete example, code, etc.
* Please Do not assume that the peco contributors know about what you are talking about.
* Please always include enough information/links to terms/tools that you refer in your report.
* Please include graphic material to explain UI/UX issues, such as screen captures and animated gifs.
## Miscellaneous
* We accept issues/PRs in English or Japanese, but English is preferred.

12
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1,12 @@
# These are supported funding model platforms
github: ["lestrrat", "mattn", "syohex"]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

9
.github/ISSUE_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,9 @@
# What is the peco version, OS, architecture?
(Specify N/A if not applicable)
# Abstract
# Discussion
(Please be specific. Include code, pseudocode, screen captures if you can)

17
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,17 @@
version: 2
updates:
- package-ecosystem: gomod
directory: "/"
schedule:
interval: weekly
time: '10:00'
open-pull-requests-limit: 10
labels:
- "go"
- "dependencies"
- "dependabot"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: weekly

66
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,66 @@
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
go: [ '1.25' ]
name: Go ${{ matrix.go }} test
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Cache Go modules
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Setup Go
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@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3
with:
install-only: true
- name: Build
run: make build
generate:
runs-on: ubuntu-latest
name: Check generated code
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Cache Go modules
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Setup Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
check-latest: true
- name: Install stringer
run: go install golang.org/x/tools/cmd/stringer@v0.42.0
- name: Run go generate
run: go generate ./...
- name: Check for uncommitted changes
run: |
if ! git diff --exit-code; then
echo "::error::Generated code is out of date. Run 'go generate ./...' and commit the result."
exit 1
fi

37
.github/workflows/codeql.yml vendored Normal file
View file

@ -0,0 +1,37 @@
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
schedule:
- cron: '40 13 * * 5'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'go' ]
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Initialize CodeQL
uses: github/codeql-action/init@v4.37.9
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v4.37.9
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4.37.9
with:
category: "/language:${{matrix.language}}"

16
.github/workflows/lint.yml vendored Normal file
View file

@ -0,0 +1,16 @@
name: lint
on: [push]
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- 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@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0
with:
version: v2.8.0
- name: Run go vet
run: go vet ./...

25
.github/workflows/manpage.yml vendored Normal file
View file

@ -0,0 +1,25 @@
name: Update man page
on:
push:
branches: [master]
paths: ['README.md']
jobs:
update-manpage:
runs-on: ubuntu-latest
steps:
- 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@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
commit-message: "docs: regenerate man page from README"
title: "docs: update man page"
body: |
Auto-generated update to `contrib/man/peco.1` from changes to `README.md`.
branch: update-manpage
delete-branch: true

31
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,31 @@
name: Release
on:
push:
tags:
- "v*"
permissions:
contents: write
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3
with:
version: "~> v2"
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

16
.gitignore vendored
View file

@ -22,4 +22,18 @@ _testmain.go
*.exe
*.test
peco
/peco
build/src
c.out
out
.build
_internal_bin
vendor
github_token
releases
dist

98
.golangci.yml Normal file
View file

@ -0,0 +1,98 @@
version: "2"
linters:
default: all
disable:
- cyclop
- depguard
- dogsled
- dupl
- embeddedstructfieldcheck
- err113
- errorlint
- exhaustive
- exhaustruct
- forbidigo
- funcorder
- funlen
- gochecknoglobals
- gochecknoinits
- gocognit
- gocritic
- gocyclo
- godot
- godox
- gosec
- gosmopolitan
- inamedparam
- interfacebloat
- ireturn
- lll
- maintidx
- makezero
- mnd
- nakedret
- nestif
- nlreturn
- noinlineerr
- nonamedreturns
- paralleltest
- perfsprint
- recvcheck
- tagalign
- tagliatelle
- testifylint
- testpackage
- thelper
- varnamelen
- wrapcheck
- wsl
- wsl_v5
settings:
govet:
disable:
- shadow
- fieldalignment
enable-all: true
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
- linters:
- revive
path: /*.go
text: don't use an underscore in package name
- linters:
- revive
path: internal/util/
text: 'var-naming: avoid meaningless package names'
- linters:
- revive
path: /*_test.go
text: 'var-naming: '
- linters:
- errcheck
path: /main.go
- linters:
- errcheck
- errchkjson
- forcetypeassert
path: /*_test.go
paths:
- third_party$
- builtin$
issues:
max-issues-per-linter: 0
max-same-issues: 0
formatters:
enable:
- gofmt
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$

44
.goreleaser.yml Normal file
View file

@ -0,0 +1,44 @@
version: 2
builds:
- id: peco
main: ./cmd/peco
binary: peco
ldflags:
- -s -w -X github.com/peco/peco.version={{.Version}}
goos:
- linux
- darwin
- windows
goarch:
- amd64
- arm
- arm64
ignore:
- goos: darwin
goarch: arm
- goos: windows
goarch: arm
- goos: windows
goarch: arm64
archives:
- formats:
- tar.gz
format_overrides:
- goos: darwin
formats:
- zip
- goos: windows
formats:
- zip
files:
- README.md
- Changes
checksum:
name_template: "checksums.txt"
algorithm: sha256
changelog:
disable: true

View file

@ -1,6 +0,0 @@
language: go
go:
- 1.1
- 1.2
- 1.3
- tip

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 |

512
Changes
View file

@ -1,6 +1,514 @@
Changes
=======
v0.6.0 - 24 Feb 2026
[Breaking Changes]
* Migrated terminal backend from termbox-go to tcell (#567).
* Removed deprecated config fields `Matcher`, `InitialMatcher`, and
`CustomMatcher`. Use `InitialFilter` and `CustomFilter` instead.
Note: `CustomFilter` uses a different format than `CustomMatcher`:
Before: "CustomMatcher": { "MyFilter": ["/path/to/cmd", "$QUERY"] }
After: "CustomFilter": { "MyFilter": { "Cmd": "/path/to/cmd", "Args": ["$QUERY"] } }
* Removed deprecated CLI option `--initial-matcher`.
Use `--initial-filter` instead.
* The action `ViewArround` has been renamed to `ViewAround` (fix typo).
The old name still works but shows a deprecation notice. If you have
`"ViewArround"` in your config, please update it to `"ViewAround"`.
* Removed `Use256Color` config field. After the tcell migration,
256-color and truecolor support is automatic via terminfo detection.
Existing configs with this field will continue to load without error.
* Shift+Tab is now mapped to Tab internally. Use `M-[,Z` in your
keymap config if you need a distinct Shift+Tab binding.
* Default key bindings for `GoToNextSelection` and
`GoToPreviousSelection` have changed. `GoToNextSelection` was
previously unbound (it silently conflicted with `KillEndOfLine` on
Ctrl+K in v0.5.x) and is now bound to Alt+j. `GoToPreviousSelection`
was previously bound to Ctrl+J and is now bound to Alt+k.
* ANSI color support is now enabled by default (`--color=auto`).
ANSI escape sequences in input are now parsed and rendered with
colors preserved. To disable, use `--color=none` or set
`"Color": "none"` in your config file.
* Query terms starting with `-` are now interpreted as negative
(exclusion) filters. In v0.5.x, `foo -bar` searched for lines
matching both "foo" and literal "-bar"; in v0.6.0 it searches for
"foo" while excluding lines containing "bar". To match a literal
hyphen prefix, escape it with a backslash: `\-bar`.
[Features]
* Added YAML configuration file support. You can now use
`~/.config/peco/config.yaml` in addition to JSON (#576).
* Added negative filter terms. Prefix a search term with `-` to exclude
matching lines, e.g. `-unittests SSO` matches "SSO" but excludes
lines containing "unittests" (#434, #592).
* Added `peco.FreezeResults` / `peco.UnfreezeResults` actions.
Freezing locks the current result set so you can clear the query,
switch filters, and continue filtering against the frozen subset (#424, #596).
* Added `--height` option for inline display. Accepts a line count
(e.g. `--height 5`) or a percentage (e.g. `--height 50%`) and
renders peco inline instead of taking over the full screen
(#471, #598).
* Added `peco.ZoomIn` / `peco.ZoomOut` actions that expand visible
context around matched lines, showing surrounding lines (#289, #600).
* Added ANSI color rendering support via `--color=auto|none` CLI flag
and `"Color"` config field. Colored output from tools like
`git log --color` or `rg --color` is now displayed with colors
preserved. Default is `auto` (enabled) (#398, #602).
* Added `IRegexp` filter for case-insensitive regexp matching, so you
no longer need to manually type `(?i)` in regex queries. `IRegexp`
can also be used as a value for `--initial-filter` / `InitialFilter`
(#564).
* Added `--exit-0` option to immediately exit with non-zero status
when the input is empty, analogous to `--select-1` (#538, #582).
* Added Ctrl+Shift key modifier and CSI u / fixterms escape sequence
support for modern terminals (#529, #581).
* Added Page Up/Down and Home/End key support (#428, #584).
* Added a new `top-down-query-bottom` layout: top-down list with
query prompt at the bottom. Use `--layout top-down-query-bottom`
or `"Layout": "top-down-query-bottom"` in config (#445, #595).
* Added man page (`peco.1`) for offline documentation (#467, #587).
* Added support for ANSI color codes in the `--prompt` string (#294, #586).
* Added `--select-all` option to select all input lines and immediately
exit without displaying the selection view. When combined with
`--query`, only matching lines are selected.
* Added `SuppressStatusMsg` config field to suppress the status message
bar at the bottom of the screen.
* Added `FilterBufSize` config field to control the internal buffer
size (in lines) used during filtering. Default is 1000.
* Added `Prompt` and `Context` style targets. `Prompt` controls the
appearance of the query prompt prefix (e.g. `QUERY>`). `Context`
controls the appearance of context lines shown by ZoomIn (default:
bold).
* Added `#RRGGBB` truecolor support in style configuration. You can
now use hex color values like `"#ff6600"` for foreground and
`"on_#003366"` for background colors.
* Added `QueryCursor` style for explicit control over the query prompt
cursor appearance. When not configured, the cursor is derived from
the `Query` style by swapping fg/bg colors.
[Bugs/Fixes]
* Fixed `ToggleQuery` (C-t) not working correctly (#563).
* Fixed fuzzy sort producing incorrect order with >1000 lines of
input due to chunk-size boundary (#557, #589).
* Fixed matched highlight style bleeding to the right edge of the
terminal (#460, #583).
* Fixed screen not being properly redrawn when stderr output from
piped commands corrupts the display (#455, #588).
* Suppressed flickering "Running query..." status message (#511, #585).
* Fixed `--null` not working correctly with custom filters (#312, #577).
* Fixed `doGoToNextSelection` and `doGoToPreviousSelection` sharing
the same default key binding (#575, #579). `GoToNextSelection` is
now bound to Alt+j and `GoToPreviousSelection` to Alt+k. The
previous Ctrl+J and Ctrl+K bindings have been removed (Ctrl+K
remains bound to `KillEndOfLine`). If you relied on Ctrl+J or
Ctrl+K for selection navigation, update your keymap config.
* Fixed `GoToPreviousSelection` being registered with a malformed
internal name (`\tdoGoToPreviousSelection`) in v0.5.x. Users who
had `"peco.GoToPreviousSelection"` in their config keymap will find
that it now correctly binds to the action.
* Fixed `lastLine` tracking variable not being updated (#574, #578).
* Fixed tab rendering off-by-one error (#672).
* Restored mouse support lost during the tcell migration (#735).
* Fixed Ctrl-C not properly exiting peco due to meta character
handling (#715, #716).
* Fixed `--select-1` not triggering on interactive queries and not
respecting context for streaming sources (#726).
* Fixed filter cache not being invalidated on freeze/unfreeze,
causing stale results (#727).
* Fixed external filters (`CustomFilter`) incorrectly running in
parallel, which could produce garbled results. They now run
sequentially (#722).
* Removed redundant Esc key timer; Escape should now feel more
responsive (#728).
* Filter errors are now reported to the status bar instead of being
silently swallowed (#682).
* Fixed screen Init error being swallowed on suspend/resume (#681).
* Fixed I/O errors during stdin read being silently ignored (#680).
* Fixed ANSI SGR parser not handling malformed escape sequences
gracefully (#692).
* Fixed potential panic on negative index in FilteredBuffer (#770).
* Fixed errors from external filters being silently dropped
(#752, #766).
* Fixed various internal race conditions, resource leaks, and
potential deadlocks.
[Performance]
* Optimized filtering for large inputs with parallelized workers,
achieving 7-30x speedup for ~1M lines (#454, #590).
* Reduced overall memory consumption (#572).
* Reduced per-query allocation overhead and GC pauses: sync.Pool for
match structs, in-place merge, slice reuse in regexp/fuzzy filters,
closure elimination, stack-allocated ANSI parsing, and fast-path
ANSI stripping (#781-#789, #765).
v0.5.11 - 21 Mar 2023
[Features]
* Added new option for "FuzzyLongestSort" flag that can be specified in your config file
as
```
{
"FuzzyLongestSort": true,
...
}
```
allows you to sort matched lines with the following ordering precedence (#539):
1. longer substring
2. earlier (left positioned) substring, and
3. shorter line.
[Miscellaneous]
* Posted project status at https://github.com/peco/peco/discussions/540
v0.5.10 - 08 Jun 2021
* Previous release was botched. No code change except for version string,
and Makefile typo.
v0.5.9 - 08 Jun 2021
* Remove support for 386 arch
* Add go.sum
* Add darwin-arm
* Fix double cursor issue (#513)
v0.5.8 - 18 Jul 2020
Features:
* Add support for 256-colors in the configuration. You must first enable
this in the config file by specifying "Use256Colors": true
v0.5.7 - 10 Jan 2020
Bugs/Fixes:
* Code introduced in v0.5.5 introduced a bug where if the input is coming
from standard input and is possibly infinite, the user would see the
query being "stuck" after the first character is entered.
If peco was applied on a finite set of data, this did not manifest itself.
(#494 fixed by #495)
v0.5.6 - 07 Jan 2020
Miscellaneous:
* I completely messed up the release process. Retagging and uploading
binaries properly. No code changes except for the version number (#493)
v0.5.5 - 02 Jan 2020
Features:
* Properly exit out of pec if --select-1 and --query are both specified,
and the query produces exactly one result (#490)
v0.5.4 - 19 Dec 2019
Miscellaneous:
* Properly set cursor position (#450)
* Update termbox-go version (#487/#488)
* Update go version to go 1.13
v0.5.3 - 12 Mar 2018
Features:
* New --print-query option prints out the query upon successful exit
* New actions ScrollFirstItem and ScrollLastItem
Miscellaneous:
* Update termbox-go version (#447)
* Documentation fixes (#444)
* Build peco using go1.10
v0.5.2 - 08 Dec 2017
Backwards Incompatible Change:
* --version flag now prints out the Go version used to build the binary
Micellaneous:
* Various doc fixes
* Build peco using go1.9.2.
* There have apparently been problems running the stock peco binary in
MacOS High Sierra, with an error message like "failed MSpanList_Insert...",
which can averted by compiling the binary with a newer Go.
Reports suggest that peco v0.5.1 is not affected by this problem, but
we're making this release anyway just to make sure that we're building
on a new-ish Go, and that users have a way to see what Go version was
used to build their binaries
v0.5.1 - 17 Mar 2017
Bugs/Fixes
* When --exec is used, and you come back from the external command,
you lost your selected location in the peco view. #410
Backwards Incompatible Change:
* --tty has been removed. it was not being used anyways.
Miscellaneous
* When scrolling right, you could potentially keep on scrolling
infinitely. This has been addressed by #409, #412
* External commands specified in --exec now receive
PECO_FILENAME, PECO_LINE_COUNT, PECO_QUERY, and
PECO_MATCHED_LINE_COUNT as environment variables
* Removed unused structs
* Fixed glide related Makefile actions
v0.5.0 - 06 Mar 2017
Backwards Incompatible Change:
* ExecuteCommand has been removed.
Features:
* A new command line option `--exec` has been added. This allows you to
execute external commands via shell, and should be used as replacement
to `ExecuteCommand`
* A new configuration option `MaxScanBufferSize` has been added. Whereas
bufio.Scanner (which peco internally relies on) only accepts lines that
are < 64kb when reading the input, specifying this option in the config
allows you to change this limit.
The default MaxScanBuferSize is 256kb.
Bugs/Fixes
* When executing external commands, the screen and the capturing of user
input would interfere when getting back to peco.
v0.4.9 - 01 Mar 2017
Bugs/Fixes
* SavedSelection under `--selection-prefix` was not properly working
(#388)
* An outstanding bug from v0.4.0 where specifying `--query` for a large
enough input would lose results in the first query execution has been
fixed (#389)
v0.4.8 - 26 Feb 2017
Features:
* A new command line option `--on-cancel` has been added. This allows you
to determine if the user has canceled the query, which can be useful in
a pipeline of commands.
* `OnCancel` does the equivalent of `--on-cancel` in the config file.
* A new command line option `--selection-prefix` has been added. This
allows you to use a string prefix instead of highlighting currently
selected lines.
* `SelectionPrefix` does the equivalent of `--selection-prefix` in the
config file.
Miscellaneous
* Build using go 1.8
v0.4.7 - 17 Dec 2016
Bugs/Fixes
* A regression of #363 appeared again, because the test was not testing
for the correct behavior (#376)
Miscellaneous
* Internal house cleaning
v0.4.6 - 14 Dec 2016
Features:
* A new fuzzy filter has now been added. See README for details (#369, #370)
Bugs/Fixes
* A very subtle timing issue that causes the search to be reset
has been resolved (#368)
v0.4.5 - 30 Oct 2016
Bugs/Fixes
* v0.4.4 broke --select-1 :/ (#363)
v0.4.4 - 23 Oct 2016
Bugs/Fixes
* Fix to force a redraw of the screen when the query becomes empty (#346)
* Fix blocking when read from an slow input source is interrupted in
the middle (#359)
* Remove gratuitous use of panic that could happen occasionally when we
bail out of setup (like the scenario above)
v0.4.3 - 05 Oct 2016
Bugs/Fixes
* Fix to apply InitialFilter setting from config file (#343)
* Fix --buffer-size option
* Fix --prompt option
* Fix deadlock when certain actions are combined in the custom
action sequence. This bug probably existed for a long time,
but probably nobody bothered to report or didn't use the
combinations that causes the deadlock (#345)
* Force redraw when SelectNone/RefreshScreen is called (#346)
v0.4.2 - 23 Aug 2016
Bugs/Fixes
* Fix single key jump mode (#331, #332)
v0.4.1 - 21 Aug 2016
Bugs/Fixes
* Fix a panic that went unnoticed (#328, #329)
v0.4.0 - 19 Aug 2016
Miscellaneous:
* There is no real user visible change for this release
* Build using go 1.7
* LOTS of internal cleanup
* Eliminated (hopefully) all of potential races
* Remove silly embedded structs that existed all over the place
* New pipeline mechanism should allow slightly faster filtering
* Many fixes to properly release resources acquired by goroutines
v0.3.6 - 11 May 2016
Features:
* Implement way to execute arbitrary commands via ExecuteCommand config
Bugs/Fixes:
* Fixed wrong default value for LayoutType
* Fixed wrong (actually lack of) extension for Windows binary
Miscellaneous:
* Changed textual cursor representation
* Changed dependency manager to glide
* Changed build system to... make. Yes. Make.
* Compile with go1.6
v0.3.5 - 30 Oct 2015
Features:
* Implement --select-1 option (#278)
Bugs/Fixes:
* Do not switch lines when there's no output (#274)
Miscellaneous:
* Fix automatic gzip generation (#275)
v0.3.4 - 29 Sep 2015
Features:
* Implemented SingleKeyJump mode, a.k.a. hit-a-hint
Bugs/Fixes:
* Fix a problem where lines were sometimes not highlighted
* Executing a query may sometimes result in a shorter list of entries
such that the page you were at before the query is no longer valid.
The cursor would now move to the closest page instead of an empty buffer
Miscellaneous:
* Only support go1.5 (we now use cross compilation and internal packages)
* `go run build/make.go build` will now build all of the peco release files
v0.3.3 - 11 Jul 2015
Features:
* Implemented ScrollLeft/ScrollRight, to read long lines
* Implemented BackToInitialFilter, to make it easier to create combined actions
that change the filter type
Bugs/Fixes
* Build properly on *BSDs
v0.3.2 - 24 Mar 2015
Bugs/Fixes
* Filtered results could be wrong when QueryExecutionDelay is set to 0 (#241)
v0.3.1 - 24 Mar 2015
Bugs/Fixes
* Regexp filter was accidentally disabled (#239/#240). Now it's back
v0.3.0 - 20 Mar 2015
* BEWARE! 50% of the code has been rewritten!
Features
* Matchers are now known as Filters
* Added StickySelection configuration option
* Added QueryExecutionDelay configuration option
* Added CustomFilter.BufferThreshold option
* For Windows, an extra space at the bottom has been added to facilitate
those users using an IM (which consumes an extra line)
Deprecations
* --no-ignore-case has been completely removed
* InitialMatcher configuration option has been deprecated. Use InitialFilter
* CustomMatcher configuration option has been deprecated. Use CustomFilter
* RotateMatcher action has been deprecated. Use RotateFilter
Miscellaneous
* To enable trace logs, compile with `-tags debug`, and enable the logs
via PECO_TRACE environment variable. e.g. `PECO_TRACE=1 peco ...`
v0.2.12 - 27 Feb 2015
Bugs/Fixes
* Lines with tabs didn't display properly.
* Matches that would fail to hilight certain parts of the text
depending on the term order
* SelectNext/SelectPrevious actions were reversed (however, these were
deprecated anyways)
Miscellaneous
* You no longer need to tap our custom tap for peco to be installed from
homebrew.
* Custom matcher processing is fater now
v0.2.11 - 16 Dec 2014
Features
* Add a InvertSelection action. Not default keymapping has been assigned
as of this change, so you need to explicitly set one in your config file.
* Display total entries in additin to number of pages
* Add experimental ToggleQuery action which enables/disables the current
query.
Miscellaneous
* Massive refactoring to avoid possible race conditions. Note that I am
not aware of any breakage caused by previous builds of peco. Even
without these changes you most likely did not see a race or it was
something you could totally ignore by simply redrawing the page.
However, since I was in the mood, and since go is awesome for providing
us the `-race` switch, I decided to silence these warnings.
* Fix handling of --layout option when no config file is not present.
v0.2.10 - 24 Sep 2014
Bugs/Fixes
* There was a race condition in the way status messages were cleared,
which could leave a message to be not cleared. This has no serious
practical impact as it's just the status message, but has been fixed.
Miscellaneous
* We stopped using godep because it no longer allowed us to use -copy=false.
This should have no effect on users.
* Update termbox-go dependency
* Remove stdout hack
v0.2.9 - 08 Sep 2014
Features
* Add SmartCase matcher, which automatically toggles between case sensitive
and insensitive modes depending on the case of your query.
* Ctrl-l is now "peco.RefreshScreen", which basically runs the current query
again
Bugs/Fixes
* User prompt is now draw *before* the filtering is executed.
* Be more strict about checking invalid matchers.
* Add more tests (work in progress...)
v0.2.8 - 01 Sep 2014
Bugs/Fixes
* Fix in v0.2.7 was apparently not enough. Things should be fixed now.
* Long standing bugs against DeleteForwardWord/DeleteBackwardWord have
been fixed
v0.2.7 - 01 Sep 2014
Bugs/Fixes
* A regression introduced in v0.2.6 for DeleteForwardChar/DeleteBackwardChar
has been fixed
* Some entries in the Keymap section of the README was wrong, and has
been fixed
v0.2.6 - 30 Aug 2014
Bugs/Fixes
* --prompt option was not properly working
Miscellaneous
* Bunch of internal cleanups to make testing easier
v0.2.5 - 18 Aug 2014
Features
* Add --layout option, which allows you to switch between `top-down`
and `bottom-up` layout mode. This is equivalent of percol's
`--prompt-bottom --result-bottom-up`. Default is `top-down`.
The same option can be specified in the config file as "Layout"
Miscellaneous
* Because of the layout option, SelectNext/SelectPrevious and
SelectNextPage/SelectPreviousPage no longer made sense.
Now all of these are DEPRECATED, and are aliases to different
action names. See the README for the details.
In particular, you would need to configure your key bindings
using these if you want to use the `bottom-up` layout
v0.2.4 - 13 Aug 2014
Features
* Add --initial-matcher command line option to specify which
matcher type to use upon start up. --no-ignore-case is deprecated
* Add InitialMatcher option to the config file. Matcher option is
deprecated.
Miscellaneous
* Binaries are now built using go1.3.1
v0.2.3 - 28 Jul 2014
Bugs/Fixes
* Escape sequences are now stripped from input, so for example,
colored output can now be fed into peco. Filtering is done against
this stripped down buffer, but the original line is used for output.
* Some color specification combinations caused weird color variations
and in some worst cases caused crashes on Windows.
v0.2.2 - 23 Jul 2014
Bugs/Fixes
* Fixed display of characters with ambigous width
Features
* on_bold style attribute has been added.
v0.2.1 - 15 Jul 2014
Features
* Added the ability to setup custom combined keymap. You can now
assign multiple commands to one key sequence.
* Key sequence that is being processed currently is displayed in the
status message bar
v0.2.0 - 07 Jul 2014
Bugs/Fixes
* Keymaps have gone through an overhaul. Hopefully it's easier for
@ -67,7 +575,7 @@ v0.1.8 - 25 Jun 2014
v0.1.7 - 22 Jun 2014
Bugs/Fixes:
* Moved repository to a Github organization: http://github.com/peco/peco
* Moved repository to a GitHub organization: http://github.com/peco/peco
* Because of the above change, a lot of links, imports needed fixing.
Features:
* Automatically set GOMAXPROCS to NumCPU. If you would like to
@ -142,4 +650,4 @@ v0.1.1 - 12 Jun 2014
* Add peco --version
v0.1.0 - 12 Jun 2014
* Initial versioned release
* Initial versioned release

20
Godeps
View file

@ -1,20 +0,0 @@
{
"ImportPath": "github.com/peco/peco",
"GoVersion": "go1.3",
"Deps": [
{
"ImportPath": "github.com/jessevdk/go-flags",
"Comment": "v1-172-g8ec9564",
"Rev": "8ec9564882e7923e632f012761c81c46dcf5bec1"
},
{
"ImportPath": "github.com/mattn/go-runewidth",
"Comment": "travisish-12-g39104c7",
"Rev": "39104c78546f11be4bbc161e39858258aac6d9b8"
},
{
"ImportPath": "github.com/nsf/termbox-go",
"Rev": "81b415f8400796494f30d99fc9df17e03acacc71"
}
]
}

34
Makefile Normal file
View file

@ -0,0 +1,34 @@
MAKEFLAGS += --no-print-directory
export PATH := $(HOME)/go/bin:$(PATH)
.DEFAULT_GOAL := build
.PHONY: build snapshot test lint generate cover clean deps check-goreleaser
check-goreleaser:
@command -v goreleaser >/dev/null 2>&1 || { echo "goreleaser not found. Install it: https://goreleaser.com/install/"; exit 1; }
build: check-goreleaser
goreleaser build --snapshot --single-target --clean
snapshot: check-goreleaser
goreleaser release --snapshot --clean
test:
go test -v -race ./...
lint:
golangci-lint run ./...
generate:
go generate ./...
cover:
go test -race -coverprofile=coverage.out ./...
go tool cover -func=coverage.out
clean:
rm -rf dist
deps:
go mod download

1104
README.md

File diff suppressed because it is too large Load diff

1197
action.go

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

457
buffer.go Normal file
View file

@ -0,0 +1,457 @@
package peco
import (
"errors"
"fmt"
"sync"
"time"
"context"
"github.com/lestrrat-go/pdebug"
runewidth "github.com/mattn/go-runewidth"
"github.com/peco/peco/line"
"github.com/peco/peco/pipeline"
)
// Buffer interface is used for containers for lines to be
// processed by peco. The unexported linesInRange method seals this
// interface to the peco package — external packages cannot implement it.
// This is intentional: linesInRange is an internal optimization for
// efficient pagination in NewFilteredBuffer, not part of the public contract.
type Buffer interface {
linesInRange(int, int) []line.Line
LineAt(int) (line.Line, error)
Size() int
}
// FilteredBuffer holds a "filtered" buffer. It holds a reference to
// the source buffer (note: should be immutable) and a list of indices
// into the source buffer
type FilteredBuffer struct {
maxcols int
src Buffer
selection []int // maps from our index to src's index
}
// MemoryBuffer is an implementation of Buffer
type MemoryBuffer struct {
done chan struct{}
doneOnce sync.Once
lines []line.Line
mutex sync.RWMutex
}
// ContextLine wraps a line.Line to mark it as a context line (non-matched
// surrounding line shown during ZoomIn). Detected via type assertion in
// ListArea.Draw() to apply the Context style.
type ContextLine struct {
line.Line
}
// NewFilteredBuffer creates a FilteredBuffer containing one page of lines from
// the source buffer, computing the maximum column width for horizontal scrolling.
func NewFilteredBuffer(src Buffer, page, perPage int) *FilteredBuffer {
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,
}
if start < 0 {
start = 0
}
// 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() {
return &fb
}
// Copy over the selections that are applicable to this filtered buffer.
end := min(start+count, src.Size())
selection := make([]int, 0, end-start)
lines := src.linesInRange(start, end)
var maxcols int
for i := start; i < end; i++ {
selection = append(selection, i)
cols := runewidth.StringWidth(lines[i-start].DisplayString())
if cols > maxcols {
maxcols = cols
}
}
fb.selection = selection
fb.maxcols = maxcols
return &fb
}
// MaxColumn returns the max column size, which controls the amount we
// can scroll to the right
func (flb *FilteredBuffer) MaxColumn() int {
return flb.maxcols
}
// LineAt returns the line at index `i`. Note that the i-th element
// in this filtered buffer may actually correspond to a totally
// different line number in the source buffer.
func (flb FilteredBuffer) LineAt(i int) (line.Line, error) {
if i < 0 || i >= len(flb.selection) {
return nil, fmt.Errorf("specified index %d is out of range (size=%d)", i, len(flb.selection))
}
return flb.src.LineAt(flb.selection[i])
}
// Size returns the number of lines in the buffer
func (flb FilteredBuffer) Size() int {
return len(flb.selection)
}
const defaultMemoryBufferCap = 1024
// NewMemoryBuffer creates a new MemoryBuffer. If cap > 0, the lines
// slice is pre-allocated with that capacity; otherwise it defaults to
// defaultMemoryBufferCap.
func NewMemoryBuffer(capacity int) *MemoryBuffer {
if capacity <= 0 {
capacity = defaultMemoryBufferCap
}
mb := &MemoryBuffer{}
mb.done = make(chan struct{})
mb.lines = make([]line.Line, 0, capacity)
return mb
}
// Size returns the number of lines currently held in the buffer, thread-safe.
func (mb *MemoryBuffer) Size() int {
mb.mutex.RLock()
defer mb.mutex.RUnlock()
return len(mb.lines)
}
// Reset clears the buffer, reinitializing the done channel and lines slice
// so the buffer can be reused for a new pipeline run.
func (mb *MemoryBuffer) Reset() {
mb.mutex.Lock()
defer mb.mutex.Unlock()
if pdebug.Enabled {
g := pdebug.Marker("MemoryBuffer.Reset")
defer g.End()
}
// Return pooled Matched objects before discarding the slice
for _, l := range mb.lines {
if m, ok := l.(*line.Matched); ok {
line.ReleaseMatched(m)
}
}
mb.done = make(chan struct{})
mb.doneOnce = sync.Once{}
mb.lines = []line.Line(nil)
}
// MarkComplete signals that the buffer is fully populated. It is safe
// to call multiple times; only the first call closes the done channel.
// Use this instead of manually closing the done channel when populating
// a MemoryBuffer outside of the pipeline (e.g. freeze).
func (mb *MemoryBuffer) MarkComplete() {
mb.doneOnce.Do(func() {
mb.mutex.Lock()
close(mb.done)
mb.mutex.Unlock()
})
}
// Done returns a channel that is closed when the buffer has been fully populated.
func (mb *MemoryBuffer) Done() <-chan struct{} {
mb.mutex.RLock()
defer mb.mutex.RUnlock()
return mb.done
}
// Accept receives lines from a pipeline input channel and appends them to the
// buffer in batches. It marks the buffer complete when the channel closes or
// the context is cancelled.
func (mb *MemoryBuffer) Accept(ctx context.Context, in <-chan line.Line, _ pipeline.ChanOutput) {
if pdebug.Enabled {
g := pdebug.Marker("MemoryBuffer.Accept")
defer g.End()
}
defer mb.MarkComplete()
// batch collects lines from the channel so we can append them
// under a single lock acquisition instead of locking per line.
batch := make([]line.Line, 0, 256)
start := time.Now()
for {
select {
case <-ctx.Done():
if pdebug.Enabled {
pdebug.Printf("MemoryBuffer received context done")
}
return
case v, ok := <-in:
if !ok {
if pdebug.Enabled {
pdebug.Printf("MemoryBuffer input channel closed (read %d lines, %s since starting accept loop)", len(mb.lines)+len(batch), time.Since(start).String())
}
// Flush remaining batch
if len(batch) > 0 {
mb.mutex.Lock()
mb.lines = append(mb.lines, batch...)
mb.mutex.Unlock()
}
return
}
batch = append(batch, v)
// Drain any additional ready values without blocking
drain:
for {
select {
case v2, ok2 := <-in:
if !ok2 {
if pdebug.Enabled {
pdebug.Printf("MemoryBuffer input channel closed (read %d lines, %s since starting accept loop)", len(mb.lines)+len(batch), time.Since(start).String())
}
mb.mutex.Lock()
mb.lines = append(mb.lines, batch...)
mb.mutex.Unlock()
return
}
batch = append(batch, v2)
default:
break drain
}
}
// Flush the batch
mb.mutex.Lock()
mb.lines = append(mb.lines, batch...)
mb.mutex.Unlock()
batch = batch[:0]
}
}
}
// AppendLine adds a line to the buffer. This is used by the benchmark tool
// to populate a MemoryBuffer that will be used as a pipeline source.
func (mb *MemoryBuffer) AppendLine(l line.Line) {
mb.mutex.Lock()
mb.lines = append(mb.lines, l)
mb.mutex.Unlock()
}
// LineAt returns the line at the given index, thread-safe.
func (mb *MemoryBuffer) LineAt(n int) (line.Line, error) {
mb.mutex.RLock()
defer mb.mutex.RUnlock()
return bufferLineAt(mb.lines, n)
}
// linesInRange returns a slice of lines between start and end indices, thread-safe.
func (mb *MemoryBuffer) linesInRange(start, end int) []line.Line {
mb.mutex.RLock()
defer mb.mutex.RUnlock()
return mb.lines[start:end]
}
// bufferLineAt is a shared helper that retrieves a line by index from a raw
// line slice, returning an error if the index is out of bounds.
func bufferLineAt(lines []line.Line, n int) (line.Line, error) {
if s := len(lines); s <= 0 || n >= s {
return nil, errors.New("empty buffer")
}
return lines[n], nil
}
// ContextBuffer holds an expanded view of a filtered buffer, inserting
// context lines from the source around each match (similar to grep -C).
type ContextBuffer struct {
entries []line.Line
maxcols int
filtered Buffer // original filtered buffer, for reference
matchEntryIndices []int // maps filtered buffer index -> entries index
}
// NewContextBuffer builds a ContextBuffer by expanding the filtered buffer
// with contextSize lines of surrounding context from the source.
func NewContextBuffer(filtered Buffer, source Buffer, contextSize int) *ContextBuffer {
cb := &ContextBuffer{
filtered: filtered,
}
filteredSize := filtered.Size()
if filteredSize == 0 {
return cb
}
sourceSize := source.Size()
// Step 1: Collect source indices of matched lines
type matchInfo struct {
srcIdx int
filteredIdx int
line line.Line
}
matches := make([]matchInfo, 0, filteredSize)
for i := range filteredSize {
l, err := filtered.LineAt(i)
if err != nil {
continue
}
srcIdx := int(l.ID())
matches = append(matches, matchInfo{srcIdx: srcIdx, filteredIdx: i, line: l})
}
if len(matches) == 0 {
return cb
}
// Step 2: Compute context ranges and merge overlapping ones
type contextRange struct {
start int // inclusive
end int // inclusive
}
ranges := make([]contextRange, 0, len(matches))
for _, m := range matches {
start := max(m.srcIdx-contextSize, 0)
end := m.srcIdx + contextSize
if end >= sourceSize {
end = sourceSize - 1
}
ranges = append(ranges, contextRange{start: start, end: end})
}
// Merge overlapping/adjacent ranges
merged := []contextRange{ranges[0]}
for i := 1; i < len(ranges); i++ {
last := &merged[len(merged)-1]
if ranges[i].start <= last.end+1 {
if ranges[i].end > last.end {
last.end = ranges[i].end
}
} else {
merged = append(merged, ranges[i])
}
}
// Step 3: Build a set of matched source indices for quick lookup
matchedSet := make(map[int]int, len(matches)) // srcIdx -> matches slice index
for i, m := range matches {
matchedSet[m.srcIdx] = i
}
// Step 4: Build entries
cb.entries = make([]line.Line, 0, merged[len(merged)-1].end-merged[0].start+1)
cb.matchEntryIndices = make([]int, filteredSize)
// Initialize to -1 so unmapped entries are obvious
for i := range cb.matchEntryIndices {
cb.matchEntryIndices[i] = -1
}
for _, r := range merged {
for idx := r.start; idx <= r.end; idx++ {
if mi, ok := matchedSet[idx]; ok {
// Use the filtered (matched) line, preserving match highlighting
cb.matchEntryIndices[matches[mi].filteredIdx] = len(cb.entries)
cb.entries = append(cb.entries, matches[mi].line)
} else {
// Context line from source
srcLine, err := source.LineAt(idx)
if err != nil {
continue
}
cb.entries = append(cb.entries, &ContextLine{srcLine})
}
}
}
// Step 5: Compute maxcols
for _, l := range cb.entries {
cols := runewidth.StringWidth(l.DisplayString())
if cols > cb.maxcols {
cb.maxcols = cols
}
}
return cb
}
// Size returns the number of lines in the context buffer.
func (cb *ContextBuffer) Size() int {
return len(cb.entries)
}
// LineAt returns the line at index i.
func (cb *ContextBuffer) LineAt(i int) (line.Line, error) {
if i < 0 || i >= len(cb.entries) {
return nil, fmt.Errorf("specified index %d is out of range (size=%d)", i, len(cb.entries))
}
return cb.entries[i], nil
}
// linesInRange returns a slice of entries between start and end indices.
func (cb *ContextBuffer) linesInRange(start, end int) []line.Line {
return cb.entries[start:end]
}
// MaxColumn returns the max column size for horizontal scrolling.
func (cb *ContextBuffer) MaxColumn() int {
return cb.maxcols
}
// MatchEntryIndices returns the mapping from filtered buffer index to
// entries index. Used by ZoomIn to map the cursor position.
func (cb *ContextBuffer) MatchEntryIndices() []int {
return cb.matchEntryIndices
}
// MemoryBufferSource wraps a completed MemoryBuffer as a pipeline.Source,
// allowing previous filter results to be reused as the input for
// incremental filtering.
type MemoryBufferSource struct {
buf *MemoryBuffer
}
// NewMemoryBufferSource creates a new MemoryBufferSource from an existing
// MemoryBuffer. The buffer should be fully populated (pipeline completed).
func NewMemoryBufferSource(buf *MemoryBuffer) *MemoryBufferSource {
return &MemoryBufferSource{buf: buf}
}
// Start iterates through the MemoryBuffer's lines and sends them
// individually to the output channel, implementing pipeline.Source.
// The output channel is closed when all lines have been sent.
func (s *MemoryBufferSource) Start(ctx context.Context, out pipeline.ChanOutput) {
defer close(out)
s.buf.mutex.RLock()
lines := s.buf.lines
s.buf.mutex.RUnlock()
for _, l := range lines {
select {
case <-ctx.Done():
return
default:
}
if err := out.Send(ctx, l); err != nil {
return
}
}
}
// Reset is a no-op for MemoryBufferSource since the underlying buffer
// is immutable (from a completed pipeline run).
func (s *MemoryBufferSource) Reset() {}

462
cmd/filterbench/main.go Normal file
View file

@ -0,0 +1,462 @@
// filterbench measures the query performance of peco's filter implementations
// against synthetically generated datasets of configurable size.
//
// Usage:
//
// go run ./cmd/filterbench [flags]
//
// Examples:
//
// go run ./cmd/filterbench -lines 1000000 -query "foo"
// go run ./cmd/filterbench -lines 500000 -query "foo bar" -filter IgnoreCase
// go run ./cmd/filterbench -lines 1000000 -query "f" -incremental "fo,foo,foob,fooba,foobar"
// go run ./cmd/filterbench -lines 1000000 -pipeline -filter IgnoreCase -query "foo"
// go run ./cmd/filterbench -lines 1000000 -input /path/to/largefile.txt
package main
import (
"bufio"
"context"
"encoding/json"
"flag"
"fmt"
"math/rand/v2"
"os"
"runtime"
"strings"
"time"
peco "github.com/peco/peco"
"github.com/peco/peco/filter"
"github.com/peco/peco/line"
"github.com/peco/peco/pipeline"
)
type result struct {
Filter string `json:"filter"`
Query string `json:"query"`
Lines int `json:"lines"`
Matches int `json:"matches"`
Duration string `json:"duration"`
DurationMs float64 `json:"duration_ms"`
LinesPerSec float64 `json:"lines_per_sec"`
Mode string `json:"mode"` // "direct", "pipeline", "FULL", "INCR"
}
type benchConfig struct {
numLines int
query string
incremental string
filterName string
inputFile string
jsonOutput bool
seed uint64
lineLen int
usePipeline bool
}
func main() {
var cfg benchConfig
flag.IntVar(&cfg.numLines, "lines", 1_000_000, "number of lines to generate")
flag.StringVar(&cfg.query, "query", "foo", "query string to filter with")
flag.StringVar(&cfg.incremental, "incremental", "", "comma-separated queries to run in sequence (simulates typing)")
flag.StringVar(&cfg.filterName, "filter", "", "filter to test (empty = all filters)")
flag.StringVar(&cfg.inputFile, "input", "", "read lines from file instead of generating them")
flag.BoolVar(&cfg.jsonOutput, "json", false, "output results as JSON")
flag.Uint64Var(&cfg.seed, "seed", 42, "random seed for data generation")
flag.IntVar(&cfg.lineLen, "line-length", 80, "average length of generated lines")
flag.BoolVar(&cfg.usePipeline, "pipeline", false, "use full pipeline (Source -> filterProcessor -> MemoryBuffer) instead of direct Apply")
flag.Parse()
lines := loadOrGenerate(cfg)
filters := buildFilters(cfg.filterName)
if len(filters) == 0 {
fmt.Fprintf(os.Stderr, "unknown filter: %s\n", cfg.filterName)
fmt.Fprintf(os.Stderr, "available: IgnoreCase, CaseSensitive, SmartCase, Regexp, IRegexp, Fuzzy, FuzzyLongest\n")
os.Exit(1)
}
queries := buildQueries(cfg)
if !cfg.jsonOutput {
fmt.Fprintf(os.Stderr, "Dataset: %d lines\n", len(lines))
fmt.Fprintf(os.Stderr, "Filters: %d\n", len(filters))
fmt.Fprintf(os.Stderr, "Queries: %v\n", queries)
fmt.Fprintf(os.Stderr, "GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0))
fmt.Fprintf(os.Stderr, "Pipeline: %v\n\n", cfg.usePipeline)
}
var results []result
if cfg.incremental != "" {
for name, f := range filters {
results = append(results, benchIncremental(name, f, lines, queries, cfg)...)
}
} else {
for name, f := range filters {
for _, q := range queries {
var r result
if cfg.usePipeline {
r = benchPipeline(name, f, lines, q)
} else {
r = benchDirect(name, f, lines, q)
}
results = append(results, r)
if !cfg.jsonOutput {
printResult(r)
}
}
}
}
if cfg.jsonOutput {
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
if err := enc.Encode(results); err != nil {
fmt.Fprintf(os.Stderr, "failed to encode results: %v\n", err)
}
}
}
func buildQueries(cfg benchConfig) []string {
if cfg.incremental != "" {
return strings.Split(cfg.incremental, ",")
}
return []string{cfg.query}
}
func buildFilters(name string) map[string]filter.Filter {
all := map[string]filter.Filter{
"IgnoreCase": filter.NewIgnoreCase(),
"CaseSensitive": filter.NewCaseSensitive(),
"SmartCase": filter.NewSmartCase(),
"Regexp": filter.NewRegexp(),
"IRegexp": filter.NewIRegexp(),
"Fuzzy": filter.NewFuzzy(false),
"FuzzyLongest": filter.NewFuzzy(true),
}
if name == "" {
return all
}
if f, ok := all[name]; ok {
return map[string]filter.Filter{name: f}
}
return nil
}
func loadOrGenerate(cfg benchConfig) []line.Line {
if cfg.inputFile != "" {
return loadFromFile(cfg.inputFile)
}
return generateLines(cfg)
}
func loadFromFile(path string) []line.Line {
f, err := os.Open(path)
if err != nil {
fmt.Fprintf(os.Stderr, "error opening file: %v\n", err)
os.Exit(1)
}
defer f.Close()
var lines []line.Line
scanner := bufio.NewScanner(f)
// Allow long lines
scanner.Buffer(make([]byte, 256*1024), 256*1024)
var id uint64
for scanner.Scan() {
lines = append(lines, line.NewRaw(id, scanner.Text(), false, false))
id++
}
if err := scanner.Err(); err != nil {
fmt.Fprintf(os.Stderr, "error reading file: %v\n", err)
os.Exit(1)
}
return lines
}
// generateLines creates a synthetic dataset. The data is designed so that:
// - Common short queries (e.g. "f", "fo") match many lines
// - Longer queries (e.g. "foobar") match progressively fewer lines
// - Lines have realistic variety in length and content
func generateLines(cfg benchConfig) []line.Line {
rng := rand.New(rand.NewPCG(cfg.seed, cfg.seed+1))
// Words that will be scattered through the corpus.
// "foo", "bar", "baz" appear frequently so short queries hit many lines.
// Longer compound words like "foobar" appear rarely.
words := []struct {
word string
freq float64 // probability of appearing in a line
}{
{"foo", 0.30},
{"bar", 0.25},
{"baz", 0.15},
{"qux", 0.10},
{"foobar", 0.03},
{"foobaz", 0.02},
{"foobarbaz", 0.005},
}
// Base character set for random filler
const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789_-./: "
lines := make([]line.Line, cfg.numLines)
for i := range lines {
var sb strings.Builder
targetLen := cfg.lineLen/2 + rng.IntN(cfg.lineLen)
for sb.Len() < targetLen {
// Randomly insert a known word
for _, w := range words {
if rng.Float64() < w.freq*0.3 { // Scale down per-character check
if sb.Len() > 0 {
sb.WriteByte(' ')
}
sb.WriteString(w.word)
}
}
// Add random filler
n := 3 + rng.IntN(8)
for j := 0; j < n && sb.Len() < targetLen; j++ {
sb.WriteByte(alphabet[rng.IntN(len(alphabet))])
}
if sb.Len() < targetLen {
sb.WriteByte(' ')
}
}
lines[i] = line.NewRaw(uint64(i), sb.String(), false, false)
}
return lines
}
// benchDirect runs a single filter+query combination using direct Apply() and returns timing results.
func benchDirect(name string, f filter.Filter, lines []line.Line, query string) result {
ctx := f.NewContext(context.Background(), query)
// Count matches using a channel consumer
matchCount := 0
ch := make(chan line.Line, 4096)
done := make(chan struct{})
go func() {
defer close(done)
for range ch {
matchCount++
}
}()
out := pipeline.ChanOutput(ch)
// Force GC before measurement
runtime.GC()
start := time.Now()
err := f.Apply(ctx, lines, out)
elapsed := time.Since(start)
close(ch)
<-done
if err != nil {
fmt.Fprintf(os.Stderr, "error: %s/%s: %v\n", name, query, err)
}
lps := float64(len(lines)) / elapsed.Seconds()
return result{
Filter: name,
Query: query,
Lines: len(lines),
Matches: matchCount,
Duration: elapsed.String(),
DurationMs: float64(elapsed.Milliseconds()),
LinesPerSec: lps,
Mode: "direct",
}
}
// benchPipeline runs a filter using the full pipeline (Source -> filterProcessor -> MemoryBuffer).
func benchPipeline(name string, f filter.Filter, lines []line.Line, query string) result {
ctx := f.NewContext(context.Background(), query)
// Build a MemoryBuffer with all lines to use as source
srcBuf := peco.NewMemoryBuffer(0)
for _, l := range lines {
func() {
srcBuf.AppendLine(l)
}()
}
src := peco.NewMemoryBufferSource(srcBuf)
// Setup pipeline
p := pipeline.New()
p.SetSource(src)
p.Add(&directFilterProcessor{filter: f, query: query})
dst := peco.NewMemoryBuffer(len(lines) / 4)
p.SetDestination(dst)
runtime.GC()
start := time.Now()
if err := p.Run(ctx); err != nil {
fmt.Fprintf(os.Stderr, "error: %s/%s: %v\n", name, query, err)
}
elapsed := time.Since(start)
matchCount := dst.Size()
lps := float64(len(lines)) / elapsed.Seconds()
return result{
Filter: name,
Query: query,
Lines: len(lines),
Matches: matchCount,
Duration: elapsed.String(),
DurationMs: float64(elapsed.Milliseconds()),
LinesPerSec: lps,
Mode: "pipeline",
}
}
// isQueryRefinement checks whether new is a refinement of prev.
func isQueryRefinement(prev, cur string) bool {
prev = strings.TrimSpace(prev)
cur = strings.TrimSpace(cur)
if prev == "" || cur == "" {
return false
}
return strings.HasPrefix(cur, prev)
}
// benchIncremental runs a sequence of queries, using previous results as source when the query is a refinement.
func benchIncremental(name string, f filter.Filter, allLines []line.Line, queries []string, cfg benchConfig) []result {
results := make([]result, 0, len(queries))
var cumulative time.Duration
var prevQuery string
var prevResults []line.Line
for _, q := range queries {
var sourceLines []line.Line
var mode string
// Check if we can use incremental filtering
if prevResults != nil && isQueryRefinement(prevQuery, q) {
sourceLines = prevResults
mode = "INCR"
} else {
sourceLines = allLines
mode = "FULL"
}
var r result
if cfg.usePipeline {
r = benchPipelineOnLines(name, f, sourceLines, q)
} else {
r = benchDirect(name, f, sourceLines, q)
}
r.Mode = mode
cumulative += time.Duration(r.DurationMs * float64(time.Millisecond))
if !cfg.jsonOutput {
fmt.Printf("[%s] ", mode)
printResult(r)
fmt.Printf(" source lines: %d cumulative: %s\n", len(sourceLines), cumulative)
}
results = append(results, r)
// Save results for next iteration
prevQuery = q
prevResults = collectMatchedLines(f, sourceLines, q)
}
if !cfg.jsonOutput {
fmt.Printf("\nCumulative time: %s\n\n", cumulative)
}
return results
}
// collectMatchedLines runs the filter and returns matched lines as a slice.
func collectMatchedLines(f filter.Filter, lines []line.Line, query string) []line.Line {
ctx := f.NewContext(context.Background(), query)
ch := make(chan line.Line, 4096)
done := make(chan struct{})
var matched []line.Line
go func() {
defer close(done)
for l := range ch {
matched = append(matched, l)
}
}()
f.Apply(ctx, lines, pipeline.ChanOutput(ch))
close(ch)
<-done
return matched
}
// benchPipelineOnLines runs a filter through the pipeline on a specific set of lines.
func benchPipelineOnLines(name string, f filter.Filter, lines []line.Line, query string) result {
ctx := f.NewContext(context.Background(), query)
srcBuf := peco.NewMemoryBuffer(0)
for _, l := range lines {
srcBuf.AppendLine(l)
}
src := peco.NewMemoryBufferSource(srcBuf)
p := pipeline.New()
p.SetSource(src)
p.Add(&directFilterProcessor{filter: f, query: query})
dst := peco.NewMemoryBuffer(len(lines) / 4)
p.SetDestination(dst)
runtime.GC()
start := time.Now()
if err := p.Run(ctx); err != nil {
fmt.Fprintf(os.Stderr, "error: %s/%s: %v\n", name, query, err)
}
elapsed := time.Since(start)
matchCount := dst.Size()
lps := float64(len(lines)) / elapsed.Seconds()
return result{
Filter: name,
Query: query,
Lines: len(lines),
Matches: matchCount,
Duration: elapsed.String(),
DurationMs: float64(elapsed.Milliseconds()),
LinesPerSec: lps,
}
}
// directFilterProcessor wraps a filter.Filter for use in a pipeline, using the
// same acceptAndFilter mechanism as peco's filterProcessor.
type directFilterProcessor struct {
filter filter.Filter
query string
}
func (p *directFilterProcessor) Accept(ctx context.Context, in <-chan line.Line, out pipeline.ChanOutput) {
peco.AcceptAndFilter(ctx, p.filter, 0, in, out)
}
func printResult(r result) {
fmt.Printf("%-16s query=%-20s %d lines %6d matches %10s (%.0f lines/sec)\n",
r.Filter, fmt.Sprintf("%q", r.Query), r.Lines, r.Matches, r.Duration, r.LinesPerSec)
}

View file

@ -3,213 +3,47 @@ package main
import (
"fmt"
"os"
"runtime"
"github.com/jessevdk/go-flags"
"github.com/nsf/termbox-go"
"context"
"github.com/lestrrat-go/pdebug"
"github.com/peco/peco"
"github.com/peco/peco/internal/util"
)
var version = "v0.2.0"
func showHelp() {
const v = `
Usage: peco [options] [FILE]
Options:
-h, --help show this help message and exit
--version print the version and exit
--rcfile=RCFILE path to the settings file
--query=QUERY pre-input query
--no-ignore-case start in case-sensitive mode
-b, --buffer-size number of lines to keep in search buffer
--null expect NUL (\0) as separator for target/output (EXPERIMENTAL)
--initial-index position of the initial index of the selection (0 base)
--prompt specify prompt
`
os.Stderr.Write([]byte(v))
}
type cmdOptions struct {
OptHelp bool `short:"h" long:"help" description:"show this help message and exit"`
OptTTY string `long:"tty" description:"path to the TTY (usually, the value of $TTY)"`
OptQuery string `long:"query"`
OptRcfile string `long:"rcfile" descriotion:"path to the settings file"`
OptNoIgnoreCase bool `long:"no-ignore-case" description:"start in case-sensitive-mode" default:"false"`
OptVersion bool `long:"version" description:"print the version and exit"`
OptBufferSize int `long:"buffer-size" short:"b" description:"number of lines to keep in search buffer"`
OptEnableNullSep bool `long:"null" description:"expect NUL (\\0) as separator for target/output"`
OptInitialIndex int `long:"initial-index" description:"position of the initial index of the selection (0 base)"`
OptPrompt string `long:"prompt"`
}
// BufferSize returns the specified buffer size. Fulfills peco.CtxOptions
func (o cmdOptions) BufferSize() int {
return o.OptBufferSize
}
// EnableNullSep returns tru if --null was specified. Fulfills peco.CtxOptions
func (o cmdOptions) EnableNullSep() bool {
return o.OptEnableNullSep
}
func (o cmdOptions) InitialIndex() int {
if o.OptInitialIndex >= 0 {
return o.OptInitialIndex + 1
} else {
return 1
}
}
func main() {
var err error
var st int
defer func() { os.Exit(st) }()
if envvar := os.Getenv("GOMAXPROCS"); envvar == "" {
runtime.GOMAXPROCS(runtime.NumCPU())
}
opts := &cmdOptions{}
p := flags.NewParser(opts, flags.PrintErrors)
args, err := p.Parse()
if err != nil {
showHelp()
st = 1
return
}
if opts.OptHelp {
showHelp()
return
}
if opts.OptVersion {
fmt.Fprintf(os.Stderr, "peco: %s\n", version)
return
}
var in *os.File
// receive in from either a file or Stdin
switch {
case len(args) > 0:
in, err = os.Open(args[0])
if err != nil {
st = 1
fmt.Fprintln(os.Stderr, err)
return
}
case !peco.IsTty(os.Stdin.Fd()):
in = os.Stdin
default:
fmt.Fprintln(os.Stderr, "You must supply something to work with via filename or stdin")
st = 1
return
}
ctx := peco.NewCtx(opts)
defer func() {
if err := recover(); err != nil {
st = 1
fmt.Fprintf(os.Stderr, "Error:\n%s", err)
}
if result := ctx.Result(); result != nil {
for _, match := range result {
line := match.Output()
if line[len(line)-1] != '\n' {
line = line + "\n"
}
fmt.Fprint(os.Stdout, line)
}
os.Exit(1)
}
}()
if opts.OptRcfile == "" {
file, err := peco.LocateRcfile()
if err == nil {
opts.OptRcfile = file
}
}
// Default matcher is IgnoreCase
ctx.SetCurrentMatcher(peco.IgnoreCaseMatch)
if opts.OptRcfile != "" {
err = ctx.ReadConfig(opts.OptRcfile)
if err != nil {
fmt.Fprintln(os.Stderr, err)
st = 1
return
}
}
if opts.OptNoIgnoreCase {
ctx.SetCurrentMatcher(peco.CaseSensitiveMatch)
}
// Try waiting for something available in the source stream
// before doing any terminal initialization (also done by termbox)
reader := ctx.NewBufferReader(in)
ctx.AddWaitGroup(1)
go reader.Loop()
// This channel blocks until we receive something from `in`
<-reader.InputReadyCh()
err = peco.TtyReady()
if err != nil {
fmt.Fprintln(os.Stderr, err)
st = 1
return
}
defer peco.TtyTerm()
err = termbox.Init()
if err != nil {
fmt.Fprintln(os.Stderr, err)
st = 1
return
}
defer termbox.Close()
// Windows handle Esc/Alt self
if runtime.GOOS == "windows" {
termbox.SetInputMode(termbox.InputEsc | termbox.InputAlt)
}
view := ctx.NewView()
filter := ctx.NewFilter()
input := ctx.NewInput()
sig := ctx.NewSignalHandler()
loopers := []interface {
Loop()
}{
view,
filter,
input,
sig,
}
for _, looper := range loopers {
ctx.AddWaitGroup(1)
go looper.Loop()
}
if len(opts.OptQuery) > 0 {
ctx.SetQuery([]rune(opts.OptQuery))
ctx.ExecQuery()
} else {
view.Refresh()
}
if len(opts.OptPrompt) > 0 {
ctx.SetPrompt([]rune(opts.OptPrompt))
}
ctx.WaitDone()
st = ctx.ExitStatus
os.Exit(_main())
}
func _main() int {
if pdebug.Enabled {
pdebug.DefaultCtx.Writer = os.Stderr
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
cli := peco.New()
if err := cli.Run(ctx); err != nil {
switch {
case util.IsCollectResultsError(err):
cli.PrintResults()
return 0
case util.IsIgnorableError(err):
if st, ok := util.GetExitStatus(err); ok {
return st
}
return 0
default:
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
return 1
}
}
return 0
}

195
config.go
View file

@ -1,195 +0,0 @@
package peco
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/nsf/termbox-go"
)
var homedirFunc = homedir
type Config struct {
// Keymap used to be directly responsible for dispatching
// events against user input, but since then this has changed
// into something that just records the user's config input
Keymap map[string]string `json:"Keymap"`
Matcher string `json:"Matcher"`
Style StyleSet `json:"Style"`
CustomMatcher map[string][]string
Prompt string `json:"Prompt"`
}
func NewConfig() *Config {
return &Config{
Keymap: make(map[string]string),
Matcher: IgnoreCaseMatch,
Style: NewStyleSet(),
Prompt: "QUERY>",
}
}
func (c *Config) ReadFilename(filename string) error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close()
err = json.NewDecoder(f).Decode(c)
if err != nil {
return err
}
return nil
}
var (
stringToFg = map[string]termbox.Attribute{
"default": termbox.ColorDefault,
"black": termbox.ColorBlack,
"red": termbox.ColorRed,
"green": termbox.ColorGreen,
"yellow": termbox.ColorYellow,
"blue": termbox.ColorBlue,
"magenta": termbox.ColorMagenta,
"cyan": termbox.ColorCyan,
"white": termbox.ColorWhite,
}
stringToBg = map[string]termbox.Attribute{
"on_default": termbox.ColorDefault,
"on_black": termbox.ColorBlack,
"on_red": termbox.ColorRed,
"on_green": termbox.ColorGreen,
"on_yellow": termbox.ColorYellow,
"on_blue": termbox.ColorBlue,
"on_magenta": termbox.ColorMagenta,
"on_cyan": termbox.ColorCyan,
"on_white": termbox.ColorWhite,
}
stringToAttr = map[string]termbox.Attribute{
"bold": termbox.AttrBold,
"underline": termbox.AttrUnderline,
"blink": termbox.AttrReverse,
}
)
type StyleSet struct {
Basic Style `json:"Basic"`
SavedSelection Style `json:"SavedSelection"`
Selected Style `json:"Selected"`
Query Style `json:"Query"`
Matched Style `json:"Matched"`
}
func NewStyleSet() StyleSet {
return StyleSet{
Basic: Style{fg: termbox.ColorDefault, bg: termbox.ColorDefault},
SavedSelection: Style{fg: termbox.ColorBlack | termbox.AttrBold, bg: termbox.ColorCyan},
Selected: Style{fg: termbox.ColorDefault | termbox.AttrUnderline, bg: termbox.ColorMagenta},
Query: Style{fg: termbox.ColorDefault, bg: termbox.ColorDefault},
Matched: Style{fg: termbox.ColorCyan, bg: termbox.ColorDefault},
}
}
type Style struct {
fg termbox.Attribute
bg termbox.Attribute
}
func (s *Style) UnmarshalJSON(buf []byte) error {
raw := []string{}
if err := json.Unmarshal(buf, &raw); err != nil {
return err
}
*s = *stringsToStyle(raw)
return nil
}
func stringsToStyle(raw []string) *Style {
style := &Style{
fg: termbox.ColorDefault,
bg: termbox.ColorDefault,
}
for _, s := range raw {
fg, ok := stringToFg[s]
if ok {
style.fg = fg
}
bg, ok := stringToBg[s]
if ok {
style.bg = bg
}
}
for _, s := range raw {
attr, ok := stringToAttr[s]
if ok {
style.fg |= attr
}
}
return style
}
var _locateRcfileIn = locateRcfileIn
func locateRcfileIn(dir string) (string, error) {
const basename = "config.json"
file := filepath.Join(dir, basename)
if _, err := os.Stat(file); err != nil {
return "", err
}
return file, nil
}
func LocateRcfile() (string, error) {
// http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
//
// Try in this order:
// $XDG_CONFIG_HOME/peco/config.json
// $XDG_CONFIG_DIR/peco/config.json (where XDG_CONFIG_DIR is listed in $XDG_CONFIG_DIRS)
// ~/.peco/config.json
home, uErr := homedirFunc()
// Try dir supplied via env var
if dir := os.Getenv("XDG_CONFIG_HOME"); dir != "" {
file, err := _locateRcfileIn(filepath.Join(dir, "peco"))
if err == nil {
return file, nil
}
} else if uErr == nil { // silently ignore failure for homedir()
// Try "default" XDG location, is user is available
file, err := _locateRcfileIn(filepath.Join(home, ".config", "peco"))
if err == nil {
return file, nil
}
}
// this standard does not take into consideration windows (duh)
// while the spec says use ":" as the separator, Go provides us
// with filepath.ListSeparator, so use it
if dirs := os.Getenv("XDG_CONFIG_DIRS"); dirs != "" {
for _, dir := range strings.Split(dirs, fmt.Sprintf("%c", filepath.ListSeparator)) {
file, err := _locateRcfileIn(filepath.Join(dir, "peco"))
if err == nil {
return file, nil
}
}
}
if uErr == nil { // silently ignore failure for homedir()
file, err := _locateRcfileIn(filepath.Join(home, ".peco"))
if err == nil {
return file, nil
}
}
return "", fmt.Errorf("Config file not found")
}

241
config/config.go Normal file
View file

@ -0,0 +1,241 @@
package config
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/goccy/go-yaml"
"github.com/peco/peco/internal/util"
)
// OnCancelBehavior specifies what happens when the user cancels peco.
type OnCancelBehavior string
const (
OnCancelSuccess OnCancelBehavior = "success"
OnCancelError OnCancelBehavior = "error"
)
// UnmarshalText parses a text value into an OnCancelBehavior, accepting
// "success" (or empty) and "error" as valid values.
func (o *OnCancelBehavior) UnmarshalText(b []byte) error {
switch s := string(b); s {
case "", "success":
*o = OnCancelSuccess
case "error":
*o = OnCancelError
default:
return fmt.Errorf("invalid OnCancel value %q: must be %q or %q", s, OnCancelSuccess, OnCancelError)
}
return nil
}
// ColorMode specifies how peco handles ANSI color codes in input.
type ColorMode string
const (
ColorModeAuto ColorMode = "auto"
ColorModeNone ColorMode = "none"
)
func (c *ColorMode) unmarshal(s string) error {
switch s {
case "", "auto":
*c = ColorModeAuto
case "none":
*c = ColorModeNone
default:
return fmt.Errorf("invalid Color value %q: must be %q or %q", s, ColorModeAuto, ColorModeNone)
}
return nil
}
// UnmarshalText implements encoding.TextUnmarshaler (used by JSON/YAML decoders).
func (c *ColorMode) UnmarshalText(b []byte) error {
return c.unmarshal(string(b))
}
// UnmarshalFlag implements go-flags Unmarshaler (used by CLI flag parsing).
func (c *ColorMode) UnmarshalFlag(s string) error {
return c.unmarshal(s)
}
// Config holds all the data that can be configured in the
// external configuration file
type Config struct {
Action map[string][]string `json:"Action" yaml:"Action"`
// Keymap used to be directly responsible for dispatching
// events against user input, but since then this has changed
// into something that just records the user's config input
Keymap map[string]string `json:"Keymap" yaml:"Keymap"`
InitialFilter string `json:"InitialFilter" yaml:"InitialFilter"`
Style StyleSet `json:"Style" yaml:"Style"`
Prompt string `json:"Prompt" yaml:"Prompt"`
Layout string `json:"Layout" yaml:"Layout"`
OnCancel OnCancelBehavior `json:"OnCancel" yaml:"OnCancel"`
CustomFilter map[string]CustomFilterConfig `json:"CustomFilter" yaml:"CustomFilter"`
QueryExecutionDelay int `json:"QueryExecutionDelay" yaml:"QueryExecutionDelay"`
StickySelection bool `json:"StickySelection" yaml:"StickySelection"`
MaxScanBufferSize int `json:"MaxScanBufferSize" yaml:"MaxScanBufferSize"`
FilterBufSize int `json:"FilterBufSize" yaml:"FilterBufSize"`
FuzzyLongestSort bool `json:"FuzzyLongestSort" yaml:"FuzzyLongestSort"`
SuppressStatusMsg bool `json:"SuppressStatusMsg" yaml:"SuppressStatusMsg"`
Color ColorMode `json:"Color" yaml:"Color"`
// If this is true, then the prefix for single key jump mode
// is displayed by default.
SingleKeyJump SingleKeyJumpConfig `json:"SingleKeyJump" yaml:"SingleKeyJump"`
// Use this prefix to denote currently selected line
SelectionPrefix string `json:"SelectionPrefix" yaml:"SelectionPrefix"`
// 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.
type SingleKeyJumpConfig struct {
ShowPrefix bool `json:"ShowPrefix" yaml:"ShowPrefix"`
}
// CustomFilterConfig is used to specify configuration parameters
// to CustomFilters
type CustomFilterConfig struct {
// Cmd is the name of the command to invoke
Cmd string `json:"Cmd" yaml:"Cmd"`
// TODO: need to check if how we use this is correct
Args []string `json:"Args" yaml:"Args"`
// BufferThreshold defines how many lines peco buffers before
// invoking the external command. If this value is big, we
// will execute the external command fewer times, but the
// results will not be generated for longer periods of time.
// If this value is small, we will execute the external command
// more often, but you pay the penalty of invoking that command
// more times.
BufferThreshold int `json:"BufferThreshold" yaml:"BufferThreshold"`
}
// DefaultPrompt is the default prompt string shown in the query line.
const DefaultPrompt = "QUERY>"
var homedirFunc = util.Homedir
// Init initializes the Config with default values
func (c *Config) Init() error {
c.Keymap = make(map[string]string)
c.Style.Init()
c.Prompt = DefaultPrompt
c.Layout = LayoutTypeTopDown
return nil
}
// ReadFilename reads the config from the given file, and
// does the appropriate processing, if any
func (c *Config) ReadFilename(filename string) error {
f, err := os.Open(filename)
if err != nil {
return fmt.Errorf("failed to open file %s: %w", filename, err)
}
defer f.Close()
switch ext := filepath.Ext(filename); ext {
case ".yaml", ".yml":
err = yaml.NewDecoder(f).Decode(c)
if err != nil {
return fmt.Errorf("failed to decode YAML: %w", err)
}
default:
err = json.NewDecoder(f).Decode(c)
if err != nil {
return fmt.Errorf("failed to decode JSON: %w", err)
}
}
if !IsValidLayoutType(c.Layout) {
return fmt.Errorf("invalid layout type: %s", c.Layout)
}
return nil
}
// Locator locates a config file in a given directory.
type Locator interface {
Locate(string) (string, error)
}
// LocatorFunc is a function that implements Locator.
type LocatorFunc func(string) (string, error)
// Locate calls the underlying function.
func (f LocatorFunc) Locate(dir string) (string, error) {
return f(dir)
}
var configFilenames = []string{"config.json", "config.yaml", "config.yml"}
// DefaultConfigLocator searches for a config file with one of the known
// filenames (config.json, config.yaml, config.yml) in the given directory.
var DefaultConfigLocator = LocatorFunc(func(dir string) (string, error) {
for _, basename := range configFilenames {
file := filepath.Join(dir, basename)
if _, err := os.Stat(file); err == nil {
return file, nil
}
}
return "", fmt.Errorf("config file not found in %s", dir)
})
// LocateRcfile attempts to find the config file in various locations
func LocateRcfile(locater Locator) (string, error) {
// http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
//
// Try in this order:
// $XDG_CONFIG_HOME/peco/config.{json,yaml,yml}
// $XDG_CONFIG_DIR/peco/config.{json,yaml,yml} (where XDG_CONFIG_DIR is listed in $XDG_CONFIG_DIRS)
// ~/.peco/config.{json,yaml,yml}
home, uErr := homedirFunc()
// Try dir supplied via env var
if dir := os.Getenv("XDG_CONFIG_HOME"); dir != "" {
if file, err := locater.Locate(filepath.Join(dir, "peco")); err == nil {
return file, nil
}
} else if uErr == nil { // silently ignore failure for homedir()
// Try "default" XDG location, is user is available
if file, err := locater.Locate(filepath.Join(home, ".config", "peco")); err == nil {
return file, nil
}
}
// this standard does not take into consideration windows (duh)
// while the spec says use ":" as the separator, Go provides us
// with filepath.ListSeparator, so use it
if dirs := os.Getenv("XDG_CONFIG_DIRS"); dirs != "" {
for dir := range strings.SplitSeq(dirs, fmt.Sprintf("%c", filepath.ListSeparator)) {
if file, err := locater.Locate(filepath.Join(dir, "peco")); err == nil {
return file, nil
}
}
}
if uErr == nil { // silently ignore failure for homedir()
if file, err := locater.Locate(filepath.Join(home, ".peco")); err == nil {
return file, nil
}
}
return "", errors.New("config file not found")
}

370
config/config_test.go Normal file
View file

@ -0,0 +1,370 @@
package config
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"github.com/goccy/go-yaml"
"github.com/stretchr/testify/require"
)
var expectedConfig = Config{
Keymap: map[string]string{
"C-j": "peco.Finish",
"C-x,C-c": "peco.Finish",
},
Layout: DefaultLayoutType,
Prompt: "[peco]",
Style: StyleSet{
Matched: Style{
Fg: ColorCyan | AttrBold,
Bg: ColorRed,
},
Query: Style{
Fg: ColorYellow | AttrBold,
Bg: ColorDefault,
},
Selected: Style{
Fg: ColorBlack | AttrUnderline,
Bg: ColorCyan,
},
SavedSelection: Style{
Fg: ColorBlack | AttrBold,
Bg: ColorCyan,
},
Prompt: Style{
Fg: ColorGreen | AttrBold,
Bg: ColorDefault,
},
Context: Style{
Fg: ColorDefault | AttrBold,
Bg: ColorDefault,
},
},
}
func TestReadRC(t *testing.T) {
txt := `
{
"Keymap": {
"C-j": "peco.Finish",
"C-x,C-c": "peco.Finish"
},
"Style": {
"Basic": ["on_default", "default"],
"Selected": ["underline", "on_cyan", "black"],
"Query": ["yellow", "bold"],
"Matched": ["cyan", "bold", "on_red"],
"Prompt": ["green", "bold"]
},
"Prompt": "[peco]"
}
`
var cfg Config
require.NoError(t, cfg.Init(), "Config.Init should succeed")
require.NoError(t, json.Unmarshal([]byte(txt), &cfg), "Unmarshalling config should succeed")
require.Equal(t, expectedConfig, cfg, "configuration matches expected")
}
func TestReadRCYAML(t *testing.T) {
txt := `
Keymap:
C-j: peco.Finish
"C-x,C-c": peco.Finish
Style:
Basic:
- on_default
- default
Selected:
- underline
- on_cyan
- black
Query:
- yellow
- bold
Matched:
- cyan
- bold
- on_red
Prompt:
- green
- bold
Prompt: "[peco]"
`
var cfg Config
require.NoError(t, cfg.Init(), "Config.Init should succeed")
require.NoError(t, yaml.Unmarshal([]byte(txt), &cfg), "Unmarshalling YAML config should succeed")
require.Equal(t, expectedConfig, cfg, "YAML configuration matches expected")
}
type stringsToStyleTest struct {
strings []string
style *Style
}
func TestStringsToStyle(t *testing.T) {
tests := []stringsToStyleTest{
{
strings: []string{"on_default", "default"},
style: &Style{Fg: ColorDefault, Bg: ColorDefault},
},
{
strings: []string{"bold", "on_blue", "yellow"},
style: &Style{Fg: ColorYellow | AttrBold, Bg: ColorBlue},
},
{
strings: []string{"underline", "on_cyan", "black"},
style: &Style{Fg: ColorBlack | AttrUnderline, Bg: ColorCyan},
},
{
strings: []string{"reverse", "on_red", "white"},
style: &Style{Fg: ColorWhite | AttrReverse, Bg: ColorRed},
},
{
strings: []string{"on_bold", "on_magenta", "green"},
style: &Style{Fg: ColorGreen, Bg: ColorMagenta | AttrBold},
},
{
strings: []string{"underline", "on_240", "214"},
style: &Style{Fg: Attribute(214+1) | AttrUnderline, Bg: Attribute(240 + 1)},
},
{
strings: []string{"#ff8800", "on_#0088ff"},
style: &Style{Fg: Attribute(0xff8800) | AttrTrueColor, Bg: Attribute(0x0088ff) | AttrTrueColor},
},
{
strings: []string{"bold", "#00ff00", "on_#000000"},
style: &Style{Fg: Attribute(0x00ff00) | AttrTrueColor | AttrBold, Bg: Attribute(0x000000) | AttrTrueColor},
},
{
strings: []string{"#000000"},
style: &Style{Fg: Attribute(0x000000) | AttrTrueColor, Bg: ColorDefault},
},
}
t.Logf("Checking strings -> color mapping...")
var a Style
for _, test := range tests {
t.Logf(" checking %s...", test.strings)
require.NoError(t, StringsToStyle(&a, test.strings), "StringsToStyle should succeed")
require.Equal(t, test.style, &a, "Expected '%s' to be '%#v', but got '%#v'", test.strings, test.style, a)
}
}
func TestLocateRcfile(t *testing.T) {
dir := t.TempDir()
homedirFunc = func() (string, error) {
return dir, nil
}
expected := []string{
filepath.Join(dir, "peco"),
filepath.Join(dir, "1", "peco"),
filepath.Join(dir, "2", "peco"),
filepath.Join(dir, "3", "peco"),
filepath.Join(dir, ".peco"),
}
i := 0
locater := LocatorFunc(func(dir string) (string, error) {
t.Logf("looking for file in %s", dir)
require.True(t, i <= len(expected)-1, "Got %d directories, only have %d", i+1, len(expected))
require.Equal(t, expected[i], dir, "Expected %s, got %s", expected[i], dir)
i++
return "", errors.New("error: Not found")
})
t.Setenv("XDG_CONFIG_HOME", dir)
t.Setenv("XDG_CONFIG_DIRS", strings.Join(
[]string{
filepath.Join(dir, "1"),
filepath.Join(dir, "2"),
filepath.Join(dir, "3"),
},
fmt.Sprintf("%c", filepath.ListSeparator),
))
LocateRcfile(locater)
expected[0] = filepath.Join(dir, ".config", "peco")
t.Setenv("XDG_CONFIG_HOME", "")
i = 0
LocateRcfile(locater)
}
func TestLocateRcfileYAML(t *testing.T) {
dir := t.TempDir()
// Create config.yaml (but not config.json) in the dir
pecoDir := filepath.Join(dir, ".peco")
require.NoError(t, os.MkdirAll(pecoDir, 0o755))
require.NoError(t, os.WriteFile(filepath.Join(pecoDir, "config.yaml"), []byte("{}"), 0o644))
homedirFunc = func() (string, error) {
return dir, nil
}
// Clear XDG vars so it falls through to ~/.peco/
t.Setenv("XDG_CONFIG_HOME", "")
t.Setenv("XDG_CONFIG_DIRS", "")
file, err := LocateRcfile(DefaultConfigLocator)
require.NoError(t, err)
require.Equal(t, filepath.Join(pecoDir, "config.yaml"), file)
}
func TestOnCancelBehavior(t *testing.T) {
t.Run("valid values via JSON", func(t *testing.T) {
for _, tc := range []struct {
input string
expected OnCancelBehavior
}{
{`{"OnCancel":"success"}`, OnCancelSuccess},
{`{"OnCancel":"error"}`, OnCancelError},
{`{}`, ""}, // absent key stays at zero value; default applied later in ApplyConfig
} {
var cfg Config
require.NoError(t, cfg.Init())
require.NoError(t, json.Unmarshal([]byte(tc.input), &cfg))
require.Equal(t, tc.expected, cfg.OnCancel)
}
})
t.Run("valid values via YAML", func(t *testing.T) {
for _, tc := range []struct {
input string
expected OnCancelBehavior
}{
{"OnCancel: success", OnCancelSuccess},
{"OnCancel: error", OnCancelError},
} {
var cfg Config
require.NoError(t, cfg.Init())
require.NoError(t, yaml.Unmarshal([]byte(tc.input), &cfg))
require.Equal(t, tc.expected, cfg.OnCancel)
}
})
t.Run("invalid value via JSON", func(t *testing.T) {
var cfg Config
require.NoError(t, cfg.Init())
err := json.Unmarshal([]byte(`{"OnCancel":"bogus"}`), &cfg)
require.Error(t, err)
require.Contains(t, err.Error(), "bogus")
})
t.Run("invalid value via YAML", func(t *testing.T) {
var cfg Config
require.NoError(t, cfg.Init())
err := yaml.Unmarshal([]byte("OnCancel: bogus"), &cfg)
require.Error(t, err)
require.Contains(t, err.Error(), "bogus")
})
}
func TestColorMode(t *testing.T) {
t.Run("valid values via JSON", func(t *testing.T) {
for _, tc := range []struct {
input string
expected ColorMode
}{
{`{"Color":"auto"}`, ColorModeAuto},
{`{"Color":"none"}`, ColorModeNone},
{`{}`, ""}, // absent key stays at zero value; default applied later in ApplyConfig
} {
var cfg Config
require.NoError(t, cfg.Init())
require.NoError(t, json.Unmarshal([]byte(tc.input), &cfg))
require.Equal(t, tc.expected, cfg.Color)
}
})
t.Run("valid values via YAML", func(t *testing.T) {
for _, tc := range []struct {
input string
expected ColorMode
}{
{"Color: auto", ColorModeAuto},
{"Color: none", ColorModeNone},
} {
var cfg Config
require.NoError(t, cfg.Init())
require.NoError(t, yaml.Unmarshal([]byte(tc.input), &cfg))
require.Equal(t, tc.expected, cfg.Color)
}
})
t.Run("invalid value via JSON", func(t *testing.T) {
var cfg Config
require.NoError(t, cfg.Init())
err := json.Unmarshal([]byte(`{"Color":"bogus"}`), &cfg)
require.Error(t, err)
require.Contains(t, err.Error(), "bogus")
})
t.Run("invalid value via YAML", func(t *testing.T) {
var cfg Config
require.NoError(t, cfg.Init())
err := yaml.Unmarshal([]byte("Color: bogus"), &cfg)
require.Error(t, err)
require.Contains(t, err.Error(), "bogus")
})
t.Run("UnmarshalFlag valid values", func(t *testing.T) {
var c ColorMode
require.NoError(t, c.UnmarshalFlag("auto"))
require.Equal(t, ColorModeAuto, c)
require.NoError(t, c.UnmarshalFlag("none"))
require.Equal(t, ColorModeNone, c)
require.NoError(t, c.UnmarshalFlag(""))
require.Equal(t, ColorModeAuto, c)
})
t.Run("UnmarshalFlag invalid value", func(t *testing.T) {
var c ColorMode
err := c.UnmarshalFlag("bogus")
require.Error(t, err)
require.Contains(t, err.Error(), "bogus")
})
}
func TestReadFilenameYAML(t *testing.T) {
dir := t.TempDir()
yamlFile := filepath.Join(dir, "config.yaml")
require.NoError(t, os.WriteFile(yamlFile, []byte(`
Keymap:
C-j: peco.Finish
"C-x,C-c": peco.Finish
Style:
Basic:
- on_default
- default
Selected:
- underline
- on_cyan
- black
Query:
- yellow
- bold
Matched:
- cyan
- bold
- on_red
Prompt:
- green
- bold
Prompt: "[peco]"
`), 0o644))
var cfg Config
require.NoError(t, cfg.Init())
require.NoError(t, cfg.ReadFilename(yamlFile))
require.Equal(t, expectedConfig, cfg)
}

78
config/height.go Normal file
View file

@ -0,0 +1,78 @@
package config
import (
"fmt"
"strconv"
"strings"
)
// HeightSpec represents a height specification that can be either
// an absolute number of lines or a percentage of terminal height.
type HeightSpec struct {
Value int
IsPercent bool
}
// ParseHeightSpec parses a height specification string.
// Valid formats: "10" (absolute lines), "50%" (percentage).
func ParseHeightSpec(s string) (HeightSpec, error) {
s = strings.TrimSpace(s)
if s == "" {
return HeightSpec{}, fmt.Errorf("empty height specification")
}
if strings.HasSuffix(s, "%") {
numStr := s[:len(s)-1]
if strings.Contains(numStr, "%") {
return HeightSpec{}, fmt.Errorf("invalid height specification: %q", s)
}
v, err := strconv.Atoi(numStr)
if err != nil {
return HeightSpec{}, fmt.Errorf("invalid height specification: %q", s)
}
if v <= 0 {
return HeightSpec{}, fmt.Errorf("height percentage must be positive: %q", s)
}
if v > 100 {
return HeightSpec{}, fmt.Errorf("height percentage must not exceed 100: %q", s)
}
return HeightSpec{Value: v, IsPercent: true}, nil
}
v, err := strconv.Atoi(s)
if err != nil {
return HeightSpec{}, fmt.Errorf("invalid height specification: %q", s)
}
if v <= 0 {
return HeightSpec{}, fmt.Errorf("height must be positive: %q", s)
}
return HeightSpec{Value: v, IsPercent: false}, nil
}
// ChromLines is the number of lines used by the prompt and status bar.
const ChromLines = 2
// Resolve converts the HeightSpec to an absolute number of screen rows,
// clamped to [ChromLines+1, termHeight].
//
// For absolute values, Value is the number of result lines — the prompt
// and status bar are added automatically (total = Value + 2).
// For percentages, Value is the percentage of the full terminal height
// (prompt and status bar are included in that total).
func (h HeightSpec) Resolve(termHeight int) int {
var height int
if h.IsPercent {
height = termHeight * h.Value / 100
} else {
height = h.Value + ChromLines
}
minHeight := ChromLines + 1 // at least 1 result line
if height < minHeight {
height = minHeight
}
if height > termHeight {
height = termHeight
}
return height
}

140
config/height_test.go Normal file
View file

@ -0,0 +1,140 @@
package config
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestParseHeightSpec(t *testing.T) {
t.Run("valid absolute", func(t *testing.T) {
spec, err := ParseHeightSpec("10")
require.NoError(t, err)
require.Equal(t, 10, spec.Value)
require.False(t, spec.IsPercent)
})
t.Run("valid percentage", func(t *testing.T) {
spec, err := ParseHeightSpec("50%")
require.NoError(t, err)
require.Equal(t, 50, spec.Value)
require.True(t, spec.IsPercent)
})
t.Run("valid 100%", func(t *testing.T) {
spec, err := ParseHeightSpec("100%")
require.NoError(t, err)
require.Equal(t, 100, spec.Value)
require.True(t, spec.IsPercent)
})
t.Run("valid with whitespace", func(t *testing.T) {
spec, err := ParseHeightSpec(" 10 ")
require.NoError(t, err)
require.Equal(t, 10, spec.Value)
require.False(t, spec.IsPercent)
})
t.Run("invalid empty", func(t *testing.T) {
_, err := ParseHeightSpec("")
require.Error(t, err)
})
t.Run("invalid whitespace only", func(t *testing.T) {
_, err := ParseHeightSpec(" ")
require.Error(t, err)
})
t.Run("invalid abc", func(t *testing.T) {
_, err := ParseHeightSpec("abc")
require.Error(t, err)
})
t.Run("invalid negative", func(t *testing.T) {
_, err := ParseHeightSpec("-5")
require.Error(t, err)
})
t.Run("invalid zero", func(t *testing.T) {
_, err := ParseHeightSpec("0")
require.Error(t, err)
})
t.Run("invalid zero percent", func(t *testing.T) {
_, err := ParseHeightSpec("0%")
require.Error(t, err)
})
t.Run("invalid double percent", func(t *testing.T) {
_, err := ParseHeightSpec("50%%")
require.Error(t, err)
})
t.Run("invalid percent over 100", func(t *testing.T) {
_, err := ParseHeightSpec("150%")
require.Error(t, err)
})
}
func TestHeightSpecResolve(t *testing.T) {
// For absolute values: Value is result lines, total = Value + ChromLines(2)
t.Run("absolute adds chrome", func(t *testing.T) {
spec := HeightSpec{Value: 10, IsPercent: false}
// 10 result lines + 2 chrome = 12 total
require.Equal(t, 12, spec.Resolve(24))
})
t.Run("absolute 1 result line", func(t *testing.T) {
spec := HeightSpec{Value: 1, IsPercent: false}
// 1 result line + 2 chrome = 3 total
require.Equal(t, 3, spec.Resolve(24))
})
t.Run("absolute 2 result lines", func(t *testing.T) {
spec := HeightSpec{Value: 2, IsPercent: false}
// 2 result lines + 2 chrome = 4 total
require.Equal(t, 4, spec.Resolve(24))
})
t.Run("absolute clamped to terminal height", func(t *testing.T) {
spec := HeightSpec{Value: 50, IsPercent: false}
// 50 + 2 = 52, clamped to 24
require.Equal(t, 24, spec.Resolve(24))
})
t.Run("absolute clamped to minimum", func(t *testing.T) {
// This shouldn't happen via ParseHeightSpec (rejects <= 0),
// but Resolve handles it defensively.
spec := HeightSpec{Value: 0, IsPercent: false}
// 0 + 2 = 2, clamped to min 3
require.Equal(t, ChromLines+1, spec.Resolve(24))
})
// For percentages: Value is percentage of total terminal height
t.Run("percentage", func(t *testing.T) {
spec := HeightSpec{Value: 50, IsPercent: true}
require.Equal(t, 12, spec.Resolve(24))
})
t.Run("percentage 100%", func(t *testing.T) {
spec := HeightSpec{Value: 100, IsPercent: true}
require.Equal(t, 24, spec.Resolve(24))
})
t.Run("percentage clamp to min", func(t *testing.T) {
spec := HeightSpec{Value: 1, IsPercent: true}
// 1% of 24 = 0, clamped to ChromLines+1 = 3
require.Equal(t, ChromLines+1, spec.Resolve(24))
})
t.Run("percentage clamp to terminal height", func(t *testing.T) {
spec := HeightSpec{Value: 100, IsPercent: true}
require.Equal(t, 5, spec.Resolve(5))
})
t.Run("small terminal clamps absolute", func(t *testing.T) {
spec := HeightSpec{Value: 10, IsPercent: false}
// 10 + 2 = 12, clamped to 3
require.Equal(t, 3, spec.Resolve(3))
})
}

24
config/layout.go Normal file
View file

@ -0,0 +1,24 @@
package config
// LayoutType describes the types of layout that peco can take
type LayoutType = string
const (
DefaultLayoutType = LayoutTypeTopDown // LayoutTypeTopDown makes the layout so the items read from top to bottom
LayoutTypeTopDown = "top-down" // LayoutTypeTopDown displays prompt at top, list top-to-bottom
LayoutTypeBottomUp = "bottom-up" // LayoutTypeBottomUp displays prompt at bottom, list bottom-to-top
LayoutTypeTopDownQueryBottom = "top-down-query-bottom" // LayoutTypeTopDownQueryBottom displays list top-to-bottom, prompt at bottom
)
// validLayoutTypes enumerates all recognized layout type values.
var validLayoutTypes = map[LayoutType]struct{}{
LayoutTypeTopDown: {},
LayoutTypeBottomUp: {},
LayoutTypeTopDownQueryBottom: {},
}
// IsValidLayoutType checks if a string is a supported layout type
func IsValidLayoutType(v LayoutType) bool {
_, ok := validLayoutTypes[v]
return ok
}

184
config/style.go Normal file
View file

@ -0,0 +1,184 @@
package config
import (
"encoding/json"
"fmt"
"strconv"
"strings"
)
// StyleSet holds styles for various sections
type StyleSet struct {
Basic Style `json:"Basic" yaml:"Basic"`
SavedSelection Style `json:"SavedSelection" yaml:"SavedSelection"`
Selected Style `json:"Selected" yaml:"Selected"`
Query Style `json:"Query" yaml:"Query"`
QueryCursor Style `json:"QueryCursor" yaml:"QueryCursor"`
Matched Style `json:"Matched" yaml:"Matched"`
Prompt Style `json:"Prompt" yaml:"Prompt"`
Context Style `json:"Context" yaml:"Context"`
}
// Attribute represents terminal display attributes such as colors
// and text styling (bold, underline, reverse). It is a uint32 bitfield:
//
// Bits 0-8: Palette color index (0=default, 1-256 for 256-color palette)
// Bits 0-23: RGB color value (when AttrTrueColor flag is set)
// Bit 24: AttrTrueColor flag — distinguishes true color from palette
// Bit 25: AttrBold
// Bit 26: AttrUnderline
// Bit 27: AttrReverse
// Bits 28-31: Reserved
type Attribute uint32
// Named palette color constants (values 0-8).
const (
ColorDefault Attribute = 0x0000
ColorBlack Attribute = 0x0001
ColorRed Attribute = 0x0002
ColorGreen Attribute = 0x0003
ColorYellow Attribute = 0x0004
ColorBlue Attribute = 0x0005
ColorMagenta Attribute = 0x0006
ColorCyan Attribute = 0x0007
ColorWhite Attribute = 0x0008
)
const (
AttrTrueColor Attribute = 0x01000000
AttrBold Attribute = 0x02000000
AttrUnderline Attribute = 0x04000000
AttrReverse Attribute = 0x08000000
)
// Style describes display attributes for foreground and background.
type Style struct {
Fg Attribute
Bg Attribute
}
var (
StringToFg = map[string]Attribute{
"default": ColorDefault,
"black": ColorBlack,
"red": ColorRed,
"green": ColorGreen,
"yellow": ColorYellow,
"blue": ColorBlue,
"magenta": ColorMagenta,
"cyan": ColorCyan,
"white": ColorWhite,
}
StringToBg = map[string]Attribute{
"on_default": ColorDefault,
"on_black": ColorBlack,
"on_red": ColorRed,
"on_green": ColorGreen,
"on_yellow": ColorYellow,
"on_blue": ColorBlue,
"on_magenta": ColorMagenta,
"on_cyan": ColorCyan,
"on_white": ColorWhite,
}
StringToFgAttr = map[string]Attribute{
"bold": AttrBold,
"underline": AttrUnderline,
"reverse": AttrReverse,
}
StringToBgAttr = map[string]Attribute{
"on_bold": AttrBold,
}
)
// NewStyleSet creates a new StyleSet struct
func NewStyleSet() *StyleSet {
ss := &StyleSet{}
ss.Init()
return ss
}
// Init initializes the StyleSet with default foreground and background colors
// for each UI element (basic, query, matched, selected, prompt, context, etc.).
func (ss *StyleSet) Init() {
ss.Basic.Fg = ColorDefault
ss.Basic.Bg = ColorDefault
ss.Query.Fg = ColorDefault
ss.Query.Bg = ColorDefault
ss.Matched.Fg = ColorCyan
ss.Matched.Bg = ColorDefault
ss.SavedSelection.Fg = ColorBlack | AttrBold
ss.SavedSelection.Bg = ColorCyan
ss.Selected.Fg = ColorDefault | AttrUnderline
ss.Selected.Bg = ColorMagenta
ss.Prompt.Fg = ColorDefault
ss.Prompt.Bg = ColorDefault
ss.Context.Fg = ColorDefault | AttrBold
ss.Context.Bg = ColorDefault
}
// UnmarshalJSON satisfies json.RawMessage.
func (s *Style) UnmarshalJSON(buf []byte) error {
raw := []string{}
if err := json.Unmarshal(buf, &raw); err != nil {
return fmt.Errorf("failed to unmarshal Style: %w", err)
}
return StringsToStyle(s, raw)
}
// UnmarshalYAML decodes a YAML array of strings into a Style.
func (s *Style) UnmarshalYAML(unmarshal func(any) error) error {
var raw []string
if err := unmarshal(&raw); err != nil {
return fmt.Errorf("failed to unmarshal Style from YAML: %w", err)
}
return StringsToStyle(s, raw)
}
// StringsToStyle parses an array of color and attribute strings (e.g. "red",
// "on_blue", "bold", "#ff00ff") into a Style's foreground and background Attributes.
func StringsToStyle(style *Style, raw []string) error {
style.Fg = ColorDefault
style.Bg = ColorDefault
for _, s := range raw {
fg, ok := StringToFg[s]
if ok {
style.Fg = fg
} else if strings.HasPrefix(s, "#") && len(s) == 7 {
if rgb, err := strconv.ParseUint(s[1:], 16, 32); err == nil {
style.Fg = Attribute(rgb) | AttrTrueColor
}
} else {
if fg, err := strconv.ParseUint(s, 10, 8); err == nil {
style.Fg = Attribute(fg + 1)
}
}
bg, ok := StringToBg[s]
if ok {
style.Bg = bg
} else if strings.HasPrefix(s, "on_#") && len(s) == 10 {
if rgb, err := strconv.ParseUint(s[4:], 16, 32); err == nil {
style.Bg = Attribute(rgb) | AttrTrueColor
}
} else {
if strings.HasPrefix(s, "on_") {
if bg, err := strconv.ParseUint(s[3:], 10, 8); err == nil {
style.Bg = Attribute(bg + 1)
}
}
}
}
for _, s := range raw {
if fgAttr, ok := StringToFgAttr[s]; ok {
style.Fg |= fgAttr
}
if bgAttr, ok := StringToBgAttr[s]; ok {
style.Bg |= bgAttr
}
}
return nil
}

View file

@ -1,15 +0,0 @@
package peco
import (
"fmt"
"os"
)
func homedir() (string, error) {
home := os.Getenv("HOME")
if home == "" {
return "", fmt.Errorf("Environment variable HOME not set")
}
return home, nil
}

View file

@ -1,17 +0,0 @@
// +build !darwin,!windows
package peco
import (
"fmt"
"os"
)
func homedir() (string, error) {
home := os.Getenv("HOME")
if home == "" {
return "", fmt.Errorf("Environment variable HOME not set")
}
return home, nil
}

View file

@ -1,120 +0,0 @@
package peco
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
"github.com/nsf/termbox-go"
)
func TestReadRC(t *testing.T) {
txt := `
{
"Keymap": {
"C-j": "peco.Finish",
"C-x,C-c": "peco.Finish"
},
"Style": {
"Basic": ["on_default", "default"],
"Selected": ["underline", "on_cyan", "black"],
"Query": ["yellow", "bold"],
"Matched": ["cyan", "bold", "on_red"]
},
"Prompt": "[peco]"
}
`
cfg := NewConfig()
if err := json.Unmarshal([]byte(txt), cfg); err != nil {
t.Fatalf("Error unmarshaling json: %s", err)
}
t.Logf("%#q", cfg)
}
type stringsToStyleTest struct {
strings []string
style *Style
}
func TestStringsToStyle(t *testing.T) {
tests := []stringsToStyleTest{
stringsToStyleTest{
strings: []string{"on_default", "default"},
style: &Style{fg: termbox.ColorDefault, bg: termbox.ColorDefault},
},
stringsToStyleTest{
strings: []string{"bold", "on_blue", "yellow"},
style: &Style{fg: termbox.ColorYellow | termbox.AttrBold, bg: termbox.ColorBlue},
},
stringsToStyleTest{
strings: []string{"underline", "on_cyan", "black"},
style: &Style{fg: termbox.ColorBlack | termbox.AttrUnderline, bg: termbox.ColorCyan},
},
stringsToStyleTest{
strings: []string{"blink", "on_red", "white"},
style: &Style{fg: termbox.ColorWhite | termbox.AttrReverse, bg: termbox.ColorRed},
},
}
t.Logf("Checking strings -> color mapping...")
for _, test := range tests {
t.Logf(" checking %s...", test.strings)
if a := stringsToStyle(test.strings); *a != *test.style {
t.Errorf("Expected '%s' to be '%#v', but got '%#v'", test.strings, test.style, a)
}
}
}
func TestLocateRcfile(t *testing.T) {
dir, err := ioutil.TempDir("", "peco-")
if err != nil {
t.Fatalf("Failed to create temporary directory: %s", err)
}
homedirFunc = func() (string, error) {
return dir, nil
}
expected := []string{
filepath.Join(dir, "peco"),
filepath.Join(dir, "1", "peco"),
filepath.Join(dir, "2", "peco"),
filepath.Join(dir, "3", "peco"),
filepath.Join(dir, ".peco"),
}
i := 0
_locateRcfileIn = func(dir string) (string, error) {
t.Logf("looking for file in %s", dir)
if i > len(expected)-1 {
t.Fatalf("Got %d directories, only have %d", i+1, len(expected))
}
if expected[i] != dir {
t.Errorf("Expected %s, got %s", expected[i], dir)
}
i++
return "", fmt.Errorf("Not found")
}
os.Setenv("XDG_CONFIG_HOME", dir)
os.Setenv("XDG_CONFIG_DIRS", strings.Join(
[]string{
filepath.Join(dir, "1"),
filepath.Join(dir, "2"),
filepath.Join(dir, "3"),
},
fmt.Sprintf("%c", filepath.ListSeparator),
))
LocateRcfile()
expected[0] = filepath.Join(dir, ".config", "peco")
os.Setenv("XDG_CONFIG_HOME", "")
i = 0
LocateRcfile()
}

10
contrib/man/generate.sh Executable file
View file

@ -0,0 +1,10 @@
#!/bin/sh
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
pandoc -s -f markdown -t man \
-V title=PECO -V section=1 \
-V header="User Commands" \
-V footer="peco" \
-o "$SCRIPT_DIR/peco.1" \
"$REPO_ROOT/README.md"

1955
contrib/man/peco.1 Normal file

File diff suppressed because it is too large Load diff

335
ctx.go
View file

@ -1,335 +0,0 @@
package peco
import (
"fmt"
"io"
"os"
"os/signal"
"sort"
"sync"
"syscall"
)
type CtxOptions interface {
EnableNullSep() bool
BufferSize() int
InitialIndex() int
}
type Selection []int
func (s Selection) Has(v int) bool {
for _, i := range []int(s) {
if i == v {
return true
}
}
return false
}
func (s *Selection) Add(v int) {
if s.Has(v) {
return
}
*s = Selection(append([]int(*s), v))
sort.Sort(s)
}
func (s *Selection) Remove(v int) {
a := []int(*s)
for k, i := range a {
if i == v {
tmp := a[:k]
tmp = append(tmp, a[k+1:]...)
*s = Selection(tmp)
return
}
}
}
func (s *Selection) Clear() {
*s = Selection([]int{})
}
func (s Selection) Len() int {
return len(s)
}
func (s Selection) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s Selection) Less(i, j int) bool {
return s[i] < s[j]
}
// Ctx contains all the important data. while you can easily access
// data in this struct from anwyehre, only do so via channels
type Ctx struct {
enableSep bool
result []Match
loopCh chan struct{}
queryCh chan string
drawCh chan []Match
statusMsgCh chan string
pagingCh chan PagingRequest
mutex sync.Mutex
query []rune
prompt []rune
caretPos int
currentLine int
currentPage struct { index, offset, perPage int }
selection Selection
lines []Match
current []Match
bufferSize int
config *Config
Matchers []Matcher
CurrentMatcher int
ExitStatus int
selectionRangeStart int
wait *sync.WaitGroup
}
func NewCtx(o CtxOptions) *Ctx {
return &Ctx{
o.EnableNullSep(),
[]Match{},
make(chan struct{}), // loopCh. You never send messages to this. no point in buffering
make(chan string, 5), // queryCh.
make(chan []Match, 5), // drawCh.
make(chan string, 5), // statusMsgCh
make(chan PagingRequest, 5), // pagingCh
sync.Mutex{},
[]rune{},
[]rune{},
0,
o.InitialIndex(),
struct { index, offset, perPage int } { 0, 1, 0 },
Selection([]int{}),
[]Match{},
nil,
o.BufferSize(),
NewConfig(),
[]Matcher{
NewIgnoreCaseMatcher(o.EnableNullSep()),
NewCaseSensitiveMatcher(o.EnableNullSep()),
NewRegexpMatcher(o.EnableNullSep()),
},
0,
0,
NoSelectionRange,
&sync.WaitGroup{},
}
}
const NoSelectionRange = -1
func (c *Ctx) ReadConfig(file string) error {
if err := c.config.ReadFilename(file); err != nil {
return err
}
if err := c.LoadCustomMatcher(); err != nil {
return err
}
c.SetCurrentMatcher(c.config.Matcher)
return nil
}
func (c *Ctx) IsBufferOverflowing() bool {
if c.bufferSize <= 0 {
return false
}
return len(c.lines) > c.bufferSize
}
func (c *Ctx) IsRangeMode() bool {
return c.selectionRangeStart != NoSelectionRange
}
func (c *Ctx) SelectedRange() Selection {
if !c.IsRangeMode() {
return Selection{}
}
selectedLines := []int{}
if c.selectionRangeStart < c.currentLine {
for i := c.selectionRangeStart; i < c.currentLine; i++ {
selectedLines = append(selectedLines, i)
}
} else {
for i := c.selectionRangeStart; i > c.currentLine; i-- {
selectedLines = append(selectedLines, i)
}
}
return Selection(selectedLines)
}
func (c *Ctx) Result() []Match {
return c.result
}
func (c *Ctx) AddWaitGroup(v int) {
c.wait.Add(v)
}
func (c *Ctx) ReleaseWaitGroup() {
c.wait.Done()
}
func (c *Ctx) WaitDone() {
c.wait.Wait()
}
func (c *Ctx) LoopCh() chan struct{} {
return c.loopCh
}
func (c *Ctx) QueryCh() chan string {
return c.queryCh
}
func (c *Ctx) DrawCh() chan []Match {
return c.drawCh
}
func (c *Ctx) StatusMsgCh() chan string {
return c.statusMsgCh
}
func (c *Ctx) PagingCh() chan PagingRequest {
return c.pagingCh
}
func (c *Ctx) Terminate() {
close(c.loopCh)
}
func (c *Ctx) ExecQuery() bool {
if len(c.query) > 0 {
c.queryCh <- string(c.query)
return true
}
return false
}
func (c *Ctx) DrawMatches(m []Match) {
c.drawCh <- m
}
func (c *Ctx) Refresh() {
c.DrawMatches(nil)
}
func (c *Ctx) Buffer() []Match {
// Copy lines so it's safe to read it
lcopy := make([]Match, len(c.lines))
copy(lcopy, c.lines)
return lcopy
}
func (c *Ctx) NewBufferReader(r io.ReadCloser) *BufferReader {
return &BufferReader{c, r, make(chan struct{})}
}
func (c *Ctx) NewView() *View {
return &View{c}
}
func (c *Ctx) NewFilter() *Filter {
return &Filter{c, make(chan string)}
}
func (c *Ctx) NewInput() *Input {
// Create a new keymap object
k := NewKeymap(c.config.Keymap)
k.ApplyKeybinding()
return &Input{c, &sync.Mutex{}, nil, k}
}
func (c *Ctx) Stop() {
close(c.LoopCh())
}
func (c *Ctx) SetQuery(q []rune) {
c.query = q
c.caretPos = len(q)
}
func (c *Ctx) Matcher() Matcher {
return c.Matchers[c.CurrentMatcher]
}
func (c *Ctx) AddMatcher(m Matcher) error {
if err := m.Verify(); err != nil {
return fmt.Errorf("Verification for custom matcher failed: %s", err)
}
c.Matchers = append(c.Matchers, m)
return nil
}
func (c *Ctx) SetCurrentMatcher(n string) bool {
for i, m := range c.Matchers {
if m.String() == n {
c.CurrentMatcher = i
return true
}
}
return false
}
func (c *Ctx) LoadCustomMatcher() error {
if len(c.config.CustomMatcher) == 0 {
return nil
}
for name, args := range c.config.CustomMatcher {
if err := c.AddMatcher(NewCustomMatcher(c.enableSep, name, args)); err != nil {
return err
}
}
return nil
}
func (c *Ctx) ExitWith(i int) {
c.ExitStatus = i
c.Stop()
}
type SignalHandler struct {
*Ctx
sigCh chan os.Signal
}
func (c *Ctx) NewSignalHandler() *SignalHandler {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
return &SignalHandler{c, sigCh}
}
func (s *SignalHandler) Loop() {
defer s.ReleaseWaitGroup()
for {
select {
case <-s.LoopCh():
return
case <-s.sigCh:
// XXX For future reference: DO NOT, and I mean DO NOT call
// termbox.Close() here. Calling termbox.Close() twice in our
// context actually BLOCKS. Can you believe it? IT BLOCKS.
//
// So if we called termbox.Close() here, and then in main()
// defer termbox.Close() blocks. Not cool.
s.ExitWith(1)
return
}
}
}
func (c *Ctx) SetPrompt(p []rune) {
c.prompt = p
}

23
event.go Normal file
View file

@ -0,0 +1,23 @@
package peco
import "github.com/peco/peco/internal/keyseq"
// EventType classifies the type of terminal event.
type EventType uint8
const (
// EventKey is a keyboard event
EventKey EventType = iota
// EventResize is a terminal resize event
EventResize
// EventError is an error event
EventError
)
// Event is peco's internal event type, decoupled from any terminal library.
type Event struct {
Type EventType
Key keyseq.KeyType
Ch rune
Mod keyseq.ModifierKey
}

573
filter.go
View file

@ -1,41 +1,564 @@
package peco
type Filter struct {
*Ctx
jobs chan string
import (
"context"
"runtime"
"strings"
"sync"
"time"
"github.com/lestrrat-go/pdebug"
"github.com/peco/peco/filter"
"github.com/peco/peco/hub"
"github.com/peco/peco/internal/buffer"
"github.com/peco/peco/line"
"github.com/peco/peco/pipeline"
)
// FilterErrorHandler handles errors that occur during filter execution.
type FilterErrorHandler interface {
HandleError(error)
}
func (f *Filter) Work(cancel chan struct{}, q string) {
if q == "" {
f.DrawMatches(nil)
// FilterErrorHandlerFunc is a function that implements FilterErrorHandler.
type FilterErrorHandlerFunc func(error)
// HandleError calls the underlying function.
func (f FilterErrorHandlerFunc) HandleError(err error) {
f(err)
}
// nopFilterErrorHandler is a FilterErrorHandler that silently discards errors.
var nopFilterErrorHandler = FilterErrorHandlerFunc(func(error) {})
// Filter is responsible for the actual "grep" part of peco
type Filter struct {
state *Peco
prevQuery string
prevResults *MemoryBuffer
prevFilterName string
prevFrozenSrc *MemoryBuffer // frozen source at cache time
prevMu sync.Mutex
}
type filterProcessor struct {
filter filter.Filter
query string
bufSize int
errorHandler FilterErrorHandler
}
// 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, eh FilterErrorHandler) *filterProcessor {
return &filterProcessor{
filter: f,
query: q,
bufSize: bufSize,
errorHandler: eh,
}
}
// Accept receives lines from the pipeline, applies the filter, and forwards matches.
func (fp *filterProcessor) Accept(ctx context.Context, in <-chan line.Line, out pipeline.ChanOutput) {
acceptAndFilter(ctx, fp.filter, fp.bufSize, fp.errorHandler, in, out)
}
// orderedChunk is a batch of lines tagged with a sequence number
// so that parallel filter results can be merged back in order.
type orderedChunk struct {
seq int
lines []line.Line
}
// orderedResult is a filtered chunk tagged with the original sequence number.
type orderedResult struct {
seq int
matched []line.Line
}
// reportFilterError reports a filter error to the given handler.
// If the context is already cancelled or err is nil, it does nothing.
func reportFilterError(ctx context.Context, err error, eh FilterErrorHandler) {
if err == nil || ctx.Err() != nil {
return
}
f.current = f.Matcher().Match(cancel, q, f.Buffer())
f.StatusMsgCh() <- ""
f.selection.Clear()
f.DrawMatches(nil)
eh.HandleError(err)
}
func (f *Filter) Loop() {
defer f.ReleaseWaitGroup()
// flusher is the single-threaded fallback used when the filter does not
// support parallel execution (e.g. Fuzzy with sortLongest).
func flusher(ctx context.Context, f filter.Filter, incoming chan []line.Line, done chan struct{}, out pipeline.ChanOutput, eh FilterErrorHandler) {
if pdebug.Enabled {
g := pdebug.Marker("flusher goroutine")
defer g.End()
}
defer close(done)
defer close(out)
// previous holds a channel that can cancel the previous
// query. This is used when multiple queries come in succession
// and the previous query is discarded anyway
var previous chan struct{}
for {
select {
case <-f.LoopCh():
case <-ctx.Done():
return
case q := <-f.QueryCh():
if previous != nil {
// Tell the previous query to stop
previous <- struct{}{}
case buf, ok := <-incoming:
if !ok {
return
}
previous = make(chan struct{}, 1)
f.StatusMsgCh() <- "Running query..."
go f.Work(previous, q)
if pdebug.Enabled {
pdebug.Printf("flusher: %#v", buf)
}
if err := f.Apply(ctx, buf, out); err != nil {
reportFilterError(ctx, err, eh)
}
buffer.ReleaseLineListBuf(buf)
}
}
}
// parallelFlusher distributes filter work across multiple goroutines
// and merges the results back in sequence order.
func parallelFlusher(ctx context.Context, f filter.Filter, incoming chan orderedChunk, done chan struct{}, out pipeline.ChanOutput, eh FilterErrorHandler) {
if pdebug.Enabled {
g := pdebug.Marker("parallelFlusher goroutine")
defer g.End()
}
defer close(done)
defer close(out)
numWorkers := max(runtime.GOMAXPROCS(0), 1)
// workCh distributes chunks to workers
workCh := make(chan orderedChunk, numWorkers*2)
// resultCh collects filtered results from workers
resultCh := make(chan orderedResult, numWorkers*2)
// Check once whether the filter supports direct collection (bypasses
// per-chunk channel allocation and goroutine spawn).
collector, canCollect := f.(filter.Collector)
// Start workers
var workerWg sync.WaitGroup
workerWg.Add(numWorkers)
for range numWorkers {
go func() {
defer workerWg.Done()
for chunk := range workCh {
select {
case <-ctx.Done():
buffer.ReleaseLineListBuf(chunk.lines)
continue
default:
}
var matched []line.Line
if canCollect {
// Fast path: collect results directly into a slice
var err error
matched, err = collector.ApplyCollect(ctx, chunk.lines)
if err != nil {
reportFilterError(ctx, err, eh)
}
} else {
// Fallback: use channel-based Apply for filters that
// don't implement Collector (e.g. ExternalCmd)
collectCh := make(chan line.Line, len(chunk.lines))
go func(chunk orderedChunk) {
if err := f.Apply(ctx, chunk.lines, pipeline.ChanOutput(collectCh)); err != nil {
reportFilterError(ctx, err, eh)
}
close(collectCh)
}(chunk)
matched = make([]line.Line, 0, len(chunk.lines)/2)
for l := range collectCh {
matched = append(matched, l)
}
}
buffer.ReleaseLineListBuf(chunk.lines)
select {
case resultCh <- orderedResult{seq: chunk.seq, matched: matched}:
case <-ctx.Done():
}
}
}()
}
// Close resultCh when all workers are done
go func() {
workerWg.Wait()
close(resultCh)
}()
// Merger goroutine: reorder results and send to out in sequence order
mergerDone := make(chan struct{})
go func() {
defer close(mergerDone)
pending := make(map[int]orderedResult)
nextSeq := 0
for result := range resultCh {
pending[result.seq] = result
// Flush as many in-order results as possible
for {
r, ok := pending[nextSeq]
if !ok {
break
}
delete(pending, nextSeq)
nextSeq++
for _, l := range r.matched {
if err := out.Send(ctx, l); err != nil {
return
}
}
}
}
// Flush any remaining (shouldn't happen if producers are well-behaved)
for seq := nextSeq; ; seq++ {
r, ok := pending[seq]
if !ok {
break
}
for _, l := range r.matched {
if err := out.Send(ctx, l); err != nil {
return
}
}
}
}()
// Dispatch incoming chunks to workers
for chunk := range incoming {
select {
case <-ctx.Done():
buffer.ReleaseLineListBuf(chunk.lines)
case workCh <- chunk:
}
}
close(workCh)
// Wait for merger to finish
<-mergerDone
}
// AcceptAndFilter is the exported entry point for the filter pipeline stage.
// It batches incoming lines and dispatches them to the filter, using parallel
// workers when the filter supports it.
func AcceptAndFilter(ctx context.Context, f filter.Filter, configBufSize int, in <-chan line.Line, out pipeline.ChanOutput) {
acceptAndFilter(ctx, f, configBufSize, nopFilterErrorHandler, 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, eh FilterErrorHandler, in <-chan line.Line, out pipeline.ChanOutput) {
useParallel := f.SupportsParallel() && runtime.GOMAXPROCS(0) > 1
buf := buffer.GetLineListBuf()
bufsiz := f.BufSize()
if bufsiz <= 0 {
if configBufSize > 0 {
bufsiz = configBufSize
} else {
bufsiz = cap(buf)
}
}
if useParallel {
acceptAndFilterParallel(ctx, f, bufsiz, buf, eh, in, out)
} else {
acceptAndFilterSerial(ctx, f, bufsiz, buf, eh, in, out)
}
}
// 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, eh FilterErrorHandler, in <-chan line.Line, out pipeline.ChanOutput) {
flush := make(chan []line.Line)
flushDone := make(chan struct{})
go flusher(ctx, f, flush, flushDone, out, eh)
defer func() { <-flushDone }()
defer close(flush)
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, eh FilterErrorHandler, in <-chan line.Line, out pipeline.ChanOutput) {
flush := make(chan orderedChunk)
flushDone := make(chan struct{})
go parallelFlusher(ctx, f, flush, flushDone, out, eh)
defer func() { <-flushDone }()
defer close(flush)
seq := 0
batchAndFlush(ctx, bufsiz, buf, in, flush, func(b []line.Line) orderedChunk {
chunk := orderedChunk{seq: seq, lines: b}
seq++
return chunk
})
}
// batchAndFlush reads lines from in, batches them into slices of up to bufsiz,
// and sends each batch to flushCh via the wrap function. Batches are flushed
// when full or every 50ms, whichever comes first.
func batchAndFlush[T any](ctx context.Context, bufsiz int, buf []line.Line, in <-chan line.Line, flushCh chan T, wrap func([]line.Line) T) {
flushTicker := time.NewTicker(50 * time.Millisecond)
defer flushTicker.Stop()
start := time.Now()
lines := 0
for {
select {
case <-ctx.Done():
if pdebug.Enabled {
pdebug.Printf("filter received done")
}
return
case <-flushTicker.C:
if len(buf) > 0 {
flushCh <- wrap(buf)
buf = buffer.GetLineListBuf()
}
case v, ok := <-in:
if !ok {
if pdebug.Enabled {
pdebug.Printf("filter input closed (read %d lines, %s since starting accept loop)", lines+len(buf), time.Since(start).String())
}
if len(buf) > 0 {
flushCh <- wrap(buf)
}
return
}
if pdebug.Enabled {
pdebug.Printf("incoming line")
lines++
}
buf = append(buf, v)
if len(buf) >= bufsiz {
flushCh <- wrap(buf)
buf = buffer.GetLineListBuf()
}
}
}
}
// NewFilter creates a new Filter bound to the given Peco state.
func NewFilter(state *Peco) *Filter {
return &Filter{
state: state,
}
}
// isQueryRefinement returns true if newQuery is a refinement of prevQuery,
// meaning the new query can only produce a subset of the previous results.
// With negative terms, refinement requires:
// 1. Positive portion of prev is a prefix of positive portion of new
// 2. All previous negative terms are still present in new
// 3. New query may have additional positive or negative terms
func isQueryRefinement(prev, cur string) bool {
prev = strings.TrimSpace(prev)
cur = strings.TrimSpace(cur)
if prev == "" || cur == "" {
return false
}
prevPos, prevNeg := filter.SplitQueryTerms(prev)
newPos, newNeg := filter.SplitQueryTerms(cur)
// Positive portion: the joined prev positive terms must be a prefix of the joined new positive terms
prevPosStr := strings.Join(prevPos, " ")
newPosStr := strings.Join(newPos, " ")
if prevPosStr != "" && !strings.HasPrefix(newPosStr, prevPosStr) {
return false
}
// All previous negative terms must still be present in new negative terms
if len(prevNeg) > 0 {
newNegSet := make(map[string]struct{}, len(newNeg))
for _, t := range newNeg {
newNegSet[t] = struct{}{}
}
for _, t := range prevNeg {
if _, ok := newNegSet[t]; !ok {
return false
}
}
}
// At least one positive or negative term must exist in both
if len(prevPos) == 0 && len(prevNeg) == 0 {
return false
}
return true
}
// Work is the actual work horse that does the matching
// in a goroutine of its own. It wraps Matcher.Match().
func (f *Filter) Work(ctx context.Context, q *hub.Payload[string]) {
defer q.Done()
query := q.Data()
if pdebug.Enabled {
g := pdebug.Marker("Filter.Work (query=%#v, batch=%#v)", query, q.Batch())
defer g.End()
}
state := f.state
if query == "" {
f.prevMu.Lock()
f.prevQuery = ""
f.prevResults = nil
f.prevFilterName = ""
f.prevFrozenSrc = nil
f.prevMu.Unlock()
state.ResetCurrentLineBuffer(ctx)
if !state.config.StickySelection {
state.Selection().Reset()
}
return
}
// Create a new pipeline
p := pipeline.New()
// Determine the source: use incremental filtering if possible
selectedFilter := state.Filters().Current()
filterName := selectedFilter.String()
var src pipeline.Source
var srcSize int
f.prevMu.Lock()
if f.prevResults != nil &&
f.prevFilterName == filterName &&
f.prevFrozenSrc == state.Frozen().Source() &&
isQueryRefinement(f.prevQuery, query) {
if pdebug.Enabled {
pdebug.Printf("Using incremental source (prev=%q, new=%q, prevSize=%d)", f.prevQuery, query, f.prevResults.Size())
}
srcSize = f.prevResults.Size()
src = NewMemoryBufferSource(f.prevResults)
}
f.prevMu.Unlock()
if src == nil {
if fs := state.Frozen().Source(); fs != nil {
src = NewMemoryBufferSource(fs)
srcSize = fs.Size()
} else {
src = state.Source()
if sizer, ok := src.(interface{ Size() int }); ok {
srcSize = sizer.Size()
}
}
}
p.SetSource(src)
ctx = selectedFilter.NewContext(ctx, query)
// Report non-cancellation filter errors (e.g. regex compilation failures)
// to the status bar so the user can see why results are missing.
onFilterError := FilterErrorHandlerFunc(func(err error) {
state.Hub().SendStatusMsg(ctx, err.Error(), 5*time.Second)
})
p.Add(newFilterProcessor(selectedFilter, query, state.config.FilterBufSize, onFilterError))
buf := NewMemoryBuffer(srcSize / 4)
p.SetDestination(buf)
state.SetCurrentLineBuffer(ctx, buf)
go func(ctx context.Context) {
defer state.Hub().SendDraw(ctx, &hub.DrawOptions{RunningQuery: true})
if err := p.Run(ctx); err != nil {
state.Hub().SendStatusMsg(ctx, err.Error(), 0)
}
}(ctx)
go func() {
if pdebug.Enabled {
g := pdebug.Marker("Periodic draw request for '%s'", query)
defer g.End()
}
t := time.NewTicker(50 * time.Millisecond)
defer t.Stop()
defer state.Hub().SendStatusMsg(ctx, "", 0)
defer state.Hub().SendDraw(ctx, &hub.DrawOptions{RunningQuery: true})
for {
select {
case <-p.Done():
return
case <-t.C:
state.Hub().SendDraw(ctx, &hub.DrawOptions{RunningQuery: true})
}
}
}()
<-p.Done()
// Save results for incremental filtering only if pipeline completed
// successfully (context not cancelled)
if ctx.Err() == nil {
f.prevMu.Lock()
f.prevQuery = query
f.prevResults = buf
f.prevFilterName = filterName
f.prevFrozenSrc = state.Frozen().Source()
f.prevMu.Unlock()
}
if !state.config.StickySelection {
state.Selection().Reset()
}
}
// Loop keeps watching for incoming queries, and upon receiving
// a query, spawns a goroutine to do the heavy work. It also
// checks for previously running queries, so we can avoid
// running many goroutines doing the grep at the same time
func (f *Filter) Loop(ctx context.Context, cancel func()) error {
defer cancel()
// wg tracks in-flight Work goroutines so Loop doesn't return
// while they are still running. This ensures clean shutdown
// without blocking new queries from starting immediately.
var wg sync.WaitGroup
defer wg.Wait()
// previous holds the function that can cancel the previous
// query. This is used when multiple queries come in succession
// and the previous query is discarded anyway
var mutex sync.Mutex
var previous func()
for {
select {
case <-ctx.Done():
return nil
case q := <-f.state.Hub().QueryCh():
workctx, workcancel := context.WithCancel(ctx)
mutex.Lock()
if previous != nil {
if pdebug.Enabled {
pdebug.Printf("Canceling previous query")
}
previous()
}
previous = workcancel
mutex.Unlock()
f.state.Hub().SendStatusMsg(ctx, "Running query...", 0)
wg.Go(func() {
f.Work(workctx, q)
})
}
}
}

77
filter/base.go Normal file
View file

@ -0,0 +1,77 @@
package filter
import (
"context"
"github.com/peco/peco/line"
"github.com/peco/peco/pipeline"
)
// LineEmitter receives matched lines from a filter.
type LineEmitter interface {
Emit(context.Context, line.Line)
}
// chanEmitter sends matched lines to a pipeline channel.
type chanEmitter struct {
out pipeline.ChanOutput
}
func (e *chanEmitter) Emit(ctx context.Context, l line.Line) {
_ = e.out.Send(ctx, l)
}
// LineCollector accumulates matched lines into a slice.
type LineCollector struct {
lines []line.Line
}
// NewLineCollector creates a LineCollector pre-allocated with the given capacity.
func NewLineCollector(n int) *LineCollector {
return &LineCollector{lines: make([]line.Line, 0, n)}
}
// Emit appends a matched line to the collector.
func (c *LineCollector) Emit(_ context.Context, l line.Line) {
c.lines = append(c.lines, l)
}
// Lines returns the accumulated matched lines.
func (c *LineCollector) Lines() []line.Line {
return c.lines
}
// lineApplier is implemented by filter types that embed baseFilter to provide
// their type-specific matching logic.
type lineApplier interface {
applyInternal(ctx context.Context, lines []line.Line, em LineEmitter) error
}
// baseFilter provides shared implementations of Apply, ApplyCollect,
// NewContext, and BufSize for filters that follow the applyInternal pattern.
// Filters embed this type and set impl to their concrete filter value.
type baseFilter struct {
impl lineApplier
}
// NewContext returns a context initialized with the given query for pipeline use.
func (b *baseFilter) NewContext(ctx context.Context, query string) context.Context {
return newContext(ctx, query)
}
func (b *baseFilter) BufSize() int {
return 0
}
// Apply runs the filter's matching logic on lines, sending matches to out.
func (b *baseFilter) Apply(ctx context.Context, lines []line.Line, out pipeline.ChanOutput) error {
return b.impl.applyInternal(ctx, lines, &chanEmitter{out: out})
}
// 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) {
c := NewLineCollector(len(lines) / 2)
err := b.impl.applyInternal(ctx, lines, c)
return c.Lines(), err
}

140
filter/base_test.go Normal file
View file

@ -0,0 +1,140 @@
package filter
import (
"context"
"testing"
"time"
"github.com/peco/peco/line"
"github.com/peco/peco/pipeline"
"github.com/stretchr/testify/require"
)
// TestApplyAndApplyCollectConsistency verifies that Apply (channel-based) and
// ApplyCollect (slice-based) produce identical results for every built-in
// filter type. This is the key invariant that the baseFilter extraction must
// preserve.
func TestApplyAndApplyCollectConsistency(t *testing.T) {
lines := makeLines(
"hello world",
"hello tests",
"goodbye world",
"goodbye tests",
"fuzzy matching example",
"another line",
)
tests := []struct {
name string
filter Filter
query string
}{
{"IgnoreCase", NewIgnoreCase(), "hello"},
{"CaseSensitive", NewCaseSensitive(), "hello"},
{"SmartCase", NewSmartCase(), "hello"},
{"Regexp", NewRegexp(), "hello.*world"},
{"IRegexp", NewIRegexp(), "HELLO"},
{"Fuzzy", NewFuzzy(false), "hw"},
{"FuzzyLongest", NewFuzzy(true), "hw"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(
tt.filter.NewContext(context.Background(), tt.query),
10*time.Second,
)
defer cancel()
// Collect via Apply (channel path)
ch := make(chan line.Line, len(lines)+1)
err := tt.filter.Apply(ctx, lines, pipeline.ChanOutput(ch))
require.NoError(t, err, "Apply should succeed")
close(ch)
var applyResults []string
for l := range ch {
applyResults = append(applyResults, l.DisplayString())
}
// Collect via ApplyCollect (direct slice path)
collector, ok := tt.filter.(Collector)
require.True(t, ok, "%s should implement Collector", tt.name)
ctx2, cancel2 := context.WithTimeout(
tt.filter.NewContext(context.Background(), tt.query),
10*time.Second,
)
defer cancel2()
collected, err := collector.ApplyCollect(ctx2, lines)
require.NoError(t, err, "ApplyCollect should succeed")
collectResults := make([]string, 0, len(collected))
for _, l := range collected {
collectResults = append(collectResults, l.DisplayString())
}
// They must produce exactly the same results
require.Equal(t, applyResults, collectResults,
"Apply and ApplyCollect should produce identical results")
require.NotEmpty(t, applyResults,
"test should produce at least one match (verify test data)")
})
}
}
// TestBufSizeDefaults verifies that built-in filters return the expected
// BufSize (0 for Regexp/Fuzzy, meaning "use config default").
func TestBufSizeDefaults(t *testing.T) {
tests := []struct {
name string
filter Filter
expected int
}{
{"IgnoreCase", NewIgnoreCase(), 0},
{"CaseSensitive", NewCaseSensitive(), 0},
{"SmartCase", NewSmartCase(), 0},
{"Regexp", NewRegexp(), 0},
{"IRegexp", NewIRegexp(), 0},
{"Fuzzy", NewFuzzy(false), 0},
{"FuzzyLongest", NewFuzzy(true), 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.expected, tt.filter.BufSize())
})
}
}
// TestNewContextStoresQuery verifies that NewContext stores the query and it
// can be retrieved by the filter's applyInternal.
func TestNewContextStoresQuery(t *testing.T) {
filters := []struct {
name string
filter Filter
}{
{"IgnoreCase", NewIgnoreCase()},
{"Fuzzy", NewFuzzy(false)},
}
for _, tt := range filters {
t.Run(tt.name, func(t *testing.T) {
ctx := tt.filter.NewContext(context.Background(), "test-query")
// The query should be stored in context — verify by running Apply
// with a line that matches "test-query"
ch := make(chan line.Line, 2)
lines := makeLines("this is a test-query line")
err := tt.filter.Apply(ctx, lines, pipeline.ChanOutput(ch))
require.NoError(t, err)
close(ch)
var results []line.Line
for l := range ch {
results = append(results, l)
}
require.Len(t, results, 1, "query stored by NewContext should be used by Apply")
})
}
}

143
filter/bench_test.go Normal file
View file

@ -0,0 +1,143 @@
package filter
import (
"context"
"strings"
"testing"
"time"
"github.com/peco/peco/internal/util"
"github.com/peco/peco/line"
"github.com/peco/peco/pipeline"
)
// BenchmarkFuzzyFilter benchmarks the fuzzy filter to measure allocations
// in the hot path (CaseInsensitiveIndexFunc closure, match slices, etc).
func BenchmarkFuzzyFilter(b *testing.B) {
lines := make([]line.Line, 200)
for i := range lines {
lines[i] = line.NewRaw(uint64(i), "this is a reasonably long line for benchmarking the fuzzy filter path", false, false)
}
f := NewFuzzy(false)
query := "trfp" // matches scattered chars across the line
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
ctx, cancel := context.WithTimeout(f.NewContext(context.Background(), query), 10*time.Second)
ch := make(chan line.Line, len(lines))
_ = f.Apply(ctx, lines, pipeline.ChanOutput(ch))
cancel()
}
}
// BenchmarkCaseInsensitiveIndexClosure measures the old closure-based approach.
func BenchmarkCaseInsensitiveIndexClosure(b *testing.B) {
txt := "this is a reasonably long line for benchmarking"
r := 'r'
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
fn := util.CaseInsensitiveIndexFunc(r)
strings.IndexFunc(txt, fn)
}
}
// BenchmarkCaseInsensitiveIndexDirect measures the new direct approach.
func BenchmarkCaseInsensitiveIndexDirect(b *testing.B) {
txt := "this is a reasonably long line for benchmarking"
r := 'r'
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
util.CaseInsensitiveIndex(txt, r)
}
}
// BenchmarkRegexpFilter benchmarks the regexp filter with overlapping matches
// to exercise the dedup/merge path and matches slice allocation.
func BenchmarkRegexpFilter(b *testing.B) {
lines := make([]line.Line, 200)
for i := range lines {
lines[i] = line.NewRaw(uint64(i), "the quick brown fox jumps over the lazy brown dog", false, false)
}
f := NewIgnoreCase()
query := "brown the"
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
ctx, cancel := context.WithTimeout(f.NewContext(context.Background(), query), 10*time.Second)
ch := make(chan line.Line, len(lines))
_ = f.Apply(ctx, lines, pipeline.ChanOutput(ch))
cancel()
}
}
// BenchmarkRegexpFilterMultiCycle simulates multiple keystroke filter cycles.
// On each cycle, the filter produces Matched objects which are collected in a
// MemoryBuffer, then Reset() is called to release them before the next cycle.
// This demonstrates pool reuse across keystrokes.
func BenchmarkRegexpFilterMultiCycle(b *testing.B) {
const numLines = 10_000
lines := make([]line.Line, numLines)
for i := range lines {
lines[i] = line.NewRaw(uint64(i), "the quick brown fox jumps over the lazy dog", false, false)
}
f := NewIgnoreCase()
query := "fox"
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
// Simulate 3 keystroke cycles
for range 3 {
ctx := f.NewContext(context.Background(), query)
ch := make(chan line.Line, numLines)
_ = f.Apply(ctx, lines, pipeline.ChanOutput(ch))
close(ch)
// Drain and release (simulating MemoryBuffer.Reset)
for l := range ch {
if m, ok := l.(*line.Matched); ok {
line.ReleaseMatched(m)
}
}
}
}
}
// BenchmarkRegexpFilterOverlapping benchmarks the regexp filter with a query
// that produces overlapping match ranges, exercising the mergeMatches path.
// Each line contains repeating "aabb" patterns; the query terms "aab" and "abb"
// produce match ranges that overlap (e.g. [0,3] and [1,4]), forcing mergeMatches
// to be called on every line. With 10k lines the aggregate allocation difference
// from in-place vs make([]int,2) becomes measurable.
func BenchmarkRegexpFilterOverlapping(b *testing.B) {
// Build a line with many "aabb" repeats so that "aab" and "abb" each match
// many times with overlapping ranges between the two terms.
base := strings.Repeat("aabb", 20) // 80 chars
lines := make([]line.Line, 10_000)
for i := range lines {
lines[i] = line.NewRaw(uint64(i), base, false, false)
}
f := NewIgnoreCase()
// "aab" and "abb" share the middle "ab" in each "aabb" group, so their
// match ranges overlap after sorting by start position.
query := "aab abb"
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
ctx, cancel := context.WithTimeout(f.NewContext(context.Background(), query), 10*time.Second)
ch := make(chan line.Line, len(lines))
_ = f.Apply(ctx, lines, pipeline.ChanOutput(ch))
cancel()
}
}

206
filter/external.go Normal file
View file

@ -0,0 +1,206 @@
package filter
import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"os/exec"
"sync"
pdebug "github.com/lestrrat-go/pdebug"
"github.com/peco/peco/line"
"github.com/peco/peco/pipeline"
)
// DefaultCustomFilterBufferThreshold is the default value
// for BufferThreshold setting on CustomFilters.
const DefaultCustomFilterBufferThreshold = 100
// queryPlaceholder is the placeholder string in external command arguments
// that gets replaced with the actual query at filter execution time.
const queryPlaceholder = "$QUERY"
// ExternalCmd is a filter that delegates matching to an external command.
type ExternalCmd struct {
args []string
cmd string
enableSep bool
idgen line.IDGenerator
name string
thresholdBufsiz int
}
// NewExternalCmd creates a new filter that uses an external
// command to filter the input
func NewExternalCmd(name string, cmd string, args []string, threshold int, idgen line.IDGenerator, enableSep bool) *ExternalCmd {
if len(args) == 0 {
args = []string{queryPlaceholder}
}
if threshold <= 0 {
threshold = DefaultCustomFilterBufferThreshold
}
return &ExternalCmd{
args: args,
cmd: cmd,
enableSep: enableSep,
idgen: idgen,
name: name,
thresholdBufsiz: threshold,
}
}
func (ecf ExternalCmd) BufSize() int {
return ecf.thresholdBufsiz
}
// NewContext returns a context initialized with the given query for pipeline use.
func (ecf *ExternalCmd) NewContext(ctx context.Context, query string) context.Context {
return newContext(ctx, query)
}
func (ecf ExternalCmd) SupportsParallel() bool {
return false
}
func (ecf ExternalCmd) String() string {
return ecf.name
}
// Apply pipes the buffered lines through the external command and sends
// matching output lines to out.
func (ecf *ExternalCmd) Apply(ctx context.Context, buf []line.Line, out pipeline.ChanOutput) (err error) {
var readerPanicErr error
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic in external filter %q: %v", ecf.name, r)
}
if err == nil && readerPanicErr != nil {
err = readerPanicErr
}
}()
if pdebug.Enabled {
g := pdebug.Marker("ExternalCmd.Apply").BindError(&err)
defer g.End()
}
query := pipeline.QueryFromContext(ctx)
args := append([]string(nil), ecf.args...)
for i, v := range args {
if v == queryPlaceholder {
args[i] = query
}
}
cmd := exec.CommandContext(ctx, ecf.cmd, args...)
if pdebug.Enabled {
pdebug.Printf("Executing command %s %v", cmd.Path, cmd.Args)
}
// When enableSep is true (--null mode), we need to preserve the original
// lines so that Output() returns the correct post-separator text.
// Build an index of display string -> original lines for lookup.
var originalLines map[string][]line.Line
if ecf.enableSep {
originalLines = make(map[string][]line.Line)
for _, l := range buf {
ds := l.DisplayString()
originalLines[ds] = append(originalLines[ds], l)
}
}
inbuf := &bytes.Buffer{}
for _, l := range buf {
inbuf.WriteString(l.DisplayString())
inbuf.WriteByte('\n')
}
cmd.Stdin = inbuf
r, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("failed to get stdout pipe: %w", err)
}
err = cmd.Start()
if err != nil {
return fmt.Errorf("failed to start command: %w", err)
}
var wg sync.WaitGroup
cmdCh := make(chan line.Line)
wg.Add(1)
go func(ctx context.Context, cmdCh chan line.Line, rdr *bufio.Reader) {
defer wg.Done()
defer func() {
if r := recover(); r != nil {
readerPanicErr = fmt.Errorf("panic in external filter %q reader: %v", ecf.name, r)
}
}()
defer close(cmdCh)
defer func() { _ = cmd.Wait() }()
for {
select {
case <-ctx.Done():
return
default:
}
b, _, err := rdr.ReadLine()
if len(b) > 0 {
s := string(b)
var l line.Line
if originalLines != nil {
if candidates, ok := originalLines[s]; ok && len(candidates) > 0 {
// Pop the first matching original line to preserve order
l = candidates[0]
originalLines[s] = candidates[1:]
}
}
if l == nil {
// No original line found (or enableSep is false):
// create a new Raw line as before
l = line.NewRaw(ecf.idgen.Next(), s, ecf.enableSep, false)
}
select {
case cmdCh <- l:
case <-ctx.Done():
return
}
}
if err != nil {
if err != io.EOF {
readerPanicErr = fmt.Errorf("failed to read from external filter %q: %w", ecf.name, err)
}
return
}
}
}(ctx, cmdCh, bufio.NewReader(r))
defer func() {
if p := cmd.Process; p != nil {
_ = p.Kill()
}
wg.Wait()
}()
for {
select {
case <-ctx.Done():
return ctx.Err()
case l, ok := <-cmdCh:
if l == nil || !ok {
return nil
}
if err := out.Send(ctx, l); err != nil {
return err
}
}
}
}

226
filter/external_test.go Normal file
View file

@ -0,0 +1,226 @@
package filter
import (
"context"
"runtime"
"sync"
"testing"
"time"
"github.com/peco/peco/line"
"github.com/peco/peco/pipeline"
"github.com/stretchr/testify/require"
)
type testIDGen struct {
mu sync.Mutex
id uint64
}
func (g *testIDGen) Next() uint64 {
g.mu.Lock()
defer g.mu.Unlock()
g.id++
return g.id
}
// collectOutput drains the output channel and returns the lines sent to it.
func collectOutput(ctx context.Context, out pipeline.ChanOutput) []line.Line {
var results []line.Line
for {
select {
case <-ctx.Done():
return results
case l, ok := <-out.OutCh():
if !ok {
return results
}
results = append(results, l)
}
}
}
func TestExternalCmd_SupportsParallel(t *testing.T) {
idgen := &testIDGen{}
ecf := NewExternalCmd("cat", "cat", nil, 0, idgen, false)
require.False(t, ecf.SupportsParallel(), "ExternalCmd must not support parallel execution")
}
func TestExternalCmd_CancelCleansUpGoroutine(t *testing.T) {
idgen := &testIDGen{}
lines := []line.Line{
line.NewRaw(idgen.Next(), "hello", false, false),
}
// Use "cat" which reads stdin and writes to stdout; it will block
// waiting for EOF on stdin, but since we provide input via a buffer
// it completes quickly. Instead, use "sleep" which blocks for a long time.
ecf := NewExternalCmd("sleep", "sleep", []string{"60"}, 0, idgen, false)
ctx, cancel := context.WithCancel(ecf.NewContext(context.Background(), "test"))
out := pipeline.ChanOutput(make(chan line.Line, 256))
// Record goroutine count before Apply
runtime.GC()
before := runtime.NumGoroutine()
done := make(chan error, 1)
go func() {
done <- ecf.Apply(ctx, lines, out)
}()
// Give the command time to start
time.Sleep(50 * time.Millisecond)
// Cancel the context
cancel()
// Apply should return promptly
select {
case err := <-done:
require.ErrorIs(t, err, context.Canceled)
case <-time.After(5 * time.Second):
require.Fail(t, "Apply did not return after context cancellation")
}
// Wait briefly for goroutine cleanup, then verify no goroutine leak
deadline := time.Now().Add(3 * time.Second)
for time.Now().Before(deadline) {
runtime.GC()
after := runtime.NumGoroutine()
// Allow some slack (test infrastructure goroutines)
if after <= before+1 {
return
}
time.Sleep(50 * time.Millisecond)
}
require.Fail(t, "goroutine leak", "before=%d, after=%d", before, runtime.NumGoroutine())
}
func TestExternalCmd_ApplyWithoutQueryContext(t *testing.T) {
idgen := &testIDGen{}
lines := []line.Line{
line.NewRaw(idgen.Next(), "hello", false, false),
}
ecf := NewExternalCmd("cat", "cat", nil, 0, idgen, false)
out := pipeline.ChanOutput(make(chan line.Line, 256))
// Call Apply with a context that does NOT have the query key set.
// QueryFromContext returns "" safely (no panic).
err := ecf.Apply(context.Background(), lines, out)
require.NoError(t, err, "Apply with missing query context should not panic or error")
}
func TestExternalCmdFilter_NullSep(t *testing.T) {
t.Run("preserves Output with enableSep", func(t *testing.T) {
idgen := &testIDGen{}
// Create lines with null separator: display\0output
lines := []line.Line{
line.NewRaw(idgen.Next(), "apple\x00/fruit/apple", true, false),
line.NewRaw(idgen.Next(), "banana\x00/fruit/banana", true, false),
line.NewRaw(idgen.Next(), "apricot\x00/fruit/apricot", true, false),
}
// Verify the lines are set up correctly
require.Equal(t, "apple", lines[0].DisplayString())
require.Equal(t, "/fruit/apple", lines[0].Output())
// Use grep to filter for lines containing "ap" (matches apple, apricot)
ecf := NewExternalCmd("grep", "grep", []string{"ap"}, 0, idgen, true)
ctx := ecf.NewContext(context.Background(), "ap")
out := pipeline.ChanOutput(make(chan line.Line, 256))
var results []line.Line
done := make(chan struct{})
go func() {
defer close(done)
results = collectOutput(ctx, out)
}()
err := ecf.Apply(ctx, lines, out)
require.NoError(t, err)
close(out)
<-done
require.Len(t, results, 2)
// The key assertion: Output() must return the post-separator text,
// not the display text that the external filter matched on.
require.Equal(t, "apple", results[0].DisplayString())
require.Equal(t, "/fruit/apple", results[0].Output())
require.Equal(t, "apricot", results[1].DisplayString())
require.Equal(t, "/fruit/apricot", results[1].Output())
})
t.Run("without enableSep creates new lines", func(t *testing.T) {
idgen := &testIDGen{}
lines := []line.Line{
line.NewRaw(idgen.Next(), "apple", false, false),
line.NewRaw(idgen.Next(), "banana", false, false),
line.NewRaw(idgen.Next(), "apricot", false, false),
}
ecf := NewExternalCmd("grep", "grep", []string{"ap"}, 0, idgen, false)
ctx := ecf.NewContext(context.Background(), "ap")
out := pipeline.ChanOutput(make(chan line.Line, 256))
var results []line.Line
done := make(chan struct{})
go func() {
defer close(done)
results = collectOutput(ctx, out)
}()
err := ecf.Apply(ctx, lines, out)
require.NoError(t, err)
close(out)
<-done
require.Len(t, results, 2)
require.Equal(t, "apple", results[0].DisplayString())
require.Equal(t, "apple", results[0].Output())
require.Equal(t, "apricot", results[1].DisplayString())
require.Equal(t, "apricot", results[1].Output())
})
t.Run("duplicate display strings preserve order", func(t *testing.T) {
idgen := &testIDGen{}
// Three lines with the same display text but different outputs
lines := []line.Line{
line.NewRaw(idgen.Next(), "dup\x00first", true, false),
line.NewRaw(idgen.Next(), "dup\x00second", true, false),
line.NewRaw(idgen.Next(), "dup\x00third", true, false),
}
// Use grep to return all lines (match literal "dup")
ecf := NewExternalCmd("grep", "grep", []string{"-F", "$QUERY"}, 0, idgen, true)
ctx := ecf.NewContext(context.Background(), "dup")
out := pipeline.ChanOutput(make(chan line.Line, 256))
var results []line.Line
done := make(chan struct{})
go func() {
defer close(done)
results = collectOutput(ctx, out)
}()
err := ecf.Apply(ctx, lines, out)
require.NoError(t, err)
close(out)
<-done
require.Len(t, results, 3)
require.Equal(t, "first", results[0].Output())
require.Equal(t, "second", results[1].Output())
require.Equal(t, "third", results[2].Output())
})
}

101
filter/filter.go Normal file
View file

@ -0,0 +1,101 @@
package filter
import (
"context"
"regexp"
"github.com/peco/peco/line"
"github.com/peco/peco/pipeline"
)
// Filter is the interface that all filter implementations must satisfy.
type Filter interface {
Apply(context.Context, []line.Line, pipeline.ChanOutput) error
BufSize() int
NewContext(context.Context, string) context.Context
String() string
// SupportsParallel returns true if this filter can safely be invoked
// concurrently on independent chunks of lines. Filters that require
// global state across all lines (e.g. sorted output) should return false.
SupportsParallel() bool
}
// Collector is an optional interface that filters can implement to return
// matched lines directly as a slice, bypassing channel-based output.
// This avoids per-chunk channel allocation and goroutine overhead in the
// parallel filter path.
type Collector interface {
ApplyCollect(context.Context, []line.Line) ([]line.Line, error)
}
// isExcluded reports whether text matches any of the given negative regexps.
func isExcluded(negRegexps []*regexp.Regexp, text string) bool {
for _, rx := range negRegexps {
if rx.MatchString(text) {
return true
}
}
return false
}
// newContext initializes the context so that it is suitable
// to be passed to `Run()`
func newContext(ctx context.Context, query string) context.Context {
return pipeline.NewQueryContext(ctx, query)
}
// checkCancelled returns ctx.Err() every 1000 iterations so that
// long-running filter loops can bail out promptly on cancellation
// without paying the cost of a channel receive on every single line.
func checkCancelled(ctx context.Context, i int) error {
if i%1000 == 0 {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
}
return nil
}
// sort related stuff
type byMatchStart [][]int
func (m byMatchStart) Len() int {
return len(m)
}
func (m byMatchStart) Swap(i, j int) {
m[i], m[j] = m[j], m[i]
}
func (m byMatchStart) Less(i, j int) bool {
if m[i][0] < m[j][0] {
return true
}
if m[i][0] == m[j][0] {
return m[i][1]-m[i][0] < m[j][1]-m[j][0]
}
return false
}
// matchContains reports whether match range a fully contains match range b.
func matchContains(a []int, b []int) bool {
return a[0] <= b[0] && a[1] >= b[1]
}
// matchOverlaps reports whether two match ranges overlap.
func matchOverlaps(a []int, b []int) bool {
return a[0] <= b[0] && a[1] >= b[0] ||
a[0] <= b[1] && a[1] >= b[1]
}
// mergeMatches combines two overlapping match ranges into a single range
// spanning both. It mutates and returns a to avoid a heap allocation.
func mergeMatches(a []int, b []int) []int {
a[0] = min(a[0], b[0])
a[1] = max(a[1], b[1])
return a
}

706
filter/filter_test.go Normal file
View file

@ -0,0 +1,706 @@
package filter
import (
"context"
"fmt"
"regexp"
"testing"
"time"
"github.com/peco/peco/line"
"github.com/peco/peco/pipeline"
"github.com/stretchr/testify/require"
)
type indexer interface {
Indices() [][]int
}
func TestCheckCancelled(t *testing.T) {
t.Run("returns nil for non-boundary iterations", func(t *testing.T) {
ctx := context.Background()
for _, i := range []int{1, 2, 999, 1001, 2999} {
require.NoError(t, checkCancelled(ctx, i))
}
})
t.Run("returns nil at boundary when context is active", func(t *testing.T) {
ctx := context.Background()
for _, i := range []int{0, 1000, 2000, 3000} {
require.NoError(t, checkCancelled(ctx, i))
}
})
t.Run("returns error at boundary when context is cancelled", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
require.ErrorIs(t, checkCancelled(ctx, 0), context.Canceled)
require.ErrorIs(t, checkCancelled(ctx, 1000), context.Canceled)
})
t.Run("returns nil off-boundary even when context is cancelled", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
require.NoError(t, checkCancelled(ctx, 1))
require.NoError(t, checkCancelled(ctx, 999))
})
}
// TestFuzzy tests the Fuzzy filter against various inputs.
//
// testFuzzy: simple substring match
// testFuzzyLongest: sorted longest substring match
// testFuzzyMatch: match position without sorting
// testFuzzyLongestMatch: match position with sorting
func TestFuzzy(t *testing.T) {
octx := t.Context()
testFuzzy(octx, t, NewFuzzy(false))
testFuzzyLongest(octx, t, NewFuzzy(true))
testFuzzyMatch(octx, t, NewFuzzy(false))
}
// testFuzzy tests if given filter matches/rejects the query.
// This test checks the following functionalities:
// - Fuzzy substring match
// - Case-insensitive match
// - Multi-byte rune match
func testFuzzy(octx context.Context, t *testing.T, filter Filter) {
testValues := []struct {
input string
query string
selected bool
}{
{"this is a test to test the fuzzy Filter", "tf", true}, // normal selection
{"this is a test to test the fuzzy Filter", "wp", false}, // incorrect selection
{"THIS IS A TEST TO TEST THE FUZZY FILTER", "tu", true}, // case insensitivity
{"this is a Test to test the fuzzy filter", "Tu", true}, // case sensitivity
{"this is a Test to test the fUzzy filter", "TU", true}, // case sensitivity
{"this is a test to test the fuzzy filter", "Tu", false}, // case sensitivity
{"this is a test to Test the fuzzy filter", "TU", false}, // case sensitivity
{"日本語は難しいです", "難", true}, // kanji
{"あ、日本語は難しいですよ", "あい", true}, // hiragana
{"パソコンは遅いですネ", "ソネ", true}, // katakana
{"🚴🏻 abcd efgh", "🚴🏻e", true}, // unicode
{"This is a test to Test the fuzzy filteR", "TTR", true},
}
for i, v := range testValues {
t.Run(fmt.Sprintf(`"%s" against "%s", expect "%t"`, v.input, v.query, v.selected), func(t *testing.T) {
ctx, cancel := context.WithTimeout(filter.NewContext(octx, v.query), 10*time.Second)
defer cancel()
ch := make(chan line.Line, 1)
l := line.NewRaw(uint64(i), v.input, false, false)
err := filter.Apply(ctx, []line.Line{l}, pipeline.ChanOutput(ch))
require.NoError(t, err, `filter.Apply should succeed`)
select {
case l, ok := <-ch:
require.True(t, ok, `channel read should succeed`)
t.Logf("%#v", l.(indexer).Indices())
case <-ctx.Done():
require.False(t, v.selected, "did NOT expect to timeout")
}
})
}
}
// testFuzzyLongest tests if given filter matches/rejects the query.
// This test check the following functionalities:
// - Longest substring match
// - Ordering the match result by precedence (substring length > match index > string length)
func testFuzzyLongest(octx context.Context, t *testing.T, filter Filter) {
testValues := []struct {
name string
query string
input []string
expect []string
}{
{
name: "The longer the matched string, the higher it ranks",
query: "abcd",
input: []string{
"abc-d",
"ab-cd",
"abcd",
"a-bcd",
},
expect: []string{
"abcd",
"abc-d",
"a-bcd",
"ab-cd",
},
},
{
name: "The earlier it matches, the higher it ranks",
query: "abcd",
input: []string{
"___abcd",
"_abcd",
"abcd",
"__abcd",
},
expect: []string{
"abcd",
"_abcd",
"__abcd",
"___abcd",
},
},
{
name: "The shorter the original string, the higher it ranks",
query: "abcd",
input: []string{
"abcdef",
"abcdefg",
"abcd",
"abcde",
},
expect: []string{
"abcd",
"abcde",
"abcdef",
"abcdefg",
},
},
{
name: "Mixed precedence",
query: "abcd",
input: []string{
"abc-d",
"ab-cd",
"abcd",
"ab_abcd",
"a-bcd",
"___abcd",
"_abcd",
"abcd",
"__abcd",
"abcdef",
"abcdefg",
"abcd",
"abcde",
},
expect: []string{
"abcd",
"abcd",
"abcd",
"abcde",
"abcdef",
"abcdefg",
"_abcd",
"__abcd",
"ab_abcd", // ab_abcd shall be above ___abcd because matched lines are stable-sorted
"___abcd",
"abc-d",
"a-bcd",
"ab-cd",
},
},
}
for i, v := range testValues {
t.Run(v.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(filter.NewContext(octx, v.query), 10*time.Second)
defer cancel()
var lines []line.Line
for _, raw := range v.input {
lines = append(lines, line.NewRaw(uint64(i), raw, false, false))
}
var actual []string
lc := make(chan line.Line)
ec := make(chan error)
go func() {
ec <- filter.Apply(ctx, lines, pipeline.ChanOutput(lc))
}()
OUTER:
for {
select {
case l := <-lc:
actual = append(actual, l.DisplayString())
case err := <-ec:
require.NoError(t, err, `filter.Apply should succeed`)
break OUTER
case <-ctx.Done():
require.Fail(t, "unexpected timeout")
}
}
require.Equal(t, v.expect, actual, "result is ordered in expected order")
})
}
}
// testFuzzyMatch tests if non-sorted & sorted Fuzzy filter returns the expected result
func TestSplitQueryTerms(t *testing.T) {
tests := []struct {
name string
query string
wantPos []string
wantNeg []string
}{
{
name: "simple positive",
query: "foo bar",
wantPos: []string{"foo", "bar"},
},
{
name: "single negative",
query: "-foo",
wantNeg: []string{"foo"},
},
{
name: "mixed positive and negative",
query: "foo -bar baz",
wantPos: []string{"foo", "baz"},
wantNeg: []string{"bar"},
},
{
name: "all negative",
query: "-foo -bar",
wantNeg: []string{"foo", "bar"},
},
{
name: "escaped negative becomes positive",
query: `\-foo`,
wantPos: []string{"-foo"},
},
{
name: "bare hyphen is positive literal",
query: "-",
wantPos: []string{"-"},
},
{
name: "double hyphen is positive literal",
query: "--",
wantPos: []string{"--"},
},
{
name: "mixed with escaping",
query: `foo -bar \-baz`,
wantPos: []string{"foo", "-baz"},
wantNeg: []string{"bar"},
},
{
name: "extra spaces are skipped",
query: " foo -bar ",
wantPos: []string{"foo"},
wantNeg: []string{"bar"},
},
{
name: "empty query",
query: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotPos, gotNeg := SplitQueryTerms(tt.query)
require.Equal(t, tt.wantPos, gotPos, "positive terms")
require.Equal(t, tt.wantNeg, gotNeg, "negative terms")
})
}
}
// collectFilterResults runs the filter and collects all emitted lines.
func collectFilterResults(t *testing.T, f Filter, query string, inputLines []line.Line) []line.Line {
t.Helper()
ctx, cancel := context.WithTimeout(f.NewContext(context.Background(), query), 10*time.Second)
defer cancel()
ch := make(chan line.Line, len(inputLines)+1)
err := f.Apply(ctx, inputLines, pipeline.ChanOutput(ch))
require.NoError(t, err, "filter.Apply should succeed")
close(ch)
var results []line.Line
for l := range ch {
results = append(results, l)
}
return results
}
func makeLines(inputs ...string) []line.Line {
lines := make([]line.Line, len(inputs))
for i, s := range inputs {
lines[i] = line.NewRaw(uint64(i), s, false, false)
}
return lines
}
func makeANSILines(inputs ...string) []line.Line {
lines := make([]line.Line, len(inputs))
for i, s := range inputs {
lines[i] = line.NewRaw(uint64(i), s, false, true)
}
return lines
}
func TestNegativeMatchingRegexp(t *testing.T) {
filters := map[string]Filter{
"IgnoreCase": NewIgnoreCase(),
"CaseSensitive": NewCaseSensitive(),
"SmartCase": NewSmartCase(),
"Regexp": NewRegexp(),
}
for name, f := range filters {
t.Run(name, func(t *testing.T) {
lines := makeLines(
"hello world",
"hello tests",
"goodbye world",
"goodbye tests",
)
t.Run("positive with negative exclusion", func(t *testing.T) {
results := collectFilterResults(t, f, "hello -tests", lines)
require.Len(t, results, 1)
require.Equal(t, "hello world", results[0].DisplayString())
})
t.Run("multiple negative terms", func(t *testing.T) {
results := collectFilterResults(t, f, "hello -world -tests", lines)
require.Len(t, results, 0)
})
})
}
}
func TestAllNegativeQuery(t *testing.T) {
filters := map[string]Filter{
"IgnoreCase": NewIgnoreCase(),
"Regexp": NewRegexp(),
"Fuzzy": NewFuzzy(false),
}
lines := makeLines(
"alpha",
"beta",
"gamma",
)
for name, f := range filters {
t.Run(name, func(t *testing.T) {
results := collectFilterResults(t, f, "-beta", lines)
require.Len(t, results, 2)
names := make([]string, 0, len(results))
for _, r := range results {
names = append(names, r.DisplayString())
}
require.Contains(t, names, "alpha")
require.Contains(t, names, "gamma")
})
}
}
func TestNegativeNoHighlight(t *testing.T) {
f := NewIgnoreCase()
lines := makeLines("alpha", "beta", "gamma")
results := collectFilterResults(t, f, "-beta", lines)
require.Len(t, results, 2)
for _, r := range results {
idx, ok := r.(indexer)
require.True(t, ok, "result should implement indexer")
require.Nil(t, idx.Indices(), "all-negative query should produce nil indices")
}
}
func TestNegativeMatchingFuzzy(t *testing.T) {
f := NewFuzzy(false)
lines := makeLines(
"hello world",
"hello tests",
"goodbye world",
"goodbye tests",
)
t.Run("fuzzy positive with negative exclusion", func(t *testing.T) {
// Fuzzy query "hlo" should match "hello" lines; -tests excludes one
results := collectFilterResults(t, f, "hlo -tests", lines)
require.Len(t, results, 1)
require.Equal(t, "hello world", results[0].DisplayString())
})
t.Run("fuzzy all-negative", func(t *testing.T) {
results := collectFilterResults(t, f, "-world", lines)
require.Len(t, results, 2)
names := make([]string, 0, len(results))
for _, r := range results {
names = append(names, r.DisplayString())
}
require.Contains(t, names, "hello tests")
require.Contains(t, names, "goodbye tests")
})
}
func TestLiteralHyphenMatching(t *testing.T) {
lines := makeLines(
"hello-world",
"hello world",
"-foo bar",
"foo bar",
"--verbose flag",
"verbose flag",
)
f := NewIgnoreCase()
t.Run("escaped negative matches literal hyphen-prefixed term", func(t *testing.T) {
// \-foo should match lines containing literal "-foo"
results := collectFilterResults(t, f, `\-foo`, lines)
require.Len(t, results, 1)
require.Equal(t, "-foo bar", results[0].DisplayString())
})
t.Run("bare hyphen matches lines containing hyphen", func(t *testing.T) {
// bare "-" should be a positive literal matching any line with a hyphen
results := collectFilterResults(t, f, "-", lines)
require.Len(t, results, 3)
names := make([]string, 0, len(results))
for _, r := range results {
names = append(names, r.DisplayString())
}
require.Contains(t, names, "hello-world")
require.Contains(t, names, "-foo bar")
require.Contains(t, names, "--verbose flag")
})
t.Run("double hyphen matches lines containing double hyphen", func(t *testing.T) {
results := collectFilterResults(t, f, "--", lines)
require.Len(t, results, 1)
require.Equal(t, "--verbose flag", results[0].DisplayString())
})
t.Run("escaped negative with positive term", func(t *testing.T) {
// Search for lines containing both "bar" and literal "-foo"
results := collectFilterResults(t, f, `bar \-foo`, lines)
require.Len(t, results, 1)
require.Equal(t, "-foo bar", results[0].DisplayString())
})
}
func TestIsExcluded(t *testing.T) {
rxFoo := regexp.MustCompile(`(?i)foo`)
rxBar := regexp.MustCompile(`(?i)bar`)
tests := []struct {
name string
negRegexps []*regexp.Regexp
text string
want bool
}{
{
name: "nil regexps never excludes",
negRegexps: nil,
text: "anything",
want: false,
},
{
name: "empty regexps never excludes",
negRegexps: []*regexp.Regexp{},
text: "anything",
want: false,
},
{
name: "single match excludes",
negRegexps: []*regexp.Regexp{rxFoo},
text: "contains foo here",
want: true,
},
{
name: "no match does not exclude",
negRegexps: []*regexp.Regexp{rxFoo},
text: "contains baz here",
want: false,
},
{
name: "second regexp matches",
negRegexps: []*regexp.Regexp{rxFoo, rxBar},
text: "has bar in it",
want: true,
},
{
name: "neither regexp matches",
negRegexps: []*regexp.Regexp{rxFoo, rxBar},
text: "has baz in it",
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.want, isExcluded(tt.negRegexps, tt.text))
})
}
}
// testFuzzyMatch tests if non-sorted & sorted Fuzzy filter returns the expected result
func testFuzzyMatch(octx context.Context, t *testing.T, filter Filter) {
testValues := []struct {
name string
sort bool
query string
input string
expect [][]int
}{
{
name: "Fuzzy: exact match",
sort: false,
query: "asdf",
input: "___asdf",
// ^^^^
expect: [][]int{
{3, 4},
{4, 5},
{5, 6},
{6, 7},
},
},
{
name: "Fuzzy: scattered match",
sort: false,
query: "asdf",
input: "as_asdf",
// ^^ ^^
expect: [][]int{
{0, 1},
{1, 2},
{5, 6},
{6, 7},
},
},
{
name: "FuzzyLongest: exact match",
sort: true,
query: "asdf",
input: "___asdf",
// ^^^^
expect: [][]int{
{3, 4},
{4, 5},
{5, 6},
{6, 7},
},
},
{
name: "FuzzyLongest: scattered match",
sort: true,
query: "asdf",
input: "as_asdf",
// ^^^^
expect: [][]int{
{3, 4},
{4, 5},
{5, 6},
{6, 7},
},
},
}
for i, v := range testValues {
t.Run(v.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(filter.NewContext(octx, v.query), 10*time.Second)
defer cancel()
filter := NewFuzzy(v.sort)
lc := make(chan line.Line)
ec := make(chan error)
go func() {
ec <- filter.Apply(ctx, []line.Line{line.NewRaw(uint64(i), v.input, false, false)}, pipeline.ChanOutput(lc))
}()
OUTER:
for {
select {
case l := <-lc:
require.Implements(t, (*indexer)(nil), l, "result is an indexer")
require.Equal(t, v.expect, l.(indexer).Indices(), "result has expected indices")
case err := <-ec:
require.NoError(t, err, `filter.Apply should succeed`)
break OUTER
case <-ctx.Done():
require.Fail(t, "unexpected timeout")
}
}
})
}
}
func TestMergeMatches(t *testing.T) {
t.Parallel()
tests := []struct {
name string
a []int
b []int
want []int
}{
{"a before b overlapping", []int{1, 5}, []int{3, 7}, []int{1, 7}},
{"b before a overlapping", []int{3, 7}, []int{1, 5}, []int{1, 7}},
{"identical ranges", []int{2, 4}, []int{2, 4}, []int{2, 4}},
{"a contains b", []int{0, 10}, []int{3, 7}, []int{0, 10}},
{"adjacent ranges", []int{0, 3}, []int{3, 6}, []int{0, 6}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := mergeMatches(tt.a, tt.b)
require.Equal(t, tt.want, got)
// Verify in-place mutation: got should be the same slice as a
require.Same(t, &tt.a[0], &got[0], "mergeMatches should mutate a in place")
})
}
}
// TestMatchAcrossANSIColorBoundary verifies that filter queries match
// against the ANSI-stripped text, so a pattern spanning characters
// rendered in different colors still produces a match.
func TestMatchAcrossANSIColorBoundary(t *testing.T) {
// "\033[31mfoo\033[34mbar\033[0m" renders as red "foo" + blue "bar",
// but the stripped text is "foobar". A query "oba" spans the boundary.
lines := makeANSILines(
"\x1b[31mfoo\x1b[34mbar\x1b[0m", // red "foo" + blue "bar"
"\x1b[32mhello\x1b[0m world", // green "hello" + plain " world"
"plain text no ansi", // no ANSI at all
"\x1b[1m\x1b[33mBRI\x1b[35mGHT\x1b[0m", // bold yellow "BRI" + bold magenta "GHT"
)
filters := map[string]Filter{
"IgnoreCase": NewIgnoreCase(),
"CaseSensitive": NewCaseSensitive(),
"Regexp": NewRegexp(),
"Fuzzy": NewFuzzy(false),
}
for name, f := range filters {
t.Run(name, func(t *testing.T) {
t.Run("match spans two color regions", func(t *testing.T) {
// "oba" spans red "foo" and blue "bar"
results := collectFilterResults(t, f, "oba", lines)
require.Len(t, results, 1)
require.Equal(t, "foobar", results[0].DisplayString())
})
t.Run("match spans color and plain regions", func(t *testing.T) {
// "lo w" spans green "hello" and plain " world"
results := collectFilterResults(t, f, "lo w", lines)
require.Len(t, results, 1)
require.Equal(t, "hello world", results[0].DisplayString())
})
t.Run("match spans bold color change", func(t *testing.T) {
// "RIGH" spans bold-yellow "BRI" and bold-magenta "GHT"
results := collectFilterResults(t, f, "RIGH", lines)
require.Len(t, results, 1)
require.Equal(t, "BRIGHT", results[0].DisplayString())
})
})
}
}

265
filter/fuzzy.go Normal file
View file

@ -0,0 +1,265 @@
package filter
import (
"context"
"fmt"
"math"
"regexp"
"sort"
"strings"
"unicode/utf8"
"github.com/peco/peco/internal/util"
"github.com/peco/peco/line"
"github.com/peco/peco/pipeline"
)
// Fuzzy is a filter that performs fuzzy matching.
type Fuzzy struct {
baseFilter
sortLongest bool
}
// NewFuzzy builds a fuzzy-finder type of filter.
// In effect, this uses a smart case filter, and for q query
// like "ABC" it matches the equivalent of "A(.*)B(.*)C(.*)".
//
// With sortLongest = true, Fuzzy filter outputs the result
// sorted in the following precedence:
// 1. Longer match
// 2. Earlier match
// 3. Shorter line length
func NewFuzzy(sortLongest bool) *Fuzzy {
ff := &Fuzzy{
sortLongest: sortLongest,
}
ff.impl = ff
return ff
}
func (ff Fuzzy) SupportsParallel() bool {
return !ff.sortLongest
}
func (ff Fuzzy) String() string {
return "Fuzzy"
}
// applyInternal performs fuzzy matching on each line, emitting matches with
// their character-level match indices for highlighting.
func (ff *Fuzzy) applyInternal(ctx context.Context, lines []line.Line, em LineEmitter) error {
originalQuery := pipeline.QueryFromContext(ctx)
// Parse negative terms and compile them as case-insensitive regexps
posTerms, negTerms := SplitQueryTerms(originalQuery)
var negRegexps []*regexp.Regexp
for _, t := range negTerms {
re, err := regexpFor(t, []string{"i"}, true)
if err != nil {
return fmt.Errorf("failed to compile negative term regexp '%s': %w", t, err)
}
negRegexps = append(negRegexps, re)
}
// Reconstruct the fuzzy query from positive terms joined together
fuzzyQuery := strings.Join(posTerms, "")
hasUpper := util.ContainsUpper(fuzzyQuery)
var matched []fuzzyMatchedItem
LINE:
for i, l := range lines {
if err := checkCancelled(ctx, i); err != nil {
return err
}
txt := l.DisplayString()
// Check negative terms first — skip if any match
if isExcluded(negRegexps, txt) {
continue LINE
}
// All-negative query: emit all non-excluded lines with nil indices
if len(fuzzyQuery) == 0 {
em.Emit(ctx, line.GetMatched(l, nil))
continue LINE
}
// Find the first valid rune of the query
firstRune := utf8.RuneError
for _, r := range fuzzyQuery {
if r != utf8.RuneError {
firstRune = r
break
}
}
if firstRune == utf8.RuneError {
return fmt.Errorf("the query has no valid character")
}
// Find all byte offsets where the first query rune appears,
// reusing txt from line 77 instead of re-fetching DisplayString().
var firstRuneOffsets []int
remaining := txt
base := 0
for len(remaining) > 0 {
var idx int
if hasUpper {
idx = strings.IndexRune(remaining, firstRune)
} else {
idx = util.CaseInsensitiveIndex(remaining, firstRune)
}
if idx == -1 {
break
}
firstRuneOffsets = append(firstRuneOffsets, base+idx)
if !ff.sortLongest {
break
}
_, n := utf8.DecodeRuneInString(remaining[idx:])
remaining = remaining[idx+n:]
base += idx + n
}
if len(firstRuneOffsets) == 0 {
continue LINE
}
// Find all candidate matches
var candidates []fuzzyMatchedItem
// Pre-allocate backing array for match pairs to avoid per-match
// heap allocations. Each match is a [start, end] pair, so we need
// 2 ints per query rune (upper bound).
queryRuneCount := utf8.RuneCountInString(fuzzyQuery)
matchBacking := make([]int, 0, queryRuneCount*2)
OUTER:
for _, offset := range firstRuneOffsets {
q := fuzzyQuery
candidateTxt := txt[offset:]
candidateBase := offset
matchBacking = matchBacking[:0]
matches := make([][]int, 0, queryRuneCount)
for len(q) > 0 {
var r rune
var n int
q, r, n = popRune(q)
if r == utf8.RuneError {
continue OUTER
}
var idx int
if hasUpper {
idx = strings.IndexRune(candidateTxt, r)
} else {
idx = util.CaseInsensitiveIndex(candidateTxt, r)
}
if idx == -1 {
continue OUTER
}
candidateTxt = candidateTxt[idx+n:]
start := candidateBase + idx
matchBacking = append(matchBacking, start, start+n)
matches = append(matches, matchBacking[len(matchBacking)-2:len(matchBacking):len(matchBacking)])
candidateBase = start + n
}
candidates = append(candidates, newFuzzyMatchedItem(l, matches))
}
if len(candidates) == 0 {
continue
}
if ff.sortLongest {
// Sort the candidate matches of a line and pick the best one
sort.SliceStable(candidates, less(candidates))
}
matched = append(matched, candidates[0])
}
if ff.sortLongest {
// Sort all matched lines
sort.SliceStable(matched, less(matched))
}
for i := range matched {
em.Emit(ctx, line.GetMatched(matched[i].line, matched[i].matches))
}
return nil
}
// popRune decodes and removes the first rune from s, returning the remainder,
// the decoded rune, and its byte width.
func popRune(s string) (string, rune, int) {
r, n := utf8.DecodeRuneInString(s)
return s[n:], r, n
}
// less returns a comparison function that orders fuzzy matches by longest
// contiguous match, earliest position, then shortest line length.
func less(s []fuzzyMatchedItem) func(i, j int) bool {
return func(i, j int) bool {
if s[i].longest != s[j].longest {
// Longer match is better
return s[i].longest > s[j].longest
}
if s[i].earliest != s[j].earliest {
// Earlier match is better
return s[i].earliest < s[j].earliest
}
// Shorter line is better
return s[i].Len() < s[j].Len()
}
}
type fuzzyMatchedItem struct {
line line.Line
matches [][]int
longest 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 {
longest := 0
count := 0
lastEnd := 0
earliest := math.MaxInt
for i := range matches {
length := matches[i][1] - matches[i][0]
if matches[i][0] == lastEnd {
count += length
} else {
count = length
}
if count > longest {
longest = count
}
lastEnd = matches[i][1]
if matches[i][0] < earliest {
earliest = matches[i][0]
}
}
return fuzzyMatchedItem{
line: line,
matches: matches,
longest: longest,
earliest: earliest,
}
}
func (f fuzzyMatchedItem) Len() int {
return len(f.line.DisplayString())
}

121
filter/parallel_test.go Normal file
View file

@ -0,0 +1,121 @@
package filter
import (
"context"
"fmt"
"testing"
"time"
"github.com/peco/peco/line"
"github.com/peco/peco/pipeline"
"github.com/stretchr/testify/require"
)
func TestSupportsParallel(t *testing.T) {
tests := []struct {
name string
filter Filter
expected bool
}{
{"Regexp", NewRegexp(), true},
{"IgnoreCase", NewIgnoreCase(), true},
{"CaseSensitive", NewCaseSensitive(), true},
{"SmartCase", NewSmartCase(), true},
{"IRegexp", NewIRegexp(), true},
{"Fuzzy(sortLongest=false)", NewFuzzy(false), true},
{"Fuzzy(sortLongest=true)", NewFuzzy(true), false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.expected, tt.filter.SupportsParallel())
})
}
}
func TestParallelFilterProducesSameResults(t *testing.T) {
// Generate test lines
const numLines = 5000
lines := make([]line.Line, numLines)
for i := range numLines {
text := fmt.Sprintf("line-%04d foo bar baz", i)
if i%3 == 0 {
text = fmt.Sprintf("line-%04d matching-pattern test", i)
}
lines[i] = line.NewRaw(uint64(i), text, false, false)
}
filters := []struct {
name string
filter Filter
query string
}{
{"IgnoreCase", NewIgnoreCase(), "matching"},
{"CaseSensitive", NewCaseSensitive(), "matching"},
{"Regexp", NewRegexp(), "matching.*test"},
{"Fuzzy", NewFuzzy(false), "mpt"},
}
for _, ft := range filters {
t.Run(ft.name, func(t *testing.T) {
ctx := ft.filter.NewContext(context.Background(), ft.query)
// Run sequentially
seqCh := make(chan line.Line, numLines)
err := ft.filter.Apply(ctx, lines, pipeline.ChanOutput(seqCh))
require.NoError(t, err)
close(seqCh)
var seqResults []string
for l := range seqCh {
seqResults = append(seqResults, l.DisplayString())
}
// Run on chunks (simulating parallel) - split into multiple chunks
chunkSize := 500
var parResults []string
for start := 0; start < len(lines); start += chunkSize {
end := min(start+chunkSize, len(lines))
chunk := lines[start:end]
ch := make(chan line.Line, len(chunk))
err := ft.filter.Apply(ctx, chunk, pipeline.ChanOutput(ch))
require.NoError(t, err)
close(ch)
for l := range ch {
parResults = append(parResults, l.DisplayString())
}
}
// Results should be identical in count and order
require.Equal(t, len(seqResults), len(parResults), "result count should match")
require.Equal(t, seqResults, parResults, "results should be identical and in same order")
})
}
}
func TestParallelFilterContextCancellation(t *testing.T) {
// Generate a large set of lines
const numLines = 100000
lines := make([]line.Line, numLines)
for i := range numLines {
lines[i] = line.NewRaw(uint64(i), fmt.Sprintf("line-%d matching-pattern", i), false, false)
}
f := NewIgnoreCase()
ctx, cancel := context.WithCancel(f.NewContext(context.Background(), "matching"))
// Cancel after a short delay
go func() {
time.Sleep(1 * time.Millisecond)
cancel()
}()
ch := make(chan line.Line, numLines)
err := f.Apply(ctx, lines, pipeline.ChanOutput(ch))
// Should return context error (cancelled)
require.Error(t, err)
require.ErrorIs(t, err, context.Canceled)
}

334
filter/regexp.go Normal file
View file

@ -0,0 +1,334 @@
package filter
import (
"context"
"fmt"
"regexp"
"sort"
"strings"
"sync"
"time"
"github.com/peco/peco/internal/util"
"github.com/peco/peco/line"
"github.com/peco/peco/pipeline"
)
// internal stuff
type regexpFlags interface {
flags(string) []string
}
type regexpFlagList []string
type regexpFlagFunc func(string) []string
type regexpQueryFactory struct {
compiled map[string]regexpQuery
mutex sync.Mutex
threshold time.Duration
}
type regexpQuery struct {
positive []*regexp.Regexp
negative []*regexp.Regexp
lastUsed time.Time
}
var ignoreCaseFlags = regexpFlagList([]string{"i"})
var defaultFlags = regexpFlagList{}
// Regexp is a filter that matches lines using regular expressions.
type Regexp struct {
baseFilter
factory *regexpQueryFactory
flags regexpFlags
quotemeta bool
name string
}
func (r regexpFlagList) flags(_ string) []string {
return []string(r)
}
func (r regexpFlagFunc) flags(s string) []string {
return r(s)
}
// regexpFor compiles q into a regexp, optionally quoting meta characters
// and prepending inline flags (e.g. case-insensitive).
func regexpFor(q string, flags []string, quotemeta bool) (*regexp.Regexp, error) {
reTxt := q
if quotemeta {
reTxt = regexp.QuoteMeta(q)
}
if len(flags) > 0 {
reTxt = fmt.Sprintf("(?%s)%s", strings.Join(flags, ""), reTxt)
}
re, err := regexp.Compile(reTxt)
if err != nil {
return nil, fmt.Errorf("failed to compile regular expression '%s': %w", reTxt, err)
}
return re, nil
}
// SplitQueryTerms splits a query string into positive and negative term slices.
// Terms starting with `-` (followed by at least one non-hyphen char) are negative (the `-` is stripped).
// Terms starting with `\-` are positive literals (the `\` is stripped).
// Bare `-` or `--` are positive literals.
// Empty tokens are skipped.
func SplitQueryTerms(query string) (positive, negative []string) {
tokens := strings.SplitSeq(strings.TrimSpace(query), " ")
for tok := range tokens {
if tok == "" {
continue
}
if strings.HasPrefix(tok, `\-`) {
// Escaped negative: treat as literal positive term (strip the backslash)
positive = append(positive, tok[1:])
} else if tok == "-" || tok == "--" {
// Bare hyphen(s): literal positive
positive = append(positive, tok)
} else if strings.HasPrefix(tok, "-") {
// Negative term: strip the leading hyphen
negative = append(negative, tok[1:])
} else {
positive = append(positive, tok)
}
}
return
}
// termsToRegexps compiles a slice of terms into regexps, using the full
// original query for flag computation (needed for SmartCase).
func termsToRegexps(terms []string, fullQuery string, flags regexpFlags, quotemeta bool) ([]*regexp.Regexp, error) {
regexps := make([]*regexp.Regexp, 0, len(terms))
for _, t := range terms {
re, err := regexpFor(t, flags.flags(fullQuery), quotemeta)
if err != nil {
return nil, fmt.Errorf("failed to compile regular expression '%s': %w", t, err)
}
regexps = append(regexps, re)
}
return regexps, nil
}
// newRegexpFilter is an internal helper that constructs a Regexp filter
// with the given name, flags, and quotemeta setting.
func newRegexpFilter(name string, flags regexpFlags, quotemeta bool) *Regexp {
rf := &Regexp{
factory: &regexpQueryFactory{
compiled: make(map[string]regexpQuery),
threshold: time.Minute,
},
flags: flags,
quotemeta: quotemeta,
name: name,
}
rf.impl = rf
return rf
}
// NewRegexp creates a new regexp based filter
func NewRegexp() *Regexp {
return newRegexpFilter("Regexp", defaultFlags, false)
}
// NewIRegexp creates a new case-insensitive regexp based filter
func NewIRegexp() *Regexp {
return newRegexpFilter("IRegexp", ignoreCaseFlags, false)
}
const maxRegexpCacheSize = 100
// evictLRU removes the least recently used entries from the cache until its
// size is at or below targetSize. The caller must hold f.mutex.
func (f *regexpQueryFactory) evictLRU(targetSize int) {
type cacheEntry struct {
key string
lastUsed time.Time
}
entries := make([]cacheEntry, 0, len(f.compiled))
for k, v := range f.compiled {
entries = append(entries, cacheEntry{key: k, lastUsed: v.lastUsed})
}
sort.Slice(entries, func(i, j int) bool {
return entries[i].lastUsed.Before(entries[j].lastUsed)
})
toRemove := len(f.compiled) - targetSize
for i := range toRemove {
delete(f.compiled, entries[i].key)
}
}
// 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) ([]*regexp.Regexp, []*regexp.Regexp, error) {
f.mutex.Lock()
defer f.mutex.Unlock()
rq, ok := f.compiled[s]
if ok {
if time.Since(rq.lastUsed) < f.threshold {
return rq.positive, rq.negative, nil
}
delete(f.compiled, s)
}
posTerms, negTerms := SplitQueryTerms(s)
var posRxs, negRxs []*regexp.Regexp
if len(posTerms) > 0 {
var err error
posRxs, err = termsToRegexps(posTerms, s, flags, quotemeta)
if err != nil {
return nil, nil, fmt.Errorf("failed to compile positive regular expressions: %w", err)
}
}
if len(negTerms) > 0 {
var err error
negRxs, err = termsToRegexps(negTerms, s, flags, quotemeta)
if err != nil {
return nil, nil, fmt.Errorf("failed to compile negative regular expressions: %w", err)
}
}
// Evict stale entries if cache is over the size limit
if len(f.compiled) >= maxRegexpCacheSize {
for k, v := range f.compiled {
if time.Since(v.lastUsed) >= f.threshold {
delete(f.compiled, k)
}
}
// If still over limit after evicting stale entries, evict the
// least recently used entries to bring cache down to half capacity.
if len(f.compiled) >= maxRegexpCacheSize {
f.evictLRU(maxRegexpCacheSize / 2)
}
}
rq.lastUsed = time.Now()
rq.positive = posRxs
rq.negative = negRxs
f.compiled[s] = rq
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, em LineEmitter) error {
query := pipeline.QueryFromContext(ctx)
posRegexps, negRegexps, err := rf.factory.Compile(query, rf.flags, rf.quotemeta)
if err != nil {
return fmt.Errorf("failed to compile queries as regular expression: %w", err)
}
// Pre-allocate match and dedup slices outside the loop so they can
// be reused across lines, avoiding per-line heap allocations.
var matches [][]int
var deduped [][]int
for i, l := range lines {
if err := checkCancelled(ctx, i); err != nil {
return err
}
v := l.DisplayString()
// Check negative terms first (fail-fast, no index collection)
if isExcluded(negRegexps, v) {
continue
}
// All-negative query: emit line with nil indices (no highlighting)
if len(posRegexps) == 0 {
em.Emit(ctx, line.GetMatched(l, nil))
continue
}
// Positive matching (existing AND logic)
allMatched := true
matches = matches[:0]
TryRegexps:
for _, rx := range posRegexps {
match := rx.FindAllStringIndex(v, -1)
if match == nil {
allMatched = false
break TryRegexps
}
matches = append(matches, match...)
}
if !allMatched {
continue
}
sort.Sort(byMatchStart(matches))
// We need to "dedupe" the results. For example, if we matched the
// same region twice, we don't want that to be drawn
deduped = deduped[:0]
for j, m := range matches {
// Always push the first one
if j == 0 {
deduped = append(deduped, m)
continue
}
prev := deduped[len(deduped)-1]
switch {
case matchContains(prev, m):
// If the previous match contains this one, then
// don't do anything
continue
case matchOverlaps(prev, m):
// If the previous match overlaps with this one,
// merge the results and make it a bigger one
deduped[len(deduped)-1] = mergeMatches(prev, m)
default:
deduped = append(deduped, m)
}
}
// Make a copy of deduped for emission since we reuse the slice
result := make([][]int, len(deduped))
copy(result, deduped)
em.Emit(ctx, line.GetMatched(l, result))
}
return nil
}
func (rf *Regexp) SupportsParallel() bool {
return true
}
func (rf *Regexp) String() string {
return rf.name
}
// NewIgnoreCase creates a case-insensitive literal string filter.
func NewIgnoreCase() *Regexp {
return newRegexpFilter("IgnoreCase", ignoreCaseFlags, true)
}
// NewCaseSensitive creates a case-sensitive literal string filter.
func NewCaseSensitive() *Regexp {
return newRegexpFilter("CaseSensitive", defaultFlags, true)
}
// NewSmartCase creates a filter that turns ON the ignore-case flag in the regexp
// if the query contains no upper-case character
func NewSmartCase() *Regexp {
return newRegexpFilter("SmartCase", regexpFlagFunc(func(q string) []string {
if util.ContainsUpper(q) {
return defaultFlags
}
return []string{"i"}
}), true)
}

80
filter/set.go Normal file
View file

@ -0,0 +1,80 @@
package filter
import (
"errors"
"sync"
pdebug "github.com/lestrrat-go/pdebug"
)
// ErrFilterNotFound is returned when a filter name does not match any
// registered filter in the Set.
var ErrFilterNotFound = errors.New("specified filter was not found")
// Set holds the collection of available filters and tracks which one
// is currently active.
type Set struct {
current int
filters []Filter
mutex sync.Mutex
}
// Reset sets the active filter back to the first one in the set.
func (fs *Set) Reset() {
fs.mutex.Lock()
defer fs.mutex.Unlock()
fs.current = 0
}
// Size returns the number of filters in the set.
func (fs *Set) Size() int {
fs.mutex.Lock()
defer fs.mutex.Unlock()
return len(fs.filters)
}
// Add appends a new filter to the set.
func (fs *Set) Add(lf Filter) {
fs.mutex.Lock()
defer fs.mutex.Unlock()
fs.filters = append(fs.filters, lf)
}
// Rotate cycles to the next filter in the set, wrapping around to the first.
func (fs *Set) Rotate() {
fs.mutex.Lock()
defer fs.mutex.Unlock()
fs.current++
if fs.current >= len(fs.filters) {
fs.current = 0
}
if pdebug.Enabled {
pdebug.Printf("Set.Rotate: now filter in effect is %s", fs.filters[fs.current])
}
}
// SetCurrentByName switches the active filter to the one matching the given
// name, returning ErrFilterNotFound if no filter matches.
func (fs *Set) SetCurrentByName(name string) error {
fs.mutex.Lock()
defer fs.mutex.Unlock()
for i, f := range fs.filters {
if f.String() == name {
fs.current = i
return nil
}
}
return ErrFilterNotFound
}
func (fs *Set) Index() int {
fs.mutex.Lock()
defer fs.mutex.Unlock()
return fs.current
}
func (fs *Set) Current() Filter {
fs.mutex.Lock()
defer fs.mutex.Unlock()
return fs.filters[fs.current]
}

386
filter_incremental_test.go Normal file
View file

@ -0,0 +1,386 @@
package peco
import (
"context"
"errors"
"fmt"
"runtime"
"sync"
"testing"
"github.com/peco/peco/filter"
"github.com/peco/peco/hub"
"github.com/peco/peco/line"
"github.com/peco/peco/pipeline"
"github.com/peco/peco/selection"
"github.com/stretchr/testify/require"
)
func TestIsQueryRefinement(t *testing.T) {
tests := []struct {
prev string
new string
expected bool
}{
// Refinements
{"foo", "foob", true},
{"foo", "foo bar", true},
{"foo bar", "foo bart", true},
{"f", "fo", true},
{"foo", "foobar", true},
// Not refinements
{"foob", "foo", false}, // backspace
{"foo bar", "foo baz", false}, // edit mid-word
{"foo", "bar", false}, // completely different
{"", "foo", false}, // empty prev
{"foo", "", false}, // empty new
{"", "", false}, // both empty
// Whitespace handling
{" foo ", " foo bar", true},
{" foo ", " foo bar", true},
}
for _, tt := range tests {
name := fmt.Sprintf("%q->%q", tt.prev, tt.new)
t.Run(name, func(t *testing.T) {
result := isQueryRefinement(tt.prev, tt.new)
require.Equal(t, tt.expected, result)
})
}
}
func TestIsQueryRefinementWithNegation(t *testing.T) {
tests := []struct {
prev string
new string
expected bool
}{
// Adding a negative term is a refinement (narrows results)
{"foo", "foo -bar", true},
// Adding more negative terms is still a refinement
{"foo -bar", "foo -bar -baz", true},
// Removing a negative term is NOT a refinement (widens results)
{"foo -bar -baz", "foo -bar", false},
// Extending positive while keeping negatives
{"foo -bar", "fooX -bar", true},
// All-negative refinement
{"-foo", "-foo -bar", true},
// Changing a negative term is not a refinement
{"-foo", "-bar", false},
// Adding positive to all-negative is a refinement
{"-foo", "hello -foo", true},
}
for _, tt := range tests {
name := fmt.Sprintf("%q->%q", tt.prev, tt.new)
t.Run(name, func(t *testing.T) {
result := isQueryRefinement(tt.prev, tt.new)
require.Equal(t, tt.expected, result)
})
}
}
func TestMemoryBufferSource(t *testing.T) {
// Create and populate a MemoryBuffer
mb := NewMemoryBuffer(0)
expected := []string{"alpha", "bravo", "charlie", "delta", "echo"}
for i, s := range expected {
mb.lines = append(mb.lines, line.NewRaw(uint64(i), s, false, false))
}
// Wrap as source
src := NewMemoryBufferSource(mb)
// Collect lines from source
ctx := context.Background()
out := make(chan line.Line, len(expected))
go src.Start(ctx, pipeline.ChanOutput(out))
var got []string
for l := range out {
got = append(got, l.DisplayString())
}
require.Equal(t, expected, got, "MemoryBufferSource should iterate all lines in order")
}
func TestMemoryBufferSourceCancellation(t *testing.T) {
mb := NewMemoryBuffer(0)
for i := range 10000 {
mb.lines = append(mb.lines, line.NewRaw(uint64(i), fmt.Sprintf("line-%d", i), false, false))
}
src := NewMemoryBufferSource(mb)
ctx, cancel := context.WithCancel(context.Background())
out := make(chan line.Line, 100)
done := make(chan struct{})
go func() {
src.Start(ctx, pipeline.ChanOutput(out))
close(done)
}()
// Cancel immediately
cancel()
<-done
// Source closed the channel; drain to count lines sent
count := 0
for range out {
count++
}
// We can't guarantee exact count due to race, but it should be less than total
// (or equal if the cancellation happened after all sends)
require.LessOrEqual(t, count, 10000)
}
// TestAcceptAndFilterSerial exercises the serial (non-parallel) batching path
// through AcceptAndFilter. It sends lines through a channel and verifies
// that the filter is applied correctly and all matching lines appear in the output.
func TestAcceptAndFilterSerial(t *testing.T) {
f := filter.NewFuzzy(true) // Fuzzy with sortLongest=true does NOT support parallel
require.False(t, f.SupportsParallel(), "Fuzzy with sortLongest should not support parallel")
inputLines := []line.Line{
line.NewRaw(0, "alpha bravo", false, false),
line.NewRaw(1, "charlie delta", false, false),
line.NewRaw(2, "alpha charlie", false, false),
line.NewRaw(3, "echo foxtrot", false, false),
line.NewRaw(4, "alpha delta", false, false),
}
ctx := f.NewContext(context.Background(), "alpha")
in := make(chan line.Line, len(inputLines))
for _, l := range inputLines {
in <- l
}
close(in)
out := make(chan line.Line, len(inputLines))
AcceptAndFilter(ctx, f, 0, in, pipeline.ChanOutput(out))
var got []string
for l := range out {
got = append(got, l.DisplayString())
}
// Fuzzy with sortLongest sorts by: longer match > earlier match > shorter line.
// All three match "alpha" at position 0 with length 5, so the tiebreaker is
// line length (shorter first). "alpha bravo" and "alpha delta" tie at 11 chars,
// so sort.SliceStable preserves their input order.
require.Equal(t, []string{"alpha bravo", "alpha delta", "alpha charlie"}, got)
}
// TestAcceptAndFilterParallel exercises the parallel batching path.
// IgnoreCase (a Regexp filter) supports parallel execution.
func TestAcceptAndFilterParallel(t *testing.T) {
if runtime.GOMAXPROCS(0) < 2 {
t.Skip("parallel path requires GOMAXPROCS >= 2")
}
f := filter.NewIgnoreCase()
require.True(t, f.SupportsParallel(), "IgnoreCase should support parallel")
inputLines := []line.Line{
line.NewRaw(0, "foobar test", false, false),
line.NewRaw(1, "football game", false, false),
line.NewRaw(2, "something else", false, false),
line.NewRaw(3, "foobaz entry", false, false),
line.NewRaw(4, "barfoo other", false, false),
line.NewRaw(5, "the foobird flies", false, false),
line.NewRaw(6, "no match here", false, false),
}
ctx := f.NewContext(context.Background(), "foo")
in := make(chan line.Line, len(inputLines))
for _, l := range inputLines {
in <- l
}
close(in)
out := make(chan line.Line, len(inputLines))
AcceptAndFilter(ctx, f, 0, in, pipeline.ChanOutput(out))
var got []string
for l := range out {
got = append(got, l.DisplayString())
}
// Parallel preserves input order via ordered chunks — verify exact order
expected := []string{"foobar test", "football game", "foobaz entry", "barfoo other", "the foobird flies"}
require.Equal(t, expected, got)
}
func TestIncrementalFiltering(t *testing.T) {
// Test that filtering "foo" then "foob" produces correct results
// and the second query runs on fewer lines
// Create test lines
allLines := []line.Line{
line.NewRaw(0, "foobar test", false, false),
line.NewRaw(1, "football game", false, false),
line.NewRaw(2, "barfoo other", false, false),
line.NewRaw(3, "something else", false, false),
line.NewRaw(4, "foobaz entry", false, false),
line.NewRaw(5, "the foobird flies", false, false),
}
f := filter.NewIgnoreCase()
// First query: "foo"
ctx1 := f.NewContext(context.Background(), "foo")
ch1 := make(chan line.Line, len(allLines))
err := f.Apply(ctx1, allLines, pipeline.ChanOutput(ch1))
require.NoError(t, err)
close(ch1)
var firstResults []line.Line
for l := range ch1 {
firstResults = append(firstResults, l)
}
// Should match: foobar, football, barfoo, foobaz, foobird
require.Len(t, firstResults, 5, "first query should match 5 lines")
// Second query: "foob" on first results only
ctx2 := f.NewContext(context.Background(), "foob")
ch2 := make(chan line.Line, len(firstResults))
err = f.Apply(ctx2, firstResults, pipeline.ChanOutput(ch2))
require.NoError(t, err)
close(ch2)
var secondResults []line.Line
for l := range ch2 {
secondResults = append(secondResults, l)
}
// Should match: foobar, foobaz, foobird (not football, not barfoo)
require.Len(t, secondResults, 3, "second query should match 3 lines")
// Verify the matched lines are correct
displayStrings := make([]string, 0, len(secondResults))
for _, l := range secondResults {
displayStrings = append(displayStrings, l.DisplayString())
}
require.Contains(t, displayStrings, "foobar test")
require.Contains(t, displayStrings, "foobaz entry")
require.Contains(t, displayStrings, "the foobird flies")
}
// TestFrozenCacheInvalidation verifies that the incremental filter cache
// is invalidated when the frozen state changes. Without this fix, cached
// results from the original source would be reused after freezing, producing
// wrong results.
func TestFrozenCacheInvalidation(t *testing.T) {
// Set up a Peco state with a recording hub
state := New()
rHub := &recordingHub{}
state.hub = rHub
state.selection = selection.New()
// Populate filters (needed by Filter.Work)
state.filters.Add(filter.NewIgnoreCase())
// Create original source with lines that include "apache" matches
src := &Source{}
src.setupDone = make(chan struct{})
for i, s := range []string{"apache config", "apache error", "APACHE LOG", "nginx proxy", "redis cache"} {
src.Append(line.NewRaw(uint64(i), s, false, false))
}
close(src.setupDone)
state.source = src
state.currentLineBuffer = src
// Create a Filter and simulate a previous "apache" query
f := NewFilter(state)
// Run a real filter for "apache" to populate the cache
q1 := hub.NewPayload("apache", false)
f.Work(context.Background(), q1)
// Verify cache was populated from original source (3 matches: apache config, apache error, APACHE LOG)
f.prevMu.Lock()
require.Equal(t, "apache", f.prevQuery)
require.NotNil(t, f.prevResults)
require.Equal(t, 3, f.prevResults.Size(), "should have 3 'apache' matches from original source")
require.Nil(t, f.prevFrozenSrc, "prevFrozenSrc should be nil (not frozen)")
f.prevMu.Unlock()
// Now freeze with a subset: only "apache config" and "nginx proxy"
frozen := NewMemoryBuffer(0)
frozen.AppendLine(line.NewRaw(0, "apache config", false, false))
frozen.AppendLine(line.NewRaw(3, "nginx proxy", false, false))
frozen.MarkComplete()
state.Frozen().Set(frozen)
// Query "apache c" — this is a refinement of "apache", so without the fix
// the stale cache (3 results from original source) would be used instead
// of filtering from the frozen buffer (which has only 1 apache match).
q2 := hub.NewPayload("apache c", false)
f.Work(context.Background(), q2)
// The result should come from the frozen buffer, not the stale cache.
// Frozen buffer has "apache config" and "nginx proxy". Filtering for
// "apache c" should match only "apache config".
buf := state.CurrentLineBuffer()
require.Equal(t, 1, buf.Size(), "should have 1 match from frozen buffer")
l, err := buf.LineAt(0)
require.NoError(t, err)
require.Equal(t, "apache config", l.Buffer())
// Verify cache is now updated with frozen source
f.prevMu.Lock()
require.Equal(t, frozen, f.prevFrozenSrc, "prevFrozenSrc should match current frozen source")
f.prevMu.Unlock()
}
// errorFilter is a mock filter that always returns an error from Apply.
type errorFilter struct {
err error
}
func (f *errorFilter) Apply(_ context.Context, _ []line.Line, _ pipeline.ChanOutput) error {
return f.err
}
func (f *errorFilter) BufSize() int { return 0 }
func (f *errorFilter) NewContext(ctx context.Context, _ string) context.Context { return ctx }
func (f *errorFilter) String() string { return "error-filter" }
func (f *errorFilter) SupportsParallel() bool { return false }
// TestFilterApplyErrorReporting verifies that when a filter's Apply method
// returns an error, the error is propagated to the onError callback (which in
// production sends a status bar message to the user).
func TestFilterApplyErrorReporting(t *testing.T) {
simulatedErr := errors.New("simulated filter error")
ef := &errorFilter{err: simulatedErr}
inputLines := []line.Line{
line.NewRaw(0, "alpha", false, false),
line.NewRaw(1, "bravo", false, false),
}
in := make(chan line.Line, len(inputLines))
for _, l := range inputLines {
in <- l
}
close(in)
var mu sync.Mutex
var reported []error
onError := FilterErrorHandlerFunc(func(err error) {
mu.Lock()
defer mu.Unlock()
reported = append(reported, err)
})
out := make(chan line.Line, len(inputLines))
acceptAndFilter(context.Background(), ef, 0, onError, in, pipeline.ChanOutput(out))
mu.Lock()
defer mu.Unlock()
require.Len(t, reported, 1, "error handler should have been called once")
require.Equal(t, simulatedErr, reported[0])
}

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")
}

25
go.mod Normal file
View file

@ -0,0 +1,25 @@
module github.com/peco/peco
go 1.25.0
require (
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.28
github.com/stretchr/testify v1.12.1
)
require (
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
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/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
)

63
go.sum Normal file
View file

@ -0,0 +1,63 @@
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
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.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=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4=
github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc=
github.com/lestrrat-go/pdebug v0.0.0-20180220043849-39f9a71bcabe h1:S7XSBlgc/eI2v47LkPPVa+infH3FuTS4tPJbqCtJovo=
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.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.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=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
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=

29
hub/bench_test.go Normal file
View file

@ -0,0 +1,29 @@
package hub_test
import (
"context"
"testing"
"github.com/peco/peco/hub"
)
// BenchmarkHubBatch measures the allocation cost of Hub.Batch context setup.
func BenchmarkHubBatch(b *testing.B) {
h := hub.New(5)
ctx := context.Background()
// Drain query channel and call Done() so batch sends unblock
go func() {
for p := range h.QueryCh() {
p.Done()
}
}()
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
h.Batch(ctx, func(bctx context.Context) {
h.SendQuery(bctx, "test")
})
}
}

10
hub/draw.go Normal file
View file

@ -0,0 +1,10 @@
package hub
// DrawOptions controls how the screen is drawn.
type DrawOptions struct {
Prompt bool // draw only the prompt
PurgeCache bool // purge display cache only
RunningQuery bool // a query is currently running
DisableCache bool // disable display cache
ForceSync bool // force a full screen sync
}

240
hub/hub.go Normal file
View file

@ -0,0 +1,240 @@
package hub
import (
"context"
"fmt"
"os"
"runtime/debug"
"sync"
"time"
pdebug "github.com/lestrrat-go/pdebug"
)
// Hub acts as the messaging hub between components -- that is,
// it controls how the communication that goes through channels
// are handled.
type Hub struct {
mutex sync.Mutex
queryCh chan *Payload[string]
drawCh chan *Payload[*DrawOptions]
statusMsgCh chan *Payload[StatusMsg]
pagingCh chan *Payload[PagingRequest]
}
// Payload is a wrapper around the actual request value that needs
// to be passed. It contains an optional channel field which can
// be filled to force synchronous communication between the
// sender and receiver
type Payload[T any] struct {
batch bool
data T
done chan struct{}
}
// NewPayload creates a new Payload with the given data and batch flag.
func NewPayload[T any](data T, batch bool) *Payload[T] {
return &Payload[T]{
data: data,
batch: batch,
}
}
// Batch returns true if this payload is part of a batch operation.
func (p *Payload[T]) Batch() bool {
return p.batch
}
// Data returns the underlying data.
func (p *Payload[T]) Data() T {
return p.data
}
// Done marks the request as done. In non-batch mode it's a no-op.
// In batch mode it signals the sender that the receiver has finished
// processing this payload.
func (p *Payload[T]) Done() {
if p.done == nil {
return
}
p.done <- struct{}{}
}
// New creates a new Hub struct
func New(bufsiz int) *Hub {
return &Hub{
queryCh: make(chan *Payload[string], bufsiz),
drawCh: make(chan *Payload[*DrawOptions], bufsiz),
statusMsgCh: make(chan *Payload[StatusMsg], bufsiz),
pagingCh: make(chan *Payload[PagingRequest], bufsiz),
}
}
// batchCtxKey is a single context key that signals both "this is a batch
// payload" and "the hub mutex is already held" for re-entrant detection.
// Using one key instead of two avoids a second context.WithValue allocation.
type batchCtxKey struct{}
// Batch allows you to synchronously send messages during the
// scope of f() being executed. The mutex is acquired automatically
// unless this is a nested Batch call (detected via context).
func (h *Hub) Batch(ctx context.Context, f func(ctx context.Context)) {
nested, _ := ctx.Value(batchCtxKey{}).(bool)
if pdebug.Enabled {
g := pdebug.Marker("Batch (nested=%t)", nested)
defer g.End()
}
if !nested {
h.mutex.Lock()
defer h.mutex.Unlock()
}
// Log and re-panic instead of silently swallowing. The mutex
// unlock defer (above) still runs during re-panic, so the hub
// won't deadlock.
defer func() {
if r := recover(); r != nil {
fmt.Fprintf(os.Stderr, "peco: panic in Hub.Batch: %v\n%s", r, debug.Stack())
panic(r)
}
}()
batchCtx := context.WithValue(ctx, batchCtxKey{}, true)
f(batchCtx)
}
var doneChPool = sync.Pool{
New: func() any {
return make(chan struct{})
},
}
// waitDone blocks until the receiver signals completion by calling Done.
func (p *Payload[T]) waitDone() {
// Save the channel reference before blocking. This read is safe because
// p.done was set by send() on this same goroutine before the payload
// was sent on the hub channel.
ch := p.done
// Block until the receiver calls Done(), which sends on ch.
<-ch
// After the receive, the receiver is finished with p.done (it already
// sent on it), so this goroutine has exclusive access. Clear the field
// and return the channel to the pool.
p.done = nil
doneChPool.Put(ch)
}
// isBatchCtx reports whether the context was created by a Batch call.
func isBatchCtx(ctx context.Context) bool {
v, _ := ctx.Value(batchCtxKey{}).(bool)
return v
}
// send is the low-level generic utility for sending typed payloads.
// Batch mode is determined from the payload's Batch() flag, which is
// set by each Send* method via isBatchCtx before calling send.
// The context is used for cancellation so sends don't block forever
// during shutdown.
func send[T any](ctx context.Context, ch chan *Payload[T], r *Payload[T]) {
if pdebug.Enabled {
g := pdebug.Marker("hub.send (isBatchMode=%t)", r.Batch())
defer g.End()
}
if r.Batch() {
// The pool only ever contains chan struct{} values (see doneChPool.Put
// in Payload.Done), so this assertion cannot fail in practice.
r.done, _ = doneChPool.Get().(chan struct{})
if pdebug.Enabled {
defer pdebug.Printf("request is part of batch operation. waiting")
}
defer r.waitDone()
}
select {
case ch <- r:
case <-ctx.Done():
}
}
// QueryCh returns the underlying channel for queries
func (h *Hub) QueryCh() chan *Payload[string] {
return h.queryCh
}
// SendQuery sends the query string to be processed by the Filter
func (h *Hub) SendQuery(ctx context.Context, q string) {
send(ctx, h.QueryCh(), NewPayload(q, isBatchCtx(ctx)))
}
// DrawCh returns the channel to redraw the terminal display
func (h *Hub) DrawCh() chan *Payload[*DrawOptions] {
return h.drawCh
}
// SendDrawPrompt sends a request to redraw the prompt only
func (h *Hub) SendDrawPrompt(ctx context.Context) {
send(ctx, h.DrawCh(), NewPayload(&DrawOptions{Prompt: true}, isBatchCtx(ctx)))
}
// SendDraw sends a request to redraw the terminal display
func (h *Hub) SendDraw(ctx context.Context, options *DrawOptions) {
if pdebug.Enabled {
pdebug.Printf("START Hub.SendDraw %v", options)
defer pdebug.Printf("END Hub.SendDraw %v", options)
}
send(ctx, h.DrawCh(), NewPayload(options, isBatchCtx(ctx)))
}
// StatusMsgCh returns the channel to update the status message
func (h *Hub) StatusMsgCh() chan *Payload[StatusMsg] {
return h.statusMsgCh
}
// SendStatusMsg sends a string to be displayed in the status message.
// If clearDelay is non-zero, the message will be cleared after that duration.
func (h *Hub) SendStatusMsg(ctx context.Context, q string, clearDelay time.Duration) {
msg := newStatusMsgReq(q, clearDelay)
send(ctx, h.StatusMsgCh(), NewPayload[StatusMsg](msg, isBatchCtx(ctx)))
}
// StatusMsg is an interface for status message requests.
type StatusMsg interface {
Message() string
Delay() time.Duration
}
type statusMsgReq struct {
msg string
delay time.Duration
}
func (r statusMsgReq) Message() string {
return r.msg
}
func (r statusMsgReq) Delay() time.Duration {
return r.delay
}
// newStatusMsgReq creates a StatusMsg with the given message text and display duration.
func newStatusMsgReq(s string, d time.Duration) *statusMsgReq {
return &statusMsgReq{
msg: s,
delay: d,
}
}
// PagingCh returns the channel to page through the results
func (h *Hub) PagingCh() chan *Payload[PagingRequest] {
return h.pagingCh
}
// SendPaging sends a request to move the cursor around
func (h *Hub) SendPaging(ctx context.Context, x PagingRequest) {
send(ctx, h.PagingCh(), NewPayload(x, isBatchCtx(ctx)))
}

187
hub/hub_test.go Normal file
View file

@ -0,0 +1,187 @@
package hub_test
import (
"context"
"testing"
"time"
"github.com/peco/peco/hub"
"github.com/stretchr/testify/require"
)
func TestHub(t *testing.T) {
ctx := context.Background()
h := hub.New(5)
done := make(map[string]time.Time)
go func() {
hr := <-h.QueryCh()
require.Equal(t, "Hello World!", hr.Data())
time.Sleep(100 * time.Millisecond)
done["query"] = time.Now()
hr.Done()
}()
go func() {
hr := <-h.DrawCh()
// Data() returns *hub.DrawOptions directly — no type assertion needed
_ = hr.Data()
time.Sleep(100 * time.Millisecond)
done["draw"] = time.Now()
hr.Done()
}()
go func() {
hr := <-h.StatusMsgCh()
// Data() returns hub.StatusMsg directly — no type assertion needed
r := hr.Data()
require.Equal(t, "Hello, World!", r.Message())
time.Sleep(100 * time.Millisecond)
done["status"] = time.Now()
hr.Done()
}()
go func() {
hr := <-h.PagingCh()
// Data() returns hub.PagingRequest directly — no type assertion needed
r := hr.Data()
require.Equal(t, hub.PagingRequestType(1), r.Type())
time.Sleep(100 * time.Millisecond)
done["paging"] = time.Now()
hr.Done()
}()
h.Batch(ctx, func(ctx context.Context) {
h.SendQuery(ctx, "Hello World!")
h.SendDraw(ctx, &hub.DrawOptions{})
h.SendStatusMsg(ctx, "Hello, World!", 0)
h.SendPaging(ctx, hub.PagingRequestType(1))
})
phases := []string{
"query",
"draw",
"status",
"paging",
}
last := len(phases) - 1
for i := range phases {
if last == i {
break
}
cur := phases[i]
next := phases[i+1]
t.Logf("Checkin if %s was fired before %s", cur, next)
require.False(t, done[next].Before(done[cur]),
"%s executed before %s", next, cur)
}
}
func TestBatchPanicPropagates(t *testing.T) {
h := hub.New(5)
ctx := context.Background()
// A panic inside a Batch callback must propagate to the caller,
// not be silently swallowed.
require.Panics(t, func() {
h.Batch(ctx, func(_ context.Context) {
panic("bug in callback")
})
}, "Batch must not silently swallow panics")
}
func TestBatchPanicReleasesLock(t *testing.T) {
h := hub.New(5)
ctx := context.Background()
// First call: panic inside a locked Batch.
func() {
defer func() { recover() }()
h.Batch(ctx, func(_ context.Context) {
panic("first call panics")
})
}()
// Second call: if the mutex was not released by the first panic,
// this will deadlock. Use a timeout to detect that.
done := make(chan struct{})
go func() {
h.Batch(ctx, func(ctx context.Context) {
// Drain the message so the synchronous send completes.
go func() {
p := <-h.QueryCh()
p.Done()
}()
h.SendQuery(ctx, "after panic")
})
close(done)
}()
select {
case <-done:
// success — mutex was properly released
case <-time.After(2 * time.Second):
require.Fail(t, "Batch deadlocked — mutex was not released after panic")
}
}
func TestBatchNestedDoesNotDeadlock(t *testing.T) {
h := hub.New(5)
ctx := context.Background()
done := make(chan struct{})
go func() {
h.Batch(ctx, func(ctx context.Context) {
// Nested Batch call — must not deadlock
h.Batch(ctx, func(ctx context.Context) {
go func() {
p := <-h.QueryCh()
p.Done()
}()
h.SendQuery(ctx, "nested")
})
})
close(done)
}()
select {
case <-done:
// success — nested Batch did not deadlock
case <-time.After(2 * time.Second):
require.Fail(t, "nested Batch deadlocked")
}
}
func TestSendStatusMsg(t *testing.T) {
t.Run("zero delay", func(t *testing.T) {
h := hub.New(5)
ctx := context.Background()
go func() {
h.SendStatusMsg(ctx, "hello", 0)
}()
p := <-h.StatusMsgCh()
defer p.Done()
require.Equal(t, "hello", p.Data().Message())
require.Equal(t, time.Duration(0), p.Data().Delay())
})
t.Run("non-zero delay", func(t *testing.T) {
h := hub.New(5)
ctx := context.Background()
go func() {
h.SendStatusMsg(ctx, "temporary", 500*time.Millisecond)
}()
p := <-h.StatusMsgCh()
defer p.Done()
require.Equal(t, "temporary", p.Data().Message())
require.Equal(t, 500*time.Millisecond, p.Data().Delay())
})
}

41
hub/paging.go Normal file
View file

@ -0,0 +1,41 @@
//go:generate stringer -type PagingRequestType -output paging_request_type_gen.go
package hub
// PagingRequestType is the type of a paging request
type PagingRequestType int
const (
ToLineAbove PagingRequestType = iota // ToLineAbove moves the selection to the line above
ToScrollPageDown // ToScrollPageDown moves the selection to the next page
ToLineBelow // ToLineBelow moves the selection to the line below
ToScrollPageUp // ToScrollPageUp moves the selection to the previous page
ToScrollLeft // ToScrollLeft scrolls screen to the left
ToScrollRight // ToScrollRight scrolls screen to the right
ToLineInPage // ToLineInPage jumps to a particular line on the page
ToScrollFirstItem // ToScrollFirstItem
ToScrollLastItem // ToScrollLastItem
)
// PagingRequest can be sent to move the selection cursor
type PagingRequest interface {
Type() PagingRequestType
}
// Type satisfies the PagingRequest interface for PagingRequestType itself
func (prt PagingRequestType) Type() PagingRequestType {
return prt
}
// JumpToLineRequest is a PagingRequest that jumps to a specific line
type JumpToLineRequest int
// Type satisfies the PagingRequest interface
func (jlr JumpToLineRequest) Type() PagingRequestType {
return ToLineInPage
}
// Line returns the target line number
func (jlr JumpToLineRequest) Line() int {
return int(jlr)
}

View file

@ -0,0 +1,32 @@
// Code generated by "stringer -type PagingRequestType -output paging_request_type_gen.go"; DO NOT EDIT.
package hub
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[ToLineAbove-0]
_ = x[ToScrollPageDown-1]
_ = x[ToLineBelow-2]
_ = x[ToScrollPageUp-3]
_ = x[ToScrollLeft-4]
_ = x[ToScrollRight-5]
_ = x[ToLineInPage-6]
_ = x[ToScrollFirstItem-7]
_ = x[ToScrollLastItem-8]
}
const _PagingRequestType_name = "ToLineAboveToScrollPageDownToLineBelowToScrollPageUpToScrollLeftToScrollRightToLineInPageToScrollFirstItemToScrollLastItem"
var _PagingRequestType_index = [...]uint8{0, 11, 27, 38, 52, 64, 77, 89, 106, 122}
func (i PagingRequestType) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_PagingRequestType_index)-1 {
return "PagingRequestType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _PagingRequestType_name[_PagingRequestType_index[idx]:_PagingRequestType_index[idx+1]]
}

118
input.go
View file

@ -1,90 +1,68 @@
package peco
import (
"sync"
"time"
"context"
"github.com/nsf/termbox-go"
"github.com/lestrrat-go/pdebug"
)
// Input handles dispatching key events to actions.
type Input struct {
*Ctx
mutex *sync.Mutex // Currently only used for protecting Alt/Esc workaround
mod *time.Timer
keymap Keymap
actions ActionMap
evsrc chan Event
state *Peco
}
func (i *Input) Loop() {
defer i.ReleaseWaitGroup()
// NewInput creates and returns a new Input instance for handling keyboard events.
func NewInput(state *Peco, am ActionMap, src chan Event) *Input {
return &Input{
actions: am,
evsrc: src,
state: state,
}
}
// XXX termbox.PollEvent() can get stuck on unexpected signal
// handling cases. We still would like to wait until the user
// (termbox) has some event for us to process, but we don't
// want to allow termbox to control/block our input loop.
//
// Solution: put termbox polling in a separate goroutine,
// and we just watch for a channel. The loop can now
// safely be implemented in terms of select {} which is
// safe from being stuck.
evCh := make(chan termbox.Event)
go func() {
for {
evCh <- termbox.PollEvent()
}
}()
// Loop runs the main input event loop, reading terminal events and dispatching them.
func (i *Input) Loop(ctx context.Context, cancel func()) error {
defer cancel()
for {
select {
case <-i.LoopCh(): // can only fall here if we closed c.loopCh
return
case ev := <-evCh:
i.handleInputEvent(ev)
}
}
}
func (i *Input) handleInputEvent(ev termbox.Event) {
switch ev.Type {
case termbox.EventError:
//update = false
case termbox.EventResize:
i.DrawMatches(nil)
case termbox.EventKey:
// ModAlt is a sequence of letters with a leading \x1b (=Esc).
// It would be nice if termbox differentiated this for us, but
// we workaround it by waiting (juuuuse a few milliseconds) for
// extra key events. If no extra events arrive, it should be Esc
// Smells like Esc or Alt. mod == nil checks for the presense
// of a previous timer
if ev.Ch == 0 && ev.Key == 27 && i.mod == nil {
tmp := ev
i.mutex.Lock()
i.mod = time.AfterFunc(50*time.Millisecond, func() {
i.mutex.Lock()
i.mod = nil
i.mutex.Unlock()
i.handleKeyEvent(tmp)
})
i.mutex.Unlock()
} else {
// it doesn't look like this is Esc or Alt. If we have a previous
// timer, stop it because this is probably Alt+ this new key
i.mutex.Lock()
if i.mod != nil {
i.mod.Stop()
i.mod = nil
ev.Mod |= termbox.ModAlt
case <-ctx.Done():
return nil
case ev, ok := <-i.evsrc:
if !ok {
return nil
}
if err := i.handleInputEvent(ctx, ev); err != nil {
return nil
}
i.mutex.Unlock()
i.handleKeyEvent(ev)
}
}
}
func (i *Input) handleKeyEvent(ev termbox.Event) {
if h := i.keymap.Handler(ev); h != nil {
h.Execute(i, ev)
return
// handleInputEvent processes a single terminal input event, mapping it to an action.
func (i *Input) handleInputEvent(ctx context.Context, ev Event) error {
if pdebug.Enabled {
g := pdebug.Marker("event received from user: %#v", ev)
defer g.End()
}
switch ev.Type {
case EventError:
return nil
case EventResize:
i.state.Hub().SendDraw(ctx, nil)
return nil
case EventKey:
// tcell already disambiguates standalone Escape from Alt+key
// sequences using its own internal escape timer. By the time
// we receive a KeyEsc event here, tcell has already determined
// it is a standalone Escape. Alt+key events arrive with ModAlt
// already set. No application-layer timer is needed.
_ = i.state.Keymap().ExecuteAction(ctx, i.state, ev)
return nil
}
return nil
}

72
input_test.go Normal file
View file

@ -0,0 +1,72 @@
package peco
import (
"context"
"sync/atomic"
"testing"
"github.com/peco/peco/internal/keyseq"
"github.com/stretchr/testify/require"
)
func TestInputDispatchesEscImmediately(t *testing.T) {
// After removing the application-layer Esc timer (tcell handles
// Esc/Alt disambiguation), verify that a bare Esc event is
// dispatched immediately without delay.
var escCount int64
testAction := ActionFunc(func(_ context.Context, _ *Peco, _ Event) {
atomic.AddInt64(&escCount, 1)
})
nameToActions["peco.TestEscImmediate"] = testAction
t.Cleanup(func() { delete(nameToActions, "peco.TestEscImmediate") })
state := New()
state.hub = nullHub{}
state.config.Keymap = map[string]string{
"Esc": "peco.TestEscImmediate",
}
state.config.Action = map[string][]string{}
require.NoError(t, state.populateKeymap())
ctx := context.Background()
input := NewInput(state, state.Keymap(), make(chan Event))
escEv := Event{Type: EventKey, Key: keyseq.KeyEsc, Ch: 0}
input.handleInputEvent(ctx, escEv)
require.Equal(t, int64(1), atomic.LoadInt64(&escCount),
"Esc action should fire immediately without timer delay")
}
func TestInputAltKeyPassthrough(t *testing.T) {
// Verify that events arriving with ModAlt already set by tcell
// are dispatched correctly.
var gotMod keyseq.ModifierKey
testAction := ActionFunc(func(_ context.Context, _ *Peco, ev Event) {
gotMod = ev.Mod
})
nameToActions["peco.TestAltRecord"] = testAction
t.Cleanup(func() { delete(nameToActions, "peco.TestAltRecord") })
state := New()
state.hub = nullHub{}
state.config.Keymap = map[string]string{
"M-f": "peco.TestAltRecord",
}
state.config.Action = map[string][]string{}
require.NoError(t, state.populateKeymap())
ctx := context.Background()
input := NewInput(state, state.Keymap(), make(chan Event))
// Simulate tcell delivering Alt+f with ModAlt already set
altEv := Event{Type: EventKey, Key: 0, Ch: 'f', Mod: keyseq.ModAlt}
input.handleInputEvent(ctx, altEv)
require.Equal(t, keyseq.ModAlt, gotMod,
"ModAlt should be preserved from tcell event")
}

View file

@ -0,0 +1,53 @@
package ansi
import "testing"
// BenchmarkParse_MultipleColors benchmarks parsing a line with several ANSI
// color changes, which exercises the parseSGR strings.Split path.
func BenchmarkParse_MultipleColors(b *testing.B) {
input := "\x1b[1;31mRed Bold\x1b[0m normal \x1b[38;5;196mExtended\x1b[0m \x1b[4;32mGreen UL\x1b[0m"
b.ReportAllocs()
for b.Loop() {
Parse(input)
}
}
// BenchmarkParse_TrueColor benchmarks truecolor parsing (38;2;R;G;B).
func BenchmarkParse_TrueColor(b *testing.B) {
input := "\x1b[38;2;255;128;0mOrange\x1b[0m \x1b[48;2;0;128;255mBlueBG\x1b[0m"
b.ReportAllocs()
for b.Loop() {
Parse(input)
}
}
// BenchmarkParseSGR benchmarks the raw SGR parameter parsing.
func BenchmarkParseSGR(b *testing.B) {
var fg, bg Attribute
b.Run("simple", func(b *testing.B) {
b.ReportAllocs()
for range b.N {
fg, bg = ColorDefault, ColorDefault
parseSGR("1;31;42", &fg, &bg)
}
})
b.Run("256color", func(b *testing.B) {
b.ReportAllocs()
for range b.N {
fg, bg = ColorDefault, ColorDefault
parseSGR("38;5;196", &fg, &bg)
}
})
b.Run("truecolor", func(b *testing.B) {
b.ReportAllocs()
for range b.N {
fg, bg = ColorDefault, ColorDefault
parseSGR("38;2;255;128;0", &fg, &bg)
}
})
}

301
internal/ansi/parser.go Normal file
View file

@ -0,0 +1,301 @@
// Package ansi provides ANSI SGR escape sequence parsing for peco.
// It extracts color/style attributes from input text and produces
// run-length encoded attribute spans alongside stripped plain text.
package ansi
import (
"strconv"
"strings"
"unicode/utf8"
)
// Attribute mirrors peco.Attribute so we avoid an import cycle.
// The values are identical and can be cast directly.
type Attribute = uint32
// Named palette color constants (matching peco.Color* values).
const (
ColorDefault Attribute = 0x0000
ColorBlack Attribute = 0x0001
ColorRed Attribute = 0x0002
ColorGreen Attribute = 0x0003
ColorYellow Attribute = 0x0004
ColorBlue Attribute = 0x0005
ColorMagenta Attribute = 0x0006
ColorCyan Attribute = 0x0007
ColorWhite Attribute = 0x0008
)
const (
AttrTrueColor Attribute = 0x01000000
AttrBold Attribute = 0x02000000
AttrUnderline Attribute = 0x04000000
AttrReverse Attribute = 0x08000000
)
// basicFgColors maps SGR codes 30-37 to palette colors.
var basicFgColors = [8]Attribute{
ColorBlack, ColorRed, ColorGreen, ColorYellow,
ColorBlue, ColorMagenta, ColorCyan, ColorWhite,
}
// AttrSpan represents a run of characters sharing identical ANSI attributes.
type AttrSpan struct {
Fg Attribute
Bg Attribute
Length int // number of runes
}
// ParseResult contains the output of ANSI parsing.
type ParseResult struct {
Stripped string // text with ANSI codes removed
Attrs []AttrSpan // run-length encoded attributes; nil if no ANSI codes found
}
// Parse parses ANSI SGR sequences from input and returns the stripped text
// along with run-length encoded per-character attributes.
// If no ANSI escape sequences are found, Attrs is nil.
func Parse(input string) ParseResult {
// Fast path: if no ESC character, return as-is
if !strings.ContainsRune(input, '\x1b') {
return ParseResult{Stripped: input, Attrs: nil}
}
var (
out strings.Builder
spans []AttrSpan
curFg = ColorDefault
curBg = ColorDefault
count int // runes in current span
)
out.Grow(len(input))
flush := func() {
if count > 0 {
spans = append(spans, AttrSpan{Fg: curFg, Bg: curBg, Length: count})
count = 0
}
}
i := 0
for i < len(input) {
if input[i] == '\x1b' && i+1 < len(input) && input[i+1] == '[' {
// Found CSI sequence: ESC [
j := i + 2
// Scan for the terminating byte (0x40-0x7E)
for j < len(input) && input[j] >= 0x20 && input[j] <= 0x3F {
j++
}
if j >= len(input) {
// Incomplete sequence at end of string: skip it
i = j
continue
}
terminator := input[j]
if terminator == 'm' {
// SGR sequence
params := input[i+2 : j]
flush()
parseSGR(params, &curFg, &curBg)
}
// Skip the entire sequence (including non-SGR ones)
i = j + 1
continue
}
// Regular character
r, size := utf8.DecodeRuneInString(input[i:])
if r == utf8.RuneError && size == 1 {
r = '?'
}
out.WriteRune(r)
count++
i += size
}
flush()
return ParseResult{
Stripped: out.String(),
Attrs: spans,
}
}
// maxSGRParams is the maximum number of semicolon-separated parameters
// we handle in a single SGR sequence. Truecolor (38;2;R;G;B) uses 5.
// Any sequence with more parameters is truncated.
const maxSGRParams = 8
// parseSGR interprets SGR parameters (the part between ESC[ and m).
// It modifies fg and bg in place based on the parameter codes.
// Parameters are split on semicolons into a stack-allocated array to
// avoid the heap allocation that strings.Split would cause.
func parseSGR(params string, fg, bg *Attribute) {
if params == "" || params == "0" {
// Reset all
*fg = ColorDefault
*bg = ColorDefault
return
}
// Split params into a stack-allocated array to avoid strings.Split's
// heap-allocated []string slice.
var parts [maxSGRParams]string
n := 0
for n < maxSGRParams {
idx := strings.IndexByte(params, ';')
if idx == -1 {
parts[n] = params
n++
break
}
parts[n] = params[:idx]
params = params[idx+1:]
n++
}
for i := 0; i < n; i++ {
code, err := strconv.Atoi(parts[i])
if err != nil {
continue
}
switch {
case code == 0:
// Reset
*fg = ColorDefault
*bg = ColorDefault
case code == 1:
*fg |= AttrBold
case code == 4:
*fg |= AttrUnderline
case code == 7:
*fg |= AttrReverse
// Basic foreground colors 30-37
case code >= 30 && code <= 37:
// Preserve attribute flags, set new color
flags := *fg & (AttrBold | AttrUnderline | AttrReverse)
*fg = basicFgColors[code-30] | flags
// Basic background colors 40-47
case code >= 40 && code <= 47:
*bg = basicFgColors[code-40]
// 256-color or truecolor foreground: 38;5;N or 38;2;R;G;B
case code == 38:
if i+1 < n {
mode, err := strconv.Atoi(parts[i+1])
if err != nil {
i++
break
}
switch mode {
case 5: // 256-color: 38;5;N
if i+2 < n {
c, err := strconv.Atoi(parts[i+2])
if err == nil && c >= 0 && c <= 255 {
flags := *fg & (AttrBold | AttrUnderline | AttrReverse)
*fg = Attribute(c+1) | flags
}
i += 2
}
case 2: // Truecolor: 38;2;R;G;B
if i+4 < n {
r, err1 := strconv.Atoi(parts[i+2])
g, err2 := strconv.Atoi(parts[i+3])
b, err3 := strconv.Atoi(parts[i+4])
if err1 == nil && err2 == nil && err3 == nil {
flags := *fg & (AttrBold | AttrUnderline | AttrReverse)
*fg = Attribute((r<<16)|(g<<8)|b) | AttrTrueColor | flags
}
i += 4
}
default:
i++
}
}
// 256-color or truecolor background: 48;5;N or 48;2;R;G;B
case code == 48:
if i+1 < n {
mode, err := strconv.Atoi(parts[i+1])
if err != nil {
i++
break
}
switch mode {
case 5: // 256-color: 48;5;N
if i+2 < n {
c, err := strconv.Atoi(parts[i+2])
if err == nil && c >= 0 && c <= 255 {
*bg = Attribute(c + 1)
}
i += 2
}
case 2: // Truecolor: 48;2;R;G;B
if i+4 < n {
r, err1 := strconv.Atoi(parts[i+2])
g, err2 := strconv.Atoi(parts[i+3])
b, err3 := strconv.Atoi(parts[i+4])
if err1 == nil && err2 == nil && err3 == nil {
*bg = Attribute((r<<16)|(g<<8)|b) | AttrTrueColor
}
i += 4
}
default:
i++
}
}
// Default foreground/background reset
case code == 39:
flags := *fg & (AttrBold | AttrUnderline | AttrReverse)
*fg = ColorDefault | flags
case code == 49:
*bg = ColorDefault
}
}
}
// ExtractSegment extracts ANSI attributes for a rune-range [start, end)
// from the given run-length encoded spans.
// Returns nil if attrs is nil or the segment is empty.
func ExtractSegment(attrs []AttrSpan, start, end int) []AttrSpan {
if attrs == nil || start >= end {
return nil
}
var result []AttrSpan
pos := 0
for _, span := range attrs {
spanEnd := pos + span.Length
if spanEnd <= start {
pos = spanEnd
continue
}
if pos >= end {
break
}
overlapStart := max(start, pos)
overlapEnd := min(end, spanEnd)
overlapLen := overlapEnd - overlapStart
if overlapLen > 0 {
result = append(result, AttrSpan{
Fg: span.Fg,
Bg: span.Bg,
Length: overlapLen,
})
}
pos = spanEnd
}
return result
}

View file

@ -0,0 +1,354 @@
package ansi
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestParse_NoANSI(t *testing.T) {
r := Parse("Hello World")
require.Equal(t, "Hello World", r.Stripped)
require.Nil(t, r.Attrs)
}
func TestParse_EmptyString(t *testing.T) {
r := Parse("")
require.Equal(t, "", r.Stripped)
require.Nil(t, r.Attrs)
}
func TestParse_BasicForegroundColors(t *testing.T) {
r := Parse("\x1b[31mRed\x1b[0m")
require.Equal(t, "Red", r.Stripped)
require.Len(t, r.Attrs, 1)
require.Equal(t, ColorRed, r.Attrs[0].Fg)
require.Equal(t, ColorDefault, r.Attrs[0].Bg)
require.Equal(t, 3, r.Attrs[0].Length)
}
func TestParse_BasicBackgroundColors(t *testing.T) {
r := Parse("\x1b[42mGreen BG\x1b[0m")
require.Equal(t, "Green BG", r.Stripped)
require.Len(t, r.Attrs, 1)
require.Equal(t, ColorDefault, r.Attrs[0].Fg)
require.Equal(t, ColorGreen, r.Attrs[0].Bg)
require.Equal(t, 8, r.Attrs[0].Length)
}
func TestParse_BoldAttribute(t *testing.T) {
r := Parse("\x1b[1;31mBold Red\x1b[0m")
require.Equal(t, "Bold Red", r.Stripped)
require.Len(t, r.Attrs, 1)
require.Equal(t, ColorRed|AttrBold, r.Attrs[0].Fg)
require.Equal(t, 8, r.Attrs[0].Length)
}
func TestParse_UnderlineAttribute(t *testing.T) {
r := Parse("\x1b[4mUnderline\x1b[0m")
require.Equal(t, "Underline", r.Stripped)
require.Len(t, r.Attrs, 1)
require.Equal(t, AttrUnderline, r.Attrs[0].Fg)
}
func TestParse_ReverseAttribute(t *testing.T) {
r := Parse("\x1b[7mReverse\x1b[0m")
require.Equal(t, "Reverse", r.Stripped)
require.Len(t, r.Attrs, 1)
require.Equal(t, AttrReverse, r.Attrs[0].Fg)
}
func TestParse_MultipleSegments(t *testing.T) {
r := Parse("AAA\x1b[31mBBB\x1b[0mCCC")
require.Equal(t, "AAABBBCCC", r.Stripped)
require.Len(t, r.Attrs, 3)
require.Equal(t, ColorDefault, r.Attrs[0].Fg)
require.Equal(t, 3, r.Attrs[0].Length)
require.Equal(t, ColorRed, r.Attrs[1].Fg)
require.Equal(t, 3, r.Attrs[1].Length)
require.Equal(t, ColorDefault, r.Attrs[2].Fg)
require.Equal(t, 3, r.Attrs[2].Length)
}
func TestParse_256Color(t *testing.T) {
// 38;5;196 = 256-color fg (bright red, index 196)
r := Parse("\x1b[38;5;196mColor256\x1b[0m")
require.Equal(t, "Color256", r.Stripped)
require.Len(t, r.Attrs, 1)
require.Equal(t, Attribute(197), r.Attrs[0].Fg) // 196+1 for palette encoding
}
func TestParse_256ColorBackground(t *testing.T) {
r := Parse("\x1b[48;5;21mBlueBG\x1b[0m")
require.Equal(t, "BlueBG", r.Stripped)
require.Len(t, r.Attrs, 1)
require.Equal(t, Attribute(22), r.Attrs[0].Bg) // 21+1
}
func TestParse_TrueColor(t *testing.T) {
// 38;2;255;128;0 = truecolor fg (orange)
r := Parse("\x1b[38;2;255;128;0mOrange\x1b[0m")
require.Equal(t, "Orange", r.Stripped)
require.Len(t, r.Attrs, 1)
expected := Attribute((255<<16)|(128<<8)) | AttrTrueColor
require.Equal(t, expected, r.Attrs[0].Fg)
}
func TestParse_TrueColorBackground(t *testing.T) {
r := Parse("\x1b[48;2;0;128;255mBG\x1b[0m")
require.Equal(t, "BG", r.Stripped)
require.Len(t, r.Attrs, 1)
expected := Attribute((0<<16)|(128<<8)|255) | AttrTrueColor
require.Equal(t, expected, r.Attrs[0].Bg)
}
func TestParse_Reset(t *testing.T) {
r := Parse("\x1b[1;31mBold Red\x1b[0m Normal")
require.Equal(t, "Bold Red Normal", r.Stripped)
require.Len(t, r.Attrs, 2)
require.Equal(t, ColorRed|AttrBold, r.Attrs[0].Fg)
require.Equal(t, 8, r.Attrs[0].Length)
require.Equal(t, ColorDefault, r.Attrs[1].Fg)
require.Equal(t, 7, r.Attrs[1].Length)
}
func TestParse_MalformedIncomplete(t *testing.T) {
// Incomplete sequence at end
r := Parse("Hello\x1b[")
require.Equal(t, "Hello", r.Stripped)
}
func TestParse_MalformedNoTerminator(t *testing.T) {
// ESC[31W — 'W' is a valid CSI terminator (non-SGR), so it is stripped
// and "orld" remains as regular text
r := Parse("Hello\x1b[31World")
require.Equal(t, "Helloorld", r.Stripped)
}
func TestParse_NonSGRSequence(t *testing.T) {
// ESC[2J is "clear screen" (not SGR) — should be stripped, not rendered
r := Parse("\x1b[2JHello")
require.Equal(t, "Hello", r.Stripped)
}
func TestParse_EmptyReset(t *testing.T) {
// ESC[m is equivalent to ESC[0m
r := Parse("\x1b[31mRed\x1b[mNormal")
require.Equal(t, "RedNormal", r.Stripped)
require.Len(t, r.Attrs, 2)
require.Equal(t, ColorRed, r.Attrs[0].Fg)
require.Equal(t, ColorDefault, r.Attrs[1].Fg)
}
func TestParse_DefaultFgBgReset(t *testing.T) {
r := Parse("\x1b[31;42mColored\x1b[39;49mReset")
require.Equal(t, "ColoredReset", r.Stripped)
require.Len(t, r.Attrs, 2)
require.Equal(t, ColorRed, r.Attrs[0].Fg)
require.Equal(t, ColorGreen, r.Attrs[0].Bg)
// code 39 resets fg, code 49 resets bg
require.Equal(t, ColorDefault, r.Attrs[1].Fg)
require.Equal(t, ColorDefault, r.Attrs[1].Bg)
}
func TestParse_ComplexCombined(t *testing.T) {
// Bold underline red on blue background
r := Parse("\x1b[1;4;31;44mStyled\x1b[0m")
require.Equal(t, "Styled", r.Stripped)
require.Len(t, r.Attrs, 1)
require.Equal(t, ColorRed|AttrBold|AttrUnderline, r.Attrs[0].Fg)
require.Equal(t, ColorBlue, r.Attrs[0].Bg)
}
func TestParseSGR_MalformedExtendedColor(t *testing.T) {
// parseSGR is tested directly to verify that malformed extended color
// sequences don't corrupt color state.
t.Run("well-formed 256-color fg still works", func(t *testing.T) {
var fg, bg Attribute
parseSGR("38;5;196", &fg, &bg)
require.Equal(t, Attribute(197), fg) // 196+1
require.Equal(t, ColorDefault, bg)
})
t.Run("well-formed truecolor fg still works", func(t *testing.T) {
var fg, bg Attribute
parseSGR("38;2;255;128;0", &fg, &bg)
expected := Attribute((255<<16)|(128<<8)) | AttrTrueColor
require.Equal(t, expected, fg)
require.Equal(t, ColorDefault, bg)
})
t.Run("well-formed 256-color bg still works", func(t *testing.T) {
var fg, bg Attribute
parseSGR("48;5;21", &fg, &bg)
require.Equal(t, ColorDefault, fg)
require.Equal(t, Attribute(22), bg) // 21+1
})
t.Run("well-formed truecolor bg still works", func(t *testing.T) {
var fg, bg Attribute
parseSGR("48;2;0;128;255", &fg, &bg)
expected := Attribute((0<<16)|(128<<8)|255) | AttrTrueColor
require.Equal(t, ColorDefault, fg)
require.Equal(t, expected, bg)
})
t.Run("malformed mode in fg 38;abc;100 does not change fg", func(t *testing.T) {
var fg, bg Attribute
fg = ColorRed // set a prior color
parseSGR("38;abc;100", &fg, &bg)
// Malformed mode should skip the sequence, leaving fg unchanged
require.Equal(t, ColorRed, fg)
require.Equal(t, ColorDefault, bg)
})
t.Run("malformed 256-color value in fg 38;5;abc does not change fg", func(t *testing.T) {
var fg, bg Attribute
fg = ColorRed
parseSGR("38;5;abc", &fg, &bg)
// Malformed color value should skip the color, leaving fg unchanged
require.Equal(t, ColorRed, fg)
require.Equal(t, ColorDefault, bg)
})
t.Run("malformed truecolor R in fg 38;2;abc;0;0 does not change fg", func(t *testing.T) {
var fg, bg Attribute
fg = ColorRed
parseSGR("38;2;abc;0;0", &fg, &bg)
require.Equal(t, ColorRed, fg)
require.Equal(t, ColorDefault, bg)
})
t.Run("malformed truecolor G in fg 38;2;0;abc;0 does not change fg", func(t *testing.T) {
var fg, bg Attribute
fg = ColorRed
parseSGR("38;2;0;abc;0", &fg, &bg)
require.Equal(t, ColorRed, fg)
require.Equal(t, ColorDefault, bg)
})
t.Run("malformed truecolor B in fg 38;2;0;0;abc does not change fg", func(t *testing.T) {
var fg, bg Attribute
fg = ColorRed
parseSGR("38;2;0;0;abc", &fg, &bg)
require.Equal(t, ColorRed, fg)
require.Equal(t, ColorDefault, bg)
})
t.Run("malformed mode in bg 48;abc;100 does not change bg", func(t *testing.T) {
var fg, bg Attribute
bg = ColorGreen
parseSGR("48;abc;100", &fg, &bg)
require.Equal(t, ColorDefault, fg)
require.Equal(t, ColorGreen, bg)
})
t.Run("malformed 256-color value in bg 48;5;abc does not change bg", func(t *testing.T) {
var fg, bg Attribute
bg = ColorGreen
parseSGR("48;5;abc", &fg, &bg)
require.Equal(t, ColorDefault, fg)
require.Equal(t, ColorGreen, bg)
})
t.Run("malformed truecolor R in bg 48;2;abc;0;0 does not change bg", func(t *testing.T) {
var fg, bg Attribute
bg = ColorGreen
parseSGR("48;2;abc;0;0", &fg, &bg)
require.Equal(t, ColorDefault, fg)
require.Equal(t, ColorGreen, bg)
})
t.Run("malformed truecolor G in bg 48;2;0;abc;0 does not change bg", func(t *testing.T) {
var fg, bg Attribute
bg = ColorGreen
parseSGR("48;2;0;abc;0", &fg, &bg)
require.Equal(t, ColorDefault, fg)
require.Equal(t, ColorGreen, bg)
})
t.Run("malformed truecolor B in bg 48;2;0;0;abc does not change bg", func(t *testing.T) {
var fg, bg Attribute
bg = ColorGreen
parseSGR("48;2;0;0;abc", &fg, &bg)
require.Equal(t, ColorDefault, fg)
require.Equal(t, ColorGreen, bg)
})
t.Run("malformed extended color followed by valid code", func(t *testing.T) {
var fg, bg Attribute
// 38;abc;100 should be skipped, then 1 (bold) should still apply
parseSGR("38;abc;100;1", &fg, &bg)
require.Equal(t, AttrBold, fg)
require.Equal(t, ColorDefault, bg)
})
t.Run("malformed 256-color followed by valid code", func(t *testing.T) {
var fg, bg Attribute
// 38;5;abc should be skipped, then 31 (red) should apply
parseSGR("38;5;abc;31", &fg, &bg)
require.Equal(t, ColorRed, fg)
require.Equal(t, ColorDefault, bg)
})
t.Run("via Parse: malformed fg 256-color does not corrupt color state", func(t *testing.T) {
// ESC[38;5; with a non-numeric parameter byte (<) that still passes
// the CSI scanner (0x3C is in 0x20-0x3F range) but fails Atoi.
// 38;5;<3 — the "<3" is not a valid integer.
r := Parse("\x1b[38;5;<3mHello\x1b[0m")
require.Equal(t, "Hello", r.Stripped)
require.Len(t, r.Attrs, 1)
require.Equal(t, ColorDefault, r.Attrs[0].Fg)
require.Equal(t, ColorDefault, r.Attrs[0].Bg)
})
}
func TestExtractSegment_Nil(t *testing.T) {
require.Nil(t, ExtractSegment(nil, 0, 5))
}
func TestExtractSegment_EmptyRange(t *testing.T) {
attrs := []AttrSpan{{Fg: ColorRed, Bg: ColorDefault, Length: 10}}
require.Nil(t, ExtractSegment(attrs, 5, 5))
}
func TestExtractSegment_FullSpan(t *testing.T) {
attrs := []AttrSpan{
{Fg: ColorRed, Bg: ColorDefault, Length: 3},
{Fg: ColorDefault, Bg: ColorDefault, Length: 7},
{Fg: ColorBlue, Bg: ColorDefault, Length: 5},
}
result := ExtractSegment(attrs, 0, 15)
require.Equal(t, attrs, result)
}
func TestExtractSegment_MiddleSlice(t *testing.T) {
attrs := []AttrSpan{
{Fg: ColorRed, Bg: ColorDefault, Length: 5},
{Fg: ColorBlue, Bg: ColorDefault, Length: 5},
{Fg: ColorGreen, Bg: ColorDefault, Length: 5},
}
// Extract runes 3..8 — partial Red(2) + full Blue(5) + partial Green(1)
result := ExtractSegment(attrs, 3, 12)
require.Len(t, result, 3)
require.Equal(t, 2, result[0].Length) // Red: runes 3..5
require.Equal(t, ColorRed, result[0].Fg)
require.Equal(t, 5, result[1].Length) // Blue: runes 5..10
require.Equal(t, ColorBlue, result[1].Fg)
require.Equal(t, 2, result[2].Length) // Green: runes 10..12
require.Equal(t, ColorGreen, result[2].Fg)
}
func TestExtractSegment_SingleSpanSlice(t *testing.T) {
attrs := []AttrSpan{
{Fg: ColorRed, Bg: ColorDefault, Length: 10},
}
result := ExtractSegment(attrs, 2, 7)
require.Len(t, result, 1)
require.Equal(t, 5, result[0].Length)
require.Equal(t, ColorRed, result[0].Fg)
}

30
internal/buffer/line.go Normal file
View file

@ -0,0 +1,30 @@
package buffer
import (
"sync"
"github.com/peco/peco/line"
)
const DefaultFilterBufSize = 1000
var lineListPool = sync.Pool{
New: func() any {
return make([]line.Line, 0, DefaultFilterBufSize)
},
}
// ReleaseLineListBuf returns a line list buffer to the sync.Pool for reuse.
func ReleaseLineListBuf(l []line.Line) {
if l == nil {
return
}
l = l[0:0]
lineListPool.Put(l) //nolint:staticcheck // SA6002: converting to pointer-based pool breaks tests
}
// GetLineListBuf retrieves a line list buffer from the sync.Pool, allocating if needed.
func GetLineListBuf() []line.Line {
l, _ := lineListPool.Get().([]line.Line)
return l
}

View file

@ -0,0 +1,61 @@
package buffer
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestGetLineListBuf(t *testing.T) {
t.Parallel()
buf := GetLineListBuf()
require.NotNil(t, buf)
require.Equal(t, 0, len(buf))
require.Equal(t, DefaultFilterBufSize, cap(buf))
}
func TestReleaseAndGetLineListBuf(t *testing.T) {
t.Parallel()
// Get a buffer, use it, release it, get it back
buf := GetLineListBuf()
require.NotNil(t, buf)
// Release it back to the pool
ReleaseLineListBuf(buf)
// Get another one — may or may not be the same one (pool behavior is not guaranteed)
buf2 := GetLineListBuf()
require.NotNil(t, buf2)
require.Equal(t, 0, len(buf2))
}
func TestReleaseLineListBufNil(t *testing.T) {
t.Parallel()
// Should not panic
ReleaseLineListBuf(nil)
}
func TestReleaseLineListBufResetsLength(t *testing.T) {
t.Parallel()
buf := GetLineListBuf()
// Simulate usage — we can't easily append real line.Line values
// without importing the line package's internals, but we can verify
// that the pool resets the slice length to 0 on release.
// The ReleaseLineListBuf function does: l = l[0:0]
// This is verified by the pool returning zero-length slices.
ReleaseLineListBuf(buf)
buf2 := GetLineListBuf()
require.Equal(t, 0, len(buf2))
}
func TestMultipleGetReleaseCycles(t *testing.T) {
t.Parallel()
for range 10 {
buf := GetLineListBuf()
require.NotNil(t, buf)
require.Equal(t, 0, len(buf))
ReleaseLineListBuf(buf)
}
}

View file

@ -7,45 +7,50 @@ type Matcher struct {
type Match struct {
Index int
Pattern KeyList
Value interface{}
Value any
}
type nodeData struct {
pattern *KeyList
value interface{}
value any
failure *TernaryNode
}
func (n *nodeData) Value() interface{} {
func (n *nodeData) Value() any {
return n.value
}
// NewMatcher creates a new Aho-Corasick matcher for multi-pattern key sequence matching.
func NewMatcher() *Matcher {
return &Matcher{
NewTernaryTrie(),
}
}
// Clear removes all patterns from the matcher, resetting it to empty state.
func (m *Matcher) Clear() {
m.Root().RemoveAll()
}
func (m *Matcher) Add(pattern KeyList, v interface{}) {
// Add inserts a key sequence pattern with an associated value into the matcher.
func (m *Matcher) Add(pattern KeyList, v any) {
m.Put(pattern, &nodeData{
pattern: &pattern,
value: v,
})
}
// Compile builds the failure links needed for Aho-Corasick matching after all patterns are added.
func (m *Matcher) Compile() error {
m.Balance()
root := m.Root().(*TernaryNode)
root, _ := m.Root().(*TernaryNode)
root.SetValue(&nodeData{failure: root})
// fill data.failure of each node.
EachWidth(m, func(n Node) bool {
parent := n.(*TernaryNode)
parent, _ := n.(*TernaryNode)
parent.Each(func(m Node) bool {
fillFailure(m.(*TernaryNode), root, parent)
child, _ := m.(*TernaryNode)
fillFailure(child, root, parent)
return true
})
return true
@ -53,6 +58,7 @@ func (m *Matcher) Compile() error {
return nil
}
// fillFailure recursively computes the failure link for curr based on its parent's failure chain.
func fillFailure(curr, root, parent *TernaryNode) {
data := getNodeData(curr)
if data == nil {
@ -68,15 +74,17 @@ func fillFailure(curr, root, parent *TernaryNode) {
data.failure = fnode
}
// Match tests a key sequence against all compiled patterns and returns matches on a channel.
func (m *Matcher) Match(text KeyList) <-chan Match {
ch := make(chan Match, 1)
go m.startMatch(text, ch)
return ch
}
// startMatch begins a new match attempt from the root of the Aho-Corasick automaton.
func (m *Matcher) startMatch(text KeyList, ch chan<- Match) {
defer close(ch)
root := m.Root().(*TernaryNode)
root, _ := m.Root().(*TernaryNode)
curr := root
for i, r := range text {
curr = getNextNode(curr, root, r)
@ -87,6 +95,7 @@ func (m *Matcher) startMatch(text KeyList, ch chan<- Match) {
}
}
// getNextNode follows failure links from node until it finds a child matching r, or returns root.
func getNextNode(node, root *TernaryNode, r Key) *TernaryNode {
for {
next, _ := node.Get(r).(*TernaryNode)
@ -99,6 +108,7 @@ func getNextNode(node, root *TernaryNode, r Key) *TernaryNode {
}
}
// fireAll emits all pattern matches found at curr by walking the failure chain back to root.
func fireAll(curr, root *TernaryNode, ch chan<- Match, idx int) {
for curr != root {
data := getNodeData(curr)
@ -113,11 +123,13 @@ func fireAll(curr, root *TernaryNode, ch chan<- Match, idx int) {
}
}
// getNodeData extracts the nodeData stored in a TernaryNode's value.
func getNodeData(node *TernaryNode) *nodeData {
d, _ := node.Value().(*nodeData)
return d
}
// getNodeFailure returns the failure link for node, falling back to root if none is set.
func getNodeFailure(node, root *TernaryNode) *TernaryNode {
next := getNodeData(node).failure
if next == nil {

View file

@ -0,0 +1,75 @@
package keyseq
import (
"testing"
"github.com/stretchr/testify/require"
)
func checkNode(t *testing.T, node Node, size int, data nodeData) {
require.NotNil(t, node, "Nil node:", data)
require.Equal(t, size, node.Size(), "Unexpected childrens")
d := node.Value().(*nodeData)
require.NotNil(t, d, "Nil data:", data, node)
if data.pattern != nil {
require.True(t, d.pattern.Equals(*data.pattern), "Pattern unmatched:", data, node, *d.pattern)
}
if data.value != nil {
require.Equal(t, data.value, d.value, "Value unmatched:", data, node, d.value)
}
require.NotNil(t, d.failure, "Nil failure:", data, node)
require.Equal(t, data.failure, d.failure, "Failure unmatched: data=%+v node=%+v d.failure=%+v",
data, node, d.failure)
}
func invalidData(failure Node) nodeData {
return nodeData{
failure: failure.(*TernaryNode),
}
}
func validData(pattern KeyList, value any, failure Node) nodeData {
return nodeData{
pattern: &pattern,
value: value,
failure: failure.(*TernaryNode),
}
}
func newTestMatcher() *Matcher {
m := NewMatcher()
m.Add(KeyList{Key{0, KeyCtrlA, rune(0)}, Key{0, KeyCtrlB, rune(0)}}, 2)
m.Add(KeyList{Key{0, KeyCtrlB, rune(0)}, Key{0, KeyCtrlC, rune(0)}}, 4)
m.Add(KeyList{Key{0, KeyCtrlB, rune(0)}, Key{0, KeyCtrlA, rune(0)}, Key{0, KeyCtrlB, rune(0)}}, 6)
m.Add(KeyList{Key{0, KeyCtrlD, rune(0)}}, 7)
m.Add(KeyList{Key{0, KeyCtrlA, rune(0)}, Key{0, KeyCtrlB, rune(0)}, Key{0, KeyCtrlC, rune(0)}, Key{0, KeyCtrlD, rune(0)}, Key{0, KeyCtrlE, rune(0)}}, 10)
m.Compile()
return m
}
func TestTree(t *testing.T) {
m := newTestMatcher()
// Check tree structure.
r := m.Root()
checkNode(t, r, 3, invalidData(r))
n1 := r.Get(NewKeyFromKey(KeyCtrlA))
checkNode(t, n1, 1, invalidData(r))
n3 := r.Get(NewKeyFromKey(KeyCtrlB))
checkNode(t, n3, 2, invalidData(r))
n7 := r.Get(NewKeyFromKey(KeyCtrlD))
checkNode(t, n7, 0, invalidData(r))
n2 := n1.Get(NewKeyFromKey(KeyCtrlB))
checkNode(t, n2, 1, validData(KeyList{NewKeyFromKey(KeyCtrlA), NewKeyFromKey(KeyCtrlB)}, 2, n3))
n4 := n3.Get(NewKeyFromKey(KeyCtrlC))
checkNode(t, n4, 0, validData(KeyList{NewKeyFromKey(KeyCtrlB), NewKeyFromKey(KeyCtrlC)}, 4, r))
n5 := n3.Get(NewKeyFromKey(KeyCtrlA))
checkNode(t, n5, 1, invalidData(n1))
n8 := n2.Get(NewKeyFromKey(KeyCtrlC))
checkNode(t, n8, 1, invalidData(n4))
n6 := n5.Get(NewKeyFromKey(KeyCtrlB))
checkNode(t, n6, 0, validData(KeyList{NewKeyFromKey(KeyCtrlB), NewKeyFromKey(KeyCtrlA), NewKeyFromKey(KeyCtrlB)}, 6, n2))
n9 := n8.Get(NewKeyFromKey(KeyCtrlD))
checkNode(t, n9, 1, invalidData(n7))
n10 := n9.Get(NewKeyFromKey(KeyCtrlE))
checkNode(t, n10, 0, validData(KeyList{NewKeyFromKey(KeyCtrlA), NewKeyFromKey(KeyCtrlB), NewKeyFromKey(KeyCtrlC), NewKeyFromKey(KeyCtrlD), NewKeyFromKey(KeyCtrlE)}, 10, r))
}

293
internal/keyseq/keys.go Normal file
View file

@ -0,0 +1,293 @@
package keyseq
import (
"fmt"
"strings"
"unicode/utf8"
)
// KeyType represents a keyboard key. The adapter layer in screen.go maps
// between peco's KeyType and tcell's key constants.
type KeyType uint16
// Function keys
const (
KeyF1 KeyType = 0xFFFF - iota
KeyF2
KeyF3
KeyF4
KeyF5
KeyF6
KeyF7
KeyF8
KeyF9
KeyF10
KeyF11
KeyF12
KeyInsert
KeyDelete
KeyHome
KeyEnd
KeyPgup
KeyPgdn
KeyArrowUp
KeyArrowDown
KeyArrowLeft
KeyArrowRight
)
// Mouse keys.
// There is an intentional gap between KeyArrowRight and MouseLeft
// (MouseLeft = KeyArrowRight - 2, not -1) for historical reasons.
const (
MouseLeft KeyType = 0xFFFF - iota - 23 // KeyArrowRight - 2
MouseMiddle // KeyArrowRight - 3
MouseRight // KeyArrowRight - 4
)
// Ctrl keys and special keys.
// These correspond to ASCII control codes.
const (
KeyCtrlTilde KeyType = 0x00
KeyCtrl2 KeyType = 0x00
KeyCtrlSpace KeyType = 0x00
KeyCtrlA KeyType = 0x01
KeyCtrlB KeyType = 0x02
KeyCtrlC KeyType = 0x03
KeyCtrlD KeyType = 0x04
KeyCtrlE KeyType = 0x05
KeyCtrlF KeyType = 0x06
KeyCtrlG KeyType = 0x07
KeyCtrlH KeyType = 0x08
KeyCtrlI KeyType = 0x09
KeyCtrlJ KeyType = 0x0A
KeyCtrlK KeyType = 0x0B
KeyCtrlL KeyType = 0x0C
KeyCtrlM KeyType = 0x0D
KeyCtrlN KeyType = 0x0E
KeyCtrlO KeyType = 0x0F
KeyCtrlP KeyType = 0x10
KeyCtrlQ KeyType = 0x11
KeyCtrlR KeyType = 0x12
KeyCtrlS KeyType = 0x13
KeyCtrlT KeyType = 0x14
KeyCtrlU KeyType = 0x15
KeyCtrlV KeyType = 0x16
KeyCtrlW KeyType = 0x17
KeyCtrlX KeyType = 0x18
KeyCtrlY KeyType = 0x19
KeyCtrlZ KeyType = 0x1A
KeyCtrlLsqBracket KeyType = 0x1B
KeyCtrl3 KeyType = 0x1B
KeyEsc KeyType = 0x1B
KeyCtrl4 KeyType = 0x1C
KeyCtrlBackslash KeyType = 0x1C
KeyCtrl5 KeyType = 0x1D
KeyCtrlRsqBracket KeyType = 0x1D
KeyCtrl6 KeyType = 0x1E
KeyCtrl7 KeyType = 0x1F
KeyCtrlSlash KeyType = 0x1F
KeyCtrlUnderscore KeyType = 0x1F
KeySpace KeyType = 0x20
KeyBackspace2 KeyType = 0x7F
KeyCtrl8 KeyType = 0x7F
)
// Aliases
const (
KeyBackspace KeyType = 0x08 // = KeyCtrlH
KeyTab KeyType = 0x09 // = KeyCtrlI
KeyEnter KeyType = 0x0D // = KeyCtrlM
)
// This map is populated in init() below.
var stringToKey = map[string]KeyType{}
var keyToString = map[KeyType]string{}
// mapkey registers a bidirectional mapping between a key name string and its KeyType constant.
func mapkey(n string, k KeyType) {
stringToKey[n] = k
keyToString[k] = n
}
func init() {
fidx := 1
for k := KeyF1; k >= KeyF12; k-- {
sk := fmt.Sprintf("F%d", fidx)
mapkey(sk, k)
fidx++
}
names := []string{
"Insert",
"Delete",
"Home",
"End",
"Pgup",
"Pgdn",
"ArrowUp",
"ArrowDown",
"ArrowLeft",
"ArrowRight",
}
for i, n := range names {
mapkey(n, KeyType(int(KeyF12)-(i+1)))
}
names = []string{
"Left",
"Middle",
"Right",
}
for i, n := range names {
sk := fmt.Sprintf("Mouse%s", n)
mapkey(sk, KeyType(int(KeyArrowRight)-(i+2)))
}
ctrlKeyNames := [][]string{
{"~", "2", "Space"},
{"a"},
{"b"},
{"c"},
{"d"},
{"e"},
{"f"},
{"g"},
{"h"},
{"i"},
{"j"},
{"k"},
{"l"},
{"m"},
{"n"},
{"o"},
{"p"},
{"q"},
{"r"},
{"s"},
{"t"},
{"u"},
{"v"},
{"w"},
{"x"},
{"y"},
{"z"},
{"[", "3"},
{"4", "\\"},
{"5", "]"},
{"6"},
{"7", "/", "_"},
}
for i, list := range ctrlKeyNames {
for _, n := range list {
sk := fmt.Sprintf("C-%s", n)
mapkey(sk, KeyType(int(KeyCtrlTilde)+i))
}
}
mapkey("BS", KeyBackspace)
mapkey("Tab", KeyTab)
mapkey("Enter", KeyEnter)
mapkey("Esc", KeyEsc)
mapkey("Space", KeySpace)
mapkey("BS2", KeyBackspace2)
mapkey("C-8", KeyCtrl8)
}
// ToKeyList parses a comma-separated key binding string (e.g. "C-x,C-c") into a list of KeySeq values.
func ToKeyList(ksk string) (KeyList, error) {
list := KeyList{}
for term := range strings.SplitSeq(ksk, ",") {
term = strings.TrimSpace(term)
k, m, ch, err := ToKey(term)
if err != nil {
return list, fmt.Errorf("failed to convert '%s': %w", term, err)
}
list = append(list, Key{m, k, ch})
}
return list, nil
}
// KeyEventToString returns a human-readable name for a key event described
// by the given key type, character, and modifier.
func KeyEventToString(key KeyType, ch rune, mod ModifierKey) (string, error) {
var s string
if key == 0 {
s = string([]rune{ch})
} else {
var ok bool
s, ok = keyToString[key]
if !ok {
return "", fmt.Errorf("no such key %d (ch=%c)", key, ch)
}
// Special case for ArrowUp/Down/Left/Right
switch s {
case "ArrowUp":
s = "^"
case "ArrowDown":
s = "v"
case "ArrowLeft":
s = "<"
case "ArrowRight":
s = ">"
}
}
if m := mod.String(); m != "" {
return m + "-" + s, nil
}
return s, nil
}
// ToKey parses a single key name string into its KeyType, modifier, and rune components.
func ToKey(key string) (KeyType, ModifierKey, rune, error) {
modifier := ModNone
// Try full string first. This handles legacy key names like "C-a",
// "C-v", "Home", "ArrowLeft", etc. that are registered in stringToKey.
if k, ok := stringToKey[key]; ok {
return k, modifier, 0, nil
}
// Parse modifier prefixes (C-, S-, M-) iteratively.
// After each prefix is stripped, try the remainder as a key name.
for {
switch {
case strings.HasPrefix(key, "C-"):
modifier |= ModCtrl
key = key[2:]
case strings.HasPrefix(key, "S-"):
modifier |= ModShift
key = key[2:]
case strings.HasPrefix(key, "M-"):
modifier |= ModAlt
key = key[2:]
default:
goto done
}
// After stripping a prefix, try as a registered key name.
// This handles e.g. "M-C-v" → strip M-, then "C-v" is found.
if k, ok := stringToKey[key]; ok {
return k, modifier, 0, nil
}
// Single ASCII char after modifier(s) → treat as rune
if len(key) == 1 {
return 0, modifier, rune(key[0]), nil
}
}
done:
// Try as a single rune (handles multi-byte chars like "せ")
ch, _ := utf8.DecodeRuneInString(key)
if ch != utf8.RuneError {
return 0, modifier, ch, nil
}
return 0, modifier, 0, fmt.Errorf("no such key %s", key)
}

View file

@ -0,0 +1,254 @@
package keyseq
import (
"testing"
"unicode/utf8"
"github.com/stretchr/testify/require"
)
func TestKeymapStrToKeyValue(t *testing.T) {
expected := map[string]KeyType{
"F1": KeyF1,
"F2": KeyF2,
"F3": KeyF3,
"F4": KeyF4,
"F5": KeyF5,
"F6": KeyF6,
"F7": KeyF7,
"F8": KeyF8,
"F9": KeyF9,
"F10": KeyF10,
"F11": KeyF11,
"F12": KeyF12,
"Insert": KeyInsert,
"Delete": KeyDelete,
"Home": KeyHome,
"End": KeyEnd,
"Pgup": KeyPgup,
"Pgdn": KeyPgdn,
"ArrowUp": KeyArrowUp,
"ArrowDown": KeyArrowDown,
"ArrowLeft": KeyArrowLeft,
"ArrowRight": KeyArrowRight,
"MouseLeft": MouseLeft,
"MouseRight": MouseRight,
"MouseMiddle": MouseMiddle,
"BS": KeyBackspace,
"Tab": KeyTab,
"Enter": KeyEnter,
"Esc": KeyEsc,
"Space": KeySpace,
"BS2": KeyBackspace2,
"C-8": KeyCtrl8,
"C-~": KeyCtrlTilde,
"C-2": KeyCtrl2,
"C-Space": KeyCtrlSpace,
"C-a": KeyCtrlA,
"C-b": KeyCtrlB,
"C-c": KeyCtrlC,
"C-d": KeyCtrlD,
"C-e": KeyCtrlE,
"C-f": KeyCtrlF,
"C-g": KeyCtrlG,
"C-h": KeyCtrlH,
"C-i": KeyCtrlI,
"C-j": KeyCtrlJ,
"C-k": KeyCtrlK,
"C-l": KeyCtrlL,
"C-m": KeyCtrlM,
"C-n": KeyCtrlN,
"C-o": KeyCtrlO,
"C-p": KeyCtrlP,
"C-q": KeyCtrlQ,
"C-r": KeyCtrlR,
"C-s": KeyCtrlS,
"C-t": KeyCtrlT,
"C-u": KeyCtrlU,
"C-v": KeyCtrlV,
"C-w": KeyCtrlW,
"C-x": KeyCtrlX,
"C-y": KeyCtrlY,
"C-z": KeyCtrlZ,
"C-[": KeyCtrlLsqBracket,
"C-3": KeyCtrl3,
"C-4": KeyCtrl4,
"C-\\": KeyCtrlBackslash,
"C-5": KeyCtrl5,
"C-]": KeyCtrlRsqBracket,
"C-6": KeyCtrl6,
"C-7": KeyCtrl7,
"C-/": KeyCtrlSlash,
"C-_": KeyCtrlUnderscore,
}
t.Logf("Checking key name -> actual key value mapping...")
for n, v := range expected {
t.Logf(" checking %s...", n)
e, modifier, _, err := ToKey(n)
require.NoError(t, err, "Key name %s not found", n)
require.Equal(t, v, e, "Expected '%s' to be '%d', but got '%d'", n, v, stringToKey[n])
require.Equal(t, ModifierKey(0), modifier, "Key name '%s' is not Alt-prefixed", n)
}
}
func TestKeymapStrToKeyValueWithAlt(t *testing.T) {
expected := map[string]struct {
key KeyType
ch rune
}{
"M-v": {0, 'v'},
"M-C-v": {KeyCtrlV, rune(0)},
"M-Space": {KeySpace, rune(0)},
"M-MouseLeft": {MouseLeft, rune(0)},
}
t.Logf("Checking Alt prefixed key name mapping...")
for n, v := range expected {
t.Logf(" checking %s...", n)
k, modifier, ch, err := ToKey(n)
require.NoError(t, err, "Failed ToKey: Key name %s", n)
require.Equal(t, ModifierKey(1), modifier, "Key name %s has Alt prefix", n)
require.Equal(t, v.key, k, "Expected '%s' to be '%d', but got '%d'", n, v.key, k)
require.Equal(t, v.ch, ch, "Expected '%s' to be '%c', but got '%c'", n, v.ch, ch)
}
}
func TestKeymapStrToKeyValueCh(t *testing.T) {
expected := []string{
"q", "w", "e", "r", "t", "y",
"🙏", "せ", "か", "い", "〠",
}
t.Logf("Checking character mapping...")
for _, n := range expected {
t.Logf(" checking %s...", n)
k, modifier, ch, err := ToKey(n)
require.NoError(t, err, "Failed ToKey: Key name %s", n)
require.Equal(t, KeyType(0), k, "Key name %s is mapped key", n)
require.NotEqual(t, ModifierKey(1), modifier, "Key name %s has Alt prefix", n)
r, _ := utf8.DecodeRuneInString(n)
require.Equal(t, r, ch, "key name %s cannot convert to rune", n)
}
}
func TestKeymapStrToKeyValueWithCtrl(t *testing.T) {
tests := []struct {
name string
key KeyType
}{
{"C-ArrowLeft", KeyArrowLeft},
{"C-ArrowRight", KeyArrowRight},
{"C-ArrowUp", KeyArrowUp},
{"C-ArrowDown", KeyArrowDown},
{"C-Home", KeyHome},
{"C-End", KeyEnd},
{"C-Delete", KeyDelete},
{"C-Insert", KeyInsert},
{"C-Pgup", KeyPgup},
{"C-Pgdn", KeyPgdn},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
k, modifier, ch, err := ToKey(tc.name)
require.NoError(t, err)
require.Equal(t, tc.key, k)
require.Equal(t, ModCtrl, modifier)
require.Equal(t, rune(0), ch)
})
}
}
func TestKeymapStrToKeyValueWithShift(t *testing.T) {
tests := []struct {
name string
key KeyType
}{
{"S-ArrowUp", KeyArrowUp},
{"S-ArrowDown", KeyArrowDown},
{"S-ArrowLeft", KeyArrowLeft},
{"S-ArrowRight", KeyArrowRight},
{"S-Home", KeyHome},
{"S-End", KeyEnd},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
k, modifier, ch, err := ToKey(tc.name)
require.NoError(t, err)
require.Equal(t, tc.key, k)
require.Equal(t, ModShift, modifier)
require.Equal(t, rune(0), ch)
})
}
}
func TestKeymapStrToKeyValueWithCombinedModifiers(t *testing.T) {
tests := []struct {
name string
key KeyType
modifier ModifierKey
}{
{"C-M-ArrowLeft", KeyArrowLeft, ModCtrl | ModAlt},
{"M-C-ArrowLeft", KeyArrowLeft, ModCtrl | ModAlt},
{"C-S-Delete", KeyDelete, ModCtrl | ModShift},
{"C-S-M-Home", KeyHome, ModCtrl | ModShift | ModAlt},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
k, modifier, ch, err := ToKey(tc.name)
require.NoError(t, err)
require.Equal(t, tc.key, k)
require.Equal(t, tc.modifier, modifier)
require.Equal(t, rune(0), ch)
})
}
}
func TestModifierKeyString(t *testing.T) {
tests := []struct {
mod ModifierKey
expected string
}{
{ModNone, ""},
{ModAlt, "M"},
{ModCtrl, "C"},
{ModShift, "S"},
{ModCtrl | ModAlt, "C-M"},
{ModCtrl | ModShift, "C-S"},
{ModShift | ModAlt, "S-M"},
{ModCtrl | ModShift | ModAlt, "C-S-M"},
}
for _, tc := range tests {
t.Run(tc.expected, func(t *testing.T) {
require.Equal(t, tc.expected, tc.mod.String())
})
}
}
func TestKeyEventToStringWithModifiers(t *testing.T) {
tests := []struct {
name string
key KeyType
ch rune
mod ModifierKey
expected string
}{
{"Ctrl+Left", KeyArrowLeft, 0, ModCtrl, "C-<"},
{"Shift+Right", KeyArrowRight, 0, ModShift, "S->"},
{"Ctrl+Alt+Delete", KeyDelete, 0, ModCtrl | ModAlt, "C-M-Delete"},
{"Alt+char", 0, 'v', ModAlt, "M-v"},
{"no modifier", KeyHome, 0, ModNone, "Home"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
s, err := KeyEventToString(tc.key, tc.ch, tc.mod)
require.NoError(t, err)
require.Equal(t, tc.expected, s)
})
}
}

View file

@ -1,49 +1,55 @@
package keyseq
import (
"fmt"
"errors"
"strings"
"sync"
"time"
"github.com/nsf/termbox-go"
)
var ErrInSequence = fmt.Errorf("Currently expecting a key sequence")
var ErrNoMatch = fmt.Errorf("Could not match key to any action")
var ErrInSequence = errors.New("expected a key sequence")
var ErrNoMatch = errors.New("could not match key to any action")
type ModifierKey int
const (
ModNone ModifierKey = iota
ModAlt
ModMax
ModNone ModifierKey = 0
ModAlt ModifierKey = 1 << 0 // 0x01
ModCtrl ModifierKey = 1 << 1 // 0x02
ModShift ModifierKey = 1 << 2 // 0x04
)
// Key is data in one trie node in the KeySequence
type Key struct {
Modifier ModifierKey // Alt, etc
Key termbox.Key
Key KeyType
Ch rune
}
// String returns a comma-separated string representation of the key list.
func (kl KeyList) String() string {
list := make([]string, len(kl))
for i := 0; i < len(kl); i++ {
for i := range kl {
list[i] = kl[i].String()
}
return strings.Join(list, ",")
}
// String returns the modifier key as a dash-separated string (e.g. "C-S-M").
func (m ModifierKey) String() string {
switch m {
case ModAlt:
return "M"
default:
return ""
var parts []string
if m&ModCtrl != 0 {
parts = append(parts, "C")
}
if m&ModShift != 0 {
parts = append(parts, "S")
}
if m&ModAlt != 0 {
parts = append(parts, "M")
}
return strings.Join(parts, "-")
}
// String returns a human-readable representation of the key, including any modifiers.
func (k Key) String() string {
var s string
if m := k.Modifier.String(); m != "" {
@ -59,13 +65,19 @@ func (k Key) String() string {
return s
}
func NewKeyFromKey(k termbox.Key) Key {
return Key{0, k, rune(0)}
// NewKeyFromKey creates a Key from a KeyType with no modifier and no rune.
func NewKeyFromKey(k KeyType) Key {
return Key{
Modifier: 0,
Key: k,
Ch: rune(0),
}
}
// KeyList is just the list of keys
type KeyList []Key
// Compare returns -1, 0, or 1 comparing k and x by modifier, key type, and character.
func (k Key) Compare(x Key) int {
if k.Modifier < x.Modifier {
return -1
@ -88,13 +100,14 @@ func (k Key) Compare(x Key) int {
return 0
}
func (k KeyList) Equals(x KeyList) bool {
if len(k) != len(x) {
// Equals reports whether kl and x contain the same keys in the same order.
func (kl KeyList) Equals(x KeyList) bool {
if len(kl) != len(x) {
return false
}
for i := 0; i < len(k); i++ {
if k[i].Compare(x[i]) != 0 {
for i := range kl {
if kl[i].Compare(x[i]) != 0 {
return false
}
}
@ -108,19 +121,24 @@ type keyseqMatcher interface {
type Keyseq struct {
*Matcher
current keyseqMatcher
mutex *sync.Mutex
prevInputTime time.Time
current keyseqMatcher
mutex sync.Mutex
}
// New creates a new Keyseq matcher for resolving multi-key bindings.
func New() *Keyseq {
return &Keyseq{NewMatcher(), nil, &sync.Mutex{}, time.Time{}}
return &Keyseq{
Matcher: NewMatcher(),
current: nil,
}
}
// InMiddleOfChain reports whether the matcher is partway through a multi-key sequence.
func (k *Keyseq) InMiddleOfChain() bool {
return k.current != nil && k.current != k.Matcher
}
// CancelChain resets the matcher to the root, abandoning any in-progress key sequence.
func (k *Keyseq) CancelChain() {
k.mutex.Lock()
defer k.mutex.Unlock()
@ -139,11 +157,12 @@ func (k *Keyseq) Current() keyseqMatcher {
return k.current
}
func (k *Keyseq) AcceptKey(key Key) (interface{}, error) {
// AcceptKey advances the key sequence matcher with the given key, returning the bound action
// if a complete sequence is matched, or ErrInSequence if more keys are expected.
func (k *Keyseq) AcceptKey(key Key) (any, error) {
// XXX should we return Action instead of interface{}?
k.mutex.Lock()
defer k.mutex.Unlock()
defer func() { k.prevInputTime = time.Now() }()
c := k.Current()
n := c.Get(key)
@ -164,7 +183,7 @@ func (k *Keyseq) AcceptKey(key Key) (interface{}, error) {
return nil, ErrInSequence
}
// If it got here, we should just rest the matcher, and return
// If it got here, we should just reset the matcher, and return
// whatever we matched
k.setCurrent(k.Matcher)
@ -174,5 +193,6 @@ func (k *Keyseq) AcceptKey(key Key) (interface{}, error) {
if data == nil {
return nil, ErrNoMatch
}
return data.(*nodeData).Value(), nil
}
nd, _ := data.(*nodeData)
return nd.Value(), nil
}

View file

@ -4,6 +4,7 @@ type TernaryTrie struct {
root TernaryNode
}
// NewTernaryTrie creates a new empty ternary search trie.
func NewTernaryTrie() *TernaryTrie {
return &TernaryTrie{}
}
@ -20,10 +21,11 @@ func (t *TernaryTrie) Get(k Key) Node {
return Get(t, KeyList{k})
}
func (t *TernaryTrie) Put(k KeyList, v interface{}) Node {
func (t *TernaryTrie) Put(k KeyList, v any) Node {
return Put(t, k, v)
}
// Size returns the total number of nodes in the trie.
func (t *TernaryTrie) Size() int {
count := 0
EachDepth(t, func(Node) bool {
@ -33,9 +35,11 @@ func (t *TernaryTrie) Size() int {
return count
}
// Balance rebalances all sibling lists in the trie for optimal search performance.
func (t *TernaryTrie) Balance() {
EachDepth(t, func(n Node) bool {
n.(*TernaryNode).Balance()
tn, _ := n.(*TernaryNode)
tn.Balance()
return true
})
t.root.Balance()
@ -45,17 +49,23 @@ type TernaryNode struct {
label Key
firstChild *TernaryNode
low, high *TernaryNode
value interface{}
value any
}
// NewTernaryNode creates a new ternary trie node with the given key label.
func NewTernaryNode(l Key) *TernaryNode {
return &TernaryNode{label: l}
}
// GetList looks up a child node matching the first key in the list.
func (n *TernaryNode) GetList(k KeyList) Node {
if len(k) == 0 {
return nil
}
return n.Get(k[0])
}
// Get searches the children of this node for a child matching key k.
func (n *TernaryNode) Get(k Key) Node {
curr := n.firstChild
for curr != nil {
@ -71,7 +81,8 @@ func (n *TernaryNode) Get(k Key) Node {
return nil
}
func (n *TernaryNode) Dig(k Key) (node Node, isnew bool) {
// 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, bool) {
curr := n.firstChild
if curr == nil {
n.firstChild = NewTernaryNode(k)
@ -105,6 +116,7 @@ func (n *TernaryNode) HasChildren() bool {
return n.firstChild != nil
}
// Size returns the number of direct children of this node.
func (n *TernaryNode) Size() int {
if n.firstChild == nil {
return 0
@ -117,6 +129,7 @@ func (n *TernaryNode) Size() int {
return count
}
// Each calls proc for every child node in sorted order, stopping early if proc returns false.
func (n *TernaryNode) Each(proc func(Node) bool) {
var f func(*TernaryNode) bool
f = func(n *TernaryNode) bool {
@ -130,6 +143,7 @@ func (n *TernaryNode) Each(proc func(Node) bool) {
f(n.firstChild)
}
// RemoveAll removes all children from this node.
func (n *TernaryNode) RemoveAll() {
n.firstChild = nil
}
@ -138,14 +152,15 @@ func (n *TernaryNode) Label() Key {
return n.label
}
func (n *TernaryNode) Value() interface{} {
func (n *TernaryNode) Value() any {
return n.value
}
func (n *TernaryNode) SetValue(v interface{}) {
func (n *TernaryNode) SetValue(v any) {
n.value = v
}
// children collects all direct child nodes into a sorted slice.
func (n *TernaryNode) children() []*TernaryNode {
children := make([]*TernaryNode, n.Size())
if n.firstChild == nil {
@ -153,13 +168,15 @@ func (n *TernaryNode) children() []*TernaryNode {
}
idx := 0
n.Each(func(child Node) bool {
children[idx] = child.(*TernaryNode)
tn, _ := child.(*TernaryNode)
children[idx] = tn
idx++
return true
})
return children
}
// Balance rebalances the children of this node into a balanced binary search tree.
func (n *TernaryNode) Balance() {
if n.firstChild == nil {
return
@ -172,6 +189,7 @@ func (n *TernaryNode) Balance() {
n.firstChild = balance(children, 0, len(children))
}
// balance recursively builds a balanced binary tree from a sorted slice of nodes.
func balance(nodes []*TernaryNode, s, e int) *TernaryNode {
count := e - s
if count <= 0 {
@ -181,11 +199,10 @@ func balance(nodes []*TernaryNode, s, e int) *TernaryNode {
} else if count == 2 {
nodes[s].high = nodes[s+1]
return nodes[s]
} else {
mid := (s + e) / 2
n := nodes[mid]
n.low = balance(nodes, s, mid)
n.high = balance(nodes, mid+1, e)
return n
}
mid := (s + e) / 2
n := nodes[mid]
n.low = balance(nodes, s, mid)
n.high = balance(nodes, mid+1, e)
return n
}

View file

@ -0,0 +1,51 @@
package keyseq
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestBalance(t *testing.T) {
trie := NewTernaryTrie()
list := make([]Key, 0, 15)
for i := range 15 {
list = append(list, Key{0, 0, rune(i)})
}
for i, k := range list {
trie.Put(KeyList{k}, i)
}
require.Equal(t, 15, trie.Size())
trie.Balance()
// After balancing, all keys must still be retrievable with correct values.
for i, k := range list {
node := trie.Get(k)
require.NotNil(t, node, "key %d should be found after Balance", i)
require.Equal(t, i, node.Value(), "value for key %d should be %d", i, i)
}
// Size must be unchanged after balancing.
require.Equal(t, 15, trie.Size())
}
func TestBalancePreservesMultiKeySequences(t *testing.T) {
trie := NewTernaryTrie()
// Insert multi-key sequences
k1 := KeyList{{0, 0, 'a'}, {0, 0, 'b'}}
k2 := KeyList{{0, 0, 'a'}, {0, 0, 'c'}}
k3 := KeyList{{0, 0, 'x'}}
trie.Put(k1, "ab")
trie.Put(k2, "ac")
trie.Put(k3, "x")
trie.Balance()
require.Equal(t, "ab", trie.GetList(k1).Value())
require.Equal(t, "ac", trie.GetList(k2).Value())
require.Equal(t, "x", trie.GetList(k3).Value())
}

View file

@ -8,14 +8,16 @@ type Trie interface {
Root() Node
GetList(KeyList) Node
Get(Key) Node
Put(KeyList, interface{}) Node
Put(KeyList, any) Node
Size() int
}
// NewTrie creates a new empty Trie for storing key sequences.
func NewTrie() Trie {
return NewTernaryTrie()
}
// Get looks up a value by key sequence path in the trie, returning nil if not found.
func Get(t Trie, k KeyList) Node {
if t == nil {
return nil
@ -30,7 +32,8 @@ func Get(t Trie, k KeyList) Node {
return n
}
func Put(t Trie, k KeyList, v interface{}) Node {
// Put inserts a value at the given key sequence path in the trie, creating nodes as needed.
func Put(t Trie, k KeyList, v any) Node {
if t == nil {
return nil
}
@ -42,6 +45,7 @@ func Put(t Trie, k KeyList, v interface{}) Node {
return n
}
// EachDepth iterates over trie nodes in depth-first order, calling proc for each node.
func EachDepth(t Trie, proc func(Node) bool) {
if t == nil {
return
@ -55,6 +59,7 @@ func EachDepth(t Trie, proc func(Node) bool) {
r.Each(f)
}
// EachWidth iterates over trie nodes in breadth-first order, calling proc for each node.
func EachWidth(t Trie, proc func(Node) bool) {
if t == nil {
return
@ -64,7 +69,10 @@ func EachWidth(t Trie, proc func(Node) bool) {
for q.Len() != 0 {
f := q.Front()
q.Remove(f)
t := f.Value.(Node)
t, ok := f.Value.(Node)
if !ok {
break
}
if !proc(t) {
break
}
@ -85,10 +93,11 @@ type Node interface {
RemoveAll()
Label() Key
Value() interface{}
SetValue(v interface{})
Value() any
SetValue(v any)
}
// Children returns all child nodes of n as a slice.
func Children(n Node) []Node {
children := make([]Node, n.Size())
idx := 0

View file

@ -0,0 +1,32 @@
package keyseq
import (
"testing"
"github.com/stretchr/testify/require"
)
func checkTrieNode(t *testing.T, n Node, k Key, value int) {
require.NotNil(t, n, "TrieNode is null")
require.Equal(t, k, n.Label(), "TrieNode.Label()")
require.Equal(t, value, n.Value().(int), "TrieNode.Value()")
}
func TestTrie(t *testing.T) {
trie := NewTrie()
for i := 1; i <= 5; i++ {
trie.Put(KeyList{Key{0, 0, rune(i)}}, 111*i)
}
nodes := Children(trie.Root())
for i := range 5 {
checkTrieNode(t, nodes[i], Key{0, 0, rune(i + 1)}, 111*(i+1))
}
require.Equal(t, 5, trie.Size())
}
func TestNotFound(t *testing.T) {
trie := NewTrie()
require.Nil(t, trie.Get(Key{999, 999, 'a'}))
}

View file

@ -0,0 +1,40 @@
package util
import (
"fmt"
"testing"
)
func BenchmarkStripANSISequenceBulk(b *testing.B) {
b.Run("10k_plain_lines", func(b *testing.B) {
lines := make([]string, 10_000)
for i := range lines {
lines[i] = fmt.Sprintf("line %d: this is a typical line without any ansi codes at all padding", i)
}
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
for _, l := range lines {
StripANSISequence(l)
}
}
})
b.Run("10k_mixed_95pct_plain", func(b *testing.B) {
lines := make([]string, 10_000)
for i := range lines {
if i%20 == 0 {
lines[i] = fmt.Sprintf("\x1b[31mline %d\x1b[0m: this has ansi codes in it for color", i)
} else {
lines[i] = fmt.Sprintf("line %d: this is a typical line without any ansi codes at all padding", i)
}
}
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
for _, l := range lines {
StripANSISequence(l)
}
}
})
}

View file

@ -0,0 +1,15 @@
package util
import (
"errors"
"os"
)
func Homedir() (string, error) {
home := os.Getenv("HOME")
if home == "" {
return "", errors.New("environment variable HOME not set")
}
return home, nil
}

View file

@ -0,0 +1,18 @@
//go:build !darwin && !windows
package util
import (
"errors"
"os"
)
// Homedir returns the current user's home directory from the HOME environment variable.
func Homedir() (string, error) {
home := os.Getenv("HOME")
if home == "" {
return "", errors.New("environment variable HOME not set")
}
return home, nil
}

View file

@ -1,11 +1,11 @@
package peco
package util
import "os/user"
func homedir() (string, error) {
func Homedir() (string, error) {
u, err := user.Current()
if err != nil {
return "", err
}
return u.HomeDir, nil
}
}

View file

@ -0,0 +1,22 @@
//go:build !windows
package util
import (
"context"
"os/exec"
)
// Shell creates an exec.Cmd that runs the given command strings via /bin/sh -c.
func Shell(ctx context.Context, cmd ...string) *exec.Cmd {
const shellpath = `/bin/sh`
const shellopt = `-c`
args := make([]string, len(cmd)+1)
args[0] = shellopt
for i := range cmd {
args[i+1] = cmd[i]
}
return exec.CommandContext(ctx, shellpath, args...)
}

View file

@ -0,0 +1,21 @@
//go:build windows
package util
import (
"context"
"os/exec"
)
func Shell(ctx context.Context, cmd ...string) *exec.Cmd {
const shellpath = `cmd`
const shellopt = `/c`
args := make([]string, len(cmd)+1)
args[0] = shellopt
for i := range cmd {
args[i+1] = cmd[i]
}
return exec.CommandContext(ctx, shellpath, args...)
}

30
internal/util/tty_bsd.go Normal file
View file

@ -0,0 +1,30 @@
//go:build darwin || freebsd || openbsd || netbsd || dragonfly
package util
import (
"syscall"
"unsafe"
)
// IsTty checks if the given fd is a tty
func IsTty(arg any) bool {
fdsrc, ok := arg.(fder)
if !ok {
return false
}
fd := fdsrc.Fd()
var termios syscall.Termios
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, fd, uintptr(syscall.TIOCGETA), uintptr(unsafe.Pointer(&termios)), 0, 0, 0)
return err == 0
}
// TtyReady checks if the tty is ready to go
func TtyReady() error {
return nil
}
// TtyTerm restores any state, if necessary
func TtyTerm() {
}

View file

@ -0,0 +1,30 @@
//go:build !darwin && !windows && !freebsd && !openbsd && !netbsd && !dragonfly
package util
import (
"syscall"
"unsafe"
)
// IsTty checks if the given fd is a tty
func IsTty(arg any) bool {
fdsrc, ok := arg.(fder)
if !ok {
return false
}
fd := fdsrc.Fd()
var termios syscall.Termios
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, fd, uintptr(syscall.TCGETS), uintptr(unsafe.Pointer(&termios)), 0, 0, 0)
return err == 0
}
// TtyReady checks if the tty is ready to go
func TtyReady() error {
return nil
}
// TtyTerm restores any state, if necessary
func TtyTerm() {
}

View file

@ -0,0 +1,68 @@
package util
import (
"fmt"
"os"
"syscall"
"unsafe"
)
var (
kernel32 = syscall.MustLoadDLL("kernel32.dll")
procSetStdHandle = kernel32.MustFindProc("SetStdHandle")
procGetConsoleMode = kernel32.MustFindProc("GetConsoleMode")
)
func getStdHandle(h int) (fd syscall.Handle) {
r, _ := syscall.GetStdHandle(h)
syscall.CloseOnExec(r)
return r
}
func setStdHandle(stdhandle int32, handle syscall.Handle) error {
r1, _, err := procSetStdHandle.Call(uintptr(stdhandle), uintptr(handle))
if r1 == 0 {
return fmt.Errorf("failed to call SetStdHandle: %w", err)
}
return nil
}
// IsTty checks if the given fd is a tty
func IsTty(arg any) bool {
fdsrc, ok := arg.(fder)
if !ok {
return false
}
fd := fdsrc.Fd()
var st uint32
r1, _, _ := procGetConsoleMode.Call(fd, uintptr(unsafe.Pointer(&st)))
return r1 != 0
}
var stdin = os.Stdin
// TtyReady checks if the tty is ready to go
func TtyReady() error {
var err error
_stdin, err := os.Open("CONIN$")
if err != nil {
return err
}
stdin = os.Stdin
os.Stdin = _stdin
syscall.Stdin = syscall.Handle(os.Stdin.Fd())
if err = setStdHandle(syscall.STD_INPUT_HANDLE, syscall.Stdin); err != nil {
return fmt.Errorf("failed to check for TtyReady: %w", err)
}
return nil
}
// TtyTerm restores any state, if necessary
func TtyTerm() {
os.Stdin = stdin
syscall.Stdin = syscall.Handle(os.Stdin.Fd())
setStdHandle(syscall.STD_INPUT_HANDLE, syscall.Stdin)
}

98
internal/util/util.go Normal file
View file

@ -0,0 +1,98 @@
package util
import (
"errors"
"regexp"
"strings"
"unicode"
)
type fder interface {
Fd() uintptr
}
// CaseInsensitiveIndexFunc returns a function that matches runes equal to r, ignoring case.
func CaseInsensitiveIndexFunc(r rune) func(rune) bool {
lr := unicode.ToUpper(r)
return func(v rune) bool {
return lr == unicode.ToUpper(v)
}
}
// CaseInsensitiveIndex returns the byte index of the first rune in s that
// is case-insensitively equal to r. Returns -1 if not found. This avoids
// the closure allocation of CaseInsensitiveIndexFunc + strings.IndexFunc.
func CaseInsensitiveIndex(s string, r rune) int {
upper := unicode.ToUpper(r)
for i, c := range s {
if unicode.ToUpper(c) == upper {
return i
}
}
return -1
}
// ContainsUpper reports whether the string contains any uppercase letter.
func ContainsUpper(query string) bool {
for _, c := range query {
if unicode.IsUpper(c) {
return true
}
}
return false
}
// Global var used to strips ansi sequences
var reANSIEscapeChars = regexp.MustCompile("\x1B\\[(?:[0-9]{1,2}(?:;[0-9]{1,2})?)*[a-zA-Z]")
// StripANSISequence strips ANSI escape sequences from the given string.
// Fast-path: if the string contains no ESC byte, return it unchanged
// to avoid a regexp allocation.
func StripANSISequence(s string) string {
if !strings.Contains(s, "\x1b") {
return s
}
return reANSIEscapeChars.ReplaceAllString(s, "")
}
type ignorable interface {
Ignorable() bool
}
type collectResults interface {
CollectResults() bool
}
type exitStatuser interface {
ExitStatus() int
}
// IsIgnorableError checks whether err implements the Ignorable interface and returns true.
func IsIgnorableError(err error) bool {
for e := err; e != nil; e = errors.Unwrap(e) {
if v, ok := e.(ignorable); ok {
return v.Ignorable()
}
}
return false
}
// IsCollectResultsError checks whether err signals that results should be collected.
func IsCollectResultsError(err error) bool {
for e := err; e != nil; e = errors.Unwrap(e) {
if v, ok := e.(collectResults); ok {
return v.CollectResults()
}
}
return false
}
// GetExitStatus extracts the exit status code from an error, returning 1 and false if not found.
func GetExitStatus(err error) (int, bool) {
for e := err; e != nil; e = errors.Unwrap(e) {
if ese, ok := e.(exitStatuser); ok {
return ese.ExitStatus(), true
}
}
return 1, false
}

173
internal/util/util_test.go Normal file
View file

@ -0,0 +1,173 @@
package util
import (
"errors"
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
func TestCaseInsensitiveIndexFunc(t *testing.T) {
t.Parallel()
tests := []struct {
name string
r rune
matchR rune
matches bool
}{
{"same lowercase", 'a', 'a', true},
{"upper matches lower", 'a', 'A', true},
{"lower matches upper", 'A', 'a', true},
{"different chars", 'a', 'b', false},
{"unicode same", 'ä', 'Ä', true},
{"digit matches itself", '1', '1', true},
{"digit vs letter", '1', 'a', false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
fn := CaseInsensitiveIndexFunc(tt.r)
require.Equal(t, tt.matches, fn(tt.matchR))
})
}
}
func TestContainsUpper(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
expected bool
}{
{"all lowercase", "hello", false},
{"all uppercase", "HELLO", true},
{"mixed case", "hEllo", true},
{"empty string", "", false},
{"numbers only", "12345", false},
{"with uppercase at end", "hellO", true},
{"with uppercase at start", "Hello", true},
{"unicode lowercase", "こんにちは", false},
{"special chars", "!@#$%", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
require.Equal(t, tt.expected, ContainsUpper(tt.input))
})
}
}
func TestStripANSISequence(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
expected string
}{
{"no ANSI", "hello", "hello"},
{"empty string", "", ""},
{"bold", "\x1B[1mhello\x1B[0m", "hello"},
{"color red", "\x1B[31mred text\x1B[0m", "red text"},
{"multiple sequences", "\x1B[1m\x1B[31mhello\x1B[0m", "hello"},
{"color with semicolon", "\x1B[1;31mbold red\x1B[0m", "bold red"},
{"mixed content", "before\x1B[32mgreen\x1B[0mafter", "beforegreenafter"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
require.Equal(t, tt.expected, StripANSISequence(tt.input))
})
}
}
// Mock error types for testing error interface checks
type mockIgnorableError struct {
ignorable bool
msg string
}
func (e *mockIgnorableError) Error() string { return e.msg }
func (e *mockIgnorableError) Ignorable() bool { return e.ignorable }
type mockCollectResultsError struct {
collect bool
msg string
}
func (e *mockCollectResultsError) Error() string { return e.msg }
func (e *mockCollectResultsError) CollectResults() bool { return e.collect }
type mockExitStatusError struct {
status int
msg string
}
func (e *mockExitStatusError) Error() string { return e.msg }
func (e *mockExitStatusError) ExitStatus() int { return e.status }
func TestIsIgnorableError(t *testing.T) {
t.Parallel()
tests := []struct {
name string
err error
expected bool
}{
{"ignorable true", &mockIgnorableError{ignorable: true, msg: "test"}, true},
{"ignorable false", &mockIgnorableError{ignorable: false, msg: "test"}, false},
{"non-ignorable error", errors.New("plain error"), false},
{"wrapped ignorable", fmt.Errorf("wrapper: %w", &mockIgnorableError{ignorable: true, msg: "inner"}), true},
{"wrapped non-ignorable", fmt.Errorf("wrapper: %w", errors.New("plain")), false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
require.Equal(t, tt.expected, IsIgnorableError(tt.err))
})
}
}
func TestIsCollectResultsError(t *testing.T) {
t.Parallel()
tests := []struct {
name string
err error
expected bool
}{
{"collect true", &mockCollectResultsError{collect: true, msg: "test"}, true},
{"collect false", &mockCollectResultsError{collect: false, msg: "test"}, false},
{"plain error", errors.New("plain error"), false},
{"wrapped collect", fmt.Errorf("wrapper: %w", &mockCollectResultsError{collect: true, msg: "inner"}), true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
require.Equal(t, tt.expected, IsCollectResultsError(tt.err))
})
}
}
func TestGetExitStatus(t *testing.T) {
t.Parallel()
tests := []struct {
name string
err error
expectedStatus int
expectedFound bool
}{
{"exit status 0", &mockExitStatusError{status: 0, msg: "test"}, 0, true},
{"exit status 1", &mockExitStatusError{status: 1, msg: "test"}, 1, true},
{"exit status 42", &mockExitStatusError{status: 42, msg: "test"}, 42, true},
{"plain error", errors.New("plain"), 1, false},
{"wrapped exit status", fmt.Errorf("wrapper: %w", &mockExitStatusError{status: 2, msg: "inner"}), 2, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
status, found := GetExitStatus(tt.err)
require.Equal(t, tt.expectedStatus, status)
require.Equal(t, tt.expectedFound, found)
})
}
}

161
issues_test.go Normal file
View file

@ -0,0 +1,161 @@
package peco
import (
"fmt"
"io"
"os"
"testing"
"time"
"context"
"github.com/peco/peco/config"
"github.com/peco/peco/filter"
"github.com/peco/peco/internal/keyseq"
"github.com/peco/peco/line"
"github.com/peco/peco/pipeline"
"github.com/stretchr/testify/require"
)
func TestIssue212_SanityCheck(t *testing.T) {
state, ctx := setupPecoTest(t)
// Check if the default layout type is honored */
// This the main issue on 212, but while we're at it, we're just
// going to check that all the default values are as expected
require.Equal(t, state.config.Layout, "top-down", "Default layout type should be 'top-down', got '%s'", state.config.Layout)
require.Equal(t, len(state.config.Keymap), 0, "Default keymap should be empty, but got '%#v'", state.config.Keymap)
defstyle := config.StyleSet{}
defstyle.Init()
require.Equal(t, state.config.Style, defstyle, "should be default style")
require.Equal(t, state.config.Prompt, "QUERY>", "Default prompt should be 'QUERY>', but got '%s'", state.config.Prompt)
// Okay, this time create a dummy config file, and read that in
f, err := os.CreateTemp(t.TempDir(), "peco-test-config")
require.NoError(t, err, "Failed to create temporary config file: %s", err)
fn := f.Name()
defer os.Remove(fn)
io.WriteString(f, `{
"Layout": "bottom-up"
}`)
f.Close()
state = newPeco()
go state.Run(ctx)
<-state.Ready()
require.NoError(t, state.config.ReadFilename(fn), "Failed to read config: %s", err)
require.Equal(t, state.config.Layout, "bottom-up", "Default layout type should be 'bottom-up', got '%s'", state.config.Layout)
}
func TestIssue345(t *testing.T) {
cfg, err := newConfig(`{
"Keymap": {
"C-t": "my.ToggleSelectionInAboveLine"
},
"Action": {
"my.ToggleSelectionInAboveLine": [
"peco.SelectUp",
"peco.ToggleSelectionAndSelectNext"
]
}
}`)
require.NoError(t, err, "newConfig should succeed")
defer os.Remove(cfg)
state := newPeco()
state.configReader = defaultConfigReader
require.NoError(t, state.config.Init(), "Config.Init should succeed")
state.Argv = append(state.Argv, []string{"--rcfile", cfg}...)
ctx, cancel := context.WithCancel(context.Background())
go state.Run(ctx)
defer cancel()
<-state.Ready()
ev := Event{
Type: EventKey,
Key: keyseq.KeyCtrlT,
}
require.NoError(t, state.Keymap().ExecuteAction(ctx, state, ev), "ExecuteAction should succeed")
// Brief pause to let async hub messages from the combined action
// be processed before context cancellation tears down goroutines.
time.Sleep(50 * time.Millisecond)
}
// TestIssue557_FilterBufSize verifies that configuring FilterBufSize allows
// FuzzyLongestSort to correctly sort results across what would otherwise be
// separate chunks. With the default buf size of 1000, lines are batched into
// 1000-line chunks and sorting only happens within each chunk. An exact match
// in chunk 2 would always appear after all chunk 1 results.
func TestIssue557_FilterBufSize(t *testing.T) {
const totalLines = 1100
const query = "exact"
// Build lines: 1 partial match, then 1098 non-matching lines, then 1 exact match.
// The exact match ("exact") is at index 1099, which with the default buf size of
// 1000 would land in the second chunk.
var lines []line.Line
lines = append(lines, line.NewRaw(0, "e_x_a_c_t filler text", false, false)) // fuzzy match
for i := 1; i < totalLines-1; i++ {
lines = append(lines, line.NewRaw(uint64(i), fmt.Sprintf("no match line %d", i), false, false))
}
lines = append(lines, line.NewRaw(uint64(totalLines-1), "exact", false, false)) // exact match, best result
f := filter.NewFuzzy(true)
// Use a configBufSize large enough to hold all lines in a single batch.
// This ensures the fuzzy filter sees all lines at once and can sort globally.
configBufSize := totalLines + 100
fp := newFilterProcessor(f, query, configBufSize, nil)
// Set up pipeline: source -> filterProcessor -> destination
p := pipeline.New()
src := &sliceSource{lines: lines}
p.SetSource(src)
p.Add(fp)
dst := NewMemoryBuffer(0)
p.SetDestination(dst)
ctx := f.NewContext(context.Background(), query)
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
err := p.Run(ctx)
require.NoError(t, err, "pipeline.Run should succeed")
require.True(t, dst.Size() >= 2, "expected at least 2 matched lines, got %d", dst.Size())
// The first result should be the exact match "exact" since it has the
// longest continuous match and shortest line length.
first, err := dst.LineAt(0)
require.NoError(t, err)
require.Equal(t, "exact", first.DisplayString(), "best match should be first")
}
// sliceSource is a simple pipeline.Source backed by a slice of lines.
type sliceSource struct {
lines []line.Line
}
func (s *sliceSource) Start(ctx context.Context, out pipeline.ChanOutput) {
defer close(out)
for _, l := range s.lines {
select {
case <-ctx.Done():
return
default:
out.Send(ctx, l)
}
}
}
func (s *sliceSource) Reset() {}

191
keymap.go
View file

@ -1,54 +1,168 @@
package peco
import (
"context"
"errors"
"fmt"
"os"
"maps"
"sort"
"strings"
"time"
"github.com/nsf/termbox-go"
"github.com/peco/peco/keyseq"
"github.com/lestrrat-go/pdebug"
"github.com/peco/peco/internal/keyseq"
)
// Keyseq is the interface for key sequence matching engines.
type Keyseq interface {
Add(keyseq.KeyList, any)
AcceptKey(keyseq.Key) (any, error)
CancelChain()
Clear()
Compile() error
InMiddleOfChain() bool
}
// Keymap holds all the key sequence to action map
type Keymap struct {
Config map[string]string
Keyseq *keyseq.Keyseq
Action map[string][]string // custom actions
seq Keyseq
}
func NewKeymap(config map[string]string) Keymap {
return Keymap{config, keyseq.New()}
// NewKeymap creates a new Keymap struct
func NewKeymap(config map[string]string, actions map[string][]string) Keymap {
return Keymap{
Config: config,
Action: actions,
seq: keyseq.New(),
}
}
func (km Keymap) Handler(ev termbox.Event) Action {
modifier := keyseq.ModNone
if (ev.Mod & termbox.ModAlt) != 0 {
modifier = keyseq.ModAlt
func (km Keymap) Sequence() Keyseq {
return km.seq
}
// ExecuteAction looks up and executes the action(s) bound to the given key event.
func (km Keymap) ExecuteAction(ctx context.Context, state *Peco, ev Event) (err error) {
if pdebug.Enabled {
g := pdebug.Marker("Keymap.ExecuteAction %v", ev).BindError(&err)
defer g.End()
}
key := keyseq.Key{modifier, ev.Key, ev.Ch}
action, err := km.Keyseq.AcceptKey(key)
a := km.LookupAction(ev)
if a == nil {
return errors.New("action not found")
}
a.Execute(ctx, state, ev)
return nil
}
// LookupAction returns the appropriate action for the given event
func (km Keymap) LookupAction(ev Event) Action {
key := keyseq.Key{
Modifier: ev.Mod,
Key: ev.Key,
Ch: ev.Ch,
}
action, err := km.seq.AcceptKey(key)
switch err {
case nil:
// Found an action!
return action.(Action)
if pdebug.Enabled {
pdebug.Printf("Keymap.Handler: Fetched action")
}
a, ok := action.(Action)
if !ok {
return ActionFunc(doNothing)
}
return wrapClearSequence(a)
case keyseq.ErrInSequence:
// TODO We're in some sort of key sequence. Remember what we have
// received so far
return ActionFunc(doNothing)
if pdebug.Enabled {
pdebug.Printf("Keymap.Handler: Waiting for more commands...")
}
return wrapRememberSequence(ActionFunc(doNothing))
default:
return ActionFunc(doAcceptChar)
if pdebug.Enabled {
pdebug.Printf("Keymap.Handler: Defaulting to doAcceptChar")
}
return wrapClearSequence(ActionFunc(doAcceptChar))
}
}
func (km Keymap) ApplyKeybinding() {
k := km.Keyseq
// wrapRememberSequence wraps an action to record the key press as part of a multi-key sequence.
func wrapRememberSequence(a Action) Action {
return ActionFunc(func(ctx context.Context, state *Peco, ev Event) {
if s, err := keyseq.KeyEventToString(ev.Key, ev.Ch, ev.Mod); err == nil {
seq := state.Inputseq()
seq.Add(s)
state.Hub().SendStatusMsg(ctx, strings.Join(seq.KeyNames(), " "), 0)
}
a.Execute(ctx, state, ev)
})
}
// wrapClearSequence wraps an action to clear the accumulated key sequence after execution.
func wrapClearSequence(a Action) Action {
return ActionFunc(func(ctx context.Context, state *Peco, ev Event) {
seq := state.Inputseq()
if s, err := keyseq.KeyEventToString(ev.Key, ev.Ch, ev.Mod); err == nil {
seq.Add(s)
}
if seq.Len() > 0 {
msg := strings.Join(seq.KeyNames(), " ")
state.Hub().SendStatusMsg(ctx, msg, 500*time.Millisecond)
seq.Reset()
}
a.Execute(ctx, state, ev)
})
}
const maxResolveActionDepth = 100
// resolveActionName maps a string action name from config to the corresponding action function.
func (km Keymap) resolveActionName(name string, depth int) (Action, error) {
if depth >= maxResolveActionDepth {
return nil, fmt.Errorf("could not resolve %s: deep recursion", name)
}
// Can it be resolved via regular nameToActions ?
v, ok := nameToActions[name]
if ok {
return v, nil
}
// Can it be resolved via combined actions?
l, ok := km.Action[name]
if ok {
actions := []Action{}
for _, actionName := range l {
child, err := km.resolveActionName(actionName, depth+1)
if err != nil {
return nil, err
}
actions = append(actions, child)
}
v = makeCombinedAction(actions...)
return v, nil
}
return nil, fmt.Errorf("could not resolve %s: no such action", name)
}
// ApplyKeybinding applies all of the custom key bindings on top of
// the default key bindings
func (km *Keymap) ApplyKeybinding() error {
k := km.seq
k.Clear()
// Copy the map
kb := map[string]Action{}
for s, a := range defaultKeyBinding {
kb[s] = a
}
maps.Copy(kb, defaultKeyBinding)
// munge the map using config
for s, as := range km.Config {
@ -57,29 +171,34 @@ func (km Keymap) ApplyKeybinding() {
continue
}
v, ok := nameToActions[as]
if !ok {
fmt.Fprintf(os.Stderr, "Unknown handler '%s'\n", as)
continue
v, err := km.resolveActionName(as, 0)
if err != nil {
return fmt.Errorf("failed to resolve action name %s: %w", as, err)
}
kb[s] = v
}
// now compile using kb
for s, a := range kb {
// there's no need to do this, but we sort keys here just to make
// debugging easier
keys := make([]string, 0, len(kb))
for s := range kb {
keys = append(keys, s)
}
sort.Strings(keys)
for _, s := range keys {
a := kb[s]
list, err := keyseq.ToKeyList(s)
if err != nil {
fmt.Fprintf(os.Stderr, "Unknown key %s: %s", s, err)
continue
return fmt.Errorf("unknown key %s: %w", s, err)
}
k.Add(list, a)
}
k.Compile()
}
// TODO: this needs to be fixed.
func (km Keymap) hasModifierMaps() bool {
return false
if err := k.Compile(); err != nil {
return fmt.Errorf("failed to compile key binding patterns: %w", err)
}
return nil
}

View file

@ -1,24 +0,0 @@
package peco
import "testing"
func TestSelection(t *testing.T) {
s := Selection([]int{})
s.Add(10)
if s.Len() != 1 {
t.Errorf("expected Len = 1, got %d", s.Len())
}
s.Add(1)
if s.Len() != 2 {
t.Errorf("expected Len = 2, got %d", s.Len())
}
s.Add(1)
if s.Len() != 2 {
t.Errorf("expected Len = 2, got %d", s.Len())
}
s.Remove(1)
if s.Len() != 1 {
t.Errorf("expected Len = 1, got %d", s.Len())
}
}

View file

@ -1,112 +0,0 @@
package keyseq
import (
"testing"
"github.com/nsf/termbox-go"
)
func checkNode(t *testing.T, node Node, size int, data nodeData) {
if node == nil {
t.Error("Nil node:", data)
}
if node.Size() != size {
t.Errorf("Unexpected childrens: %d != %d", node.Size(), size)
}
d := node.Value().(*nodeData)
if d == nil {
t.Error("Nil data:", data, node)
}
if data.pattern != nil && !d.pattern.Equals(*data.pattern) {
t.Error("Pattern unmatched:", data, node, *d.pattern)
}
if data.value != nil && d.value != data.value {
t.Error("Value unmatched:", data, node, d.value)
}
if d.failure == nil {
t.Error("Nil failure:", data, node)
} else if d.failure != data.failure {
t.Errorf("Failure unmatched: data=%+v node=%+v d.failure=%+v",
data, node, d.failure)
}
}
func invalidData(failure Node) nodeData {
return nodeData{
failure: failure.(*TernaryNode),
}
}
func validData(pattern KeyList, value interface{}, failure Node) nodeData {
return nodeData{
pattern: &pattern,
value: value,
failure: failure.(*TernaryNode),
}
}
func newTestMatcher() *Matcher {
m := NewMatcher()
m.Add(KeyList{Key{0, termbox.KeyCtrlA, rune(0)}, Key{0, termbox.KeyCtrlB, rune(0)}}, 2)
m.Add(KeyList{Key{0, termbox.KeyCtrlB, rune(0)}, Key{0, termbox.KeyCtrlC, rune(0)}}, 4)
m.Add(KeyList{Key{0, termbox.KeyCtrlB, rune(0)}, Key{0, termbox.KeyCtrlA, rune(0)}, Key{0, termbox.KeyCtrlB, rune(0)}}, 6)
m.Add(KeyList{Key{0, termbox.KeyCtrlD, rune(0)}}, 7)
m.Add(KeyList{Key{0, termbox.KeyCtrlA, rune(0)}, Key{0, termbox.KeyCtrlB, rune(0)}, Key{0, termbox.KeyCtrlC, rune(0)}, Key{0, termbox.KeyCtrlD, rune(0)}, Key{0, termbox.KeyCtrlE, rune(0)}}, 10)
m.Compile()
return m
}
func TestTree(t *testing.T) {
m := newTestMatcher()
// Check tree structure.
r := m.Root()
checkNode(t, r, 3, invalidData(r))
n1 := r.Get(NewKeyFromKey(termbox.KeyCtrlA))
checkNode(t, n1, 1, invalidData(r))
n3 := r.Get(NewKeyFromKey(termbox.KeyCtrlB))
checkNode(t, n3, 2, invalidData(r))
n7 := r.Get(NewKeyFromKey(termbox.KeyCtrlD))
checkNode(t, n7, 0, invalidData(r))
n2 := n1.Get(NewKeyFromKey(termbox.KeyCtrlB))
checkNode(t, n2, 1, validData(KeyList{NewKeyFromKey(termbox.KeyCtrlA), NewKeyFromKey(termbox.KeyCtrlB)}, 2, n3))
n4 := n3.Get(NewKeyFromKey(termbox.KeyCtrlC))
checkNode(t, n4, 0, validData(KeyList{NewKeyFromKey(termbox.KeyCtrlB), NewKeyFromKey(termbox.KeyCtrlC)}, 4, r))
n5 := n3.Get(NewKeyFromKey(termbox.KeyCtrlA))
checkNode(t, n5, 1, invalidData(n1))
n8 := n2.Get(NewKeyFromKey(termbox.KeyCtrlC))
checkNode(t, n8, 1, invalidData(n4))
n6 := n5.Get(NewKeyFromKey(termbox.KeyCtrlB))
checkNode(t, n6, 0, validData(KeyList{NewKeyFromKey(termbox.KeyCtrlB), NewKeyFromKey(termbox.KeyCtrlA), NewKeyFromKey(termbox.KeyCtrlB)}, 6, n2))
n9 := n8.Get(NewKeyFromKey(termbox.KeyCtrlD))
checkNode(t, n9, 1, invalidData(n7))
n10 := n9.Get(NewKeyFromKey(termbox.KeyCtrlE))
checkNode(t, n10, 0, validData(KeyList{NewKeyFromKey(termbox.KeyCtrlA), NewKeyFromKey(termbox.KeyCtrlB), NewKeyFromKey(termbox.KeyCtrlC), NewKeyFromKey(termbox.KeyCtrlD), NewKeyFromKey(termbox.KeyCtrlE)}, 10, r))
}
func assertMatches(t *testing.T, exp, act []Match) {
if len(act) != len(exp) {
t.Errorf("[]Match length is not %d (%d)", len(exp), len(act))
t.Logf(" expected: %v", exp)
t.Logf(" actually: %v", act)
}
for i, e := range exp {
dump := false
a := act[i]
if a.Index != e.Index {
t.Errorf("Index not matched at #%d\n", i)
dump = true
}
if !a.Pattern.Equals(e.Pattern) {
t.Errorf("Pattern not matched at #%d\n", i)
dump = true
}
if a.Value != e.Value {
t.Errorf("Value not matched at #%d\n", i)
dump = true
}
if dump {
t.Logf(" expected: %+v", e)
t.Logf(" actually: %+v", a)
}
}
}

View file

@ -1,146 +0,0 @@
package keyseq
import (
"fmt"
"strings"
"unicode/utf8"
"github.com/nsf/termbox-go"
)
// This map is populated using some magic numbers, which must match
// the values defined in termbox-go. Verification against the actual
// termbox constants are done in the test
var stringToKey = map[string]termbox.Key{}
var keyToString = map[termbox.Key]string{}
func mapkey(n string, k termbox.Key) {
stringToKey[n] = k
keyToString[k] = n
}
func init() {
fidx := 12
for k := termbox.KeyF1; k > termbox.KeyF12; k-- {
sk := fmt.Sprintf("F%d", fidx)
mapkey(sk, k)
fidx--
}
names := []string{
"Insert",
"Delete",
"Home",
"End",
"Pgup",
"Pgdn",
"ArrowUp",
"ArrowDown",
"ArrowLeft",
"ArrowRight",
}
for i, n := range names {
mapkey(n, termbox.Key(int(termbox.KeyF12)-(i+1)))
}
names = []string{
"Left",
"Middle",
"Right",
}
for i, n := range names {
sk := fmt.Sprintf("Mouse%s", n)
mapkey(sk, termbox.Key(int(termbox.KeyArrowRight)-(i+2)))
}
whacky := [][]string{
{"~", "2", "Space"},
{"a"},
{"b"},
{"c"},
{"d"},
{"e"},
{"f"},
{"g"},
{"h"},
{"i"},
{"j"},
{"k"},
{"l"},
{"m"},
{"n"},
{"o"},
{"p"},
{"q"},
{"r"},
{"s"},
{"t"},
{"u"},
{"v"},
{"w"},
{"x"},
{"y"},
{"z"},
{"[", "3"},
{"4", "\\"},
{"5", "]"},
{"6"},
{"7", "/", "_"},
}
for i, list := range whacky {
for _, n := range list {
sk := fmt.Sprintf("C-%s", n)
mapkey(sk, termbox.Key(int(termbox.KeyCtrlTilde)+i))
}
}
mapkey("BS", termbox.KeyBackspace)
mapkey("Tab", termbox.KeyTab)
mapkey("Enter", termbox.KeyEnter)
mapkey("Esc", termbox.KeyEsc)
mapkey("Space", termbox.KeySpace)
mapkey("BS2", termbox.KeyBackspace2)
mapkey("C-8", termbox.KeyCtrl8)
// panic(fmt.Sprintf("%#q", stringToKey))
}
func ToKeyList(ksk string) (KeyList, error) {
list := KeyList{}
for _, term := range strings.Split(ksk, ",") {
term = strings.TrimSpace(term)
k, m, ch, err := ToKey(term)
if err != nil {
return list, fmt.Errorf("Failed to convert '%s': %s", term, err)
}
list = append(list, Key{m, k, ch})
}
return list, nil
}
func ToKey(key string) (k termbox.Key, modifier ModifierKey, ch rune, err error) {
modifier = ModNone
if strings.HasPrefix(key, "M-") {
modifier = ModAlt
key = key[2:]
if len(key) == 1 {
ch = rune(key[0])
return
}
}
var ok bool
k, ok = stringToKey[key]
if !ok {
// If this is a single rune, just allow it
ch, _ = utf8.DecodeRuneInString(key)
if ch != utf8.RuneError {
return
}
err = fmt.Errorf("No such key %s", key)
}
return
}

View file

@ -1,69 +0,0 @@
package keyseq
import (
"testing"
"github.com/nsf/termbox-go"
)
func TestKeymapStrToKeyValue(t *testing.T) {
expected := map[string]termbox.Key{
"Insert": termbox.KeyInsert,
"MouseLeft": termbox.MouseLeft,
"C-k": termbox.KeyCtrlK,
"C-h": termbox.KeyCtrlH,
"C-i": termbox.KeyCtrlI,
"C-l": termbox.KeyCtrlL,
"C-m": termbox.KeyCtrlM,
"C-[": termbox.KeyCtrlLsqBracket,
"C-\\": termbox.KeyCtrlBackslash,
"C-_": termbox.KeyCtrlUnderscore,
"C-8": termbox.KeyCtrl8,
}
t.Logf("Checking key name -> actual key value mapping...")
for n, v := range expected {
t.Logf(" checking %s...", n)
// TODO ch isn't being checked
e, modifier, _, err := ToKey(n)
if err != nil {
t.Errorf("Key name %s not found", n)
}
if e != v {
t.Errorf("Expected '%s' to be '%d', but got '%d'", n, v, stringToKey[n])
}
if modifier != 0 {
t.Errorf("Key name '%s' is not Alt-prefixed", n)
}
}
}
func TestKeymapStrToKeyValueWithAlt(t *testing.T) {
expected := map[string]struct {
key termbox.Key
ch rune
}{
"M-v": {0, 'v'},
"M-C-v": {termbox.KeyCtrlV, rune(0)},
"M-Space": {termbox.KeySpace, rune(0)},
"M-MouseLeft": {termbox.MouseLeft, rune(0)},
}
t.Logf("Checking Alt prefixed key name mapping...")
for n, v := range expected {
t.Logf(" checking %s...", n)
k, modifier, ch, err := ToKey(n)
if err != nil {
t.Errorf("Failed ToKey: Key name %s", n)
}
if modifier != 1 {
t.Errorf("Key name %s has Alt prefix", n)
}
if k != v.key {
t.Errorf("Expected '%s' to be '%d', but got '%d'", n, v.key, k)
}
if ch != v.ch {
t.Errorf("Expected '%s' to be '%c', but got '%c'", n, v.ch, ch)
}
}
}

View file

@ -1,70 +0,0 @@
package keyseq
import "testing"
func assertNilBoth(t *testing.T, n *TernaryNode) {
if n.low != nil {
t.Errorf("low node has value", &n.low)
}
if n.high != nil {
t.Errorf("high node has value", &n.high)
}
}
func TestBalance(t *testing.T) {
trie := NewTernaryTrie()
list := []Key{}
for i := 0; i < 15; i++ {
list = append(list, Key{0, 0, rune(i)})
}
for i, k := range list {
trie.Put(KeyList{k}, i)
}
if s := trie.Size(); s != 15 {
t.Fatalf("Size() returns not 15: %d", s)
}
trie.Balance()
/*
n8 := trie.Root().(*TernaryNode).firstChild
checkTrieNode(t, n8, '8', 7)
n4 := n8.low
checkTrieNode(t, n4, '4', 3)
n12 := n8.high
checkTrieNode(t, n12, 'C', 11)
n2 := n4.low
checkTrieNode(t, n2, '2', 1)
n6 := n4.high
checkTrieNode(t, n6, '6', 5)
n10 := n12.low
checkTrieNode(t, n10, 'A', 9)
n14 := n12.high
checkTrieNode(t, n14, 'E', 13)
n1 := n2.low
checkTrieNode(t, n1, '1', 0)
n3 := n2.high
checkTrieNode(t, n3, '3', 2)
n5 := n6.low
checkTrieNode(t, n5, '5', 4)
n7 := n6.high
checkTrieNode(t, n7, '7', 6)
n9 := n10.low
checkTrieNode(t, n9, '9', 8)
n11 := n10.high
checkTrieNode(t, n11, 'B', 10)
n13 := n14.low
checkTrieNode(t, n13, 'D', 12)
n15 := n14.high
checkTrieNode(t, n15, 'F', 14)
assertNilBoth(t, n1)
assertNilBoth(t, n3)
assertNilBoth(t, n5)
assertNilBoth(t, n7)
assertNilBoth(t, n9)
assertNilBoth(t, n11)
assertNilBoth(t, n13)
assertNilBoth(t, n15)
*/
}

View file

@ -1,40 +0,0 @@
package keyseq
import (
"testing"
)
func checkTrieNode(t *testing.T, n Node, k Key, value int) {
if n == nil {
t.Fatal("TrieNode is null")
}
if l := n.Label(); l != k {
t.Errorf("TrieNode.Label() expected:'%c' actual:'%c'", k, l)
}
if v := n.Value().(int); v != value {
t.Errorf("TrieNode.Value() expected:%d actual:%d", value, v)
}
}
func TestTrie(t *testing.T) {
trie := NewTrie()
for i := 1; i <= 5; i++ {
trie.Put(KeyList{Key{0, 0, rune(i)}}, 111 * i)
}
nodes := Children(trie.Root())
for i := 0; i < 5; i++ {
checkTrieNode(t, nodes[i], Key{0, 0, rune(i+1)}, 111 * (i + 1))
}
if s := trie.Size(); s != 5 {
t.Errorf("trie.Size() returns not 5: %d", s)
}
}
func TestNotFound(t *testing.T) {
trie := NewTrie()
if trie.Get(Key{999,999,'a'}) != nil {
t.Errorf("found 'not_exist' in empty trie")
}
}

Some files were not shown because too many files have changed in this diff Show more