Skip to main content

reifydb_core/value/frame/
render.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Copyright (c) 2025 ReifyDB
3
4use std::fmt::{self, Write};
5
6use reifydb_type::{
7	util::unicode::UnicodeWidthStr,
8	value::frame::{column::FrameColumn, frame::Frame},
9};
10
11/// Frame renderer with various rendering options
12pub struct FrameRenderer;
13
14impl FrameRenderer {
15	/// Render the frame with all features enabled (including encoded numbers if present)
16	pub fn render_full(frame: &Frame) -> Result<String, fmt::Error> {
17		let mut output = String::new();
18		Self::render_full_to(frame, &mut output)?;
19		Ok(output)
20	}
21
22	/// Render the frame without encoded numbers
23	pub fn render_without_row_numbers(frame: &Frame) -> Result<String, fmt::Error> {
24		let mut output = String::new();
25		Self::render_without_row_numbers_to(frame, &mut output)?;
26		Ok(output)
27	}
28
29	/// Render the frame with all features to the given formatter
30	pub fn render_full_to(frame: &Frame, f: &mut dyn Write) -> fmt::Result {
31		Self::render_internal(frame, f, true)
32	}
33
34	/// Render the frame without encoded numbers to the given formatter
35	pub fn render_without_row_numbers_to(frame: &Frame, f: &mut dyn Write) -> fmt::Result {
36		Self::render_internal(frame, f, false)
37	}
38
39	/// Internal rendering implementation
40	fn render_internal(frame: &Frame, f: &mut dyn Write, include_row_numbers: bool) -> fmt::Result {
41		let row_count = frame.first().map_or(0, |c| c.data.len());
42		let has_row_numbers = include_row_numbers && !frame.row_numbers.is_empty();
43		let col_count = frame.len()
44			+ if has_row_numbers {
45				1
46			} else {
47				0
48			};
49
50		// Get the display order for regular columns
51		let column_order = Self::get_column_display_order(frame);
52
53		let mut col_widths = vec![0; col_count];
54
55		// If we have encoded numbers, calculate width for encoded number column
56		let row_num_col_idx = if has_row_numbers {
57			// Row number column is always first
58			let row_num_header = "rownum";
59			col_widths[0] = Self::display_width(row_num_header);
60
61			// Calculate max width needed for encoded numbers
62			for row_num in &frame.row_numbers {
63				let s = row_num.to_string();
64				col_widths[0] = col_widths[0].max(Self::display_width(&s));
65			}
66			1 // Start regular columns at index 1
67		} else {
68			0 // Start regular columns at index 0
69		};
70
71		for (display_idx, &col_idx) in column_order.iter().enumerate() {
72			let col = &frame[col_idx];
73			let display_name = Self::escape_control_chars(&col.name);
74			col_widths[row_num_col_idx + display_idx] = Self::display_width(&display_name);
75		}
76
77		for row_numberx in 0..row_count {
78			for (display_idx, &col_idx) in column_order.iter().enumerate() {
79				let col = &frame[col_idx];
80				let s = Self::extract_string_value(col, row_numberx);
81				col_widths[row_num_col_idx + display_idx] =
82					col_widths[row_num_col_idx + display_idx].max(Self::display_width(&s));
83			}
84		}
85
86		// Add padding
87		for w in &mut col_widths {
88			*w += 2;
89		}
90
91		let sep = format!("+{}+", col_widths.iter().map(|w| "-".repeat(*w + 2)).collect::<Vec<_>>().join("+"));
92		writeln!(f, "{}", sep)?;
93
94		let mut header = Vec::new();
95
96		// Add encoded number header if present
97		if has_row_numbers {
98			let w = col_widths[0];
99			let name = "rownum";
100			let pad = w - Self::display_width(name);
101			let l = pad / 2;
102			let r = pad - l;
103			header.push(format!(" {:left$}{}{:right$} ", "", name, "", left = l, right = r));
104		}
105
106		// Add regular column headers
107		for (display_idx, &col_idx) in column_order.iter().enumerate() {
108			let col = &frame[col_idx];
109			let w = col_widths[row_num_col_idx + display_idx];
110			let name = Self::escape_control_chars(&col.name);
111			let pad = w - Self::display_width(&name);
112			let l = pad / 2;
113			let r = pad - l;
114			header.push(format!(" {:left$}{}{:right$} ", "", name, "", left = l, right = r));
115		}
116
117		writeln!(f, "|{}|", header.join("|"))?;
118
119		writeln!(f, "{}", sep)?;
120
121		for row_numberx in 0..row_count {
122			let mut row = Vec::new();
123
124			// Add encoded number value if present
125			if has_row_numbers {
126				let w = col_widths[0];
127				let s = frame.row_numbers[row_numberx].to_string();
128				let pad = w - Self::display_width(&s);
129				let l = pad / 2;
130				let r = pad - l;
131				row.push(format!(" {:left$}{}{:right$} ", "", s, "", left = l, right = r));
132			}
133
134			// Add regular column values
135			for (display_idx, &col_idx) in column_order.iter().enumerate() {
136				let col = &frame[col_idx];
137				let w = col_widths[row_num_col_idx + display_idx];
138				let s = Self::extract_string_value(col, row_numberx);
139				let pad = w - Self::display_width(&s);
140				let l = pad / 2;
141				let r = pad - l;
142				row.push(format!(" {:left$}{}{:right$} ", "", s, "", left = l, right = r));
143			}
144
145			writeln!(f, "|{}|", row.join("|"))?;
146		}
147
148		writeln!(f, "{}", sep)
149	}
150
151	/// Calculate the display width of a string, handling newlines properly.
152	/// For strings with newlines, returns the width of the longest line.
153	/// For strings without newlines, returns the unicode display width.
154	fn display_width(s: &str) -> usize {
155		if s.contains('\n') {
156			s.lines().map(|line| line.width()).max().unwrap_or(0)
157		} else {
158			s.width()
159		}
160	}
161
162	/// Escape newlines and tabs in a string for single-line display.
163	/// Replaces '\n' with "\\n" and '\t' with "\\t".
164	fn escape_control_chars(s: &str) -> String {
165		s.replace('\n', "\\n").replace('\t', "\\t")
166	}
167
168	/// Create a column display order (no special handling needed since encoded numbers are separate)
169	fn get_column_display_order(frame: &Frame) -> Vec<usize> {
170		(0..frame.len()).collect()
171	}
172
173	/// Extract string value from column at given encoded index, with proper escaping
174	fn extract_string_value(col: &FrameColumn, row_numberx: usize) -> String {
175		let s = col.data.as_string(row_numberx);
176		Self::escape_control_chars(&s)
177	}
178}