[Add] basic split
This commit is contained in:
parent
a0ff6b1082
commit
3cdf7c62d5
77 changed files with 506 additions and 648 deletions
242
iota-cli/src/screens/main_screen.rs
Normal file
242
iota-cli/src/screens/main_screen.rs
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
use crate::{
|
||||
elements::{
|
||||
console_card::ConsoleCard,
|
||||
elements::{InteractableElement, JoinableElement},
|
||||
graph_card::{GRAPHS, GraphCard},
|
||||
log_card::LogCard,
|
||||
},
|
||||
interaction_result::InteractionResult,
|
||||
screens::screens::{NavDirection, Screen},
|
||||
ui::UI,
|
||||
};
|
||||
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use ratatui::{
|
||||
Frame,
|
||||
layout::{Constraint, Layout, Margin, Rect},
|
||||
widgets::{Block, Borders},
|
||||
};
|
||||
|
||||
use std::{any::Any, sync::Arc};
|
||||
|
||||
pub struct MainScreen {
|
||||
elements: Vec<Box<dyn InteractableElement>>,
|
||||
nav_grid: Vec<Vec<Option<usize>>>,
|
||||
selected_coords: (usize, usize),
|
||||
graphs_open: bool,
|
||||
}
|
||||
|
||||
impl MainScreen {
|
||||
pub async fn new(ui: Arc<UI>) -> Self {
|
||||
let mut elements: Vec<Box<dyn InteractableElement>> = Vec::new();
|
||||
|
||||
let nav_grid = vec![
|
||||
vec![Some(0), Some(2)],
|
||||
vec![Some(0), Some(3)],
|
||||
vec![Some(1), Some(4)],
|
||||
];
|
||||
|
||||
let mut log_card = LogCard::new();
|
||||
log_card.set_borders(Borders::TOP.union(Borders::RIGHT).union(Borders::LEFT));
|
||||
let mut console_card = ConsoleCard::new("Console", "");
|
||||
console_card.set_joins(Borders::TOP);
|
||||
|
||||
elements.push(Box::new(log_card));
|
||||
elements.push(Box::new(console_card));
|
||||
|
||||
let mut ram_graph = GraphCard::new(ui.clone(), GRAPHS::Ram, "RAM".into());
|
||||
ram_graph.set_borders(Borders::TOP.union(Borders::LEFT).union(Borders::RIGHT));
|
||||
elements.push(Box::new(ram_graph));
|
||||
let mut cpu_graph = GraphCard::new(ui.clone(), GRAPHS::Cpu, "CPU".into());
|
||||
cpu_graph.set_borders(Borders::TOP.union(Borders::LEFT).union(Borders::RIGHT));
|
||||
cpu_graph.set_joins(Borders::TOP);
|
||||
elements.push(Box::new(cpu_graph));
|
||||
let mut ping_graph = GraphCard::new(ui.clone(), GRAPHS::Ping, "Ping".into());
|
||||
ping_graph.set_joins(Borders::TOP);
|
||||
elements.push(Box::new(ping_graph));
|
||||
|
||||
let graphs_open = true;
|
||||
|
||||
let mut screen = MainScreen {
|
||||
elements,
|
||||
nav_grid,
|
||||
selected_coords: (1, 0),
|
||||
graphs_open,
|
||||
};
|
||||
screen.focus_current();
|
||||
screen
|
||||
}
|
||||
|
||||
fn focus_current(&mut self) {
|
||||
let (y, x) = self.selected_coords;
|
||||
if let Some(Some(index)) = self.nav_grid.get(y).and_then(|row| row.get(x)) {
|
||||
if let Some(element) = self.elements.get_mut(*index) {
|
||||
if element.can_focus() {
|
||||
element.focus(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn unfocus_current(&mut self, y: usize, x: usize) {
|
||||
if let Some(Some(index)) = self.nav_grid.get(y).and_then(|row| row.get(x)) {
|
||||
if let Some(element) = self.elements.get_mut(*index) {
|
||||
element.focus(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn navigate(&mut self, direction: NavDirection) {
|
||||
let (current_row, current_col) = self.selected_coords;
|
||||
let current_element = self.nav_grid[current_row][current_col];
|
||||
|
||||
self.unfocus_current(current_row, current_col);
|
||||
|
||||
let (delta_row, delta_col) = match direction {
|
||||
NavDirection::Up => (-1isize, 0),
|
||||
NavDirection::Down => (1, 0),
|
||||
NavDirection::Left => (0, -1),
|
||||
NavDirection::Right => (0, 1),
|
||||
_ => (0, 0),
|
||||
};
|
||||
|
||||
let mut next_row = current_row as isize;
|
||||
let mut next_col = current_col as isize;
|
||||
|
||||
loop {
|
||||
next_row += delta_row;
|
||||
next_col += delta_col;
|
||||
|
||||
if next_row < 0 || next_col < 0 {
|
||||
self.selected_coords = (
|
||||
(next_row - delta_row) as usize,
|
||||
(next_col - delta_col) as usize,
|
||||
);
|
||||
break;
|
||||
}
|
||||
let next_row_u = next_row as usize;
|
||||
let next_col_u = next_col as usize;
|
||||
|
||||
if next_row_u >= self.nav_grid.len() {
|
||||
self.selected_coords = (
|
||||
(next_row - delta_row) as usize,
|
||||
(next_col - delta_col) as usize,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if let Some(row) = self.nav_grid.get(next_row_u) {
|
||||
if next_col_u >= row.len() {
|
||||
self.selected_coords = (
|
||||
(next_row - delta_row) as usize,
|
||||
(next_col - delta_col) as usize,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if let Some(next_element) = row[next_col_u] {
|
||||
if Some(next_element) != current_element {
|
||||
self.selected_coords = (next_row_u, next_col_u);
|
||||
self.focus_current();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.focus_current();
|
||||
}
|
||||
}
|
||||
|
||||
impl Screen for MainScreen {
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn render(&self, f: &mut Frame, rect: Rect) {
|
||||
let main_block = Block::default().title("Main").borders(Borders::ALL);
|
||||
f.render_widget(main_block, rect);
|
||||
|
||||
let inner = rect.inner(Margin {
|
||||
vertical: 1,
|
||||
horizontal: 1,
|
||||
});
|
||||
|
||||
let graphs_width = if self.graphs_open { 30 } else { 2 };
|
||||
let main_width = inner.width.saturating_sub(graphs_width);
|
||||
|
||||
let horizontal_chunks = Layout::default()
|
||||
.direction(ratatui::layout::Direction::Horizontal)
|
||||
.constraints([
|
||||
Constraint::Length(main_width),
|
||||
Constraint::Length(graphs_width),
|
||||
])
|
||||
.split(inner);
|
||||
|
||||
let left_area = horizontal_chunks[0];
|
||||
let right_area = horizontal_chunks[1];
|
||||
|
||||
let left_rows =
|
||||
Layout::vertical([Constraint::Min(0), Constraint::Length(3)]).split(left_area);
|
||||
|
||||
if let Some(log) = self.elements.get(0) {
|
||||
log.as_element().render(f, left_rows[0]);
|
||||
}
|
||||
|
||||
if let Some(console) = self.elements.get(1) {
|
||||
console.as_element().render(f, left_rows[1]);
|
||||
}
|
||||
|
||||
let graph_elements: Vec<_> = self
|
||||
.elements
|
||||
.iter()
|
||||
.filter(|el| el.as_any().is::<GraphCard>())
|
||||
.collect();
|
||||
|
||||
if !graph_elements.is_empty() {
|
||||
let graph_chunks = Layout::vertical(
|
||||
graph_elements
|
||||
.iter()
|
||||
.map(|_| Constraint::Ratio(1, graph_elements.len() as u32))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.split(right_area);
|
||||
|
||||
for (el, area) in graph_elements.iter().zip(graph_chunks.iter()) {
|
||||
el.as_element().render(f, *area);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_input(&mut self, event: KeyEvent) -> InteractionResult {
|
||||
match event.code {
|
||||
KeyCode::Up => self.navigate(NavDirection::Up),
|
||||
KeyCode::Down => self.navigate(NavDirection::Down),
|
||||
KeyCode::Left => self.navigate(NavDirection::Left),
|
||||
KeyCode::Right => self.navigate(NavDirection::Right),
|
||||
KeyCode::Enter | KeyCode::Char(' ') if self.selected_coords.1 == 1 => {
|
||||
self.graphs_open = !self.graphs_open;
|
||||
for element in self.elements.iter_mut() {
|
||||
if let Some(graph) = element.as_any_mut().downcast_mut::<GraphCard>() {
|
||||
graph.set_open(self.graphs_open);
|
||||
}
|
||||
}
|
||||
return InteractionResult::Handled;
|
||||
}
|
||||
_ => {
|
||||
let (y, x) = self.selected_coords;
|
||||
if let Some(Some(index)) = self.nav_grid.get(y).and_then(|r| r.get(x)) {
|
||||
if let Some(el) = self.elements.get_mut(*index) {
|
||||
return el.interact(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
InteractionResult::Handled
|
||||
}
|
||||
}
|
||||
493
iota-cli/src/screens/md_viewer.rs
Normal file
493
iota-cli/src/screens/md_viewer.rs
Normal file
|
|
@ -0,0 +1,493 @@
|
|||
use crossterm::event::{self, Event, KeyCode, KeyEvent};
|
||||
use ratatui::{
|
||||
DefaultTerminal,
|
||||
prelude::*,
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Borders, Paragraph, Wrap},
|
||||
};
|
||||
use std::{any::Any, time::Duration};
|
||||
|
||||
use crate::{interaction_result::InteractionResult, screens::screens::Screen};
|
||||
|
||||
pub struct FileViewer {
|
||||
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 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 {
|
||||
Self {
|
||||
title,
|
||||
text: parse_document(content.to_owned()),
|
||||
scroll: 0,
|
||||
scroll_x: 0,
|
||||
}
|
||||
}
|
||||
pub fn force_popup(mut self, mut terminal: DefaultTerminal) -> DefaultTerminal {
|
||||
loop {
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let area = f.area();
|
||||
self.draw(f, area);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
if event::poll(Duration::from_millis(100)).unwrap() {
|
||||
let ev = event::read().unwrap();
|
||||
self.handle_event(&ev);
|
||||
|
||||
if matches!(ev, Event::Key(k) if k.code == KeyCode::Char('q')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
terminal
|
||||
}
|
||||
fn draw(&self, f: &mut Frame, area: Rect) {
|
||||
use ratatui::text::Text;
|
||||
|
||||
let mut rendered_lines = Vec::new();
|
||||
|
||||
for display_line in &self.text {
|
||||
if display_line.scrollable {
|
||||
let content: String = display_line
|
||||
.line
|
||||
.spans
|
||||
.iter()
|
||||
.map(|s| s.content.clone())
|
||||
.collect();
|
||||
|
||||
let start = self.scroll_x as usize;
|
||||
let width = area.width as usize - 2;
|
||||
|
||||
let visible = if start < content.chars().count() {
|
||||
content.chars().skip(start).take(width).collect()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let mut chars: Vec<char> = visible.chars().collect();
|
||||
|
||||
if start > 0 && !chars.is_empty() {
|
||||
chars[0] = '<';
|
||||
}
|
||||
|
||||
if start + width < content.chars().count() && !chars.is_empty() {
|
||||
let last = chars.len() - 1;
|
||||
chars[last] = '>';
|
||||
}
|
||||
|
||||
let visible: String = chars.into_iter().collect();
|
||||
|
||||
rendered_lines.push(Line::from(Span::styled(
|
||||
visible,
|
||||
display_line
|
||||
.line
|
||||
.spans
|
||||
.first()
|
||||
.map(|s| s.style)
|
||||
.unwrap_or_default(),
|
||||
)));
|
||||
} else {
|
||||
rendered_lines.push(display_line.line.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let paragraph = Paragraph::new(Text::from(rendered_lines))
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(format!("{} - [Q to close]", self.title.as_str(),)),
|
||||
)
|
||||
.wrap(Wrap { trim: false })
|
||||
.scroll((self.scroll, 0));
|
||||
|
||||
f.render_widget(paragraph, area);
|
||||
}
|
||||
|
||||
pub fn handle_event(&mut self, event: &Event) {
|
||||
if let Event::Key(key) = event {
|
||||
match key.code {
|
||||
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),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn parse_document(input: String) -> Vec<DisplayLine> {
|
||||
let mut lines_vec = Vec::new();
|
||||
let mut in_code_block = false;
|
||||
let liness: Vec<String> = input.lines().map(String::from).collect();
|
||||
let mut i = 0;
|
||||
|
||||
while i < liness.len() {
|
||||
let raw = &liness[i];
|
||||
|
||||
if raw.trim().starts_with("```") {
|
||||
in_code_block = !in_code_block;
|
||||
let code: String = if raw.trim().replace("```", "").is_empty() {
|
||||
"──".to_string()
|
||||
} else {
|
||||
raw.trim().replace("```", "")
|
||||
};
|
||||
lines_vec.push(DisplayLine {
|
||||
line: Line::from(Span::styled(
|
||||
format!("────────{}────────", code),
|
||||
Style::default().fg(Color::DarkGray),
|
||||
)),
|
||||
scrollable: false,
|
||||
});
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if in_code_block {
|
||||
lines_vec.push(DisplayLine {
|
||||
line: Line::from(Span::styled(
|
||||
raw.to_string(),
|
||||
Style::default().fg(Color::Yellow),
|
||||
)),
|
||||
scrollable: false,
|
||||
});
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if raw.starts_with("### ") {
|
||||
lines_vec.push(DisplayLine {
|
||||
line: Line::from(Span::styled(
|
||||
raw.trim_start_matches("### ").to_string(),
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)),
|
||||
scrollable: false,
|
||||
});
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if raw.starts_with("## ") {
|
||||
lines_vec.push(DisplayLine {
|
||||
line: Line::from(Span::styled(
|
||||
raw.trim_start_matches("## ").to_string(),
|
||||
Style::default()
|
||||
.fg(Color::LightCyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)),
|
||||
scrollable: false,
|
||||
});
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if raw.starts_with("# ") {
|
||||
lines_vec.push(DisplayLine {
|
||||
line: Line::from(Span::styled(
|
||||
raw.trim_start_matches("# ").to_string(),
|
||||
Style::default()
|
||||
.fg(Color::Gray)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)),
|
||||
scrollable: false,
|
||||
});
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if raw.trim_start().starts_with("- ") {
|
||||
let indent = raw.chars().take_while(|c| *c == ' ').count();
|
||||
lines_vec.push(DisplayLine {
|
||||
line: Line::from(Span::raw(format!(
|
||||
"{}• {}",
|
||||
" ".repeat(indent),
|
||||
raw.trim_start_matches("- ")
|
||||
))),
|
||||
scrollable: false,
|
||||
});
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if raw.trim().starts_with('|') && raw.contains('|') {
|
||||
let mut table_lines = vec![raw.clone()];
|
||||
let mut j = i + 1;
|
||||
while j < liness.len() && liness[j].trim().starts_with('|') {
|
||||
table_lines.push(liness[j].clone());
|
||||
j += 1;
|
||||
}
|
||||
|
||||
let table = parse_table(&table_lines.iter().map(|s| s.as_str()).collect::<Vec<_>>());
|
||||
lines_vec.extend(table_to_lines(table));
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
|
||||
lines_vec.push(DisplayLine {
|
||||
line: Line::from(parse_inline(raw.as_str())),
|
||||
scrollable: false,
|
||||
});
|
||||
i += 1;
|
||||
}
|
||||
|
||||
lines_vec
|
||||
}
|
||||
|
||||
fn parse_inline(input: &str) -> Vec<Span<'static>> {
|
||||
let mut spans = Vec::new();
|
||||
let mut buf = String::new();
|
||||
|
||||
let mut bold = false;
|
||||
let mut underline = false;
|
||||
let mut code = false;
|
||||
|
||||
let mut chars = input.chars().peekable();
|
||||
|
||||
while let Some(c) = chars.next() {
|
||||
let toggle = match c {
|
||||
'*' if chars.peek() == Some(&'*') => {
|
||||
chars.next();
|
||||
Some("bold")
|
||||
}
|
||||
'_' if chars.peek() == Some(&'_') => {
|
||||
chars.next();
|
||||
Some("underline")
|
||||
}
|
||||
'`' => Some("code"),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(kind) = toggle {
|
||||
flush_span(&mut spans, &mut buf, current_style(bold, underline, code));
|
||||
|
||||
match kind {
|
||||
"bold" => bold = !bold,
|
||||
"underline" => underline = !underline,
|
||||
"code" => code = !code,
|
||||
_ => {}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
buf.push(c);
|
||||
}
|
||||
|
||||
flush_span(&mut spans, &mut buf, current_style(bold, underline, code));
|
||||
spans
|
||||
}
|
||||
|
||||
fn current_style(bold: bool, underline: bool, code: bool) -> Style {
|
||||
let mut style = Style::default();
|
||||
|
||||
if bold {
|
||||
style = style.add_modifier(Modifier::BOLD);
|
||||
}
|
||||
if underline {
|
||||
style = style.add_modifier(Modifier::UNDERLINED);
|
||||
}
|
||||
if code {
|
||||
style = style.fg(Color::Yellow);
|
||||
}
|
||||
|
||||
style
|
||||
}
|
||||
#[derive(Clone)]
|
||||
pub struct DisplayLine {
|
||||
line: Line<'static>,
|
||||
scrollable: bool,
|
||||
}
|
||||
|
||||
fn table_to_lines(table: Vec<Vec<String>>) -> Vec<DisplayLine> {
|
||||
if table.len() < 2 {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
let header = &table[0];
|
||||
let mut column_heights = vec![0; table[0].len()];
|
||||
|
||||
for row in table.iter().skip(1) {
|
||||
for (i, cell) in row.iter().enumerate() {
|
||||
let lines = cell.lines().count().max(1);
|
||||
column_heights[i] += lines;
|
||||
}
|
||||
}
|
||||
|
||||
let widths: Vec<usize> = header
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, h)| {
|
||||
let h_len = h.chars().count().max(1);
|
||||
if i == 0 {
|
||||
table
|
||||
.iter()
|
||||
.map(|row| row.get(i).map(|c| c.chars().count()).unwrap_or(0))
|
||||
.max()
|
||||
.unwrap_or(h_len)
|
||||
} else {
|
||||
let max = (3 * h_len) as usize;
|
||||
max.max(h_len)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut lines = Vec::new();
|
||||
|
||||
for (row_idx, row) in table.iter().enumerate() {
|
||||
if row_idx == 1 {
|
||||
let divider = widths
|
||||
.iter()
|
||||
.map(|w| "─".repeat(*w))
|
||||
.collect::<Vec<_>>()
|
||||
.join("─┼─");
|
||||
|
||||
lines.push(DisplayLine {
|
||||
line: Line::from(Span::styled(divider, Style::default().fg(Color::DarkGray))),
|
||||
scrollable: true,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
let wrapped_cells: Vec<Vec<String>> = row
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, cell)| wrap_cell(cell, widths[i]))
|
||||
.collect();
|
||||
|
||||
let row_height = wrapped_cells.iter().map(|c| c.len()).max().unwrap_or(1);
|
||||
|
||||
for line_idx in 0..row_height {
|
||||
let mut line = String::new();
|
||||
|
||||
for (i, cell) in wrapped_cells.iter().enumerate() {
|
||||
let content = cell.get(line_idx).map(String::as_str).unwrap_or("");
|
||||
line.push_str(&format!("{:width$}", content, width = widths[i]));
|
||||
if i < wrapped_cells.len() - 1 {
|
||||
line.push_str(" │ ");
|
||||
}
|
||||
}
|
||||
|
||||
let style = if row_idx == 0 {
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default().fg(Color::Green)
|
||||
};
|
||||
|
||||
lines.push(DisplayLine {
|
||||
line: Line::from(Span::styled(line, style)),
|
||||
scrollable: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
lines
|
||||
}
|
||||
fn wrap_cell(cell: &str, width: usize) -> Vec<String> {
|
||||
if width == 0 {
|
||||
return vec![String::new()];
|
||||
}
|
||||
|
||||
let mut lines = Vec::new();
|
||||
let mut current = String::new();
|
||||
|
||||
for word in cell.split_whitespace() {
|
||||
let word_len = word.chars().count();
|
||||
let current_len = current.chars().count();
|
||||
|
||||
if current_len == 0 {
|
||||
if word_len <= width {
|
||||
current.push_str(word);
|
||||
} else {
|
||||
for chunk in word.chars().collect::<Vec<_>>().chunks(width) {
|
||||
lines.push(chunk.iter().collect());
|
||||
}
|
||||
}
|
||||
} else if current_len + 1 + word_len <= width {
|
||||
current.push(' ');
|
||||
current.push_str(word);
|
||||
} else {
|
||||
lines.push(current);
|
||||
current = String::new();
|
||||
|
||||
if word_len <= width {
|
||||
current.push_str(word);
|
||||
} else {
|
||||
for chunk in word.chars().collect::<Vec<_>>().chunks(width) {
|
||||
lines.push(chunk.iter().collect());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !current.is_empty() {
|
||||
lines.push(current);
|
||||
}
|
||||
|
||||
if lines.is_empty() {
|
||||
lines.push(String::new());
|
||||
}
|
||||
|
||||
lines
|
||||
}
|
||||
|
||||
fn flush_span(spans: &mut Vec<Span>, buf: &mut String, style: Style) {
|
||||
if !buf.is_empty() {
|
||||
spans.push(Span::styled(buf.clone(), style));
|
||||
buf.clear();
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_table(lines: &[&str]) -> Vec<Vec<String>> {
|
||||
let mut table = Vec::new();
|
||||
|
||||
for &line in lines {
|
||||
if !line.starts_with('|') || !line.contains('|') {
|
||||
break;
|
||||
}
|
||||
let row: Vec<String> = line
|
||||
.trim_matches('|')
|
||||
.split('|')
|
||||
.map(|s| s.trim().to_string())
|
||||
.collect();
|
||||
table.push(row);
|
||||
}
|
||||
|
||||
table
|
||||
}
|
||||
25
iota-cli/src/screens/screens.rs
Normal file
25
iota-cli/src/screens/screens.rs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
use std::any::Any;
|
||||
|
||||
use crossterm::event::KeyEvent;
|
||||
use ratatui::{Frame, layout::Rect};
|
||||
|
||||
use crate::interaction_result::InteractionResult;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum NavDirection {
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
|
||||
Next,
|
||||
Prev,
|
||||
}
|
||||
|
||||
pub trait Screen: Send + Sync + Any {
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||
|
||||
fn render(&self, f: &mut Frame, rect: Rect);
|
||||
fn handle_input(&mut self, event: KeyEvent) -> InteractionResult;
|
||||
}
|
||||
343
iota-cli/src/screens/terms_checker.rs
Normal file
343
iota-cli/src/screens/terms_checker.rs
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
use crate::{
|
||||
interaction_result::InteractionResult,
|
||||
screens::{md_viewer::FileViewer, screens::Screen},
|
||||
ui::UI,
|
||||
};
|
||||
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 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 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(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,
|
||||
}
|
||||
}
|
||||
}
|
||||
682
iota-cli/src/screens/terms_updater.rs
Normal file
682
iota-cli/src/screens/terms_updater.rs
Normal file
|
|
@ -0,0 +1,682 @@
|
|||
use crate::{
|
||||
interaction_result::InteractionResult,
|
||||
screens::{md_viewer::FileViewer, screens::Screen},
|
||||
};
|
||||
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 tokio::sync::oneshot;
|
||||
|
||||
pub struct TermsUpdaterScreen {
|
||||
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(
|
||||
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 {
|
||||
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 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 Tensamin’s 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 Tensamin’s 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 - You’ll 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 fut = Box::pin(async move {
|
||||
let content = get_terms(terms_type.clone()).await.unwrap();
|
||||
Box::new(FileViewer::new(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_newest_link(Type::EULA));
|
||||
InteractionResult::Handled
|
||||
}
|
||||
Focus::Tos => {
|
||||
let _ = open::that(get_newest_link(Type::TOS));
|
||||
InteractionResult::Handled
|
||||
}
|
||||
Focus::Pp => {
|
||||
let _ = open::that(get_newest_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,
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue