@@ -3,6 +3,8 @@ package ui
33import (
44 "context"
55 "os"
6+ "runtime"
7+ "strconv"
68
79 tea "github.com/charmbracelet/bubbletea"
810 "github.com/charmbracelet/x/term"
@@ -17,21 +19,67 @@ func Run(ctx context.Context, mode Mode, events <-chan domain.Event, meta Meta)
1719func RunWithCancel (ctx context.Context , mode Mode , events <- chan domain.Event , actions chan <- domain.Action , meta Meta , cancel func ()) error {
1820 m := NewModel (ctx , mode , events , actions , meta , cancel )
1921
20- // Bubble Tea relies on terminal size messages to render the UI. In some environments
21- // (e.g., wrapped CLIs, certain PTYs, or misconfigured terminals) the size cannot be
22- // detected, leaving the UI stuck at the "Loading…" placeholder. Seed a sensible
23- // initial size so the UI can render and progress even if WindowSizeMsg never arrives.
24- if w , h , err := term .GetSize (os .Stdout .Fd ()); err == nil && w > 0 && h > 0 {
22+ in := os .Stdin
23+ out := os .Stdout
24+ var tty * os.File
25+
26+ // If we're launched through a wrapper that doesn't preserve TTY stdio (e.g., a PHP
27+ // bootstrapper), Bubble Tea might not be able to detect the real window size. When
28+ // possible, attach directly to /dev/tty.
29+ if runtime .GOOS != "windows" && (! term .IsTerminal (in .Fd ()) || ! term .IsTerminal (out .Fd ())) {
30+ f , err := os .OpenFile ("/dev/tty" , os .O_RDWR , 0 )
31+ if err == nil {
32+ tty = f
33+ in = f
34+ out = f
35+ }
36+ }
37+ if tty != nil {
38+ defer tty .Close ()
39+ }
40+
41+ // Seed a sensible initial size so the UI can render even if WindowSizeMsg never arrives.
42+ if w , h , ok := detectTerminalSize (in , out , tty ); ok {
2543 m .width = w
2644 m .height = h
27- m .reflow ()
2845 } else {
2946 m .width = 80
3047 m .height = 24
31- m .reflow ()
3248 }
49+ m .reflow ()
3350
34- p := tea .NewProgram (m , tea .WithAltScreen ())
51+ p := tea .NewProgram (m , tea .WithAltScreen (), tea . WithInput ( in ), tea . WithOutput ( out ) )
3552 _ , err := p .Run ()
3653 return err
3754}
55+
56+ func detectTerminalSize (in * os.File , out * os.File , tty * os.File ) (w int , h int , ok bool ) {
57+ fds := []uintptr {}
58+ if out != nil {
59+ fds = append (fds , out .Fd ())
60+ }
61+ if in != nil {
62+ fds = append (fds , in .Fd ())
63+ }
64+ if tty != nil {
65+ fds = append (fds , tty .Fd ())
66+ }
67+ fds = append (fds , os .Stderr .Fd ())
68+
69+ for _ , fd := range fds {
70+ if fd == 0 || ! term .IsTerminal (fd ) {
71+ continue
72+ }
73+ if tw , th , err := term .GetSize (fd ); err == nil && tw > 0 && th > 0 {
74+ return tw , th , true
75+ }
76+ }
77+
78+ if cols , err := strconv .Atoi (os .Getenv ("COLUMNS" )); err == nil && cols > 0 {
79+ if lines , err := strconv .Atoi (os .Getenv ("LINES" )); err == nil && lines > 0 {
80+ return cols , lines , true
81+ }
82+ }
83+
84+ return 0 , 0 , false
85+ }
0 commit comments