[WIP] Terms update support

This commit is contained in:
Alex-Emmet 2026-02-06 22:47:42 +01:00
commit b81276dd18
6 changed files with 251 additions and 91 deletions

View file

@ -3,41 +3,71 @@ use crate::{
doc::Doc, doc::Doc,
terms_checker, terms_checker,
terms_getter::{Type, get_current_docs, get_newest_docs}, terms_getter::{Type, get_current_docs, get_newest_docs},
terms_updater,
}, },
util::file_util::load_file, util::file_util::{load_file, save_file},
}; };
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use tokio::task;
pub struct ConsentManager; pub struct ConsentManager;
impl ConsentManager { impl ConsentManager {
pub async fn check() -> (bool, bool) { pub async fn check() -> (bool, bool) {
let file = load_file("", "agreements"); let file = load_file("", "agreements");
let file_state = ConsentState::from_str(&file).sanitize(); let mut file_state = ConsentState::from_str(&file).sanitize();
let accepted_state = if !&file_state.accepted_eula {
terms_checker::run_consent_ui(file_state).await if !file_state.accepted_eula {
} else { file_state = terms_checker::run_consent_ui(file_state).await;
file_state
println!("{}", &file_state.clone().sanitize().to_string());
println!("-----------------------");
save_file("", "agreements", &file_state.to_string());
}
if !file_state.accepted_eula {
return (false, false);
}
if let Some((eula_update, tos_update, privacy_update)) = Self::get_updates().await {
let is_forced = eula_update.is_err() || tos_update.is_err() || privacy_update.is_err();
let updater_logic = async move {
let state_to_update = terms_updater::run_consent_ui(
file_state,
eula_update,
tos_update,
privacy_update,
)
.await;
println!("{}", &state_to_update.clone().sanitize().to_string());
save_file("", "agreements", &state_to_update.sanitize().to_string());
}; };
if let Some((current_eula, current_tos, current_privacy)) = get_current_docs().await { if is_forced {
if current_eula.equals_some(&accepted_state.eula) { updater_logic.await;
if current_tos.equals_some(&accepted_state.tos)
&& current_privacy.equals_some(&accepted_state.pp)
{
(true, true)
} else { } else {
(true, false) task::spawn(updater_logic);
}
} else {
(false, false)
}
} else {
println!("There was an error while loading our EULA, please retry later!");
(false, false)
} }
} }
pub async fn check_updates() -> Option<(Option<Doc>, Option<Doc>, Option<Doc>)> { let final_state = ConsentState::from_str(&load_file("", "agreements")).sanitize();
(
final_state.accepted_eula,
final_state.accepted_tos && final_state.accepted_pp,
)
}
async fn get_updates() -> Option<(
// Ok(None) indicates no update
// Ok(Some) Indicates a future update
// Err indicates a update that has to be accepted before the programm can continue
Result<Option<Doc>, Doc>,
Result<Option<Doc>, Doc>,
Result<Option<Doc>, Doc>,
)> {
let file = load_file("", "agreements"); let file = load_file("", "agreements");
let accepted_state = ConsentState::from_str(&file).sanitize(); let accepted_state = ConsentState::from_str(&file).sanitize();
@ -46,7 +76,48 @@ impl ConsentManager {
Some((newest_eula, newest_tos, newest_privacy)), Some((newest_eula, newest_tos, newest_privacy)),
) = (get_current_docs().await, get_newest_docs().await) ) = (get_current_docs().await, get_newest_docs().await)
{ {
None let eula_update: Result<Option<Doc>, Doc> =
if current_eula.equals_some(&accepted_state.eula) {
if current_eula.equals(&newest_eula) {
Ok(None)
} else {
Ok(Some(newest_eula))
}
} else if newest_eula.equals_some(&accepted_state.eula) {
Ok(None)
} else {
Err(current_eula)
};
let tos_update: Result<Option<Doc>, Doc> = if newest_tos
.equals_some(&accepted_state.tos)
{
Ok(None)
} else if accepted_state.accepted_tos && current_tos.equals_some(&accepted_state.tos) {
if current_tos.equals(&newest_tos) {
Ok(None)
} else {
Ok(Some(newest_tos))
}
} else {
Err(current_tos)
};
let privacy_update: Result<Option<Doc>, Doc> =
if newest_privacy.equals_some(&accepted_state.privacy) {
Ok(None)
} else if accepted_state.accepted_pp
&& current_privacy.equals_some(&accepted_state.privacy)
{
if current_privacy.equals(&newest_privacy) {
Ok(None)
} else {
Ok(Some(newest_privacy))
}
} else {
Err(current_privacy)
};
Some((eula_update, tos_update, privacy_update))
} else { } else {
None None
} }
@ -66,7 +137,7 @@ pub struct ConsentState {
pub accepted_eula: bool, pub accepted_eula: bool,
pub tos: Option<Doc>, pub tos: Option<Doc>,
pub accepted_tos: bool, pub accepted_tos: bool,
pub pp: Option<Doc>, pub privacy: Option<Doc>,
pub accepted_pp: bool, pub accepted_pp: bool,
} }
@ -79,7 +150,7 @@ impl ConsentState {
self self
} }
async fn to_string(self) -> String { pub fn to_string(&self) -> String {
let current_secs = SystemTime::now() let current_secs = SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
.unwrap() .unwrap()
@ -93,9 +164,13 @@ impl ConsentState {
current_secs current_secs
); );
if self.accepted_eula if let Some(eula) = &self.eula {
&& let Some(eula) = self.eula println!(
{ "EULA: {}, {}, {}",
self.accepted_eula,
eula.get_version(),
eula.get_hash()
);
file_out.push_str(&format!("\ 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/\ \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={}\
@ -104,17 +179,17 @@ impl ConsentState {
", self.accepted_eula, eula.get_version(), eula.get_hash())); ", self.accepted_eula, eula.get_version(), eula.get_hash()));
if self.accepted_tos if self.accepted_tos
&& let Some(tos) = self.tos && let Some(tos) = &self.tos
{ {
file_out.push_str(&format!("\ 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/\ \n\"Terms-of-Service=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={}\ \nTerms-of-Service={}\
\nToS-VERSION={}\ \nTerms-of-Service-VERSION={}\
\nToS-HASH={}\ \nTerms-of-Service-HASH={}\
", self.accepted_tos, tos.get_version(), tos.get_hash())); ", self.accepted_tos, tos.get_version(), tos.get_hash()));
} }
if self.accepted_pp if self.accepted_pp
&& let Some(pp) = self.pp && let Some(pp) = &self.privacy
{ {
file_out.push_str(&format!("\ 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/\ \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/\
@ -152,17 +227,17 @@ impl ConsentState {
eula_version = v.to_string(); eula_version = v.to_string();
} else if let Some(v) = line.strip_prefix("EULA-HASH=") { } else if let Some(v) = line.strip_prefix("EULA-HASH=") {
eula_hash = v.to_string(); eula_hash = v.to_string();
} else if let Some(v) = line.strip_prefix("ToS=") { } else if let Some(v) = line.strip_prefix("Terms-of-Service=") {
tos = v == "true"; tos = v == "true";
} else if let Some(v) = line.strip_prefix("ToS-VERSION=") { } else if let Some(v) = line.strip_prefix("Terms-of-Service-VERSION=") {
tos_version = v.to_string(); tos_version = v.to_string();
} else if let Some(v) = line.strip_prefix("ToS-HASH=") { } else if let Some(v) = line.strip_prefix("Terms-of-Service-HASH=") {
tos_hash = v.to_string(); tos_hash = v.to_string();
} else if let Some(v) = line.strip_prefix("PrivacyPolicy=") { } else if let Some(v) = line.strip_prefix("Privacy-Policy=") {
pp = v == "true"; pp = v == "true";
} else if let Some(v) = line.strip_prefix("PrivacyPolicy-VERSION=") { } else if let Some(v) = line.strip_prefix("Privacy-Policy-VERSION=") {
pp_version = v.to_string(); pp_version = v.to_string();
} else if let Some(v) = line.strip_prefix("PrivacyPolicy-HASH=") { } else if let Some(v) = line.strip_prefix("Privacy-Policy-HASH=") {
pp_hash = v.to_string(); pp_hash = v.to_string();
} else if let Some(v) = line.strip_prefix("UNIX=") { } else if let Some(v) = line.strip_prefix("UNIX=") {
unix = v.to_string(); unix = v.to_string();
@ -173,7 +248,7 @@ impl ConsentState {
accepted_eula: eula, accepted_eula: eula,
eula: Some(Doc::new(eula_version, eula_hash, Type::EULA, unix)), eula: Some(Doc::new(eula_version, eula_hash, Type::EULA, unix)),
accepted_pp: pp, accepted_pp: pp,
pp: Some(Doc::new(pp_version, pp_hash, Type::PP, unix)), privacy: Some(Doc::new(pp_version, pp_hash, Type::PP, unix)),
accepted_tos: tos, accepted_tos: tos,
tos: Some(Doc::new(tos_version, tos_hash, Type::TOS, unix)), tos: Some(Doc::new(tos_version, tos_hash, Type::TOS, unix)),
} }

View file

@ -2,12 +2,12 @@ use json::{JsonValue, object::Object};
use crate::{terms::terms_getter::Type, util::file_util::load_file}; use crate::{terms::terms_getter::Type, util::file_util::load_file};
#[derive(Clone, Debug)] #[derive(Clone, Debug, PartialEq, Eq)]
#[allow(unused)] #[allow(unused)]
pub struct Doc { pub struct Doc {
version: String, version: String,
hash: String, hash: String,
doc_type: Type, pub doc_type: Type,
timestamp: u64, timestamp: u64,
} }

View file

@ -1,4 +1,4 @@
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Focus { pub enum Focus {
Eula, Eula,
Tos, Tos,
@ -7,6 +7,7 @@ pub enum Focus {
Continue, Continue,
ContinueAll, ContinueAll,
} }
impl Focus { impl Focus {
pub fn next(&mut self, state: (bool, bool, bool)) { pub fn next(&mut self, state: (bool, bool, bool)) {
*self = match self { *self = match self {

View file

@ -2,7 +2,7 @@ use crate::terms::{
consent_state::{ConsentState, UserChoice}, consent_state::{ConsentState, UserChoice},
focus::Focus, focus::Focus,
md_viewer::FileViewer, md_viewer::FileViewer,
terms_getter::{Type, get_link, get_terms}, terms_getter::{Type, get_current_docs, get_link, get_terms},
}; };
use crossterm::event::{self, Event, KeyCode}; use crossterm::event::{self, Event, KeyCode};
use ratatui::{ use ratatui::{
@ -52,19 +52,19 @@ pub async fn run_consent_ui(mut consent: ConsentState) -> ConsentState {
height: content_height, height: content_height,
}); });
let eula_text = if size.width < 70 { let eula_text = if size.width < 70 {
"EULA ¹ (https://legal.tensamin.net/eula/)" "EULA ¹ (https://legal.tensamin.net/eula/newest/)"
} else { } else {
"End User Licence Agreement ¹ (https://legal.tensamin.net/eula/)" "End User Licence Agreement ¹ (https://legal.tensamin.net/eula/newest/)"
}; };
let tos_text = if size.width < 72 { let tos_text = if size.width < 72 {
"ToS ² (https://legal.tensamin.net/terms-of-service/)" "ToS ² (https://legal.tensamin.net/terms-of-service/newest/)"
} else { } else {
"Terms of Service ² (https://legal.tensamin.net/terms-of-service/)" "Terms of Service ² (https://legal.tensamin.net/terms-of-service/newest/)"
}; };
let pp_text = if size.width < 68 { let pp_text = if size.width < 68 {
"PP ² (https://legal.tensamin.net/privacy-policy/)" "PP ² (https://legal.tensamin.net/privacy-policy/newest/)"
} else { } else {
"Privacy Policy ² (https://legal.tensamin.net/privacy-policy/)" "Privacy Policy ² (https://legal.tensamin.net/privacy-policy/newest/)"
}; };
let (mut optional_lines, agree_lines): (Vec<i16>, Vec<&str>) = let (mut optional_lines, agree_lines): (Vec<i16>, Vec<&str>) =
@ -275,6 +275,12 @@ pub async fn run_consent_ui(mut consent: ConsentState) -> ConsentState {
}; };
ratatui::restore(); ratatui::restore();
if let Some((eula, tos, privacy)) = get_current_docs().await {
consent.eula = Some(eula);
consent.tos = Some(tos);
consent.privacy = Some(privacy);
}
match result { match result {
UserChoice::AcceptAll => { UserChoice::AcceptAll => {
consent.accepted_eula = true; consent.accepted_eula = true;
@ -292,6 +298,7 @@ pub async fn run_consent_ui(mut consent: ConsentState) -> ConsentState {
consent.accepted_pp = false; consent.accepted_pp = false;
} }
}; };
consent consent
} }

3
src/terms/terms_getter.rs Normal file → Executable file
View file

@ -2,7 +2,7 @@ use json::JsonValue::Object;
use crate::terms::doc::Doc; use crate::terms::doc::Doc;
#[derive(Clone, Debug)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Type { pub enum Type {
EULA, EULA,
TOS, TOS,
@ -87,7 +87,6 @@ pub async fn get_newest_docs() -> Option<(Doc, Doc, Doc)> {
None None
} }
} }
pub async fn get_terms(terms_type: Type) -> Option<String> { pub async fn get_terms(terms_type: Type) -> Option<String> {
let body = reqwest::get(format!( let body = reqwest::get(format!(
"https://legal.tensamin.net/api/text/{}/", "https://legal.tensamin.net/api/text/{}/",

View file

@ -1,5 +1,6 @@
use crate::terms::{ use crate::terms::{
consent_state::UserChoice, consent_state::{ConsentState, UserChoice},
doc::Doc,
focus::Focus, focus::Focus,
md_viewer::FileViewer, md_viewer::FileViewer,
terms_getter::{Type, get_link, get_terms}, terms_getter::{Type, get_link, get_terms},
@ -13,10 +14,29 @@ use ratatui::{
}; };
use std::time::Duration; use std::time::Duration;
pub async fn run_consent_ui() -> UserChoice { pub async fn run_consent_ui(
mut consent: ConsentState,
consent_eula: Result<Option<Doc>, Doc>,
consent_tos: Result<Option<Doc>, Doc>,
consent_pp: Result<Option<Doc>, Doc>,
) -> ConsentState {
let mut terminal = ratatui::init(); let mut terminal = ratatui::init();
let (eula_needed, eula_future, future_eula) = match consent_eula {
let (mut eula, mut tos, mut pp) = (false, false, false); Ok(Some(future)) => (true, true, Some(future)),
Ok(None) => (false, false, None),
Err(future) => (true, false, Some(future)),
};
let (tos_needed, tos_future, future_tos) = match consent_tos {
Ok(Some(future)) => (true, true, Some(future)),
Ok(None) => (false, false, None),
Err(future) => (true, false, Some(future)),
};
let (pp_needed, pp_future, future_privacy) = match consent_pp {
Ok(Some(future)) => (true, true, Some(future)),
Ok(None) => (false, false, None),
Err(future) => (true, false, Some(future)),
};
let (mut eula, mut tos, mut pp) = (!eula_needed, !tos_needed, !pp_needed);
let mut focus = Focus::Eula; let mut focus = Focus::Eula;
let result = loop { let result = loop {
@ -51,28 +71,68 @@ pub async fn run_consent_ui() -> UserChoice {
width: content_width, width: content_width,
height: content_height, height: content_height,
}); });
let eula_text = if size.width < 70 {
"EULA ¹ (https://legal.tensamin.net/eula/)"
let mut text_lines = Vec::new();
if eula_needed {
if eula_future {
if size.width < 70 {
text_lines.push(checkbox("EULA ¹³ (https://legal.tensamin.net/eula/newest/)", eula, focus == Focus::Eula, true));
} else { } else {
"End User Licence Agreement ¹ (https://legal.tensamin.net/eula/)" text_lines.push(checkbox("End User Licence Agreement ¹³ (https://legal.tensamin.net/eula/newest/)", eula, focus == Focus::Eula, true));
}; }
let tos_text = if size.width < 72 {
"ToS ² (https://legal.tensamin.net/terms-of-service/)"
} else { } else {
"Terms of Service ² (https://legal.tensamin.net/terms-of-service/)" if size.width < 70 {
}; text_lines.push(checkbox("EULA ¹ (https://legal.tensamin.net/eula/newest/)", eula, focus == Focus::Eula, true));
let pp_text = if size.width < 68 {
"PP ² (https://legal.tensamin.net/privacy-policy/)"
} else { } else {
"Privacy Policy ² (https://legal.tensamin.net/privacy-policy/)" text_lines.push(checkbox("End User Licence Agreement ¹ (https://legal.tensamin.net/eula/newest/)", eula, focus == Focus::Eula, true));
}; }
}
}
if tos_needed {
if tos_future {
if size.width < 70 {
text_lines.push(checkbox("ToS ²³ (https://legal.tensamin.net/terms-of-service/newest/)", tos, focus == Focus::Tos, eula));
} else {
text_lines.push(checkbox("Terms of Service ²³ (https://legal.tensamin.net/terms-of-service/newest/)", tos, focus == Focus::Tos, eula));
}
} else {
if size.width < 70 {
text_lines.push(checkbox("ToS ² (https://legal.tensamin.net/terms-of-service/newest/)", tos, focus == Focus::Tos, eula));
} else {
text_lines.push(checkbox("Terms of Service ² (https://legal.tensamin.net/terms-of-service/newest/)", tos, focus == Focus::Tos, eula));
}
}
}
if pp_needed {
if pp_future {
if size.width < 70 {
text_lines.push(checkbox("PP ²³ (https://legal.tensamin.net/privacy-policy/newest/)", pp, focus == Focus::Pp, eula));
} else {
text_lines.push(checkbox("Privacy Policy ²³ (https://legal.tensamin.net/privacy-policy/newest/)", pp, focus == Focus::Pp, eula));
}
} else {
if size.width < 70 {
text_lines.push(checkbox("PP ² (https://legal.tensamin.net/privacy-policy/newest/)", pp, focus == Focus::Pp, pp));
} else {
text_lines.push(checkbox("Privacy Policy ² (https://legal.tensamin.net/privacy-policy/newest/)", pp, focus == Focus::Pp, eula));
}
}
}
text_lines.push(Line::from(""));
text_lines.push(Line::from("¹ Necessary, ² Optional, ³ Future"));
text_lines.push(Line::from(""));
let (mut optional_lines, agree_lines): (Vec<i16>, Vec<&str>) = let (mut optional_lines, agree_lines): (Vec<i16>, Vec<&str>) =
if size.width > 143 { if size.width > 143 {
( (
vec![7, 3, 7, 4], vec![7, 3, 7, 4],
vec![ vec![
"",
"By selecting Continue, you confirm that you have read, understood and agree to the End User License Agreement and applicable Terms of Service.", "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.", "Tensamin services require acceptance of the Terms of Service and Privacy Policy.",
@ -97,7 +157,6 @@ pub async fn run_consent_ui() -> UserChoice {
( (
vec![8, 3, 8, 4], vec![8, 3, 8, 4],
vec![ vec![
"",
"By selecting Continue, you confirm that you have read, understood and", "By selecting Continue, you confirm that you have read, understood and",
"agree to the End User License Agreement and applicable Terms of Service.", "agree to the End User License Agreement and applicable Terms of Service.",
"", "",
@ -111,7 +170,6 @@ pub async fn run_consent_ui() -> UserChoice {
( (
vec![9, 3, 10, 4], vec![9, 3, 10, 4],
vec![ vec![
"",
"By selecting Continue, you confirm that you have", "By selecting Continue, you confirm that you have",
"read, understood and agree to the End User License", "read, understood and agree to the End User License",
"Agreement and applicable Terms of Service.", "Agreement and applicable Terms of Service.",
@ -124,13 +182,8 @@ pub async fn run_consent_ui() -> UserChoice {
] ]
) )
}; };
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 { for line in agree_lines {
text_lines.insert(text_lines.len(), Line::from(line)); text_lines.insert(text_lines.len(), Line::from(line));
@ -191,10 +244,10 @@ pub async fn run_consent_ui() -> UserChoice {
} }
let consent_block = Paragraph::new(Text::from(text_lines)) let consent_block = Paragraph::new(Text::from(text_lines))
.block(Block::default().title(" Tensamin User Consent [Q to Quit] ").borders(Borders::ALL)); .block(Block::default().title(format!(" Update Tensamin User Consent [Q to Quit] {}, {}, {}", pp_needed, eula_needed, tos_needed)).borders(Borders::ALL));
f.render_widget(consent_block, chunks[0]); f.render_widget(consent_block, chunks[0]);
draw_buttons(f, chunks[1], (eula, tos, pp)); draw_buttons(f, chunks[1], focus, (eula, tos, pp));
}) })
.unwrap(); .unwrap();
@ -275,7 +328,23 @@ pub async fn run_consent_ui() -> UserChoice {
}; };
ratatui::restore(); ratatui::restore();
result match result {
UserChoice::AcceptAll => {
consent.accepted_eula = true;
consent.accepted_tos = true;
consent.accepted_pp = true;
consent.eula = future_eula;
consent.tos = future_tos;
consent.privacy = future_privacy;
}
UserChoice::AcceptEULA => {
consent.accepted_eula = true;
consent.eula = future_eula;
}
UserChoice::Deny => {}
};
consent
} }
#[allow(mismatched_lifetime_syntaxes)] #[allow(mismatched_lifetime_syntaxes)]
@ -313,7 +382,12 @@ fn draw_button(f: &mut ratatui::Frame, area: Rect, label: &str, style: Style) {
.block(Block::default().borders(Borders::ALL)); .block(Block::default().borders(Borders::ALL));
f.render_widget(p, area); f.render_widget(p, area);
} }
fn draw_buttons(f: &mut ratatui::Frame, area: Rect, state: (bool, bool, bool)) { fn draw_buttons(
f: &mut ratatui::Frame,
area: Rect,
current_focus: Focus,
state: (bool, bool, bool),
) {
let buttons = vec![ let buttons = vec![
("[Q] Cancel", Focus::Cancel), ("[Q] Cancel", Focus::Cancel),
("Continue", Focus::Continue), ("Continue", Focus::Continue),
@ -339,9 +413,11 @@ fn draw_buttons(f: &mut ratatui::Frame, area: Rect, state: (bool, bool, bool)) {
}; };
x += width; x += width;
let is_focused = current_focus == *focus;
let style = match focus { let style = match focus {
Focus::Cancel => { Focus::Cancel => {
if focus == &Focus::Cancel { if is_focused {
Style::default() Style::default()
.fg(Color::Black) .fg(Color::Black)
.bg(Color::Red) .bg(Color::Red)
@ -350,20 +426,22 @@ fn draw_buttons(f: &mut ratatui::Frame, area: Rect, state: (bool, bool, bool)) {
Style::default().fg(Color::Red) Style::default().fg(Color::Red)
} }
} }
Focus::Continue => { Focus::Continue => {
if focus == &Focus::Continue && state.0 { if is_focused && state.0 {
Style::default() Style::default()
.fg(Color::Black) .fg(Color::Black)
.bg(Color::Green) .bg(Color::Green)
.add_modifier(Modifier::BOLD) .add_modifier(Modifier::BOLD)
} else if state.0 && state.1 && state.2 { } else if state.0 {
Style::default().fg(Color::Green) Style::default().fg(Color::Green) // enabled but not focused
} else { } else {
Style::default().fg(Color::DarkGray) Style::default().fg(Color::DarkGray)
} }
} }
Focus::ContinueAll => { Focus::ContinueAll => {
if focus == &Focus::ContinueAll && state.0 && state.1 && state.2 { if is_focused && state.0 && state.1 && state.2 {
Style::default() Style::default()
.fg(Color::Black) .fg(Color::Black)
.bg(Color::Green) .bg(Color::Green)
@ -374,13 +452,13 @@ fn draw_buttons(f: &mut ratatui::Frame, area: Rect, state: (bool, bool, bool)) {
Style::default().fg(Color::DarkGray) Style::default().fg(Color::DarkGray)
} }
} }
_ => Style::default().fg(Color::DarkGray), _ => Style::default().fg(Color::DarkGray),
}; };
draw_button(f, chunk, label, style); draw_button(f, chunk, label, style);
} }
} }
fn compute_widths(area_width: u16, min_widths: &[u16]) -> Vec<u16> { fn compute_widths(area_width: u16, min_widths: &[u16]) -> Vec<u16> {
let mut widths = vec![0; min_widths.len()]; let mut widths = vec![0; min_widths.len()];
let mut remaining: Vec<usize> = (0..min_widths.len()).collect(); let mut remaining: Vec<usize> = (0..min_widths.len()).collect();