diff --git a/src/main.rs b/src/main.rs index bde57f9..764139d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,7 +29,6 @@ use crate::langu::language_manager::format; use crate::omikron::omikron_connection::{OMIKRON_CONNECTION, OmikronConnection}; use crate::server::server::start; use crate::terms::consent_state; -use crate::terms::terms_checker; use crate::users::user_manager; use crate::util::config_util::CONFIG; use crate::util::file_util::has_dir; diff --git a/src/terms/consent_state.rs b/src/terms/consent_state.rs index 5ada255..fd8546e 100644 --- a/src/terms/consent_state.rs +++ b/src/terms/consent_state.rs @@ -1,6 +1,5 @@ use crate::{ - terms::terms_checker, - terms::terms_getter::get_current_docs, + terms::{doc::Doc, terms_checker, terms_getter::Type}, util::file_util::{load_file, save_file}, }; use std::time::{SystemTime, UNIX_EPOCH}; @@ -11,31 +10,19 @@ impl ConsentManager { let file = load_file("", "agreements"); let existing = ConsentState::from_str(&file).sanitize(); - let final_state = if existing.eula { + let final_state = if existing.accepted_eula { existing } else { - let choice = terms_checker::run_consent_ui().await; - let state = match choice { - UserChoice::Deny => ConsentState::denied(), - UserChoice::AcceptEULA => ConsentState { - eula: true, - tos: false, - pp: false, - }, - UserChoice::AcceptAll => ConsentState { - eula: true, - tos: true, - pp: true, - }, - }; - let state = state.sanitize(); - if let Ok(string) = state.to_string().await { - save_file("", "agreements", &string); - } + let state = terms_checker::run_consent_ui(existing).await.sanitize(); + let string = state.clone().to_string().await; + save_file("", "agreements", &string); state }; - (final_state.eula, final_state.pp && final_state.tos) + ( + final_state.accepted_eula, + final_state.accepted_pp && final_state.accepted_tos, + ) } } @@ -46,93 +33,123 @@ pub enum UserChoice { AcceptAll, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] pub struct ConsentState { - pub eula: bool, - pub tos: bool, - pub pp: bool, + pub eula: Option, + pub accepted_eula: bool, + pub tos: Option, + pub accepted_tos: bool, + pub pp: Option, + pub accepted_pp: bool, } impl ConsentState { - fn denied() -> Self { - Self { - eula: false, - pp: false, - tos: false, - } - } - fn sanitize(mut self) -> Self { - if !self.eula { - self.tos = false; - self.pp = false; + if !self.accepted_eula { + self.accepted_tos = false; + self.accepted_pp = false; } self } - async fn to_string(self) -> Result { + async fn to_string(self) -> String { let current_secs = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_secs(); - if let Some((eula, tos, pp)) = get_current_docs().await { - Ok(format!( - "\ - \"EULA=true\" indicates that you read, understood and accepted the End User Licence agreement. You can find our EULA at https://legal.tensamin.net/eula/\ + let mut file_out: String = format!( + "This file reflects the current consent state used by the application.\ + \nIt may be regenerated or overwritten by the application.\ + \nThis file was last edited by Tensamin at:\ + \nUNIX-SECOND={}", + current_secs + ); + + if self.accepted_eula + && let Some(eula) = self.eula + { + file_out.push_str(&format!("\ + \n\"EULA=true\" indicates that you read, understood and accepted Tensamin's End User Licence agreement. You can find our EULA at https://legal.tensamin.net/eula/\ \nEULA={}\ \nEULA-VERSION={}\ \nEULA-HASH={}\ - \n\"PrivacyPolicy=true\" indicates that you read, understood and accepted the Privacy Policy. You can find our Privacy Policy at https://legal.tensamin.net/privacy-policy/\ - \nPrivacyPolicy={}\ - \nPrivacyPolicy-VERSION={}\ - \nPrivacyPolicy-HASH={}\ - \n\"ToS=true\" indicates that you read, understood and accepted the Terms of Service. You can find our Terms of Service at https://legal.tensamin.net/terms-of-service/\ - \nToS={}\ - \nToS-VERSION={}\ - \nToS-HASH={}\ - \nThis file reflects the current consent state used by the application.\ - \nIt may be regenerated or overwritten by the application.\ - \nThis file was last edited by Tensamin at:\ - \nUNIX-SECOND={}\ - ", - self.eula, - eula.get_version(), - eula.get_hash(), - self.tos, - tos.get_version(), - tos.get_hash(), - self.pp, - pp.get_version(), - pp.get_hash(), - current_secs - )) + ", self.accepted_eula, eula.get_version(), eula.get_hash())); + + if self.accepted_tos + && let Some(tos) = self.tos + { + file_out.push_str(&format!("\ + \n\"ToS=true\" indicates that you read, understood and accepted Tensamin's Terms of Service. You can find our Terms of Serivce at https://legal.tensamin.net/tos/\ + \nToS={}\ + \nToS-VERSION={}\ + \nToS-HASH={}\ + ", self.accepted_tos, tos.get_version(), tos.get_hash())); + } + if self.accepted_pp + && let Some(pp) = self.pp + { + file_out.push_str(&format!("\ + \n\"Privacy-Policy=true\" indicates that you read, understood and accepted Tensamin's Privacy Policy. You can find our Privacy Policy at https://legal.tensamin.net/privacy/\ + \nPrivacy-Policy={}\ + \nPrivacy-Policy-VERSION={}\ + \nPrivacy-Policy-HASH={}\ + ", self.accepted_pp, pp.get_version(), pp.get_hash())); + } } else { - Err(()) + file_out.push_str("\ + \n\"EULA=true\" indicates that you read, understood and accepted Tensamin's End User Licence Agreement. You can find Tensamin's EULA at https://legal.tensamin.net/eula/\ + \nEULA=false\ + "); } + file_out } fn from_str(s: &str) -> Self { let mut eula = false; + let mut eula_version = String::new(); + let mut eula_hash = String::new(); let mut pp = false; + let mut pp_version = String::new(); + let mut pp_hash = String::new(); let mut tos = false; + let mut tos_version = String::new(); + let mut tos_hash = String::new(); + + let mut unix = String::new(); for line in s.lines() { if let Some(v) = line.strip_prefix("EULA=") { eula = v == "true"; + } else if let Some(v) = line.strip_prefix("EULA-VERSION=") { + eula_version = v.to_string(); + } else if let Some(v) = line.strip_prefix("EULA-HASH=") { + eula_hash = v.to_string(); } else if let Some(v) = line.strip_prefix("ToS=") { tos = v == "true"; + } else if let Some(v) = line.strip_prefix("ToS-VERSION=") { + tos_version = v.to_string(); + } else if let Some(v) = line.strip_prefix("ToS-HASH=") { + tos_hash = v.to_string(); } else if let Some(v) = line.strip_prefix("PrivacyPolicy=") { pp = v == "true"; + } else if let Some(v) = line.strip_prefix("PrivacyPolicy-VERSION=") { + pp_version = v.to_string(); + } else if let Some(v) = line.strip_prefix("PrivacyPolicy-HASH=") { + pp_hash = v.to_string(); + } else if let Some(v) = line.strip_prefix("UNIX=") { + unix = v.to_string(); } } - - Self { eula, pp, tos }.sanitize() - } - pub fn can_continue(&self) -> bool { - self.eula - } - pub fn can_continue_all(&self) -> bool { - self.eula && self.pp && self.tos + let unix: u64 = unix.parse::().unwrap_or(0); + Self { + accepted_eula: eula, + eula: Some(Doc::new(eula_version, eula_hash, Type::EULA, unix)), + accepted_pp: pp, + pp: Some(Doc::new(pp_version, pp_hash, Type::PP, unix)), + accepted_tos: tos, + tos: Some(Doc::new(tos_version, tos_hash, Type::TOS, unix)), + } + .sanitize() } } diff --git a/src/terms/doc.rs b/src/terms/doc.rs index f46872c..4a87603 100644 --- a/src/terms/doc.rs +++ b/src/terms/doc.rs @@ -2,7 +2,7 @@ use json::{JsonValue, object::Object}; use crate::{terms::terms_getter::Type, util::file_util::load_file}; -#[derive(Clone)] +#[derive(Clone, Debug)] #[allow(unused)] pub struct Doc { version: String, @@ -13,6 +13,15 @@ pub struct Doc { #[allow(dead_code)] impl Doc { + pub fn new(version: String, hash: String, doc_type: Type, timestamp: u64) -> Doc { + Doc { + version, + hash, + doc_type, + timestamp, + } + } + pub fn get_version(&self) -> String { self.version.clone() } diff --git a/src/terms/focus.rs b/src/terms/focus.rs index 3ac4140..f6691e4 100644 --- a/src/terms/focus.rs +++ b/src/terms/focus.rs @@ -1,5 +1,3 @@ -use crate::terms::consent_state::ConsentState; - #[derive(Debug, Clone, Copy, PartialEq)] pub enum Focus { Eula, @@ -10,20 +8,20 @@ pub enum Focus { ContinueAll, } impl Focus { - pub fn next(&mut self, state: ConsentState) { + pub fn next(&mut self, state: (bool, bool, bool)) { *self = match self { Focus::Eula => Focus::Tos, Focus::Tos => Focus::Pp, Focus::Pp => Focus::Cancel, Focus::Cancel => { - if state.can_continue() { + if state.0 { Focus::Continue } else { Focus::Eula } } Focus::Continue => { - if state.can_continue_all() { + if state.0 && state.1 && state.2 { Focus::ContinueAll } else { Focus::Eula @@ -33,12 +31,12 @@ impl Focus { }; } - pub fn prev(&mut self, state: ConsentState) { + pub fn prev(&mut self, state: (bool, bool, bool)) { *self = match self { Focus::Eula => { - if state.can_continue_all() { + if state.0 && state.1 && state.2 { Focus::ContinueAll - } else if state.can_continue() { + } else if state.0 { Focus::Continue } else { Focus::Cancel diff --git a/src/terms/mod.rs b/src/terms/mod.rs index 1c71e7e..b83d090 100644 --- a/src/terms/mod.rs +++ b/src/terms/mod.rs @@ -4,3 +4,4 @@ pub mod focus; pub mod md_viewer; pub mod terms_checker; pub mod terms_getter; +pub mod terms_updater; diff --git a/src/terms/terms_checker.rs b/src/terms/terms_checker.rs index 29d45d4..e78a267 100644 --- a/src/terms/terms_checker.rs +++ b/src/terms/terms_checker.rs @@ -13,14 +13,10 @@ use ratatui::{ }; use std::time::Duration; -pub async fn run_consent_ui() -> UserChoice { +pub async fn run_consent_ui(consent: ConsentState) -> ConsentState { let mut terminal = ratatui::init(); - let mut state = ConsentState { - eula: false, - tos: false, - pp: false, - }; + let (mut eula, mut tos, mut pp) = (false, false, false); let mut focus = Focus::Eula; let result = loop { @@ -129,9 +125,9 @@ pub async fn run_consent_ui() -> UserChoice { ) }; let mut text_lines = vec![ - checkbox(eula_text, state.eula, focus == Focus::Eula, true), - checkbox(tos_text, state.tos, focus == Focus::Tos, state.eula), - checkbox(pp_text, state.pp, focus == Focus::Pp, state.eula), + checkbox(eula_text, eula, focus == Focus::Eula, true), + checkbox(tos_text, tos, focus == Focus::Tos, eula), + checkbox(pp_text, pp, focus == Focus::Pp, eula), Line::from(""), Line::from("¹ Necessary, ² Optional"), ]; @@ -198,7 +194,7 @@ pub async fn run_consent_ui() -> UserChoice { .block(Block::default().title(" Tensamin User Consent [Q to Quit] ").borders(Borders::ALL)); f.render_widget(consent_block, chunks[0]); - draw_buttons(f, chunks[1], &state); + draw_buttons(f, chunks[1], focus, (eula, tos, pp)); }) .unwrap(); @@ -213,8 +209,8 @@ pub async fn run_consent_ui() -> UserChoice { } match key.code { KeyCode::Esc => break UserChoice::Deny, - KeyCode::Up | KeyCode::Left => focus.prev(state), - KeyCode::Down | KeyCode::Right | KeyCode::Tab => focus.next(state), + KeyCode::Up | KeyCode::Left => focus.prev((eula, tos, pp)), + KeyCode::Down | KeyCode::Right | KeyCode::Tab => focus.next((eula, tos, pp)), KeyCode::Char('q') | KeyCode::Char('Q') => break UserChoice::Deny, KeyCode::Char('o') | KeyCode::Char('O') => { let terms_type = match focus { @@ -248,26 +244,26 @@ pub async fn run_consent_ui() -> UserChoice { } KeyCode::Char(' ') => match focus { Focus::Eula => { - state.eula = !state.eula; - state.tos = false; - state.pp = false; + eula = !eula; + tos = false; + pp = false; } Focus::Tos => { - if state.eula { - state.tos = !state.tos + if eula { + tos = !tos } } Focus::Pp => { - if state.eula { - state.pp = !state.pp + if eula { + pp = !pp } } _ => {} }, KeyCode::Enter => match focus { Focus::Cancel => break UserChoice::Deny, - Focus::Continue if state.can_continue() => break UserChoice::AcceptEULA, - Focus::ContinueAll if state.can_continue_all() => { + Focus::Continue if eula => break UserChoice::AcceptEULA, + Focus::ContinueAll if eula && tos && pp => { break UserChoice::AcceptAll; } _ => {} @@ -279,7 +275,24 @@ pub async fn run_consent_ui() -> UserChoice { }; ratatui::restore(); - result + match result { + UserChoice::AcceptAll => { + consent.accepted_eula == true; + consent.accepted_tos == true; + consent.accepted_pp == true; + } + UserChoice::AcceptEULA => { + consent.accepted_eula == true; + consent.accepted_tos == true; + consent.accepted_pp == true; + } + UserChoice::Deny => { + consent.accepted_eula == false; + consent.accepted_tos == false; + consent.accepted_pp == false; + } + }; + consent } #[allow(mismatched_lifetime_syntaxes)] @@ -317,7 +330,12 @@ fn draw_button(f: &mut ratatui::Frame, area: Rect, label: &str, style: Style) { .block(Block::default().borders(Borders::ALL)); f.render_widget(p, area); } -fn draw_buttons(f: &mut ratatui::Frame, area: Rect, state: &ConsentState) { +fn draw_buttons( + f: &mut ratatui::Frame, + area: Rect, + current_focus: Focus, + state: (bool, bool, bool), +) { let buttons = vec![ ("[Q] Cancel", Focus::Cancel), ("Continue", Focus::Continue), @@ -343,9 +361,11 @@ fn draw_buttons(f: &mut ratatui::Frame, area: Rect, state: &ConsentState) { }; x += width; + let is_focused = current_focus == *focus; + let style = match focus { Focus::Cancel => { - if focus == &Focus::Cancel { + if is_focused { Style::default() .fg(Color::Black) .bg(Color::Red) @@ -354,30 +374,33 @@ fn draw_buttons(f: &mut ratatui::Frame, area: Rect, state: &ConsentState) { Style::default().fg(Color::Red) } } + Focus::Continue => { - if focus == &Focus::Continue && state.can_continue() { + if is_focused && state.0 { Style::default() .fg(Color::Black) .bg(Color::Green) .add_modifier(Modifier::BOLD) - } else if state.can_continue() { - Style::default().fg(Color::Green) + } else if state.0 { + Style::default().fg(Color::Green) // enabled but not focused } else { Style::default().fg(Color::DarkGray) } } + Focus::ContinueAll => { - if focus == &Focus::ContinueAll && state.can_continue_all() { + if is_focused && state.0 && state.1 && state.2 { Style::default() .fg(Color::Black) .bg(Color::Green) .add_modifier(Modifier::BOLD) - } else if state.can_continue_all() { + } else if state.0 && state.1 && state.2 { Style::default().fg(Color::Green) } else { Style::default().fg(Color::DarkGray) } } + _ => Style::default().fg(Color::DarkGray), }; diff --git a/src/terms/terms_getter.rs b/src/terms/terms_getter.rs index 4de8298..2439ca7 100644 --- a/src/terms/terms_getter.rs +++ b/src/terms/terms_getter.rs @@ -2,7 +2,7 @@ use json::JsonValue::Object; use crate::terms::doc::Doc; -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum Type { EULA, TOS, diff --git a/src/terms/terms_updater.rs b/src/terms/terms_updater.rs new file mode 100644 index 0000000..5a3b4ec --- /dev/null +++ b/src/terms/terms_updater.rs @@ -0,0 +1,422 @@ +use crate::terms::{ + consent_state::UserChoice, + focus::Focus, + md_viewer::FileViewer, + terms_getter::{Type, get_link, get_terms}, +}; +use crossterm::event::{self, Event, KeyCode}; +use ratatui::{ + layout::{Alignment, Constraint, Direction, Layout, Rect}, + style::{Color, Modifier, Style}, + text::{Line, Span, Text}, + widgets::{Block, Borders, Paragraph}, +}; +use std::time::Duration; + +pub async fn run_consent_ui() -> UserChoice { + let mut terminal = ratatui::init(); + + let (mut eula, mut tos, mut pp) = (false, false, false); + let mut focus = Focus::Eula; + + let result = loop { + let mut too_small = false; + terminal + .draw(|f| { + let mut needed_height = 5; + + let size = f.area(); + + if size.height < 6 + || size.width < 27 { + f.render_widget(Line::from(format!("too small {}/63 by {}/12", size.width, size.height)), size); + return; + } + + let max_width = 150; + let max_height = 20; + + let content_width = if max_width < size.width { max_width } else { size.width} ; + let content_height = if max_height < size.height { max_height } else { size.height} ; + + let horizontal_margin = (size.width.saturating_sub(content_width)) / 2; + let vertical_margin = (size.height.saturating_sub(content_height)) / 2; + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(7), Constraint::Length(3)]) + .split(Rect { + x: horizontal_margin, + y: vertical_margin, + width: content_width, + height: content_height, + }); + let eula_text = if size.width < 70 { + "EULA ¹ (https://legal.tensamin.net/eula/)" + } else { + "End User Licence Agreement ¹ (https://legal.tensamin.net/eula/)" + }; + let tos_text = if size.width < 72 { + "ToS ² (https://legal.tensamin.net/terms-of-service/)" + } else { + "Terms of Service ² (https://legal.tensamin.net/terms-of-service/)" + }; + let pp_text = if size.width < 68 { + "PP ² (https://legal.tensamin.net/privacy-policy/)" + } else { + "Privacy Policy ² (https://legal.tensamin.net/privacy-policy/)" + }; + + let (mut optional_lines, agree_lines): (Vec, Vec<&str>) = + if size.width > 143 { + ( + vec![7, 3, 7, 4], + vec![ + "", + "By selecting Continue, you confirm that you have read, understood and agree to the End User License Agreement and applicable Terms of Service.", + "", + "Tensamin services require acceptance of the Terms of Service and Privacy Policy.", + "", + "While having a document selected press O to view in this UI or press L to open as a link.", + ] + ) + } else if size.width > 92 { + ( + vec![8, 3, 8, 4], + vec![ + "", + "By selecting Continue, you confirm that you have read, understood and agree to the End User", + "License Agreement and applicable Terms of Service.", + "", + "Tensamin services require acceptance of the Terms of Service and Privacy Policy.", + "", + "While having a document selected press O to view in this UI or press L to open as a link.", + ] + ) + } else if size.width > 73 { + ( + vec![8, 3, 8, 4], + vec![ + "", + "By selecting Continue, you confirm that you have read, understood and", + "agree to the End User License Agreement and applicable Terms of Service.", + "", + "Tensamin services require acceptance of the ToS and Privacy Policy.", + "", + "While having a document selected press O to view in this UI or press L", + "to open as a link.", + ] + ) + } else { + ( + vec![9, 3, 10, 4], + vec![ + "", + "By selecting Continue, you confirm that you have", + "read, understood and agree to the End User License", + "Agreement and applicable Terms of Service.", + "", + "Tensamin services require acceptance of the", + "Terms of Service and Privacy Policy.", + "", + "While having a document selected press O to view", + "in this UI or press L to open as a link.", + ] + ) + }; + let mut text_lines = vec![ + checkbox(eula_text, eula, focus == Focus::Eula, true), + checkbox(tos_text, tos, focus == Focus::Tos, eula), + checkbox(pp_text, pp, focus == Focus::Pp, eula), + Line::from(""), + Line::from("¹ Necessary, ² Optional"), + ]; + + for line in agree_lines { + text_lines.insert(text_lines.len(), Line::from(line)); + } + + while size.height - 5 < text_lines.len() as u16 { + if optional_lines.len() == 0 { + break; + } + text_lines.remove(optional_lines[0] as usize); + optional_lines.remove(0); + } + needed_height += text_lines.len(); + + + + if size.width < 60 || size.height < needed_height as u16 { + let width_style = if size.width > 76 { + Style::default().fg(Color::Green) + } else if size.width >= 60 { + Style::default().fg(Color::Yellow) + } else { + Style::default().fg(Color::Red) + }; + + let height_style = if size.height < 12 { + Style::default().fg(Color::Red) + } else if size.height > 19 { + Style::default().fg(Color::Green) + } else { + Style::default().fg(Color::Yellow) + }; + + let warning_text = Text::from(vec![ + Line::from(vec![ + Span::raw("Width: "), + Span::styled(format!("{}", size.width), width_style), + Span::raw(" / 60"), + ]), + Line::from(vec![ + Span::raw("Height: "), + Span::styled(format!("{}", size.height), height_style), + Span::raw(format!(" / 12")), + ]), + ]); + + let warning = Paragraph::new(warning_text) + .alignment(Alignment::Center) + .block( + Block::default() + .borders(Borders::ALL) + .title("UI Too Small (Q to Quit)"), + ); + + too_small = true; + f.render_widget(warning, size); + return; + } + + let consent_block = Paragraph::new(Text::from(text_lines)) + .block(Block::default().title(" Tensamin User Consent [Q to Quit] ").borders(Borders::ALL)); + f.render_widget(consent_block, chunks[0]); + + draw_buttons(f, chunks[1], (eula, tos, pp)); + }) + .unwrap(); + + if event::poll(Duration::from_millis(200)).unwrap() { + if let Event::Key(key) = event::read().unwrap() { + if too_small { + if matches!(key.code, KeyCode::Char('q') | KeyCode::Char('Q')) { + break UserChoice::Deny; + } else { + continue; + } + } + match key.code { + KeyCode::Esc => break UserChoice::Deny, + KeyCode::Up | KeyCode::Left => focus.prev((eula, tos, pp)), + KeyCode::Down | KeyCode::Right | KeyCode::Tab => focus.next((eula, tos, pp)), + KeyCode::Char('q') | KeyCode::Char('Q') => break UserChoice::Deny, + KeyCode::Char('o') | KeyCode::Char('O') => { + let terms_type = match focus { + Focus::Eula => Type::EULA, + Focus::Tos => Type::TOS, + Focus::Pp => Type::PP, + _ => continue, + }; + + if let Some(eula) = get_terms(terms_type.clone()).await { + terminal = FileViewer::new(terms_type.to_string(), &eula) + .force_popup(terminal); + } else { + terminal = FileViewer::new( + terms_type.to_string(), + "### A loading error occured\ + \nTry reloading this site or retry in a moment.", + ) + .force_popup(terminal); + } + } + KeyCode::Char('l') | KeyCode::Char('L') => { + let terms_type = match focus { + Focus::Eula => Type::EULA, + Focus::Tos => Type::TOS, + Focus::Pp => Type::PP, + _ => continue, + }; + + let _ = open::that(get_link(terms_type)); + } + KeyCode::Char(' ') => match focus { + Focus::Eula => { + eula = !eula; + tos = false; + pp = false; + } + Focus::Tos => { + if eula { + tos = !tos + } + } + Focus::Pp => { + if eula { + pp = !pp + } + } + _ => {} + }, + KeyCode::Enter => match focus { + Focus::Cancel => break UserChoice::Deny, + Focus::Continue if eula => break UserChoice::AcceptEULA, + Focus::ContinueAll if eula && tos && pp => { + break UserChoice::AcceptAll; + } + _ => {} + }, + _ => {} + } + } + } + }; + ratatui::restore(); + + result +} + +#[allow(mismatched_lifetime_syntaxes)] +fn checkbox(label: &str, checked: bool, active: bool, allowed: bool) -> Line { + let box_char = if checked { "[x]" } else { "[ ]" }; + let (box_style, text_style) = if active { + if allowed { + ( + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ) + } else { + ( + Style::default().fg(Color::Gray), + Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), + ) + } + } else { + (Style::default(), Style::default()) + }; + Line::from(vec![ + Span::styled(box_char, box_style), + Span::raw(" "), + Span::styled(label, text_style), + ]) +} + +fn draw_button(f: &mut ratatui::Frame, area: Rect, label: &str, style: Style) { + let p = Paragraph::new(Span::styled(label, style)) + .alignment(Alignment::Center) + .block(Block::default().borders(Borders::ALL)); + f.render_widget(p, area); +} +fn draw_buttons(f: &mut ratatui::Frame, area: Rect, state: (bool, bool, bool)) { + let buttons = vec![ + ("[Q] Cancel", Focus::Cancel), + ("Continue", Focus::Continue), + ("Continue with Tensamin Services", Focus::ContinueAll), + ]; + + let padding = 2; + let min_widths: Vec = buttons + .iter() + .map(|(label, _)| label.len() as u16 + padding) + .collect(); + + let widths = compute_widths(area.width, &min_widths); + + let mut x = area.x; + + for ((label, focus), width) in buttons.iter().zip(widths) { + let chunk = Rect { + x, + y: area.y, + width, + height: area.height, + }; + x += width; + + let style = match focus { + Focus::Cancel => { + if focus == &Focus::Cancel { + Style::default() + .fg(Color::Black) + .bg(Color::Red) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(Color::Red) + } + } + Focus::Continue => { + if focus == &Focus::Continue && state.0 { + Style::default() + .fg(Color::Black) + .bg(Color::Green) + .add_modifier(Modifier::BOLD) + } else if state.0 && state.1 && state.2 { + Style::default().fg(Color::Green) + } else { + Style::default().fg(Color::DarkGray) + } + } + Focus::ContinueAll => { + if focus == &Focus::ContinueAll && state.0 && state.1 && state.2 { + Style::default() + .fg(Color::Black) + .bg(Color::Green) + .add_modifier(Modifier::BOLD) + } else if state.0 && state.1 && state.2 { + Style::default().fg(Color::Green) + } else { + Style::default().fg(Color::DarkGray) + } + } + _ => Style::default().fg(Color::DarkGray), + }; + + draw_button(f, chunk, label, style); + } +} + +fn compute_widths(area_width: u16, min_widths: &[u16]) -> Vec { + let mut widths = vec![0; min_widths.len()]; + let mut remaining: Vec = (0..min_widths.len()).collect(); + + let mut remaining_width = area_width; + + while !remaining.is_empty() { + let count = remaining.len() as u16; + let equal = remaining_width / count; + + let mut clamped = Vec::new(); + + for &i in &remaining { + if min_widths[i] > equal { + widths[i] = min_widths[i]; + remaining_width -= min_widths[i]; + clamped.push(i); + } + } + + if clamped.is_empty() { + let mut remainder = remaining_width % count; + for &i in &remaining { + widths[i] = equal + + if remainder > 0 { + remainder -= 1; + 1 + } else { + 0 + }; + } + break; + } + + remaining.retain(|i| !clamped.contains(i)); + } + + widths +}