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::{normalize, TableData};
105use super::sort::{compare_rows, SortKey};
106use super::{frame_builders, headers, style, Column, Frame, Value};
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`].
173pub struct StreamingTable<K> {
174    columns: Vec<Column>,
175    frame: Frame,
176    window: Window,
177    capacity: Option<usize>,
178    sort: Vec<SortKey>,
179    /// Rows by insertion sequence number, so iteration is insertion order.
180    entries: BTreeMap<u64, Entry<K>>,
181    index: HashMap<K, u64>,
182    next_seq: u64,
183    next_version: u64,
184    evicted: u64,
185    cache: Mutex<Cache>,
186}
187
188impl<K> fmt::Debug for StreamingTable<K> {
189    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190        f.debug_struct("StreamingTable")
191            .field("columns", &self.columns)
192            .field("rows", &self.entries.len())
193            .field("window", &self.window)
194            .field("capacity", &self.capacity)
195            .field("sort", &self.sort)
196            .field("evicted", &self.evicted)
197            .finish()
198    }
199}
200
201frame_builders!([K] StreamingTable<K>);
202
203impl<K: Eq + Hash + Clone> StreamingTable<K> {
204    /// An empty table under `columns`.
205    pub fn new(columns: impl IntoIterator<Item = Column>) -> Self {
206        StreamingTable {
207            columns: columns.into_iter().collect(),
208            frame: Frame::default(),
209            window: Window::All,
210            capacity: None,
211            sort: Vec::new(),
212            entries: BTreeMap::new(),
213            index: HashMap::new(),
214            next_seq: 0,
215            next_version: 0,
216            evicted: 0,
217            cache: Mutex::new(Cache::default()),
218        }
219    }
220
221    /// Show only part of the rows.
222    pub fn window(mut self, window: Window) -> Self {
223        self.window = window;
224        self
225    }
226
227    /// Change the window.
228    pub fn set_window(&mut self, window: Window) {
229        self.window = window;
230    }
231
232    /// Keep at most `rows` rows: inserting beyond that evicts the oldest
233    /// (first inserted). Evicted rows count as earlier rows in the indicator.
234    pub fn capacity(mut self, rows: usize) -> Self {
235        self.capacity = Some(rows);
236        self.evict();
237        self
238    }
239
240    /// Show rows sorted by `keys` (stable over insertion order, empty cells
241    /// last), with header indicators; an empty list restores insertion order.
242    /// Sorting reorders cached rows without re-rendering them.
243    pub fn set_sort(&mut self, keys: impl IntoIterator<Item = SortKey>) {
244        self.sort = keys.into_iter().collect();
245    }
246
247    /// Builder form of [`set_sort`](Self::set_sort).
248    pub fn sort_by(mut self, keys: impl IntoIterator<Item = SortKey>) -> Self {
249        self.set_sort(keys);
250        self
251    }
252
253    fn version(&mut self) -> u64 {
254        self.next_version += 1;
255        self.next_version
256    }
257
258    fn evict(&mut self) {
259        let Some(capacity) = self.capacity else {
260            return;
261        };
262        while self.entries.len() > capacity {
263            let Some((_, entry)) = self.entries.pop_first() else {
264                break;
265            };
266            self.index.remove(&entry.key);
267            self.evicted += 1;
268        }
269    }
270
271    /// Insert a row, or replace the row under `key` in place (it keeps its
272    /// position). Returns `true` when the key is new. Missing cells are
273    /// `Null`; extra cells are dropped. Writing identical values is not a
274    /// change and does not re-render the row.
275    pub fn upsert(&mut self, key: K, row: impl IntoIterator<Item = Value>) -> bool {
276        let values = normalize(row, self.columns.len());
277        if let Some(&seq) = self.index.get(&key) {
278            let entry = self.entries.get(&seq).expect("indexed rows exist");
279            if entry.values != values {
280                let version = self.version();
281                let entry = self.entries.get_mut(&seq).expect("indexed rows exist");
282                entry.values = values;
283                entry.version = version;
284            }
285            return false;
286        }
287        let seq = self.next_seq;
288        self.next_seq += 1;
289        let version = self.version();
290        self.index.insert(key.clone(), seq);
291        self.entries.insert(
292            seq,
293            Entry {
294                key,
295                values,
296                version,
297            },
298        );
299        self.evict();
300        true
301    }
302
303    /// Set one cell of the row under `key`. Returns `false` when there is no
304    /// such row or column.
305    pub fn update_cell(&mut self, key: &K, column: usize, value: impl Into<Value>) -> bool {
306        let value = value.into();
307        let Some(&seq) = self.index.get(key) else {
308            return false;
309        };
310        if column >= self.columns.len() {
311            return false;
312        }
313        if self.entries[&seq].values[column] != value {
314            let version = self.version();
315            let entry = self.entries.get_mut(&seq).expect("indexed rows exist");
316            entry.values[column] = value;
317            entry.version = version;
318        }
319        true
320    }
321
322    /// Remove the row under `key`, returning its cells.
323    pub fn remove(&mut self, key: &K) -> Option<Vec<Value>> {
324        let seq = self.index.remove(key)?;
325        self.entries.remove(&seq).map(|entry| entry.values)
326    }
327
328    /// Remove every row (the evicted count is kept).
329    pub fn clear(&mut self) {
330        self.entries.clear();
331        self.index.clear();
332    }
333
334    /// The cells of the row under `key`.
335    pub fn get(&self, key: &K) -> Option<&[Value]> {
336        let seq = self.index.get(key)?;
337        self.entries.get(seq).map(|entry| entry.values.as_slice())
338    }
339
340    /// Whether a row is stored under `key`.
341    pub fn contains_key(&self, key: &K) -> bool {
342        self.index.contains_key(key)
343    }
344
345    /// The number of stored rows (shown or not).
346    pub fn len(&self) -> usize {
347        self.entries.len()
348    }
349
350    /// Whether no rows are stored.
351    pub fn is_empty(&self) -> bool {
352        self.entries.is_empty()
353    }
354
355    /// How many rows [`capacity`](Self::capacity) has evicted.
356    pub fn evicted(&self) -> u64 {
357        self.evicted
358    }
359
360    /// The columns.
361    pub fn columns(&self) -> &[Column] {
362        &self.columns
363    }
364
365    /// Stored rows in insertion order.
366    pub fn rows(&self) -> impl Iterator<Item = (&K, &[Value])> + '_ {
367        self.entries
368            .values()
369            .map(|entry| (&entry.key, entry.values.as_slice()))
370    }
371
372    /// Render-cache counters since creation (or [`reset_stats`](Self::reset_stats)).
373    pub fn stats(&self) -> RenderStats {
374        self.lock().stats
375    }
376
377    /// Zero the counters.
378    pub fn reset_stats(&self) {
379        self.lock().stats = RenderStats::default();
380    }
381
382    /// Drop every cached row so the next frame renders from scratch. Needed
383    /// only when rendering to a console with a different theme, since cached
384    /// lines keep the styles they were rendered with.
385    pub fn invalidate(&self) {
386        let mut cache = self.lock();
387        cache.rows.clear();
388        cache.layout = None;
389    }
390
391    fn lock(&self) -> std::sync::MutexGuard<'_, Cache> {
392        self.cache.lock().unwrap_or_else(PoisonError::into_inner)
393    }
394
395    /// Sequence numbers in display order.
396    fn order(&self) -> Vec<u64> {
397        let mut order: Vec<u64> = self.entries.keys().copied().collect();
398        if !self.sort.is_empty() {
399            order.sort_by(|a, b| {
400                compare_rows(&self.entries[a].values, &self.entries[b].values, &self.sort)
401            });
402        }
403        order
404    }
405
406    /// The shown rows' sequence numbers, and the counts of earlier and later
407    /// rows not shown.
408    fn visible(&self) -> (Vec<u64>, u64, u64) {
409        let order = self.order();
410        let total = order.len();
411        match self.window {
412            Window::All => (order, self.evicted, 0),
413            Window::Head(n) => {
414                let shown = n.min(total);
415                (
416                    order[..shown].to_vec(),
417                    self.evicted,
418                    (total - shown) as u64,
419                )
420            }
421            Window::Tail(n) => {
422                let start = total.saturating_sub(n);
423                (order[start..].to_vec(), self.evicted + start as u64, 0)
424            }
425        }
426    }
427
428    /// A snapshot of every stored row, in display order, with the same
429    /// columns and sort — for grouping and aggregates.
430    pub fn to_data(&self) -> TableData {
431        let mut data = TableData::new(self.columns.clone());
432        data.frame = self.frame.clone();
433        for seq in self.order() {
434            data.push(self.entries[&seq].values.iter().cloned());
435        }
436        data.sort_by(self.sort.iter().copied())
437    }
438
439    /// The shown rows as one core table, rendered from scratch (without the
440    /// window's indicator lines).
441    pub fn to_table(&self, console: &Console) -> Table {
442        let headers = headers(console, &self.columns, &self.sort);
443        let mut table = self.frame.table(&self.columns, &headers, true, true);
444        for seq in self.visible().0 {
445            let values = &self.entries[&seq].values;
446            table.add_row_text(
447                self.columns
448                    .iter()
449                    .zip(values)
450                    .map(|(column, value)| column.cell(value))
451                    .collect(),
452            );
453        }
454        table
455    }
456
457    fn indicator(
458        console: &Console,
459        options: &ConsoleOptions,
460        count: u64,
461        what: &str,
462    ) -> Vec<Vec<Segment>> {
463        let ellipsis = if console.ascii_only() { "..." } else { "…" };
464        let plural = if count == 1 { "" } else { "s" };
465        let text = Text::styled(
466            format!("{ellipsis} {count} {what} row{plural}"),
467            style(console, "table.more"),
468        )
469        .no_wrap(true)
470        .overflow(Overflow::Ellipsis);
471        // One line per indicator, whatever width or height the frame is
472        // given: too narrow a frame cuts it short with an ellipsis.
473        let mut options = options.clone();
474        options.height = None;
475        console.render_lines(&text, &options, false)
476    }
477
478    /// Render the frame as lines, reusing every cached row that is still valid.
479    fn render_lines(&self, console: &Console, options: &ConsoleOptions) -> Vec<Vec<Segment>> {
480        let mut cache = self.lock();
481        let cache = &mut *cache;
482        cache.stats.frames += 1;
483        let (visible, earlier, later) = self.visible();
484        let headers = headers(console, &self.columns, &self.sort);
485
486        let mut out = Vec::new();
487        if earlier > 0 {
488            out.extend(Self::indicator(console, options, earlier, "earlier"));
489        }
490        // Drop cached rows that are gone or out of the window.
491        let shown: std::collections::HashSet<u64> = visible.iter().copied().collect();
492        cache.rows.retain(|seq, _| shown.contains(seq));
493
494        if self.columns.is_empty() || visible.is_empty() {
495            let table = self.frame.table(&self.columns, &headers, true, true);
496            out.extend(table_lines(&table, console, options));
497        } else {
498            self.render_rows(console, options, cache, &visible, &headers, &mut out);
499        }
500        if later > 0 {
501            out.extend(Self::indicator(console, options, later, "more"));
502        }
503        out
504    }
505
506    fn render_rows(
507        &self,
508        console: &Console,
509        options: &ConsoleOptions,
510        cache: &mut Cache,
511        visible: &[u64],
512        headers: &[Text],
513        out: &mut Vec<Vec<Segment>>,
514    ) {
515        // Format and measure new or changed rows.
516        for seq in visible {
517            let entry = &self.entries[seq];
518            if cache
519                .rows
520                .get(seq)
521                .is_some_and(|row| row.version == entry.version)
522            {
523                continue;
524            }
525            let cells: Vec<Text> = self
526                .columns
527                .iter()
528                .zip(&entry.values)
529                .map(|(column, value)| column.cell(value))
530                .collect();
531            let widths = cells.iter().map(|cell| cell.measurement().1).collect();
532            cache.rows.insert(
533                *seq,
534                RowCache {
535                    version: entry.version,
536                    cells,
537                    widths,
538                    lines: None,
539                },
540            );
541            cache.stats.rows_prepared += 1;
542        }
543
544        // The widest cell of each column, header included: the core sizes a
545        // column from its cells' maximum widths only, so a table of these
546        // cells gets the same widths as the whole table.
547        let mut widest: Vec<(usize, Option<u64>)> = headers
548            .iter()
549            .map(|header| (header.measurement().1, None))
550            .collect();
551        for seq in visible {
552            for (best, &width) in widest.iter_mut().zip(&cache.rows[seq].widths) {
553                if width > best.0 {
554                    *best = (width, Some(*seq));
555                }
556            }
557        }
558        let proxy: Vec<Text> = widest
559            .iter()
560            .enumerate()
561            .map(|(column, (_, seq))| match seq {
562                Some(seq) => cache.rows[seq].cells[column].clone(),
563                None => headers[column].clone(),
564            })
565            .collect();
566        let key = LayoutKey {
567            width: options.max_width,
568            maxima: widest.iter().map(|(width, _)| *width).collect(),
569            ascii_only: console.ascii_only(),
570            safe_box: console.safe_box(),
571            legacy_windows: console.legacy_windows(),
572        };
573        if cache.layout.as_ref() != Some(&key) {
574            for row in cache.rows.values_mut() {
575                row.lines = None;
576            }
577            cache.layout = Some(key);
578            cache.stats.relayouts += 1;
579        }
580
581        // How many lines the proxy row and the box edges take.
582        let edge = self.frame.edge_lines();
583        let render = |frame: &Frame, rows: &[&[Text]], show_header: bool| {
584            let mut table = frame.table(&self.columns, headers, show_header, true);
585            for row in rows {
586                table.add_row_text(row.to_vec());
587            }
588            table_lines(&table, console, options)
589        };
590        let bare = Frame {
591            title: None,
592            caption: None,
593            ..self.frame.clone()
594        };
595        let proxy_height = render(&bare, &[&proxy], false).len() - 2 * edge;
596
597        // Title, top edge, header and header separator.
598        let head = Frame {
599            caption: None,
600            ..self.frame.clone()
601        };
602        let head = render(&head, &[&proxy], true);
603        out.extend_from_slice(&head[..head.len() - proxy_height - edge]);
604
605        for seq in visible {
606            let row = cache.rows.get_mut(seq).expect("prepared above");
607            if row.lines.is_none() {
608                let lines = render(&bare, &[&proxy, &row.cells], false);
609                row.lines = Some(lines[edge + proxy_height..lines.len() - edge].to_vec());
610                cache.stats.rows_rendered += 1;
611            }
612            out.extend(row.lines.iter().flatten().cloned());
613        }
614
615        // Bottom edge and caption.
616        let foot = Frame {
617            title: None,
618            ..self.frame.clone()
619        };
620        let foot = render(&foot, &[&proxy], false);
621        out.extend_from_slice(&foot[edge + proxy_height..]);
622    }
623}
624
625/// A core table's lines, as it streams them.
626pub(super) fn table_lines(
627    table: &Table,
628    console: &Console,
629    options: &ConsoleOptions,
630) -> Vec<Vec<Segment>> {
631    let mut lines = Vec::new();
632    let result: Result<(), Infallible> = table.try_for_each_line(console, options, |line| {
633        lines.push(line);
634        Ok(())
635    });
636    match result {
637        Ok(()) => lines,
638        Err(never) => match never {},
639    }
640}
641
642impl<K: Eq + Hash + Clone> Renderable for StreamingTable<K> {
643    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
644        crate::event::flatten(self.render_lines(console, options))
645    }
646
647    fn measure(&self, console: &Console, options: &ConsoleOptions) -> rich::measure::Measurement {
648        self.to_table(console).measure(console, options)
649    }
650}
651
652impl<K: Eq + Hash + Clone> crate::a11y::AccessibleText for StreamingTable<K> {
653    fn accessible_text(&self, width: usize) -> String {
654        let console = Console::builder().width(width.max(1)).build();
655        self.to_table(&console).accessible_text(width)
656    }
657}