[WIP] Consent UI, via main UI render System

This commit is contained in:
Alex Emmet 2026-02-18 18:35:56 +01:00
commit a0fddf723f
18 changed files with 1625 additions and 1040 deletions

View file

@ -10,7 +10,6 @@ use ratatui::{
widgets::{Block, Borders, Paragraph, Wrap},
};
use std::any::Any;
use std::sync::Arc;
#[derive(Clone)]
pub struct UiLogEntry {
@ -89,13 +88,13 @@ impl InteractableElement for LogCard {
if self.scroll > 0 {
self.scroll -= 1;
}
InteractionResult::Handeled
InteractionResult::Handled
}
KeyCode::Down => {
self.scroll += 1;
InteractionResult::Handeled
InteractionResult::Handled
}
_ => InteractionResult::Unhandeled,
_ => InteractionResult::Unhandled,
}
}

66
src/gui/input_handler.rs Normal file
View file

@ -0,0 +1,66 @@
use crate::ACTIVE_TASKS;
use crate::gui::ui::UI;
use crate::{RELOAD, SHUTDOWN};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, poll, read};
use std::sync::Arc;
use std::time::Duration;
pub fn setup_input_handler(ui: Arc<UI>) {
tokio::spawn(async move {
ACTIVE_TASKS
.lock()
.unwrap()
.push("Input Handler".to_string());
loop {
{
let should_shutdown = *SHUTDOWN.read().await;
if should_shutdown {
break;
}
}
let has_event = match poll(Duration::from_millis(100)) {
Ok(true) => true,
Ok(false) => false,
Err(_) => false,
};
if has_event {
match read() {
Ok(event) => {
if let Event::Key(key_event) = event {
if key_event.kind == KeyEventKind::Press {
let ui_clone = ui.clone();
handle_input(key_event, ui_clone).await;
}
}
}
Err(_) => (),
}
}
}
{
let mut tasks = ACTIVE_TASKS.lock().unwrap();
tasks.retain(|t| t != "Input Handler");
}
});
}
pub async fn handle_input(key: KeyEvent, ui: Arc<UI>) {
match (key.code, key.modifiers) {
(KeyCode::Char('q'), KeyModifiers::CONTROL) => {
*SHUTDOWN.write().await = true;
}
(KeyCode::Char('c'), KeyModifiers::CONTROL) => {
*SHUTDOWN.write().await = true;
}
(KeyCode::Char('r'), KeyModifiers::CONTROL) => {
*RELOAD.write().await = true;
*SHUTDOWN.write().await = true;
}
_ => {
ui.handle_input(key).await;
}
}
}

View file

@ -5,14 +5,15 @@ use std::pin::Pin;
use crate::gui::screens::screens::Screen;
pub enum InteractionResult {
CloseScreen,
OpenScreen {
screen: Box<dyn Screen>,
},
OpenFutureScreen {
screen: Pin<Box<dyn Future<Output = Box<dyn Screen>> + Send>>,
},
Handeled,
Unhandeled,
Handled,
Unhandled,
}
impl Debug for InteractionResult {
@ -20,8 +21,9 @@ impl Debug for InteractionResult {
match self {
InteractionResult::OpenScreen { screen: _ } => write!(f, "OpenScreen"),
InteractionResult::OpenFutureScreen { screen: _ } => write!(f, "OpenFutureScreen"),
InteractionResult::Handeled => write!(f, "Handeled"),
InteractionResult::Unhandeled => write!(f, "Unhandeled"),
InteractionResult::CloseScreen => write!(f, "CloseScreen"),
InteractionResult::Handled => write!(f, "Handled"),
InteractionResult::Unhandled => write!(f, "Unhandled"),
}
}
}

View file

@ -4,7 +4,10 @@ pub mod elements {
}
pub mod screens {
pub mod screens;
pub mod terms_checker;
pub mod terms_updater;
}
pub mod app_state;
pub mod input_handler;
pub mod interaction_result;
pub mod ui;

View file

