🗝
summary refs log tree commit diff
path: root/src/tui/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/tui/mod.rs')
-rw-r--r--src/tui/mod.rs159
1 files changed, 159 insertions, 0 deletions
diff --git a/src/tui/mod.rs b/src/tui/mod.rs
new file mode 100644
index 0000000..7c748c1
--- /dev/null
+++ b/src/tui/mod.rs
@@ -0,0 +1,159 @@
+mod accounts;
+
+use std::{
+    io::{Read, Write},
+    os::unix::net::UnixStream,
+};
+
+use cursive::{
+    event::Key,
+    theme::{BorderStyle, Palette, Theme},
+    utils::markup::StyledString,
+    view::Margins,
+    views::{Dialog, PaddedView, Panel, ScrollView, SelectView, TextView},
+    Cursive, CursiveExt,
+};
+use eyre::ContextCompat;
+
+use crate::ipc::{CreateInviteRequest, IPCResponse, RequestDefinition, ResponseDefinition};
+
+pub fn run() -> eyre::Result<()> {
+    let Some(socket_path) = std::env::args().skip(2).next() else {
+        eyre::bail!("must specify socket path");
+    };
+    let stream = UnixStream::connect(socket_path)?;
+    let ipc = Ipc(stream);
+
+    let mut siv = Cursive::new();
+    siv.set_user_data(ipc);
+
+    siv.set_theme(Theme {
+        shadow: false,
+        borders: BorderStyle::Simple,
+        palette: Palette::terminal_default(),
+    });
+
+    enum SelectItem {
+        Accounts,
+        Invite,
+    }
+    siv.add_layer(
+        SelectView::new()
+            .item("accounts", SelectItem::Accounts)
+            .item("invite", SelectItem::Invite)
+            .on_submit(move |siv, item| match item {
+                SelectItem::Accounts => accounts::show(siv),
+                SelectItem::Invite => invite(siv),
+            })
+            .float("dissociate"),
+    );
+
+    siv.add_global_callback(Key::Esc, |siv| {
+        if siv.pop_layer().is_none() || siv.screen().is_empty() {
+            siv.quit();
+        }
+    });
+
+    siv.run();
+
+    Ok(())
+}
+
+fn invite(siv: &mut Cursive) {
+    if let Some(invite) = siv.ipc(CreateInviteRequest {}) {
+        siv.add_layer(
+            Dialog::around(invite.link.text_view().hpad())
+                .button("ok", |siv| {
+                    siv.pop_layer();
+                })
+                .title("invite")
+                .pad(),
+        );
+    }
+}
+
+struct Ipc(UnixStream);
+
+impl Ipc {
+    fn exec<T: RequestDefinition>(
+        &mut self,
+        request: T,
+    ) -> eyre::Result<Result<T::Response, <T::Response as ResponseDefinition>::Error>> {
+        let request_buffer =
+            bincode::encode_to_vec(request.into_request(), bincode::config::standard())?;
+        self.0
+            .write_all(&(request_buffer.len() as u16).to_be_bytes())?;
+        self.0.write_all(&request_buffer)?;
+        let mut response_len_buffer = [0u8; 2];
+        self.0.read_exact(&mut response_len_buffer)?;
+        let response_len = u16::from_be_bytes(response_len_buffer) as usize;
+        let mut response_buffer = vec![0u8; response_len];
+        self.0.read_exact(&mut response_buffer)?;
+        let (response, _): (IPCResponse, _) =
+            bincode::decode_from_slice(&response_buffer, bincode::config::standard())?;
+        T::Response::from_response(response).wrap_err("got wrong response type")
+    }
+}
+
+trait CursiveIpc {
+    fn ipc<T: RequestDefinition>(&mut self, request: T) -> Option<T::Response>;
+}
+
+impl CursiveIpc for Cursive {
+    fn ipc<T: RequestDefinition>(&mut self, request: T) -> Option<T::Response> {
+        match self.user_data::<Ipc>().unwrap().exec(request) {
+            Ok(Ok(resp)) => Some(resp),
+            Err(err) => {
+                self.add_layer(
+                    Dialog::new()
+                        .content(format!("ipc error: {err:?}").text_view())
+                        .button("quit", |siv| siv.quit()),
+                );
+                None
+            }
+            Ok(Err(err)) => {
+                self.add_layer(
+                    Dialog::new()
+                        .content(format!("error: {err}").text_view())
+                        .button("ok", |siv| {
+                            siv.pop_layer();
+                        }),
+                );
+                None
+            }
+        }
+    }
+}
+
+trait ViewExt: Sized {
+    fn float(self, title: &str) -> PaddedView<Panel<ScrollView<PaddedView<Self>>>>;
+    fn pad(self) -> PaddedView<Self>;
+    fn hpad(self) -> PaddedView<Self>;
+}
+
+impl<V: Sized> ViewExt for V {
+    fn float(self, title: &str) -> PaddedView<Panel<ScrollView<PaddedView<Self>>>> {
+        PaddedView::new(
+            Margins::lrtb(1, 1, 1, 1),
+            Panel::new(ScrollView::new(PaddedView::new(Margins::lr(1, 1), self))).title(title),
+        )
+    }
+
+    fn pad(self) -> PaddedView<Self> {
+        PaddedView::new(Margins::lrtb(1, 1, 1, 1), self)
+    }
+
+    fn hpad(self) -> PaddedView<Self> {
+        PaddedView::new(Margins::lr(1, 1), self)
+    }
+}
+
+trait ToTextView {
+    fn text_view(self) -> TextView;
+}
+
+impl<S: Into<StyledString>> ToTextView for S {
+    fn text_view(self) -> TextView {
+        TextView::new(self)
+    }
+}