From ba8dd91af9af86a6079dfe8414ff31b9b166f992 Mon Sep 17 00:00:00 2001 From: Paul-Christian Volkmer Date: Wed, 20 Dec 2023 01:35:52 +0100 Subject: [PATCH] Add a terminal-based client as example --- Cargo.toml | 7 ++ README.md | 13 +- examples/arsnova-client-tui.rs | 212 +++++++++++++++++++++++++++++++++ examples/arsnova-client.gif | Bin 0 -> 29698 bytes 4 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 examples/arsnova-client-tui.rs create mode 100644 examples/arsnova-client.gif diff --git a/Cargo.toml b/Cargo.toml index d923309..6f2f574 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,14 @@ edition = "2021" license = "LGPL-3.0-or-later" authors = ["Paul-Christian Volkmer"] +[[example]] +name = "arsnova-client-tui" + [dependencies] +clap = { version = "4.4", features = ["std", "help", "usage", "derive", "error-context"], default-features = false } +ratatui = "0.25" +crossterm = "0.27" + futures-util = "0.3" reqwest = { version = "0.11", features = ["json", "rustls-tls"], default-features = false } serde = { version = "1.0", features = ["derive"] } diff --git a/README.md b/README.md index 078619b..bd6583c 100644 --- a/README.md +++ b/README.md @@ -49,4 +49,15 @@ Forward feedback to a channel: let (tx, rx) = tokio::sync::mpsc::channel::(10); client.on_feedback_changed( & cli.room, FeedbackHandler::Sender(tx.clone())).await; -``` \ No newline at end of file +``` + +## Example + +See [`examples/arsnova-client-tui.rs`](examples/arsnova-client-tui.rs) for a simple terminal-based feedback client +application. + +```bash +arsnova-client-tui 23269388 +``` + +![arsnova-client-tui](examples/arsnova-client.gif) \ No newline at end of file diff --git a/examples/arsnova-client-tui.rs b/examples/arsnova-client-tui.rs new file mode 100644 index 0000000..b49d73f --- /dev/null +++ b/examples/arsnova-client-tui.rs @@ -0,0 +1,212 @@ +/* + * This file is part of arsnova-client + * + * Copyright (C) 2023 Paul-Christian Volkmer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +use std::io::{stdout, Stdout}; + +use clap::Parser; +use crossterm::event::{KeyCode, KeyEventKind}; +use crossterm::terminal::{ + disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, +}; +use crossterm::{event, ExecutableCommand}; +use ratatui::backend::CrosstermBackend; +use ratatui::layout::{Alignment, Constraint, Direction, Layout}; +use ratatui::style::Stylize; +use ratatui::text::{Line, Span}; +use ratatui::widgets::Paragraph; +use ratatui::Terminal; +use tokio::select; +use tokio::sync::mpsc::Receiver; + +use arsnova_client::{Client, Feedback, FeedbackHandler}; + +#[derive(Parser)] +#[command(author, version, about = "Terminal-based ARSnova live feedback client", long_about = None)] +#[command(arg_required_else_help(true))] +pub struct Cli { + #[arg(help = "Raum")] + room: String, + #[arg( + short = 'u', + long = "url", + help = "API-URL", + default_value = "https://ars.particify.de/api" + )] + url: String, +} + +#[tokio::main(worker_threads = 3)] +async fn main() -> Result<(), ()> { + let cli = Cli::parse(); + + let mut client = match Client::new(&cli.url) { + Ok(client) => client, + Err(_) => return Err(()), + }; + + if client.guest_login().await.is_err() { + return Err(()); + } + + stdout().execute(EnterAlternateScreen).map_err(|_| ())?; + enable_raw_mode().map_err(|_| ())?; + let mut terminal = Terminal::new(CrosstermBackend::new(stdout())).map_err(|_| ())?; + terminal.clear().map_err(|_| ())?; + + let (tx, rx) = tokio::sync::mpsc::channel::(10); + + let l1 = client.on_feedback_changed(&cli.room, FeedbackHandler::Sender(tx.clone())); + + let _ = tx + .clone() + .send(client.get_feedback(&cli.room).await.unwrap()) + .await; + + let room_info = client.get_room_info(&cli.room).await.map_err(|_| ())?; + let title = format!("Live Feedback: {} ({})", room_info.name, room_info.short_id); + + let l2 = create_ui(&mut terminal, &title, rx); + + let l3 = tokio::spawn(async { + loop { + if event::poll(std::time::Duration::from_millis(16)) + .map_err(|_| ()) + .is_ok() + { + if let event::Event::Key(key) = event::read().map_err(|_| ()).unwrap() { + if key.kind == KeyEventKind::Press && key.code == KeyCode::Esc { + break; + } + } + } + } + }); + + select! { + _ = l1 => {}, + _ = l2 => {}, + _ = l3 => {}, + } + + let _ = stdout().execute(LeaveAlternateScreen).map_err(|_| ()); + let _ = disable_raw_mode().map_err(|_| ()).map_err(|_| ()); + + Ok(()) +} + +async fn create_ui( + terminal: &mut Terminal>, + title: &str, + mut rx: Receiver, +) -> Result<(), ()> { + fn feedback_paragraph(feedback: &Feedback, idx: usize, width: usize) -> Paragraph<'static> { + let value = match idx { + 0 => feedback.very_good, + 1 => feedback.good, + 2 => feedback.bad, + 3 => feedback.very_bad, + _ => 0, + }; + + let icon = match idx { + 0 => "Super ", + 1 => "Gut ", + 2 => "Nicht so gut", + 3 => "Schlecht ", + _ => " ", + }; + + let width = width - 24; + + let l = ((value as f32 / feedback.count_votes() as f32) * width as f32) as usize; + + match idx { + 0..=3 => Paragraph::new(Line::from(vec![ + Span::raw(format!("{} : ", icon)), + Span::raw(format!("[{: >5}] ", value)).dim(), + Span::raw("■".to_string().repeat(l).to_string()) + .green() + .on_black(), + Span::raw(" ".to_string().repeat(width - l).to_string()).on_black(), + ])), + _ => Paragraph::default(), + } + } + + loop { + let feedback = match rx.recv().await { + Some(feedback) => feedback, + _ => continue, + }; + + let _ = terminal.draw(|frame| { + let layout = Layout::default() + .direction(Direction::Vertical) + .constraints(vec![ + Constraint::Max(1), + Constraint::Max(6), + Constraint::Min(1), + Constraint::Max(1), + ]) + .split(frame.size()); + + frame.render_widget( + Paragraph::new(title) + .white() + .on_blue() + .bold() + .alignment(Alignment::Center), + layout[0], + ); + + frame.render_widget( + Paragraph::new(format!("{} Antworten", feedback.count_votes())) + .white() + .bold() + .alignment(Alignment::Center), + layout[2], + ); + + frame.render_widget( + Paragraph::new("Beenden mit ") + .on_blue() + .alignment(Alignment::Left), + layout[3], + ); + + let feedback_layout = Layout::default() + .direction(Direction::Vertical) + .constraints(vec![ + Constraint::Max(1), + Constraint::Max(1), + Constraint::Max(1), + Constraint::Max(1), + ]) + .margin(1) + .split(layout[1]); + + [0usize, 1, 2, 3].iter().for_each(|&idx| { + frame.render_widget( + feedback_paragraph(&feedback, idx, feedback_layout[idx].width as usize), + feedback_layout[idx], + ) + }); + }); + } +} diff --git a/examples/arsnova-client.gif b/examples/arsnova-client.gif new file mode 100644 index 0000000000000000000000000000000000000000..58171feca953517f79b870a4d19f0316b1257059 GIT binary patch literal 29698 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