Last active
April 9, 2017 11:22
-
-
Save qryxip/bee0b77a41aa1a6c90adcc243bfa0f4d to your computer and use it in GitHub Desktop.
Simple IRC client using https://github.com/aatxe/irc and https://github.com/gchp/rustbox .
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env run-cargo-script | |
//!```cargo | |
//![dependencies] | |
//!chrono = "0.3.0" | |
//!irc = "0.11.6" | |
//!lazy_static = "0.2.6" | |
//!regex = "0.2.1" | |
//!rustbox = "0.9.0" | |
//!``` | |
extern crate chrono; | |
extern crate irc; | |
extern crate regex; | |
extern crate rustbox; | |
#[macro_use] | |
extern crate lazy_static; | |
use std::cmp; | |
use std::collections::{vec_deque, HashMap, VecDeque}; | |
use std::default::Default; | |
use std::io; | |
use std::iter::FromIterator; | |
use std::str; | |
use std::sync::{Arc, Mutex}; | |
use std::thread; | |
use std::time::Duration; | |
use chrono::datetime::DateTime; | |
use chrono::offset::local::Local; | |
use irc::client::data::Config as IrcConfig; | |
use irc::client::data::command::Command as IrcCommand; | |
use irc::client::data::message::Message; | |
use irc::client::server::{IrcServer, Server}; | |
use irc::client::server::utils::ServerExt; | |
use regex::Regex; | |
use rustbox::{self as rb, Color, Event, RustBox, Style}; | |
fn main() { | |
let mut client = Client::new(); | |
let _ = client.run(); | |
} | |
lazy_static! { | |
static ref RE_NICK_PREFIX: Regex = Regex::new("^(.+)!(.+)$").unwrap(); | |
static ref RE_USERCMD_EXIT: Regex = Regex::new(r"^/(?i)(exit)\s*$").unwrap(); | |
static ref RE_USERCMD_QUIT: Regex = Regex::new(r"^/(?i)(quit)\s*$").unwrap(); | |
} | |
enum UserCommand { | |
SayInCurrentChannel(String), | |
IrcCommand(IrcCommand), | |
InvalidOperation(String), | |
TerminateApp, | |
Nothing, | |
} | |
struct Client { | |
rustbox: RustBox, | |
message_input: MessageInput, | |
message_log: Arc<Mutex<MessageLog>>, | |
} | |
impl Client { | |
fn new() -> Client { | |
Client { | |
rustbox: RustBox::init(Default::default()).unwrap(), | |
message_input: MessageInput::new(), | |
message_log: Arc::new(Mutex::new(MessageLog::new())), | |
} | |
} | |
fn run(&mut self) -> io::Result<()> { | |
let (channel_name, server) = self.login()?; | |
self.chat(channel_name, server) | |
} | |
fn login(&mut self) -> io::Result<(String, IrcServer)> { | |
let mut server_uri = None::<String>; | |
let mut channel = None::<String>; | |
let mut nickname = None::<String>; | |
while server_uri.is_none() || channel.is_none() || nickname.is_none() { | |
self.adjust_cursor_pos(); | |
self.rustbox.clear(); | |
self.draw_frames(); | |
self.draw_message_input(); | |
let mes1 = format!("server-uri : {}", | |
server_uri.clone().unwrap_or(format!("???"))); | |
let mes2 = format!("channel : {}", | |
channel.clone().unwrap_or(format!("???"))); | |
let mes3 = format!("nickname : {}", | |
nickname.clone().unwrap_or(format!("???"))); | |
self.rustbox.print_fromiter_default(0, 0, mes1.chars()); | |
self.rustbox.print_fromiter_default(0, 1, mes2.chars()); | |
self.rustbox.print_fromiter_default(0, 2, mes3.chars()); | |
self.rustbox.present(); | |
match self.rustbox.poll_event(false) { | |
Ok(Event::KeyEvent(key)) => { | |
let command = self.message_input.key_input(key); | |
match (command, &server_uri, &channel, &nickname) { | |
(UserCommand::TerminateApp, _, _, _) => panic!(), | |
(UserCommand::SayInCurrentChannel(s), &None, &None, &None) => { | |
server_uri = Some(s); | |
} | |
(UserCommand::SayInCurrentChannel(s), &Some(_), &None, &None) => { | |
channel = Some(s); | |
} | |
(UserCommand::SayInCurrentChannel(s), &Some(_), &Some(_), &None) => { | |
nickname = Some(s); | |
} | |
(UserCommand::SayInCurrentChannel(_), _, _, _) => panic!(), | |
_ => {} | |
} | |
} | |
Err(e) => panic!("{}", e), | |
_ => {} | |
} | |
} | |
let channel_name = channel.clone().unwrap(); | |
let irc_config = IrcConfig { | |
server: server_uri, | |
channels: channel.map(|c| vec![c]), | |
nickname: nickname, | |
port: Some(6667), | |
..Default::default() | |
}; | |
let server = IrcServer::from_config(irc_config)?; | |
server.identify()?; | |
Ok((channel_name, server.clone())) | |
} | |
fn chat(&mut self, channel_name: String, server: IrcServer) -> io::Result<()> { | |
self.start_receiving_responses(server.clone(), channel_name.clone()); | |
loop { | |
self.rustbox.clear(); | |
self.adjust_cursor_pos(); | |
self.draw_frames(); | |
self.draw_message_input(); | |
self.draw_message_log(&channel_name); | |
self.rustbox.present(); | |
match self.rustbox.peek_event(Duration::from_millis(16), false) { | |
Ok(Event::KeyEvent(key)) => { | |
match self.message_input.key_input(key) { | |
UserCommand::TerminateApp => break, | |
UserCommand::SayInCurrentChannel(s) => { | |
let cmd = IrcCommand::PRIVMSG(channel_name.clone(), s.clone()); | |
server.send(Message::from(cmd.clone()))?; | |
let mut lock = self.message_log.lock().unwrap(); | |
(*lock).push(&channel_name, Message::from(cmd)); | |
} | |
UserCommand::IrcCommand(cmd) => server.send(Message::from(cmd))?, | |
_ => {} | |
} | |
} | |
Err(e) => panic!("{}", e), | |
_ => {} | |
} | |
} | |
Ok(()) | |
} | |
fn start_receiving_responses(&mut self, server: IrcServer, channel_name: String) { | |
// TODO: Parse responses from the server. | |
let message_log_clone = self.message_log.clone(); | |
let f = move || for m in server.iter() { | |
match m { | |
Ok(Message { | |
tags: _, | |
prefix: _, | |
command: IrcCommand::PING(s, ops), | |
}) => { | |
let cmd = IrcCommand::PONG(s, ops); | |
server.send(Message::from(cmd)).unwrap(); | |
} | |
Ok(m) => { | |
let mut lock = message_log_clone.lock().unwrap(); | |
(*lock).push(&channel_name, m); | |
} | |
Err(e) => panic!("{}", e), | |
} | |
}; | |
thread::spawn(f); | |
} | |
fn draw_frames(&mut self) { | |
let y = self.rustbox.height() - 2; | |
for i in 0..self.rustbox.width() + 1 { | |
self.rustbox.print_char_default(i, y, '─'); | |
} | |
} | |
fn draw_message_input(&mut self) { | |
let y = self.rustbox.height() - 1; | |
let left = self.message_input.chars_of_prompt(); | |
self.rustbox | |
.print_fromiter_color(0, y, Color::Green, left); | |
let x = self.message_input.prompt_rune_width(); | |
let right = self.message_input.char_refs_of_message().map(|p| *p); | |
self.rustbox.print_fromiter_default(x, y, right); | |
} | |
fn draw_message_log(&mut self, channel_name: &String) { | |
fn left_len(date: &DateTime<Local>, m: &Message) -> usize { | |
format!("{} ", date).rune_width() + | |
match m { | |
&Message { | |
tags: _, | |
prefix: Some(ref prefix), | |
command: IrcCommand::PRIVMSG(_, _), | |
} if RE_NICK_PREFIX.is_match(prefix) => { | |
let caps = RE_NICK_PREFIX.captures(prefix).unwrap(); | |
caps[1].rune_width() | |
} | |
&Message { | |
tags: _, | |
prefix: _, | |
command: IrcCommand::JOIN(_, _, _), | |
} | | |
&Message { | |
tags: _, | |
prefix: _, | |
command: IrcCommand::QUIT(_), | |
} => cmp::min("-->".len(), "<--".len()), | |
_ => "Response:".len(), | |
} | |
} | |
let mut lock = self.message_log.lock().unwrap(); | |
let v = (*lock).get(channel_name); | |
let h = cmp::min(self.rustbox.height() - 2, v.len()); | |
let w = v.iter() | |
.map(|p| left_len(&p.0, &p.1)) | |
.max() | |
.unwrap_or(0); | |
for y in v.len() - h..v.len() { | |
let left = format!("{}", v[y].0); | |
self.rustbox.print_message(y, w, left.as_str(), &v[y].1); | |
} | |
} | |
fn adjust_cursor_pos(&mut self) { | |
let x = self.message_input.cursor_x_on_screen() as isize; | |
let y = self.rustbox.width() as isize - 1; | |
self.rustbox.set_cursor(x, y); | |
} | |
} | |
struct MessageInput { | |
content: VecDeque<char>, | |
prompt: String, | |
cursor_index: usize, | |
} | |
impl MessageInput { | |
fn new() -> MessageInput { | |
MessageInput { | |
content: VecDeque::new(), | |
prompt: format!("> "), | |
cursor_index: 0, | |
} | |
} | |
fn prompt_rune_width(&self) -> usize { | |
self.prompt.rune_width() | |
} | |
fn chars_of_prompt(&self) -> str::Chars { | |
self.prompt.chars() | |
} | |
fn char_refs_of_message(&self) -> vec_deque::Iter<char> { | |
self.content.iter() | |
} | |
fn cursor_x_on_screen(&self) -> usize { | |
self.prompt.rune_width() + | |
(0..self.cursor_index) | |
.map(|i| self.content[i].rune_width()) | |
.fold(0, |a, b| a + b) | |
} | |
fn set_prompt(&mut self, prompt: String) { | |
self.prompt = prompt; | |
} | |
fn key_input(&mut self, key: rb::Key) -> UserCommand { | |
use rb::Key::{Backspace, Char, Ctrl, Delete, End, Enter, Esc, Home, Left, Right}; | |
match key { | |
Ctrl('c') | Ctrl('z') | Esc => UserCommand::TerminateApp, | |
Ctrl('j') | Ctrl('m') | Enter => self.submit(), | |
Ctrl('h') | Backspace => self.delete_backward_char(), | |
Ctrl('d') | Delete => self.delete_forward_char(), | |
Ctrl('b') | Left => self.move_cursor_left(), | |
Ctrl('f') | Right => self.move_cursor_right(), | |
Ctrl('a') | Home => self.move_beginning_of_line(), | |
Ctrl('e') | End => self.move_end_of_line(), | |
Ctrl('w') => self.delete_backward_word(), | |
Ctrl('u') => self.kill_line_backward(), | |
Ctrl('k') => self.kill_line_forward(), | |
Char(c) => self.insert_char(c), | |
_ => UserCommand::Nothing, | |
} | |
} | |
fn submit(&mut self) -> UserCommand { | |
// TODO: Enable more commands. | |
let text = String::from_iter(self.content.iter().map(|p| *p)); | |
self.content.clear(); | |
self.cursor_index = 0; | |
if RE_USERCMD_EXIT.is_match(text.as_str()) || RE_USERCMD_QUIT.is_match(text.as_str()) { | |
UserCommand::TerminateApp | |
} else if text.is_empty() { | |
UserCommand::Nothing | |
} else if text.chars().nth(0) == Some('/') && text.len() > 1 { | |
UserCommand::InvalidOperation(format!("Error: unknown command \"{}\"", text)) | |
} else { | |
UserCommand::SayInCurrentChannel(text) | |
} | |
} | |
fn insert_char(&mut self, c: char) -> UserCommand { | |
self.content.insert(self.cursor_index, c); | |
self.cursor_index += 1; | |
UserCommand::Nothing | |
} | |
fn move_cursor_left(&mut self) -> UserCommand { | |
if self.cursor_index > 0 { | |
self.cursor_index -= 1; | |
} | |
UserCommand::Nothing | |
} | |
fn move_cursor_right(&mut self) -> UserCommand { | |
if self.cursor_index < self.content.len() { | |
self.cursor_index += 1; | |
} | |
UserCommand::Nothing | |
} | |
fn move_beginning_of_line(&mut self) -> UserCommand { | |
self.cursor_index = 0; | |
UserCommand::Nothing | |
} | |
fn move_end_of_line(&mut self) -> UserCommand { | |
self.cursor_index = self.content.len(); | |
UserCommand::Nothing | |
} | |
fn delete_backward_char(&mut self) -> UserCommand { | |
if self.cursor_index > 0 { | |
self.content.remove(self.cursor_index - 1); | |
self.move_cursor_left(); | |
} | |
UserCommand::Nothing | |
} | |
fn delete_forward_char(&mut self) -> UserCommand { | |
if self.cursor_index < self.content.len() { | |
self.content.remove(self.cursor_index); | |
} | |
UserCommand::Nothing | |
} | |
fn delete_backward_word(&mut self) -> UserCommand { | |
let mut num_chars_to_delete = 0; | |
while self.cursor_index - num_chars_to_delete > 1 && | |
self.content[self.cursor_index - num_chars_to_delete - 1] == ' ' { | |
num_chars_to_delete += 1; | |
} | |
while self.cursor_index - num_chars_to_delete > 1 && | |
self.content[self.cursor_index - num_chars_to_delete - 1] != ' ' { | |
num_chars_to_delete += 1; | |
} | |
if self.cursor_index - num_chars_to_delete == 1 { | |
num_chars_to_delete += 1; | |
} | |
self.content | |
.drain(self.cursor_index - num_chars_to_delete..self.cursor_index); | |
self.cursor_index -= num_chars_to_delete; | |
UserCommand::Nothing | |
} | |
fn kill_line_backward(&mut self) -> UserCommand { | |
for _ in 0..self.cursor_index + 1 { | |
self.content.pop_front(); | |
} | |
self.cursor_index = 0; | |
UserCommand::Nothing | |
} | |
fn kill_line_forward(&mut self) -> UserCommand { | |
for _ in 0..self.content.len() - self.cursor_index { | |
self.content.pop_back(); | |
} | |
UserCommand::Nothing | |
} | |
} | |
struct MessageLog { | |
content: HashMap<String, Vec<(DateTime<Local>, Message)>>, | |
} | |
impl MessageLog { | |
fn new() -> MessageLog { | |
MessageLog { content: HashMap::new() } | |
} | |
fn get(&mut self, channel_name: &String) -> &Vec<(DateTime<Local>, Message)> { | |
if self.content.get(channel_name).is_none() { | |
self.content.insert(channel_name.clone(), Vec::new()); | |
} | |
self.content.get(channel_name).unwrap() | |
} | |
fn push(&mut self, channel_name: &String, message: Message) { | |
if self.content.get(channel_name).is_none() { | |
self.content.insert(channel_name.clone(), Vec::new()); | |
} | |
let mut v = self.content.get_mut(channel_name).unwrap(); | |
v.push((Local::now(), message)); | |
} | |
} | |
trait PrintIrcMessage { | |
fn print_message(&mut self, y: usize, left_width: usize, time: &str, message: &Message); | |
} | |
impl PrintIrcMessage for RustBox { | |
fn print_message(&mut self, y: usize, left_width: usize, time: &str, message: &Message) { | |
static SEP: &'static str = " | "; | |
self.print_default(0, y, time); | |
self.print_color(left_width, y, Color::Green, SEP); | |
match message { | |
&Message { | |
tags: _, | |
prefix: Some(ref prefix), | |
command: IrcCommand::PRIVMSG(_, ref s), | |
} if RE_NICK_PREFIX.is_match(prefix.as_str()) => { | |
let nickname = &RE_NICK_PREFIX.captures(prefix.as_str()).unwrap()[1]; | |
let w = nickname.rune_width(); | |
self.print_fromiter_color(left_width - w, y, Color::Green, nickname.chars()); | |
self.print_fromiter_default(left_width + SEP.len(), y, s.chars()); | |
} | |
&Message { | |
tags: _, | |
prefix: None, | |
command: IrcCommand::PRIVMSG(_, ref s), | |
} => { | |
self.print_default(left_width - 4, y, "YOU:"); | |
self.print_fromiter_default(left_width + SEP.len(), y, s.chars()); | |
} | |
&Message { | |
tags: _, | |
prefix: Some(ref prefix), | |
command: IrcCommand::QUIT(_), | |
} if RE_NICK_PREFIX.is_match(prefix.as_str()) => { | |
let caps = RE_NICK_PREFIX.captures(prefix.as_str()).unwrap(); | |
self.print_color(left_width - 3, y, Color::Red, "<--"); | |
let x = left_width + SEP.len(); | |
self.print_fromiter_color(x, y, Color::Magenta, caps[1].chars()); | |
let x = x + caps[1].rune_width() + 1; | |
self.print_char_color(x, y, Color::Green, '('); | |
let x = x + 1; | |
self.print_fromiter_color(x, y, Color::Cyan, caps[2].chars()); | |
let x = x + caps[2].rune_width(); | |
self.print_char_color(x, y, Color::Green, ')'); | |
let x = x + 1; | |
self.print_color(x, y, Color::Red, " has exit"); | |
} | |
&Message { | |
tags: _, | |
prefix: Some(ref prefix), | |
command: IrcCommand::JOIN(ref ch, _, _), | |
} if RE_NICK_PREFIX.is_match(prefix.as_str()) => { | |
let caps = RE_NICK_PREFIX.captures(prefix.as_str()).unwrap(); | |
self.print_color(left_width - 3, y, Color::Green, "-->"); | |
let x = left_width + SEP.len(); | |
self.print_fromiter_color(x, y, Color::Magenta, caps[1].chars()); | |
let x = x + caps[1].rune_width() + 1; | |
self.print_char_color(x, y, Color::Green, '('); | |
let x = x + 1; | |
self.print_fromiter_color(x, y, Color::Cyan, caps[2].chars()); | |
let x = x + caps[2].rune_width(); | |
self.print_char_color(x, y, Color::Green, ')'); | |
let x = x + 1; | |
self.print_color(x, y, Color::Green, " has joined "); | |
let x = x + " has joined ".len(); | |
self.print_fromiter_default(x, y, ch.chars()); | |
} | |
&Message { | |
ref tags, | |
ref prefix, | |
ref command, | |
} => { | |
let right = format!("{{{:?}, {:?}, {:?}}}", tags, prefix, command); | |
self.print_default(left_width - 9, y, "Response:"); | |
self.print_fromiter_default(left_width + SEP.len(), y, right.chars()); | |
} | |
} | |
} | |
} | |
trait PrintUtf8 { | |
fn print_fromiter<I>(&mut self, x: usize, y: usize, sty: Style, fg: Color, bg: Color, it: I) | |
where I: Iterator<Item = char>; | |
} | |
impl PrintUtf8 for RustBox { | |
fn print_fromiter<I>(&mut self, x: usize, y: usize, sty: Style, fg: Color, bg: Color, it: I) | |
where I: Iterator<Item = char> | |
{ | |
let mut i = x; | |
for c in it { | |
self.print_char(i, y, sty, fg, bg, c); | |
i += c.rune_width(); | |
} | |
} | |
} | |
trait PrintDefault { | |
fn print_default(&mut self, x: usize, y: usize, s: &str); | |
fn print_char_default(&mut self, x: usize, y: usize, ch: char); | |
fn print_fromiter_default<I: Iterator<Item = char>>(&mut self, x: usize, y: usize, it: I); | |
fn print_color(&mut self, x: usize, y: usize, fg: Color, s: &str); | |
fn print_char_color(&mut self, x: usize, y: usize, fg: Color, ch: char); | |
fn print_fromiter_color<I>(&mut self, x: usize, y: usize, fg: Color, it: I) | |
where I: Iterator<Item = char>; | |
} | |
impl PrintDefault for RustBox { | |
fn print_default(&mut self, x: usize, y: usize, s: &str) { | |
self.print(x, y, rb::RB_NORMAL, Color::Default, Color::Default, s); | |
} | |
fn print_char_default(&mut self, x: usize, y: usize, ch: char) { | |
self.print_char(x, y, rb::RB_NORMAL, Color::Default, Color::Default, ch); | |
} | |
fn print_fromiter_default<I: Iterator<Item = char>>(&mut self, x: usize, y: usize, it: I) { | |
self.print_fromiter(x, y, rb::RB_NORMAL, Color::Default, Color::Default, it); | |
} | |
fn print_color(&mut self, x: usize, y: usize, fg: Color, s: &str) { | |
self.print(x, y, rb::RB_NORMAL, fg, Color::Default, s); | |
} | |
fn print_char_color(&mut self, x: usize, y: usize, fg: Color, ch: char) { | |
self.print_char(x, y, rb::RB_NORMAL, fg, Color::Default, ch); | |
} | |
fn print_fromiter_color<I>(&mut self, x: usize, y: usize, fg: Color, it: I) | |
where I: Iterator<Item = char> | |
{ | |
self.print_fromiter(x, y, rb::RB_NORMAL, fg, Color::Default, it); | |
} | |
} | |
trait RuneWidth { | |
fn rune_width(&self) -> usize; | |
} | |
impl RuneWidth for char { | |
fn rune_width(&self) -> usize { | |
// TODO: ambiguous-width characters | |
let r = *self as u32; | |
if r >= 0x1100 && | |
(r <= 0x115f || r == 0x2329 || r == 0x232a || | |
(r >= 0x2e80 && r <= 0xa4cf && r != 0x303f) || | |
(r >= 0xac00 && r <= 0xd7a3) || (r >= 0xf900 && r <= 0xfaff) || | |
(r >= 0xfe30 && r <= 0xfe6f) || (r >= 0xff00 && r <= 0xff60) || | |
(r >= 0xffe0 && r <= 0xffe6) || | |
(r >= 0x20000 && r <= 0x2fffd) || | |
(r >= 0x30000 && r <= 0x3fffd)) { | |
2 | |
} else { | |
1 | |
} | |
} | |
} | |
impl RuneWidth for str { | |
fn rune_width(&self) -> usize { | |
self.chars() | |
.map(|c| c.rune_width()) | |
.fold(0, |a, b| a + b) | |
} | |
} | |
impl RuneWidth for String { | |
fn rune_width(&self) -> usize { | |
self.as_str().rune_width() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment