Skip to main content

reifydb_core/value/frame/
render.rs

1// SPDX-License-Identifier: Apache-2.0
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 has_created_at = !frame.created_at.is_empty();
44		let has_updated_at = !frame.updated_at.is_empty();
45		let col_count = frame.len()
46			+ if has_row_numbers {
47				1
48			} else {
49				0
50			} + if has_created_at {
51			1
52		} else {
53			0
54		} + if has_updated_at {
55			1
56		} else {
57			0
58		};
59
60		// Get the display order for regular columns
61		let column_order = Self::get_column_display_order(frame);
62
63		let mut col_widths = vec![0; col_count];
64
65		// Calculate widths for system columns
66		let mut sys_col_idx = 0;
67		if has_row_numbers {
68			col_widths[sys_col_idx] = Self::display_width("#rownum");
69			for row_num in &frame.row_numbers {
70				col_widths[sys_col_idx] =
71					col_widths[sys_col_idx].max(Self::display_width(&row_num.to_string()));
72			}
73			sys_col_idx += 1;
74		}
75		if has_created_at {
76			col_widths[sys_col_idx] = Self::display_width("#created_at");
77			for ts in &frame.created_at {
78				col_widths[sys_col_idx] =
79					col_widths[sys_col_idx].max(Self::display_width(&ts.to_string()));
80			}
81			sys_col_idx += 1;
82		}
83		if has_updated_at {
84			col_widths[sys_col_idx] = Self::display_width("#updated_at");
85			for ts in &frame.updated_at {
86				col_widths[sys_col_idx] =
87					col_widths[sys_col_idx].max(Self::display_width(&ts.to_string()));
88			}
89			sys_col_idx += 1;
90		}
91		let row_num_col_idx = sys_col_idx;
92
93		for (display_idx, &col_idx) in column_order.iter().enumerate() {
94			let col = &frame[col_idx];
95			let display_name = Self::escape_control_chars(&col.name);
96			col_widths[row_num_col_idx + display_idx] = Self::display_width(&display_name);
97		}
98
99		for row_numberx in 0..row_count {
100			for (display_idx, &col_idx) in column_order.iter().enumerate() {
101				let col = &frame[col_idx];
102				let s = Self::extract_string_value(col, row_numberx);
103				col_widths[row_num_col_idx + display_idx] =
104					col_widths[row_num_col_idx + display_idx].max(Self::display_width(&s));
105			}
106		}
107
108		// Add padding
109		for w in &mut col_widths {
110			*w += 2;
111		}
112
113		let sep = format!("+{}+", col_widths.iter().map(|w| "-".repeat(*w + 2)).collect::<Vec<_>>().join("+"));
114		writeln!(f, "{}", sep)?;
115
116		let mut header = Vec::new();
117
118		// Add system column headers
119		let mut sys_idx = 0;
120		if has_row_numbers {
121			let w = col_widths[sys_idx];
122			let name = "#rownum";
123			let pad = w - Self::display_width(name);
124			let l = pad / 2;
125			let r = pad - l;
126			header.push(format!(" {:left$}{}{:right$} ", "", name, "", left = l, right = r));
127			sys_idx += 1;
128		}
129		if has_created_at {
130			let w = col_widths[sys_idx];
131			let name = "#created_at";
132			let pad = w - Self::display_width(name);
133			let l = pad / 2;
134			let r = pad - l;
135			header.push(format!(" {:left$}{}{:right$} ", "", name, "", left = l, right = r));
136			sys_idx += 1;
137		}
138		if has_updated_at {
139			let w = col_widths[sys_idx];
140			let name = "#updated_at";
141			let pad = w - Self::display_width(name);
142			let l = pad / 2;
143			let r = pad - l;
144			header.push(format!(" {:left$}{}{:right$} ", "", name, "", left = l, right = r));
145			sys_idx += 1;
146		}
147		let _ = sys_idx;
148
149		// Add regular column headers
150		for (display_idx, &col_idx) in column_order.iter().enumerate() {
151			let col = &frame[col_idx];
152			let w = col_widths[row_num_col_idx + display_idx];
153			let name = Self::escape_control_chars(&col.name);
154			let pad = w - Self::display_width(&name);
155			let l = pad / 2;
156			let r = pad - l;
157			header.push(format!(" {:left$}{}{:right$} ", "", name, "", left = l, right = r));
158		}
159
160		writeln!(f, "|{}|", header.join("|"))?;
161
162		writeln!(f, "{}", sep)?;
163
164		for row_numberx in 0..row_count {
165			let mut row = Vec::new();
166
167			// Add system column values
168			let mut sys_idx = 0;
169			if has_row_numbers {
170				let w = col_widths[sys_idx];
171				let s = frame.row_numbers[row_numberx].to_string();
172				let pad = w - Self::display_width(&s);
173				let l = pad / 2;
174				let r = pad - l;
175				row.push(format!(" {:left$}{}{:right$} ", "", s, "", left = l, right = r));
176				sys_idx += 1;
177			}
178			if has_created_at {
179				let w = col_widths[sys_idx];
180				let s = frame.created_at[row_numberx].to_string();
181				let pad = w - Self::display_width(&s);
182				let l = pad / 2;
183				let r = pad - l;
184				row.push(format!(" {:left$}{}{:right$} ", "", s, "", left = l, right = r));
185				sys_idx += 1;
186			}
187			if has_updated_at {
188				let w = col_widths[sys_idx];
189				let s = frame.updated_at[row_numberx].to_string();
190				let pad = w - Self::display_width(&s);
191				let l = pad / 2;
192				let r = pad - l;
193				row.push(format!(" {:left$}{}{:right$} ", "", s, "", left = l, right = r));
194				sys_idx += 1;
195			}
196			let _ = sys_idx;
197
198			// Add regular column values
199			for (display_idx, &col_idx) in column_order.iter().enumerate() {
200				let col = &frame[col_idx];
201				let w = col_widths[row_num_col_idx + display_idx];
202				let s = Self::extract_string_value(col, row_numberx);
203				let pad = w - Self::display_width(&s);
204				let l = pad / 2;
205				let r = pad - l;
206				row.push(format!(" {:left$}{}{:right$} ", "", s, "", left = l, right = r));
207			}
208
209			writeln!(f, "|{}|", row.join("|"))?;
210		}
211
212		writeln!(f, "{}", sep)
213	}
214
215	/// Calculate the display width of a string, handling newlines properly.
216	/// For strings with newlines, returns the width of the longest line.
217	/// For strings without newlines, returns the unicode display width.
218	fn display_width(s: &str) -> usize {
219		if s.contains('\n') {
220			s.lines().map(|line| line.width()).max().unwrap_or(0)
221		} else {
222			s.width()
223		}
224	}
225
226	/// Escape newlines and tabs in a string for single-line display.
227	/// Replaces '\n' with "\\n" and '\t' with "\\t".
228	fn escape_control_chars(s: &str) -> String {
229		s.replace('\n', "\\n").replace('\t', "\\t")
230	}
231
232	/// Create a column display order (no special handling needed since encoded numbers are separate)
233	fn get_column_display_order(frame: &Frame) -> Vec<usize> {
234		(0..frame.len()).collect()
235	}
236
237	/// Extract string value from column at given encoded index, with proper escaping
238	fn extract_string_value(col: &FrameColumn, row_numberx: usize) -> String {
239		let s = col.data.as_string(row_numberx);
240		Self::escape_control_chars(&s)
241	}
242}