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:
parent
83966babec
commit
f19e2893c3
|
@ -15,6 +15,8 @@ chrono-tz = "0.5"
|
|||
time = "0.1.42"
|
||||
structopt = "0.2.10"
|
||||
rustyline = "5.0.0"
|
||||
tui = "0.5"
|
||||
termion = "1.5"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
|
|
@ -57,6 +57,7 @@ pub struct ImportProcessParameters {
|
|||
pub input_file_date_separator: String,
|
||||
pub input_file_has_iso_date_style: bool,
|
||||
pub should_export: bool,
|
||||
pub print_menu: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn import_and_process_final(
|
||||
|
|
363
src/main.rs
363
src/main.rs
|
@ -9,9 +9,24 @@
|
|||
|
||||
|
||||
use std::ffi::OsString;
|
||||
use structopt::StructOpt;
|
||||
use std::path::PathBuf;
|
||||
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 transaction;
|
||||
|
@ -60,6 +75,11 @@ pub(crate) struct Flags {
|
|||
#[structopt(name = "date conforms to ISO 8601", short = "i", long = "iso")]
|
||||
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 be ignored if -a is not set (the wizard will always ask to output).
|
||||
#[structopt(name = "suppress reports", short, long = "suppress")]
|
||||
|
@ -125,9 +145,12 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||
transactions_map,
|
||||
) = 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.");
|
||||
|
||||
|
@ -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;
|
||||
// test::run_tests(
|
||||
// &transactions_map,
|
||||
|
|
|
@ -70,6 +70,7 @@ pub (crate) fn run_setup(args: super::Cli) -> Result<(PathBuf, ImportProcessPara
|
|||
lk_basis_date_preserved: true, // TODO
|
||||
should_export: should_export,
|
||||
export_path: output_dir_path,
|
||||
print_menu: args.flags.print_menu,
|
||||
};
|
||||
|
||||
Ok((input_file_path, settings))
|
||||
|
|
Loading…
Reference in New Issue