[WIP] Terms update support

This commit is contained in:
Alex Emmet 2026-02-05 23:55:02 +01:00
commit 3574aca541
8 changed files with 584 additions and 115 deletions

View file

@ -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;

View file

@ -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 {
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<Doc>,
pub accepted_eula: bool,
pub tos: Option<Doc>,
pub accepted_tos: bool,
pub pp: Option<Doc>,
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<String, ()> {
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/\
", 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={}\
\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
))
} else {
Err(())
", 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 {
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()
let unix: u64 = unix.parse::<u64>().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)),
}
pub fn can_continue(&self) -> bool {
self.eula
}
pub fn can_continue_all(&self) -> bool {
self.eula && self.pp && self.tos
.sanitize()
}
}

View file

@ -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()
}

View file

@ -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

View file

@ -4,3 +4,4 @@ pub mod focus;
pub mod md_viewer;
pub mod terms_checker;
pub mod terms_getter;
pub mod terms_updater;

View file

@ -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),
};

View file

@ -2,7 +2,7 @@ use json::JsonValue::Object;
use crate::terms::doc::Doc;
#[derive(Clone)]
#[derive(Clone, Debug)]
pub enum Type {
EULA,
TOS,

422
src/terms/terms_updater.rs Normal file
View file

@ -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<i16>, 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<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 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<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
}