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( &mut self, request: T, ) -> eyre::Result::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(&mut self, request: T) -> Option; } impl CursiveIpc for Cursive { fn ipc(&mut self, request: T) -> Option { match self.user_data::().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>>>; fn pad(self) -> PaddedView; fn hpad(self) -> PaddedView; } impl ViewExt for V { fn float(self, title: &str) -> PaddedView>>> { 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 { PaddedView::new(Margins::lrtb(1, 1, 1, 1), self) } fn hpad(self) -> PaddedView { PaddedView::new(Margins::lr(1, 1), self) } } trait ToTextView { fn text_view(self) -> TextView; } impl> ToTextView for S { fn text_view(self) -> TextView { TextView::new(self) } }