parley-cli 0.2.0

Terminal-first review tool for AI-generated code changes
Documentation
use super::*;

impl TuiApp {
    pub(super) async fn handle_settings_editor_key(
        &mut self,
        key: KeyEvent,
        service: &ReviewService,
    ) -> Result<()> {
        if matches!(key.code, KeyCode::Esc) {
            self.settings_editor = None;
            self.status_line = "settings edit cancelled".into();
            return Ok(());
        }
        if matches!(key.code, KeyCode::Enter) {
            return self.save_settings_editor(service).await;
        }

        let Some(editor) = self.settings_editor.as_mut() else {
            return Ok(());
        };

        match key.code {
            KeyCode::Left => {
                editor.cursor_col = editor.cursor_col.saturating_sub(1);
            }
            KeyCode::Right => {
                editor.cursor_col = (editor.cursor_col + 1).min(editor.value.chars().count());
            }
            KeyCode::Home => editor.cursor_col = 0,
            KeyCode::End => editor.cursor_col = editor.value.chars().count(),
            KeyCode::Backspace if editor.cursor_col > 0 => {
                remove_char_at(&mut editor.value, editor.cursor_col - 1);
                editor.cursor_col -= 1;
            }
            KeyCode::Delete if editor.cursor_col < editor.value.chars().count() => {
                remove_char_at(&mut editor.value, editor.cursor_col);
            }
            KeyCode::Char(ch)
                if key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT =>
            {
                insert_char_at(&mut editor.value, editor.cursor_col, ch);
                editor.cursor_col += 1;
            }
            _ => {}
        }

        Ok(())
    }

    pub(super) async fn handle_theme_picker_key(
        &mut self,
        key: KeyEvent,
        service: &ReviewService,
    ) -> Result<()> {
        if matches!(key.code, KeyCode::Esc) {
            self.theme_picker = None;
            self.status_line = "theme picker closed".into();
            return Ok(());
        }
        if matches!(key.code, KeyCode::Enter) {
            return self.apply_theme_picker_selection(service).await;
        }

        let Some(picker) = self.theme_picker.as_mut() else {
            return Ok(());
        };
        match key.code {
            KeyCode::Up | KeyCode::Char('k') => {
                picker.selected_index = picker.selected_index.saturating_sub(1);
            }
            KeyCode::Down | KeyCode::Char('j') => {
                let max_index = self.themes.len().saturating_sub(1);
                picker.selected_index = (picker.selected_index + 1).min(max_index);
            }
            KeyCode::Home | KeyCode::Char('g') => {
                picker.selected_index = 0;
            }
            KeyCode::End => {
                picker.selected_index = self.themes.len().saturating_sub(1);
            }
            KeyCode::PageUp => {
                picker.selected_index = picker.selected_index.saturating_sub(8);
            }
            KeyCode::PageDown => {
                let max_index = self.themes.len().saturating_sub(1);
                picker.selected_index = (picker.selected_index + 8).min(max_index);
            }
            KeyCode::Char('G') if key.modifiers.contains(KeyModifiers::SHIFT) => {
                picker.selected_index = self.themes.len().saturating_sub(1);
            }
            _ => {}
        }
        if picker.selected_index < picker.scroll {
            picker.scroll = picker.selected_index;
        }
        let lower_bound = picker.scroll.saturating_add(8);
        if picker.selected_index > lower_bound {
            picker.scroll = picker.selected_index.saturating_sub(8);
        }
        Ok(())
    }

    pub(super) async fn handle_commit_picker_key(
        &mut self,
        key: KeyEvent,
        service: &ReviewService,
    ) -> Result<()> {
        if matches!(key.code, KeyCode::Esc) {
            self.commit_picker = None;
            self.status_line = "commit picker closed".into();
            return Ok(());
        }

        if matches!(key.code, KeyCode::Enter) {
            let filtered = self.commit_picker_filtered_indices();
            let Some(picker) = self.commit_picker.as_ref() else {
                return Ok(());
            };
            if filtered.is_empty() {
                self.status_line = "no commits match the current search".into();
                return Ok(());
            }
            let selected = picker.selected_index.min(filtered.len().saturating_sub(1));
            let commit = picker
                .commits
                .get(filtered[selected])
                .cloned()
                .context("selected commit is unavailable")?;
            self.commit_picker = None;
            self.diff_source = crate::git::diff::DiffSource::Commit { rev: commit.oid };
            self.refresh_review_and_diff(service).await?;
            self.status_line = format!("diff source set to {}", commit.short_oid);
            return Ok(());
        }

        let filtered_len = self.commit_picker_filtered_indices().len();
        let Some(picker) = self.commit_picker.as_mut() else {
            return Ok(());
        };
        match key.code {
            KeyCode::Up => {
                picker.selected_index = picker.selected_index.saturating_sub(1);
            }
            KeyCode::Down => {
                let max_index = filtered_len.saturating_sub(1);
                picker.selected_index = (picker.selected_index + 1).min(max_index);
            }
            KeyCode::Home => {
                picker.selected_index = 0;
            }
            KeyCode::End => {
                picker.selected_index = filtered_len.saturating_sub(1);
            }
            KeyCode::PageUp => {
                picker.selected_index = picker.selected_index.saturating_sub(8);
            }
            KeyCode::PageDown => {
                let max_index = filtered_len.saturating_sub(1);
                picker.selected_index = (picker.selected_index + 8).min(max_index);
            }
            KeyCode::Left => {
                picker.cursor_col = picker.cursor_col.saturating_sub(1);
            }
            KeyCode::Right => {
                picker.cursor_col = (picker.cursor_col + 1).min(picker.query.chars().count());
            }
            KeyCode::Char('a') if key.modifiers.contains(KeyModifiers::CONTROL) => {
                picker.cursor_col = 0;
            }
            KeyCode::Char('e') if key.modifiers.contains(KeyModifiers::CONTROL) => {
                picker.cursor_col = picker.query.chars().count();
            }
            KeyCode::Backspace if picker.cursor_col > 0 => {
                remove_char_at(&mut picker.query, picker.cursor_col - 1);
                picker.cursor_col -= 1;
                picker.selected_index = 0;
                picker.scroll = 0;
            }
            KeyCode::Delete if picker.cursor_col < picker.query.chars().count() => {
                remove_char_at(&mut picker.query, picker.cursor_col);
                picker.selected_index = 0;
                picker.scroll = 0;
            }
            KeyCode::Char(ch)
                if key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT =>
            {
                insert_char_at(&mut picker.query, picker.cursor_col, ch);
                picker.cursor_col += 1;
                picker.selected_index = 0;
                picker.scroll = 0;
            }
            _ => {}
        }

        let refreshed_len = self.commit_picker_filtered_indices().len();
        if let Some(picker) = self.commit_picker.as_mut() {
            if refreshed_len == 0 {
                picker.selected_index = 0;
                picker.scroll = 0;
            } else {
                picker.selected_index = picker.selected_index.min(refreshed_len.saturating_sub(1));
                if picker.selected_index < picker.scroll {
                    picker.scroll = picker.selected_index;
                }
                let lower_bound = picker.scroll.saturating_add(8);
                if picker.selected_index > lower_bound {
                    picker.scroll = picker.selected_index.saturating_sub(8);
                }
            }
        }
        Ok(())
    }

