magi-code 0.96.1

Repository-aware CLI coding agent for terminal work
Documentation
use super::*;

#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub(crate) enum ThemeSaveState {
    #[default]
    Idle,
    Pending,
    Failed(String),
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ThemePickerState {
    pub(crate) entries: Vec<crate::appearance::ThemeCatalogEntry>,
    pub(crate) filtered_indices: Vec<usize>,
    pub(crate) active_variant: opaline::ThemeVariant,
    catalog_initialized: bool,
    pub(crate) query: String,
    pub(crate) query_cursor: usize,
    pub(crate) selected: usize,
    pub(crate) preview_theme_id: String,
    pub(crate) opening_revision: u64,
    pub(crate) loading: bool,
    pub(crate) notice: Option<String>,
    pub(crate) error: Option<String>,
    pub(crate) catalog_request_id: Option<u64>,
    pub(crate) save_state: ThemeSaveState,
}

impl ThemePickerState {
    pub(super) fn new(
        preview_theme_id: String,
        opening_revision: u64,
        loading: bool,
        catalog_request_id: Option<u64>,
    ) -> Self {
        Self {
            entries: Vec::new(),
            filtered_indices: Vec::new(),
            active_variant: opaline::ThemeVariant::Dark,
            catalog_initialized: false,
            query: String::new(),
            query_cursor: 0,
            selected: 0,
            preview_theme_id,
            opening_revision,
            loading,
            notice: None,
            error: None,
            catalog_request_id,
            save_state: ThemeSaveState::Idle,
        }
    }

    fn selected_entry(&self) -> Option<&crate::appearance::ThemeCatalogEntry> {
        self.filtered_indices
            .get(self.selected)
            .and_then(|index| self.entries.get(*index))
    }

    pub(super) fn selected_id(&self) -> Option<String> {
        self.selected_entry().map(|entry| entry.id.clone())
    }

    pub(super) fn clamp(&mut self) {
        let item_count = self.filtered_indices.len();
        clamp_picker_selection(&mut self.selected, item_count);
    }

    fn filtered_selection(
        &self,
        query: &str,
        variant: opaline::ThemeVariant,
    ) -> (Vec<usize>, usize) {
        let selected_id = self.selected_entry().map(|entry| entry.id.as_str());
        let needle = query.to_lowercase();
        let indices: Vec<_> = self
            .entries
            .iter()
            .enumerate()
            .filter(|(_, entry)| {
                entry.variant == variant
                    && (needle.is_empty()
                        || entry.id.to_lowercase().contains(&needle)
                        || entry.display_name.to_lowercase().contains(&needle))
            })
            .map(|(index, _)| index)
            .collect();
        let selected = selected_id
            .and_then(|id| {
                indices
                    .iter()
                    .position(|index| self.entries[*index].id == id)
            })
            .unwrap_or(0);
        (indices, selected)
    }

    pub(crate) fn selected_id_after_filter(
        &self,
        query: &str,
        variant: opaline::ThemeVariant,
    ) -> Option<String> {
        let (indices, selected) = self.filtered_selection(query, variant);
        indices
            .get(selected)
            .map(|index| self.entries[*index].id.clone())
    }

    pub(crate) fn next_variant(&self) -> opaline::ThemeVariant {
        match self.active_variant {
            opaline::ThemeVariant::Dark => opaline::ThemeVariant::Light,
            opaline::ThemeVariant::Light => opaline::ThemeVariant::Dark,
        }
    }

    pub(super) fn refilter(&mut self) {
        (self.filtered_indices, self.selected) =
            self.filtered_selection(&self.query, self.active_variant);
    }

    pub(super) fn set_catalog(
        &mut self,
        entries: Vec<crate::appearance::ThemeCatalogEntry>,
        notice: Option<String>,
    ) {
        if !self.catalog_initialized {
            self.active_variant = entries
                .iter()
                .find(|entry| entry.id == self.preview_theme_id)
                .map_or(opaline::ThemeVariant::Dark, |entry| entry.variant);
            self.catalog_initialized = true;
        }
        self.entries = entries;
        self.loading = false;
        self.error = None;
        self.notice = notice;
        self.catalog_request_id = None;
        self.refilter();
        if let Some(index) = self.filtered_indices.iter().position(|index| {
            self.entries
                .get(*index)
                .is_some_and(|entry| entry.id == self.preview_theme_id)
        }) {
            self.selected = index;
        }
    }

    pub(super) fn set_error(&mut self, error: String) {
        self.loading = false;
        self.catalog_request_id = None;
        self.error = Some(error);
    }

    pub(super) fn edit_query(
        &mut self,
        edit: impl FnOnce(&mut String, &mut usize),
    ) -> Option<String> {
        if self.loading
            || !matches!(
                self.save_state,
                ThemeSaveState::Idle | ThemeSaveState::Failed(_)
            )
        {
            return None;
        }
        edit(&mut self.query, &mut self.query_cursor);
        self.refilter();
        self.selected_id()
    }

    pub(super) fn move_selection(
        &mut self,
        amount: usize,
        down: bool,
        visible_rows: usize,
    ) -> Option<String> {
        if self.loading
            || !matches!(
                self.save_state,
                ThemeSaveState::Idle | ThemeSaveState::Failed(_)
            )
        {
            return None;
        }
        if !self.filtered_indices.is_empty() {
            if down {
                self.selected = self
                    .selected
                    .saturating_add(amount)
                    .min(self.filtered_indices.len() - 1);
            } else {
                self.selected = self.selected.saturating_sub(amount);
            }
        }
        self.clamp();
        let _ = visible_rows;
        self.selected_id()
    }
}