After way too long, the first version that does at least something

This commit is contained in:
Gregor Reitzenstein 2020-09-29 13:22:34 +02:00
commit 0935bbcb1d
10 changed files with 456 additions and 0 deletions

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "schema"]
path = schema
url = ../Diflouroborane/schema

1
schema Submodule

@ -0,0 +1 @@
Subproject commit 3392b9ac25eba7225212a1220c2d2e9e2bb3ebd9

113
src/app.rs Normal file
View File

@ -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<F> {
Connecting(F),
Connected(Api)
}
/// Application state struct
pub struct Sute<S, F> {
// TODO: BE SMART. Inputs change the state, resize signals change the state, futures completing
// change the state.
pub state: Mutable<SuteState>,
statesig: MutableSignalCloned<SuteState>,
signal: S,
inputs: Inputs,
api: Option<ConnState<F>>,
}
impl<S: Unpin, F: Unpin> Sute<S, F> {
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<S: Signal<Item=(u16,u16)> + Unpin, F: Future<Output=Api> + Unpin> Signal for Sute<S, F> {
type Item = SuteState;
fn poll_change(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
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<String>,
}
impl SuteState {
pub fn new() -> Self {
Self {
active_win: Window::Main,
size: (80,20),
running: true,
tick: 0,
server: None
}
}
}

7
src/banner.rs Normal file
View File

@ -0,0 +1,7 @@
pub const BANNER: &str = "
. _ _
.... ... ... .||. .... / V \\
||. ' || || || .|...|| >-(_)-<
. '|.. || || || || \\_/ \\_/
|'..|' '|..'|. '|.' '|...' ̔-´
";

4
src/config.rs Normal file
View File

@ -0,0 +1,4 @@
#[derive(Clone, Debug)]
pub struct Config {
pub server: String,
}

45
src/input.rs Normal file
View File

@ -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<Key>,
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<Option<Self::Item>> {
Pin::new(&mut self.rx).poll_next(cx)
}
}

72
src/main.rs Normal file
View File

@ -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(())
}

119
src/schema.rs Normal file
View File

@ -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<A: AsyncToSocketAddrs>(addr: A) -> impl Future<Output=Api> {
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::<connection_capnp::message::Builder>().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<bool, io::Error> {
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<Output=()> {
let mut outer = capnp::message::Builder::new_default();
let mut builder = outer.init_root::<connection_capnp::message::Builder>()
.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<bool, io::Error> {
let message = capnp_futures::serialize::read_message(&mut self.stream, capnp::message::ReaderOptions::default()).await.unwrap().unwrap();
let m = message.get_root::<connection_capnp::message::Reader>().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);
}
}

47
src/ui/mod.rs Normal file
View File

@ -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<B: Backend>(f: &mut Frame<B>, 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<B: Backend>(f: &mut Frame<B>, app: &mut SuteState, layout_chunk: Rect) {
f.render_widget(Block::default()
.title("Header")
.borders(Borders::ALL), layout_chunk);
}
fn draw_main<B: Backend>(f: &mut Frame<B>, 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<B: Backend>(f: &mut Frame<B>, 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);
}

45
src/util.rs Normal file
View File

@ -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<Self, io::Error> {
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<Option<Self::Item>> {
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)
}
}
}
}
}