use crate::mode::OutputMode;
use crate::theme::Theme;
#[derive(Debug, Clone)]
pub struct SqlModelConsole {
mode: OutputMode,
theme: Theme,
plain_width: usize,
}
impl SqlModelConsole {
#[must_use]
pub fn new() -> Self {
Self {
mode: OutputMode::detect(),
theme: Theme::default(),
plain_width: 80,
}
}
#[must_use]
pub fn with_mode(mode: OutputMode) -> Self {
Self {
mode,
theme: Theme::default(),
plain_width: 80,
}
}
#[must_use]
pub fn with_theme(theme: Theme) -> Self {
Self {
mode: OutputMode::detect(),
theme,
plain_width: 80,
}
}
#[must_use]
pub fn theme(mut self, theme: Theme) -> Self {
self.theme = theme;
self
}
#[must_use]
pub fn plain_width(mut self, width: usize) -> Self {
self.plain_width = width;
self
}
#[must_use]
pub const fn mode(&self) -> OutputMode {
self.mode
}
#[must_use]
pub const fn get_theme(&self) -> &Theme {
&self.theme
}
#[must_use]
pub const fn get_plain_width(&self) -> usize {
self.plain_width
}
pub fn set_mode(&mut self, mode: OutputMode) {
self.mode = mode;
}
pub fn set_theme(&mut self, theme: Theme) {
self.theme = theme;
}
#[must_use]
pub fn is_rich(&self) -> bool {
self.mode == OutputMode::Rich
}
#[must_use]
pub fn is_plain(&self) -> bool {
self.mode == OutputMode::Plain
}
#[must_use]
pub fn is_json(&self) -> bool {
self.mode == OutputMode::Json
}
pub fn print(&self, message: &str) {
match self.mode {
OutputMode::Rich => {
println!("{}", strip_markup(message));
}
OutputMode::Plain => {
println!("{}", strip_markup(message));
}
OutputMode::Json => {
eprintln!("{}", strip_markup(message));
}
}
}
pub fn print_raw(&self, message: &str) {
println!("{message}");
}
pub fn status(&self, message: &str) {
match self.mode {
OutputMode::Rich => {
eprintln!("{}", strip_markup(message));
}
OutputMode::Plain | OutputMode::Json => {
eprintln!("{}", strip_markup(message));
}
}
}
pub fn success(&self, message: &str) {
self.print_styled_status(message, "green", "\u{2713}"); }
pub fn error(&self, message: &str) {
self.print_styled_status(message, "red bold", "\u{2717}"); }
pub fn warning(&self, message: &str) {
self.print_styled_status(message, "yellow", "\u{26A0}"); }
pub fn info(&self, message: &str) {
self.print_styled_status(message, "cyan", "\u{2139}"); }
fn print_styled_status(&self, message: &str, _style: &str, icon: &str) {
match self.mode {
OutputMode::Rich => {
eprintln!("{icon} {message}");
}
OutputMode::Plain => {
eprintln!("{message}");
}
OutputMode::Json => {
eprintln!("{icon} {message}");
}
}
}
pub fn rule(&self, title: Option<&str>) {
match self.mode {
OutputMode::Rich => {
self.plain_rule(title);
}
OutputMode::Plain | OutputMode::Json => {
self.plain_rule(title);
}
}
}
fn plain_rule(&self, title: Option<&str>) {
let width = self.plain_width;
match title {
Some(t) => {
let title_len = t.chars().count();
if title_len + 4 >= width {
eprintln!("-- {t} --");
} else {
let padding = (width - title_len - 2) / 2;
let left = "-".repeat(padding);
let right_padding = width - padding - title_len - 2;
let right = "-".repeat(right_padding);
eprintln!("{left} {t} {right}");
}
}
None => {
eprintln!("{}", "-".repeat(width));
}
}
}
pub fn print_json<T: serde::Serialize>(&self, value: &T) -> Result<(), serde_json::Error> {
let json = serde_json::to_string(value)?;
println!("{json}");
Ok(())
}
pub fn print_json_pretty<T: serde::Serialize>(
&self,
value: &T,
) -> Result<(), serde_json::Error> {
let json = serde_json::to_string_pretty(value)?;
match self.mode {
OutputMode::Rich => {
#[cfg(feature = "rich")]
{
println!("{json}");
return Ok(());
}
#[cfg(not(feature = "rich"))]
println!("{json}");
}
OutputMode::Plain | OutputMode::Json => {
println!("{json}");
}
}
Ok(())
}
pub fn newline(&self) {
println!();
}
pub fn newline_stderr(&self) {
eprintln!();
}
}
impl Default for SqlModelConsole {
fn default() -> Self {
Self::new()
}
}
#[must_use]
pub fn strip_markup(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let chars: Vec<char> = s.chars().collect();
let mut i = 0;
while i < chars.len() {
let c = chars[i];
if c == '[' {
let mut j = i + 1;
let mut found_close = false;
let mut close_idx = 0;
while j < chars.len() {
if chars[j] == ']' {
found_close = true;
close_idx = j;
break;
}
if chars[j] == '[' {
break;
}
j += 1;
}
if found_close {
let tag_content: String = chars[i + 1..close_idx].iter().collect();
let is_markup = is_rich_markup_tag(&tag_content);
if is_markup {
i = close_idx + 1;
continue;
}
}
result.push(c);
} else {
result.push(c);
}
i += 1;
}
result
}
#[must_use]
fn is_rich_markup_tag(tag_content: &str) -> bool {
if tag_content.starts_with('/') {
return true;
}
if tag_content.contains(' ') || tag_content.contains('=') {
return true;
}
let normalized = tag_content.to_ascii_lowercase();
matches!(
normalized.as_str(),
"bold"
| "dim"
| "italic"
| "underline"
| "strike"
| "blink"
| "reverse"
| "black"
| "red"
| "green"
| "yellow"
| "blue"
| "magenta"
| "cyan"
| "white"
| "default"
| "bright_black"
| "bright_red"
| "bright_green"
| "bright_yellow"
| "bright_blue"
| "bright_magenta"
| "bright_cyan"
| "bright_white"
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_strip_markup_basic() {
assert_eq!(strip_markup("[bold]text[/]"), "text");
assert_eq!(strip_markup("[red]hello[/]"), "hello");
}
#[test]
fn test_strip_markup_with_style() {
assert_eq!(strip_markup("[red on white]hello[/]"), "hello");
assert_eq!(strip_markup("[bold italic]styled[/]"), "styled");
}
#[test]
fn test_strip_markup_no_markup() {
assert_eq!(strip_markup("no markup"), "no markup");
assert_eq!(strip_markup("plain text"), "plain text");
}
#[test]
fn test_strip_markup_nested() {
assert_eq!(strip_markup("[bold][italic]nested[/][/]"), "nested");
assert_eq!(strip_markup("[red][bold][dim]deep[/][/][/]"), "deep");
}
#[test]
fn test_strip_markup_multiple() {
assert_eq!(
strip_markup("[bold]hello[/] [italic]world[/]"),
"hello world"
);
}
#[test]
fn test_strip_markup_preserves_brackets() {
assert_eq!(strip_markup("array[0]"), "array[0]");
assert_eq!(strip_markup("func(a[i])"), "func(a[i])");
assert_eq!(strip_markup("items[idx]"), "items[idx]");
assert_eq!(strip_markup("[idx] should stay"), "[idx] should stay");
}
#[test]
fn test_strip_markup_strips_known_single_tags() {
assert_eq!(strip_markup("[bold]x[/]"), "x");
assert_eq!(strip_markup("[red]x[/red]"), "x");
}
#[test]
fn test_strip_markup_empty() {
assert_eq!(strip_markup(""), "");
assert_eq!(strip_markup("[bold][/]"), "");
}
#[test]
fn test_console_creation() {
let console = SqlModelConsole::new();
assert!(matches!(
console.mode(),
OutputMode::Plain | OutputMode::Rich | OutputMode::Json
));
}
#[test]
fn test_with_mode() {
let console = SqlModelConsole::with_mode(OutputMode::Plain);
assert!(console.is_plain());
assert!(!console.is_rich());
assert!(!console.is_json());
let console = SqlModelConsole::with_mode(OutputMode::Rich);
assert!(console.is_rich());
assert!(!console.is_plain());
let console = SqlModelConsole::with_mode(OutputMode::Json);
assert!(console.is_json());
}
#[test]
fn test_with_theme() {
let light_theme = Theme::light();
let console = SqlModelConsole::with_theme(light_theme.clone());
assert_eq!(console.get_theme().success.rgb(), light_theme.success.rgb());
}
#[test]
fn test_builder_methods() {
let console = SqlModelConsole::new().plain_width(120);
assert_eq!(console.get_plain_width(), 120);
}
#[test]
fn test_set_mode() {
let mut console = SqlModelConsole::new();
console.set_mode(OutputMode::Json);
assert!(console.is_json());
}
#[test]
fn test_default() {
let console1 = SqlModelConsole::default();
let console2 = SqlModelConsole::new();
assert_eq!(console1.mode(), console2.mode());
}
#[test]
fn test_json_output() {
use serde::Serialize;
#[derive(Serialize)]
struct TestData {
name: String,
value: i32,
}
let console = SqlModelConsole::with_mode(OutputMode::Json);
let data = TestData {
name: "test".to_string(),
value: 42,
};
let result = console.print_json(&data);
assert!(result.is_ok());
}
#[test]
fn test_json_pretty_output() {
use serde::Serialize;
#[derive(Serialize)]
struct TestData {
items: Vec<i32>,
}
let console = SqlModelConsole::with_mode(OutputMode::Plain);
let data = TestData {
items: vec![1, 2, 3],
};
let result = console.print_json_pretty(&data);
assert!(result.is_ok());
}
}