shellac_codec/
lib.rs

1use capnp::{
2    message::{self, ReaderOptions},
3    serialize_packed as capn_serialize,
4};
5use serde::{Deserialize, Serialize};
6use shellac_capnp::{request::Reader as RequestReader, response::Builder as ResponseBuilder};
7use std::{
8    convert::TryInto,
9    fmt,
10    io::{self, BufRead, Write},
11};
12
13// Codec definition
14#[allow(dead_code)]
15mod shellac_capnp {
16    include!(concat!(env!("OUT_DIR"), "/shellac_capnp.rs"));
17}
18
19/// Parsing error
20#[derive(Debug)]
21pub enum Error {
22    /// The word is out of bound for the given argv length
23    WordOutOfRange { word: u16, argv_len: u32 },
24    /// Incorrect cap'n proto format
25    Capnp(capnp::Error),
26    /// The requested variable is not in the schema
27    NotInSchema(capnp::NotInSchema),
28}
29
30impl From<capnp::NotInSchema> for Error {
31    fn from(cause: capnp::NotInSchema) -> Self { Error::NotInSchema(cause) }
32}
33
34impl From<capnp::Error> for Error {
35    fn from(cause: capnp::Error) -> Self { Error::Capnp(cause) }
36}
37
38impl std::error::Error for Error {}
39impl fmt::Display for Error {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        match self {
42            Error::WordOutOfRange { word, argv_len } => write!(
43                f,
44                "the word {} can't be autocompleted because it is out of bound for argc = {}",
45                word, argv_len
46            ),
47            Error::Capnp(e) => write!(f, "{}", e),
48            Error::NotInSchema(e) => write!(f, "{}", e),
49        }
50    }
51}
52
53/// A ShellAC autocompletion request
54#[derive(Default, Clone, Debug, Hash, PartialEq, Eq, Deserialize)]
55pub struct AutocompRequest {
56    argv: Vec<String>,
57    word: u16,
58}
59
60/// A ShellAC autocompletion reply. The type parameter is to allow borrowed or owned string types
61#[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize)]
62pub struct Reply<T> {
63    pub choices: Vec<Suggestion<T>>,
64}
65
66/// One of two kind of suggestion type
67#[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize)]
68pub enum SuggestionType<T> {
69    /// A literal suggestion (ex: `-b` after git checkout)
70    Literal(T),
71    /// A command to execute while removing the provided prefix (ex: if the user typed `git
72    /// checkout mybr`, execute `git branch --no-color --list 'mybr*'` with prefix `mybr`)
73    Command { command: Vec<T>, prefix: T },
74}
75
76/// A single autocompletion suggestion
77#[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize)]
78pub struct Suggestion<T> {
79    /// The suggestion
80    suggestion: SuggestionType<T>,
81    /// It's description. May be provided to the user to indicate the effect of a given suggestion
82    /// (ex: `-b` after `git checkout` could have the description `create a new branch`)
83    description: T,
84}
85
86impl AutocompRequest {
87    /// Generate a new ShellAC autocompletion request. Panics if the word is greater than the argv
88    /// length.
89    pub fn new(argv: Vec<String>, word: u16) -> Self {
90        if word as usize >= argv.len() {
91            eprintln!(
92                "Word {} is out of bound for argv '{:?}' in ShellAC autocompletion request",
93                word, argv
94            );
95            panic!();
96        }
97        Self { argv, word }
98    }
99}
100
101impl<T> Suggestion<T> {
102    /// Generate a new suggestion to encode
103    pub const fn new(suggestion: SuggestionType<T>, description: T) -> Self {
104        Self { suggestion, description }
105    }
106
107    /// Get the associated description
108    pub const fn description(&self) -> &T { &self.description }
109
110    /// Get the associated suggestion
111    pub const fn suggestion(&self) -> &SuggestionType<T> { &self.suggestion }
112}
113
114// This is really ugly, but rust does not support impl Trait in trait bounds
115type Out<'a, E> = std::iter::Map<
116    capnp::traits::ListIter<
117        capnp::struct_list::Reader<'a, shellac_capnp::suggestion::Owned>,
118        shellac_capnp::suggestion::Reader<'a>,
119    >,
120    fn(shellac_capnp::suggestion::Reader<'a>) -> Result<(SuggestionType<&'a str>, &'a str), E>,
121>;
122
123fn convert<T: From<capnp::Error> + From<capnp::NotInSchema>>(
124    choice: shellac_capnp::suggestion::Reader,
125) -> Result<(SuggestionType<&str>, &str), T> {
126    Ok((
127        match choice.get_arg().which()? {
128            shellac_capnp::suggestion::arg::Which::Literal(lit) => SuggestionType::Literal(lit?),
129            shellac_capnp::suggestion::arg::Which::Command(cmd) => {
130                let cmd = cmd?;
131                let prefix = cmd.get_prefix()?;
132                let command = cmd.get_args()?.iter().collect::<Result<Vec<_>, _>>()?;
133                SuggestionType::Command { command, prefix }
134            }
135        },
136        choice.get_description()?,
137    ))
138}
139
140/// Read a ShellAC Server reply without (necessarily) collecting.
141///
142/// ```rust
143/// use std::io::{self, BufReader};
144///
145/// shellac::read_reply::<_, _, shellac::Error, _>(&mut BufReader::new(io::stdin()), |suggestions| {
146///     for (suggestion, description) in suggestions.map(Result::unwrap) {
147///         println!("Suggestion: '{:?}' ({})", suggestion, description);
148///     }
149///     Ok(())
150/// });
151/// ```
152pub fn read_reply<R, T, E, F>(reader: &mut R, f: F) -> Result<T, E>
153where
154    E: From<capnp::Error> + From<capnp::NotInSchema>,
155    R: BufRead,
156    F: FnOnce(Out<'_, E>) -> Result<T, E>,
157{
158    let request = capnp::serialize_packed::read_message(reader, ReaderOptions::default())?;
159
160    let choices = request.get_root::<shellac_capnp::response::Reader>()?.get_choices()?;
161    f(choices.iter().map(convert))
162}
163
164/// Write a request to a listening ShellAC server.
165///
166/// ```rust
167/// use std::io;
168/// use shellac::AutocompRequest;
169///
170/// shellac::write_request(
171///     &mut io::stdout(),
172///     &AutocompRequest::new(vec!["git".into(), "checkout".into(), "myb".into()], 2)
173/// ).unwrap();
174/// ```
175pub fn write_request<W: Write>(writer: &mut W, input: &AutocompRequest) -> Result<(), io::Error> {
176    let mut message = capnp::message::Builder::new_default();
177    let mut output = message.init_root::<shellac_capnp::request::Builder>();
178    output.set_word(input.word);
179
180    let len = input.argv.len().try_into().expect("Too many output choices");
181    let mut reply_argv = output.init_argv(len);
182    for (i, arg) in input.argv.iter().enumerate() {
183        reply_argv.reborrow().set(i as u32, arg);
184    }
185
186    capnp::serialize_packed::write_message(writer, &message)
187}
188
189/// Send a reply like the `ShellAC` server would to the other end of a the Writer, where a client
190/// would listen. You should not need this
191pub fn write_reply<'a, W: Write, T: AsRef<str> + 'a, I: IntoIterator<Item = &'a Suggestion<T>>>(
192    writer: &mut W,
193    choices: I,
194) -> Result<(), io::Error>
195where
196    I::IntoIter: ExactSizeIterator,
197{
198    let mut message = message::Builder::new_default();
199    let reply = message.init_root::<ResponseBuilder>();
200
201    let choices = choices.into_iter();
202    let mut reply_choices =
203        reply.init_choices(choices.len().try_into().expect("Too many output choices"));
204    for (i, choice) in choices.enumerate() {
205        let mut reply_choice = reply_choices.reborrow().get(i as u32);
206        match choice.suggestion() {
207            SuggestionType::Literal(lit) => {
208                reply_choice.reborrow().init_arg().set_literal(lit.as_ref())
209            }
210            SuggestionType::Command { command, prefix } => {
211                let mut builder = reply_choice.reborrow().init_arg().init_command();
212                builder.set_prefix(prefix.as_ref());
213                let mut args = builder.init_args(command.len() as u32);
214                for (i, arg) in command.iter().enumerate() {
215                    args.set(i as u32, arg.as_ref());
216                }
217            }
218        }
219        reply_choice.set_description(choice.description().as_ref());
220    }
221
222    capn_serialize::write_message(writer, &message)
223}
224
225/// Read a `ShellAC` Request without allocating. You should not need this
226pub fn read_request<
227    'a,
228    R: BufRead + 'a,
229    T,
230    E: From<Error>,
231    F: FnOnce(u16, capnp::text_list::Reader<'_>, &RequestReader) -> Result<T, E>,
232>(
233    reader: &mut R,
234    f: F,
235) -> Result<T, E> {
236    let request =
237        capn_serialize::read_message(reader, ReaderOptions::default()).map_err(Into::into)?;
238    let request = request.get_root::<RequestReader>().map_err(Into::into)?;
239
240    let argv = request.get_argv().map_err(Into::into)?;
241    let word = request.get_word();
242
243    if u32::from(word) > argv.len() {
244        Err(Error::WordOutOfRange { word, argv_len: argv.len() }.into())
245    } else {
246        f(word, argv, &request)
247    }
248}