I'm using a Live
display to show the content of a Table
which grows over time. Eventually there is a vertical overflow and in that case I'd like the oldest (i.e. topmost) rows to vanish while the most recent rows should be shown together with the header, i.e. the content should be scrolled. The vertical_overflow
parameter of the live display provides a "visible"
option, but this makes the header of the table vanish. Obviously this is a Table
specific issue, since the header should stay but the content should be scrolled.
import time
from rich.live import Live
from rich.table import Table
table = Table()
table.add_column('Time')
table.add_column('Message')
with Live(table, refresh_per_second=5, vertical_overflow='visible'):
for i in range(100):
time.sleep(0.2)
table.add_row(time.asctime(), f'Event {i:03d}')
The left part shows the behavior with vertical_overflow='visible'
and the right part shows the desired behavior:
So far I'm using a workaround with a separate data structure to hold the rows and then creating the table from scratch every time a new row is to be added. This doesn't seem to be very efficient, so I'm wondering if there's a better solution. This workaround also fails for multi-line rows as it counts them as a single row (hence overflow will occur).
from collections import deque
import os
import time
from rich.live import Live
from rich.table import Table
def generate_table(rows):
table = Table()
table.add_column('Time')
table.add_column('Message')
for row in rows:
table.add_row(*row)
return table
width, height = os.get_terminal_size()
messages = deque(maxlen=height-4) # save space for header and footer
with Live(generate_table(messages), refresh_per_second=5) as live:
for i in range(100):
time.sleep(0.2)
messages.append((time.asctime(), f'Event {i:03d}'))
live.update(generate_table(messages))
I was working on the same thing recently and couldn't find a built-in solution either. Since you're rendering a live display, the table won't have more than ~100 rows, so efficiency should not be a concern.
Here is my solution. It repeatedly removes rows from the top until the table fits. This is measured by putting a table into a Layout
which truncates the table at the bottom if it does not fit.
from collections import deque
import os
import time
from rich.live import Live
from rich.table import Table
from rich.layout import Layout
from rich.console import Console
def generate_table(rows):
layout = Layout()
console = Console()
table = Table()
table.add_column('Time')
table.add_column('Message')
rows = list(rows)
# This would also get the height:
# render_map = layout.render(console, console.options)
# render_map[layout].region.height
n_rows = os.get_terminal_size()[1]
while n_rows >= 0:
table = Table()
table.add_column('Time')
table.add_column('Message')
for row in rows[-n_rows:]:
table.add_row(*row)
layout.update(table)
render_map = layout.render(console, console.options)
if len(render_map[layout].render[-1]) > 2:
# The table is overflowing
n_rows -= 1
else:
break
return table
width, height = os.get_terminal_size()
messages = deque(maxlen=height-4) # save space for header and footer
with Live(generate_table(messages), refresh_per_second=5) as live:
for i in range(100):
time.sleep(0.2)
messages.append((time.asctime(), f'Event {i:03d}'))
live.update(generate_table(messages))
The magic line here is if len(render_map[layout].render[-1]) > 2:
.
It's a hacky way to tell if the table is being printed in its entirety.
If it is, the last element of render_map[layout].render
will look like
[
Segment('└──────────────────────────┘', Style()),
Segment(' ',)
]
or like
[
Segment(
'
',
)
]
but if it is truncated it will look like
[
Segment('│', Style()),
Segment(' ', Style()),
Segment(
'37',
Style(color=Color('cyan', ColorType.STANDARD, number=6), bold=True, italic=False)
),
Segment(' ', Style()),
Segment(' ', Style()),
Segment('│', Style()),
Segment(' ',)
]