From dfdba65d1a1fb378853c66d94f545336c2a3b6c9 Mon Sep 17 00:00:00 2001 From: Daniel Sanchez Quiros Date: Tue, 18 Oct 2022 16:02:15 +0200 Subject: [PATCH] Added main skeleton for toy-chat example --- examples/Cargo.toml | 5 + examples/toy-chat/Cargo.toml | 14 +++ examples/toy-chat/src/main.rs | 183 ++++++++++++++++++++++++++++++ examples/toy-chat/src/protocol.rs | 29 +++++ waku/src/lib.rs | 4 +- 5 files changed, 233 insertions(+), 2 deletions(-) create mode 100644 examples/Cargo.toml create mode 100644 examples/toy-chat/Cargo.toml create mode 100644 examples/toy-chat/src/main.rs create mode 100644 examples/toy-chat/src/protocol.rs diff --git a/examples/Cargo.toml b/examples/Cargo.toml new file mode 100644 index 0000000..0ea93d3 --- /dev/null +++ b/examples/Cargo.toml @@ -0,0 +1,5 @@ +[workspace] + +members = [ + "toy-chat" +] \ No newline at end of file diff --git a/examples/toy-chat/Cargo.toml b/examples/toy-chat/Cargo.toml new file mode 100644 index 0000000..5108f94 --- /dev/null +++ b/examples/toy-chat/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "toy-chat" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +waku = { path = "../../waku" } +tui = "0.19" +crossterm = "0.25" +unicode-width = "0.1" +prost = "0.11" +once_cell = "1.15" \ No newline at end of file diff --git a/examples/toy-chat/src/main.rs b/examples/toy-chat/src/main.rs new file mode 100644 index 0000000..682fbca --- /dev/null +++ b/examples/toy-chat/src/main.rs @@ -0,0 +1,183 @@ +mod protocol; + +use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use std::{error::Error, io}; +use tui::{ + backend::{Backend, CrosstermBackend}, + layout::{Constraint, Direction, Layout}, + style::{Color, Modifier, Style}, + text::{Span, Spans, Text}, + widgets::{Block, Borders, List, ListItem, Paragraph}, + Frame, Terminal, +}; +use unicode_width::UnicodeWidthStr; +use waku::{waku_new, Result, WakuNodeConfig, WakuNodeHandle}; + +enum InputMode { + Normal, + Editing, +} + +/// App holds the state of the application +struct App { + /// Current value of the input box + input: String, + /// Current input mode + input_mode: InputMode, + /// History of recorded messages + messages: Vec, +} + +impl Default for App { + fn default() -> App { + App { + input: String::new(), + input_mode: InputMode::Normal, + messages: Vec::new(), + } + } +} + +fn main() -> std::result::Result<(), Box> { + // setup terminal + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + // create app and run it + let app = App::default(); + let res = run_app(&mut terminal, app); + + // restore terminal + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + + if let Err(err) = res { + println!("{:?}", err) + } + + Ok(()) +} + +fn run_app(terminal: &mut Terminal, mut app: App) -> io::Result<()> { + loop { + terminal.draw(|f| ui(f, &app))?; + + if let Event::Key(key) = event::read()? { + match app.input_mode { + InputMode::Normal => match key.code { + KeyCode::Char('e') => { + app.input_mode = InputMode::Editing; + } + KeyCode::Char('q') => { + return Ok(()); + } + _ => {} + }, + InputMode::Editing => match key.code { + KeyCode::Enter => { + app.messages.push(app.input.drain(..).collect()); + } + KeyCode::Char(c) => { + app.input.push(c); + } + KeyCode::Backspace => { + app.input.pop(); + } + KeyCode::Esc => { + app.input_mode = InputMode::Normal; + } + _ => {} + }, + } + } + } +} + +fn ui(f: &mut Frame, app: &App) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(2) + .constraints( + [ + Constraint::Length(1), + Constraint::Length(3), + Constraint::Min(1), + ] + .as_ref(), + ) + .split(f.size()); + + let (msg, style) = match app.input_mode { + InputMode::Normal => ( + vec![ + Span::raw("Press "), + Span::styled("q", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to exit, "), + Span::styled("e", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to start editing."), + ], + Style::default().add_modifier(Modifier::RAPID_BLINK), + ), + InputMode::Editing => ( + vec![ + Span::raw("Press "), + Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to stop editing, "), + Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to record the message"), + ], + Style::default(), + ), + }; + let mut text = Text::from(Spans::from(msg)); + text.patch_style(style); + let help_message = Paragraph::new(text); + f.render_widget(help_message, chunks[0]); + + let input = Paragraph::new(app.input.as_ref()) + .style(match app.input_mode { + InputMode::Normal => Style::default(), + InputMode::Editing => Style::default().fg(Color::Yellow), + }) + .block(Block::default().borders(Borders::ALL).title("Input")); + f.render_widget(input, chunks[1]); + match app.input_mode { + InputMode::Normal => + // Hide the cursor. `Frame` does this by default, so we don't need to do anything here + {} + + InputMode::Editing => { + // Make the cursor visible and ask tui-rs to put it at the specified coordinates after rendering + f.set_cursor( + // Put cursor past the end of the input text + chunks[1].x + app.input.width() as u16 + 1, + // Move one line down, from the border to the input line + chunks[1].y + 1, + ) + } + } + + let messages: Vec = app + .messages + .iter() + .enumerate() + .map(|(i, m)| { + let content = vec![Spans::from(Span::raw(format!("{}: {}", i, m)))]; + ListItem::new(content) + }) + .collect(); + let messages = List::new(messages).block(Block::default().borders(Borders::ALL).title("Chat")); + f.render_widget(messages, chunks[2]); +} diff --git a/examples/toy-chat/src/protocol.rs b/examples/toy-chat/src/protocol.rs new file mode 100644 index 0000000..0181204 --- /dev/null +++ b/examples/toy-chat/src/protocol.rs @@ -0,0 +1,29 @@ +use once_cell::sync::{Lazy, OnceCell}; +use prost::{ + encoding::{bytes, string, uint64}, + Message, +}; +use waku::{Encoding, WakuContentTopic, WakuMessage}; + +const TOY_CHAT_CONTENT_TOPIC: Lazy = Lazy::new(|| WakuContentTopic { + application_name: "toy-chat".into(), + version: 2, + content_topic_name: "huilong".into(), + encoding: Encoding::Proto, +}); + +#[derive(Clone, Message)] +pub struct Chat2Message { + #[prost(uint64, tag = "1")] + timestamp: u64, + #[prost(string, tag = "2")] + nick: String, + #[prost(bytes, tag = "3")] + payload: Vec, +} + +impl Chat2Message { + pub fn message(&self) -> String { + String::from_utf8(self.payload.clone()).unwrap() + } +} diff --git a/waku/src/lib.rs b/waku/src/lib.rs index 968819b..cc7f979 100644 --- a/waku/src/lib.rs +++ b/waku/src/lib.rs @@ -14,8 +14,8 @@ pub use node::{ pub use general::{ ContentFilter, DecodedPayload, Encoding, FilterSubscription, MessageId, MessageIndex, - PagingOptions, PeerId, ProtocolId, StoreQuery, StoreResponse, WakuContentTopic, WakuMessage, - WakuMessageVersion, WakuPubSubTopic, + PagingOptions, PeerId, ProtocolId, Result, StoreQuery, StoreResponse, WakuContentTopic, + WakuMessage, WakuMessageVersion, WakuPubSubTopic, }; pub use events::{waku_set_event_callback, Event, Signal, WakuMessageEvent};