pub mod block_options;
mod messages;
pub mod style_options;
pub mod table;
use crate::block_options::BlockOptions;
use crate::messages::Messages;
use crate::style_options::StyleOptions;
use crate::table::Table;
use crossterm::event::KeyModifiers;
use crossterm::style::{style, Print, PrintStyledContent};
use crossterm::{
event,
event::{read, Event, KeyCode},
queue,
style::{Color, ResetColor, SetBackgroundColor, SetForegroundColor, Stylize},
terminal,
terminal::{disable_raw_mode, enable_raw_mode},
};
use std::error::Error;
use std::io::{self, stdout, Write};
use std::time::Duration;
use textwrap::{fill, Options};
pub struct RusticPrint {}
impl RusticPrint {
pub fn new() -> RusticPrint {
RusticPrint {}
}
pub fn block<T>(&self, messages: T, block_options: BlockOptions) -> ()
where
T: Into<Messages>,
{
self.render_block(messages, block_options)
.expect("Failed to render b");
}
fn render_block<T>(&self, message: T, block_options: BlockOptions) -> Result<(), Box<dyn Error>>
where
T: Into<Messages>,
{
let message = message.into();
let mut stdout = stdout();
let term_width = terminal::size().unwrap_or((120, 0)).0 as usize;
let mut wrap_width = if term_width > 120 { 120 } else { term_width };
if cfg!(windows) {
wrap_width = wrap_width.saturating_sub(1);
}
queue!(stdout, Print("\n"))?;
let mut prefix = block_options.prefix.clone();
if prefix.is_empty() {
prefix = " ".to_string();
}
if block_options.padding {
print_padding_line(&mut stdout, wrap_width, &block_options, &prefix)?;
}
let block_type = block_options.block_type.clone().unwrap_or_default();
let initial_indent = if !block_type.is_empty() {
format!("{}[{}] ", prefix, block_type)
} else {
prefix.clone()
};
let subsequent_indent = format!(
"{}{}",
prefix,
" ".repeat(initial_indent.len().saturating_sub(prefix.len()))
);
let messages_vec: Vec<String> = match message {
Messages::Single(ref msg) => vec![msg.clone()],
Messages::Multiple(ref msgs) => msgs.clone(),
};
for (i, msg) in messages_vec.iter().enumerate() {
if i > 0 {
print_padding_line(&mut stdout, wrap_width, &block_options, &prefix)?;
}
let effective_options = if i == 0 {
Options::new(wrap_width)
.initial_indent(&initial_indent)
.subsequent_indent(&subsequent_indent)
} else {
Options::new(wrap_width)
.initial_indent(&subsequent_indent)
.subsequent_indent(&subsequent_indent)
};
for line in fill(msg, &effective_options).lines() {
styled_print_line(&mut stdout, line, wrap_width, &block_options)?;
}
}
if block_options.padding {
print_padding_line(&mut stdout, wrap_width, &block_options, &prefix)?;
}
queue!(stdout, Print("\n"))?;
stdout.flush()?;
Ok(())
}
fn render_underline_with_char(
&self,
message: &str,
underline_char: char,
style_options: Option<StyleOptions>,
) -> Result<(), Box<dyn Error>> {
let mut stdout = stdout();
let mut message = style(message);
let mut underline = style(underline_char.to_string().repeat(message.to_string().len()));
if let Some(style_options) = style_options {
if let Some(foreground) = style_options.foreground {
message = message.with(foreground);
underline = underline.with(foreground);
queue!(stdout, SetForegroundColor(foreground))?;
}
if let Some(background) = style_options.background {
message = message.on(background);
underline = underline.on(background);
queue!(stdout, SetBackgroundColor(background))?;
}
}
queue!(
stdout,
PrintStyledContent(message),
ResetColor,
Print("\n"),
PrintStyledContent(underline),
ResetColor,
Print("\n")
)?;
queue!(stdout, Print("\n"))?;
stdout.flush()?;
Ok(())
}
pub fn underline_with_char(
&self,
message: &str,
underline_char: char,
style_options: Option<StyleOptions>,
) {
self.render_underline_with_char(message, underline_char, style_options)
.expect("Failed to render underline");
}
pub fn title(&self, message: &str) {
self.underline_with_char(
message,
'=',
Some(StyleOptions {
foreground: Some(Color::DarkGreen),
background: None,
}),
);
}
pub fn section(&self, message: &str) {
self.underline_with_char(
message,
'-',
Some(StyleOptions {
foreground: Some(Color::DarkGreen),
background: None,
}),
);
}
pub fn success<T>(&self, messages: T)
where
T: Into<Messages>,
{
self.render_block(
messages,
BlockOptions {
style: Some(StyleOptions {
foreground: Some(Color::Black),
background: Some(Color::DarkGreen),
}),
block_type: Some("OK".to_string()),
padding: true,
..Default::default()
},
)
.expect("Failed to print success block");
}
pub fn caution<T>(&self, messages: T)
where
T: Into<Messages>,
{
self.render_block(
messages,
BlockOptions {
style: Some(StyleOptions {
foreground: Some(Color::Grey),
background: Some(Color::DarkRed),
}),
block_type: Some("CAUTION".to_string()),
prefix: " ! ".to_string(),
padding: true,
..Default::default()
},
)
.expect("Failed to print caution block");
}
pub fn error<T>(&self, messages: T)
where
T: Into<Messages>,
{
self.render_block(
messages,
BlockOptions {
style: Some(StyleOptions {
foreground: Some(Color::Grey),
background: Some(Color::DarkRed),
}),
block_type: Some("ERROR".to_string()),
prefix: " ".to_string(),
padding: true,
..Default::default()
},
)
.expect("Failed to print error block");
}
pub fn comment<T>(&self, messages: T)
where
T: Into<Messages>,
{
self.render_block(
messages,
BlockOptions {
prefix: " // ".to_string(),
..Default::default()
},
)
.expect("Failed to print comment block");
}
pub fn warning<T>(&self, messages: T)
where
T: Into<Messages>,
{
self.render_block(
messages,
BlockOptions {
style: Some(StyleOptions {
foreground: Some(Color::Black),
background: Some(Color::DarkYellow),
}),
block_type: Some("WARNING".to_string()),
padding: true,
..Default::default()
},
)
.expect("Failed to print comment block");
}
pub fn info<T>(&self, messages: T)
where
T: Into<Messages>,
{
self.render_block(
messages,
BlockOptions {
style: Some(StyleOptions {
foreground: Some(Color::Green),
background: None,
}),
block_type: Some("INFO".to_string()),
padding: true,
..Default::default()
},
)
.expect("Failed to print info block");
}
pub fn note<T>(&self, messages: T)
where
T: Into<Messages>,
{
self.render_block(
messages,
BlockOptions {
style: Some(StyleOptions {
foreground: Some(Color::DarkYellow),
background: None,
}),
block_type: Some("NOTE".to_string()),
prefix: " ! ".to_string(),
..Default::default()
},
)
.expect("Failed to print info block");
}
pub fn listing<T>(&self, items: Vec<T>)
where
T: std::fmt::Display,
{
let mut stdout = stdout();
for (_, item) in items.iter().enumerate() {
queue!(stdout, Print(format!("* {}\n", item))).expect("Failed to queue print");
}
stdout.flush().expect("Failed to flush stdout");
}
pub fn text<T>(&self, message: T)
where
T: std::fmt::Display,
{
let mut stdout = stdout();
let term_width = terminal::size().unwrap_or((120, 0)).0 as usize;
let options = Options::new(term_width.clone() - 1)
.initial_indent(" ")
.subsequent_indent(" ");
for line in fill(&*message.to_string(), options).lines() {
queue!(stdout, Print(line), Print("\n")).expect("Failed to queue print");
}
stdout.flush().expect("Failed to flush stdout");
}
pub fn table(&self, headers: Vec<&str>, rows: Vec<Vec<&str>>) {
let table = Table::new(headers, rows);
table.print_table();
}
pub fn confirm(&self, question: &str, default: bool) -> bool {
let mut stdout = io::stdout();
enable_raw_mode().expect("Failed to enable raw mode");
let default_answer = if default { "yes" } else { "no" };
print!(
"{} (yes/no) [{}]:\r\n > ",
question.green(),
default_answer.yellow()
);
stdout.flush().expect("Failed to flush stdout");
let mut input = String::new();
loop {
if let Event::Key(key_event) = read().expect("Failed to read event") {
match key_event.code {
KeyCode::Char(c) => {
print!("{}", c);
input.push(c);
}
KeyCode::Enter => {
println!();
break;
}
KeyCode::Backspace => {
if !input.is_empty() {
input.pop();
print!("\x08 \x08"); }
}
_ => {}
}
stdout.flush().expect("Failed to flush stdout");
}
}
disable_raw_mode().expect("Failed to disable raw mode");
println!();
if input.trim().is_empty() {
default
} else if input.trim().eq_ignore_ascii_case("yes") || input.trim().eq_ignore_ascii_case("y")
{
true
} else {
false
}
}
pub fn ask(
&self,
question: &str,
default: Option<&str>,
validator: Option<Box<dyn Fn(&str) -> Result<(), String>>>,
) -> String {
let mut stdout = io::stdout();
loop {
Self::ask_question(question, default);
stdout.flush().expect("Failed to flush stdout");
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.expect("Failed to read line");
let input = input.trim();
let answer = if input.is_empty() {
default.unwrap_or("").to_string()
} else {
input.to_string()
};
if let Some(ref validate) = validator {
match validate(&answer) {
Ok(_) => return answer,
Err(err) => {
println!("{}", err.red());
}
}
} else {
return answer;
}
}
}
fn ask_question(question: &str, default_text: Option<&str>) {
let default_text = if let Some(dt) = default_text {
format!(" [{}]", dt.dark_green())
} else {
String::new()
};
print!("{}{}:\n> ", question.dark_green(), default_text);
}
pub fn choice(&self, question: &str, choices: &[&str], default: Option<&str>) -> String {
loop {
let mut stdout = stdout();
if default.is_some() {
print!("{} [{}]:\n", question.green(), default.unwrap().green());
} else {
print!("{}:\n", question.green());
}
for (i, choice) in choices.iter().enumerate() {
println!(" [{}] {}", i.to_string().green(), choice);
}
print!("> ");
stdout.flush().unwrap();
let (_, prompt_row) = crossterm::cursor::position().unwrap();
enable_raw_mode().expect("Failed to enable raw mode");
let mut input_buffer = String::new();
let mut selected_index = choices
.iter()
.position(|&c| c == default.unwrap_or(""))
.unwrap_or(0);
if default.is_some() {
print!("{}", choices[selected_index]);
}
stdout.flush().unwrap();
loop {
if event::poll(Duration::from_millis(500)).unwrap() {
match event::read().unwrap() {
Event::Key(key_event) => {
match key_event.code {
KeyCode::Enter => break,
KeyCode::Tab => {
input_buffer = choices[selected_index].to_string();
}
KeyCode::Up => {
selected_index = if selected_index == 0 {
choices.len() - 1
} else {
selected_index - 1
};
input_buffer.clear();
}
KeyCode::Down => {
selected_index = (selected_index + 1) % choices.len();
input_buffer.clear();
}
KeyCode::Char(c) => {
if c == 'c'
&& key_event.modifiers.contains(KeyModifiers::CONTROL)
{
disable_raw_mode().unwrap();
std::process::exit(0);
}
input_buffer.push(c);
if let Ok(idx) = input_buffer.parse::<usize>() {
if idx < choices.len() {
selected_index = idx;
}
}
for (i, &choice) in choices.iter().enumerate() {
if choice
.to_lowercase()
.starts_with(&input_buffer.to_lowercase())
{
selected_index = i;
break;
}
}
}
KeyCode::Backspace => {
input_buffer.pop();
}
_ => {}
}
use crossterm::{
cursor::MoveTo,
queue,
style::{
Color, ResetColor, SetBackgroundColor, SetForegroundColor,
},
terminal::{Clear, ClearType},
};
queue!(
stdout,
MoveTo(2, prompt_row),
Clear(ClearType::UntilNewLine)
)
.unwrap();
if input_buffer.is_empty() {
print!("{}", choices[selected_index]);
} else {
let suggestion = choices[selected_index];
if suggestion
.to_lowercase()
.starts_with(&input_buffer.to_lowercase())
{
let remainder = &suggestion[input_buffer.len()..];
print!("{}", input_buffer);
queue!(
stdout,
SetForegroundColor(Color::White),
SetBackgroundColor(Color::Grey)
)
.unwrap();
print!("{}", remainder);
queue!(stdout, ResetColor).unwrap();
} else {
print!("{}", input_buffer);
}
}
stdout.flush().unwrap();
}
_ => {}
}
}
}
disable_raw_mode().expect("Failed to disable raw mode");
println!();
let final_choice = if input_buffer.is_empty() {
choices[selected_index]
} else if let Ok(idx) = input_buffer.parse::<usize>() {
if idx < choices.len() {
choices[idx]
} else {
""
}
} else {
let mut found = "";
for &choice in choices {
if choice.to_lowercase() == input_buffer.to_lowercase() {
found = choice;
break;
}
}
found
};
if final_choice.is_empty() {
self.error(format!(
"Invalid selection: \"{}\". Please enter a valid index or choice.",
input_buffer
));
continue;
} else if selected_index > choices.len() {
self.error(format!(
"Invalid selection: \"{}\". Please enter a valid index or choice.",
input_buffer
));
continue;
} else {
return final_choice.to_string();
}
}
}
}
fn print_padding_line(
stdout: &mut impl Write,
wrap_width: usize,
block_options: &BlockOptions,
prefix: &str,
) -> Result<(), Box<dyn Error>> {
let line = if wrap_width > prefix.len() {
format!("{}{}", prefix, " ".repeat(wrap_width - prefix.len()))
} else {
prefix.to_string()
};
if let Some(style_cfg) = &block_options.style {
let mut styled_line = style(line);
if let Some(bg) = style_cfg.background {
styled_line = styled_line.on(bg);
}
if let Some(fg) = style_cfg.foreground {
styled_line = styled_line.with(fg);
}
queue!(
stdout,
PrintStyledContent(styled_line),
ResetColor,
Print("\r\n")
)?;
return Ok(());
}
queue!(stdout, Print(line), Print("\r\n"))?;
Ok(())
}
fn styled_print_line(
stdout: &mut impl Write,
line: &str,
wrap_width: usize,
block_options: &BlockOptions,
) -> Result<(), Box<dyn Error>> {
let end_padding = " ".repeat(wrap_width.saturating_sub(line.len()));
let mut styled = style(format!("{}{}", line, end_padding));
if let Some(style_cfg) = &block_options.style {
if let Some(bg) = style_cfg.background {
styled = styled.on(bg);
}
if let Some(fg) = style_cfg.foreground {
styled = styled.with(fg);
}
}
queue!(stdout, PrintStyledContent(styled), ResetColor, Print("\n"))?;
Ok(())
}