    pub(super) async fn handle_review_picker_key(
        &mut self,
        key: KeyEvent,
        service: &ReviewService,
    ) -> Result<()> {
        if matches!(key.code, KeyCode::Esc) {
            self.review_picker = None;
            self.status_line = "review picker closed".into();
            return Ok(());
        }

        if matches!(key.code, KeyCode::Enter) {
            let filtered = self.review_picker_filtered_indices();
            let Some(picker) = self.review_picker.as_ref() else {
                return Ok(());
            };
            if filtered.is_empty() {
                let name = picker.query.trim().to_string();
                self.review_picker = None;
                self.settings_editor = Some(SettingsEditorState {
                    kind: SettingsEditorKind::CreateReview,
                    cursor_col: name.chars().count(),
                    value: name,
                });
                self.status_line = "creating review".into();
                return Ok(());
            }
            let selected = picker.selected_index.min(filtered.len().saturating_sub(1));
            let review = picker
                .reviews
                .get(filtered[selected])
                .cloned()
                .context("selected review is unavailable")?;
            self.review_picker = None;
            self.review_name = review.name.clone();
            self.log_path = service.review_log_path(&self.review_name)?;
            self.reload_review(service).await?;
            self.status_line = format!("review context set to {}", review.name);
            return Ok(());
        }

        let filtered_len = self.review_picker_filtered_indices().len();
        let Some(picker) = self.review_picker.as_mut() else {
            return Ok(());
        };
        match key.code {
            KeyCode::Up => {
                picker.selected_index = picker.selected_index.saturating_sub(1);
            }
            KeyCode::Down => {
                let max_index = filtered_len.saturating_sub(1);
                picker.selected_index = (picker.selected_index + 1).min(max_index);
            }
            KeyCode::Home => {
                picker.selected_index = 0;
            }
            KeyCode::End => {
                picker.selected_index = filtered_len.saturating_sub(1);
            }
            KeyCode::PageUp => {
                picker.selected_index = picker.selected_index.saturating_sub(8);
            }
            KeyCode::PageDown => {
                let max_index = filtered_len.saturating_sub(1);
                picker.selected_index = (picker.selected_index + 8).min(max_index);
            }
            KeyCode::Left => {
                picker.cursor_col = picker.cursor_col.saturating_sub(1);
            }
            KeyCode::Right => {
                picker.cursor_col = (picker.cursor_col + 1).min(picker.query.chars().count());
            }
            KeyCode::Char('a') if key.modifiers.contains(KeyModifiers::CONTROL) => {
                picker.cursor_col = 0;
            }
            KeyCode::Char('e') if key.modifiers.contains(KeyModifiers::CONTROL) => {
                picker.cursor_col = picker.query.chars().count();
            }
            KeyCode::Backspace if picker.cursor_col > 0 => {
                remove_char_at(&mut picker.query, picker.cursor_col - 1);
                picker.cursor_col -= 1;
                picker.selected_index = 0;
                picker.scroll = 0;
            }
            KeyCode::Delete if picker.cursor_col < picker.query.chars().count() => {
                remove_char_at(&mut picker.query, picker.cursor_col);
                picker.selected_index = 0;
                picker.scroll = 0;
            }
            KeyCode::Char(ch)
                if key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT =>
            {
                insert_char_at(&mut picker.query, picker.cursor_col, ch);
                picker.cursor_col += 1;
                picker.selected_index = 0;
                picker.scroll = 0;
            }
            _ => {}
        }

        let refreshed_len = self.review_picker_filtered_indices().len();
        if let Some(picker) = self.review_picker.as_mut() {
            if refreshed_len == 0 {
                picker.selected_index = 0;
                picker.scroll = 0;
            } else {
                picker.selected_index = picker.selected_index.min(refreshed_len.saturating_sub(1));
                if picker.selected_index < picker.scroll {
                    picker.scroll = picker.selected_index;
                }
                let lower_bound = picker.scroll.saturating_add(8);
                if picker.selected_index > lower_bound {
                    picker.scroll = picker.selected_index.saturating_sub(8);
                }
            }
        }
        Ok(())
    }
}