@@ -471,23 +471,11 @@ def _append_line(text: str, color: str | None = None) -> None:
471471 def _worker () -> None :
472472 rc = - 1
473473 try :
474- import pty as _pty , io as _io
475- env = {** os .environ , "PYTHONUNBUFFERED" : "1" ,
476- "TERM" : "xterm-256color" , "COLUMNS" : "100" }
477- master_fd , slave_fd = _pty .openpty ()
478- state .proc = subprocess .Popen (
479- cmd ,
480- stdout = slave_fd , stderr = slave_fd ,
481- close_fds = True ,
482- cwd = str (_get_output_dir ()),
483- env = env ,
484- )
485- os .close (slave_fd )
486-
487- _MAX_LINES = 500
488- _ANSI_RE = re .compile (
474+ import io as _io
475+ _ANSI_RE = re .compile (
489476 r"\x1b\[[0-9;]*[mABCDEFGHJKST]|\x1b\][^\x07]*\x07"
490477 )
478+ _MAX_LINES = 500
491479 _last_update = time .monotonic ()
492480 buf = "" # current line being assembled
493481
@@ -511,47 +499,91 @@ def _new_line() -> None:
511499 if excess > 0 :
512500 del self ._lines .controls [:excess ]
513501
514- try :
515- with _io .open (master_fd , "rb" , closefd = True ) as master :
516- while True :
517- try :
518- chunk = master .read (4096 )
519- except OSError :
520- break
521- if not chunk :
522- break
523- text = _ANSI_RE .sub (
524- "" , chunk .decode ("utf-8" , errors = "replace" )
525- )
526- for ch in text :
527- if ch == "\r " :
528- # Carriage return: next chars overwrite
529- # the current line — discard old buffer.
530- buf = ""
531- elif ch == "\n " :
532- if not buf .startswith ("<frozen importlib" ):
533- item = _cur ()
534- item .value = buf
535- low = buf .lower ().lstrip ()
536- if low .startswith (
537- ("error" , "traceback" ,
538- "exception" )):
539- item .color = C_ERROR
540- elif low .startswith ("warning" ):
541- item .color = C_WARN
542- _new_line ()
543- buf = ""
544- else :
545- buf += ch
546-
547- now = time .monotonic ()
548- if now - _last_update >= _UPDATE_INTERVAL :
549- if buf :
550- _cur ().value = buf
551- self .page .update ()
552- _last_update = now
553- except OSError :
554- pass
502+ def _process_chunk (raw : bytes ) -> None :
503+ nonlocal buf
504+ text = _ANSI_RE .sub (
505+ "" , raw .decode ("utf-8" , errors = "replace" )
506+ )
507+ for ch in text :
508+ if ch == "\r " :
509+ buf = ""
510+ elif ch == "\n " :
511+ if not buf .startswith ("<frozen importlib" ):
512+ item = _cur ()
513+ item .value = buf
514+ low = buf .lower ().lstrip ()
515+ if low .startswith (
516+ ("error" , "traceback" , "exception" )):
517+ item .color = C_ERROR
518+ elif low .startswith ("warning" ):
519+ item .color = C_WARN
520+ _new_line ()
521+ buf = ""
522+ else :
523+ buf += ch
524+
525+ # ── Launch subprocess ─────────────────────────────────────
526+ # On Unix use a pty so tqdm/rich see a real terminal and use
527+ # \r for in-place refresh. On Windows pty is unavailable so
528+ # fall back to a plain pipe (progress bars produce extra lines
529+ # but work correctly).
530+ env = {** os .environ , "PYTHONUNBUFFERED" : "1" }
531+ _use_pty = False
532+ if sys .platform != "win32" :
533+ try :
534+ import pty as _pty
535+ master_fd , slave_fd = _pty .openpty ()
536+ env .update ({"TERM" : "xterm-256color" , "COLUMNS" : "100" })
537+ state .proc = subprocess .Popen (
538+ cmd ,
539+ stdout = slave_fd , stderr = slave_fd ,
540+ close_fds = True ,
541+ cwd = str (_get_output_dir ()),
542+ env = env ,
543+ )
544+ os .close (slave_fd )
545+ _use_pty = True
546+ except Exception :
547+ _use_pty = False
548+
549+ if not _use_pty :
550+ state .proc = subprocess .Popen (
551+ cmd ,
552+ stdout = subprocess .PIPE ,
553+ stderr = subprocess .STDOUT ,
554+ cwd = str (_get_output_dir ()),
555+ env = env ,
556+ )
557+
558+ # ── Stream output ─────────────────────────────────────────
559+ if _use_pty :
560+ try :
561+ with _io .open (master_fd , "rb" , closefd = True ) as master :
562+ while True :
563+ try :
564+ chunk = master .read (4096 )
565+ except OSError :
566+ break
567+ if not chunk :
568+ break
569+ _process_chunk (chunk )
570+ now = time .monotonic ()
571+ if now - _last_update >= _UPDATE_INTERVAL :
572+ if buf :
573+ _cur ().value = buf
574+ self .page .update ()
575+ _last_update = now
576+ except OSError :
577+ pass
578+ else :
579+ for line in state .proc .stdout :
580+ _process_chunk (line .encode ())
581+ now = time .monotonic ()
582+ if now - _last_update >= _UPDATE_INTERVAL :
583+ if buf :
584+ _cur ().value = buf
585+ self .page .update ()
586+ _last_update = now
555587
556588 if buf :
557589 _cur ().value = buf
0 commit comments