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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
// Copyright (C) 2023 Andreas Hartmann <hartan@7x.de>
// GNU General Public License v3.0+ (https://www.gnu.org/licenses/gpl-3.0.txt)
// SPDX-License-Identifier: GPL-3.0-or-later

//! # Command providers
//!
//! Command providers can be anything from where a command could be installed. These differ from
//! the environments in that a provider merely tells whether a program can be found there or not,
//! but it doesn't provide the ability to immediately run the program. On the other hand, an
//! execution environment is something that allows the active execution of commands.
//!
//! Generally speaking, a provider is any sort of package manager (`cargo`, `pip`, `apt`, `dnf`,
//! ...).
//!
//! Execution environments may have zero or more providers available. Containers usually don't have
//! providers, whereas a host operating system will offer some means of installing packages.
//!
//!
//! ## Implementing a new provider
//!
//! The following instructions relate to writing a new provider in Rust, with the goal of inclusion
//! into the application in mind. Of course you don't have to submit your own provider for
//! inclusion if you don't want to. Similarly, you can write a [`mod@custom`] provider first and
//! then open an issue to discuss long-term reimplementation and inclusion in the application.
//!
//! Here are the mandatory steps you must take to write a new provider:
//!
//! - Create a new module file in the `providers` module (here) and name if after the desired
//!   provider.
//! - Next, in the `providers` root module:
//!     1. Include your new module (`pub mod ...`)
//!     2. Import the module into the code (`use ...`)
//!     3. Add your new module to the [`Provider`] struct
//!     4. Pass through the [`fmt::Display`] of your new module for [`Provider`]
//! - In your new provider module:
//!     1. Import the prelude: `use crate::provider::prelude::*;`
//!     2. Create a new type (struct) for your provider with any internal state you need (or none
//!        at all)
//!     3. Implement [`fmt::Display`], **and please adhere to the projects naming convention**. For
//!        example, `dnf` is usually referred to as DNF.
//!     4. Implement [`IsProvider`] for your new provider
//!     5. (Optional) Write some tests, in case your provider can be tested
//! - In `src/lib.rs`:
//!     1. Add your provider into the `providers` vector
//!
//! Following you will find a set of guidelines to help you in the process of writing a robust
//! provider. It's written as a checklist, so feel free to copy it and check the boxes if you want.
//!
//! - Interacting with the provider:
//!     - [ ] Test all the different existent queries you can find (i.e. when your provider
//!           returns a successful result for existing applications). Some providers change their
//!           output format based on what they find, for example by adding optional fields.
//!     - [ ] Test a non-existent search query (Personally I like 'asdwasda' for that purpose) and
//!           return such errors as [`ProviderError::NotFound`]
//!     - [ ] When pre-conditions for your provider aren't fulfilled (i.e. it needs a specific
//!           application/package manager), return a [`ProviderError::Requirements`]
//!     - [ ] Prefer offline operation and fall back to online operation only when e.g. package
//!           manager caches don't exist. Log an info message in such cases so the user can deduce
//!           from the log why searching took longer than expected.
//!     - [ ] Prefer parsing results from machine-readable output when possible
//! - Error handling:
//!     - [ ] **Never cause a panic** by calling e.g. [`unwrap()`](`Result::unwrap()`): Prefer
//!           wrapping errors into [`anyhow::Error`] and returning that as
//!           [`ProviderError::ApplicationError`] instead. Panics ruin the TUI output and don't add
//!           any value for the end-user.
//!     - [ ] If you want to recover from errors, consider writing a concrete error type for your
//!           provider
//!     - [ ] Check if [`ProviderError`] already has the error variant you need and reuse that
//!           before reimplementing your own version of it
//! - Other things:
//!     - [ ] A [`Candidate`] must have *at least* its' `package` and `actions.execute` fields
//!           populated. Try to populate as many as you can.
//!     - [ ] Prefer a precise search and returning only few relevant results over generic searches
//!           with many results. Built-in providers currently try to stick with less or equal 10
//!           results each.
//!     - [ ] Try to check whether any of the results you find are already installed. Most package
//!           managers will happily report that e.g. `coreutils` provides the `ls` command, but
//!           `coreutils` is likely already installed.
//!
//! Feel free to look at some of the existing providers as a reference.
pub mod apt;
pub mod cargo;
pub mod custom;
pub mod cwd;
pub mod dnf;
pub mod flatpak;
pub mod pacman;
pub mod path;

