Created
July 5, 2021 09:34
-
-
Save ilmoi/38c4d7d04a265f6a4c829b1648fdde2d to your computer and use it in GitHub Desktop.
State machine for a tui app in rust
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use crate::eth::generate_and_save_mnemonic; | |
use crate::tui::util::event::{Event, Events}; | |
use crate::tui::util::{StatefulList, TabsState}; | |
use bip39::Mnemonic; | |
use std::collections::HashMap; | |
use std::io::Stdout; | |
use std::{error::Error, io, thread}; | |
use termion::raw::RawTerminal; | |
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; | |
use tui::widgets::{List, ListItem, Tabs}; | |
use tui::{ | |
backend::TermionBackend, | |
layout::{Alignment, Constraint, Direction, Layout, Rect}, | |
style::{Color, Modifier, Style}, | |
text::{Span, Spans}, | |
widgets::{Block, Borders, Clear, Paragraph, Wrap}, | |
Frame, Terminal, | |
}; | |
// ----------------------------------------------------------------------------- app state | |
struct AppState { | |
screen: Screen, | |
mnemonic: Option<Mnemonic>, | |
file_uuid: Option<String>, | |
} | |
impl AppState { | |
fn fresh_state() -> Self { | |
Self { | |
screen: Screen::Welcome, | |
mnemonic: None, | |
file_uuid: None, | |
} | |
} | |
} | |
// ----------------------------------------------------------------------------- screens | |
#[derive(Hash, Eq, PartialEq, Clone)] | |
enum Screen { | |
Welcome, | |
NewWallet, | |
} | |
impl Screen { | |
fn init_screens() -> HashMap<Screen, Box<dyn Drawable>> { | |
let mut h: HashMap<Screen, Box<dyn Drawable>> = HashMap::new(); | |
h.insert(Screen::Welcome, Box::new(Welcome::new())); | |
h.insert(Screen::NewWallet, Box::new(NewWallet::new())); | |
h | |
} | |
} | |
// ----------------------------------------------------------------------------- drawable | |
trait Drawable { | |
fn draw_body( | |
&mut self, | |
body_chunk: Rect, | |
body_block: Block, | |
f: &mut Frame<TermBck>, | |
state: &mut AppState, | |
); | |
fn set_keybinding(&mut self, key: Key, state: &mut AppState); | |
} | |
// ----------------------------------------------------------------------------- 1: welcome | |
struct Welcome<'a> { | |
list_app: ListApp<'a>, | |
} | |
impl<'a> Welcome<'a> { | |
fn new() -> Self { | |
Self { | |
list_app: ListApp::new(vec![ | |
"create new", | |
"import existing", | |
"login with passphrase", | |
]), | |
} | |
} | |
} | |
impl<'a> Drawable for Welcome<'a> { | |
fn draw_body( | |
&mut self, | |
body_chunk: Rect, | |
body_block: Block, | |
f: &mut Frame<TermBck>, | |
_state: &mut AppState, | |
) { | |
let items: Vec<ListItem> = self | |
.list_app | |
.items | |
.items | |
.iter() | |
.map(|i| ListItem::new(Span::from(i.to_owned()))) | |
.collect(); | |
let items = List::new(items) | |
.block(body_block) | |
.highlight_style(Style::default().bg(Color::Blue)) | |
.highlight_symbol(">> "); | |
f.render_stateful_widget(items, body_chunk, &mut self.list_app.items.state); | |
} | |
fn set_keybinding(&mut self, key: Key, state: &mut AppState) { | |
match key { | |
Key::Char('\n') => { | |
// println!("{}", self.list_app.items.state.selected().unwrap()); | |
state.screen = Screen::NewWallet; | |
} | |
Key::Left => { | |
self.list_app.items.unselect(); | |
} | |
Key::Down => { | |
self.list_app.items.next(); | |
} | |
Key::Up => { | |
self.list_app.items.previous(); | |
} | |
_ => {} | |
} | |
} | |
} | |
// ----------------------------------------------------------------------------- 2: new wallet | |
struct NewWallet {} | |
impl NewWallet { | |
fn new() -> Self { | |
Self {} | |
} | |
} | |
impl Drawable for NewWallet { | |
fn draw_body( | |
&mut self, | |
body_chunk: Rect, | |
body_block: Block, | |
f: &mut Frame<TermBck>, | |
state: &mut AppState, | |
) { | |
//todo ask for pw | |
if state.mnemonic.is_none() { | |
// thread::spawn(|| { | |
let (mnemonic, file_uuid) = generate_and_save_mnemonic(); | |
state.mnemonic = Some(mnemonic); | |
state.file_uuid = Some(file_uuid); | |
// }); | |
} | |
let text = vec![ | |
Spans::from("Generating a new wallet..."), | |
Spans::from(vec![ | |
Span::raw("Your mnemonic is:"), | |
Span::styled( | |
format!("{}", state.mnemonic.as_ref().unwrap()), | |
Style::default().fg(Color::Yellow), | |
), | |
]), | |
Spans::from("Write it down and hide it in a good place."), | |
Spans::from(vec![ | |
Span::raw("We also generated a Keystore file for you and saved it under "), | |
Span::styled( | |
format!("/keys/{}", state.file_uuid.as_ref().unwrap()), | |
Style::default().fg(Color::Cyan), | |
), | |
Span::raw("."), | |
]), | |
Spans::from("It's encrypted with your password so you can share it - but best not to."), | |
Spans::from(vec![ | |
Span::raw("Hit "), | |
Span::styled("<Enter>", Style::default().add_modifier(Modifier::BOLD)), | |
Span::raw(" when ready to proceed."), | |
]), | |
]; | |
let p = Paragraph::new(text).block(body_block); | |
f.render_widget(p, body_chunk); | |
} | |
fn set_keybinding(&mut self, key: Key, state: &mut AppState) { | |
match key { | |
Key::Char('\n') => { | |
// println!("ready to proceed!"); | |
state.screen = Screen::Welcome; | |
} | |
_ => {} | |
} | |
} | |
} | |
// ----------------------------------------------------------------------------- main fn | |
pub fn draw_screen() -> Result<(), Box<dyn Error>> { | |
let mut terminal = init_terminal().unwrap(); | |
let events = Events::new(); | |
// app state | |
let mut state = AppState::fresh_state(); | |
let mut current_screen: &mut Box<dyn Drawable>; | |
// pre-initialized screens | |
// NOTE 1: trade-off: we don't have to re-init on every loop turn, but we might init screens that we never visit | |
// given the ratio of # of screens to # of loop turns this makes sense | |
// NOTE 2: need mut because some screens hold their own state (eg lists) | |
let mut screens = Screen::init_screens(); | |
loop { | |
current_screen = screens.get_mut(&state.screen).unwrap(); | |
terminal.draw(|f| { | |
let body_chunk = draw_standard_grid(f); | |
let body_block = Block::default().borders(Borders::ALL); | |
current_screen.draw_body(body_chunk, body_block, f, &mut state); | |
}); | |
match events.next()? { | |
Event::Input(input) => match input { | |
Key::Char('q') => { | |
break; | |
} | |
_ => current_screen.set_keybinding(input, &mut state), | |
}, | |
_ => {} | |
} | |
} | |
Ok(()) | |
} | |
// ----------------------------------------------------------------------------- helpers | |
type TermBck = TermionBackend<AlternateScreen<RawTerminal<Stdout>>>; | |
fn init_terminal() -> Result<Terminal<TermBck>, Box<dyn Error>> { | |
let stdout = io::stdout().into_raw_mode()?; | |
// let stdout = MouseTerminal::from(stdout); | |
let stdout = AlternateScreen::from(stdout); | |
let backend = TermionBackend::new(stdout); | |
let mut terminal = Terminal::new(backend)?; | |
Ok(terminal) | |
} | |
fn draw_standard_grid(f: &mut Frame<TermBck>) -> Rect { | |
let size = f.size(); | |
let chunks = Layout::default() | |
.direction(Direction::Vertical) | |
// .margin(20) | |
.constraints([ | |
Constraint::Length(3), | |
Constraint::Min(0), | |
Constraint::Length(3), | |
]) | |
.split(size); | |
// ----------------------------------------------------------------------------- header | |
let header = Block::default().borders(Borders::ALL); | |
let title = Paragraph::new("degen 🍌 wallet") | |
.block(header) | |
.alignment(Alignment::Center); | |
f.render_widget(title, chunks[0]); | |
// ----------------------------------------------------------------------------- footer | |
let footer = Block::default().borders(Borders::ALL); | |
let text = Spans::from(vec![ | |
Span::styled("<q>", Style::default().add_modifier(Modifier::BOLD)), | |
Span::raw(" quit"), | |
Span::raw(" "), | |
Span::styled("<Esc>", Style::default().add_modifier(Modifier::BOLD)), | |
Span::raw(" go back"), | |
Span::raw(" "), | |
Span::styled("<Enter>", Style::default().add_modifier(Modifier::BOLD)), | |
Span::raw(" select"), | |
Span::raw(" "), | |
Span::styled("← ↑ → ↓", Style::default().add_modifier(Modifier::BOLD)), | |
Span::raw(" move around"), | |
]); | |
let tips = Paragraph::new(text) | |
.block(footer) | |
.alignment(Alignment::Center); | |
f.render_widget(tips, chunks[2]); | |
let body_chunk = chunks[1]; | |
body_chunk | |
} | |
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { | |
let popup_layout = Layout::default() | |
.direction(Direction::Vertical) | |
.constraints( | |
[ | |
Constraint::Percentage((100 - percent_y) / 2), | |
Constraint::Percentage(percent_y), | |
Constraint::Percentage((100 - percent_y) / 2), | |
] | |
.as_ref(), | |
) | |
.split(r); | |
Layout::default() | |
.direction(Direction::Horizontal) | |
.constraints( | |
[ | |
Constraint::Percentage((100 - percent_x) / 2), | |
Constraint::Percentage(percent_x), | |
Constraint::Percentage((100 - percent_x) / 2), | |
] | |
.as_ref(), | |
) | |
.split(popup_layout[1])[1] | |
} | |
fn centered_rect_fixed(fixed_x: u16, fixed_y: u16, r: Rect) -> Rect { | |
let popup_layout = Layout::default() | |
.direction(Direction::Vertical) | |
.constraints( | |
[ | |
Constraint::Percentage((100 - fixed_y) / 2), | |
Constraint::Percentage(fixed_y), | |
Constraint::Percentage((100 - fixed_y) / 2), | |
] | |
.as_ref(), | |
) | |
.split(r); | |
Layout::default() | |
.direction(Direction::Horizontal) | |
.constraints( | |
[ | |
Constraint::Percentage((100 - fixed_x) / 2), | |
Constraint::Percentage(fixed_x), | |
Constraint::Percentage((100 - fixed_x) / 2), | |
] | |
.as_ref(), | |
) | |
.split(popup_layout[1])[1] | |
} | |
struct TabsApp<'a> { | |
tabs: TabsState<'a>, | |
} | |
struct ListApp<'a> { | |
items: StatefulList<&'a str>, | |
} | |
impl<'a> ListApp<'a> { | |
fn new(items: Vec<&'a str>) -> ListApp<'a> { | |
Self { | |
items: StatefulList::with_items(items), | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment