Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Send files #11

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ tui = { version = "0.12.0", default-features = false, features = ['crossterm'] }
whoami = "0.9.0"
chrono = "0.4.19"
clap = "2.33.3"
unicode-width = "0.1.8"
70 changes: 61 additions & 9 deletions src/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ use std::net::SocketAddr;

#[derive(Serialize, Deserialize)]
enum NetMessage {
HelloLan(String, u16), // user_name, server_port
HelloUser(String), // user_name
UserMessage(String), // content
HelloLan(String, u16), // user_name, server_port
HelloUser(String), // user_name
UserMessage(String), // content
UserData(String, Vec<u8>), // file_name, data
}

enum Event {
Expand Down Expand Up @@ -134,6 +135,25 @@ impl Application {
state.add_message(message);
}
}
NetMessage::UserData(file_name, data) => {
if state.user_name(endpoint).is_some() {
// safe unwrap due to check
let user = state.user_name(endpoint).unwrap().to_owned();
let path = std::env::temp_dir().join("termchat");
let user_path = path.join(&user);
// Ignore already exists error
let _ = std::fs::create_dir_all(&user_path);
let file_path = user_path.join(file_name);

if let Err(e) = std::fs::write(file_path, data) {
state.add_message(termchat_error_message(format!(
"termchat: Failed to write data sent from user: {}",
user
)));
state.add_message(termchat_error_message(e.to_string()));
}
}
}
},
NetEvent::AddedEndpoint(_) => (),
NetEvent::RemovedEndpoint(endpoint) => {
Expand Down Expand Up @@ -162,18 +182,21 @@ impl Application {
state.all_user_endpoints(),
NetMessage::UserMessage(input.clone()),
) {
LogMessage::new(
String::from("termchat :"),
MessageType::Error(format_errors(e)),
)
termchat_error_message(stringify_sendall_errors(e))
} else {
LogMessage::new(
format!("{} (me)", self.user_name),
MessageType::Content(input),
MessageType::Content(input.clone()),
)
};

state.add_message(message);

if let Err(parse_error) = self.parse_input(&input, &mut state) {
state.add_message(termchat_error_message(
parse_error.to_string(),
));
}
}
}
KeyCode::Delete => {
Expand Down Expand Up @@ -219,6 +242,31 @@ impl Application {
ui::draw(&mut self.terminal, &state)?;
}
}

fn parse_input(&mut self, input: &str, state: &mut ApplicationState) -> Result<()> {
const SEND_COMMAND: &str = "?send";
const READ_FILENAME_ERROR: &str = "Unable to read file name";

if input.starts_with(SEND_COMMAND) {
let path =
std::path::Path::new(input.split_whitespace().nth(1).ok_or("No file specifed")?);
let file_name = path
.file_name()
.ok_or(READ_FILENAME_ERROR)?
.to_str()
.ok_or(READ_FILENAME_ERROR)?
.to_string();
let data = std::fs::read(path)?;

self.network
.send_all(
state.all_user_endpoints(),
NetMessage::UserData(file_name, data),
)
.map_err(stringify_sendall_errors)?;
}
Ok(())
}
}

impl Drop for Application {
Expand Down Expand Up @@ -246,7 +294,11 @@ fn clean_terminal() {
}
}