use crate::error::prelude::*;
use async_std::task;
use enum_dispatch::enum_dispatch;
use prelude::*;
use serde_derive::Deserialize;

use apt::Apt;
use cargo::Cargo;
use custom::Custom;
use cwd::Cwd;
use dnf::Dnf;
use flatpak::Flatpak;
use pacman::Pacman;
use path::Path;

pub(crate) mod prelude {
    pub use crate::{
        environment::{Environment, ExecutionError, IsEnvironment},
        provider::{Actions, Candidate, Error as ProviderError, IsProvider, Provider, Query},
        util::{cmd, CommandLine, OutputMatcher},
    };

    pub use anyhow::{anyhow, Context};
    pub use async_trait::async_trait;
    pub use logerr::LoggableError;
    pub use thiserror::Error as ThisError;

    pub use std::{fmt, sync::Arc};

    pub type ProviderResult<T> = std::result::Result<T, ProviderError>;
}

/// A command provider.
#[async_trait]
#[enum_dispatch]
pub trait IsProvider: std::fmt::Debug + std::fmt::Display {
    /// Search for a command with this provider in `env`.
    async fn search_internal(
        &self,
        command: &str,
        target_env: Arc<crate::environment::Environment>,
    ) -> ProviderResult<Vec<Candidate>>;
}

/// Search for `command` inside the given [`IsProvider`], targeting a specific [`Environment`].
///
/// Calls the [`IsProvider`]s search function and wraps the result in a more digestible [`Query`]
/// instance.
pub async fn search_in(
    provider: Arc<Provider>,
    command: &str,
    target_env: Arc<Environment>,
) -> Query {
    let results =
        match IsProvider::search_internal(provider.as_ref(), command, target_env.clone()).await {
            Ok(results) if !results.is_empty() => Ok(results),
            // Provider didn't error but turned up no results either, so this is a "not found"
            Ok(_) => Err(ProviderError::NotFound(command.to_string())),
            Err(error) => Err(error),
        };

    Query {
        env: target_env,
        provider,
        term: command.to_string(),
        results,
    }
}

/// Wrapper type for everything implementing [`IsProvider`].
#[enum_dispatch(IsProvider)]
#[derive(Debug, PartialEq)]
pub enum Provider {
    Apt,
    Cargo,
    Custom,
    Cwd,
    Dnf,
    Flatpak,
    Pacman,
    Path,
}

impl fmt::Display for Provider {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Apt(val) => write!(f, "{}", val),
            Self::Cargo(val) => write!(f, "{}", val),
            Self::Custom(val) => write!(f, "{}", val),
            Self::Cwd(val) => write!(f, "{}", val),
            Self::Dnf(val) => write!(f, "{}", val),
            Self::Flatpak(val) => write!(f, "{}", val),
            Self::Pacman(val) => write!(f, "{}", val),
            Self::Path(val) => write!(f, "{}", val),
        }
    }
}

/// A convenient representation of a search query with its' results.
///
/// Refer to [`search_in`] to see how to obtain a `Query` conveniently.
#[derive(Debug)]
pub struct Query {
    /// The environment where the query was executed
    pub env: Arc<crate::environment::Environment>,
    /// The provider that generated this query result
    pub provider: Arc<Provider>,
    /// The term that was searched for
    pub term: String,
    /// Result of the query
    pub results: ProviderResult<Vec<Candidate>>,
}

impl Query {
    /// Install result number `index`.
    ///
    /// Picks the [`Candidate`] at the given `index` if it exists, and attempts to execute their
    /// `install` action, if one is provided.
    pub fn install(&self, index: usize) -> Result<()> {
        let err_context = || {
            format!(
                "failed to install package '{}' from {} inside {}",
                self.term, self.provider, self.env,
            )
        };

        let results = self
            .results
            .as_ref()
            .or_else(|error| {
                Err(anyhow::anyhow!("{}", error)).with_context(|| {
                    format!("provider '{}' doesn't offer results to run", self.provider)
                })
            })
            .with_context(err_context)?;
        let candidate = results
            .get(index)
            .with_context(|| format!("requested candidate {} doesn't exist", index))
            .with_context(err_context)?;

        if let Some(ref command) = candidate.actions.install {
            || -> anyhow::Result<()> {
                let mut command = command.clone();
                // Installation is a process triggered by the user and runs interactively. Give the
                // user a change to enter passwords etc.
                command.is_interactive(true);
                task::block_on(self.env.execute(command)?.status())
                    .map(|_| ())
                    .map_err(anyhow::Error::new)
            }()
            .with_context(err_context)
            .map_err(CnfError::ApplicationError)?;
        }
        Ok(())
    }

