Merge pull request #681 from peco/fix-screen-init-resume-error

Propagate screen Init error on resume
This commit is contained in:
lestrrat 2026-02-18 09:35:10 +09:00 committed by GitHub
commit c4e02ac1ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 60 additions and 24 deletions

View file

@ -390,7 +390,10 @@ func doFinish(ctx context.Context, state *Peco, _ Event) {
state.screen.Suspend() state.screen.Suspend()
err = cmd.Run() err = cmd.Run()
state.screen.Resume(ctx) if err := state.screen.Resume(ctx); err != nil {
state.Exit(fmt.Errorf("failed to resume screen: %w", err))
return
}
state.Hub().SendDraw(ctx, &hub.DrawOptions{DisableCache: true}) state.Hub().SendDraw(ctx, &hub.DrawOptions{DisableCache: true})
if err != nil { if err != nil {
// bail out, or otherwise the user cannot know what happened // bail out, or otherwise the user cannot know what happened

View file

@ -156,7 +156,7 @@ type Screen interface {
Flush() error Flush() error
PollEvent(context.Context, *Config) chan Event PollEvent(context.Context, *Config) chan Event
Print(PrintArgs) int Print(PrintArgs) int
Resume(context.Context) Resume(context.Context) error
SetCell(int, int, rune, Attribute, Attribute) SetCell(int, int, rune, Attribute, Attribute)
SetCursor(int, int) SetCursor(int, int)
Size() (int, int) Size() (int, int)

View file

@ -261,8 +261,8 @@ func (s *SimScreen) Size() (int, int) {
return s.screen.Size() return s.screen.Size()
} }
func (s *SimScreen) Resume(_ context.Context) {} func (s *SimScreen) Resume(_ context.Context) error { return nil }
func (s *SimScreen) Suspend() {} func (s *SimScreen) Suspend() {}
// Sync records a "Sync" event via the interceptor. This satisfies the // Sync records a "Sync" event via the interceptor. This satisfies the
// optional syncer interface used by BasicLayout.DrawScreen when // optional syncer interface used by BasicLayout.DrawScreen when

View file

@ -20,7 +20,7 @@ import (
type TcellScreen struct { type TcellScreen struct {
mutex sync.Mutex mutex sync.Mutex
screen tcell.Screen screen tcell.Screen
resumeCh chan chan struct{} resumeCh chan chan error
suspendCh chan struct{} suspendCh chan struct{}
doneCh chan struct{} // closed on permanent Close() to signal goroutines to exit doneCh chan struct{} // closed on permanent Close() to signal goroutines to exit
closeOnce sync.Once // ensures doneCh is closed exactly once closeOnce sync.Once // ensures doneCh is closed exactly once
@ -179,7 +179,7 @@ func (t *TcellScreen) Init(_ *Config) error {
func NewTcellScreen() *TcellScreen { func NewTcellScreen() *TcellScreen {
return &TcellScreen{ return &TcellScreen{
suspendCh: make(chan struct{}), suspendCh: make(chan struct{}),
resumeCh: make(chan chan struct{}), resumeCh: make(chan chan error),
doneCh: make(chan struct{}), doneCh: make(chan struct{}),
errWriter: os.Stderr, errWriter: os.Stderr,
} }
@ -293,8 +293,12 @@ func (t *TcellScreen) PollEvent(ctx context.Context, cfg *Config) chan Event {
case <-t.doneCh: case <-t.doneCh:
return return
case replyCh := <-t.resumeCh: case replyCh := <-t.resumeCh:
_ = t.Init(cfg) if err := t.Init(cfg); err != nil {
close(replyCh) fmt.Fprintf(t.errWriter, "peco: failed to re-initialize screen on resume: %v\n", err)
replyCh <- err
} else {
replyCh <- nil
}
continue continue
} }
} }
@ -309,8 +313,12 @@ func (t *TcellScreen) PollEvent(ctx context.Context, cfg *Config) chan Event {
case <-t.doneCh: case <-t.doneCh:
return return
case replyCh := <-t.resumeCh: case replyCh := <-t.resumeCh:
_ = t.Init(cfg) if err := t.Init(cfg); err != nil {
close(replyCh) fmt.Fprintf(t.errWriter, "peco: failed to re-initialize screen on resume: %v\n", err)
replyCh <- err
} else {
replyCh <- nil
}
} }
continue continue
} }
@ -328,7 +336,7 @@ func (t *TcellScreen) Suspend() {
} }
} }
func (t *TcellScreen) Resume(ctx context.Context) { func (t *TcellScreen) Resume(ctx context.Context) error {
// Resume must be a block operation, because we can't safely proceed // Resume must be a block operation, because we can't safely proceed
// without actually knowing that the screen has been re-initialized. // without actually knowing that the screen has been re-initialized.
// So we send a channel where we expect a reply back, and wait for that. // So we send a channel where we expect a reply back, and wait for that.
@ -337,16 +345,18 @@ func (t *TcellScreen) Resume(ctx context.Context) {
// polling goroutine is not yet waiting on resumeCh, a non-blocking // polling goroutine is not yet waiting on resumeCh, a non-blocking
// send would silently drop the message and the subsequent receive // send would silently drop the message and the subsequent receive
// would block forever. // would block forever.
ch := make(chan struct{}) ch := make(chan error, 1)
select { select {
case t.resumeCh <- ch: case t.resumeCh <- ch:
case <-ctx.Done(): case <-ctx.Done():
return return ctx.Err()
} }
select { select {
case <-ch: case err := <-ch:
return err
case <-ctx.Done(): case <-ctx.Done():
return ctx.Err()
} }
} }

View file

@ -225,4 +225,4 @@ func (s *InlineScreen) SendEvent(_ Event) {}
func (s *InlineScreen) Suspend() {} func (s *InlineScreen) Suspend() {}
// Resume is a no-op for inline mode. // Resume is a no-op for inline mode.
func (s *InlineScreen) Resume(_ context.Context) {} func (s *InlineScreen) Resume(_ context.Context) error { return nil }

View file

@ -3,6 +3,7 @@ package peco
import ( import (
"bytes" "bytes"
"context" "context"
"errors"
"testing" "testing"
"time" "time"
@ -28,7 +29,7 @@ func (s *recordingScreen) Close() error { retur
func (s *recordingScreen) Flush() error { return nil } func (s *recordingScreen) Flush() error { return nil }
func (s *recordingScreen) PollEvent(context.Context, *Config) chan Event { return nil } func (s *recordingScreen) PollEvent(context.Context, *Config) chan Event { return nil }
func (s *recordingScreen) Print(args PrintArgs) int { return screenPrint(s, args) } func (s *recordingScreen) Print(args PrintArgs) int { return screenPrint(s, args) }
func (s *recordingScreen) Resume(context.Context) {} func (s *recordingScreen) Resume(context.Context) error { return nil }
func (s *recordingScreen) SetCursor(int, int) {} func (s *recordingScreen) SetCursor(int, int) {}
func (s *recordingScreen) SendEvent(Event) {} func (s *recordingScreen) SendEvent(Event) {}
func (s *recordingScreen) Suspend() {} func (s *recordingScreen) Suspend() {}
@ -240,7 +241,7 @@ func TestTcellScreenPollingGoroutineExitsOnClose(t *testing.T) {
case <-tb.doneCh: case <-tb.doneCh:
return return
case replyCh := <-tb.resumeCh: case replyCh := <-tb.resumeCh:
close(replyCh) replyCh <- nil
} }
}() }()
@ -313,16 +314,16 @@ func TestTcellScreenResumeNoDeadlock(t *testing.T) {
defer cancel() defer cancel()
// Simulate the polling goroutine: receive from resumeCh after a short delay, // Simulate the polling goroutine: receive from resumeCh after a short delay,
// then close the reply channel (as PollEvent does after re-init). // then send nil error (as PollEvent does after successful re-init).
go func() { go func() {
time.Sleep(50 * time.Millisecond) time.Sleep(50 * time.Millisecond)
replyCh := <-tb.resumeCh replyCh := <-tb.resumeCh
close(replyCh) replyCh <- nil
}() }()
done := make(chan struct{}) done := make(chan struct{})
go func() { go func() {
tb.Resume(ctx) require.NoError(t, tb.Resume(ctx))
close(done) close(done)
}() }()
@ -344,10 +345,10 @@ func TestTcellScreenResumeDoesNotDropSend(t *testing.T) {
go func() { go func() {
replyCh := <-tb.resumeCh replyCh := <-tb.resumeCh
close(received) close(received)
close(replyCh) replyCh <- nil
}() }()
tb.Resume(ctx) require.NoError(t, tb.Resume(ctx))
select { select {
case <-received: case <-received:
@ -367,7 +368,8 @@ func TestTcellScreenResumeContextCancelled(t *testing.T) {
done := make(chan struct{}) done := make(chan struct{})
go func() { go func() {
tb.Resume(ctx) err := tb.Resume(ctx)
require.Error(t, err)
close(done) close(done)
}() }()
@ -393,7 +395,8 @@ func TestTcellScreenResumeContextCancelledWhileWaitingForReply(t *testing.T) {
done := make(chan struct{}) done := make(chan struct{})
go func() { go func() {
tb.Resume(ctx) err := tb.Resume(ctx)
require.Error(t, err)
close(done) close(done)
}() }()
@ -411,3 +414,23 @@ func TestTcellScreenResumeContextCancelledWhileWaitingForReply(t *testing.T) {
// Verify context was indeed cancelled. // Verify context was indeed cancelled.
require.Error(t, ctx.Err()) require.Error(t, ctx.Err())
} }
func TestTcellScreenResumeInitError(t *testing.T) {
tb := NewTcellScreen()
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
initErr := errors.New("simulated screen init failure")
// Simulate the polling goroutine: receive from resumeCh and send
// an error as if Init() failed.
go func() {
replyCh := <-tb.resumeCh
replyCh <- initErr
}()
err := tb.Resume(ctx)
require.Error(t, err)
require.Equal(t, initErr, err)
}