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},
    },
    SyncUiMessage,
};

#[derive(Debug, PartialEq, EnumIter)]
pub(in crate::tui) enum SyncTuiPane {
    SyncStatePane,
    LogPane,
}

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

    ph_state_text: String,
    fs_state_text: String,
    state_lines: Vec<String>,
    state_max_line_width: usize,
    state_rect: Rect,
    state_v_scroll: usize,
    state_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 SyncTuiView {
    max_tui_log_len: usize,
    state: Mutex<SyncTuiViewState>,
}

impl SyncTuiView {
    pub fn new(max_tui_log_len: usize, log_file: Option<File>) -> Arc<Self> {
        Arc::new(Self {
            max_tui_log_len,
            state: Mutex::new(SyncTuiViewState {
                log_file,
                active_pane: SyncTuiPane::LogPane,

                ph_state_text: String::new(),
                fs_state_text: String::new(),
                state_lines: vec!["Initializing...".to_string()],
                state_max_line_width: 0,
                state_rect: Rect::default(),
                state_v_scroll: 0,
                state_h_scroll: 0,

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

    fn compose_state_pane(state: &SyncTuiViewState) -> String {
        let mut result = String::new();

        if !state.ph_state_text.is_empty() {
            result.push_str("\n[Price History]\n");
            result.push_str(&state.ph_state_text);
        }

        if !state.fs_state_text.is_empty() && !state.ph_state_text.is_empty() {
            result.push_str("\n\n[Funding Settlements]\n");
            result.push_str(&state.fs_state_text);
        }

        result
    }
}

impl TuiLogManager for SyncTuiView {
    type State = SyncTuiViewState;

    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("`SyncTuiView` mutex can't be poisoned")
    }
}

impl TuiView for SyncTuiView {
    type UiMessage = SyncUiMessage;

    type TuiPane = SyncTuiPane;

    fn get_active_scroll_data(state: &Self::State) -> (usize, usize, &Rect, usize, usize) {
        match state.active_pane {
            SyncTuiPane::SyncStatePane => (
                state.state_v_scroll,
                state.state_h_scroll,
                &state.state_rect,
                state.state_lines.len(),
                state.state_max_line_width,
            ),
            SyncTuiPane::LogPane => (
                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 {
            SyncTuiPane::SyncStatePane => (&mut state.state_v_scroll, &mut state.state_h_scroll),
            SyncTuiPane::LogPane => (&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 {
            SyncTuiPane::SyncStatePane => (
                "Sync State",
                &state.state_lines,
                state.state_v_scroll,
                state.state_h_scroll,
                state.state_rect,
                state.active_pane == SyncTuiPane::SyncStatePane,
            ),
            SyncTuiPane::LogPane => (
                "Log",
                &state.log_entries,
                state.log_v_scroll,
                state.log_h_scroll,
                state.log_rect,
                state.active_pane == SyncTuiPane::LogPane,
            ),
        }
    }

    fn get_pane_data_mut(
        state: &mut Self::State,
        pane: Self::TuiPane,
    ) -> (&mut Vec<String>, &mut usize, &mut usize) {
        match pane {
            SyncTuiPane::SyncStatePane => (
                &mut state.state_lines,
                &mut state.state_max_line_width,
                &mut state.state_v_scroll,
            ),
            SyncTuiPane::LogPane => (
                &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 {
            SyncUiMessage::PriceHistoryStateUpdate(text) => {
                let mut state_guard = self.get_state();
                state_guard.ph_state_text = text;
                let combined = Self::compose_state_pane(&state_guard);
                drop(state_guard);
                self.update_pane_content(SyncTuiPane::SyncStatePane, combined);
                Ok(false)
            }
            SyncUiMessage::FundingSettlementsStateUpdate(text) => {
                let mut state_guard = self.get_state();
                state_guard.fs_state_text = text;
                let combined = Self::compose_state_pane(&state_guard);
                drop(state_guard);
                self.update_pane_content(SyncTuiPane::SyncStatePane, combined);
                Ok(false)
            }
            SyncUiMessage::LogEntry(entry) => {
                self.add_log_entry(entry)?;
                Ok(false)
            }
            SyncUiMessage::ShutdownCompleted => Ok(true),
        }
    }

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

        let main_chunks = Layout::default()
            .direction(Direction::Horizontal)
            .constraints([Constraint::Length(56), Constraint::Min(0)])
            .split(main_area);

        let mut state_guard = self.get_state();

        state_guard.state_rect = main_chunks[0];
        state_guard.log_rect = main_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 {
            SyncTuiPane::SyncStatePane => SyncTuiPane::LogPane,
            SyncTuiPane::LogPane => SyncTuiPane::SyncStatePane,
        };
    }
}