1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
use super::meili::SearchClient;
use super::{ingest, meili, DEFAULT_INDEX, DEFAULT_KEY, DEFAULT_URL};
use cargo_metadata::semver::Version;
use std::io::stdin;
use std::io::stdout;
use std::io::Write as _;
use std::ops::ControlFlow;

/// Simple terminal search client
#[derive(argh::FromArgs)]
#[argh(subcommand, name = "repl")]
pub struct Repl {
    /// name of the meilisearch index to create/query
    #[argh(positional, default = "DEFAULT_INDEX.into()")]
    index_name: String,

    #[argh(switch, description = "ingest before starting the repl")]
    ingest: bool,

    /// meilisearch URL
    #[argh(option, long = "url", default = "DEFAULT_URL.into()")]
    meilisearch_url: String,

    /// meilisearch master key (must support both read and write)
    #[argh(option, long = "master-key", default = "DEFAULT_KEY.into()")]
    meilisearch_master_key: String,

    /// release version to use in URLs
    #[argh(option, long = "release-version")]
    release_version: Option<Version>,

    /// exclude one or more crates
    #[argh(option, long = "exclude-crate")]
    exclude_crates: Vec<String>,
}

impl Repl {
    pub fn run(self) -> anyhow::Result<()> {
        let client = meili::connect(&self.meilisearch_url, &self.meilisearch_master_key)?;

        if self.ingest {
            let documents = ingest::run(
                self.release_version.clone(),
                &self.exclude_crates,
                "nightly",
            )?;
            client.index(&self.index_name, &documents)?;
        }

        let mut lines = stdin().lines();
        loop {
            stdout().write_all(b"\n> ")?;
            stdout().flush()?;

            match lines.next().transpose()? {
                Some(line) => match self.handle_line(&client, &line)? {
                    ControlFlow::Continue(_) => continue,
                    ControlFlow::Break(_) => break Ok(()),
                },
                None => break Ok(()),
            }
        }
    }

    fn handle_line(&self, search: &SearchClient, line: &str) -> anyhow::Result<ControlFlow<()>> {
        let line = line.trim();
        match line {
            "quit" | "q" | "" => return Ok(ControlFlow::Break(())),
            "reindex" => {
                let documents = ingest::run(
                    self.release_version.clone(),
                    &self.exclude_crates,
                    "nightly",
                )?;
                search.index(&self.index_name, &documents)?;
            }
            _ => {
                for result in search.query(&self.index_name, line, Some(4))? {
                    let content = result
                        .content()
                        .split('\n')
                        .map(|line| format!("   {line}"))
                        .collect::<Vec<_>>()
                        .join("\n");

                    println!("### {} [{}]", result.title(), result.url(),);
                    if content.len() > 200 {
                        println!("{}…\n", &content[..200]);
                    } else {
                        println!("{content}\n");
                    }
                }
            }
        }
        Ok(ControlFlow::Continue(()))
    }
}