quantoxide 0.5.5

Rust framework for developing, backtesting, and deploying Bitcoin futures trading strategies.
Documentation
use std::{
    fs::File,
    sync::{Arc, Mutex, MutexGuard},
};

use ratatui::{
    Frame,
    layout::{Constraint, Direction, Layout, Rect},
};
use strum::EnumIter;

use super::{
    super::{
        error::Result,
        view::{TuiLogManager, TuiView},
    },
    LiveUiMessage,
};

#[derive(Debug, PartialEq, EnumIter)]
pub(in crate::tui) enum LiveTuiPane {
    Trades,
    Summary,
    Log,
}

pub(in crate::tui) struct LiveTuiViewState {
    log_file: Option<File>,
    active_pane: LiveTuiPane,

    trades_lines: Vec<String>,
    trades_max_line_width: usize,
    trades_rect: Rect,
    trades_v_scroll: usize,
    trades_h_scroll: usize,

    summary_lines: Vec<String>,
    summary_max_line_width: usize,
    summary_rect: Rect,
    summary_v_scroll: usize,
    summary_h_scroll: usize,

    log_entries: Vec<String>,
    log_max_line_width: usize,
    log_rect: Rect,
    log_v_scroll: usize,
    log_h_scroll: usize,
}

pub(in crate::tui) struct LiveTuiView {
    max_tui_log_len: usize,
    state: Mutex<LiveTuiViewState>,
}

impl LiveTuiView {
    pub fn new(max_tui_log_len: usize, log_file: Option<File>) -> Arc<Self> {
        Arc::new(Self {
            max_tui_log_len,
            state: Mutex::new(LiveTuiViewState {
                log_file,
                active_pane: LiveTuiPane::Log,

                trades_lines: vec!["Initializing...".to_string()],
                trades_max_line_width: 0,
                trades_rect: Rect::default(),
                trades_v_scroll: 0,
                trades_h_scroll: 0,

                summary_lines: vec!["Initializing...".to_string()],
                summary_max_line_width: 0,
                summary_rect: Rect::default(),
                summary_v_scroll: 0,
                summary_h_scroll: 0,

                log_entries: Vec::new(),
                log_max_line_width: 0,
                log_rect: Rect::default(),
                log_v_scroll: 0,
                log_h_scroll: 0,
            }),
        })
    }
}

impl TuiLogManager for LiveTuiView {
    type State = LiveTuiViewState;

    fn get_max_tui_log_len(&self) -> usize {
        self.max_tui_log_len
    }

    fn get_log_components_mut(
        state: &mut Self::State,
    ) -> (
        Option<&mut File>,
        &mut Vec<String>,
        &mut usize,
        Rect,
        &mut usize,
    ) {
        (
            state.log_file.as_mut(),
            &mut state.log_entries,
            &mut state.log_max_line_width,
            state.log_rect,
            &mut state.log_v_scroll,
        )
    }

    fn get_state(&self) -> MutexGuard<'_, Self::State> {
        self.state
            .lock()
            .expect("`LiveTuiView` mutex can't be poisoned")
    }
}

impl TuiView for LiveTuiView {
    type UiMessage = LiveUiMessage;

    type TuiPane = LiveTuiPane;

    fn get_active_scroll_data(state: &Self::State) -> (usize, usize, &Rect, usize, usize) {
        match state.active_pane {
            LiveTuiPane::Trades => (
                state.trades_v_scroll,
                state.trades_h_scroll,
                &state.trades_rect,
                state.trades_lines.len(),
                state.trades_max_line_width,
            ),
            LiveTuiPane::Summary => (
                state.summary_v_scroll,
                state.summary_h_scroll,
                &state.summary_rect,
                state.summary_lines.len(),
                state.summary_max_line_width,
            ),
            LiveTuiPane::Log => (
                state.log_v_scroll,
                state.log_h_scroll,
                &state.log_rect,
                state.log_entries.len(),
                state.log_max_line_width,
            ),
        }
    }

    fn get_active_scroll_mut(state: &mut Self::State) -> (&mut usize, &mut usize) {
        match state.active_pane {
            LiveTuiPane::Trades => (&mut state.trades_v_scroll, &mut state.trades_h_scroll),
            LiveTuiPane::Summary => (&mut state.summary_v_scroll, &mut state.summary_h_scroll),
            LiveTuiPane::Log => (&mut state.log_v_scroll, &mut state.log_h_scroll),
        }
    }

    fn get_pane_render_info(
        state: &Self::State,
        pane: Self::TuiPane,
    ) -> (&'static str, &Vec<String>, usize, usize, Rect, bool) {
        match pane {
            LiveTuiPane::Trades => (
                "Trades",
                &state.trades_lines,
                state.trades_v_scroll,
                state.trades_h_scroll,
                state.trades_rect,
                state.active_pane == LiveTuiPane::Trades,
            ),
            LiveTuiPane::Summary => (
                "Trades Summary",
                &state.summary_lines,
                state.summary_v_scroll,
                state.summary_h_scroll,
                state.summary_rect,
                state.active_pane == LiveTuiPane::Summary,
            ),
            LiveTuiPane::Log => (
                "Log",
                &state.log_entries,
                state.log_v_scroll,
                state.log_h_scroll,
                state.log_rect,
                state.active_pane == LiveTuiPane::Log,
            ),
        }
    }

    fn get_pane_data_mut(
        state: &mut Self::State,
        pane: Self::TuiPane,
    ) -> (&mut Vec<String>, &mut usize, &mut usize) {
        match pane {
            LiveTuiPane::Trades => (
                &mut state.trades_lines,
                &mut state.trades_max_line_width,
                &mut state.trades_v_scroll,
            ),
            LiveTuiPane::Summary => (
                &mut state.summary_lines,
                &mut state.summary_max_line_width,
                &mut state.summary_v_scroll,
            ),
            LiveTuiPane::Log => (
                &mut state.log_entries,
                &mut state.log_max_line_width,
                &mut state.log_v_scroll,
            ),
        }
    }

    fn handle_ui_message(&self, message: Self::UiMessage) -> Result<bool> {
        match message {
            LiveUiMessage::TradesUpdate(trades_table) => {
                self.update_pane_content(LiveTuiPane::Trades, trades_table);
                Ok(false)
            }
            LiveUiMessage::SummaryUpdate(summary) => {
                self.update_pane_content(LiveTuiPane::Summary, summary);
                Ok(false)
            }
            LiveUiMessage::LogEntry(entry) => {
                self.add_log_entry(entry)?;
                Ok(false)
            }
            LiveUiMessage::ShutdownCompleted => Ok(true),
        }
    }

    fn render(&self, f: &mut Frame) {
        let main_area = Self::get_main_area(f);

        let main_chunks = Layout::default()
            .direction(Direction::Vertical)
            .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
            .split(main_area);

        let bottom_chunks = Layout::default()
            .direction(Direction::Horizontal)
            .constraints([Constraint::Length(46), Constraint::Min(0)].as_ref())
            .split(main_chunks[1]);

        let mut state_guard = self.get_state();

        state_guard.trades_rect = main_chunks[0];
        state_guard.summary_rect = bottom_chunks[0];
        state_guard.log_rect = bottom_chunks[1];

        Self::render_panes(f, &state_guard);
    }

    fn switch_pane(&self) {
        let mut state_guard = self.get_state();

        state_guard.active_pane = match state_guard.active_pane {
            LiveTuiPane::Trades => LiveTuiPane::Log,
            LiveTuiPane::Log => LiveTuiPane::Summary,
            LiveTuiPane::Summary => LiveTuiPane::Trades,
        };
    }
}