diff --git a/Cargo.lock b/Cargo.lock index 27914d1..e2c7140 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -248,6 +248,31 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags 1.3.2", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crptls" version = "0.2.2" @@ -263,7 +288,7 @@ dependencies = [ [[package]] name = "cryptools" -version = "0.12.4" +version = "0.12.5" dependencies = [ "chrono", "crptls", @@ -313,12 +338,6 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" -[[package]] -name = "either" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" - [[package]] name = "endian-type" version = "0.1.2" @@ -441,15 +460,6 @@ dependencies = [ "cc", ] -[[package]] -name = "itertools" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.9" @@ -477,6 +487,16 @@ version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.11" @@ -492,6 +512,18 @@ version = "2.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" +[[package]] +name = "mio" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys", +] + [[package]] name = "nibble_vec" version = "0.1.0" @@ -533,6 +565,29 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall 0.4.1", + "smallvec", + "windows-targets", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -671,6 +726,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_termios" version = "0.1.1" @@ -828,6 +892,36 @@ dependencies = [ "serde", ] +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + [[package]] name = "simdutf8" version = "0.1.4" @@ -990,15 +1084,13 @@ dependencies = [ [[package]] name = "tui" -version = "0.8.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b871b61f4c4b81e630215cd12e0ec29953d4545898e21a9e023b7520a74a9f9" +checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1" dependencies = [ "bitflags 1.3.2", "cassowary", - "either", - "itertools", - "log", + "crossterm", "termion 1.5.5", "unicode-segmentation", "unicode-width", diff --git a/Cargo.toml b/Cargo.toml index ec98981..8ec0837 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cryptools" -version = "0.12.4" +version = "0.12.5" authors = ["scoobybejesus "] edition = "2021" description = "Command-line utility for processing cryptocurrency transactions into 'lots' and 'movements'." @@ -26,7 +26,7 @@ rust_decimal_macros = "1.32.0" chrono = { version = "0.4.31", features = ["serde"] } structopt = "0.2.10" rustyline = "12.0.0" -tui = { version = "0.8", optional = true, features = ['termion'] } +tui = { version = "0.19", optional = true, features = ['termion'] } termion = { version = "2.0.1", optional = true } dotenv = "0.15.0" diff --git a/src/mytui/app.rs b/src/mytui/app.rs index 6c3f5b9..b9f2f9c 100644 --- a/src/mytui/app.rs +++ b/src/mytui/app.rs @@ -7,6 +7,7 @@ use std::collections::HashMap; use crptls::transaction::{Transaction, ActionRecord}; use crptls::account::{Account, RawAccount}; use crptls::core_functions::ImportProcessParameters; +use tui::widgets::ListState; use crate::export::{export_csv, export_je, export_txt}; @@ -24,36 +25,51 @@ pub (crate) const REPORTS: [&'static str; 11] = [ "11. TXT: Bookkeeping journal entries", ]; -pub struct ListState { +pub struct StatefulList { pub items: Vec, - pub selected: usize, + pub state: ListState, } -impl ListState { +impl StatefulList { - fn new(items: Vec) -> ListState { - ListState { items, selected: 0 } + fn new(items: Vec) -> StatefulList { + StatefulList { items, state: ListState::default() } } fn select_previous(&mut self) { - - if self.selected > 0 { - self.selected -= 1; - } + let i = match self.state.selected() { + Some(i) => { + if i == 0 { + self.items.len() - 1 + } else { + i - 1 + } + } + None => 0, + }; + self.state.select(Some(i)); } fn select_next(&mut self) { - - if self.selected < self.items.len() - 1 { - self.selected += 1 - } + let i = match self.state.selected() { + Some(i) => { + if i >= self.items.len() - 1 { + 0 + } else { + i + 1 + } + } + None => 0, + }; + self.state.select(Some(i)); } + } pub struct PrintWindow<'a> { pub title: &'a str, pub should_quit: bool, - pub tasks: ListState<&'a str>, + pub tasks: StatefulList<&'a str>, pub to_print_by_idx: Vec, pub to_print_by_title: Vec<&'a str>, } @@ -61,10 +77,13 @@ pub struct PrintWindow<'a> { impl<'a> PrintWindow<'a> { pub fn new(title: &'a str) -> PrintWindow<'a> { + let mut tasks = StatefulList::new(REPORTS.to_vec()); + tasks.state.select(Some(0)); + PrintWindow { title, should_quit: false, - tasks: ListState::new(REPORTS.to_vec()), + tasks, to_print_by_idx: Vec::with_capacity(REPORTS.len()), to_print_by_title: Vec::with_capacity(REPORTS.len()), } @@ -78,7 +97,7 @@ impl<'a> PrintWindow<'a> { self.tasks.select_next(); } - pub fn on_key(&mut self, c: char) { + pub fn on_key(&mut self, c: char) -> Result<(), Box> { match c { @@ -90,7 +109,7 @@ impl<'a> PrintWindow<'a> { self.should_quit = true; } 'x' => { - let selected = self.tasks.selected; + let selected = self.tasks.state.selected().unwrap(); if self.to_print_by_idx.contains(&selected) {} else { self.to_print_by_idx.push(selected); self.to_print_by_title.push(self.tasks.items[selected]) @@ -99,7 +118,7 @@ impl<'a> PrintWindow<'a> { self.tasks.select_next(); } 'd' => { - let selected_idx = self.tasks.selected; + let selected_idx = self.tasks.state.selected().unwrap(); self.to_print_by_idx.retain(|&x| x != selected_idx ); let selected_str = self.tasks.items[selected_idx]; self.to_print_by_title.retain(|&x| x != selected_str ); @@ -107,6 +126,7 @@ impl<'a> PrintWindow<'a> { } _ => {} } + Ok(()) } fn change_vecs_to_chrono_order(vec: &mut Vec, strvec: &mut Vec<&str>) { diff --git a/src/mytui/print_menu_tui.rs b/src/mytui/print_menu_tui.rs index 3d38c5f..d7e6742 100644 --- a/src/mytui/print_menu_tui.rs +++ b/src/mytui/print_menu_tui.rs @@ -46,14 +46,14 @@ pub (crate) fn print_menu_tui( loop { - ui::draw(&mut terminal, &app)?; + ui::draw(&mut terminal, &mut app)?; if let Event::Input(key) = events.next()? { match key { Key::Char(c) => { - app.on_key(c); + app.on_key(c)?; } Key::Up => { app.on_up(); diff --git a/src/mytui/ui.rs b/src/mytui/ui.rs index dacaa57..238d898 100644 --- a/src/mytui/ui.rs +++ b/src/mytui/ui.rs @@ -1,35 +1,57 @@ // Copyright (c) 2017-2023, scoobybejesus // Redistributions must include the license: https://github.com/scoobybejesus/cryptools/blob/master/LEGAL.txt -use std::io; +use std::error::Error; use ::tui::Terminal; use ::tui::style::{Color, Modifier, Style}; -use ::tui::widgets::{Widget, Block, Borders, SelectableList, Text, Paragraph, List}; +use ::tui::text::{Text, Span, Spans}; +use tui::widgets::{Wrap, ListItem}; +use ::tui::widgets::{Block, Borders, Paragraph, List}; use ::tui::layout::{Layout, Constraint, Direction}; use ::tui::backend::Backend; use crate::mytui::app::{PrintWindow, REPORTS}; -pub fn draw(terminal: &mut Terminal, app: &PrintWindow) -> Result<(), io::Error> { +pub fn draw(terminal: &mut Terminal, app: &mut PrintWindow) -> Result<(), Box> { - terminal.draw(|mut f| { + terminal.draw(|f| { - let instructions = [ - Text::raw("\nPress '"), - Text::styled("x", Style::default().fg(Color::Cyan).modifier(Modifier::BOLD)), - Text::raw("' to add the selected report to the list of reports to print/export.\n"), - Text::raw("\nPress '"), - Text::styled("d", Style::default().fg(Color::Yellow).modifier(Modifier::BOLD)), - Text::raw("' to delete the selected report from the list of reports to print/export.\n"), - Text::raw("\nPress '"), - Text::styled("p", Style::default().fg(Color::Green).modifier(Modifier::BOLD)), - Text::raw("' to print/export the selected reports.\n"), - Text::raw("\nPress '"), - Text::styled("q", Style::default().fg(Color::Red).modifier(Modifier::BOLD)), - Text::raw("' to quit without printing.\n\n"), + let instructions = vec![ + Spans::from(vec![Span::raw("")]), + + Spans::from(vec![ + Span::raw(" Press '"), + Span::styled("x", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)), + Span::raw("' to add the selected report to the list of reports to print/export."), + ]), + + Spans::from(vec![Span::raw("")]), + + Spans::from(vec![ + Span::raw(" Press '"), + Span::styled("d", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)), + Span::raw("' to delete the selected report from the list of reports to print/export."), + ]), + + Spans::from(vec![Span::raw("")]), + + Spans::from(vec![ + Span::raw(" Press '"), + Span::styled("p", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)), + Span::raw("' to print/export the selected reports."), + ]), + + Spans::from(vec![Span::raw("")]), + + Spans::from(vec![ + Span::raw(" Press '"), + Span::styled("q", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)), + Span::raw("' to quit without printing."), + ]), ]; + let rpts_to_prnt = app.to_print_by_title.iter().map(|&rpt_to_prnt| { Text::styled( format!("{}", rpt_to_prnt), @@ -40,53 +62,60 @@ pub fn draw(terminal: &mut Terminal, app: &PrintWindow) -> Result let top_level_chunks = Layout::default() .constraints([ Constraint::Length(1), - Constraint::Length(2 + (instructions.len() as u16 / 3 * 2)), // 2 for title "Instructions", plus 3 TEXT array elements/instruction + Constraint::Length(instructions.len() as u16 + 2), Constraint::Length(REPORTS.len() as u16 + 2), Constraint::Length(rpts_to_prnt.len() as u16 + 2), Constraint::Length(1), ].as_ref()) .split(f.size()); - Paragraph::new(instructions.iter()) - .block( - Block::default() - .borders(Borders::NONE) - .title("Instructions") - .title_style(Style::default().fg(Color::Blue).modifier(Modifier::BOLD).modifier(Modifier::UNDERLINED)), + let pg1 = Paragraph::new(instructions) + .block(Block::default() + .borders(Borders::NONE) + .title(Span::styled( + "Instructions", + Style::default().fg(Color::LightMagenta).add_modifier(Modifier::BOLD).add_modifier(Modifier::UNDERLINED) + )) ) - .wrap(true) - .render(&mut f, top_level_chunks[1]); + .wrap(Wrap {trim: false}); + f.render_widget(pg1, top_level_chunks[1]); let level_2_chunks = Layout::default() .constraints([Constraint::Percentage(10), Constraint::Percentage(80),Constraint::Percentage(10),].as_ref()) .direction(Direction::Horizontal) .split(top_level_chunks[2]); - SelectableList::default() - .block( - Block::default() - .borders(Borders::ALL) - .title("Reports available for exporting") - .title_style(Style::default().fg(Color::White).modifier(Modifier::BOLD).modifier(Modifier::UNDERLINED)) + let report_list_items: Vec<_> = app.tasks.items.iter().map(|i| ListItem::new(*i)).collect(); + + let items = List::new(report_list_items) + .block(Block::default() + .borders(Borders::ALL) + .title(Span::styled( + "Reports available for exporting", + Style::default().fg(Color::White).add_modifier(Modifier::BOLD).add_modifier(Modifier::UNDERLINED) + )) ) - .items(&app.tasks.items) - .select(Some(app.tasks.selected)) - .highlight_style(Style::default().fg(Color::Yellow).modifier(Modifier::BOLD)) - .highlight_symbol(">") - .render(&mut f, level_2_chunks[1]); + .highlight_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)) + .highlight_symbol(">"); + f.render_stateful_widget(items, level_2_chunks[1], &mut app.tasks.state); let level_2_chunks = Layout::default() .constraints([Constraint::Percentage(10), Constraint::Percentage(80),Constraint::Percentage(10),].as_ref()) .direction(Direction::Horizontal) .split(top_level_chunks[3]); - List::new(rpts_to_prnt) - .block( - Block::default() - .borders(Borders::ALL) - .title("Reports to be exported") - .title_style(Style::default().fg(Color::LightYellow).modifier(Modifier::BOLD).modifier(Modifier::UNDERLINED)) - ) - .render(&mut f, level_2_chunks[1]); - }) + let rpts_to_prnt: Vec<_> = app.to_print_by_title.iter().map(|i| ListItem::new(*i)).collect(); + + let to_print = List::new(rpts_to_prnt) + .block(Block::default() + .borders(Borders::ALL) + .title(Span::styled( + "Reports to be exported", + Style::default().fg(Color::LightYellow).add_modifier(Modifier::BOLD).add_modifier(Modifier::UNDERLINED) + )) + ); + f.render_widget(to_print, level_2_chunks[1]); + })?; + + Ok(()) } \ No newline at end of file