diff options
author | mia <mia@mia.jetzt> | 2024-04-16 19:05:41 -0700 |
---|---|---|
committer | mia <mia@mia.jetzt> | 2024-04-16 19:05:41 -0700 |
commit | 796b2cafc798a7faa80a007002831a4c40635fe8 (patch) | |
tree | d8e68590524f4adab7ff8ff6e2cb3dfbb0c64b37 /src/tui | |
download | dissociate-0.1.0.tar.gz dissociate-0.1.0.zip |
initial commit v0.1.0
Diffstat (limited to 'src/tui')
-rw-r--r-- | src/tui/accounts.rs | 169 | ||||
-rw-r--r-- | src/tui/mod.rs | 159 |
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) + } +} |