Skip to content

Instantly share code, notes, and snippets.

@qryxip
Last active April 9, 2017 11:22
Show Gist options
  • Save qryxip/bee0b77a41aa1a6c90adcc243bfa0f4d to your computer and use it in GitHub Desktop.
Save qryxip/bee0b77a41aa1a6c90adcc243bfa0f4d to your computer and use it in GitHub Desktop.
#!/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