@@ -43,6 +43,7 @@ def __init__(self, total: int, c1: RGB = DEFAULT_C1, c2: RGB = DEFAULT_C2):
4343 self .c1 , self .c2 = c1 , c2
4444 self .curr = 0
4545 self .progress = 0 # Track logical progress out of total
46+ self .start_time = time .time ()
4647 if self .t > self .w :
4748 self .g = list (interp_xyz (c1 , c2 , self .t + 1 ))
4849 else :
@@ -52,10 +53,11 @@ def __init__(self, total: int, c1: RGB = DEFAULT_C1, c2: RGB = DEFAULT_C2):
5253
5354 @staticmethod
5455 def randgrad () -> tuple [RGB , RGB ]:
55- return (
56- RGB (randint ( 0 , 255 ), randint ( 0 , 255 ), randint ( 0 , 255 )),
57- RGB ( randint ( 0 , 255 ), randint ( 0 , 255 ), randint ( 0 , 255 )),
56+ rgb1 = RGB ( randint ( 0 , 255 ), randint ( 0 , 255 ), randint ( 0 , 255 ))
57+ rgb2 = RGB (
58+ ( rgb1 . r + ( 255 // 2 )) % 255 , ( rgb1 . g + ( 255 // 2 )) % 255 , ( rgb1 . b + ( 255 // 2 )) % 255
5859 )
60+ return (rgb1 , rgb2 )
5961
6062 def _pbar (self ) -> Iterator [tuple [tuple [int , RGB ]]]:
6163 for x in range (self .w + 1 ): # TODO: i'm so dumb, why do I need a +1 here?
@@ -74,17 +76,56 @@ def __iter__(self) -> PBar:
7476 def _true_colour (rgb : RGB ) -> str :
7577 return f'\x1b [48;2;{ rgb .r } ;{ rgb .g } ;{ rgb .b } m'
7678
79+ @staticmethod
80+ def _format_time (seconds : float ) -> str :
81+ 'Format seconds into human-readable time string.'
82+
83+ if seconds < 60 :
84+ isec , fsec = divmod (seconds , 1 )
85+ return f'{ int (isec )} .{ int (fsec * 100 ):02d} '
86+ elif seconds < 3600 :
87+ isec , fsec = divmod (seconds , 60 )
88+ return f'{ int (isec )} :{ fsec :05.2f} '
89+ else :
90+ hours , rem = divmod (seconds , 3600 )
91+ mins , secs = divmod (rem , 60 )
92+ return f'{ int (hours )} :{ int (mins )} :{ secs :05.2f} '
93+
94+ def _print_info (self ) -> None :
95+ 'Print progress info in the line above the bar.'
96+
97+ elapsed = time .time () - self .start_time
98+
99+ if self .progress == 0 :
100+ eta_str = '??:??'
101+ else :
102+ eta_str = self ._format_time ((elapsed / self .progress ) * (self .t - self .progress ))
103+
104+ pct = (self .progress / self .t ) * 100
105+ item_info = f'[\x1b [1;32m{ self .progress } \x1b [0m/{ self .t } ] \x1b [1;97m{ pct :.1f} %\x1b [0m'
106+ time_info = f'\x1b [92m+{ self ._format_time (elapsed )} \x1b [0m \x1b [93m-{ eta_str } \x1b [0m'
107+
108+ # Clear the line and print info above the progress bar
109+ _print_to_terminal (
110+ '\x1b 7' # save cursor position
111+ f'\x1b [{ self .h - 1 } ;0H' # move to line above bar
112+ '\x1b [2K' # clear entire line
113+ f'{ item_info } | { time_info } '
114+ '\x1b 8' # restore cursor position
115+ )
116+
77117 def _print_bar_chars (self , s : str ) -> None :
78118 _print_to_terminal (
79119 '\x1b 7' # save cursor position
80120 f'\x1b [{ self .h } ;{ self .curr } H' # move to bottom line
81- f'{ s } ' # the " bar" characters
121+ f'{ s } ' # the ' bar' characters
82122 '\x1b [0m' # reset color
83123 '\x1b 8' # restore cursor position
84124 )
85125
86126 def _initial_bar (self ) -> None :
87- "print initial bar in end color"
127+ 'print initial bar in end color'
128+
88129 self ._print_bar_chars (
89130 f'\x1b [48;2;{ self .c2 .r } ;{ self .c2 .g } ;{ self .c2 .b } m' + ' ' * self .w + '\x1b [0m'
90131 )
@@ -98,7 +139,8 @@ def __next__(self) -> None:
98139 raise StopIteration
99140
100141 def update (self , n : int ) -> None :
101- "update the progress bar by n steps"
142+ 'update the progress bar by n steps'
143+
102144 self .progress = min (self .progress + n , self .t )
103145 # Calculate target terminal position based on logical progress
104146 target_pos = int ((self .progress / (self .t )) * self .w )
@@ -108,16 +150,21 @@ def update(self, n: int) -> None:
108150 next (self )
109151 except StopIteration :
110152 break
153+ # Update progress info
154+ self ._print_info ()
111155
112156 def __enter__ (self ) -> PBar :
157+ self .start_time = time .time ()
113158 _print_to_terminal (
114- '\n ' # ensure space for scrollbar
159+ '\x1b [?25l' # hide cursor
160+ '\n \n ' # ensure space for info line and scrollbar
115161 '\x1b 7' # save cursor position
116- f'\x1b [0;{ self .h - 1 } r' # set top & bottom regions (margins)
162+ f'\x1b [0;{ self .h - 2 } r' # set top & bottom regions (margins) - reserve 2 lines
117163 '\x1b 8' # restore cursor position
118- '\x1b [1A ' # move cursor up
164+ '\x1b [2A ' # move cursor up 2 lines
119165 )
120166 self ._initial_bar ()
167+ self ._print_info ()
121168 return self
122169
123170 def __exit__ (self , _exc_type : type , _exc_val : BaseException , _exc_tb : type ) -> None :
@@ -128,6 +175,7 @@ def __exit__(self, _exc_type: type, _exc_val: BaseException, _exc_tb: type) -> N
128175 except StopIteration :
129176 break
130177 _print_to_terminal (
178+ '\x1b [?25h' # show cursor
131179 f'\x1b [0;{ self .h } r' # reset margins
132180 f'\x1b [{ self .h } ;0H' # move to bottom line
133181 '\n '
@@ -136,8 +184,8 @@ def __exit__(self, _exc_type: type, _exc_val: BaseException, _exc_tb: type) -> N
136184
137185
138186if __name__ == '__main__' :
139- with PBar (100 , * PBar .randgrad ()) as pbar :
140- for i in range (100 ):
141- time .sleep (0.01 )
187+ with PBar (200 , * PBar .randgrad ()) as pbar :
188+ for i in range (200 ):
189+ time .sleep (randint ( int ( 0.01 * 100 ), int ( 0.1 * 100 )) / 100 )
142190 print (f'-> { i } ' )
143191 pbar .update (1 )
0 commit comments