implemented notifications and messages
This commit is contained in:
parent
28a3812812
commit
695ac6daa2
12 changed files with 1023 additions and 315 deletions
166
harsh-client/src/commands.rs
Normal file
166
harsh-client/src/commands.rs
Normal file
|
@ -0,0 +1,166 @@
|
|||
use harsh_common::ClientRequest;
|
||||
|
||||
pub enum Command {
|
||||
Help,
|
||||
Request(ClientRequest),
|
||||
}
|
||||
|
||||
pub fn parse(input: &str) -> Option<Command> {
|
||||
let mut parts = smart_split(input).into_iter();
|
||||
let command = match parts.next()?.as_str() {
|
||||
"help" => return Some(Command::Help),
|
||||
"ping" => {
|
||||
let rest = parts.collect::<Box<[_]>>();
|
||||
let content = rest.join(" ");
|
||||
ClientRequest::new_ping(content)
|
||||
}
|
||||
"chanls" => ClientRequest::new_channel_list(),
|
||||
"chanadd" => {
|
||||
let name = parts.next()?;
|
||||
ClientRequest::new_channel_create(name)
|
||||
}
|
||||
"chandel" => {
|
||||
let id = parts.next()?.parse().ok()?;
|
||||
ClientRequest::new_channel_delete(id)
|
||||
}
|
||||
"changname" => {
|
||||
let id = parts.next()?.parse().ok()?;
|
||||
ClientRequest::new_channel_get_name(id)
|
||||
}
|
||||
"chansname" => {
|
||||
let id = parts.next()?.parse().ok()?;
|
||||
let name = parts.next()?;
|
||||
ClientRequest::new_channel_set_name(id, name)
|
||||
}
|
||||
"msgls" => {
|
||||
let channel_id = parts.next()?.parse().ok()?;
|
||||
ClientRequest::new_message_list(channel_id)
|
||||
}
|
||||
"msgadd" => {
|
||||
let channel_id = parts.next()?.parse().ok()?;
|
||||
let content = parts.next()?;
|
||||
ClientRequest::new_message_create(channel_id, content)
|
||||
}
|
||||
"msgdel" => {
|
||||
let channel_id = parts.next()?.parse().ok()?;
|
||||
let id = parts.next()?.parse().ok()?;
|
||||
ClientRequest::new_message_delete(channel_id, id)
|
||||
}
|
||||
"msggcont" => {
|
||||
let channel_id = parts.next()?.parse().ok()?;
|
||||
let id = parts.next()?.parse().ok()?;
|
||||
ClientRequest::new_message_get_content(channel_id, id)
|
||||
}
|
||||
"msgscont" => {
|
||||
let channel_id = parts.next()?.parse().ok()?;
|
||||
let id = parts.next()?.parse().ok()?;
|
||||
let content = parts.next()?;
|
||||
ClientRequest::new_message_set_content(channel_id, id, content)
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
Some(Command::Request(command))
|
||||
}
|
||||
|
||||
pub const CMDS: &'static [Description] = &[
|
||||
// all commands
|
||||
Description::new("help", &[], "returns a help message"),
|
||||
Description::new(
|
||||
"ping",
|
||||
&["content"],
|
||||
"sends a ping with the specified content",
|
||||
),
|
||||
Description::new("chanls", &[], "list channels"),
|
||||
Description::new("chanadd", &["name"], "creates a new channel"),
|
||||
Description::new("chandel", &["id"], "delete a channel by its id"),
|
||||
Description::new("changname", &["id"], "get a channel's name"),
|
||||
Description::new("chansname", &["id", "name"], "set a channel's name"),
|
||||
Description::new("msgls", &["channel_id"], "list messages"),
|
||||
Description::new("msgadd", &["channel_id", "content"], "create a message"),
|
||||
Description::new("msgdel", &["channel_id", "id"], "delete a message"),
|
||||
Description::new("msggcont", &["channel_id", "id"], "get a message's content"),
|
||||
Description::new(
|
||||
"msgscont",
|
||||
&["channel_id", "id", "content"],
|
||||
"set a message's content",
|
||||
),
|
||||
];
|
||||
|
||||
pub fn smart_split(input: &str) -> Vec<String> {
|
||||
let input = input.trim();
|
||||
let mut result = Vec::new();
|
||||
|
||||
let mut capturing = false;
|
||||
let mut ignoring = false;
|
||||
let mut current = String::new();
|
||||
for char in input.chars() {
|
||||
let char: char = char;
|
||||
if ignoring {
|
||||
current.push(char);
|
||||
ignoring = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
match char {
|
||||
'\\' => {
|
||||
ignoring = true;
|
||||
}
|
||||
'"' => {
|
||||
capturing = !capturing;
|
||||
}
|
||||
' ' if !capturing => {
|
||||
result.push(current);
|
||||
current = String::new();
|
||||
}
|
||||
_ => current.push(char),
|
||||
}
|
||||
}
|
||||
result.push(current);
|
||||
result
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_smart_split() {
|
||||
assert_eq!(
|
||||
smart_split("hello world"),
|
||||
vec!["hello".to_string(), "world".to_string()]
|
||||
);
|
||||
assert_eq!(
|
||||
smart_split(r#""lorem ipsum" "dolor amit""#),
|
||||
vec!["lorem ipsum".to_string(), "dolor amit".to_string()]
|
||||
);
|
||||
assert_eq!(
|
||||
smart_split(r#"lorem "ipsum do"lor "amit""#),
|
||||
vec![
|
||||
"lorem".to_string(),
|
||||
"ipsum dolor".to_string(),
|
||||
"amit".to_string()
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
pub struct Description {
|
||||
name: &'static str,
|
||||
params: &'static [&'static str],
|
||||
desc: &'static str,
|
||||
}
|
||||
|
||||
impl Description {
|
||||
pub const fn new(
|
||||
name: &'static str,
|
||||
params: &'static [&'static str],
|
||||
desc: &'static str,
|
||||
) -> Self {
|
||||
Self { name, desc, params }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn help() {
|
||||
for &Description { name, params, desc } in CMDS {
|
||||
let mut usage = params.iter().map(|s| s.to_string()).collect::<Vec<_>>();
|
||||
usage.insert(0, name.to_string());
|
||||
let usage = usage.join(" ");
|
||||
println!("{name}:\n\tusage:\t\t{usage}\n\tdescription:\t{desc}");
|
||||
}
|
||||
}
|
|
@ -1,25 +1,38 @@
|
|||
use std::time::Duration;
|
||||
use std::{
|
||||
io::{stdout, Write},
|
||||
process::exit,
|
||||
};
|
||||
|
||||
use harsh_common::ServerRequest;
|
||||
use tokio::{
|
||||
io::{stdin, AsyncBufReadExt, AsyncWriteExt, BufReader},
|
||||
net::TcpStream,
|
||||
time,
|
||||
};
|
||||
|
||||
const ADDRESS: &'static str = "localhost:42069";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
println!("starting client ...");
|
||||
let stream = TcpStream::connect("localhost:8080").await.unwrap();
|
||||
println!("connected to 'localhost:8080'");
|
||||
println!("[main/info] starting client ...");
|
||||
let stream = TcpStream::connect(ADDRESS).await.unwrap();
|
||||
println!("[main/info] connected to '{ADDRESS}'");
|
||||
let (reader, writer) = stream.into_split();
|
||||
tokio::spawn(async {
|
||||
let mut reader = BufReader::new(reader);
|
||||
loop {
|
||||
let mut line = String::new();
|
||||
reader.read_line(&mut line).await.unwrap();
|
||||
println!("received '{line}'");
|
||||
time::sleep(Duration::from_millis(100)).await;
|
||||
match reader.read_line(&mut line).await {
|
||||
Ok(0) => {
|
||||
break;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
if let Some(parsed) = ServerRequest::try_parse(&line) {
|
||||
println!("[main/info] received '{parsed:?}'");
|
||||
}
|
||||
}
|
||||
println!("[main/info] connection closed, goodbye.");
|
||||
exit(0);
|
||||
});
|
||||
|
||||
let input_loop = tokio::spawn(async {
|
||||
|
@ -27,14 +40,16 @@ async fn main() {
|
|||
let mut writer = writer;
|
||||
|
||||
loop {
|
||||
print!("$> ");
|
||||
stdout().lock().flush().unwrap();
|
||||
let mut line = String::new();
|
||||
input.read_line(&mut line).await.unwrap();
|
||||
let input = commands::parse(&line);
|
||||
match input {
|
||||
None => println!("failed to parse command"),
|
||||
None => println!("[main/warn] failed to parse command"),
|
||||
Some(commands::Command::Help) => commands::help(),
|
||||
Some(commands::Command::Request(cmd)) => {
|
||||
println!("sending..");
|
||||
println!("[main/info] sending..");
|
||||
writer.write_all(cmd.serialize().as_bytes()).await.unwrap();
|
||||
writer.write_all(b"\n").await.unwrap();
|
||||
}
|
||||
|
@ -42,135 +57,8 @@ async fn main() {
|
|||
}
|
||||
});
|
||||
|
||||
println!("awaiting input ...");
|
||||
println!("[main/info] awaiting input ...");
|
||||
input_loop.await.unwrap();
|
||||
}
|
||||
|
||||
mod commands {
|
||||
use harsh_common::ClientRequest;
|
||||
|
||||
pub enum Command {
|
||||
Help,
|
||||
Request(ClientRequest),
|
||||
}
|
||||
|
||||
pub fn parse(input: &str) -> Option<Command> {
|
||||
let mut parts = smart_split(input).into_iter();
|
||||
let command = match parts.next()?.as_str() {
|
||||
"help" => return Some(Command::Help),
|
||||
"ping" => {
|
||||
let rest = parts.collect::<Box<[_]>>();
|
||||
let content = rest.join(" ");
|
||||
ClientRequest::new_ping(content)
|
||||
}
|
||||
"chanls" => ClientRequest::new_channel_list(),
|
||||
"chanadd" => {
|
||||
let name = parts.next()?;
|
||||
ClientRequest::new_channel_create(name)
|
||||
}
|
||||
"chandel" => {
|
||||
let id = parts.next()?.parse().ok()?;
|
||||
ClientRequest::new_channel_delete(id)
|
||||
}
|
||||
"changetname" => {
|
||||
let id = parts.next()?.parse().ok()?;
|
||||
ClientRequest::new_channel_get_name(id)
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
Some(Command::Request(command))
|
||||
}
|
||||
|
||||
pub const CMDS: &'static [Description] = &[
|
||||
// all commands
|
||||
Description::new("help", &[], "returns a help message"),
|
||||
Description::new(
|
||||
"ping",
|
||||
&["content"],
|
||||
"sends a ping with the specified content",
|
||||
),
|
||||
Description::new("chanls", &[], "list channels"),
|
||||
Description::new("chanadd", &["name"], "creates a new channel"),
|
||||
Description::new("chandel", &["id"], "delete a channel by its id"),
|
||||
Description::new("changetname", &["id"], "get a channel's name"),
|
||||
];
|
||||
|
||||
pub fn smart_split(input: &str) -> Vec<String> {
|
||||
let input = input.trim();
|
||||
let mut result = Vec::new();
|
||||
|
||||
let mut capturing = false;
|
||||
let mut ignoring = false;
|
||||
let mut current = String::new();
|
||||
for char in input.chars() {
|
||||
let char: char = char;
|
||||
if ignoring {
|
||||
current.push(char);
|
||||
ignoring = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
match char {
|
||||
'\\' => {
|
||||
ignoring = true;
|
||||
}
|
||||
'"' => {
|
||||
capturing = !capturing;
|
||||
}
|
||||
' ' if !capturing => {
|
||||
result.push(current);
|
||||
current = String::new();
|
||||
}
|
||||
_ => current.push(char),
|
||||
}
|
||||
}
|
||||
result.push(current);
|
||||
result
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_smart_split() {
|
||||
assert_eq!(
|
||||
smart_split("hello world"),
|
||||
vec!["hello".to_string(), "world".to_string()]
|
||||
);
|
||||
assert_eq!(
|
||||
smart_split(r#""lorem ipsum" "dolor amit""#),
|
||||
vec!["lorem ipsum".to_string(), "dolor amit".to_string()]
|
||||
);
|
||||
assert_eq!(
|
||||
smart_split(r#"lorem "ipsum do"lor "amit""#),
|
||||
vec![
|
||||
"lorem".to_string(),
|
||||
"ipsum dolor".to_string(),
|
||||
"amit".to_string()
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
pub struct Description {
|
||||
name: &'static str,
|
||||
params: &'static [&'static str],
|
||||
desc: &'static str,
|
||||
}
|
||||
|
||||
impl Description {
|
||||
pub const fn new(
|
||||
name: &'static str,
|
||||
params: &'static [&'static str],
|
||||
desc: &'static str,
|
||||
) -> Self {
|
||||
Self { name, desc, params }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn help() {
|
||||
for &Description { name, params, desc } in CMDS {
|
||||
let mut usage = params.iter().map(|s| s.to_string()).collect::<Vec<_>>();
|
||||
usage.insert(0, name.to_string());
|
||||
let usage = usage.join(" ");
|
||||
println!("{name}:\n\tusage:\n\t\t{usage}\n\n\tdescription:\n\t\t{desc}\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
mod commands;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue