commit 0935bbcb1d5d6b7938ba7542b250c6fda3d99ed5 Author: Gregor Reitzenstein Date: Tue Sep 29 13:22:34 2020 +0200 After way too long, the first version that does at least something diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..56119e8 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "schema"] + path = schema + url = ../Diflouroborane/schema diff --git a/schema b/schema new file mode 160000 index 0000000..3392b9a --- /dev/null +++ b/schema @@ -0,0 +1 @@ +Subproject commit 3392b9ac25eba7225212a1220c2d2e9e2bb3ebd9 diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..e274bb5 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,113 @@ +use std::pin::Pin; +use std::task::{Context, Poll}; + +use futures::prelude::*; +use futures_signals::signal::{Mutable, Signal, MutableSignalCloned}; + +use termion::event::Key; + +use crate::input::Inputs; +use crate::schema::Api; + +#[derive(PartialEq, Eq, Debug, Clone)] +pub enum Window { + Main, + Help, +} + +enum ConnState { + Connecting(F), + Connected(Api) +} + +/// Application state struct +pub struct Sute { + + // TODO: BE SMART. Inputs change the state, resize signals change the state, futures completing + // change the state. + + pub state: Mutable, + statesig: MutableSignalCloned, + signal: S, + inputs: Inputs, + api: Option>, +} + +impl Sute { + pub fn new(s: S, api: F) -> Self { + let inputs = Inputs::new(); + let state = Mutable::new(SuteState::new()); + + Self { + statesig: state.signal_cloned(), + state: state, + signal: s, + inputs: inputs, + api: Some(ConnState::Connecting(api)), + } + } + + fn handle_resize(&mut self, new_size: (u16,u16)) { + (self.state.lock_mut()).size = new_size; + } + + fn handle_input(&mut self, key: Key) { + // TODO modify signal implementation so we don't have to modify the state on all ticks. + let mut state = self.state.lock_mut(); + state.tick = state.tick + 1; + match key { + Key::Char('q') => state.running = false, + Key::Char('?') => state.active_win = Window::Help, + Key::Esc => { + if state.active_win == Window::Help { + state.active_win = Window::Main; + } + } + _ => {} + } + } +} + +impl + Unpin, F: Future + Unpin> Signal for Sute { + type Item = SuteState; + fn poll_change(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + if let Poll::Ready(Some(key)) = Pin::new(&mut self.inputs).poll_next(cx) { + self.handle_input(key); + } + if let Poll::Ready(Some(size)) = Pin::new(&mut self.signal).poll_change(cx) { + self.handle_resize(size); + } + if let Some(ConnState::Connecting(mut apif)) = self.api.take() { + if let Poll::Ready(api) = Pin::new(&mut apif).poll(cx) { + self.api = Some(ConnState::Connected(api)); + } else { + self.api = Some(ConnState::Connecting(apif)); + } + } + + // TODO chunk this? + + Pin::new(&mut self.statesig).poll_change(cx) + } +} + +#[derive(Debug, Clone)] +pub struct SuteState { + pub active_win: Window, + pub size: (u16,u16), + pub running: bool, + pub tick: usize, + pub server: Option, +} + +impl SuteState { + pub fn new() -> Self { + Self { + active_win: Window::Main, + size: (80,20), + running: true, + tick: 0, + server: None + } + } +} diff --git a/src/banner.rs b/src/banner.rs new file mode 100644 index 0000000..7ede1c1 --- /dev/null +++ b/src/banner.rs @@ -0,0 +1,7 @@ +pub const BANNER: &str = " + . _ _ + .... ... ... .||. .... / V \\ +||. ' || || || .|...|| >-(_)-< +. '|.. || || || || \\_/ \\_/ +|'..|' '|..'|. '|.' '|...' ̔-´ +"; diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..9b67e69 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,4 @@ +#[derive(Clone, Debug)] +pub struct Config { + pub server: String, +} diff --git a/src/input.rs b/src/input.rs new file mode 100644 index 0000000..49e23b5 --- /dev/null +++ b/src/input.rs @@ -0,0 +1,45 @@ +use std::io; +use std::thread; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use termion::event::Key; +use termion::input::TermRead; + +use futures::Stream; +use futures::channel::mpsc; +use futures::SinkExt; + +pub struct Inputs { + rx: mpsc::Receiver, + hndl: thread::JoinHandle<()>, +} + +impl Inputs { + pub fn new() -> Self { + let (mut tx, rx) = mpsc::channel(64); + + let hndl = thread::spawn(move || { + let stdin = io::stdin(); + let keys = stdin.keys(); + for key in keys { + if key.is_err() { + break; + } + if let Err(_) = smol::block_on(tx.send(key.unwrap())) { + break; // and thus stop the thread + } + } + }); + + Self { rx, hndl } + } +} + +impl Stream for Inputs { + type Item = Key; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + Pin::new(&mut self.rx).poll_next(cx) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..de13248 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,72 @@ +use std::io; +use std::sync::{Arc, Mutex}; + +use tui::backend::{Backend, TermionBackend}; +use tui::Terminal; +use termion::raw::IntoRawMode; +use termion::input::TermRead; +use termion::event::Key; + +use futures::StreamExt; +use futures_signals::signal::SignalExt; + +use clap::{App, Arg}; + +mod banner; +mod config; +mod app; +mod input; +mod util; +mod ui; +mod schema; + +use banner::BANNER; + +fn main() -> Result<(), io::Error> { + + let matches = App::new("sute 🌸") + .version(env!("CARGO_PKG_VERSION")) + .author(env!("CARGO_PKG_AUTHORS")) + .about(env!("CARGO_PKG_DESCRIPTION")) + .usage("Press `?` in the GUI to see keybindings") + .before_help(BANNER) + .arg(Arg::with_name("config") + .short("c") + .long("config") + .help("Specify configuration file path") + .takes_value(true)) + .arg(Arg::with_name("server") + .short("s") + .long("server") + .help("Connect to the specified address[:port] as server") + .takes_value(true)) + .get_matches(); + + let server = matches.value_of("server").unwrap_or("localhost"); + + let stdout = io::stdout().into_raw_mode()?; + let backend = TermionBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + terminal.hide_cursor()?; + + let resize = util::Resize::new()?; + let api = schema::Api::connect(server); + let app = app::Sute::new(resize, Box::pin(api)); + + let mut stream = app.to_stream(); + loop { + if let Some(mut state) = smol::block_on(stream.next()) { + if !state.running { + break; + } + + terminal.draw(|f| ui::draw_ui(f, &mut state))?; + } else { + break; + } + } + + terminal.show_cursor()?; + + Ok(()) +} diff --git a/src/schema.rs b/src/schema.rs new file mode 100644 index 0000000..7ab5dc3 --- /dev/null +++ b/src/schema.rs @@ -0,0 +1,119 @@ +use std::ffi::CStr; + +use futures::prelude::*; + +use smol::io; +use smol::net::{TcpStream, AsyncToSocketAddrs}; +use smol::LocalExecutor; +use smol::Task; + +use rsasl::SASL; + +use capnp_rpc::{twoparty, RpcSystem, rpc_twoparty_capnp}; + +mod auth_capnp { + include!(concat!(env!("OUT_DIR"), "/schema/auth_capnp.rs")); +} +mod connection_capnp { + include!(concat!(env!("OUT_DIR"), "/schema/connection_capnp.rs")); +} +mod api_capnp { + include!(concat!(env!("OUT_DIR"), "/schema/api_capnp.rs")); +} + +const PLAIN: *const libc::c_char = b"PLAIN" as *const u8 as *const libc::c_char; + +pub struct Api { + stream: TcpStream +} + +impl Api { + pub fn new(stream: TcpStream) -> Self { + Self { stream } + } + pub fn connect(addr: A) -> impl Future { + let f = async { + let mut stream = TcpStream::connect(addr).await.unwrap(); + println!("Doing a hecking connect!"); + + let mut api = Api::new(stream); + + api.handshake().await.unwrap(); + if api.authenticate().await.unwrap() { + println!("Authentication successful"); + } else { + println!("Authentication failed!"); + } + + api + }; + + f + } + async fn handshake(&mut self) -> Result<(), io::Error> { + let host = "localhost"; + let program = format!("{}-{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")); + let version = (0u32,1u32); + + let mut outer = capnp::message::Builder::new_default(); + let mut builder = outer.init_root::().init_greet(); + + builder.set_host(host); + builder.set_major(version.0); + builder.set_minor(version.1); + builder.set_program(program); + + capnp_futures::serialize::write_message(&mut self.stream, outer).await.unwrap(); + + println!("{}", program); + + Ok(()) + } + + /// Authenticate to the server. Returns true on success, false on error + async fn authenticate(&mut self) -> Result { + let mut sasl = SASL::new().unwrap(); + let plain = std::ffi::CString::new("PLAIN").unwrap(); + let mut sess = sasl.client_start(&plain).unwrap(); + sess.set_property(rsasl::Property::GSASL_AUTHID, b"testuser"); + sess.set_property(rsasl::Property::GSASL_PASSWORD, b"testpass"); + + if let rsasl::Step::Done(data) = sess.step(&[]).unwrap() { + self.send_authentication_request("PLAIN", Some(&data)).await; + } else { + println!("Sasl said moar data"); + } + + Ok(self.receive_challenge().await?) + } + + fn send_authentication_request(&mut self, mech: &str, init: Option<&[u8]>) -> impl Future { + let mut outer = capnp::message::Builder::new_default(); + let mut builder = outer.init_root::() + .init_auth() + .init_request(); + builder.set_mechanism(mech); + + if let Some(data) = init { + builder.init_initial_response().set_initial(data); + } + + let stream = self.stream.clone(); + capnp_futures::serialize::write_message(stream, outer).map(|r| r.unwrap()) + } + + async fn receive_challenge(&mut self) -> Result { + let message = capnp_futures::serialize::read_message(&mut self.stream, capnp::message::ReaderOptions::default()).await.unwrap().unwrap(); + let m = message.get_root::().unwrap(); + + if let Ok(connection_capnp::message::Which::Auth(Ok(r))) = m.which() { + if let Ok(auth_capnp::auth_message::Outcome(Ok(r))) = r.which() { + if let Ok(auth_capnp::outcome::Result::Successful) = r.get_result() { + return Ok(true); + } + } + } + + return Ok(false); + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs new file mode 100644 index 0000000..c1a86f2 --- /dev/null +++ b/src/ui/mod.rs @@ -0,0 +1,47 @@ +use tui::{ + backend::Backend, + layout::{Layout, Direction, Constraint, Rect}, + widgets::{Paragraph, Block, Borders}, + Frame, +}; + +use crate::app::SuteState; + +pub fn draw_ui(f: &mut Frame, app: &mut SuteState) { + let outer_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(6), + Constraint::Min(1), + Constraint::Length(3), + ].as_ref(), + ) + .split(f.size()); + + draw_header(f, app, outer_layout[0]); + draw_main(f, app, outer_layout[1]); + draw_command_line(f, app, outer_layout[2]); +} + +fn draw_header(f: &mut Frame, app: &mut SuteState, layout_chunk: Rect) { + f.render_widget(Block::default() + .title("Header") + .borders(Borders::ALL), layout_chunk); +} + +fn draw_main(f: &mut Frame, app: &mut SuteState, layout_chunk: Rect) { + let chunk = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(20), Constraint::Percentage(80)].as_ref()) + .split(layout_chunk); + + f.render_widget(Paragraph::new(app.server.as_ref().map(|s| s.as_str()).unwrap_or("Not connected")), chunk[0]); + f.render_widget(Paragraph::new("Main"), chunk[1]); +} + +fn draw_command_line(f: &mut Frame, app: &mut SuteState, layout_chunk: Rect) { + f.render_widget(Block::default() + .title("Command line") + .borders(Borders::ALL), layout_chunk); + f.render_widget(Paragraph::new(">"), layout_chunk); +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..9aa829e --- /dev/null +++ b/src/util.rs @@ -0,0 +1,45 @@ +use std::io; +use std::pin::Pin; + +use futures::prelude::*; +use futures::ready; +use futures::task::{Context, Poll}; +use futures_signals::signal::Signal; + + +pub struct Resize { + inner: smol::net::unix::UnixStream, + size: (u16, u16), +} + +impl Resize { + pub fn new() -> Result { + let size = termion::terminal_size()?; + let (a, inner) = smol::net::unix::UnixStream::pair()?; + signal_hook::pipe::register(signal_hook::SIGWINCH, a)?; + Ok(Self { inner, size }) + } +} + +impl Signal for Resize { + type Item = (u16,u16); + + fn poll_change(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + if let Err(e) = ready!(AsyncRead::poll_read(Pin::new(&mut self.inner), cx, &mut [0])) { + println!("Error in checking signals: {}", e); + + Poll::Ready(None) + } else { + match termion::terminal_size() { + Ok(s) => { + self.size = s; + Poll::Ready(Some(s)) + } + Err(e) => { + println!("Error in checking signals: {}", e); + Poll::Ready(None) + } + } + } + } +}