use crate::command_processor::*; use crate::settings::Settings; use crate::veilid_client_capnp::*; use crossbeam_channel::Sender; use cursive::align::*; use cursive::event::*; use cursive::theme::*; use cursive::traits::*; use cursive::utils::markup::StyledString; use cursive::views::*; use cursive::Cursive; use cursive::CursiveRunnable; use cursive_flexi_logger_view::{CursiveLogWriter, FlexiLoggerView}; use log::*; use std::cell::RefCell; use std::collections::{HashMap, VecDeque}; use std::rc::Rc; use thiserror::Error; ////////////////////////////////////////////////////////////// /// struct Dirty { pub value: T, dirty: bool, } impl Dirty { pub fn new(value: T) -> Self { Self { value: value, dirty: true, } } pub fn set(&mut self, value: T) { self.value = value; self.dirty = true; } pub fn get(&self) -> &T { &self.value } // pub fn get_mut(&mut self) -> &mut T { // &mut self.value // } pub fn take_dirty(&mut self) -> bool { let is_dirty = self.dirty; self.dirty = false; is_dirty } } pub type UICallback = Box; struct UIState { attachment_state: Dirty, connection_state: Dirty, } impl UIState { pub fn new() -> Self { Self { attachment_state: Dirty::new(AttachmentState::Detached), connection_state: Dirty::new(ConnectionState::Disconnected), } } } #[derive(Error, Debug)] #[error("???")] struct UIError; pub struct UIInner { ui_state: UIState, log_colors: HashMap, cmdproc: Option, cb_sink: Sender>, cmd_history: VecDeque, cmd_history_position: usize, cmd_history_max_size: usize, connection_dialog_state: Option, } type Handle = Rc>; #[derive(Clone)] pub struct UI { siv: Handle, inner: Handle, } impl UI { pub fn new(node_log_scrollback: usize, settings: &Settings) -> Self { cursive_flexi_logger_view::resize(node_log_scrollback); // Instantiate the cursive runnable /* // reduces flicker, but it costs cpu let mut runnable = CursiveRunnable::new( || -> Result, UIError> { let crossterm_backend = cursive::backends::crossterm::Backend::init().unwrap(); let buffered_backend = cursive_buffered_backend::BufferedBackend::new(crossterm_backend); Ok(Box::new(buffered_backend)) }, ); */ let runnable = cursive::default(); // Make the callback mechanism easily reachable let cb_sink = runnable.cb_sink().clone(); // Create the UI object let this = Self { siv: Rc::new(RefCell::new(runnable)), inner: Rc::new(RefCell::new(UIInner { ui_state: UIState::new(), log_colors: Default::default(), cmdproc: None, cmd_history: { let mut vd = VecDeque::new(); vd.push_back("".to_string()); vd }, cmd_history_position: 0, cmd_history_max_size: settings.interface.command_line.history_size, connection_dialog_state: None, cb_sink: cb_sink, })), }; let mut siv = this.siv.borrow_mut(); let mut inner = this.inner.borrow_mut(); // Make the inner object accessible in callbacks easily siv.set_user_data(this.inner.clone()); // Create layouts let mut mainlayout = LinearLayout::vertical().with_name("main-layout"); mainlayout.get_mut().add_child( Panel::new( FlexiLoggerView::new_scrollable() .with_name("node-events") .full_screen(), ) .title_position(HAlign::Left) .title("Node Events"), ); mainlayout.get_mut().add_child( Panel::new(ScrollView::new( TextView::new("Peer Table") .with_name("peers") .fixed_height(8) .scrollable(), )) .title_position(HAlign::Left) .title("Peers"), ); let mut command = StyledString::new(); command.append_styled("Command> ", ColorStyle::title_primary()); // mainlayout.get_mut().add_child( LinearLayout::horizontal() .child(TextView::new(command)) .child( EditView::new() .on_submit(|s, text| { UI::on_command_line_entered(s, text); }) .on_edit(|s, text, cursor| UI::on_command_line_edit(s, text, cursor)) .on_up_down(|s, dir| { UI::on_command_line_history(s, dir); }) .style(ColorStyle::new( PaletteColor::Background, PaletteColor::Secondary, )) .with_name("command-line") .full_screen() .fixed_height(1), ) .child( Button::new("Attach", |s| { UI::on_button_attach_pressed(s); }) .with_name("button-attach"), ), ); let mut version = StyledString::new(); version.append_styled( concat!(" | veilid-cli v", env!("CARGO_PKG_VERSION")), ColorStyle::highlight_inactive(), ); mainlayout.get_mut().add_child( LinearLayout::horizontal() .color(Some(ColorStyle::highlight_inactive())) .child( TextView::new("") .with_name("status-bar") .full_screen() .fixed_height(1), ) .child(TextView::new(version)), ); siv.add_fullscreen_layer(mainlayout); UI::setup_colors(&mut siv, &mut inner, &settings); UI::setup_quit_handler(&mut siv); drop(inner); drop(siv); this } fn command_processor(s: &mut Cursive) -> CommandProcessor { let inner = Self::inner(s); inner.cmdproc.as_ref().unwrap().clone() } fn inner(s: &mut Cursive) -> std::cell::Ref<'_, UIInner> { s.user_data::>().unwrap().borrow() } fn inner_mut(s: &mut Cursive) -> std::cell::RefMut<'_, UIInner> { s.user_data::>().unwrap().borrow_mut() } fn setup_colors(siv: &mut CursiveRunnable, inner: &mut UIInner, settings: &Settings) { // Make colors let mut theme = cursive::theme::load_default(); theme.shadow = settings.interface.theme.shadow; theme.borders = BorderStyle::from(&settings.interface.theme.borders); theme.palette.set_color( "background", Color::parse(settings.interface.theme.colors.background.as_str()).unwrap(), ); theme.palette.set_color( "shadow", Color::parse(settings.interface.theme.colors.shadow.as_str()).unwrap(), ); theme.palette.set_color( "view", Color::parse(settings.interface.theme.colors.view.as_str()).unwrap(), ); theme.palette.set_color( "primary", Color::parse(settings.interface.theme.colors.primary.as_str()).unwrap(), ); theme.palette.set_color( "secondary", Color::parse(settings.interface.theme.colors.secondary.as_str()).unwrap(), ); theme.palette.set_color( "tertiary", Color::parse(settings.interface.theme.colors.tertiary.as_str()).unwrap(), ); theme.palette.set_color( "title_primary", Color::parse(settings.interface.theme.colors.title_primary.as_str()).unwrap(), ); theme.palette.set_color( "title_secondary", Color::parse(settings.interface.theme.colors.title_secondary.as_str()).unwrap(), ); theme.palette.set_color( "highlight", Color::parse(settings.interface.theme.colors.highlight.as_str()).unwrap(), ); theme.palette.set_color( "highlight_inactive", Color::parse(settings.interface.theme.colors.highlight_inactive.as_str()).unwrap(), ); theme.palette.set_color( "highlight_text", Color::parse(settings.interface.theme.colors.highlight_text.as_str()).unwrap(), ); siv.set_theme(theme); // Make log colors let mut colors = HashMap::::new(); colors.insert( Level::Trace, Color::parse(settings.interface.theme.log_colors.trace.as_str()).unwrap(), ); colors.insert( Level::Debug, Color::parse(settings.interface.theme.log_colors.debug.as_str()).unwrap(), ); colors.insert( Level::Info, Color::parse(settings.interface.theme.log_colors.info.as_str()).unwrap(), ); colors.insert( Level::Warn, Color::parse(settings.interface.theme.log_colors.warn.as_str()).unwrap(), ); colors.insert( Level::Error, Color::parse(settings.interface.theme.log_colors.error.as_str()).unwrap(), ); inner.log_colors = colors; } fn setup_quit_handler(siv: &mut Cursive) { siv.clear_global_callbacks(cursive::event::Event::CtrlChar('c')); siv.set_on_pre_event(cursive::event::Event::CtrlChar('c'), UI::quit_handler); siv.set_global_callback(cursive::event::Event::Key(Key::Esc), UI::quit_handler); } fn quit_handler(siv: &mut Cursive) { siv.add_layer( Dialog::text("Do you want to exit?") .button("Yes", |s| s.quit()) .button("No", |mut s| { s.pop_layer(); UI::setup_quit_handler(&mut s); }), ); siv.set_on_pre_event(cursive::event::Event::CtrlChar('c'), |s| { s.quit(); }); siv.set_global_callback(cursive::event::Event::Key(Key::Esc), |mut s| { s.pop_layer(); UI::setup_quit_handler(&mut s); }); } pub fn cursive_flexi_logger(&mut self) -> Box { let mut flv = cursive_flexi_logger_view::cursive_flexi_logger(self.siv.borrow().cb_sink().clone()); flv.set_colors(self.inner.borrow().log_colors.clone()); flv } pub fn set_command_processor(&mut self, cmdproc: CommandProcessor) { let mut inner = self.inner.borrow_mut(); inner.cmdproc = Some(cmdproc); let _ = inner.cb_sink.send(Box::new(UI::update_cb)); } pub fn set_attachment_state(&mut self, state: AttachmentState) { let mut inner = self.inner.borrow_mut(); inner.ui_state.attachment_state.set(state); let _ = inner.cb_sink.send(Box::new(UI::update_cb)); } pub fn set_connection_state(&mut self, state: ConnectionState) { let mut inner = self.inner.borrow_mut(); inner.ui_state.connection_state.set(state); let _ = inner.cb_sink.send(Box::new(UI::update_cb)); } pub fn add_node_event(&mut self, event: &str) { let inner = self.inner.borrow_mut(); let color = inner.log_colors.get(&Level::Info).unwrap().clone(); for line in event.lines() { cursive_flexi_logger_view::push_to_log(StyledString::styled(line, color)); } let _ = inner.cb_sink.send(Box::new(UI::update_cb)); } pub fn quit(&mut self) { let inner = self.inner.borrow_mut(); let _ = inner.cb_sink.send(Box::new(|s| { s.quit(); })); } // Note: Cursive is not re-entrant, can't borrow_mut self.siv again after this pub async fn run_async(&mut self) { let mut siv = self.siv.borrow_mut(); siv.run_async().await; } // pub fn run(&mut self) { // let mut siv = self.siv.borrow_mut(); // siv.run(); // } ///////////////////////////////////////////////////////////////////////////////////// // Private functions // fn main_layout(s: &mut Cursive) -> ViewRef { // s.find_name("main-layout").unwrap() // } // fn column_layout(s: &mut Cursive) -> ViewRef { // s.find_name("column-layout").unwrap() // } // fn button_layout(s: &mut Cursive) -> ViewRef { // s.find_name("button-layout").unwrap() // } // fn peers(s: &mut Cursive) -> ViewRef { // s.find_name("peers").unwrap() // } // fn node_events(s: &mut Cursive) -> ViewRef { // s.find_name("node-events").unwrap() // } fn command_line(s: &mut Cursive) -> ViewRef { s.find_name("command-line").unwrap() } fn button_attach(s: &mut Cursive) -> ViewRef