binocular-cli 0.2.3

Not exactly a telescope, but it's useful sometimes. TUI to search/navigate through files and workspaces.
Documentation
use crate::preview::request::command::execute_preview_command;
use crate::preview::request::diff::build_diff_preview;
use crate::preview::request::git::{
    branch::build_git_branch_preview, commit::build_git_commit_preview,
    history::build_history_preview,
};
use crate::preview::{build_path_preview, PreviewContent, PreviewRequest};
use ratatui::text::Text;
use ratatui_image::picker::Picker;

pub(crate) enum PreviewExecution {
    Completed(PreviewRequest, PreviewContent),
    Superseded(PreviewRequest),
}
pub(crate) struct PreviewExecutor {
    picker: Picker,
    preview_command: Option<String>,
    delimiter: String,
    log_max_entries: usize,
}

impl PreviewExecutor {
    pub(crate) fn new(
        picker: Picker,
        preview_command: Option<String>,
        delimiter: String,
        log_max_entries: usize,
    ) -> Self {
        Self {
            picker,
            preview_command,
            delimiter,
            log_max_entries,
        }
    }

    pub(crate) fn execute<F>(
        &self,
        request: PreviewRequest,
        mut poll_replacement: F,
    ) -> PreviewExecution
    where
        F: FnMut() -> Option<PreviewRequest>,
    {
        match request {
            PreviewRequest::Path { source, path } => self.execute_builtin_preview(
                PreviewRequest::Path {
                    source,
                    path: path.clone(),
                },
                &path,
            ),
            PreviewRequest::Diff {
                source,
                left,
                right,
            } => {
                let preview = build_diff_preview(&left, &right);
                PreviewExecution::Completed(
                    PreviewRequest::Diff {
                        source,
                        left,
                        right,
                    },
                    preview,
                )
            }
            PreviewRequest::GitHistory {
                source,
                repo_root,
                commit,
                path,
                line,
            } => {
                let preview = build_history_preview(&repo_root, &commit, &path);
                PreviewExecution::Completed(
                    PreviewRequest::GitHistory {
                        source,
                        repo_root,
                        commit,
                        path,
                        line,
                    },
                    preview,
                )
            }
            PreviewRequest::GitBranch {
                source,
                repo_root,
                branch,
            } => {
                let preview = build_git_branch_preview(&repo_root, &branch);
                PreviewExecution::Completed(
                    PreviewRequest::GitBranch {
                        source,
                        repo_root,
                        branch,
                    },
                    preview,
                )
            }
            PreviewRequest::GitCommit {
                source,
                repo_root,
                commit,
            } => {
                let preview = build_git_commit_preview(&repo_root, &commit);
                PreviewExecution::Completed(
                    PreviewRequest::GitCommit {
                        source,
                        repo_root,
                        commit,
                    },
                    preview,
                )
            }
            PreviewRequest::Grep {
                source,
                path,
                line,
                text,
            } => self.execute_builtin_preview(
                PreviewRequest::Grep {
                    source,
                    path: path.clone(),
                    line,
                    text,
                },
                &path,
            ),
            PreviewRequest::StructuredLog { source, path } => self.execute_builtin_preview(
                PreviewRequest::StructuredLog {
                    source,
                    path: path.clone(),
                },
                &path,
            ),
            PreviewRequest::StdinOrCommand { source, item } => {
                let request = PreviewRequest::StdinOrCommand {
                    source,
                    item: item.clone(),
                };
                if let Some(command) = self.preview_command.as_deref() {
                    execute_preview_command(
                        request,
                        &item,
                        command,
                        &self.delimiter,
                        &mut poll_replacement,
                    )
                } else {
                    PreviewExecution::Completed(request, PreviewContent::PlainText(Text::default()))
                }
            }
        }
    }

    fn execute_builtin_preview(&self, request: PreviewRequest, path: &str) -> PreviewExecution {
        let preview = build_path_preview(path, &self.picker, self.log_max_entries);
        PreviewExecution::Completed(request, preview)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::preview::request::git::ansi::parse_ansi_text;
    use crate::preview::PreviewSource;
    use crate::search::types::SearchItem;
    use ratatui::style::Color;
    use std::sync::atomic::{AtomicUsize, Ordering};

    fn command_request(item: &str) -> PreviewRequest {
        PreviewRequest::StdinOrCommand {
            source: PreviewSource::SearchItem(SearchItem::stdin(item)),
            item: item.to_string(),
        }
    }

    #[test]
    fn preview_command_replacement_returns_latest_request() {
        let executor = PreviewExecutor::new(
            Picker::halfblocks(),
            Some("sh -c 'sleep 1'".to_string()),
            ":".to_string(),
            100_000,
        );
        let replacement = command_request("replacement");
        let polls = AtomicUsize::new(0);

        let outcome = executor.execute(command_request("initial"), || {
            if polls.fetch_add(1, Ordering::Relaxed) == 0 {
                Some(replacement.clone())
            } else {
                None
            }
        });

        match outcome {
            PreviewExecution::Superseded(request) => assert_eq!(request, replacement),
            PreviewExecution::Completed(_, _) => panic!("expected replacement"),
        }
    }

    #[test]
    fn preview_command_timeout_surfaces_message() {
        let executor = PreviewExecutor::new(
            Picker::halfblocks(),
            Some("sh -c 'sleep 3'".to_string()),
            ":".to_string(),
            100_000,
        );

        let outcome = executor.execute(command_request("initial"), || None);

        match outcome {
            PreviewExecution::Completed(_, PreviewContent::PlainText(text)) => {
                let rendered = text
                    .lines
                    .iter()
                    .map(|line| line.to_string())
                    .collect::<Vec<_>>()
                    .join("\n");
                assert!(rendered.contains("timed out"));
            }
            PreviewExecution::Completed(_, _) => panic!("expected plain text timeout"),
            PreviewExecution::Superseded(_) => panic!("expected timeout"),
        }
    }

    #[test]
    fn ansi_empty_reset_code_resets_style_before_next_line() {
        let text = parse_ansi_text("\u{1b}[31m--\u{1b}[m\nfile.rs\n".to_string());

        assert_eq!(text.lines.len(), 2);
        assert_eq!(text.lines[0].spans.len(), 1);
        assert_eq!(text.lines[0].spans[0].content, "--");
        assert_eq!(text.lines[0].spans[0].style.fg, Some(Color::Red));

        assert_eq!(text.lines[1].spans.len(), 1);
        assert_eq!(text.lines[1].spans[0].content, "file.rs");
        assert_ne!(text.lines[1].spans[0].style.fg, Some(Color::Red));
    }
}