Skip to content

Commit

Permalink
Refactor (#19)
Browse files Browse the repository at this point in the history
* Added rustfmt custom file

* Added CLI validators

* State as application member. Splitted event functions

* Added Config

* Removed terminal as member of application

* Added renderer concept

* Renaming Message types. Some refactorizations

* Introduced command and actions

* Separated actions and commands

* Adapted read_file and UserData to Command/Action API

* Adapted progress message

* Fixed issues from refactorization

* Update Readme.md

* Apply fmt

* Fixed rustfmt.toml
  • Loading branch information
lemunozm authored Nov 24, 2020
1 parent fd598ff commit 4a0f5a4
Show file tree
Hide file tree
Showing 16 changed files with 557 additions and 623 deletions.
9 changes: 9 additions & 0 deletions .rustfmt.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
control_brace_style = "ClosingNextLine"
use_small_heuristics = "Max"
reorder_impl_items = true
reorder_imports = false
reorder_modules = false
trailing_semicolon = false
use_field_init_shorthand = true
use_try_shorthand = true
where_single_line = true
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,10 @@ You can set a custom tcp sever port with `-t <port>`
### Commands
Termchat treats messages containings the following commands in a special way:

- **`?send <$path_to_file>`**: sends the specified file to everyone on the network, exp: `?send ./myfile`
- **`?send <$path_to_file>`**: sends the specified file to everyone on the network,
example: `?send ./myfile`

Note: The received files can be found in `/tmp/termchat/<termchat-username>/<file_name>`

## Frequently Asked Questions

Expand Down
12 changes: 12 additions & 0 deletions src/action.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use crate::state::{State};

use message_io::network::{NetworkManager};

pub enum Processing {
Completed,
Partial,
}

pub trait Action: Send {
fn process(&mut self, state: &mut State, network: &mut NetworkManager) -> Processing;
}
505 changes: 200 additions & 305 deletions src/application.rs

Large diffs are not rendered by default.

31 changes: 0 additions & 31 deletions src/application/commands.rs

This file was deleted.

63 changes: 0 additions & 63 deletions src/application/read_event.rs

This file was deleted.

36 changes: 36 additions & 0 deletions src/commands.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
pub mod send_file;

use crate::action::{Action};
use crate::util::{Result};

use std::collections::{HashMap};

pub trait Command {
fn name(&self) -> &'static str;
fn parse_params(&self, params: Vec<&str>) -> Result<Box<dyn Action>>;
}

#[derive(Default)]
pub struct CommandManager {
parsers: HashMap<&'static str, Box<dyn Command>>,
}

impl CommandManager {
pub const COMMAND_PREFIX: &'static str = "?";

pub fn with(mut self, command_parser: impl Command + 'static) -> Self {
self.parsers.insert(command_parser.name(), Box::new(command_parser));
self
}

pub fn find_command_action(&self, input: &str) -> Option<Result<Box<dyn Action>>> {
let mut input = input.split_whitespace();
let start = input.next().expect("Input must have some content");
if start.starts_with(Self::COMMAND_PREFIX) {
if let Some(parser) = self.parsers.get(&start[1..]) {
return Some(parser.parse_params(input.collect()))
}
}
None
}
}
80 changes: 80 additions & 0 deletions src/commands/send_file.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use crate::action::{Action, Processing};
use crate::commands::{Command};
use crate::state::{State};
use crate::message::{NetMessage, Chunk};
use crate::util::{Result};

use message_io::network::{NetworkManager};

use std::path::{Path};
use std::io::{Read};

pub struct SendFileCommand;

impl Command for SendFileCommand {
fn name(&self) -> &'static str {
"send"
}

fn parse_params(&self, params: Vec<&str>) -> Result<Box<dyn Action>> {
let file_path = params.get(0).ok_or("No file specified")?;
match SendFile::new(file_path) {
Ok(action) => Ok(Box::new(action)),
Err(e) => Err(e),
}
}
}

pub struct SendFile {
file: std::fs::File,
file_name: String,
file_size: u64,
progress_id: Option<usize>,
}

impl SendFile {
const CHUNK_SIZE: usize = 65500;

pub fn new(file_path: &str) -> Result<SendFile> {
const READ_FILENAME_ERROR: &str = "Unable to read file name";
let file_path = Path::new(file_path);
let file_name = file_path
.file_name()
.ok_or(READ_FILENAME_ERROR)?
.to_str()
.ok_or(READ_FILENAME_ERROR)?
.to_string();

let file_size = std::fs::metadata(file_path)?.len();
let file = std::fs::File::open(file_path)?;

Ok(SendFile { file, file_name, file_size, progress_id: None })
}
}