@ -0,0 +1,353 @@
use crate::{
gui::{interaction_result::InteractionResult, screens::screens::Screen, ui::UI},
terms::{
buttons::{checkbox, draw_buttons},
consent_state::UserChoice,
focus::Focus,
md_viewer::FileViewer,
terms_getter::{Type, get_link, get_terms},
},
};
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::{
Frame,
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Style},
text::{Line, Span, Text},
widgets::{Block, Borders, Paragraph},
};
use std::{any::Any, pin::Pin, sync::Arc};
use tokio::sync::oneshot;
pub struct TermsCheckerScreen {
ui: Arc<UI>,
sender: Option<oneshot::Sender<UserChoice>>,
eula: bool,
tos: bool,
pp: bool,
focus: Focus,
}
impl TermsCheckerScreen {
pub fn new(ui: Arc<UI>, sender: Option<oneshot::Sender<UserChoice>>) -> Self {
Self {
ui,
sender,
eula: false,
tos: false,
pp: false,
focus: Focus::Eula,
}
}
}
impl Screen for TermsCheckerScreen {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn get_ui(&self) -> &Arc<UI> {
&self.ui
}
fn render(&self, f: &mut Frame, size: Rect) {
let mut needed_height = 5;
if size.height < 6 || size.width < 27 {
f.render_widget(
Line::from(format!("too small {}/63 by {}/13", size.width, size.height)),
size,
);
return;
}
let max_width = 150;
let max_height = 26;
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![8, 3, 8, 5],
vec![
"",
"By selecting Continue, you confirm that you 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![9, 3, 9, 5],
vec![
"",
"By selecting Continue, you confirm that you 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![9, 3, 9, 5],
vec![
"",
"By selecting Continue, you confirm that you 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![10, 3, 11, 5],
vec![
"",
"By selecting Continue, you confirm that you 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, self.eula, self.focus == Focus::Eula, true),
checkbox(tos_text, self.tos, self.focus == Focus::Tos, self.eula),
checkbox(pp_text, self.pp, self.focus == Focus::Pp, self.eula),
Line::from(""),
Line::from("¹ Necessary required to run the program"),
Line::from("² Optional required only for Tensamin services"),
];
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 > 19 {
Style::default().fg(Color::Green)
} else if size.height >= 13 {
Style::default().fg(Color::Yellow)
} else {
Style::default().fg(Color::Red)
};
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!(" / 13")),
]),
]);
let warning = Paragraph::new(warning_text)
.alignment(Alignment::Center)
.block(
Block::default()
.borders(Borders::ALL)
.title(format!("UI Too Small [Q to Quit]")),
);
f.render_widget(warning, size);
return;
}
let consent_block = Paragraph::new(Text::from(text_lines)).block(
Block::default()
.title(format!(" Tensamin User Consent [Q to Quit] ",))
.borders(Borders::ALL),
);
f.render_widget(consent_block, chunks[0]);
draw_buttons(
f,
chunks[1],
self.focus,
(self.eula, self.tos && self.pp),
true,
false,
true,
);
}
fn handle_input(&mut self, event: KeyEvent) -> InteractionResult {
let mut possible_states = vec![Focus::Eula, Focus::Tos, Focus::Pp, Focus::Cancel];
if self.eula {
possible_states.push(Focus::Continue);
if self.tos && self.pp {
possible_states.push(Focus::ContinueAll);
}
}
match event.code {
KeyCode::Esc => {
if let Some(sender) = self.sender.take() {
let _ = sender.send(UserChoice::Deny);
}
InteractionResult::CloseScreen
}
KeyCode::Up | KeyCode::Left => {
self.focus.prev(&possible_states);
InteractionResult::Handled
}
KeyCode::Down | KeyCode::Right | KeyCode::Tab => {
self.focus.next(&possible_states);
InteractionResult::Handled
}
KeyCode::Char('q') | KeyCode::Char('Q') => {
if let Some(sender) = self.sender.take() {
let _ = sender.send(UserChoice::Deny);
}
InteractionResult::CloseScreen
}
KeyCode::Char('o') | KeyCode::Char('O') => {
let terms_type = match self.focus {
Focus::Eula => Some(Type::EULA),
Focus::Tos => Some(Type::TOS),
Focus::Pp => Some(Type::PP),
_ => None,
};
if let Some(terms_type) = terms_type {
let ui = self.ui.clone();
let fut: Pin<Box<dyn Future<Output = Box<dyn Screen>> + Send>> =
Box::pin(async move {
let content = get_terms(terms_type.clone()).await.unwrap();
let screen: FileViewer =
FileViewer::new(ui.clone(), terms_type.to_string(), &content);
Box::new(screen) as Box<dyn Screen>
});
InteractionResult::OpenFutureScreen { screen: fut }
} else {
InteractionResult::Unhandled
}
}
KeyCode::Char('l') | KeyCode::Char('L') => match self.focus {
Focus::Eula => {
let _ = open::that(get_link(Type::EULA));
InteractionResult::Handled
}
Focus::Tos => {
let _ = open::that(get_link(Type::TOS));
InteractionResult::Handled
}
Focus::Pp => {
let _ = open::that(get_link(Type::PP));
InteractionResult::Handled
}
_ => InteractionResult::Unhandled,
},
KeyCode::Enter | KeyCode::Char(' ') => match self.focus {
Focus::Eula => {
self.eula = !self.eula;
self.tos = false;
self.pp = false;
InteractionResult::Handled
}
Focus::Tos if self.eula => {
self.tos = !self.tos;
InteractionResult::Handled
}
Focus::Pp if self.eula => {
self.pp = !self.pp;
InteractionResult::Handled
}
Focus::Cancel => {
if let Some(sender) = self.sender.take() {
let _ = sender.send(UserChoice::Deny);
}
InteractionResult::CloseScreen
}
Focus::Continue if self.eula => {
if let Some(sender) = self.sender.take() {
let _ = sender.send(UserChoice::AcceptEULA);
}
InteractionResult::CloseScreen
}
Focus::ContinueAll if self.eula && self.tos && self.pp => {
if let Some(sender) = self.sender.take() {
let _ = sender.send(UserChoice::AcceptAll);
}
InteractionResult::CloseScreen
}
_ => InteractionResult::Unhandled,
},
_ => InteractionResult::Unhandled,
}
}
}

View file

