🗝
summary refs log tree commit diff
path: root/src/tui
diff options
context:
space:
mode:
authormia <mia@mia.jetzt>2024-04-16 19:05:41 -0700
committermia <mia@mia.jetzt>2024-04-16 19:05:41 -0700
commit796b2cafc798a7faa80a007002831a4c40635fe8 (patch)
treed8e68590524f4adab7ff8ff6e2cb3dfbb0c64b37 /src/tui
downloaddissociate-796b2cafc798a7faa80a007002831a4c40635fe8.tar.gz
dissociate-796b2cafc798a7faa80a007002831a4c40635fe8.zip
initial commit v0.1.0
Diffstat (limited to 'src/tui')
-rw-r--r--src/tui/accounts.rs169
-rw-r--r--src/tui/mod.rs159
2 files changed, 328 insertions, 0 deletions
diff --git a/src/tui/accounts.rs b/src/tui/accounts.rs
new file mode 100644
index 0000000..a43028c
--- /dev/null
+++ b/src/tui/accounts.rs
@@ -0,0 +1,169 @@
+use std::{cell::RefCell, rc::Rc};
+
+use cursive::{
+    view::Nameable,
+    views::{Button, Dialog, LinearLayout, SelectView, TextArea},
+    Cursive,
+};
+
+use crate::ipc::{
+    DeleteAccountRequest, GetAccountRequest, ListAccountsRequest, UpdateScopesRequest,
+};
+
+use super::{CursiveIpc, ToTextView, ViewExt};
+
+pub fn show(siv: &mut Cursive) {
+    if let Some(accounts) = siv.ipc(ListAccountsRequest {}) {
+        siv.add_layer(
+            SelectView::new()
+                .autojump()
+                .with_all_str(accounts.names)
+                .on_submit(|siv, name: &str| {
+                    manage_account(siv, name);
+                })
+                .float("accounts"),
+        );
+    }
+}
+
+fn manage_account(siv: &mut Cursive, name: &str) {
+    let name = name.to_string();
+    if let Some(_) = siv.ipc(GetAccountRequest { name: name.clone() }) {
+        siv.add_layer(
+            LinearLayout::vertical()
+                .child({
+                    let name = name.clone();
+                    enum SelectOption {
+                        Back,
+                        Scopes,
+                        Delete,
+                    }
+                    SelectView::new()
+                        .item("back", SelectOption::Back)
+                        .item("scopes", SelectOption::Scopes)
+                        .item("delete", SelectOption::Delete)
+                        .on_submit(move |siv, opt| match opt {
+                            SelectOption::Back => {
+                                siv.pop_layer();
+                            }
+                            SelectOption::Scopes => edit_scopes(siv, &name),
+                            SelectOption::Delete => delete_account(siv, &name),
+                        })
+                })
+                .float(&name),
+        )
+    }
+}
+
+fn edit_scopes(siv: &mut Cursive, name: &str) {
+    let name = name.to_string();
+    let Some(account) = siv.ipc(GetAccountRequest { name: name.clone() }) else {
+        siv.pop_layer();
+        return; // it was just there!
+    };
+    let scopes = Rc::new(RefCell::new(account.scopes));
+    enum SelectOption {
+        Scope(String),
+        Back,
+        Add,
+    }
+    siv.add_layer(
+        SelectView::new()
+            .autojump()
+            .item("back", SelectOption::Back)
+            .item("add", SelectOption::Add)
+            .with_all(
+                scopes
+                    .clone()
+                    .borrow()
+                    .iter()
+                    .map(|scope| (scope.to_string(), SelectOption::Scope(scope.clone()))),
+            )
+            .on_submit(move |siv, opt| {
+                match opt {
+                    SelectOption::Scope(scope) => {
+                        let mut scopes = scopes.borrow_mut();
+                        let index = scopes
+                            .iter()
+                            .enumerate()
+                            .find(|(_, check)| *check == scope)
+                            .unwrap()
+                            .0;
+                        scopes.swap_remove(index);
+                        ipc_update_scopes(siv, &name, &scopes);
+                        siv.pop_layer();
+                        edit_scopes(siv, &name);
+                    }
+                    SelectOption::Back => {
+                        siv.pop_layer();
+                    }
+                    SelectOption::Add => add_scope(siv, name.clone(), scopes.clone()),
+                };
+            })
+            .float("edit scopes"),
+    );
+}
+
+fn add_scope(siv: &mut Cursive, name: String, scopes: Rc<RefCell<Vec<String>>>) {
+    siv.add_layer(
+        LinearLayout::vertical()
+            .child(TextArea::new().with_name("scope"))
+            .child(Button::new("add", {
+                move |siv| {
+                    let text_area = siv.find_name::<TextArea>("scope").unwrap();
+                    let scope = text_area.get_content();
+                    if scope.is_empty() {
+                        siv.pop_layer();
+                        return;
+                    }
+                    let mut scopes = scopes.borrow_mut();
+                    scopes.push(scope.to_string());
+                    ipc_update_scopes(siv, &name, &scopes);
+                    siv.pop_layer();
+                    siv.pop_layer();
+                    edit_scopes(siv, &name);
+                }
+            }))
+            .float("add scope"),
+    )
+}
+
+fn ipc_update_scopes(siv: &mut Cursive, name: &str, scopes: &Vec<String>) {
+    siv.ipc(UpdateScopesRequest {
+        account: name.to_string(),
+        scopes: scopes.clone(),
+    });
+}
+
+fn delete_account(siv: &mut Cursive, name: &str) {
+    let name = name.to_string();
+    siv.add_layer(
+        LinearLayout::vertical()
+            .child(format!("delete {name}?").text_view())
+            .child(TextArea::new().with_name("confirm"))
+            .child(Button::new("submit", move |siv| {
+                if let Some(confirm) = siv.find_name::<TextArea>("confirm") {
+                    if confirm.get_content() == "yes" {
+                        if siv
+                            .ipc(DeleteAccountRequest { name: name.clone() })
+                            .is_some()
+                        {
+                            siv.add_layer(Dialog::around("success".text_view()).button(
+                                "ok",
+                                |siv| {
+                                    // all the way back to the root
+                                    siv.pop_layer();
+                                    siv.pop_layer();
+                                    siv.pop_layer();
+                                    siv.pop_layer();
+                                },
+                            ))
+                        }
+                    } else {
+                        siv.pop_layer();
+                    }
+                }
+            }))
+            .float("are you sure"),
+    );
+}
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)
+    }
+}