cargo-port 0.4.2

A TUI for inspecting and managing Rust projects
use crossterm::event::KeyCode;
use ratatui::layout::Position;
use tui_pane::Hittable;
use tui_pane::Viewport;

use crate::sccache::StatsResult;
use crate::tui::hit_test::HoverTarget;
use crate::tui::panes::PaneId;

#[derive(Clone, Debug, Eq, PartialEq)]
pub(super) struct SccacheTarget {
    pub(super) label: String,
    pub(super) value: String,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub(super) enum SccacheStatus {
    Loading,
    Ready { lines: Vec<String> },
    Failed { lines: Vec<String> },
}

pub struct SccachePane {
    status:            SccacheStatus,
    request_id:        u64,
    line_targets:      Vec<Option<usize>>,
    selectable_values: Vec<SccacheTarget>,
    pub viewport:      Viewport,
}

impl SccachePane {
    pub const fn new() -> Self {
        Self {
            status:            SccacheStatus::Loading,
            request_id:        0,
            line_targets:      Vec::new(),
            selectable_values: Vec::new(),
            viewport:          Viewport::new(),
        }
    }

    pub(super) const fn status(&self) -> &SccacheStatus { &self.status }

    pub const fn viewport_mut(&mut self) -> &mut Viewport { &mut self.viewport }

    pub(super) fn set_line_targets(
        &mut self,
        line_targets: Vec<Option<usize>>,
        selectable_values: Vec<SccacheTarget>,
    ) {
        self.line_targets = line_targets;
        self.selectable_values = selectable_values;
    }

    pub fn row_at(&self, pos: Position) -> Option<usize> {
        let inner = self.viewport.content_area();
        if inner.width == 0 || inner.height == 0 || !inner.contains(pos) {
            return None;
        }
        let line_index = usize::from(pos.y.saturating_sub(inner.y)) + self.viewport.scroll_offset();
        self.line_targets.get(line_index).copied().flatten()
    }

    pub(super) fn line_for_selection(&self, selection: usize) -> Option<usize> {
        self.line_targets
            .iter()
            .position(|target| target.is_some_and(|target| target == selection))
    }

    pub(super) fn selected_copy_value(&self) -> Option<&SccacheTarget> {
        self.selectable_values.get(self.viewport.pos())
    }

    pub fn start_loading(&mut self) -> u64 {
        self.request_id = self.request_id.saturating_add(1);
        self.status = SccacheStatus::Loading;
        self.viewport.home();
        self.request_id
    }

    pub fn apply_result(&mut self, request_id: u64, result: StatsResult) {
        if self.request_id != request_id {
            return;
        }
        self.status = match result {
            StatsResult::Ready(lines) => SccacheStatus::Ready { lines },
            StatsResult::Failed(lines) => SccacheStatus::Failed { lines },
        };
        self.viewport.home();
    }

    pub fn handle_navigation_key(&mut self, code: KeyCode) {
        match code {
            KeyCode::Up => self.viewport.up(),
            KeyCode::Down => self.viewport.down(),
            KeyCode::Home => self.viewport.home(),
            KeyCode::End => {
                let last = self.viewport.len().saturating_sub(1);
                self.viewport.set_pos(last);
            },
            KeyCode::PageUp => self.viewport.page_up(),
            KeyCode::PageDown => self.viewport.page_down(),
            _ => {},
        }
    }
}

impl Hittable<HoverTarget> for SccachePane {
    fn hit_test_at(&self, pos: Position) -> Option<HoverTarget> {
        self.row_at(pos).map(|row| HoverTarget::PaneRow {
            pane: PaneId::Sccache,
            row,
        })
    }
}

impl Default for SccachePane {
    fn default() -> Self { Self::new() }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn starting_another_request_retries_and_ignores_the_previous_result() {
        let mut pane = SccachePane::new();
        let previous_request = pane.start_loading();
        pane.apply_result(
            previous_request,
            StatsResult::Ready(vec!["previous".to_string()]),
        );

        let current_request = pane.start_loading();
        assert_eq!(pane.status(), &SccacheStatus::Loading);

        pane.apply_result(
            previous_request,
            StatsResult::Failed(vec!["late failure".to_string()]),
        );
        assert_eq!(pane.status(), &SccacheStatus::Loading);

        pane.apply_result(
            current_request,
            StatsResult::Ready(vec!["current".to_string()]),
        );
        assert_eq!(
            pane.status(),
            &SccacheStatus::Ready {
                lines: vec!["current".to_string()],
            }
        );
    }
}