#![allow(clippy::manual_let_else)]
#![allow(clippy::redundant_closure_for_method_calls)]
use richrs::color::Color;
use richrs::console::{Console, StringWriter};
use richrs::emoji::Emoji;
use richrs::panel::Panel;
use richrs::rule::Rule;
use richrs::style::Style;
use richrs::table::{Column, Table};
use richrs::text::Text;
use richrs::tree::{Tree, TreeNode};
use serde::{Deserialize, Serialize};
use std::io::{self, BufRead, Write};
#[derive(Debug, Deserialize)]
struct TestRequest {
test: String,
data: serde_json::Value,
}
#[derive(Debug, Serialize)]
struct TestResponse {
success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
output: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
error: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
data: Option<serde_json::Value>,
}
#[derive(Debug, Serialize)]
struct StyleData {
input: String,
bold: Option<bool>,
italic: Option<bool>,
underline: Option<bool>,
strike: Option<bool>,
dim: Option<bool>,
reverse: Option<bool>,
blink: Option<bool>,
conceal: Option<bool>,
color: Option<String>,
bgcolor: Option<String>,
}
impl StyleData {
fn from_style(input: &str, style: &Style) -> Self {
Self {
input: input.to_owned(),
bold: style.attributes.bold,
italic: style.attributes.italic,
underline: style.attributes.underline,
strike: style.attributes.strike,
dim: style.attributes.dim,
reverse: style.attributes.reverse,
blink: style.attributes.blink,
conceal: style.attributes.conceal,
color: style.color.as_ref().map(color_to_string),
bgcolor: style.bgcolor.as_ref().map(color_to_string),
}
}
}
fn color_to_string(color: &Color) -> String {
match *color {
Color::Default => "default".to_owned(),
Color::Standard(std) => format!("{std:?}").to_lowercase(),
Color::Palette(idx) => format!("color({idx})"),
Color::Rgb { r, g, b } => format!("#{r:02x}{g:02x}{b:02x}"),
_ => "unknown".to_owned(),
}
}
fn run_style_parse_test(data: &serde_json::Value) -> TestResponse {
let Some(style_str) = data.get("style").and_then(|v| v.as_str()) else {
return TestResponse {
success: false,
output: None,
error: Some("missing 'style' field in test data".to_owned()),
data: None,
};
};
match Style::parse(style_str) {
Ok(style) => {
let style_data = StyleData::from_style(style_str, &style);
TestResponse {
success: true,
output: None,
error: None,
data: serde_json::to_value(style_data).ok(),
}
}
Err(e) => TestResponse {
success: false,
output: None,
error: Some(format!("{e}")),
data: None,
},
}
}
fn run_style_render_test(data: &serde_json::Value) -> TestResponse {
let style_str = match data.get("style").and_then(|v| v.as_str()) {
Some(s) => s,
None => {
return TestResponse {
success: false,
output: None,
error: Some("missing 'style' field in test data".to_owned()),
data: None,
};
}
};
let text_str = data.get("text").and_then(|v| v.as_str()).unwrap_or("test");
match Style::parse(style_str) {
Ok(style) => {
let ansi = style.to_ansi();
let reset = style.to_ansi_reset();
let output = format!("{ansi}{text_str}{reset}");
TestResponse {
success: true,
output: Some(output),
error: None,
data: None,
}
}
Err(e) => TestResponse {
success: false,
output: None,
error: Some(format!("{e}")),
data: None,
},
}
}
fn run_text_render_test(data: &serde_json::Value) -> TestResponse {
let text_str = match data.get("text").and_then(|v| v.as_str()) {
Some(s) => s,
None => {
return TestResponse {
success: false,
output: None,
error: Some("missing 'text' field in test data".to_owned()),
data: None,
};
}
};
let style_str = data.get("style").and_then(|v| v.as_str());
let text = if let Some(style_str) = style_str {
match Style::parse(style_str) {
Ok(style) => Text::styled(text_str, style),
Err(e) => {
return TestResponse {
success: false,
output: None,
error: Some(format!("{e}")),
data: None,
};
}
}
} else {
Text::from_str(text_str)
};
let _width = data.get("width").and_then(|v| v.as_u64()).unwrap_or(80);
let mut output = String::new();
for segment in text.to_segments() {
output.push_str(&segment.to_ansi());
}
TestResponse {
success: true,
output: Some(output),
error: None,
data: None,
}
}
fn run_panel_render_test(data: &serde_json::Value) -> TestResponse {
let content = match data.get("content").and_then(|v| v.as_str()) {
Some(s) => s,
None => {
return TestResponse {
success: false,
output: None,
error: Some("missing 'content' field in test data".to_owned()),
data: None,
};
}
};
let title = data.get("title").and_then(|v| v.as_str());
let subtitle = data.get("subtitle").and_then(|v| v.as_str());
let width = data.get("width").and_then(|v| v.as_u64()).unwrap_or(80);
let width = usize::try_from(width).unwrap_or(80);
let text = Text::from_str(content);
let mut panel = Panel::new(text);
if let Some(title) = title {
panel = panel.title(title);
}
if let Some(subtitle) = subtitle {
panel = panel.subtitle(subtitle);
}
let segments = panel.render(width);
let mut output = String::new();
for segment in segments {
output.push_str(&segment.to_ansi());
}
TestResponse {
success: true,
output: Some(output),
error: None,
data: None,
}
}
fn run_console_render_test(data: &serde_json::Value) -> TestResponse {
let markup = match data.get("markup").and_then(|v| v.as_str()) {
Some(s) => s,
None => {
return TestResponse {
success: false,
output: None,
error: Some("missing 'markup' field in test data".to_owned()),
data: None,
};
}
};
let width = data.get("width").and_then(|v| v.as_u64()).unwrap_or(80);
let width = usize::try_from(width).unwrap_or(80);
let writer = StringWriter::new();
let mut console = Console::with_writer(writer);
console.set_width(width);
match console.print(markup) {
Ok(()) => {
let output = render_markup_to_string(markup, width);
TestResponse {
success: true,
output: Some(output),
error: None,
data: None,
}
}
Err(e) => TestResponse {
success: false,
output: None,
error: Some(format!("{e}")),
data: None,
},
}
}
fn render_markup_to_string(markup: &str, _width: usize) -> String {
use richrs::markup::Markup;
match Markup::parse(markup) {
Ok(parsed) => {
let text = parsed.to_text();
let mut output = String::new();
for segment in text.to_segments() {
output.push_str(&segment.to_ansi());
}
output
}
Err(_) => markup.to_owned(),
}
}
fn run_color_parse_test(data: &serde_json::Value) -> TestResponse {
let color_str = match data.get("color").and_then(|v| v.as_str()) {
Some(s) => s,
None => {
return TestResponse {
success: false,
output: None,
error: Some("missing 'color' field in test data".to_owned()),
data: None,
};
}
};
match Color::parse(color_str) {
Ok(color) => {
let color_data = serde_json::json!({
"input": color_str,
"display": color.to_string(),
"ansi_fg": color.to_ansi_fg(),
"ansi_bg": color.to_ansi_bg(),
});
TestResponse {
success: true,
output: None,
error: None,
data: Some(color_data),
}
}
Err(e) => TestResponse {
success: false,
output: None,
error: Some(format!("{e}")),
data: None,
},
}
}
fn run_emoji_lookup_test(data: &serde_json::Value) -> TestResponse {
let name = match data.get("name").and_then(|v| v.as_str()) {
Some(s) => s,
None => {
return TestResponse {
success: false,
output: None,
error: Some("missing 'name' field in test data".to_owned()),
data: None,
};
}
};
match Emoji::new(name) {
Ok(emoji) => TestResponse {
success: true,
output: Some(emoji.to_string()),
error: None,
data: None,
},
Err(e) => TestResponse {
success: false,
output: None,
error: Some(format!("{e}")),
data: None,
},
}
}
fn run_table_render_test(data: &serde_json::Value) -> TestResponse {
let columns = match data.get("columns").and_then(|v| v.as_array()) {
Some(cols) => cols.iter().filter_map(|c| c.as_str()).collect::<Vec<_>>(),
None => {
return TestResponse {
success: false,
output: None,
error: Some("missing 'columns' field in test data".to_owned()),
data: None,
};
}
};
let rows = match data.get("rows").and_then(|v| v.as_array()) {
Some(r) => r,
None => {
return TestResponse {
success: false,
output: None,
error: Some("missing 'rows' field in test data".to_owned()),
data: None,
};
}
};
let width = data.get("width").and_then(|v| v.as_u64()).unwrap_or(80);
let width = usize::try_from(width).unwrap_or(80);
let mut table = Table::new();
for col_name in &columns {
table.add_column(Column::new(*col_name));
}
for row in rows {
if let Some(cells) = row.as_array() {
let cell_strs: Vec<&str> = cells.iter().filter_map(|c| c.as_str()).collect();
table.add_row_cells(cell_strs);
}
}
let segments = table.render(width);
let mut output = String::new();
for segment in segments {
output.push_str(&segment.to_ansi());
}
TestResponse {
success: true,
output: Some(output),
error: None,
data: None,
}
}
fn run_rule_render_test(data: &serde_json::Value) -> TestResponse {
let title = data.get("title").and_then(|v| v.as_str());
let width = data.get("width").and_then(|v| v.as_u64()).unwrap_or(80);
let width = usize::try_from(width).unwrap_or(80);
let rule = if let Some(title) = title {
Rule::with_title(title)
} else {
Rule::new()
};
let segments = rule.render(width);
let mut output = String::new();
for segment in segments {
output.push_str(&segment.to_ansi());
}
TestResponse {
success: true,
output: Some(output),
error: None,
data: None,
}
}
fn run_tree_render_test(data: &serde_json::Value) -> TestResponse {
let label = data.get("label").and_then(|v| v.as_str()).unwrap_or("root");
let children = data.get("children").and_then(|v| v.as_array());
let mut tree = Tree::new(label);
if let Some(children) = children {
for child in children {
if let Some(child_label) = child.as_str() {
tree.add(TreeNode::new(child_label));
}
}
}
let segments = tree.render();
let mut output = String::new();
for segment in segments {
output.push_str(&segment.to_ansi());
}
TestResponse {
success: true,
output: Some(output),
error: None,
data: None,
}
}
fn run_test(request: &TestRequest) -> TestResponse {
match request.test.as_str() {
"style_parse" => run_style_parse_test(&request.data),
"style_render" => run_style_render_test(&request.data),
"text_render" => run_text_render_test(&request.data),
"panel_render" => run_panel_render_test(&request.data),
"console_render" => run_console_render_test(&request.data),
"color_parse" => run_color_parse_test(&request.data),
"emoji_lookup" => run_emoji_lookup_test(&request.data),
"table_render" => run_table_render_test(&request.data),
"rule_render" => run_rule_render_test(&request.data),
"tree_render" => run_tree_render_test(&request.data),
_ => TestResponse {
success: false,
output: None,
error: Some(format!("unknown test: '{}'", request.test)),
data: None,
},
}
}
fn main() {
let stdin = io::stdin();
let stdout = io::stdout();
let mut stdout = stdout.lock();
for line in stdin.lock().lines() {
let line = match line {
Ok(l) => l,
Err(e) => {
let response = TestResponse {
success: false,
output: None,
error: Some(format!("failed to read input: {e}")),
data: None,
};
if let Ok(json) = serde_json::to_string(&response) {
let _ = writeln!(stdout, "{json}");
}
continue;
}
};
if line.trim().is_empty() {
continue;
}
let request: TestRequest = match serde_json::from_str(&line) {
Ok(r) => r,
Err(e) => {
let response = TestResponse {
success: false,
output: None,
error: Some(format!("failed to parse request: {e}")),
data: None,
};
if let Ok(json) = serde_json::to_string(&response) {
let _ = writeln!(stdout, "{json}");
}
continue;
}
};
let response = run_test(&request);
if let Ok(json) = serde_json::to_string(&response) {
let _ = writeln!(stdout, "{json}");
}
let _ = stdout.flush();
}
}