Skip to content

Commit

Permalink
pczt to-qr: Add TUI-based render option
Browse files Browse the repository at this point in the history
  • Loading branch information
str4d committed Dec 12, 2024
1 parent f4b1df3 commit 5843c98
Show file tree
Hide file tree
Showing 3 changed files with 294 additions and 19 deletions.
71 changes: 58 additions & 13 deletions src/commands/pczt/qr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,16 @@ use nokhwa::{
};
use pczt::Pczt;
use qrcode::{render::unicode, QrCode};
use tokio::io::{stdin, stdout, AsyncReadExt, AsyncWriteExt};
use tokio::io::{stdin, stdout, AsyncReadExt, AsyncWriteExt, Stdout};

use crate::ShutdownListener;

#[cfg(feature = "tui")]
use crate::tui::Tui;

#[cfg(feature = "tui")]
mod tui;

const ZCASH_PCZT: &str = "zcash-pczt";
const UR_ZCASH_PCZT: &str = "ur:zcash-pczt";

Expand All @@ -27,10 +33,17 @@ pub(crate) struct Send {
default = "500"
)]
interval: u64,

#[cfg(feature = "tui")]
pub(crate) tui: bool,
}

impl Send {
pub(crate) async fn run(self, mut shutdown: ShutdownListener) -> Result<(), anyhow::Error> {
pub(crate) async fn run(
self,
mut shutdown: ShutdownListener,
#[cfg(feature = "tui")] tui: Tui,
) -> Result<(), anyhow::Error> {
let mut buf = vec![];
stdin().read_to_end(&mut buf).await?;

Expand All @@ -45,6 +58,20 @@ impl Send {
)
.map_err(|e| anyhow!("Failed to encode PCZT packet: {:?}", e))?;

#[cfg(feature = "tui")]
let tui_handle = if self.tui {
let mut app = tui::App::new(shutdown.tui_quit_signal());
let handle = app.handle();
tokio::spawn(async move {
if let Err(e) = app.run(tui).await {
tracing::error!("Error while running TUI: {e}");
}
});
Some(handle)
} else {
None
};

let mut encoder = ur::Encoder::new(&pczt_packet, 100, ZCASH_PCZT)
.map_err(|e| anyhow!("Failed to build UR encoder: {e}"))?;

Expand All @@ -60,17 +87,35 @@ impl Send {
let ur = encoder
.next_part()
.map_err(|e| anyhow!("Failed to encode PCZT part: {e}"))?;
let code = QrCode::new(&ur.to_ascii_uppercase())?;
let string = code
.render::<unicode::Dense1x2>()
.dark_color(unicode::Dense1x2::Light)
.light_color(unicode::Dense1x2::Dark)
.quiet_zone(true)
.build();

stdout.write_all(format!("{string}\n").as_bytes()).await?;
stdout.write_all(format!("{ur}\n\n\n\n").as_bytes()).await?;
stdout.flush().await?;

async fn render_cli(stdout: &mut Stdout, ur: String) -> anyhow::Result<()> {
let code = QrCode::new(ur.to_ascii_uppercase())?;
let string = code
.render::<unicode::Dense1x2>()
.dark_color(unicode::Dense1x2::Light)
.light_color(unicode::Dense1x2::Dark)
.quiet_zone(true)
.build();

stdout.write_all(format!("{string}\n").as_bytes()).await?;
stdout.write_all(format!("{ur}\n\n\n\n").as_bytes()).await?;
stdout.flush().await?;

Ok(())
}

#[cfg(feature = "tui")]
if let Some(handle) = tui_handle.as_ref() {
if handle.set_ur(ur) {
// TUI exited.
return Ok(());
}
} else {
render_cli(&mut stdout, ur).await?;
}

#[cfg(not(feature = "tui"))]
render_cli(&mut stdout, ur).await?;
}
}
}
Expand Down
214 changes: 214 additions & 0 deletions src/commands/pczt/qr/tui.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
use crossterm::event::KeyCode;
use futures_util::FutureExt;
use qrcode::{render::unicode, QrCode};
use ratatui::{
prelude::*,
widgets::{Block, Paragraph},
};
use tokio::sync::{mpsc, oneshot};
use tracing::{error, info, warn};
use tui_logger::{TuiLoggerLevelOutput, TuiLoggerSmartWidget};

use crate::tui;

pub(super) struct AppHandle {
action_tx: mpsc::UnboundedSender<Action>,
}

impl AppHandle {
/// Returns `true` if the TUI exited.
pub(super) fn set_ur(&self, ur: String) -> bool {
match self.action_tx.send(Action::SetUr(ur)) {
Ok(()) => false,
Err(e) => {
error!("Failed to send: {}", e);
true
}
}
}
}

pub(super) struct App {
should_quit: bool,
notify_shutdown: Option<oneshot::Sender<()>>,
ur: Option<String>,
action_tx: mpsc::UnboundedSender<Action>,
action_rx: mpsc::UnboundedReceiver<Action>,
logger_state: tui_logger::TuiWidgetState,
}

impl App {
pub(super) fn new(notify_shutdown: oneshot::Sender<()>) -> Self {
let (action_tx, action_rx) = mpsc::unbounded_channel();
Self {
should_quit: false,
notify_shutdown: Some(notify_shutdown),
ur: None,
action_tx,
action_rx,
logger_state: tui_logger::TuiWidgetState::new(),
}
}

pub(super) fn handle(&self) -> AppHandle {
AppHandle {
action_tx: self.action_tx.clone(),
}
}

pub(super) async fn run(&mut self, mut tui: tui::Tui) -> anyhow::Result<()> {
tui.enter()?;

loop {
let action_queue_len = self.action_rx.len();
if action_queue_len >= 50 {
warn!("Action queue lagging! Length: {}", action_queue_len);
}

let next_event = tui.next().fuse();
let next_action = self.action_rx.recv().fuse();
tokio::select! {
Some(event) = next_event => if let Some(action) = Action::for_event(event) {
self.action_tx.send(action)?;
},
Some(action) = next_action => match action {
Action::Quit => {
info!("Quit requested");
self.should_quit = true;
let _ = self.notify_shutdown.take().expect("should only occur once").send(());
break;
}
Action::Tick => {}
Action::LoggerEvent(event) => self.logger_state.transition(event),
Action::SetUr(ur) => self.ur = Some(ur),
Action::Render => {
tui.draw(|f| self.ui(f))?;
}
}
}

if self.should_quit {
break;
}
}

self.action_rx.close();
tui.exit()?;

Ok(())
}

fn ui(&mut self, frame: &mut Frame) {
let [upper_area, mid_area, log_area] = Layout::vertical([
Constraint::Min(0),
Constraint::Length(3),
Constraint::Length(15),
])
.areas(frame.area());

let defrag_area = {
let block = Block::bordered().title("Wallet Defragmentor");
let inner_area = block.inner(upper_area);
frame.render_widget(block, upper_area);
inner_area
};

if let Some(ur) = &self.ur {
let code = QrCode::new(ur.to_ascii_uppercase()).unwrap();
let string = code
.render::<unicode::Dense1x2>()
.dark_color(unicode::Dense1x2::Light)
.light_color(unicode::Dense1x2::Dark)
.quiet_zone(true)
.build();

let lines = string.lines();

for (i, line) in lines.into_iter().enumerate() {
frame.render_widget(
Paragraph::new(line),
Rect::new(
defrag_area.x,
defrag_area.y + i as u16,
line.len() as u16,
1,
),
);
}

frame.render_widget(
Paragraph::new(Line::from_iter(Some(ur))).block(Block::bordered().title("UR")),
mid_area,
);
}

frame.render_widget(
TuiLoggerSmartWidget::default()
.title_log("Log Entries")
.title_target("Log Target Selector")
.style_error(Style::default().fg(Color::Red))
.style_debug(Style::default().fg(Color::Green))
.style_warn(Style::default().fg(Color::Yellow))
.style_trace(Style::default().fg(Color::Magenta))
.style_info(Style::default().fg(Color::Cyan))
.output_separator(':')
.output_timestamp(Some("%H:%M:%S".to_string()))
.output_level(Some(TuiLoggerLevelOutput::Abbreviated))
.output_target(true)
.output_file(true)
.output_line(true)
.state(&self.logger_state),
log_area,
);
}
}

#[derive(Clone)]
pub(super) enum Action {
Quit,
Tick,
LoggerEvent(tui_logger::TuiWidgetEvent),
SetUr(String),
Render,
}

impl Action {
fn for_event(event: tui::Event) -> Option<Self> {
match event {
tui::Event::Error => None,
tui::Event::Tick => Some(Action::Tick),
tui::Event::Render => Some(Action::Render),
tui::Event::Key(key) => match key.code {
KeyCode::Char('q') => Some(Action::Quit),
KeyCode::Char(' ') => {
Some(Action::LoggerEvent(tui_logger::TuiWidgetEvent::SpaceKey))
}
KeyCode::Up => Some(Action::LoggerEvent(tui_logger::TuiWidgetEvent::UpKey)),
KeyCode::Down => Some(Action::LoggerEvent(tui_logger::TuiWidgetEvent::DownKey)),
KeyCode::Left => Some(Action::LoggerEvent(tui_logger::TuiWidgetEvent::LeftKey)),
KeyCode::Right => Some(Action::LoggerEvent(tui_logger::TuiWidgetEvent::RightKey)),
KeyCode::Char('+') => {
Some(Action::LoggerEvent(tui_logger::TuiWidgetEvent::PlusKey))
}
KeyCode::Char('-') => {
Some(Action::LoggerEvent(tui_logger::TuiWidgetEvent::MinusKey))
}
KeyCode::Char('h') => {
Some(Action::LoggerEvent(tui_logger::TuiWidgetEvent::HideKey))
}
KeyCode::Char('f') => {
Some(Action::LoggerEvent(tui_logger::TuiWidgetEvent::FocusKey))
}
KeyCode::PageUp => {
Some(Action::LoggerEvent(tui_logger::TuiWidgetEvent::PrevPageKey))
}
KeyCode::PageDown => {
Some(Action::LoggerEvent(tui_logger::TuiWidgetEvent::NextPageKey))
}
KeyCode::Esc => Some(Action::LoggerEvent(tui_logger::TuiWidgetEvent::EscapeKey)),
_ => None,
},
_ => None,
}
}
}
28 changes: 22 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,21 @@ fn main() -> Result<(), anyhow::Error> {
#[cfg(not(feature = "tui"))]
let tui_logger: Option<()> = None;
#[cfg(feature = "tui")]
let tui_logger =
if let Some(Command::Sync(commands::sync::Command { defrag: true, .. })) = opts.command {
let tui_logger = match opts.command {
Some(Command::Sync(commands::sync::Command { defrag: true, .. })) => {
tui_logger::init_logger(level_filter.parse().unwrap())?;
Some(tui_logger::tracing_subscriber_layer())
} else {
None
};
}
#[cfg(feature = "pczt-qr")]
Some(Command::Pczt(commands::pczt::Command::ToQr(commands::pczt::qr::Send {
tui: true,
..
}))) => {
tui_logger::init_logger(level_filter.parse().unwrap())?;
Some(tui_logger::tracing_subscriber_layer())
}
_ => None,
};

let stdout_logger = if tui_logger.is_none() {
let filter = tracing_subscriber::EnvFilter::from(level_filter);
Expand Down Expand Up @@ -166,7 +174,15 @@ fn main() -> Result<(), anyhow::Error> {
commands::pczt::Command::Combine(command) => command.run().await,
commands::pczt::Command::Send(command) => command.run(opts.wallet_dir).await,
#[cfg(feature = "pczt-qr")]
commands::pczt::Command::ToQr(command) => command.run(shutdown).await,
commands::pczt::Command::ToQr(command) => {
command
.run(
shutdown,
#[cfg(feature = "tui")]
tui,
)
.await
}
#[cfg(feature = "pczt-qr")]
commands::pczt::Command::FromQr(command) => command.run(shutdown).await,
},
Expand Down

0 comments on commit 5843c98

Please sign in to comment.