Added TUI print menu. Added tui and termion crates. Needs to be factored out of main(). Resolves #36 and #37.

This commit is contained in:
scoobybejesus 2019-10-20 01:11:52 -04:00
parent 83966babec
commit f19e2893c3
4 changed files with 364 additions and 3 deletions

View File

@ -15,6 +15,8 @@ chrono-tz = "0.5"
time = "0.1.42" time = "0.1.42"
structopt = "0.2.10" structopt = "0.2.10"
rustyline = "5.0.0" rustyline = "5.0.0"
tui = "0.5"
termion = "1.5"
[profile.release] [profile.release]
lto = true lto = true

View File

@ -57,6 +57,7 @@ pub struct ImportProcessParameters {
pub input_file_date_separator: String, pub input_file_date_separator: String,
pub input_file_has_iso_date_style: bool, pub input_file_has_iso_date_style: bool,
pub should_export: bool, pub should_export: bool,
pub print_menu: bool,
} }
pub(crate) fn import_and_process_final( pub(crate) fn import_and_process_final(

View File

@ -9,9 +9,24 @@
use std::ffi::OsString; use std::ffi::OsString;
use structopt::StructOpt;
use std::path::PathBuf; use std::path::PathBuf;
use std::error::Error; use std::error::Error;
use std::io;
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
use tui::Terminal;
use tui::backend::{TermionBackend, Backend};
use tui::style::{Color, Modifier, Style};
use tui::widgets::{Widget, Block, Borders, SelectableList, Text, Paragraph};
use tui::layout::{Layout, Constraint, Direction};
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
use termion::input::MouseTerminal;
use termion::event::Key;
use termion::input::TermRead;
use structopt::StructOpt;
mod account; mod account;
mod transaction; mod transaction;
@ -60,6 +75,11 @@ pub(crate) struct Flags {
#[structopt(name = "date conforms to ISO 8601", short = "i", long = "iso")] #[structopt(name = "date conforms to ISO 8601", short = "i", long = "iso")]
iso_date: bool, iso_date: bool,
/// Once the import file has been fully processed, the user will be presented
/// with a menu for manually selecting which reports to print/export.
#[structopt(name = "print menu", short, long = "print-menu")]
print_menu: bool,
/// This will prevent the program from writing reports to files. /// This will prevent the program from writing reports to files.
/// This will be ignored if -a is not set (the wizard will always ask to output). /// This will be ignored if -a is not set (the wizard will always ask to output).
#[structopt(name = "suppress reports", short, long = "suppress")] #[structopt(name = "suppress reports", short, long = "suppress")]
@ -125,9 +145,12 @@ fn main() -> Result<(), Box<dyn Error>> {
transactions_map, transactions_map,
) = core_functions::import_and_process_final(input_file_path, &settings)?; ) = core_functions::import_and_process_final(input_file_path, &settings)?;
let should_export = settings.should_export; let mut should_export_all = settings.should_export;
let present_print_menu_tui = settings.print_menu;
if should_export { if present_print_menu_tui { should_export_all = false }
if should_export_all {
println!("Creating reports now."); println!("Creating reports now.");
@ -195,6 +218,340 @@ fn main() -> Result<(), Box<dyn Error>> {
} }
if present_print_menu_tui {
const TASKS: [&'static str; 9] = [
"1. CSV: Account Sums",
"2. CSV: Account Sums (Non-zero only)",
"3. CSV: Account Sums (Orig. basis vs like-kind basis)",
"4. CSV: Transactions by movement (every movement)",
"5. CSV: Transactions by movement (summarized by long-term/short-term)",
"6. CSV: Transactions by movement (every movement, w/ orig. and like-kind basis",
"7. TXT: Accounts by lot (every movement)",
"8. TXT: Accounts by lot (every lot balance)",
"9. TXT: Accounts by lot (every non-zero lot balance)",
];
pub struct ListState<I> {
pub items: Vec<I>,
pub selected: usize,
}
impl<I> ListState<I> {
fn new(items: Vec<I>) -> ListState<I> {
ListState { items, selected: 0 }
}
fn select_previous(&mut self) {
if self.selected > 0 {
self.selected -= 1;
}
}
fn select_next(&mut self) {
if self.selected < self.items.len() - 1 {
self.selected += 1
}
}
}
pub struct PrintWindow<'a> {
pub title: &'a str,
pub should_quit: bool,
pub tasks: ListState<(&'a str)>,
pub to_print: Vec<usize>,
}
impl<'a> PrintWindow<'a> {
pub fn new(title: &'a str) -> PrintWindow<'a> {
PrintWindow {
title,
should_quit: false,
tasks: ListState::new(TASKS.to_vec()),
to_print: Vec::with_capacity(TASKS.len() + 3),
}
}
pub fn on_up(&mut self) {
self.tasks.select_previous();
}
pub fn on_down(&mut self) {
self.tasks.select_next();
}
pub fn on_key(&mut self, c: char) {
match c {
'q' => {
self.should_quit = true;
self.to_print = Vec::with_capacity(0)
}
'p' => {
Self::change_vec_to_chrono_order_and_dedup(&mut self.to_print);
self.should_quit = true;
}
'x' => {
self.to_print.push(self.tasks.selected)
}
_ => {}
}
}
fn change_vec_to_chrono_order_and_dedup(vec: &mut Vec<usize>) {
let length = vec.len();
for _ in 0..length {
for j in 0..length-1 {
if vec[j] > vec[j+1] {
vec.swap(j, j+1)
}
}
}
vec.dedup();
}
}
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)?;
terminal.hide_cursor()?;
pub fn draw<B: Backend>(terminal: &mut Terminal<B>, app: &PrintWindow) -> Result<(), io::Error> {
terminal.draw(|mut f| {
let chunks = Layout::default()
.constraints([
Constraint::Length(1),
Constraint::Length(8),
Constraint::Length(TASKS.len() as u16 + 2),
Constraint::Percentage(35)
].as_ref())
.split(f.size());
let text = [
Text::raw("\nPress '"),
Text::styled("x", Style::default().fg(Color::LightGreen)),
Text::raw("' to add the selected report to the list of reports to print/export.\n"),
Text::raw("\nPress '"),
Text::styled("p", Style::default().fg(Color::Green)),
Text::raw("' to print/export the selected reports.\n"),
Text::raw("\nPress '"),
Text::styled("q", Style::default().fg(Color::Red)),
Text::raw("' to quit without printing.\n\n"),
];
Paragraph::new(text.iter())
.block(
Block::default()
.borders(Borders::NONE)
.title("Instructions")
.title_style(Style::default().fg(Color::Blue).modifier(Modifier::BOLD)),
)
.wrap(true)
.render(&mut f, chunks[1]);
let draw_chunk = Layout::default()
.constraints([Constraint::Percentage(10), Constraint::Percentage(80),Constraint::Percentage(10),].as_ref())
.direction(Direction::Horizontal)
.split(chunks[2]);
SelectableList::default()
.block(Block::default().borders(Borders::ALL).title("Report List"))
.items(&app.tasks.items)
.select(Some(app.tasks.selected))
.highlight_style(Style::default().fg(Color::Yellow).modifier(Modifier::BOLD))
.highlight_symbol(">")
.render(&mut f, draw_chunk[1]);
})
}
pub enum Event<I> {
Input(I),
Tick,
}
pub struct Events {
rx: mpsc::Receiver<Event<Key>>,
input_handle: thread::JoinHandle<()>,
tick_handle: thread::JoinHandle<()>,
}
let events = Events::with_config(Config {
tick_rate: Duration::from_millis(250u64),
..Config::default()
});
#[derive(Debug, Clone, Copy)]
pub struct Config {
pub exit_key: Key,
pub tick_rate: Duration,
}
impl Default for Config {
fn default() -> Config {
Config {
exit_key: Key::Char('q'),
tick_rate: Duration::from_millis(250),
}
}
}
impl Events {
pub fn new() -> Events {
Events::with_config(Config::default())
}
pub fn with_config(config: Config) -> Events {
let (tx, rx) = mpsc::channel();
let input_handle = {
let tx = tx.clone();
thread::spawn(move || {
let stdin = io::stdin();
for evt in stdin.keys() {
match evt {
Ok(key) => {
if let Err(_) = tx.send(Event::Input(key)) {
return;
}
if key == config.exit_key {
return;
}
}
Err(_) => {}
}
}
})
};
let tick_handle = {
let tx = tx.clone();
thread::spawn(move || {
let tx = tx.clone();
loop {
tx.send(Event::Tick).unwrap();
thread::sleep(config.tick_rate);
}
})
};
Events {
rx,
input_handle,
tick_handle,
}
}
pub fn next(&self) -> Result<Event<Key>, mpsc::RecvError> {
self.rx.recv()
}
}
let mut app = PrintWindow::new("Reports");
loop {
draw(&mut terminal, &app)?;
match events.next()? {
Event::Input(key) => match key {
Key::Char(c) => {
app.on_key(c);
}
Key::Up => {
app.on_up();
}
Key::Down => {
app.on_down();
}
Key::Left => {
// app.on_left();
}
Key::Right => {
// app.on_right();
}
_ => {}
},
_ => {}
}
if app.should_quit {
break;
}
}
// Seem to need both of these for the native terminal to be available for println!()'s below
std::mem::drop(terminal);
std::thread::sleep(Duration::from_millis(10));
for report in app.to_print {
println!("Exporting: {}", TASKS[report]);
match report + 1 {
1 => {
csv_export::_1_account_sums_to_csv(
&settings,
&raw_acct_map,
&account_map
);
}
2 => {
csv_export::_2_account_sums_nonzero_to_csv(
&account_map,
&settings,
&raw_acct_map
);
}
3 => {
csv_export::_3_account_sums_to_csv_with_orig_basis(
&settings,
&raw_acct_map,
&account_map
);
}
4 => {
csv_export::_4_transaction_mvmt_detail_to_csv(
&settings,
&action_records_map,
&raw_acct_map,
&account_map,
&transactions_map
)?;
}
5 => {
csv_export::_5_transaction_mvmt_summaries_to_csv(
&settings,
&action_records_map,
&raw_acct_map,
&account_map,
&transactions_map
)?;
}
6 => {
csv_export::_6_transaction_mvmt_detail_to_csv_w_orig(
&settings,
&action_records_map,
&raw_acct_map,
&account_map,
&transactions_map
)?;
}
7 => {
txt_export::_1_account_lot_detail_to_txt(
&settings,
&raw_acct_map,
&account_map,
&transactions_map,
&action_records_map
)?;
}
8 => {
txt_export::_2_account_lot_summary_to_txt(
&settings,
&raw_acct_map,
&account_map,
)?;
}
9 => {
txt_export::_3_account_lot_summary_non_zero_to_txt(
&settings,
&raw_acct_map,
&account_map,
)?;
}
_ => {}
}
}
}
// use tests::test; // use tests::test;
// test::run_tests( // test::run_tests(
// &transactions_map, // &transactions_map,

View File

@ -70,6 +70,7 @@ pub (crate) fn run_setup(args: super::Cli) -> Result<(PathBuf, ImportProcessPara
lk_basis_date_preserved: true, // TODO lk_basis_date_preserved: true, // TODO
should_export: should_export, should_export: should_export,
export_path: output_dir_path, export_path: output_dir_path,
print_menu: args.flags.print_menu,
}; };
Ok((input_file_path, settings)) Ok((input_file_path, settings))