use rusty_bubbles::textinput;
use rusty_bubbletea::model::Model as ModelTrait;
use rusty_bubbletea::{batch, quit, Cmd, KeyPressMsg, Msg, Program, View};
use rusty_lipgloss::{new_style, Color, Style};
const TANG: &str = "#FF985A";
const ANCHOVY: &str = "#719AFC";
const GUAC: &str = "#12C78F";
const CHERRY: &str = "#FF388B";
struct ErrMsg(String);
fn input_style() -> Style {
new_style().foreground_color(Color::parse(TANG))
}
fn continue_style() -> Style {
new_style().foreground_color(Color::parse(ANCHOVY))
}
fn valid_style() -> Style {
new_style().foreground_color(Color::parse(GUAC))
}
fn err_style() -> Style {
new_style().foreground_color(Color::parse(CHERRY))
}
struct Model {
isbn_input: textinput::Model,
title_input: textinput::Model,
focused_input: usize,
err: Option<String>,
}
impl Model {
fn can_find_book(&self) -> bool {
let correct_isbn_given =
self.isbn_input.err.is_none() && !self.isbn_input.value().is_empty();
let correct_title_given =
self.title_input.err.is_none() && !self.title_input.value().is_empty();
correct_isbn_given && correct_title_given
}
fn initial_model() -> Self {
let mut isbn_input = textinput::new();
let _ = isbn_input.focus();
isbn_input.placeholder = "978-X-XXX-XXXXX-X".to_string();
isbn_input.char_limit = 17;
isbn_input.set_width(30);
isbn_input.prompt = String::new();
isbn_input.validate = Some(Box::new(isbn13_validator));
let mut title_input = textinput::new();
title_input.blur();
title_input.placeholder = "Title".to_string();
title_input.char_limit = 100;
title_input.set_width(100);
title_input.prompt = String::new();
title_input.validate = Some(Box::new(book_title_validator));
Model {
isbn_input,
title_input,
focused_input: 0,
err: None,
}
}
}
fn isbn13_validator(s: &str) -> Result<(), String> {
let s = s.replace('-', "");
if s.len() != 13 {
return Err("ISBN is of wrong length".to_string());
}
if !s.chars().all(|c| c.is_ascii_digit()) {
return Err("ISBN contains invalid characters".to_string());
}
let gs1_prefix = &s[..3];
if gs1_prefix != "978" && gs1_prefix != "979" {
return Err("ISBN has invalid GS1 prefix".to_string());
}
let sum: usize = s
.chars()
.enumerate()
.map(|(i, c)| {
let mut n = c.to_digit(10).unwrap() as usize;
if i % 2 != 0 {
n *= 3;
}
n
})
.sum();
if !sum.is_multiple_of(10) {
return Err("ISBN has invalid check digit".to_string());
}
Ok(())
}
const BANNED_TITLE_WORDS: &[&str] = &[
"very", "bad", "words", "that", "should", "not", "appear", "in", "book", "titles",
];
fn book_title_validator(s: &str) -> Result<(), String> {
let s = s.trim();
if s.is_empty() {
return Err("Book title is empty".to_string());
}
for banned_word in BANNED_TITLE_WORDS {
if s.contains(banned_word) {
return Err(format!("Book title contains banned word {:?}", banned_word));
}
}
Ok(())
}
impl ModelTrait for Model {
fn init(&self) -> Cmd {
Some(Box::new(|| Some(textinput::blink())))
}
fn update(&mut self, msg: &dyn Msg) -> Cmd {
if let Some(k) = msg.as_any().downcast_ref::<KeyPressMsg>() {
match k.0.to_string().as_str() {
"up" | "down" => {
match self.focused_input {
0 => {
self.focused_input = 1;
let _ = self.title_input.focus();
self.isbn_input.blur();
}
_ => {
self.focused_input = 0;
let _ = self.isbn_input.focus();
self.title_input.blur();
}
}
}
"enter" => {
if self.can_find_book() {
return quit();
}
}
"ctrl+c" | "esc" => return quit(),
_ => {}
}
}
if let Some(err) = msg.as_any().downcast_ref::<ErrMsg>() {
self.err = Some(err.0.clone());
return None;
}
let isbn_command = self.isbn_input.update(msg);
let title_command = self.title_input.update(msg);
batch(vec![isbn_command, title_command])
}
fn view(&self) -> View {
let mut continue_text = String::new();
if self.can_find_book() {
continue_text = continue_style().render("Find ->");
}
let mut isbn_error_text = String::new();
if !self.isbn_input.value().is_empty() {
if let Some(err) = &self.isbn_input.err {
isbn_error_text = err_style().render(err);
} else {
isbn_error_text = valid_style().render("Valid ISBN");
}
}
let mut title_error_text = String::new();
if !self.title_input.value().is_empty() {
if let Some(err) = &self.title_input.err {
title_error_text = err_style().render(err);
} else {
title_error_text = valid_style().render("Valid title");
}
}
View::new(
&(format!(
" Search book:\n {} \n {} \n {} \n\n {} \n {} \n {} \n\n {} \n",
input_style().width(30).render("ISBN"),
self.isbn_input.view(),
isbn_error_text,
input_style().width(30).render("Title"),
self.title_input.view(),
title_error_text,
continue_text,
)),
)
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let p = Program::new(Model::initial_model());
p.run()?;
Ok(())
}