Replace Termbox (name references) with Tcell

This commit is contained in:
Daisuke Maki 2026-02-16 23:50:19 +09:00
parent c2c812db24
commit a1d7596b1a
11 changed files with 72 additions and 78 deletions

View file

@ -806,34 +806,34 @@ For now, styles of following 7 items can be customized in `config.json`.
### Foreground Colors
- `"black"` for `termbox.ColorBlack`
- `"red"` for `termbox.ColorRed`
- `"green"` for `termbox.ColorGreen`
- `"yellow"` for `termbox.ColorYellow`
- `"blue"` for `termbox.ColorBlue`
- `"magenta"` for `termbox.ColorMagenta`
- `"cyan"` for `termbox.ColorCyan`
- `"white"` for `termbox.ColorWhite`
- `"black"` for `tcell.ColorBlack`
- `"red"` for `tcell.ColorRed`
- `"green"` for `tcell.ColorGreen`
- `"yellow"` for `tcell.ColorYellow`
- `"blue"` for `tcell.ColorBlue`
- `"magenta"` for `tcell.ColorMagenta`
- `"cyan"` for `tcell.ColorCyan`
- `"white"` for `tcell.ColorWhite`
- `"0"`-`"255"` for 256color ([Use256Color](#use256color) must be enabled)
### Background Colors
- `"on_black"` for `termbox.ColorBlack`
- `"on_red"` for `termbox.ColorRed`
- `"on_green"` for `termbox.ColorGreen`
- `"on_yellow"` for `termbox.ColorYellow`
- `"on_blue"` for `termbox.ColorBlue`
- `"on_magenta"` for `termbox.ColorMagenta`
- `"on_cyan"` for `termbox.ColorCyan`
- `"on_white"` for `termbox.ColorWhite`
- `"on_black"` for `tcell.ColorBlack`
- `"on_red"` for `tcell.ColorRed`
- `"on_green"` for `tcell.ColorGreen`
- `"on_yellow"` for `tcell.ColorYellow`
- `"on_blue"` for `tcell.ColorBlue`
- `"on_magenta"` for `tcell.ColorMagenta`
- `"on_cyan"` for `tcell.ColorCyan`
- `"on_white"` for `tcell.ColorWhite`
- `"on_0"`-`"on_255"` for 256color ([Use256Color](#use256color) must be enabled)
### Attributes
- `"bold"` for fg: `termbox.AttrBold`
- `"underline"` for fg: `termbox.AttrUnderline`
- `"reverse"` for fg: `termbox.AttrReverse`
- `"on_bold"` for bg: `termbox.AttrBold` (this attribute actually makes the background blink on some platforms/environments, e.g. linux console, xterm...)
- `"bold"` for fg: `tcell.AttrBold`
- `"underline"` for fg: `tcell.AttrUnderline`
- `"reverse"` for fg: `tcell.AttrReverse`
- `"on_bold"` for bg: `tcell.AttrBold` (this attribute actually makes the background blink on some platforms/environments, e.g. linux console, xterm...)
## CustomFilter

View file

@ -14,9 +14,7 @@ const (
EventError
)
// Event is peco's internal event type, replacing termbox.Event.
// This decouples all code outside the screen adapter from the
// terminal library.
// Event is peco's internal event type, decoupled from any terminal library.
type Event struct {
Type EventType
Key keyseq.KeyType

View file

@ -49,7 +49,7 @@ func (i *Input) handleInputEvent(ctx context.Context, ev Event) error {
return nil
case EventKey:
// ModAlt is a sequence of letters with a leading \x1b (=Esc).
// It would be nice if termbox differentiated this for us, but
// The terminal library doesn't differentiate this for us, so
// we workaround it by waiting (juuuust a few milliseconds) for
// extra key events. If no extra events arrive, it should be Esc

View file

@ -6,10 +6,8 @@ import (
"unicode/utf8"
)
// KeyType represents a keyboard key. Values are defined to match termbox-go's
// constants so that the adapter layer in screen.go can do simple type casts
// during Phase 1 of the migration. In Phase 2, the adapter will map between
// peco's KeyType and tcell's key constants.
// 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
@ -39,8 +37,8 @@ const (
)
// Mouse keys.
// In termbox-go there is one internal gap constant between KeyArrowRight
// and MouseLeft, so MouseLeft = KeyArrowRight - 2 (not -1).
// 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
@ -211,8 +209,7 @@ func ToKeyList(ksk string) (KeyList, error) {
}
// KeyEventToString returns a human-readable name for a key event described
// by the given key type, character, and modifier. This replaces the old
// EventToString that took a termbox.Event directly.
// by the given key type, character, and modifier.
func KeyEventToString(key KeyType, ch rune, mod ModifierKey) (string, error) {
s := ""
if key == 0 {

View file

@ -145,7 +145,7 @@ func (u UserPrompt) Draw(state *Peco) {
fg := u.styles.Query.fg
bg := u.styles.Query.bg
// Used to notify termbox where our cursor is
// Used to notify the screen where our cursor is
var posX int
switch ql {

View file

@ -118,7 +118,7 @@ func New() *Peco {
idgen: newIDGen(),
queryExecDelay: 50 * time.Millisecond,
readyCh: make(chan struct{}),
screen: NewTermbox(),
screen: NewTcellScreen(),
selection: NewSelection(),
maxScanBufferSize: bufio.MaxScanTokenSize,
}
@ -425,7 +425,7 @@ func (p *Peco) Run(ctx context.Context) (err error) {
}
p.source = src
// If --height is specified, use InlineScreen instead of the default Termbox screen
// If --height is specified, use InlineScreen instead of the default TcellScreen
if p.heightSpec != nil {
p.screen = NewInlineScreen(*p.heightSpec)
}

View file

@ -13,9 +13,8 @@ import (
"github.com/peco/peco/internal/keyseq"
)
// Termbox implements the Screen interface using tcell/v2.
// The name is kept for compatibility with the rest of the codebase.
type Termbox struct {
// TcellScreen implements the Screen interface using tcell/v2.
type TcellScreen struct {
mutex sync.Mutex
screen tcell.Screen
resumeCh chan chan struct{}
@ -76,7 +75,7 @@ func tcellEventToEvent(tev tcell.Event) Event {
if key == tcell.KeyRune {
r := ev.Rune()
// Special case: space must be sent as KeySpace with Ch=0
// to match termbox behavior expected by doAcceptChar
// to match the convention expected by doAcceptChar
if r == ' ' {
return Event{
Type: EventKey,
@ -159,7 +158,7 @@ func attributeToTcellStyle(fg, bg Attribute) tcell.Style {
return style
}
func (t *Termbox) Init(cfg *Config) error {
func (t *TcellScreen) Init(cfg *Config) error {
screen, err := tcell.NewScreen()
if err != nil {
return fmt.Errorf("failed to create tcell screen: %w", err)
@ -173,8 +172,8 @@ func (t *Termbox) Init(cfg *Config) error {
return t.PostInit(cfg)
}
func NewTermbox() *Termbox {
return &Termbox{
func NewTcellScreen() *TcellScreen {
return &TcellScreen{
suspendCh: make(chan struct{}),
resumeCh: make(chan chan struct{}),
doneCh: make(chan struct{}),
@ -184,7 +183,7 @@ func NewTermbox() *Termbox {
// finiScreen finalizes the tcell screen without signaling a permanent
// shutdown. Used by the suspend handler so the goroutine continues
// to listen for further suspend/resume cycles.
func (t *Termbox) finiScreen() {
func (t *TcellScreen) finiScreen() {
t.mutex.Lock()
s := t.screen
t.screen = nil
@ -197,16 +196,16 @@ func (t *Termbox) finiScreen() {
// Close permanently shuts down the screen and signals all goroutines
// started by PollEvent to exit.
func (t *Termbox) Close() error {
func (t *TcellScreen) Close() error {
if pdebug.Enabled {
pdebug.Printf("Termbox: Close")
pdebug.Printf("TcellScreen: Close")
}
t.finiScreen()
t.closeOnce.Do(func() { close(t.doneCh) })
return nil
}
func (t *Termbox) SetCursor(x, y int) {
func (t *TcellScreen) SetCursor(x, y int) {
t.mutex.Lock()
defer t.mutex.Unlock()
if t.screen == nil {
@ -218,12 +217,12 @@ func (t *Termbox) SetCursor(x, y int) {
// SendEvent is used to allow programmers generate random
// events, but it's only useful for testing purposes.
// When interacting with tcell, this method is a noop
func (t *Termbox) SendEvent(_ Event) {
func (t *TcellScreen) SendEvent(_ Event) {
// no op
}
// Flush calls tcell's Show to synchronize the screen
func (t *Termbox) Flush() error {
func (t *TcellScreen) Flush() error {
t.mutex.Lock()
defer t.mutex.Unlock()
if t.screen == nil {
@ -236,7 +235,7 @@ func (t *Termbox) Flush() error {
// Sync forces a complete redraw of every cell on the physical display.
// This recovers from screen corruption caused by external output (e.g.,
// STDERR messages written directly to the terminal).
func (t *Termbox) Sync() {
func (t *TcellScreen) Sync() {
t.mutex.Lock()
defer t.mutex.Unlock()
if t.screen == nil {
@ -248,7 +247,7 @@ func (t *Termbox) Sync() {
// PollEvent returns a channel that you can listen to for
// terminal events. The actual polling is done in a
// separate goroutine
func (t *Termbox) PollEvent(ctx context.Context, cfg *Config) chan Event {
func (t *TcellScreen) PollEvent(ctx context.Context, cfg *Config) chan Event {
evCh := make(chan Event)
go func() {
@ -313,14 +312,14 @@ func (t *Termbox) PollEvent(ctx context.Context, cfg *Config) chan Event {
return evCh
}
func (t *Termbox) Suspend() {
func (t *TcellScreen) Suspend() {
select {
case t.suspendCh <- struct{}{}:
default:
}
}
func (t *Termbox) Resume(ctx context.Context) {
func (t *TcellScreen) Resume(ctx context.Context) {
// Resume must be a block operation, because we can't safely proceed
// without actually knowing that the screen has been re-initialized.
// So we send a channel where we expect a reply back, and wait for that.
@ -343,7 +342,7 @@ func (t *Termbox) Resume(ctx context.Context) {
}
// SetCell writes to the terminal
func (t *Termbox) SetCell(x, y int, ch rune, fg, bg Attribute) {
func (t *TcellScreen) SetCell(x, y int, ch rune, fg, bg Attribute) {
t.mutex.Lock()
defer t.mutex.Unlock()
if t.screen == nil {
@ -354,7 +353,7 @@ func (t *Termbox) SetCell(x, y int, ch rune, fg, bg Attribute) {
}
// Size returns the dimensions of the current terminal
func (t *Termbox) Size() (int, int) {
func (t *TcellScreen) Size() (int, int) {
t.mutex.Lock()
defer t.mutex.Unlock()
if t.screen == nil {
@ -374,7 +373,7 @@ type PrintArgs struct {
ANSIAttrs []ansi.AttrSpan // per-character ANSI attributes for this segment
}
func (t *Termbox) Print(args PrintArgs) int {
func (t *TcellScreen) Print(args PrintArgs) int {
return screenPrint(t, args)
}

View file

@ -209,7 +209,7 @@ func (s *InlineScreen) PollEvent(ctx context.Context, cfg *Config) chan Event {
return evCh
}
// SendEvent is a no-op for InlineScreen (same as Termbox).
// SendEvent is a no-op for InlineScreen (same as TcellScreen).
func (s *InlineScreen) SendEvent(_ Event) {}
// Suspend is a no-op for inline mode.

View file

@ -5,6 +5,6 @@ package peco
// PostInit is a no-op on POSIX systems. tcell auto-detects
// color capability via terminfo.
func (t *Termbox) PostInit(cfg *Config) error {
func (t *TcellScreen) PostInit(cfg *Config) error {
return nil
}

View file

@ -8,12 +8,12 @@ import (
"github.com/stretchr/testify/require"
)
// TestTermboxSuspendHandlerExitsOnClose verifies that the suspend handler
// TestTcellScreenSuspendHandlerExitsOnClose verifies that the suspend handler
// goroutine (started by PollEvent) exits when Close() is called, even if
// the context has not been cancelled. This is the goroutine leak described
// in CODE_REVIEW.md §7.1.
func TestTermboxSuspendHandlerExitsOnClose(t *testing.T) {
tb := NewTermbox()
func TestTcellScreenSuspendHandlerExitsOnClose(t *testing.T) {
tb := NewTcellScreen()
// Use a context that will NOT be cancelled during this test.
// The goroutine must exit via doneCh, not ctx.Done().
@ -46,11 +46,11 @@ func TestTermboxSuspendHandlerExitsOnClose(t *testing.T) {
}
}
// TestTermboxPollingGoroutineExitsOnClose verifies that a goroutine blocked
// TestTcellScreenPollingGoroutineExitsOnClose verifies that a goroutine blocked
// on resumeCh (as the polling goroutine would be after screen finalization)
// exits when Close() is called.
func TestTermboxPollingGoroutineExitsOnClose(t *testing.T) {
tb := NewTermbox()
func TestTcellScreenPollingGoroutineExitsOnClose(t *testing.T) {
tb := NewTcellScreen()
ctx := context.Background()
@ -78,11 +78,11 @@ func TestTermboxPollingGoroutineExitsOnClose(t *testing.T) {
}
}
// TestTermboxCloseIdempotent verifies that Close() can be called multiple
// TestTcellScreenCloseIdempotent verifies that Close() can be called multiple
// times without panicking (important because the suspend handler calls
// finiScreen and then Close() is called at shutdown).
func TestTermboxCloseIdempotent(t *testing.T) {
tb := NewTermbox()
func TestTcellScreenCloseIdempotent(t *testing.T) {
tb := NewTcellScreen()
require.NotPanics(t, func() {
tb.Close()
@ -91,11 +91,11 @@ func TestTermboxCloseIdempotent(t *testing.T) {
})
}
// TestTermboxSuspendThenClose verifies that a suspend (which calls finiScreen)
// TestTcellScreenSuspendThenClose verifies that a suspend (which calls finiScreen)
// followed by a permanent Close() works correctly — the doneCh should be
// closed by Close() even though finiScreen was already called.
func TestTermboxSuspendThenClose(t *testing.T) {
tb := NewTermbox()
func TestTcellScreenSuspendThenClose(t *testing.T) {
tb := NewTcellScreen()
ctx := context.Background()
@ -130,8 +130,8 @@ func TestTermboxSuspendThenClose(t *testing.T) {
}
}
func TestTermboxResumeNoDeadlock(t *testing.T) {
tb := NewTermbox()
func TestTcellScreenResumeNoDeadlock(t *testing.T) {
tb := NewTcellScreen()
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
@ -158,8 +158,8 @@ func TestTermboxResumeNoDeadlock(t *testing.T) {
}
}
func TestTermboxResumeDoesNotDropSend(t *testing.T) {
tb := NewTermbox()
func TestTcellScreenResumeDoesNotDropSend(t *testing.T) {
tb := NewTcellScreen()
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
@ -181,8 +181,8 @@ func TestTermboxResumeDoesNotDropSend(t *testing.T) {
}
}
func TestTermboxResumeContextCancelled(t *testing.T) {
tb := NewTermbox()
func TestTcellScreenResumeContextCancelled(t *testing.T) {
tb := NewTcellScreen()
ctx, cancel := context.WithCancel(context.Background())
@ -203,8 +203,8 @@ func TestTermboxResumeContextCancelled(t *testing.T) {
}
}
func TestTermboxResumeContextCancelledWhileWaitingForReply(t *testing.T) {
tb := NewTermbox()
func TestTcellScreenResumeContextCancelledWhileWaitingForReply(t *testing.T) {
tb := NewTcellScreen()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

View file

@ -1,6 +1,6 @@
package peco
// PostInit is a no-op on Windows. tcell handles input mode automatically.
func (t *Termbox) PostInit(cfg *Config) error {
func (t *TcellScreen) PostInit(cfg *Config) error {
return nil
}