Skip to main content

rich_ext/table/
stream.rs

1//! A keyed table for append/update workloads under a live display.
2//!
3//! [`StreamingTable`] holds rows under stable keys: [`upsert`] appends a new
4//! key and replaces an existing one in place, [`update_cell`] changes one
5//! cell, [`remove`] drops a row. Rows keep their insertion order unless a
6//! sort is set. A [`Window`] shows only the first or last N rows with an
7//! `… N more rows` line, and [`capacity`] evicts the oldest rows for
8//! log-like streams.
9//!
10//! # Only changed rows are re-rendered
11//!
12//! Each row caches its formatted cells, their widths and its rendered lines.
13//! A frame re-formats only rows written since the last frame, and re-lays out
14//! only those rows — unless the column widths change (a new row widens a
15//! column, the widest row goes away, the width available changes), in which
16//! case every visible row is laid out again. [`stats`] counts both, so the
17//! claim is testable.
18//!
19//! The rendering itself is the core [`Table`]'s, never a copy of it: widths
20//! come from the core's column sizing, which depends only on each column's
21//! widest cell. Each changed row is rendered by a core table holding one
22//! *proxy* row of those widest cells plus the row itself, so the row lays
23//! out at exactly the widths the whole table would give it. The output is
24//! byte-for-byte what [`to_table`] renders (plus the window's indicator).
25//!
26//! Row separators (`show_lines`) are not supported; use [`TableData`] for a
27//! static table that needs them.
28//!
29//! ```
30//! use rich::{Console, Justify};
31//! use rich_ext::table::{Column, StreamingTable, Value};
32//!
33//! let mut jobs = StreamingTable::new([
34//!     Column::new("job"),
35//!     Column::new("state"),
36//!     Column::new("done").justify(Justify::Right),
37//! ]);
38//! jobs.upsert("build", ["build".into(), "running".into(), Value::Int(40)]);
39//! jobs.upsert("test", ["test".into(), "queued".into(), Value::Int(0)]);
40//!
41//! let console = Console::builder().width(40).build();
42//! console.render_export(&jobs); // first frame: both rows render
43//! jobs.update_cell(&"build", 2, Value::Int(80));
44//! let out = console.render_export(&jobs); // only `build` renders again
45//! assert!(out.contains("│ build │ running │   80 │"), "{out}");
46//! assert_eq!(jobs.stats().rows_rendered, 3);
47//! ```
48//!
49//! Under a [`LiveCoordinator`](crate::live::LiveCoordinator), render the
50//! table into its region after each batch of changes:
51//!
52//! ```
53//! use rich::protocol::{Support, TargetCapabilities};
54//! use rich::Theme;
55//! use rich_ext::live::LiveCoordinator;
56//! use rich_ext::table::{Column, StreamingTable, Window};
57//! use rich_ext::target::{RenderTarget, TargetKind};
58//!
59//! let capabilities = TargetCapabilities {
60//!     width: 40,
61//!     height: 10,
62//!     color_system: None,
63//!     interactive: false,
64//!     unicode: true,
65//!     hyperlinks: false,
66//!     sixel: Support::Unsupported,
67//! };
68//! let target = RenderTarget::new(TargetKind::Custom, capabilities, Theme::default_theme());
69//! let mut log = StreamingTable::new([Column::new("#"), Column::new("event")])
70//!     .window(Window::Tail(2));
71//!
72//! let mut out = Vec::new();
73//! let mut live = LiveCoordinator::new(&mut out, target.clone());
74//! let region = live.add(target.segments(&log)).unwrap();
75//! for (n, event) in ["start", "fetch", "build"].into_iter().enumerate() {
76//!     log.upsert(n, [n.into(), event.into()]);
77//!     live.update(region.clone(), target.segments(&log)).unwrap();
78//!     live.refresh().unwrap();
79//! }
80//! live.finish().unwrap();
81//! drop(live);
82//! let out = String::from_utf8(out).unwrap();
83//! assert!(out.starts_with("… 1 earlier row\n"), "{out}");
84//! assert!(out.contains("│ 2 │ build │"), "{out}");
85//! ```
86//!
87//! [`upsert`]: StreamingTable::upsert
88//! [`update_cell`]: StreamingTable::update_cell
89//! [`remove`]: StreamingTable::remove
90//! [`capacity`]: StreamingTable::capacity
91//! [`stats`]: StreamingTable::stats
92//! [`to_table`]: StreamingTable::to_table
93//! [`Table`]: rich::Table
94//! [`TableData`]: super::TableData
95
96use std::collections::{BTreeMap, HashMap};
97use std::convert::Infallible;
98use std::fmt;
99use std::hash::Hash;
100use std::sync::{Mutex, PoisonError};
101
102use rich::{Console, ConsoleOptions, LineRenderable, Overflow, Renderable, Segment, Table, Text};
103
104use super::data::{TableData, normalize};
105use super::sort::{SortKey, compare_rows};
106use super::{Column, Frame, Value, frame_builders, headers, style};
107
108/// Which rows a [`StreamingTable`] shows.
109#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
110pub enum Window {
111    /// Every row.
112    #[default]
113    All,
114    /// The first N rows in display order, then `… N more rows`.
115    Head(usize),
116    /// The last N rows in display order, after `… N earlier rows` — for
117    /// log-like appends.
118    Tail(usize),
119}
120
121/// Counters for a [`StreamingTable`]'s render cache.
122#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
123pub struct RenderStats {
124    /// Frames rendered.
125    pub frames: u64,
126    /// Rows whose cells were formatted and measured (new or changed rows).
127    pub rows_prepared: u64,
128    /// Rows laid out by the core table (changed rows, or every visible row
129    /// after a relayout).
130    pub rows_rendered: u64,
131    /// Frames where the column widths or the available width changed, so
132    /// every cached row was laid out again.
133    pub relayouts: u64,
134}
135
136struct Entry<K> {
137    key: K,
138    values: Vec<Value>,
139    version: u64,
140}
141
142/// One row's cached work.
143struct RowCache {
144    version: u64,
145    cells: Vec<Text>,
146    widths: Vec<usize>,
147    lines: Option<Vec<Vec<Segment>>>,
148}
149
150/// What a row's rendered lines depend on besides its own cells.
151#[derive(Clone, Debug, PartialEq, Eq)]
152struct LayoutKey {
153    width: usize,
154    maxima: Vec<usize>,
155    ascii_only: bool,
156    safe_box: bool,
157    legacy_windows: bool,
158}
159
160#[derive(Default)]
161struct Cache {
162    rows: HashMap<u64, RowCache>,
163    layout: Option<LayoutKey>,
164    stats: RenderStats,
165}
166
167/// Keyed rows rendered incrementally; see the [module docs](self).
168///
169/// `K` is the stable row key (a job id, a path, a sequence number). Cells are
170/// [`Value`]s shown through each [`Column`]'s formatter; the frame (title,
171/// caption, box, edges, expand, border style) is set with the same builders
172/// as [`TableData`].
173#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
174pub struct StreamingTable<K> {
175    columns: Vec<Column>,
176    frame: Frame,
177    window: Window,
178    capacity: Option<usize>,
179    sort: Vec<SortKey>,
180    /// Rows by insertion sequence number, so iteration is insertion order.
181    entries: BTreeMap<u64, Entry<K>>,
182    index: HashMap<K, u64>,
183    next_seq: u64,
184    next_version: u64,
185    evicted: u64,
186    cache: Mutex<Cache>,
187}
188
189impl<K> fmt::Debug for StreamingTable<K> {
190    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
191        f.debug_struct("StreamingTable")
192            .field("columns", &self.columns)
193            .field("rows", &self.entries.len())
194            .field("window", &self.window)
195            .field("capacity", &self.capacity)
196            .field("sort", &self.sort)
197            .field("evicted", &self.evicted)
198            .finish()
199    }
200}
201
202frame_builders!([K] StreamingTable<K>);
203
204impl<K: Eq + Hash + Clone> StreamingTable<K> {
205    /// An empty table under `columns`.
206    pub fn new(columns: impl IntoIterator<Item = Column>) -> Self {
207        StreamingTable {
208            columns: columns.into_iter().collect(),
209            frame: Frame::default(),
210            window: Window::All,
211            capacity: None,
212            sort: Vec::new(),
213            entries: BTreeMap::new(),
214            index: HashMap::new(),
215            next_seq: 0,
216            next_version: 0,
217            evicted: 0,
218            cache: Mutex::new(Cache::default()),
219        }
220    }
221
222    /// Show only part of the rows.
223    pub fn window(mut self, window: Window) -> Self {
224        self.window = window;
225        self
226    }
227
228    /// Change the window.
229    pub fn set_window(&mut self, window: Window) {
230        self.window = window;
231    }
232
233    /// Keep at most `rows` rows: inserting beyond that evicts the oldest
234    /// (first inserted). Evicted rows count as earlier rows in the indicator.
235    pub fn capacity(mut self, rows: usize) -> Self {
236        self.capacity = Some(rows);
237        self.evict();
238        self
239    }
240
241    /// Show rows sorted by `keys` (stable over insertion order, empty cells
242    /// last), with header indicators; an empty list restores insertion order.
243    /// Sorting reorders cached rows without re-rendering them.
244    pub fn set_sort(&mut self, keys: impl IntoIterator<Item = SortKey>) {
245        self.sort = keys.into_iter().collect();
246    }
247
248    /// Builder form of [`set_sort`](Self::set_sort).
249    pub fn sort_by(mut self, keys: impl IntoIterator<Item = SortKey>) -> Self {
250        self.set_sort(keys);
251        self
252    }
253
254    fn version(&mut self) -> u64 {
255        self.next_version += 1;
256        self.next_version
257    }
258
259    fn evict(&mut self) {
260        let Some(capacity) = self.capacity else {
261            return;
262        };
263        while self.entries.len() > capacity {
264            let Some((_, entry)) = self.entries.pop_first() else {
265                break;
266            };
267            self.index.remove(&entry.key);
268            self.evicted += 1;
269        }
270    }
271
272    /// Insert a row, or replace the row under `key` in place (it keeps its
273    /// position). Returns `true` when the key is new. Missing cells are
274    /// `Null`; extra cells are dropped. Writing identical values is not a
275    /// change and does not re-render the row.
276    pub fn upsert(&mut self, key: K, row: impl IntoIterator<Item = Value>) -> bool {
277        let values = normalize(row, self.columns.len());
278        if let Some(&seq) = self.index.get(&key) {
279            let entry = self.entries.get(&seq).expect("indexed rows exist");
280            if entry.values != values {
281                let version = self.version();
282                let entry = self.entries.get_mut(&seq).expect("indexed rows exist");
283                entry.values = values;
284                entry.version = version;
285            }
286            return false;
287        }
288        let seq = self.next_seq;
289        self.next_seq += 1;
290        let version = self.version();
291        self.index.insert(key.clone(), seq);
292        self.entries.insert(
293            seq,
294            Entry {
295                key,
296                values,
297                version,
298            },
299        );
300        self.evict();
301        true
302    }
303
304    /// Set one cell of the row under `key`. Returns `false` when there is no
305    /// such row or column.
306    pub fn update_cell(&mut self, key: &K, column: usize, value: impl Into<Value>) -> bool {
307        let value = value.into();
308        let Some(&seq) = self.index.get(key) else {
309            return false;
310        };
311        if column >= self.columns.len() {
312            return false;
313        }
314        if self.entries[&seq].values[column] != value {
315            let version = self.version();
316            let entry = self.entries.get_mut(&seq).expect("indexed rows exist");
317            entry.values[column] = value;
318            entry.version = version;
319        }
320        true
321    }
322
323    /// Remove the row under `key`, returning its cells.
324    pub fn remove(&mut self, key: &K) -> Option<Vec<Value>> {
325        let seq = self.index.remove(key)?;
326        self.entries.remove(&seq).map(|entry| entry.values)
327    }
328
329    /// Remove every row (the evicted count is kept).
330    pub fn clear(&mut self) {
331        self.entries.clear();
332        self.index.clear();
333    }
334
335    /// The cells of the row under `key`.
336    pub fn get(&self, key: &K) -> Option<&[Value]> {
337        let seq = self.index.get(key)?;
338        self.entries.get(seq).map(|entry| entry.values.as_slice())
339    }
340
341    /// Whether a row is stored under `key`.
342    pub fn contains_key(&self, key: &K) -> bool {
343        self.index.contains_key(key)
344    }
345
346    /// The number of stored rows (shown or not).
347    pub fn len(&self) -> usize {
348        self.entries.len()
349    }
350
351    /// Whether no rows are stored.
352    pub fn is_empty(&self) -> bool {
353        self.entries.is_empty()
354    }
355
356    /// How many rows [`capacity`](Self::capacity) has evicted.
357    pub fn evicted(&self) -> u64 {
358        self.evicted
359    }
360
361    /// The columns.
362    pub fn columns(&self) -> &[Column] {
363        &self.columns
364    }
365
366    /// Stored rows in insertion order.
367    pub fn rows(&self) -> impl Iterator<Item = (&K, &[Value])> + '_ {
368        self.entries
369            .values()
370            .map(|entry| (&entry.key, entry.values.as_slice()))
371    }
372
373    /// Render-cache counters since creation (or [`reset_stats`](Self::reset_stats)).
374    pub fn stats(&self) -> RenderStats {
375        self.lock().stats
376    }
377
378    /// Zero the counters.
379    pub fn reset_stats(&self) {
380        self.lock().stats = RenderStats::default();
381    }
382
383    /// Drop every cached row so the next frame renders from scratch. Needed
384    /// only when rendering to a console with a different theme, since cached
385    /// lines keep the styles they were rendered with.
386    pub fn invalidate(&self) {
387        let mut cache = self.lock();
388        cache.rows.clear();
389        cache.layout = None;
390    }
391
392    fn lock(&self) -> std::sync::MutexGuard<'_, Cache> {
393        self.cache.lock().unwrap_or_else(PoisonError::into_inner)
394    }
395
396    /// Sequence numbers in display order.
397    fn order(&self) -> Vec<u64> {
398        let mut order: Vec<u64> = self.entries.keys().copied().collect();
399        if !self.sort.is_empty() {
400            order.sort_by(|a, b| {
401                compare_rows(&self.entries[a].values, &self.entries[b].values, &self.sort)
402            });
403        }
404        order
405    }
406
407    /// The shown rows' sequence numbers, and the counts of earlier and later
408    /// rows not shown.
409    fn visible(&self) -> (Vec<u64>, u64, u64) {
410        let order = self.order();
411        let total = order.len();
412        match self.window {
413            Window::All => (order, self.evicted, 0),
414            Window::Head(n) => {
415                let shown = n.min(total);
416                (
417                    order[..shown].to_vec(),
418                    self.evicted,
419                    (total - shown) as u64,
420                )
421            }
422            Window::Tail(n) => {
423                let start = total.saturating_sub(n);
424                (order[start..].to_vec(), self.evicted + start as u64, 0)
425            }
426        }
427    }
428
429    /// A snapshot of every stored row, in display order, with the same
430    /// columns and sort — for grouping and aggregates.
431    pub fn to_data(&self) -> TableData {
432        let mut data = TableData::new(self.columns.clone());
433        data.frame = self.frame.clone();
434        for seq in self.order() {
435            data.push(self.entries[&seq].values.iter().cloned());
436        }
437        data.sort_by(self.sort.iter().copied())
438    }
439
440    /// The shown rows as one core table, rendered from scratch (without the
441    /// window's indicator lines).
442    pub fn to_table(&self, console: &Console) -> Table {
443        let headers = headers(console, &self.columns, &self.sort);
444        let mut table = self.frame.table(&self.columns, &headers, true, true);
445        for seq in self.visible().0 {
446            let values = &self.entries[&seq].values;
447            table.add_row_text(
448                self.columns
449                    .iter()
450                    .zip(values)
451                    .map(|(column, value)| column.cell(value))
452                    .collect(),
453            );
454        }
455        table
456    }
457
458    fn indicator(
459        console: &Console,
460        options: &ConsoleOptions,
461        count: u64,
462        what: &str,
463    ) -> Vec<Vec<Segment>> {
464        let ellipsis = if console.ascii_only() { "..." } else { "…" };
465        let plural = if count == 1 { "" } else { "s" };
466        let text = Text::styled(
467            format!("{ellipsis} {count} {what} row{plural}"),
468            style(console, "table.more"),
469        )
470        .no_wrap(true)
471        .overflow(Overflow::Ellipsis);
472        // One line per indicator, whatever width or height the frame is
473        // given: too narrow a frame cuts it short with an ellipsis.
474        let mut options = options.clone();
475        options.height = None;
476        console.render_lines(&text, &options, false)
477    }
478
479    /// Render the frame as lines, reusing every cached row that is still valid.
480    fn render_lines(&self, console: &Console, options: &ConsoleOptions) -> Vec<Vec<Segment>> {
481        let mut cache = self.lock();
482        let cache = &mut *cache;
483        cache.stats.frames += 1;
484        let (visible, earlier, later) = self.visible();
485        let headers = headers(console, &self.columns, &self.sort);
486
487        let mut out = Vec::new();
488        if earlier > 0 {
489            out.extend(Self::indicator(console, options, earlier, "earlier"));
490        }
491        // Drop cached rows that are gone or out of the window.
492        let shown: std::collections::HashSet<u64> = visible.iter().copied().collect();
493        cache.rows.retain(|seq, _| shown.contains(seq));
494
495        if self.columns.is_empty() || visible.is_empty() {
496            let table = self.frame.table(&self.columns, &headers, true, true);
497            out.extend(table_lines(&table, console, options));
498        } else {
499            self.render_rows(console, options, cache, &visible, &headers, &mut out);
500        }
501        if later > 0 {
502            out.extend(Self::indicator(console, options, later, "more"));
503        }
504        out
505    }
506
507    fn render_rows(
508        &self,
509        console: &Console,
510        options: &ConsoleOptions,
511        cache: &mut Cache,
512        visible: &[u64],
513        headers: &[Text],
514        out: &mut Vec<Vec<Segment>>,
515    ) {
516        // Format and measure new or changed rows.
517        for seq in visible {
518            let entry = &self.entries[seq];
519            if cache
520                .rows
521                .get(seq)
522                .is_some_and(|row| row.version == entry.version)
523            {
524                continue;
525            }
526            let cells: Vec<Text> = self
527                .columns
528                .iter()
529                .zip(&entry.values)
530                .map(|(column, value)| column.cell(value))
531                .collect();
532            let widths = cells.iter().map(|cell| cell.measurement().1).collect();
533            cache.rows.insert(
534                *seq,
535                RowCache {
536                    version: entry.version,
537                    cells,
538                    widths,
539                    lines: None,
540                },
541            );
542            cache.stats.rows_prepared += 1;
543        }
544
545        // The widest cell of each column, header included: the core sizes a
546        // column from its cells' maximum widths only, so a table of these
547        // cells gets the same widths as the whole table.
548        let mut widest: Vec<(usize, Option<u64>)> = headers
549            .iter()
550            .map(|header| (header.measurement().1, None))
551            .collect();
552        for seq in visible {
553            for (best, &width) in widest.iter_mut().zip(&cache.rows[seq].widths) {
554                if width > best.0 {
555                    *best = (width, Some(*seq));
556                }
557            }
558        }
559        let proxy: Vec<Text> = widest
560            .iter()
561            .enumerate()
562            .map(|(column, (_, seq))| match seq {
563                Some(seq) => cache.rows[seq].cells[column].clone(),
564                None => headers[column].clone(),
565            })
566            .collect();
567        let key = LayoutKey {
568            width: options.max_width,
569            maxima: widest.iter().map(|(width, _)| *width).collect(),
570            ascii_only: console.ascii_only(),
571            safe_box: console.safe_box(),
572            legacy_windows: console.legacy_windows(),
573        };
574        if cache.layout.as_ref() != Some(&key) {
575            for row in cache.rows.values_mut() {
576                row.lines = None;
577            }
578            cache.layout = Some(key);
579            cache.stats.relayouts += 1;
580        }
581
582        // How many lines the proxy row and the box edges take.
583        let edge = self.frame.edge_lines();
584        let render = |frame: &Frame, rows: &[&[Text]], show_header: bool| {
585            let mut table = frame.table(&self.columns, headers, show_header, true);
586            for row in rows {
587                table.add_row_text(row.to_vec());
588            }
589            table_lines(&table, console, options)
590        };
591        let bare = Frame {
592            title: None,
593            caption: None,
594            ..self.frame.clone()
595        };
596        let proxy_height = render(&bare, &[&proxy], false).len() - 2 * edge;
597
598        // Title, top edge, header and header separator.
599        let head = Frame {
600            caption: None,
601            ..self.frame.clone()
602        };
603        let head = render(&head, &[&proxy], true);
604        out.extend_from_slice(&head[..head.len() - proxy_height - edge]);
605
606        for seq in visible {
607            let row = cache.rows.get_mut(seq).expect("prepared above");
608            if row.lines.is_none() {
609                let lines = render(&bare, &[&proxy, &row.cells], false);
610                row.lines = Some(lines[edge + proxy_height..lines.len() - edge].to_vec());
611                cache.stats.rows_rendered += 1;
612            }
613            out.extend(row.lines.iter().flatten().cloned());
614        }
615
616        // Bottom edge and caption.
617        let foot = Frame {
618            title: None,
619            ..self.frame.clone()
620        };
621        let foot = render(&foot, &[&proxy], false);
622        out.extend_from_slice(&foot[edge + proxy_height..]);
623    }
624}
625
626/// A core table's lines, as it streams them.
627pub(super) fn table_lines(
628    table: &Table,
629    console: &Console,
630    options: &ConsoleOptions,
631) -> Vec<Vec<Segment>> {
632    let mut lines = Vec::new();
633    let result: Result<(), Infallible> = table.try_for_each_line(console, options, |line| {
634        lines.push(line);
635        Ok(())
636    });
637    match result {
638        Ok(()) => lines,
639        Err(never) => match never {},
640    }
641}
642
643impl<K: Eq + Hash + Clone> Renderable for StreamingTable<K> {
644    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
645        crate::event::flatten(self.render_lines(console, options))
646    }
647
648    fn measure(&self, console: &Console, options: &ConsoleOptions) -> rich::measure::Measurement {
649        self.to_table(console).measure(console, options)
650    }
651}
652
653impl<K: Eq + Hash + Clone> crate::a11y::AccessibleText for StreamingTable<K> {
654    fn accessible_text(&self, width: usize) -> String {
655        let console = Console::builder().width(width.max(1)).build();
656        self.to_table(&console).accessible_text(width)
657    }
658}