@ -0,0 +1,701 @@
use crate::{
gui::{interaction_result::InteractionResult, screens::screens::Screen, ui::UI},
terms::{
buttons::{checkbox, draw_buttons},
consent_state::{UpdateDecision, UserChoice},
doc::Doc,
focus::Focus,
md_viewer::FileViewer,
terms_getter::{Type, get_link, get_terms},
},
};
use chrono::{Local, TimeZone, Utc};
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::{
Frame,
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Style},
text::{Line, Span, Text},
widgets::{Block, Borders, Paragraph},
};
use std::any::Any;
use std::sync::Arc;
use tokio::sync::oneshot;
pub struct TermsUpdaterScreen {
ui: Arc<UI>,
sender: Option<oneshot::Sender<UserChoice>>,
eula_needed: bool,
tos_needed: bool,
pp_needed: bool,
eula_future: bool,
tos_future: bool,
pp_future: bool,
eula_for_future: Option<Doc>,
tos_for_future: Option<Doc>,
pp_for_future: Option<Doc>,
update_needed: bool,
eula: bool,
tos: bool,
pp: bool,
focus: Focus,
}
impl TermsUpdaterScreen {
pub fn new(
ui: Arc<UI>,
consent_eula: UpdateDecision,
consent_tos: UpdateDecision,
consent_pp: UpdateDecision,
sender: Option<oneshot::Sender<UserChoice>>,
) -> Self {
let (eula_needed, eula_future, eula_for_future): (bool, bool, Option<Doc>) =
match consent_eula {
UpdateDecision::NoChange => (false, false, None),
UpdateDecision::Future { newest } => (true, true, Some(newest)),
UpdateDecision::Forced(doc) => (true, false, Some(doc)),
};
let (tos_needed, tos_future, tos_for_future): (bool, bool, Option<Doc>) = match consent_tos
{
UpdateDecision::NoChange => (false, false, None),
UpdateDecision::Future { newest } => (true, true, Some(newest)),
UpdateDecision::Forced(doc) => (true, false, Some(doc)),
};
let (pp_needed, pp_future, pp_for_future): (bool, bool, Option<Doc>) = match consent_pp {
UpdateDecision::NoChange => (false, false, None),
UpdateDecision::Future { newest } => (true, true, Some(newest)),
UpdateDecision::Forced(doc) => (true, false, Some(doc)),
};
let focus = if eula_needed {
Focus::Eula
} else if tos_needed {
Focus::Tos
} else if pp_needed {
Focus::Pp
} else {
Focus::Cancel
};
let update_needed = (eula_needed && !eula_future)
|| (tos_needed && !tos_future)
|| (pp_needed && !pp_future);
Self {
ui,
sender,
eula_needed,
tos_needed,
pp_needed,
eula_for_future,
tos_for_future,
pp_for_future,
eula_future,
tos_future,
pp_future,
update_needed,
eula: !eula_needed,
tos: !tos_needed,
pp: !pp_needed,
focus,
}
}
}
impl Screen for TermsUpdaterScreen {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn get_ui(&self) -> &Arc<UI> {
&self.ui
}
fn render(&self, f: &mut Frame, size: Rect) {
let mut needed_height = 5;
if size.height < 6 || size.width < 27 {
f.render_widget(
Line::from(format!("too small {}/63 by {}/15", size.width, size.height)),
size,
);
return;
}
let max_width = 150;
let max_height = 30;
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 mut text_lines: Vec<Line> = Vec::new();
let mut header_lines = 0;
let separator = 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 self.eula_needed {
header_lines += 1;
if self.eula_future {
if size.width < 80 {
text_lines.push(checkbox(
"EULA ¹³ (https://legal.tensamin.net/eula/newest/)",
self.eula,
self.focus == Focus::Eula,
true,
));
header_lines += 1;
let unix_timestamp = self.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 {
text_lines.push(checkbox(
"End User Licence Agreement ¹³ (https://legal.tensamin.net/eula/newest/)",
self.eula,
self.focus == Focus::Eula,
true,
));
header_lines += 1;
let unix_timestamp = self.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 {
if size.width < 80 {
text_lines.push(checkbox(
"EULA ¹ (https://legal.tensamin.net/eula/newest/)",
self.eula,
self.focus == Focus::Eula,
true,
));
} else {
text_lines.push(checkbox(
"End User Licence Agreement ¹ (https://legal.tensamin.net/eula/newest/)",
self.eula,
self.focus == Focus::Eula,
true,
));
}
}
}
if self.tos_needed {
header_lines += 1;
if self.tos_future {
if size.width < 80 {
text_lines.push(checkbox(
"ToS ²³ (https://legal.tensamin.net/tos/newest/)",
self.tos,
self.focus == Focus::Tos,
self.eula,
));
header_lines += 1;
let unix_timestamp = self.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 {
text_lines.push(checkbox(
"Terms of Service ²³ (https://legal.tensamin.net/terms-of-service/newest/)",
self.tos,
self.focus == Focus::Tos,
self.eula,
));
header_lines += 1;
let unix_timestamp = self.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 {
if size.width < 80 {
text_lines.push(checkbox(
"ToS ² (https://legal.tensamin.net/tos/newest/)",
self.tos,
self.focus == Focus::Tos,
self.eula,
));
} else {
text_lines.push(checkbox(
"Terms of Service ² (https://legal.tensamin.net/terms-of-service/newest/)",
self.tos,
self.focus == Focus::Tos,
self.eula,
));
}
}
}
if self.pp_needed {
header_lines += 1;
if self.pp_future {
if size.width < 80 {
text_lines.push(checkbox(
"PP ²³ (https://legal.tensamin.net/privacy-policy/newest/)",
self.pp,
self.focus == Focus::Pp,
self.eula,
));
header_lines += 1;
let unix_timestamp = self.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 {
text_lines.push(checkbox(
"Privacy Policy ²³ (https://legal.tensamin.net/privacy-policy/newest/)",
self.pp,
self.focus == Focus::Pp,
self.eula,
));
header_lines += 1;
let unix_timestamp = self.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 {
if size.width < 80 {
text_lines.push(checkbox(
"PP ² (https://legal.tensamin.net/privacy-policy/newest/)",
self.pp,
self.focus == Focus::Pp,
self.eula,
));
} else {
text_lines.push(checkbox(
"Privacy Policy ² (https://legal.tensamin.net/privacy-policy/newest/)",
self.pp,
self.focus == Focus::Pp,
self.eula,
));
}
}
}
text_lines.push(Line::from(""));
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(""));
let mut optional_lines: Vec<i16> = if size.width > 143 {
if self.tos_needed || self.pp_needed {
text_lines.push(Line::from("By selecting Downgrade, you confirm that you 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 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 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 self.tos_needed || self.pp_needed {
vec![
header_lines + 7,
header_lines,
header_lines + 7,
separator,
header_lines + 2,
]
} else {
vec![
header_lines + 6,
header_lines,
header_lines + 6,
separator,
header_lines + 2,
]
}
} else if size.width > 92 {
if self.tos_needed || self.pp_needed {
text_lines.push(Line::from(
"By selecting Continue, you confirm that you 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 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 self.tos_needed || self.pp_needed {
vec![
header_lines + 8,
header_lines,
header_lines + 8,
separator,
header_lines + 2,
]
} else {
vec![
header_lines + 7,
header_lines,
header_lines + 7,
separator,
header_lines + 2,
]
}
} else if size.width > 73 {
if self.tos_needed || self.pp_needed {
text_lines.push(Line::from(
"By selecting Continue, you confirm that you 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 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 self.tos_needed || self.pp_needed {
vec![
header_lines + 8,
header_lines,
header_lines + 8,
separator,
header_lines + 2,
]
} else {
vec![
header_lines + 7,
header_lines,
header_lines + 7,
separator,
header_lines + 2,
]
}
} else {
if self.tos_needed || self.pp_needed {
text_lines.push(Line::from("By selecting Continue, you confirm that you"));
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"));
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 self.tos_needed || self.pp_needed {
vec![
header_lines + 10,
header_lines,
header_lines + 11,
separator,
header_lines + 2,
]
} else {
vec![
header_lines + 8,
header_lines,
header_lines + 9,
separator,
header_lines + 2,
]
}
};
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();
let q_informer = if self.update_needed {
"Q to Exit"
} else {
"Q to Cancel"
};
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 > 20 {
Style::default().fg(Color::Green)
} else if size.height >= (header_lines as u16 + 10) {
Style::default().fg(Color::Yellow)
} else {
Style::default().fg(Color::Red)
};
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!(" / {}", header_lines + 10)),
]),
]);
let warning = Paragraph::new(warning_text)
.alignment(Alignment::Center)
.block(
Block::default()
.borders(Borders::ALL)
.title(format!("UI Too Small [{}]", q_informer)),
);
f.render_widget(warning, size);
return;
}
let consent_block = Paragraph::new(Text::from(text_lines)).block(
Block::default()
.title(format!(" Update Tensamin User Consent [{}] ", q_informer))
.borders(Borders::ALL),
);
f.render_widget(consent_block, chunks[0]);
let downgrade_scenario = self.tos_needed || self.pp_needed;
draw_buttons(
f,
chunks[1],
self.focus,
(self.eula, self.tos && self.pp),
self.update_needed,
downgrade_scenario,
self.pp_needed || self.tos_needed,
);
}
fn handle_input(&mut self, event: KeyEvent) -> InteractionResult {
let mut possible_states = Vec::new();
if self.eula_needed {
possible_states.push(Focus::Eula);
}
if self.tos_needed {
possible_states.push(Focus::Tos);
}
if self.pp_needed {
possible_states.push(Focus::Pp);
}
possible_states.push(Focus::Cancel);
if self.eula {
possible_states.push(Focus::Continue);
if self.tos && self.pp {
possible_states.push(Focus::ContinueAll);
}
}
match event.code {
KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => {
if let Some(sender) = self.sender.take() {
let _ = sender.send(UserChoice::Deny);
}
return InteractionResult::CloseScreen;
}
KeyCode::Enter | KeyCode::Char(' ') => match self.focus {
Focus::Eula => {
self.eula = !self.eula;
self.tos = !self.tos_needed;
self.pp = !self.pp_needed;
InteractionResult::Handled
}
Focus::Tos if self.eula => {
self.tos = !self.tos;
InteractionResult::Handled
}
Focus::Pp if self.eula => {
self.pp = !self.pp;
InteractionResult::Handled
}
Focus::Cancel => {
if let Some(sender) = self.sender.take() {
let _ = sender.send(UserChoice::Deny);
}
InteractionResult::CloseScreen
}
Focus::Continue if self.eula => {
if let Some(sender) = self.sender.take() {
let _ = sender.send(UserChoice::AcceptEULA);
}
InteractionResult::CloseScreen
}
Focus::ContinueAll if self.eula && self.tos && self.pp => {
if let Some(sender) = self.sender.take() {
let _ = sender.send(UserChoice::AcceptAll);
}
InteractionResult::CloseScreen
}
_ => InteractionResult::Unhandled,
},
KeyCode::Char('o') | KeyCode::Char('O') => {
let terms_type = match self.focus {
Focus::Eula => Some(Type::EULA),
Focus::Tos => Some(Type::TOS),
Focus::Pp => Some(Type::PP),
_ => None,
};
if let Some(terms_type) = terms_type {
let ui = self.ui.clone();
let fut = Box::pin(async move {
let content = get_terms(terms_type.clone()).await.unwrap();
Box::new(FileViewer::new(
ui.clone(),
terms_type.to_string(),
&content,
)) as Box<dyn Screen>
});
return InteractionResult::OpenFutureScreen { screen: fut };
} else {
InteractionResult::Unhandled
}
}
KeyCode::Char('l') | KeyCode::Char('L') => match self.focus {
Focus::Eula => {
let _ = open::that(get_link(Type::EULA));
InteractionResult::Handled
}
Focus::Tos => {
let _ = open::that(get_link(Type::TOS));
InteractionResult::Handled
}
Focus::Pp => {
let _ = open::that(get_link(Type::PP));
InteractionResult::Handled
}
_ => InteractionResult::Unhandled,
},
KeyCode::Up | KeyCode::Left => {
self.focus.prev(&possible_states);
InteractionResult::Handled
}
KeyCode::Down | KeyCode::Right | KeyCode::Tab => {
self.focus.next(&possible_states);
InteractionResult::Handled
}
_ => InteractionResult::Unhandled,
}
}
}

View file

@ -1,197 +1,64 @@
use crossterm::{
cursor::MoveTo,
execute,
terminal::{Clear, ClearType, EnterAlternateScreen},
};
use ratatui::{
Terminal,
backend::{Backend, CrosstermBackend},
layout::Size,
};
use std::{
io::{self, Error},
sync::{Arc, Mutex},
};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crate::gui::{interaction_result::InteractionResult, screens::screens::Screen};
struct ResizableBackend<'a> {
backend: CrosstermBackend<&'a mut Vec<u8>>,
size: Size,
}
impl<'a> ResizableBackend<'a> {
fn new(buf: &'a mut Vec<u8>, width: u16, height: u16) -> Self {
Self {
backend: CrosstermBackend::new(buf),
size: Size::new(width, height),
}
}
}
impl<'a> Backend for ResizableBackend<'a> {
type Error = Error;
fn draw<'b, I>(&mut self, content: I) -> io::Result<()>
where
I: Iterator<Item = (u16, u16, &'b ratatui::buffer::Cell)>,
{
self.backend.draw(content)
}
fn hide_cursor(&mut self) -> io::Result<()> {
self.backend.hide_cursor()
}
fn show_cursor(&mut self) -> io::Result<()> {
self.backend.show_cursor()
}
#[allow(deprecated)]
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
self.backend.get_cursor()
}
#[allow(deprecated)]
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
self.backend.set_cursor(x, y)
}
fn clear(&mut self) -> io::Result<()> {
self.backend.clear()
}
fn clear_region(&mut self, region: ratatui::backend::ClearType) -> io::Result<()> {
self.backend.clear_region(region)
}
fn size(&self) -> Result<Size, Error> {
Ok(self.size)
}
fn flush(&mut self) -> io::Result<()> {
self.backend.flush()
}
fn get_cursor_position(&mut self) -> Result<ratatui::prelude::Position, Self::Error> {
todo!()
}
fn set_cursor_position<P: Into<ratatui::prelude::Position>>(
&mut self,
_position: P,
) -> Result<(), Self::Error> {
todo!()
}
fn window_size(&mut self) -> Result<ratatui::prelude::backend::WindowSize, Self::Error> {
todo!()
}
}
use crossterm::event::KeyEvent;
use ratatui::{Terminal, backend::CrosstermBackend, init};
use std::sync::{Arc, Mutex};
use tokio::sync::RwLock;
/// UI state and rendering
pub struct UI {
pub cols: Arc<Mutex<u16>>,
pub rows: Arc<Mutex<u16>>,
screen: Arc<Mutex<Option<Box<dyn Screen>>>>,
cached_render: Arc<Mutex<Vec<u8>>>,
pub terminal: Arc<Mutex<Terminal<CrosstermBackend<std::io::Stdout>>>>,
screen: Arc<RwLock<Option<Box<dyn Screen>>>>,
}
impl UI {
pub fn new(cols: u16, rows: u16) -> Self {
pub fn new() -> Self {
let terminal = init();
Self {
cols: Arc::new(Mutex::new(cols)),
rows: Arc::new(Mutex::new(rows)),
screen: Arc::new(Mutex::new(None)),
cached_render: Arc::new(Mutex::new(Vec::new())),
terminal: Arc::new(Mutex::new(terminal)),
screen: Arc::new(RwLock::new(None)),
}
}
pub fn set_screen(&self, screen: Box<dyn Screen>) {
*self.screen.lock().unwrap() = Some(screen);
pub async fn set_screen(&self, screen: Box<dyn Screen>) {
*self.screen.write().await = Some(screen);
}
pub fn resize(&self, cols: u32, rows: u32) {
*self.cols.lock().unwrap() = cols as u16;
*self.rows.lock().unwrap() = rows as u16;
}
pub async fn handle_input(&self, input: &[u8]) {
let key_event = if input.len() == 1 {
let c = input[0] as char;
if c.is_ascii() {
Some(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE))
pub async fn handle_input(self: Arc<Self>, key_event: KeyEvent) {
let result = {
let mut guard = self.screen.write().await;
if let Some(screen) = guard.as_mut() {
screen.handle_input(key_event)
} else {
None
return;
}
} else {
None
};
let event = match input {
b"\x1b[A" => Some(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE)),
b"\x1b[B" => Some(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)),
b"\x1b[C" => Some(KeyEvent::new(KeyCode::Right, KeyModifiers::NONE)),
b"\x1b[D" => Some(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE)),
b"\r" | b"\n" => Some(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)),
b"\x7f" => Some(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE)),
_ => key_event,
};
if let Some(event) = event {
let result = {
let mut guard = self.screen.lock().unwrap();
if let Some(screen) = guard.as_mut() {
screen.handle_input(event)
} else {
return;
}
};
match result {
InteractionResult::OpenScreen { screen } => {
self.set_screen(screen);
}
InteractionResult::OpenFutureScreen { screen: fut } => {
let ui = self.clone();
let screen = fut.await;
ui.set_screen(screen);
}
_ => {}
match result {
InteractionResult::OpenScreen { screen } => {
self.set_screen(screen).await;
}
InteractionResult::OpenFutureScreen { screen: fut } => {
let ui = self.clone();
let screen = fut.await;
ui.set_screen(screen).await;
}
InteractionResult::CloseScreen => {
/* TODO
self.set_screen();
*/
}
InteractionResult::Handled => {}
InteractionResult::Unhandled => {}
}
}
pub fn render(&self) -> Vec<u8> {
let mut buf = Vec::new();
execute!(
&mut buf,
EnterAlternateScreen,
Clear(ClearType::All),
MoveTo(0, 0)
)
.unwrap();
{
let backend = ResizableBackend::new(
&mut buf,
*self.cols.lock().unwrap(),
*self.rows.lock().unwrap(),
);
let mut terminal = Terminal::new(backend).unwrap();
if let Some(screen) = self.screen.lock().unwrap().as_ref() {
let _ = terminal.draw(|f| screen.render(f, f.area()));
}
pub async fn render(&self) {
if let Some(screen) = self.screen.read().await.as_ref() {
let mut terminal = self.terminal.lock().unwrap();
terminal
.draw(|f| {
screen.render(f, f.area());
})
.unwrap();
}
*self.cached_render.lock().unwrap() = buf.clone();
buf
}
}

View file

@ -20,11 +20,12 @@ mod util;
use crate::communities::community_manager;
use crate::communities::interactables::registry;
use crate::gui::app_state::AppState;
use crate::gui::ui;
use crate::gui::input_handler::setup_input_handler;
use crate::gui::ui::UI;
use crate::langu::language_creator;
use crate::omikron::omikron_connection::{OMIKRON_CONNECTION, OmikronConnection};
use crate::server::server::start;
use crate::terms::consent_state;
use crate::terms::consent_state::ConsentManager;
use crate::users::user_manager;
use crate::util::config_util::CONFIG;
use crate::util::file_util::download_and_extract_zip;
@ -45,17 +46,29 @@ async fn main() {
*RELOAD.write().await = false;
*SHUTDOWN.write().await = false;
let tui = ui::UI::new(100, 100);
tokio::spawn(async move { tui.render() });
let ui = Arc::new(UI::new());
setup_input_handler(ui.clone());
// EULA
let (tos, pp) = consent_state::ConsentManager::check().await;
if !tos {
let ui_clone = ui.clone();
tokio::spawn(async move {
loop {
ui_clone.render().await;
sleep(Duration::from_millis(16)).await;
}
});
let (eula, tos_pp) = ConsentManager::check(ui.clone()).await;
if !eula {
ui.terminal.lock().unwrap().clear();
ui.terminal.lock().unwrap().flush();
println!("You need to accept our End User Licence Agreement before launching!");
println!("You can find this at 'agreements'!");
return;
}
if !pp {
if !tos_pp {
ui.terminal.lock().unwrap().clear();
ui.terminal.lock().unwrap().flush();
println!(
"Please accept our Privacy Policy & Terms of Serivce before using Tensamin Services!"
);
@ -171,5 +184,7 @@ async fn main() {
community_manager::clear();
*APP_STATE.lock().unwrap() = AppState::new();
}
ui.terminal.lock().unwrap().clear();
ui.terminal.lock().unwrap().flush();
}
}

View file

@ -1,66 +1,137 @@
use tokio::sync::oneshot;
use crate::{
gui::{
screens::{terms_checker::TermsCheckerScreen, terms_updater::TermsUpdaterScreen},
ui::UI,
},
terms::{
doc::Doc,
terms_checker,
terms_getter::{Type, get_current_docs, get_newest_docs},
terms_updater,
},
util::file_util::{load_file, save_file},
};
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
pub struct ConsentManager;
impl ConsentManager {
pub async fn check() -> (bool, bool) {
pub async fn check(ui: Arc<UI>) -> (bool, bool) {
let (tx, rx) = oneshot::channel();
let file = load_file("", "agreements");
let (mut file_state, changed) = ConsentState::from_str(&file).sanitize();
let mut file_state = ConsentState::from_str(&file).sanitize();
ui.set_screen(Box::new(TermsCheckerScreen::new(ui.clone(), Some(tx))))
.await;
if changed {
save_file("", "agreements", &file_state.to_string());
let result = rx.await.unwrap_or(UserChoice::Deny);
match result {
UserChoice::AcceptEULA | UserChoice::AcceptAll => {
if let Some((current_eula, current_tos, current_privacy)) = get_current_docs().await
{
file_state.accepted_eula = true;
file_state.eula = Some(current_eula);
if matches!(result, UserChoice::AcceptAll) {
file_state.accepted_tos = true;
file_state.accepted_pp = true;
file_state.tos = Some(current_tos);
file_state.privacy = Some(current_privacy);
}
}
}
UserChoice::Deny => {
return (false, false);
}
}
if !file_state.accepted_eula {
file_state = terms_checker::run_consent_ui(file_state).await;
save_file("", "agreements", &file_state.to_string());
}
if !file_state.accepted_eula {
return (false, false);
}
save_file("", "agreements", &file_state.to_string());
if let Some((eula_update, tos_update, privacy_update)) = Self::get_updates().await {
let _is_forced = matches!(eula_update, UpdateDecision::Forced(_))
|| matches!(tos_update, UpdateDecision::Forced(_))
|| matches!(privacy_update, UpdateDecision::Forced(_));
let updater_logic = async move {
let (state_to_update, _) = terms_updater::run_consent_ui(
file_state,
eula_update,
tos_update,
privacy_update,
)
.await
.sanitize();
if let Some((eula_update, tos_update, privacy_update)) = Self::get_updates().await {
let is_forced = matches!(eula_update, UpdateDecision::Forced(_))
|| matches!(tos_update, UpdateDecision::Forced(_))
|| matches!(privacy_update, UpdateDecision::Forced(_));
save_file("", "agreements", &state_to_update.to_string());
};
// ======================================================== //
// TODO: Implement UI Drawing order, draw update As PoPup //
// ======================================================== //
//if is_forced {
updater_logic.await;
//} else {
//task::spawn(updater_logic);
//}
let (tx, rx) = oneshot::channel();
ui.set_screen(Box::new(TermsUpdaterScreen::new(
ui.clone(),
eula_update.clone(),
tos_update.clone(),
privacy_update.clone(),
Some(tx),
)))
.await;
if is_forced {
let result = rx.await.unwrap_or(UserChoice::Deny);
let file = load_file("", "agreements");
let mut state = ConsentState::from_str(&file).sanitize();
match result {
UserChoice::AcceptAll => {
state.accepted_eula = true;
state.accepted_tos = true;
state.accepted_pp = true;
}
UserChoice::AcceptEULA => {
state.accepted_eula = true;
}
UserChoice::Deny => {
return (false, false);
}
}
let state = state.sanitize();
save_file("", "agreements", &state.to_string());
} else {
tokio::spawn(async move {
let result = rx.await.unwrap_or(UserChoice::Deny);
let file = load_file("", "agreements");
let mut state = ConsentState::from_str(&file).sanitize();
match result {
UserChoice::AcceptAll => {
state.future_eula = match eula_update {
UpdateDecision::Future { newest } => Some(newest),
_ => state.future_eula,
};
state.future_tos = match tos_update {
UpdateDecision::Future { newest } => Some(newest),
_ => state.future_tos,
};
state.future_privacy = match privacy_update {
UpdateDecision::Future { newest } => Some(newest),
_ => state.future_privacy,
};
}
UserChoice::AcceptEULA => {
if let UpdateDecision::Future { newest } = eula_update {
state.future_eula = Some(newest);
}
}
UserChoice::Deny => {}
}
let state = state.sanitize();
save_file("", "agreements", &state.to_string());
});
}
}
}
let file = load_file("", "agreements");
let (final_state, changed) = ConsentState::from_str(&file).sanitize();
if changed {
save_file("", "agreements", &final_state.to_string());
}
let final_state = ConsentState::from_str(&file).sanitize();
save_file("", "agreements", &final_state.to_string());
(
final_state.accepted_eula,
final_state.accepted_tos && final_state.accepted_pp,
@ -76,10 +147,9 @@ impl ConsentManager {
UpdateDecision,
)> {
let file = load_file("", "agreements");
let (accepted_state, changed) = ConsentState::from_str(&file).sanitize();
if changed {
save_file("", "agreements", &accepted_state.to_string());
}
let accepted_state = ConsentState::from_str(&file).sanitize();
save_file("", "agreements", &accepted_state.to_string());
if let (
Some((current_eula, current_tos, current_privacy)),
Some((newest_eula, newest_tos, newest_privacy)),
@ -186,8 +256,7 @@ pub struct ConsentState {
}
impl ConsentState {
fn sanitize(mut self) -> (Self, bool) {
let mut changed = false;
fn sanitize(mut self) -> Self {
let current_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
@ -197,30 +266,26 @@ impl ConsentState {
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 {
self.accepted_tos = false;
self.accepted_pp = false;
changed = true;
}
(self, changed)
self
}
pub fn to_string(&self) -> String {
@ -356,7 +421,7 @@ impl ConsentState {
pp_version = v.to_string();
} else if let Some(v) = line.strip_prefix("Privacy-Policy-HASH=") {
pp_hash = v.to_string();
} else if let Some(v) = line.strip_prefix("UNIX=") {
} else if let Some(v) = line.strip_prefix("UNIX-SECOND=") {
unix = v.to_string();
} else if let Some(v) = line.strip_prefix("FUTURE-EULA-VERSION=") {
future_eula_version = v.to_string();
@ -382,7 +447,7 @@ impl ConsentState {
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_pp_time = future_pp_time.parse::<u64>().unwrap_or(0);
let (state, _) = Self {
let state = Self {
accepted_eula: eula,
eula: Some(Doc::new(eula_version, eula_hash, Type::EULA, unix)),
accepted_pp: pp,

View file

@ -1,23 +1,62 @@
use crossterm::event::{self, Event, KeyCode};
use crossterm::event::{self, Event, KeyCode, KeyEvent};
use ratatui::{
DefaultTerminal,
prelude::*,
text::{Line, Span},
widgets::{Block, Borders, Paragraph, Wrap},
};
use std::time::Duration;
use std::{any::Any, sync::Arc, time::Duration};
use crate::gui::{interaction_result::InteractionResult, screens::screens::Screen, ui::UI};
#[derive(Default)]
pub struct FileViewer {
ui: Arc<UI>,
title: String,
text: Vec<DisplayLine>,
scroll: u16,
scroll_x: u16,
}
impl Screen for FileViewer {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn get_ui(&self) -> &Arc<UI> {
&self.ui
}
fn render(&self, f: &mut Frame, rect: Rect) {
self.draw(f, rect);
}
fn handle_input(&mut self, event: KeyEvent) -> InteractionResult {
match event.code {
KeyCode::Char('q') | KeyCode::Esc => {
return InteractionResult::CloseScreen;
}
KeyCode::Down => self.scroll = self.scroll.saturating_add(1),
KeyCode::Up => self.scroll = self.scroll.saturating_sub(1),
KeyCode::PageDown => self.scroll = self.scroll.saturating_add(10),
KeyCode::PageUp => self.scroll = self.scroll.saturating_sub(10),
KeyCode::Right => self.scroll_x = self.scroll_x.saturating_add(2),
KeyCode::Left => self.scroll_x = self.scroll_x.saturating_sub(2),
_ => {}
}
InteractionResult::Unhandled
}
}
impl FileViewer {
pub fn new(title: String, content: &str) -> Self {
pub fn new(ui: Arc<UI>, title: String, content: &str) -> Self {
Self {
ui,
title,
text: parse_document(content.to_owned()),
scroll: 0,

View file

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

View file

@ -1,311 +0,0 @@
use crate::terms::{
buttons::{checkbox, draw_buttons},
consent_state::{ConsentState, UserChoice},
focus::Focus,
md_viewer::FileViewer,
terms_getter::{Type, get_current_docs, get_link, get_terms},
};
use crossterm::event::{self, Event, KeyCode};
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Style},
text::{Line, Span, Text},
widgets::{Block, Borders, Paragraph},
};
use std::time::Duration;
pub async fn run_consent_ui(mut consent: ConsentState) -> ConsentState {
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 {}/13", size.width, size.height)), size);
return;
}
let max_width = 150;
let max_height = 26;
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![8, 3, 8, 5],
vec![
"",
"By selecting Continue, you confirm that you 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![9, 3, 9, 5],
vec![
"",
"By selecting Continue, you confirm that you 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![9, 3, 9, 5],
vec![
"",
"By selecting Continue, you confirm that you 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![10, 3, 11, 5],
vec![
"",
"By selecting Continue, you confirm that you 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 required to run the program"),
Line::from("² Optional required only for Tensamin services")
];
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 > 19 {
Style::default().fg(Color::Green)
} else if size.height >= 13 {
Style::default().fg(Color::Yellow)
} else {
Style::default().fg(Color::Red)
};
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!(" / 13")),
]),
]);
let warning = Paragraph::new(warning_text)
.alignment(Alignment::Center)
.block(
Block::default()
.borders(Borders::ALL)
.title(format!("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], focus, (eula, tos && pp), true, false, true);
})
.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;
}
}
let mut possible_states = vec![Focus::Eula, Focus::Tos, Focus::Pp, Focus::Cancel];
if eula {
possible_states.push(Focus::Continue);
if tos && pp {
possible_states.push(Focus::ContinueAll);
}
}
match key.code {
KeyCode::Esc => break UserChoice::Deny,
KeyCode::Up | KeyCode::Left => focus.prev(&possible_states),
KeyCode::Down | KeyCode::Right | KeyCode::Tab => focus.next(&possible_states),
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();
if let Some((eula, tos, privacy)) = get_current_docs().await {
consent.eula = Some(eula);
consent.tos = Some(tos);
consent.privacy = Some(privacy);
}
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 = false;
consent.accepted_pp = false;
}
UserChoice::Deny => {
consent.accepted_eula = false;
consent.accepted_tos = false;
consent.accepted_pp = false;
}
};
consent
}

View file

@ -1,480 +0,0 @@
use crate::terms::{
buttons::{checkbox, draw_buttons},
consent_state::{ConsentState, UpdateDecision, UserChoice},
doc::Doc,
focus::Focus,
md_viewer::FileViewer,
terms_getter::{Type, get_newest_link, get_terms},
};
use chrono::{Local, TimeZone, Utc};
use crossterm::event::{self, Event, KeyCode};
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Style},
text::{Line, Span, Text},
widgets::{Block, Borders, Paragraph},
};
use std::time::Duration;
pub async fn run_consent_ui(
mut consent: ConsentState,
consent_eula: UpdateDecision,
consent_tos: UpdateDecision,
consent_pp: UpdateDecision,
) -> ConsentState {
let mut terminal = ratatui::init();
let (eula_needed, eula_future, eula_for_future): (bool, bool, Option<Doc>) = match consent_eula
{
UpdateDecision::NoChange => (false, false, None),
UpdateDecision::Future { newest } => (true, true, Some(newest)),
UpdateDecision::Forced(doc) => (true, false, Some(doc)),
};
let (tos_needed, tos_future, tos_for_future): (bool, bool, Option<Doc>) = match consent_tos {
UpdateDecision::NoChange => (false, false, None),
UpdateDecision::Future { newest } => (true, true, Some(newest)),
UpdateDecision::Forced(doc) => (true, false, Some(doc)),
};
let (pp_needed, pp_future, pp_for_future): (bool, bool, Option<Doc>) = match consent_pp {
UpdateDecision::NoChange => (false, false, None),
UpdateDecision::Future { newest } => (true, true, Some(newest)),
UpdateDecision::Forced(doc) => (true, false, Some(doc)),
};
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 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 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 {}/15", size.width, size.height)), size);
return;
}
let max_width = 150;
let max_height = 30;
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 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 {
header_lines += 1;
if eula_future {
if size.width < 80 {
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 {
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 {
if size.width < 80 {
text_lines.push(checkbox("EULA ¹ (https://legal.tensamin.net/eula/newest/)", eula, focus == Focus::Eula, true));
} else {
text_lines.push(checkbox("End User Licence Agreement ¹ (https://legal.tensamin.net/eula/newest/)", eula, focus == Focus::Eula, true));
}
}
}
if tos_needed {
header_lines += 1;
if tos_future {
if size.width < 80 {
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 {
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 {
if size.width < 80 {
text_lines.push(checkbox("ToS ² (https://legal.tensamin.net/tos/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 {
header_lines += 1;
if pp_future {
if size.width < 80 {
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 {
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 {
if size.width < 80 {
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));
}
}
}
text_lines.push(Line::from(""));
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(""));
let mut optional_lines: Vec<i16> =
if size.width > 143 {
if tos_needed || pp_needed {
text_lines.push(Line::from("By selecting Downgrade, you confirm that you 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 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 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 + 7, header_lines, header_lines + 7, seperator, header_lines + 2]
} else {
vec![header_lines + 6, header_lines, header_lines + 6, seperator, header_lines + 2]
}
} else if size.width > 92 {
if tos_needed || pp_needed {
text_lines.push(Line::from("By selecting Continue, you confirm that you 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 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 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 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",));
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",));
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]
}
};
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();
let q_informer = if update_needed {
"Q to Exit"
} else {
"Q to Cancel"
};
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 > 20 {
Style::default().fg(Color::Green)
} else if size.height >= (header_lines as u16 + 10) {
Style::default().fg(Color::Yellow)
} else {
Style::default().fg(Color::Red)
};
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!(" / {}", header_lines + 10)),
]),
]);
let warning = Paragraph::new(warning_text)
.alignment(Alignment::Center)
.block(
Block::default()
.borders(Borders::ALL)
.title(format!("UI Too Small [{}]", q_informer)),
);
too_small = true;
f.render_widget(warning, size);
return;
}
let consent_block = Paragraph::new(Text::from(text_lines))
.block(Block::default().title(format!(" Update Tensamin User Consent [{}] ", q_informer)).borders(Borders::ALL));
f.render_widget(consent_block, chunks[0]);
let downgrade_scenario = tos_needed || pp_needed;
draw_buttons(
f,
chunks[1],
focus,
(eula, tos && pp),
update_needed,
downgrade_scenario,
pp_needed || tos_needed
);
})
.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;
}
}
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 {
possible_states.push(Focus::Continue);
if tos && pp && (pp_needed || tos_needed) {
possible_states.push(Focus::ContinueAll);
}
}
match key.code {
KeyCode::Esc => break UserChoice::Deny,
KeyCode::Up | KeyCode::Left => focus.prev(&possible_states),
KeyCode::Down | KeyCode::Right | KeyCode::Tab => focus.next(&possible_states),
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_newest_link(terms_type));
}
KeyCode::Char(' ') | KeyCode::Enter => match focus {
Focus::Eula => {
eula = !eula;
tos = !tos_needed;
pp = !pp_needed;
}
Focus::Tos => {
if eula {
tos = !tos
}
}
Focus::Pp => {
if eula {
pp = !pp
}
}
Focus::Cancel => break UserChoice::Deny,
Focus::Continue if eula => break UserChoice::AcceptEULA,
Focus::ContinueAll if eula && tos && pp => {
break UserChoice::AcceptAll;
}
_ => {}
},
_ => {}
}
}
}
};
ratatui::restore();
match result {
UserChoice::AcceptAll => {
consent.accepted_eula = true;
consent.accepted_tos = true;
consent.accepted_pp = true;
if let Some(_) = eula_for_future {
consent.future_eula = eula_for_future;
}
if let Some(_) = tos_for_future {
consent.future_tos = tos_for_future;
}
if let Some(_) = pp_for_future {
consent.future_privacy = pp_for_future;
}
}
UserChoice::AcceptEULA => {
consent.accepted_eula = true;
if let Some(_) = eula_for_future {
consent.future_eula = eula_for_future;
}
}
UserChoice::Deny => {}
}
consent
}