fn format_errors(e: Vec<(message_io::network::Endpoint, io::Error)>) -> String {
fn termchat_error_message(e: String) -> LogMessage {
LogMessage::new(String::from("termchat: "), MessageType::Error(e))
}

fn stringify_sendall_errors(e: Vec<(message_io::network::Endpoint, io::Error)>) -> String {
let mut out = String::new();
for (endpoint, error) in e {
let msg = format!("Failed to connect to {}, error: {}", endpoint, error);
Expand Down
32 changes: 27 additions & 5 deletions src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ impl LogMessage {
pub struct ApplicationState {
messages: Vec<LogMessage>,
scroll_messages_view: usize,
input: String,
input: Vec<char>,
input_cursor: usize,
lan_users: HashMap<Endpoint, String>,
users_id: HashMap<String, usize>,
Expand All @@ -55,7 +55,7 @@ impl ApplicationState {
ApplicationState {
messages: Vec::new(),
scroll_messages_view: 0,
input: String::new(),
input: vec![],
input_cursor: 0,
lan_users: HashMap::new(),
users_id: HashMap::new(),
Expand All @@ -71,12 +71,34 @@ impl ApplicationState {
self.scroll_messages_view
}

pub fn input(&self) -> &str {
pub fn input(&self) -> &[char] {
&self.input
}

pub fn input_cursor(&self) -> usize {
self.input_cursor
pub fn ui_input_cursor(&self, width: usize) -> (u16, u16) {
let mut position = (0, 0);

for current_char in self.input.iter().take(self.input_cursor) {
let char_width = unicode_width::UnicodeWidthChar::width(*current_char).unwrap_or(0);

position.0 += char_width;

match position.0.cmp(&width) {
std::cmp::Ordering::Equal => {
position.0 = 0;
position.1 += 1;
}
std::cmp::Ordering::Greater => {
// Handle a char with width > 1 at the end of the row
// width - (char_width - 1) accounts for the empty column(s) left behind
position.0 -= width - (char_width - 1);
position.1 += 1;
}
_ => (),
}
}

(position.0 as u16, position.1 as u16)
}

pub fn user_name(&self, endpoint: Endpoint) -> Option<&String> {
Expand Down
53 changes: 37 additions & 16 deletions src/ui.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use super::state::{ApplicationState, MessageType};
use super::util::SplitEach;
use super::util::split_each;
use crate::util::Result;

use tui::backend::CrosstermBackend;
Expand Down Expand Up @@ -55,12 +55,15 @@ fn draw_messages_panel(
Span::styled(&message.user, Style::default().fg(color)),
Span::styled(" is offline", Style::default().fg(color)),
]),
MessageType::Content(content) => Spans::from(vec![
Span::styled(date, Style::default().fg(Color::DarkGray)),
Span::styled(&message.user, Style::default().fg(color)),
Span::styled(": ", Style::default().fg(color)),
Span::raw(content),
]),
MessageType::Content(content) => {
let mut ui_message = vec![
Span::styled(date, Style::default().fg(Color::DarkGray)),
Span::styled(&message.user, Style::default().fg(color)),
Span::styled(": ", Style::default().fg(color)),
];
ui_message.extend(parse_content(content));
Spans::from(ui_message)
}
MessageType::Error(error) => Spans::from(vec![
Span::styled(date, Style::default().fg(Color::DarkGray)),
Span::styled(&message.user, Style::default().fg(Color::Red)),
Expand All @@ -83,18 +86,38 @@ fn draw_messages_panel(
frame.render_widget(messages_panel, chunk);
}

fn parse_content<'a>(content: &'a str) -> Vec<Span<'a>> {
const SEND_COMMAND: &str = "?send";

if content.starts_with(SEND_COMMAND) {
content
.splitn(2, SEND_COMMAND)
.enumerate()
.map(|(index, part)| {
// ?send
if index == 0 {
Span::styled(SEND_COMMAND, Style::default().fg(Color::LightYellow))
} else {
Span::raw(part)
}
})
.collect()
} else {
vec![Span::raw(content)]
}
}

fn draw_input_panel(
frame: &mut Frame<CrosstermBackend<Stdout>>,
state: &ApplicationState,
chunk: Rect,
) {
let inner_width = (chunk.width - 2) as usize;

let input = state
.input()
.split_each(inner_width)
.iter()
.map(|line| Spans::from(vec![Span::raw(*line)]))
let input = state.input().iter().collect::<String>();
let input = split_each(input, inner_width)
.into_iter()
.map(|line| Spans::from(vec![Span::raw(line)]))
.collect::<Vec<_>>();

let input_panel = Paragraph::new(input)
Expand All @@ -107,8 +130,6 @@ fn draw_input_panel(

frame.render_widget(input_panel, chunk);

frame.set_cursor(
chunk.x + 1 + (state.input_cursor() % inner_width) as u16,
chunk.y + 1 + (state.input_cursor() / inner_width) as u16,
)
let input_cursor = state.ui_input_cursor(inner_width);
frame.set_cursor(chunk.x + 1 + input_cursor.0, chunk.y + 1 + input_cursor.1)
}
31 changes: 18 additions & 13 deletions src/util.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
pub type Error = Box<dyn std::error::Error + Send + Sync>;
pub type Result<T> = std::result::Result<T, Error>;

pub trait SplitEach {
fn split_each(&self, n: usize) -> Vec<&Self>;
}
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
pub fn split_each(input: String, width: usize) -> Vec<String> {
let mut splitted = Vec::with_capacity(input.width() / width);
let mut row = String::new();

let mut index = 0;

impl SplitEach for str {
fn split_each(&self, n: usize) -> Vec<&str> {
let mut splitted =
Vec::with_capacity(self.len() / n + if self.len() % n > 0 { 1 } else { 0 });
let mut last = self;
while !last.is_empty() {
let (chunk, rest) = last.split_at(std::cmp::min(n, last.len()));
splitted.push(chunk);
last = rest;
for current_char in input.chars() {
if (index != 0 && index == width) || index + current_char.width().unwrap_or(0) > width {
splitted.push(row.drain(..).collect());
index = 0;
}
splitted

row.push(current_char);
index += current_char.width().unwrap_or(0);
}
// leftover
if !row.is_empty() {
splitted.push(row.drain(..).collect());
}
splitted
}