impl Action for SendFile {
fn process(&mut self, state: &mut State, network: &mut NetworkManager) -> Processing {
if self.progress_id.is_none() {
let id = state.add_progress_message(&self.file_name, self.file_size);
self.progress_id = Some(id);
}

let mut data = [0; Self::CHUNK_SIZE];
let (bytes_read, chunk, processing) = match self.file.read(&mut data) {
Ok(0) => (0, Chunk::End, Processing::Completed),
Ok(bytes_read) => (bytes_read, Chunk::Data(data.to_vec()), Processing::Partial),
Err(error) => {
let msg = format!("Error sending file. error: {}", error);
state.add_system_error_message(msg);
(0, Chunk::Error, Processing::Completed)
}
};

state.progress_message_update(self.progress_id.unwrap(), bytes_read as u64);

let message = NetMessage::UserData(self.file_name.clone(), chunk);
network.send_all(state.all_user_endpoints(), message).ok(); //Best effort

processing
}
}
46 changes: 24 additions & 22 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
mod application;
mod state;
mod terminal_events;
mod message;
mod renderer;
mod action;
mod commands;
mod ui;
mod util;

use application::Application;
use application::{Application, Config};

use clap::{App, Arg};

use std::net::{SocketAddrV4};

fn main() {
let os_username = whoami::username();

Expand All @@ -20,13 +26,21 @@ fn main() {
.long("discovery")
.short("d")
.default_value("238.255.0.1:5877")
.validator(|addr| match addr.parse::<SocketAddrV4>() {
Ok(_) => Ok(()),
Err(_) => Err("The value must have syntax ipv4:port".into()),
})
.help("Multicast address to found others 'termchat' applications"),
)
.arg(
Arg::with_name("tcp_server_port")
.long("tcp-server-port")
.short("t")
.default_value("0")
.validator(|port| match port.parse::<u16>() {
Ok(_) => Ok(()),
Err(_) => Err("The value must be in range 0..65535".into()),
})
.help("Tcp server port used when communicating with other termchat instances"),
)
.arg(
Expand All @@ -38,31 +52,19 @@ fn main() {
)
.get_matches();

// The next unwraps are safe because we specified a default value

let discovery_addr = match matches.value_of("discovery").unwrap().parse() {
Ok(discovery_addr) => discovery_addr,
Err(_) => return eprintln!("'discovery' must be a valid multicast address"),
// The next unwraps are safe because we specified a default value and a validator
let config = Config {
discovery_addr: matches.value_of("discovery").unwrap().parse().unwrap(),
tcp_server_port: matches.value_of("tcp_server_port").unwrap().parse().unwrap(),
user_name: matches.value_of("username").unwrap().into(),
};

let tcp_server_port = match matches.value_of("tcp_server_port").unwrap().parse() {
Ok(port) => port,
Err(_) => return eprintln!("Unable to parse tcp server port"),
let result = match Application::new(&config) {
Ok(mut app) => app.run(),
Err(e) => Err(e),
};

let name = matches.value_of("username").unwrap();

let error = match Application::new(discovery_addr, tcp_server_port, &name) {
Ok(mut app) => {
if let Err(e) = app.run() {
Some(e)
} else {
None
}
}
Err(e) => Some(e),
};
if let Some(e) = error {
if let Err(e) = result {
// app is now dropped we can print to stderr safely
eprintln!("termchat exited with error: {}", e);
}
Expand Down
16 changes: 16 additions & 0 deletions src/message.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
pub enum Chunk {
Data(Vec<u8>),
Error,
End,
}

#[derive(Serialize, Deserialize)]
pub enum NetMessage {
HelloLan(String, u16), // user_name, server_port
HelloUser(String), // user_name
UserMessage(String), // content
UserData(String, Chunk), // file_name, chunk
}
43 changes: 43 additions & 0 deletions src/renderer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use crate::ui::{self};
use crate::state::{State};
use crate::util::{Result};

use crossterm::terminal::{self};
use crossterm::{ExecutableCommand};

use tui::{Terminal};
use tui::backend::{CrosstermBackend};

use std::io::{self, Stdout};

pub struct Renderer {
terminal: Terminal<CrosstermBackend<Stdout>>,
}

impl Renderer {
pub fn new() -> Result<Renderer> {
terminal::enable_raw_mode()?;
io::stdout().execute(terminal::EnterAlternateScreen)?;

Ok(Renderer { terminal: Terminal::new(CrosstermBackend::new(io::stdout()))? })
}

pub fn render(&mut self, state: &State) -> Result<()> {
self.terminal.draw(|frame| ui::draw(frame, state, frame.size()))?;
Ok(())
}
}

impl Drop for Renderer {
fn drop(&mut self) {
io::stdout().execute(terminal::LeaveAlternateScreen).expect("Could not execute to stdout");
terminal::disable_raw_mode().expect("Terminal doesn't support to disable raw mode");
if std::thread::panicking() {
eprintln!(
"{}, example: {}",
"termchat paniced, to log the error you can redirect stderror to a file",
"termchat 2> termchat_log"
);
}
}
}
Loading

0 comments on commit 4a0f5a4

Please sign in to comment.