Skip to content

fix: resolve interactive picker hang after fzf exits#16

Merged
ahmedelgabri merged 1 commit intomainfrom
fix/interactive-hang
Feb 22, 2026
Merged

fix: resolve interactive picker hang after fzf exits#16
ahmedelgabri merged 1 commit intomainfrom
fix/interactive-hang

Conversation

@ahmedelgabri
Copy link
Owner

Summary

Fixes #15 — all interactive commands (add, remove, switch, destroy) hung permanently after the fzf picker closed, and Ctrl-C was ineffective.

Root cause: two bugs in internal/picker/picker.go:

  1. Output channel deadlock — the fzf library does not close opts.Output when Run() returns. The goroutine draining selected items via for s := range outputChan blocked forever, so wg.Wait() deadlocked and picker.Run() never returned.

  2. Signal handling hijacked — fzf calls signal.Notify for SIGINT/SIGTERM but never calls signal.Stop. After fzf exits, Go's runtime still intercepts Ctrl-C and routes it to the abandoned channel, making the process unkillable.

Fix:

  • close(outputChan) after fzf.Run() returns to unblock the draining goroutine
  • signal.Reset(os.Interrupt, syscall.SIGTERM) to restore default signal handling
  • ParseOptions(false, ...) to avoid inheriting $FZF_DEFAULT_OPTS which could cause unexpected behavior in embedded use

Test plan

  • All 109 BATS E2E tests pass
  • All Go unit tests pass
  • go vet clean
  • Nix build passes (pre-push hook)
  • Manual test: git wt add interactive mode — picker appears, selection works, Ctrl-C exits cleanly
  • Manual test: git wt remove interactive mode — picker appears, selection works, Ctrl-C exits cleanly

Two bugs caused all interactive commands (add, remove, switch, destroy)
to hang permanently after the fzf picker closed:

1. The fzf library does not close the Output channel when Run() returns,
   so the goroutine draining selected items via `range outputChan` blocked
   forever, deadlocking picker.Run() at wg.Wait().

2. The fzf library registers signal.Notify for SIGINT/SIGTERM inside its
   terminal loop but never calls signal.Stop. After fzf exits, Go's
   runtime continued intercepting Ctrl-C and routing it to an unread
   channel, making the process unkillable.

Additionally, switch ParseOptions from inheriting $FZF_DEFAULT_OPTS
(which could introduce conflicting options in the embedded context) to
using only the options we explicitly pass.

Closes #15
@ahmedelgabri ahmedelgabri merged commit 571050d into main Feb 22, 2026
7 checks passed
@ahmedelgabri ahmedelgabri deleted the fix/interactive-hang branch February 22, 2026 23:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Newest version hangs on git wt add interactively

1 participant