[Add] Terms Updater

This commit is contained in:
Alex-Emmet 2026-02-07 18:46:07 +01:00
commit bfefa46839
9 changed files with 507 additions and 468 deletions

5
Cargo.lock generated
View file

@ -351,9 +351,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.42" version = "0.4.43"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
dependencies = [ dependencies = [
"iana-time-zone", "iana-time-zone",
"js-sys", "js-sys",
@ -1552,6 +1552,7 @@ dependencies = [
"axum", "axum",
"base64", "base64",
"bytes", "bytes",
"chrono",
"cmake", "cmake",
"color-eyre", "color-eyre",
"crossterm 0.29.0", "crossterm 0.29.0",

View file

@ -59,3 +59,4 @@ strum_macros = "0.27.2"
ratatui = "0.30.0" ratatui = "0.30.0"
ratatui_input = "0.1.3" ratatui_input = "0.1.3"
open = "5.3.3" open = "5.3.3"
chrono = "0.4.43"

175
src/terms/buttons.rs Normal file
View file

@ -0,0 +1,175 @@
use ratatui::{
layout::{Alignment, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph},
};
use crate::terms::focus::Focus;
#[allow(mismatched_lifetime_syntaxes)]
pub 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),
])
}
pub 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);
}
pub fn draw_buttons(
f: &mut ratatui::Frame,
area: Rect,
current_focus: Focus,
state: (bool, bool),
update_needed: bool,
downgrade_scenario: bool,
tos_or_privacy: bool,
) {
let cancel_text = if update_needed {
"[Q] Quit"
} else {
"[Q] Not now"
};
let continue_text = if downgrade_scenario {
"Downgrade"
} else {
"Continue"
};
let mut buttons = vec![
(cancel_text, Focus::Cancel),
(continue_text, Focus::Continue),
];
if tos_or_privacy {
buttons.push(("Continue with Tensamin Services", Focus::ContinueAll));
}
let padding = 2;
let min_widths: Vec<u16> = 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 is_focused = current_focus == *focus;
let style = match focus {
Focus::Cancel => {
if is_focused {
Style::default()
.fg(Color::Black)
.bg(Color::Red)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::Red)
}
}
Focus::Continue => {
if is_focused && state.0 {
Style::default()
.fg(Color::Black)
.bg(Color::Green)
.add_modifier(Modifier::BOLD)
} else if state.0 {
Style::default().fg(Color::Green)
} else {
Style::default().fg(Color::DarkGray)
}
}
Focus::ContinueAll => {
if is_focused && state.1 {
Style::default()
.fg(Color::Black)
.bg(Color::Green)
.add_modifier(Modifier::BOLD)
} else if state.1 {
Style::default().fg(Color::Green)
} else {
Style::default().fg(Color::DarkGray)
}
}
_ => Style::default().fg(Color::DarkGray),
};
draw_button(f, chunk, label, style);
}
}
pub fn compute_widths(area_width: u16, min_widths: &[u16]) -> Vec<u16> {
let mut widths = vec![0; min_widths.len()];
let mut remaining: Vec<usize> = (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
}

View file

@ -13,14 +13,15 @@ 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 mut file_state = ConsentState::from_str(&file).sanitize(); let (mut file_state, changed) = ConsentState::from_str(&file).sanitize();
if changed {
save_file("", "agreements", &file_state.to_string());
}
if !file_state.accepted_eula { if !file_state.accepted_eula {
file_state = terms_checker::run_consent_ui(file_state).await; file_state = terms_checker::run_consent_ui(file_state).await;
println!("{}", &file_state.clone().sanitize().to_string());
println!("-----------------------");
save_file("", "agreements", &file_state.to_string()); save_file("", "agreements", &file_state.to_string());
} }
@ -34,17 +35,16 @@ impl ConsentManager {
|| matches!(privacy_update, UpdateDecision::Forced(_)); || matches!(privacy_update, UpdateDecision::Forced(_));
let updater_logic = async move { let updater_logic = async move {
let state_to_update = terms_updater::run_consent_ui( let (state_to_update, _) = terms_updater::run_consent_ui(
file_state, file_state,
eula_update, eula_update,
tos_update, tos_update,
privacy_update, privacy_update,
) )
.await; .await
.sanitize();
println!("{}", &state_to_update.clone().sanitize().to_string()); save_file("", "agreements", &state_to_update.to_string());
save_file("", "agreements", &state_to_update.sanitize().to_string());
}; };
// ======================================================== // // ======================================================== //
// TODO: Implement UI Drawing order, draw update As PoPup // // TODO: Implement UI Drawing order, draw update As PoPup //
@ -56,7 +56,11 @@ impl ConsentManager {
//} //}
} }
let final_state = ConsentState::from_str(&load_file("", "agreements")).sanitize(); let file = load_file("", "agreements");
let (final_state, changed) = ConsentState::from_str(&file).sanitize();
if changed {
save_file("", "agreements", &final_state.to_string());
}
( (
final_state.accepted_eula, final_state.accepted_eula,
final_state.accepted_tos && final_state.accepted_pp, final_state.accepted_tos && final_state.accepted_pp,
@ -72,8 +76,10 @@ impl ConsentManager {
UpdateDecision, UpdateDecision,
)> { )> {
let file = load_file("", "agreements"); let file = load_file("", "agreements");
let accepted_state = ConsentState::from_str(&file).sanitize(); let (accepted_state, changed) = ConsentState::from_str(&file).sanitize();
if changed {
save_file("", "agreements", &accepted_state.to_string());
}
if let ( if let (
Some((current_eula, current_tos, current_privacy)), Some((current_eula, current_tos, current_privacy)),
Some((newest_eula, newest_tos, newest_privacy)), Some((newest_eula, newest_tos, newest_privacy)),
@ -82,46 +88,68 @@ impl ConsentManager {
let eula_update: UpdateDecision = if current_eula.equals_some(&accepted_state.eula) { let eula_update: UpdateDecision = if current_eula.equals_some(&accepted_state.eula) {
if current_eula.equals(&newest_eula) { if current_eula.equals(&newest_eula) {
UpdateDecision::NoChange UpdateDecision::NoChange
} else {
if newest_eula.equals_some(&accepted_state.future_eula) {
UpdateDecision::NoChange
} else { } else {
UpdateDecision::Future { UpdateDecision::Future {
newest: newest_eula, newest: newest_eula,
} }
} }
}
} else if newest_eula.equals_some(&accepted_state.eula) { } else if newest_eula.equals_some(&accepted_state.eula) {
UpdateDecision::NoChange UpdateDecision::NoChange
} else { } else {
UpdateDecision::Forced(current_eula) UpdateDecision::Forced(current_eula)
}; };
let tos_update: UpdateDecision = if newest_tos.equals_some(&accepted_state.tos) { let tos_update: UpdateDecision = if !accepted_state.accepted_tos
|| newest_tos.equals_some(&accepted_state.tos)
{
UpdateDecision::NoChange UpdateDecision::NoChange
} else if accepted_state.accepted_tos && current_tos.equals_some(&accepted_state.tos) { } else if accepted_state.accepted_tos && current_tos.equals_some(&accepted_state.tos) {
if current_tos.equals(&newest_tos) { if current_tos.equals(&newest_tos) {
UpdateDecision::NoChange UpdateDecision::NoChange
} else {
if newest_tos.equals_some(&accepted_state.future_tos) {
UpdateDecision::NoChange
} else { } else {
UpdateDecision::Future { newest: newest_tos } UpdateDecision::Future { newest: newest_tos }
} }
}
} else { } else {
UpdateDecision::Forced(current_tos) UpdateDecision::Forced(current_tos)
}; };
let privacy_update: UpdateDecision = let privacy_update: UpdateDecision = if !accepted_state.accepted_pp
if newest_privacy.equals_some(&accepted_state.privacy) { || newest_privacy.equals_some(&accepted_state.privacy)
{
UpdateDecision::NoChange UpdateDecision::NoChange
} else if accepted_state.accepted_pp } else if accepted_state.accepted_pp
&& current_privacy.equals_some(&accepted_state.privacy) && current_privacy.equals_some(&accepted_state.privacy)
{ {
if current_privacy.equals(&newest_privacy) { if current_privacy.equals(&newest_privacy) {
UpdateDecision::NoChange UpdateDecision::NoChange
} else {
if newest_privacy.equals_some(&accepted_state.future_privacy) {
UpdateDecision::NoChange
} else { } else {
UpdateDecision::Future { UpdateDecision::Future {
newest: newest_privacy, newest: newest_privacy,
} }
} }
}
} else { } else {
UpdateDecision::Forced(current_privacy) UpdateDecision::Forced(current_privacy)
}; };
Some((eula_update, tos_update, privacy_update)) match (&eula_update, &tos_update, &privacy_update) {
(
&UpdateDecision::NoChange,
&UpdateDecision::NoChange,
&UpdateDecision::NoChange,
) => None,
_ => Some((eula_update, tos_update, privacy_update)),
}
} else { } else {
None None
} }
@ -158,12 +186,41 @@ pub struct ConsentState {
} }
impl ConsentState { impl ConsentState {
fn sanitize(mut self) -> Self { fn sanitize(mut self) -> (Self, bool) {
let mut changed = false;
let current_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
if let Some(future_eula) = self.future_eula.clone() {
if future_eula.get_time() < current_secs {
self.eula = Some(future_eula);
self.future_eula = None;
changed = true;
}
}
if let Some(future_tos) = self.future_tos.clone() {
if future_tos.get_time() < current_secs {
self.tos = Some(future_tos);
self.future_tos = None;
changed = true;
}
}
if let Some(future_privacy) = self.future_privacy.clone() {
if future_privacy.get_time() < current_secs {
self.privacy = Some(future_privacy);
self.future_privacy = None;
changed = true;
}
}
if !self.accepted_eula { if !self.accepted_eula {
self.accepted_tos = false; self.accepted_tos = false;
self.accepted_pp = false; self.accepted_pp = false;
changed = true;
} }
self (self, changed)
} }
pub fn to_string(&self) -> String { pub fn to_string(&self) -> String {
@ -325,7 +382,7 @@ impl ConsentState {
let future_eula_time = future_eula_time.parse::<u64>().unwrap_or(0); let future_eula_time = future_eula_time.parse::<u64>().unwrap_or(0);
let future_tos_time = future_tos_time.parse::<u64>().unwrap_or(0); let future_tos_time = future_tos_time.parse::<u64>().unwrap_or(0);
let future_pp_time = future_pp_time.parse::<u64>().unwrap_or(0); let future_pp_time = future_pp_time.parse::<u64>().unwrap_or(0);
Self { let (state, _) = Self {
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,
@ -363,6 +420,8 @@ impl ConsentState {
None None
}, },
} }
.sanitize() .sanitize();
state
} }
} }

View file

@ -98,7 +98,7 @@ impl FileViewer {
.block( .block(
Block::default() Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.title(self.title.as_str()), .title(format!("{} - [Q to close]", self.title.as_str(),)),
) )
.wrap(Wrap { trim: false }) .wrap(Wrap { trim: false })
.scroll((self.scroll, 0)); .scroll((self.scroll, 0));

View file

@ -1,3 +1,4 @@
pub mod buttons;
pub mod consent_state; pub mod consent_state;
pub mod doc; pub mod doc;
pub mod focus; pub mod focus;

View file

@ -1,4 +1,5 @@
use crate::terms::{ use crate::terms::{
buttons::{checkbox, draw_buttons},
consent_state::{ConsentState, UserChoice}, consent_state::{ConsentState, UserChoice},
focus::Focus, focus::Focus,
md_viewer::FileViewer, md_viewer::FileViewer,
@ -7,7 +8,7 @@ use crate::terms::{
use crossterm::event::{self, Event, KeyCode}; use crossterm::event::{self, Event, KeyCode};
use ratatui::{ use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect}, layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style}, style::{Color, Style},
text::{Line, Span, Text}, text::{Line, Span, Text},
widgets::{Block, Borders, Paragraph}, widgets::{Block, Borders, Paragraph},
}; };
@ -29,12 +30,12 @@ pub async fn run_consent_ui(mut consent: ConsentState) -> ConsentState {
if size.height < 6 if size.height < 6
|| size.width < 27 { || size.width < 27 {
f.render_widget(Line::from(format!("too small {}/63 by {}/12", size.width, size.height)), size); f.render_widget(Line::from(format!("too small {}/63 by {}/13", size.width, size.height)), size);
return; return;
} }
let max_width = 150; let max_width = 150;
let max_height = 20; let max_height = 26;
let content_width = if max_width < size.width { max_width } else { size.width} ; 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 content_height = if max_height < size.height { max_height } else { size.height} ;
@ -52,28 +53,28 @@ 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/newest/)" "EULA ¹ (https://legal.tensamin.net/eula/)"
} else { } else {
"End User Licence Agreement ¹ (https://legal.tensamin.net/eula/newest/)" "End User Licence Agreement ¹ (https://legal.tensamin.net/eula/)"
}; };
let tos_text = if size.width < 72 { let tos_text = if size.width < 72 {
"ToS ² (https://legal.tensamin.net/terms-of-service/newest/)" "ToS ² (https://legal.tensamin.net/terms-of-service/)"
} else { } else {
"Terms of Service ² (https://legal.tensamin.net/terms-of-service/newest/)" "Terms of Service ² (https://legal.tensamin.net/terms-of-service/)"
}; };
let pp_text = if size.width < 68 { let pp_text = if size.width < 68 {
"PP ² (https://legal.tensamin.net/privacy-policy/newest/)" "PP ² (https://legal.tensamin.net/privacy-policy/)"
} else { } else {
"Privacy Policy ² (https://legal.tensamin.net/privacy-policy/newest/)" "Privacy Policy ² (https://legal.tensamin.net/privacy-policy/)"
}; };
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![8, 3, 8, 5],
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 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.",
"", "",
@ -82,10 +83,10 @@ pub async fn run_consent_ui(mut consent: ConsentState) -> ConsentState {
) )
} else if size.width > 92 { } else if size.width > 92 {
( (
vec![8, 3, 8, 4], vec![9, 3, 9, 5],
vec![ vec![
"", "",
"By selecting Continue, you confirm that you have read, understood and agree to the End User", "By selecting Continue, you confirm that you have agree to the End User",
"License Agreement and applicable Terms of Service.", "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.",
@ -95,7 +96,7 @@ pub async fn run_consent_ui(mut consent: ConsentState) -> ConsentState {
) )
} else if size.width > 73 { } else if size.width > 73 {
( (
vec![8, 3, 8, 4], vec![9, 3, 9, 5],
vec![ vec![
"", "",
"By selecting Continue, you confirm that you have read, understood and", "By selecting Continue, you confirm that you have read, understood and",
@ -109,11 +110,11 @@ pub async fn run_consent_ui(mut consent: ConsentState) -> ConsentState {
) )
} else { } else {
( (
vec![9, 3, 10, 4], vec![10, 3, 11, 5],
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", "agree to the End User License",
"Agreement and applicable Terms of Service.", "Agreement and applicable Terms of Service.",
"", "",
"Tensamin services require acceptance of the", "Tensamin services require acceptance of the",
@ -129,9 +130,9 @@ pub async fn run_consent_ui(mut consent: ConsentState) -> ConsentState {
checkbox(tos_text, tos, focus == Focus::Tos, eula), checkbox(tos_text, tos, focus == Focus::Tos, eula),
checkbox(pp_text, pp, focus == Focus::Pp, eula), checkbox(pp_text, pp, focus == Focus::Pp, eula),
Line::from(""), Line::from(""),
Line::from("¹ Necessary, ² Optional"), Line::from("¹ Necessary required to run the program"),
Line::from("² Optional required only for Tensamin services")
]; ];
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));
} }
@ -156,12 +157,12 @@ pub async fn run_consent_ui(mut consent: ConsentState) -> ConsentState {
Style::default().fg(Color::Red) Style::default().fg(Color::Red)
}; };
let height_style = if size.height < 12 { let height_style = if size.height > 19 {
Style::default().fg(Color::Red)
} else if size.height > 19 {
Style::default().fg(Color::Green) Style::default().fg(Color::Green)
} else { } else if size.height >= 13 {
Style::default().fg(Color::Yellow) Style::default().fg(Color::Yellow)
} else {
Style::default().fg(Color::Red)
}; };
let warning_text = Text::from(vec![ let warning_text = Text::from(vec![
@ -173,7 +174,7 @@ pub async fn run_consent_ui(mut consent: ConsentState) -> ConsentState {
Line::from(vec![ Line::from(vec![
Span::raw("Height: "), Span::raw("Height: "),
Span::styled(format!("{}", size.height), height_style), Span::styled(format!("{}", size.height), height_style),
Span::raw(format!(" / 12")), Span::raw(format!(" / 13")),
]), ]),
]); ]);
@ -182,7 +183,7 @@ pub async fn run_consent_ui(mut consent: ConsentState) -> ConsentState {
.block( .block(
Block::default() Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.title("UI Too Small (Q to Quit)"), .title(format!("UI Too Small [Q to Quit]")),
); );
too_small = true; too_small = true;
@ -194,7 +195,7 @@ pub async fn run_consent_ui(mut consent: ConsentState) -> ConsentState {
.block(Block::default().title(" Tensamin User Consent [Q to Quit] ").borders(Borders::ALL)); .block(Block::default().title(" Tensamin User Consent [Q to Quit] ").borders(Borders::ALL));
f.render_widget(consent_block, chunks[0]); f.render_widget(consent_block, chunks[0]);
draw_buttons(f, chunks[1], focus, (eula, tos, pp)); draw_buttons(f, chunks[1], focus, (eula, tos && pp), true, false, true);
}) })
.unwrap(); .unwrap();
@ -296,8 +297,8 @@ pub async fn run_consent_ui(mut consent: ConsentState) -> ConsentState {
} }
UserChoice::AcceptEULA => { UserChoice::AcceptEULA => {
consent.accepted_eula = true; consent.accepted_eula = true;
consent.accepted_tos = true; consent.accepted_tos = false;
consent.accepted_pp = true; consent.accepted_pp = false;
} }
UserChoice::Deny => { UserChoice::Deny => {
consent.accepted_eula = false; consent.accepted_eula = false;
@ -308,155 +309,3 @@ pub async fn run_consent_ui(mut consent: ConsentState) -> ConsentState {
consent consent
} }
#[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,
current_focus: Focus,
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<u16> = 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 is_focused = current_focus == *focus;
let style = match focus {
Focus::Cancel => {
if is_focused {
Style::default()
.fg(Color::Black)
.bg(Color::Red)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::Red)
}
}
Focus::Continue => {
if is_focused && state.0 {
Style::default()
.fg(Color::Black)
.bg(Color::Green)
.add_modifier(Modifier::BOLD)
} else if state.0 {
Style::default().fg(Color::Green) // enabled but not focused
} else {
Style::default().fg(Color::DarkGray)
}
}
Focus::ContinueAll => {
if is_focused && 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<u16> {
let mut widths = vec![0; min_widths.len()];
let mut remaining: Vec<usize> = (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
}

View file

@ -29,6 +29,9 @@ impl Type {
pub fn get_link(terms_type: Type) -> String { pub fn get_link(terms_type: Type) -> String {
format!("https://legal.tensamin.net/{}/", terms_type.to_str()) format!("https://legal.tensamin.net/{}/", terms_type.to_str())
} }
pub fn get_newest_link(terms_type: Type) -> String {
format!("https://legal.tensamin.net/{}/newest/", terms_type.to_str())
}
pub async fn get_current_docs() -> Option<(Doc, Doc, Doc)> { pub async fn get_current_docs() -> Option<(Doc, Doc, Doc)> {
let body = reqwest::get("https://legal.tensamin.net/api/current/") let body = reqwest::get("https://legal.tensamin.net/api/current/")

View file

@ -1,19 +1,20 @@
use crate::terms::{ use crate::terms::{
buttons::{checkbox, draw_buttons},
consent_state::{ConsentState, UpdateDecision, UserChoice}, consent_state::{ConsentState, UpdateDecision, UserChoice},
doc::Doc, doc::Doc,
focus::Focus, focus::Focus,
md_viewer::FileViewer, md_viewer::FileViewer,
terms_getter::{Type, get_link, get_terms}, terms_getter::{Type, get_newest_link, get_terms},
}; };
use chrono::{Local, TimeZone, Utc};
use crossterm::event::{self, Event, KeyCode}; use crossterm::event::{self, Event, KeyCode};
use ratatui::{ use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect}, layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style}, style::{Color, Style},
text::{Line, Span, Text}, text::{Line, Span, Text},
widgets::{Block, Borders, Paragraph}, widgets::{Block, Borders, Paragraph},
}; };
use std::time::Duration; use std::time::Duration;
pub async fn run_consent_ui( pub async fn run_consent_ui(
mut consent: ConsentState, mut consent: ConsentState,
consent_eula: UpdateDecision, consent_eula: UpdateDecision,
@ -38,9 +39,18 @@ pub async fn run_consent_ui(
UpdateDecision::Future { newest } => (true, true, Some(newest)), UpdateDecision::Future { newest } => (true, true, Some(newest)),
UpdateDecision::Forced(doc) => (true, false, Some(doc)), UpdateDecision::Forced(doc) => (true, false, Some(doc)),
}; };
let update_needed = eula_needed || tos_needed || pp_needed; let update_needed =
(eula_needed && !eula_future) || (tos_needed && !tos_future) || (pp_needed && !pp_future);
let (mut eula, mut tos, mut pp) = (!eula_needed, !tos_needed, !pp_needed); let (mut eula, mut tos, mut pp) = (!eula_needed, !tos_needed, !pp_needed);
let mut focus = Focus::Eula; let mut focus = if eula_needed {
Focus::Eula
} else if tos_needed {
Focus::Tos
} else if pp_needed {
Focus::Pp
} else {
Focus::Cancel
};
let result = loop { let result = loop {
let mut too_small = false; let mut too_small = false;
@ -52,12 +62,12 @@ pub async fn run_consent_ui(
if size.height < 6 if size.height < 6
|| size.width < 27 { || size.width < 27 {
f.render_widget(Line::from(format!("too small {}/63 by {}/12", size.width, size.height)), size); f.render_widget(Line::from(format!("too small {}/63 by {}/15", size.width, size.height)), size);
return; return;
} }
let max_width = 150; let max_width = 150;
let max_height = 20; let max_height = 30;
let content_width = if max_width < size.width { max_width } else { size.width} ; 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 content_height = if max_height < size.height { max_height } else { size.height} ;
@ -76,18 +86,46 @@ pub async fn run_consent_ui(
}); });
let mut text_lines = Vec::new(); let mut text_lines: Vec<Line> = Vec::new();
let mut header_lines = 0;
let seperator = if size.width > 72 {
header_lines += 3;
text_lines.push(Line::from("You previously accepted earlier versions of Tensamins legal documents."));
text_lines.push(Line::from("Some of them have been updated and are listed below for your review."));
text_lines.push(Line::from(""));
2
} else {
header_lines += 5;
text_lines.push(Line::from("You previously accepted earlier "));
text_lines.push(Line::from("versions of Tensamins legal documents."));
text_lines.push(Line::from("Some of them have been updated and"));
text_lines.push(Line::from("are listed below for your review."));
text_lines.push(Line::from(""));
4
};
if eula_needed { if eula_needed {
header_lines += 1;
if eula_future { if eula_future {
if size.width < 70 { if size.width < 80 {
text_lines.push(checkbox("EULA ¹³ (https://legal.tensamin.net/eula/newest/)", eula, focus == Focus::Eula, true)); text_lines.push(checkbox("EULA ¹³ (https://legal.tensamin.net/eula/newest/)", eula, focus == Focus::Eula, true));
header_lines += 1;
let unix_timestamp = eula_for_future.clone().unwrap().get_time() as i64;
let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap();
let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M");
text_lines.push(Line::from(format!(" Goes into effect on {}", date)));
} else { } else {
text_lines.push(checkbox("End User Licence Agreement ¹³ (https://legal.tensamin.net/eula/newest/)", eula, focus == Focus::Eula, true)); text_lines.push(checkbox("End User Licence Agreement ¹³ (https://legal.tensamin.net/eula/newest/)", eula, focus == Focus::Eula, true));
header_lines += 1;
let unix_timestamp = eula_for_future.clone().unwrap().get_time() as i64;
let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap();
let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M");
text_lines.push(Line::from(format!(" Goes into effect on {}", date)));
} }
} else { } else {
if size.width < 70 { if size.width < 80 {
text_lines.push(checkbox("EULA ¹ (https://legal.tensamin.net/eula/newest/)", eula, focus == Focus::Eula, true)); text_lines.push(checkbox("EULA ¹ (https://legal.tensamin.net/eula/newest/)", eula, focus == Focus::Eula, true));
} else { } else {
text_lines.push(checkbox("End User Licence Agreement ¹ (https://legal.tensamin.net/eula/newest/)", eula, focus == Focus::Eula, true)); text_lines.push(checkbox("End User Licence Agreement ¹ (https://legal.tensamin.net/eula/newest/)", eula, focus == Focus::Eula, true));
@ -96,15 +134,28 @@ pub async fn run_consent_ui(
} }
if tos_needed { if tos_needed {
header_lines += 1;
if tos_future { if tos_future {
if size.width < 70 { if size.width < 80 {
text_lines.push(checkbox("ToS ²³ (https://legal.tensamin.net/terms-of-service/newest/)", tos, focus == Focus::Tos, eula)); text_lines.push(checkbox("ToS ²³ (https://legal.tensamin.net/tos/newest/)", tos, focus == Focus::Tos, eula));
header_lines += 1;
let unix_timestamp = tos_for_future.clone().unwrap().get_time() as i64;
let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap();
let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M");
text_lines.push(Line::from(format!(" Goes into effect on {}", date)));
} else { } else {
text_lines.push(checkbox("Terms of Service ²³ (https://legal.tensamin.net/terms-of-service/newest/)", tos, focus == Focus::Tos, eula)); text_lines.push(checkbox("Terms of Service ²³ (https://legal.tensamin.net/terms-of-service/newest/)", tos, focus == Focus::Tos, eula));
header_lines += 1;
let unix_timestamp = tos_for_future.clone().unwrap().get_time() as i64;
let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap();
let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M");
text_lines.push(Line::from(format!(" Goes into effect on {}", date)));
} }
} else { } else {
if size.width < 70 { if size.width < 80 {
text_lines.push(checkbox("ToS ² (https://legal.tensamin.net/terms-of-service/newest/)", tos, focus == Focus::Tos, eula)); text_lines.push(checkbox("ToS ² (https://legal.tensamin.net/tos/newest/)", tos, focus == Focus::Tos, eula));
} else { } else {
text_lines.push(checkbox("Terms of Service ² (https://legal.tensamin.net/terms-of-service/newest/)", tos, focus == Focus::Tos, eula)); text_lines.push(checkbox("Terms of Service ² (https://legal.tensamin.net/terms-of-service/newest/)", tos, focus == Focus::Tos, eula));
} }
@ -112,15 +163,28 @@ pub async fn run_consent_ui(
} }
if pp_needed { if pp_needed {
header_lines += 1;
if pp_future { if pp_future {
if size.width < 70 { if size.width < 80 {
text_lines.push(checkbox("PP ²³ (https://legal.tensamin.net/privacy-policy/newest/)", pp, focus == Focus::Pp, eula)); text_lines.push(checkbox("PP ²³ (https://legal.tensamin.net/privacy-policy/newest/)", pp, focus == Focus::Pp, eula));
header_lines += 1;
let unix_timestamp = pp_for_future.clone().unwrap().get_time() as i64;
let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap();
let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M");
text_lines.push(Line::from(format!(" Goes into effect on {}", date)));
} else { } else {
text_lines.push(checkbox("Privacy Policy ²³ (https://legal.tensamin.net/privacy-policy/newest/)", pp, focus == Focus::Pp, eula)); text_lines.push(checkbox("Privacy Policy ²³ (https://legal.tensamin.net/privacy-policy/newest/)", pp, focus == Focus::Pp, eula));
header_lines += 1;
let unix_timestamp = pp_for_future.clone().unwrap().get_time() as i64;
let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap();
let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M");
text_lines.push(Line::from(format!(" Goes into effect on {}", date)));
} }
} else { } else {
if size.width < 70 { if size.width < 80 {
text_lines.push(checkbox("PP ² (https://legal.tensamin.net/privacy-policy/newest/)", pp, focus == Focus::Pp, pp)); text_lines.push(checkbox("PP ² (https://legal.tensamin.net/privacy-policy/newest/)", pp, focus == Focus::Pp, eula));
} else { } else {
text_lines.push(checkbox("Privacy Policy ² (https://legal.tensamin.net/privacy-policy/newest/)", pp, focus == Focus::Pp, eula)); text_lines.push(checkbox("Privacy Policy ² (https://legal.tensamin.net/privacy-policy/newest/)", pp, focus == Focus::Pp, eula));
} }
@ -128,70 +192,100 @@ pub async fn run_consent_ui(
} }
text_lines.push(Line::from("")); text_lines.push(Line::from(""));
text_lines.push(Line::from("¹ Necessary, ² Optional, ³ Future")); text_lines.push(Line::from("¹ Necessary to run the program"));
text_lines.push(Line::from("² Optional - only for Tensamin services"));
if size.width < 100 {
text_lines.push(Line::from("³ Future version - consent stored now, takes effect later"));
} else {
text_lines.push(Line::from("³ Future version - Youll continue using this version, automatically updated when changes apply."));
}
text_lines.push(Line::from("")); text_lines.push(Line::from(""));
let (mut optional_lines, agree_lines): (Vec<i16>, Vec<&str>) = let mut optional_lines: Vec<i16> =
if size.width > 143 { if size.width > 143 {
( if tos_needed || pp_needed {
vec![7, 3, 7, 4], text_lines.push(Line::from("By selecting Downgrade, you confirm that you have agree to the End User License Agreement and applicable Terms of Service."));
vec![ text_lines.push(Line::from("On Downgrade: Tensamin Services will deactivate once these changes apply."));
"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 { } else {
( text_lines.push(Line::from("By selecting Continue, you confirm that you have agree to the End User License Agreement and applicable Terms of Service."));
vec![9, 3, 10, 4], }
vec![ text_lines.push(Line::from(""));
"By selecting Continue, you confirm that you have", text_lines.push(Line::from("Tensamin services require acceptance of the Terms of Service and Privacy Policy."));
"read, understood and agree to the End User License", text_lines.push(Line::from(""));
"Agreement and applicable Terms of Service.", text_lines.push(Line::from("While having a document selected press O to view in this UI or press L to open as a link."));
"",
"Tensamin services require acceptance of the", if tos_needed || pp_needed {
"Terms of Service and Privacy Policy.", vec![header_lines + 7, header_lines, header_lines + 7, seperator, header_lines + 2]
"", } else {
"While having a document selected press O to view", vec![header_lines + 6, header_lines, header_lines + 6, seperator, header_lines + 2]
"in this UI or press L to open as a link.", }
] } else if size.width > 92 {
) if tos_needed || pp_needed {
text_lines.push(Line::from("By selecting Continue, you confirm that you have agree to the End User"));
text_lines.push(Line::from("License Agreement and applicable Terms of Service."));
text_lines.push(Line::from("On Downgrade: Tensamin Services will deactivate once these changes apply."));
} else {
text_lines.push(Line::from("By selecting Continue, you confirm that you have agree to the End User"));
text_lines.push(Line::from("License Agreement and applicable Terms of Service."));
}
text_lines.push(Line::from(""));
text_lines.push(Line::from("Tensamin services require acceptance of the Terms of Service and Privacy Policy."));
text_lines.push(Line::from(""));
text_lines.push(Line::from("While having a document selected press O to view in this UI or press L to open as a link."));
if tos_needed || pp_needed {
vec![header_lines + 8, header_lines, header_lines + 8, seperator, header_lines + 2]
} else {
vec![header_lines + 7, header_lines, header_lines + 7, seperator, header_lines + 2]
}
} else if size.width > 73 {
if tos_needed || pp_needed {
text_lines.push(Line::from("By selecting Continue, you confirm that you have read, understood and"));
text_lines.push(Line::from("agree to the End User License Agreement and applicable Terms of Service."));
text_lines.push(Line::from("On Downgrade: Tensamin Services will deactivate once these changes apply."));
} else {
text_lines.push(Line::from("By selecting Continue, you confirm that you have read, understood and"));
text_lines.push(Line::from("agree to the End User License Agreement and applicable Terms of Service."));
}
text_lines.push(Line::from("",));
text_lines.push(Line::from("Tensamin services require acceptance of the ToS and Privacy Policy.",));
text_lines.push(Line::from("",));
text_lines.push(Line::from("While having a document selected press O to view in this UI or press L",));
text_lines.push(Line::from("to open as a link.",));
if tos_needed || pp_needed {
vec![header_lines + 8, header_lines, header_lines + 8, seperator, header_lines + 2]
} else {
vec![header_lines + 7, header_lines, header_lines + 7, seperator, header_lines + 2]
}
} else {
if tos_needed || pp_needed {
text_lines.push(Line::from("By selecting Continue, you confirm that you have",));
text_lines.push(Line::from("agree to the End User License",));
text_lines.push(Line::from("Agreement and applicable Terms of Service.",));
text_lines.push(Line::from("On Downgrade: Tensamin Services will deactivate"));
text_lines.push(Line::from("once these changes apply."));
} else {
text_lines.push(Line::from("By selecting Continue, you confirm that you have",));
text_lines.push(Line::from("agree to the End User License",));
text_lines.push(Line::from("Agreement and applicable Terms of Service.",));
}
text_lines.push(Line::from("",));
text_lines.push(Line::from("Tensamin services require acceptance of the",));
text_lines.push(Line::from("Terms of Service and Privacy Policy.",));
text_lines.push(Line::from("",));
text_lines.push(Line::from("While having a document selected press O to view",));
text_lines.push(Line::from("in this UI or press L to open as a link.",));
if tos_needed || pp_needed {
vec![header_lines + 10, header_lines, header_lines + 11, seperator, header_lines + 2]
} else {
vec![header_lines + 8, header_lines, header_lines + 9, seperator, header_lines + 2]
}
}; };
for line in agree_lines {
text_lines.insert(text_lines.len(), Line::from(line));
}
while size.height - 5 < text_lines.len() as u16 { while size.height - 5 < text_lines.len() as u16 {
if optional_lines.len() == 0 { if optional_lines.len() == 0 {
break; break;
@ -202,7 +296,11 @@ pub async fn run_consent_ui(
needed_height += text_lines.len(); needed_height += text_lines.len();
let q_informer = if update_needed {
"Q to Exit"
} else {
"Q to Cancel"
};
if size.width < 60 || size.height < needed_height as u16 { if size.width < 60 || size.height < needed_height as u16 {
let width_style = if size.width > 76 { let width_style = if size.width > 76 {
Style::default().fg(Color::Green) Style::default().fg(Color::Green)
@ -212,12 +310,12 @@ pub async fn run_consent_ui(
Style::default().fg(Color::Red) Style::default().fg(Color::Red)
}; };
let height_style = if size.height < 12 { let height_style = if size.height > 20 {
Style::default().fg(Color::Red)
} else if size.height > 19 {
Style::default().fg(Color::Green) Style::default().fg(Color::Green)
} else { } else if size.height >= (header_lines as u16 + 10) {
Style::default().fg(Color::Yellow) Style::default().fg(Color::Yellow)
} else {
Style::default().fg(Color::Red)
}; };
let warning_text = Text::from(vec![ let warning_text = Text::from(vec![
@ -229,16 +327,17 @@ pub async fn run_consent_ui(
Line::from(vec![ Line::from(vec![
Span::raw("Height: "), Span::raw("Height: "),
Span::styled(format!("{}", size.height), height_style), Span::styled(format!("{}", size.height), height_style),
Span::raw(format!(" / 12")), Span::raw(format!(" / {}", header_lines + 10)),
]), ]),
]); ]);
let warning = Paragraph::new(warning_text) let warning = Paragraph::new(warning_text)
.alignment(Alignment::Center) .alignment(Alignment::Center)
.block( .block(
Block::default() Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.title("UI Too Small (Q to Quit)"), .title(format!("UI Too Small [{}]", q_informer)),
); );
too_small = true; too_small = true;
@ -247,7 +346,7 @@ pub async fn run_consent_ui(
} }
let consent_block = Paragraph::new(Text::from(text_lines)) let consent_block = Paragraph::new(Text::from(text_lines))
.block(Block::default().title(format!(" Update Tensamin User Consent [Q to Quit] {}, {}, {}", pp_needed, eula_needed, tos_needed)).borders(Borders::ALL)); .block(Block::default().title(format!(" Update Tensamin User Consent [{}] ", q_informer)).borders(Borders::ALL));
f.render_widget(consent_block, chunks[0]); f.render_widget(consent_block, chunks[0]);
let downgrade_scenario = tos_needed || pp_needed; let downgrade_scenario = tos_needed || pp_needed;
@ -258,6 +357,7 @@ pub async fn run_consent_ui(
(eula, tos && pp), (eula, tos && pp),
update_needed, update_needed,
downgrade_scenario, downgrade_scenario,
pp_needed || tos_needed
); );
}) })
.unwrap(); .unwrap();
@ -271,10 +371,19 @@ pub async fn run_consent_ui(
continue; continue;
} }
} }
let mut possible_states = vec![Focus::Eula, Focus::Tos, Focus::Pp, Focus::Cancel]; let mut possible_states = vec![Focus::Eula];
if tos_needed {
possible_states.push(Focus::Tos);
}
if pp_needed {
possible_states.push(Focus::Pp);
}
possible_states.push(Focus::Cancel);
if eula { if eula {
possible_states.push(Focus::Continue); possible_states.push(Focus::Continue);
if tos && pp { if tos && pp && (pp_needed || tos_needed) {
possible_states.push(Focus::ContinueAll); possible_states.push(Focus::ContinueAll);
} }
} }
@ -311,9 +420,9 @@ pub async fn run_consent_ui(
_ => continue, _ => continue,
}; };
let _ = open::that(get_link(terms_type)); let _ = open::that(get_newest_link(terms_type));
} }
KeyCode::Char(' ') => match focus { KeyCode::Char(' ') | KeyCode::Enter => match focus {
Focus::Eula => { Focus::Eula => {
eula = !eula; eula = !eula;
tos = !tos_needed; tos = !tos_needed;
@ -329,9 +438,6 @@ pub async fn run_consent_ui(
pp = !pp pp = !pp
} }
} }
_ => {}
},
KeyCode::Enter => match focus {
Focus::Cancel => break UserChoice::Deny, Focus::Cancel => break UserChoice::Deny,
Focus::Continue if eula => break UserChoice::AcceptEULA, Focus::Continue if eula => break UserChoice::AcceptEULA,
Focus::ContinueAll if eula && tos && pp => { Focus::ContinueAll if eula && tos && pp => {
@ -351,180 +457,24 @@ pub async fn run_consent_ui(
consent.accepted_eula = true; consent.accepted_eula = true;
consent.accepted_tos = true; consent.accepted_tos = true;
consent.accepted_pp = true; consent.accepted_pp = true;
if let Some(_) = eula_for_future {
consent.future_eula = eula_for_future; consent.future_eula = eula_for_future;
}
if let Some(_) = tos_for_future {
consent.future_tos = tos_for_future; consent.future_tos = tos_for_future;
}
if let Some(_) = pp_for_future {
consent.future_privacy = pp_for_future; consent.future_privacy = pp_for_future;
} }
}
UserChoice::AcceptEULA => { UserChoice::AcceptEULA => {
consent.accepted_eula = true; consent.accepted_eula = true;
if let Some(_) = eula_for_future {
consent.future_eula = eula_for_future; consent.future_eula = eula_for_future;
} }
}
UserChoice::Deny => {} UserChoice::Deny => {}
} }
consent consent
} }
#[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,
current_focus: Focus,
state: (bool, bool),
update_needed: bool,
downgrade_scenario: bool,
) {
let cancel_text = if update_needed {
"[Q] Quit"
} else {
"[Q] Not now"
};
let continue_text = if downgrade_scenario {
"Downgrade"
} else {
"Continue"
};
let buttons = vec![
(cancel_text, Focus::Cancel),
(continue_text, Focus::Continue),
("Continue with Tensamin Services", Focus::ContinueAll),
];
let padding = 2;
let min_widths: Vec<u16> = 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 is_focused = current_focus == *focus;
let style = match focus {
Focus::Cancel => {
if is_focused {
Style::default()
.fg(Color::Black)
.bg(Color::Red)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::Red)
}
}
Focus::Continue => {
if is_focused && state.0 {
Style::default()
.fg(Color::Black)
.bg(Color::Green)
.add_modifier(Modifier::BOLD)
} else if state.0 {
Style::default().fg(Color::Green)
} else {
Style::default().fg(Color::DarkGray)
}
}
Focus::ContinueAll => {
if is_focused && state.1 {
Style::default()
.fg(Color::Black)
.bg(Color::Green)
.add_modifier(Modifier::BOLD)
} else if state.1 {
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<u16> {
let mut widths = vec![0; min_widths.len()];
let mut remaining: Vec<usize> = (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
}