Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion cmd/prx/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,14 @@ func main() {
return err
}

// Hand tea a duped stdout and point FD 1 at /dev/null so the
// background autoupdater (which writes directly to FD 1) can't
// clobber tea's terminal-control sequences and leave the terminal
// in alt-screen mode after exit.
teaOut := autoupdate.RedirectStdoutToDevNull()

m := tui.New(a)
p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseCellMotion())
p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseCellMotion(), tea.WithOutput(teaOut))
go func() {
p.Send(tui.SetProgramMsg{Program: p})
}()
Expand Down
31 changes: 31 additions & 0 deletions internal/autoupdate/stdout_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,34 @@ func suppressStdout() func() {
_ = syscall.Close(origStdout)
}
}

// RedirectStdoutToDevNull dups the current stdout into a new *os.File and
// points file descriptor 1 at /dev/null. The returned file is a handle to the
// real terminal for callers (e.g. Bubble Tea) that need to keep writing to it;
// anything written directly to FD 1 afterward (including by third-party
// libraries like go-selfupdate) is discarded.
//
// Callers should pass the returned file to tea.WithOutput so the TUI's
// terminal-control sequences reach the real terminal even while the
// autoupdater is running.
func RedirectStdoutToDevNull() *os.File {
origFD, err := syscall.Dup(syscall.Stdout)
if err != nil {
return os.Stdout
}

devNull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0)
if err != nil {
_ = syscall.Close(origFD)
return os.Stdout
}

if err := unix.Dup2(int(devNull.Fd()), syscall.Stdout); err != nil {
_ = devNull.Close()
_ = syscall.Close(origFD)
return os.Stdout
}
_ = devNull.Close()

return os.NewFile(uintptr(origFD), "/dev/stdout")
}
7 changes: 7 additions & 0 deletions internal/autoupdate/stdout_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@

package autoupdate

import "os"

// suppressStdout returns a no-op on Windows.
func suppressStdout() func() {
return func() {}
}

// RedirectStdoutToDevNull is a no-op on Windows and returns os.Stdout.
func RedirectStdoutToDevNull() *os.File {
return os.Stdout
}
11 changes: 11 additions & 0 deletions internal/tui/model_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ func (m *Model) tryStartupTransition() {
}
}
if !hasVisible {
// Don't transition to bulk approve until every repo's list fetches have
// returned — otherwise a fast-returning repo with no visible cards can
// trigger the bulk/fireworks scene before slower repos' cards arrive.
nRepos := len(m.app.Repos)
if m.openListsDone < nRepos || m.mergedListsDone < nRepos || m.trackedListsDone < nRepos {
return
}
if len(m.cards) > 0 {
// Cards exist but all are reviewed — show bulk approve (fireworks).
logger.Info("tryStartupTransition: %d cards but none visible, entering bulk approve", len(m.cards))
Expand Down Expand Up @@ -111,6 +118,10 @@ func (m Model) isCardVisible(card *PRCard) bool {
if card.HasNewContent {
return true // new comments since last review — keep visible
}
// Closed-without-merge PRs have nothing actionable; only surface on new activity.
if card.PR != nil && card.PR.State == "CLOSED" {
return false
}
return !card.UserHasReviewed && !card.UserHasReacted
}

Expand Down