diff --git a/client/src/args.rs b/client/src/args.rs index 6fe7573..d7b020b 100644 --- a/client/src/args.rs +++ b/client/src/args.rs @@ -2,11 +2,12 @@ use structopt::StructOpt; #[derive(StructOpt)] pub enum Opts { - Chat, + Chat(ChatOpts), } -impl Opts { - pub fn get() -> Self { - Self::from_args() - } +#[derive(StructOpt)] +pub struct ChatOpts { + /// whether or not to give up on connecting to the daemon after 10 ms + #[structopt(long)] + pub no_timeout: bool, } diff --git a/client/src/fifo.rs b/client/src/fifo.rs new file mode 100644 index 0000000..39eb88f --- /dev/null +++ b/client/src/fifo.rs @@ -0,0 +1,58 @@ +use crate::{args::ChatOpts, State}; +use anyhow::{bail, Context, Result}; +use common::client_daemon::{ClientToDaemonMsg, DaemonToClientMsg}; +use std::{fs::File, io::Read, panic, thread, time::Duration}; + +impl<'a> State<'a> { + /// try to open the two fifo files in their respective modes. timeout after 10 ms, unless the + /// `--no-timeout` option was provided. + pub(crate) fn try_get_fifos(opts: &ChatOpts) -> Result<(File, File)> { + let thread = thread::spawn(|| { + let home_dir = dirs::home_dir().context("couldn't get home directory")?; + + let ctodbuf = File::options() + .write(true) + .open(home_dir.join(".rclc/ctodbuf")) + .context("couldn't open client->daemon fifo")?; + + let dtocbuf = File::open(home_dir.join(".rclc/dtocbuf")) + .context("couldn't open daemon->client fifo")?; + + Ok((ctodbuf, dtocbuf)) + }); + + if !opts.no_timeout { + thread::sleep(Duration::from_millis(10)); + } + + if opts.no_timeout || thread.is_finished() { + match thread.join() { + Ok(o) => o, + Err(e) => panic::resume_unwind(e), + } + } else { + bail!("daemon is not running (10 ms timeout reached)"); + } + } + + pub(crate) fn send_fifo_msg(&mut self, msg: &ClientToDaemonMsg) -> Result<()> { + rmp_serde::encode::write(&mut self.ctodbuf, &msg) + .with_context(|| format!("couldn't write message {msg:?} to fifo")) + } + + pub(crate) fn handle_fifo_msg(&mut self) -> Result<()> { + let mut data = Vec::new(); + self.dtocbuf + .read_to_end(&mut data) + .context("couldn't read from fifo")?; + match rmp_serde::from_slice::(&data) { + Ok(msg) => print!("got fifo msg: {msg:?}\n\r"), + Err(e) => self.print_upwards(|| { + bunt::print!("{$red}invalid message from daemon: {}{/$}\n\r", e); + Ok(()) + })?, + } + + Ok(()) + } +} diff --git a/client/src/main.rs b/client/src/main.rs index ca23c30..2648d3d 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -1,23 +1,19 @@ use anyhow::{bail, Context, Result}; -use args::Opts; -use common::client_daemon::{ClientToDaemonMsg, DaemonToClientMsg}; -use crossterm::{ - cursor, - event::{self, Event, KeyCode, KeyEvent, KeyModifiers}, - terminal::{self, ClearType}, - tty::IsTty, - ExecutableCommand, QueueableCommand, -}; +use args::{ChatOpts, Opts}; +use crossterm::{event, terminal, tty::IsTty}; use mio::{unix::SourceFd, Events, Interest, Poll, Token}; use std::{ fs::File, - io::{self, Read, StdoutLock, Write}, + io::{self, StdoutLock, Write}, ops::ControlFlow, os::unix::prelude::AsRawFd, time::Duration, }; +use structopt::StructOpt; mod args; +mod fifo; +mod tui; const PROMPT: &str = "> "; @@ -35,41 +31,15 @@ struct State<'a> { } impl<'a> State<'a> { - fn new() -> Result { - let mut stdout = io::stdout().lock(); + fn new(opts: ChatOpts) -> Result { + let stdout = io::stdout().lock(); if !stdout.is_tty() { bail!("stdout is not a tty"); } terminal::enable_raw_mode().context("couldn't enable raw mode")?; - let home_dir = dirs::home_dir().context("couldn't get home directory")?; - - print!("waiting for daemon connection..."); - stdout.flush()?; - // aquire a write handle on the ctod fifo - // this blocks the client until the daemon opens the file to read - let ctodbuf = File::options() - .write(true) - .open(home_dir.join(".rclc/ctodbuf")) - .context("couldn't open client->daemon fifo")?; - - // clear waiting message - stdout - .queue(terminal::Clear(ClearType::CurrentLine))? - .execute(cursor::MoveToColumn(0))?; - - print!("waiting for daemon response..."); - stdout.flush()?; - // aquire a read handle on the dtoc file - // this blocks until the daemon opens the file to write - let dtocbuf = File::open(home_dir.join(".rclc/dtocbuf")) - .context("couldn't open daemon->client fifo")?; - - // clear waiting message - stdout - .queue(terminal::Clear(ClearType::CurrentLine))? - .execute(cursor::MoveToColumn(0))?; + let (ctodbuf, dtocbuf) = Self::try_get_fifos(&opts)?; Ok(Self { stdout, @@ -134,116 +104,24 @@ impl<'a> State<'a> { Ok(()) } - - /// clear the prompt line, perform a function (function is assumed to print something - /// followed by `"\n\r"`), then restore the prompt line. stdout is not flushed. - fn print_upwards(&mut self, f: F) -> Result<()> - where - F: Fn() -> Result<()>, - { - self.stdout - .queue(terminal::Clear(ClearType::CurrentLine))? - .queue(cursor::MoveToColumn(0))?; - f()?; - print!("{}{}", PROMPT, self.input); - Ok(()) - } - - fn send_fifo_msg(&mut self, msg: &ClientToDaemonMsg) -> Result<()> { - rmp_serde::encode::write(&mut self.ctodbuf, &msg) - .with_context(|| format!("couldn't write message {msg:?} to fifo")) - } - - fn handle_fifo_msg(&mut self) -> Result<()> { - let mut data = Vec::new(); - self.dtocbuf - .read_to_end(&mut data) - .context("couldn't read from fifo")?; - match rmp_serde::from_slice::(&data) { - Ok(msg) => print!("got fifo msg: {msg:?}\n\r"), - Err(e) => self.print_upwards(|| { - bunt::print!("{$red}invalid message from daemon: {}{/$}\n\r", e); - Ok(()) - })?, - } - - Ok(()) - } - - fn handle_term_evt(&mut self) -> Result> { - let event = event::read().context("couldn't read next terminal event")?; - match event { - Event::Key(kev) => self.handle_kev(kev), - _ => Ok(ControlFlow::Continue(())), - } - } - - fn handle_kev(&mut self, kev: KeyEvent) -> Result> { - match kev.code { - KeyCode::Char('c') if kev.modifiers == KeyModifiers::CONTROL => { - // notify the event loop to break - return Ok(ControlFlow::Break(())); - } - KeyCode::Backspace => { - if !self.input.is_empty() { - self.input.pop(); - print!("\x08 \x08"); - } - } - KeyCode::Enter => { - if self.input.is_empty() { - return Ok(ControlFlow::Continue(())); - } - - self.send_fifo_msg(&ClientToDaemonMsg::Send(self.input.clone()))?; - self.input.clear(); - self.stdout - .queue(terminal::Clear(ClearType::CurrentLine))? - .queue(cursor::MoveToColumn(0))?; - print!("{}", PROMPT); - } - KeyCode::Char(c) => { - self.input.push(c); - print!("{c}"); - } - _ => (), - } - - Ok(ControlFlow::Continue(())) - } } -fn chat() -> Result<()> { - let mut state = State::new()?; +fn chat(opts: ChatOpts) -> Result<()> { + let mut state = State::new(opts)?; state.start()?; Ok(()) } -#[allow(unused_must_use)] -/// Try our best to clean up the terminal state; if too many errors happen, just print some newlines and call it good. -fn cleanup() { - let mut stdout = io::stdout().lock(); - if stdout.is_tty() { - stdout.execute(cursor::Show); - if terminal::disable_raw_mode().is_ok() { - print!("\n\n"); - } else { - print!("\n\r\n\r"); - } - stdout.execute(terminal::Clear(ClearType::CurrentLine)); - } -} - fn go() -> Result<()> { - match Opts::get() { - Opts::Chat => chat(), + match Opts::from_args() { + Opts::Chat(chatopts) => chat(chatopts), } } fn main() { if let Err(e) = go() { - cleanup(); + tui::cleanup(); println!("rclc client error: {e:?}"); } } diff --git a/client/src/tui.rs b/client/src/tui.rs new file mode 100644 index 0000000..a42cffb --- /dev/null +++ b/client/src/tui.rs @@ -0,0 +1,85 @@ +use std::{io, ops::ControlFlow}; + +use crate::{State, PROMPT}; +use anyhow::{Context, Result}; +use common::client_daemon::ClientToDaemonMsg; +use crossterm::{ + cursor, + event::{self, Event, KeyCode, KeyEvent, KeyModifiers}, + terminal::{self, ClearType}, + tty::IsTty, + ExecutableCommand, QueueableCommand, +}; + +impl<'a> State<'a> { + /// clear the prompt line, perform a function (function is assumed to print something + /// followed by `"\n\r"`), then restore the prompt line. stdout is not flushed. + pub(crate) fn print_upwards(&mut self, f: F) -> Result<()> + where + F: Fn() -> Result<()>, + { + self.stdout + .queue(terminal::Clear(ClearType::CurrentLine))? + .queue(cursor::MoveToColumn(0))?; + f()?; + print!("{}{}", PROMPT, self.input); + Ok(()) + } + + pub(crate) fn handle_term_evt(&mut self) -> Result> { + let event = event::read().context("couldn't read next terminal event")?; + match event { + Event::Key(kev) => self.handle_kev(kev), + _ => Ok(ControlFlow::Continue(())), + } + } + + pub(crate) fn handle_kev(&mut self, kev: KeyEvent) -> Result> { + match kev.code { + KeyCode::Char('c') if kev.modifiers == KeyModifiers::CONTROL => { + // notify the event loop to break + return Ok(ControlFlow::Break(())); + } + KeyCode::Backspace => { + if !self.input.is_empty() { + self.input.pop(); + print!("\x08 \x08"); + } + } + KeyCode::Enter => { + if self.input.is_empty() { + return Ok(ControlFlow::Continue(())); + } + + self.send_fifo_msg(&ClientToDaemonMsg::Send(self.input.clone()))?; + self.input.clear(); + self.stdout + .queue(terminal::Clear(ClearType::CurrentLine))? + .queue(cursor::MoveToColumn(0))?; + print!("{}", PROMPT); + } + KeyCode::Char(c) => { + self.input.push(c); + print!("{c}"); + } + _ => (), + } + + Ok(ControlFlow::Continue(())) + } +} + +#[allow(unused_must_use)] +/// Try our best to clean up the terminal state; if too many errors happen, just print some newlines and call it good. +pub fn cleanup() { + let mut stdout = io::stdout().lock(); + if stdout.is_tty() { + stdout.execute(cursor::Show); + if terminal::disable_raw_mode().is_ok() { + print!("\n\n"); + } else { + print!("\n\r\n\r"); + } + stdout.execute(terminal::Clear(ClearType::CurrentLine)); + } +}