    /// Execute result number `index`.
    ///
    /// Picks the [`Candidate`] at the given `index` if it exists, and attempts to execute their
    /// `execute` action, if one is provided.
    pub fn run(&self, index: usize, args: &[&str]) -> Result<()> {
        let err_context = || {
            format!(
                "failed to run package '{}' from {} inside {}",
                self.term, self.provider, self.env,
            )
        };

        let results = self
            .results
            .as_ref()
            .or_else(|error| {
                Err(anyhow::anyhow!("{}", error)).with_context(|| {
                    format!("provider '{}' doesn't offer results to run", self.provider)
                })
            })
            .with_context(err_context)?;
        let candidate = results
            .get(index)
            .with_context(|| format!("requested candidate {} doesn't exist", index))
            .with_context(err_context)?;
        let command = &candidate.actions.execute;

        || -> anyhow::Result<()> {
            let mut cmd = command.clone();
            cmd.append(args);

            task::block_on(self.env.execute(cmd)?.status())
                .map(|_| ())
                .map_err(anyhow::Error::new)
        }()
        .with_context(err_context)
        .map_err(CnfError::ApplicationError)
    }
}

/// Potential candidate for a command.
#[derive(Debug, Default, Deserialize)]
pub struct Candidate {
    /// Name of the package/command that provides "command" to be found
    pub package: String,
    /// Description of the package/command
    #[serde(default)]
    pub description: String,
    /// Version of the package/command
    #[serde(default)]
    pub version: String,
    /// Origin of the package/command
    #[serde(default)]
    pub origin: String,
    /// Actions available on this candidate
    pub actions: Actions,
}

impl fmt::Display for Candidate {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        macro_rules! write_format {
            ($string:expr, $var:expr) => {
                writeln!(
                    f,
                    "{:>14}: {}",
                    $string,
                    if $var.is_empty() { "n/a" } else { $var }
                )
            };
        }

        write_format!("Package", &self.package)?;
        write_format!("Description", &self.description)?;
        write_format!("Version", &self.version)?;
        write_format!("Origin", &self.origin)?;
        write_format!(
            "Needs install",
            if self.actions.install.is_some() {
                "yes"
            } else {
                "no"
            }
        )
    }
}

/// Action specification for a [Candidate].
#[derive(Debug, Default, Deserialize)]
pub struct Actions {
    /// Action to install a [Candidate]. If this is `None`, the candidate doesn't require
    /// installing (most likely because it is already installed/available). A `Some(_)` contains
    /// the command to execute in order to install the [Candidate].
    #[serde(default)]
    pub install: Option<CommandLine>,
    /// Action to execute a [Candidate]. Must contain the command required to execute this
    /// candidate
    pub execute: CommandLine,
}

#[derive(ThisError, Debug)]
pub enum Error {
    /// Requested executable couldn't be found.
    ///
    /// Return this error if the executable that `cnf` is queried for cannot be found in a
    /// provider. If the provider has other issues (i.e. a certain package manager isn't
    /// present), return a [`Requirements`][ProviderError::Requirements] error instead.
    #[error("command not found: '{0}'")]
    NotFound(String),

    /// Provider requirements aren't fulfilled (e.g. some binary/package manager is missing).
    ///
    /// Please provide a human-readable, actionable description of what the respective provider
    /// requires.
    #[error("requirement not fulfilled: '{0}'")]
    Requirements(String),

    /// Transparent error from any source.
    #[error(transparent)]
    ApplicationError(#[from] anyhow::Error),

    /// Required feature not implemented yet.
    #[error("please implement '{0}' first!")]
    NotImplemented(String),

    #[error(transparent)]
    Execution(ExecutionError),
}

impl From<CnfError> for ProviderError {
    fn from(value: CnfError) -> Self {
        match value {
            CnfError::NotFound(val) => Self::NotFound(val),
            _ => Self::ApplicationError(anyhow::Error::new(value)),
        }
    }
}

impl From<ExecutionError> for ProviderError {
    fn from(value: ExecutionError) -> Self {
        match value {
            ExecutionError::NotFound(val) => ProviderError::Requirements(val),
            _ => ProviderError::Execution(value),
        }
    }
}