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
pub use command::{
AppCommand, Autocomplete, AutocompleteSuggestion, Command, CommandAlias, CommandMatches,
ContextAwareParse, Runnable,
};
pub use meta::AppMeta;
#[cfg(test)]
pub use command::assert_autocomplete;
mod command;
mod meta;
use crate::storage::backup::{import, BackupData};
use crate::utils::CaseInsensitiveStr;
use initiative_macros::motd;
/// The application wrapper. Its inner [`AppMeta`] object holds metadata associated with the
/// application, including ephemeral storage of journal entries and the object representing the
/// underlying data storage.
///
/// The main methods of interest are [`App::command`], called when a command is run by the user,
/// and [`App::autocomplete`], called to get a list of suggestions for a given input.
#[derive(Debug)]
pub struct App {
meta: AppMeta,
}
/// An event that can occur while the app is running that may require special handling by the UI.
#[derive(Debug)]
pub enum Event {
/// The user typed the `export` command and the journal backup is ready to download.
Export(BackupData),
/// The user typed the `import` command and should be prompted to select a file to import.
Import,
}
impl App {
pub fn new(meta: AppMeta) -> App {
App { meta }
}
/// Initialize a running application. This is done as a separate step from the constructor
/// because it runs asynchronously. Its purpose, in turn, is to trigger the underlying data
/// store to initialize, which may involve opening a database connection.
pub async fn init(&mut self) -> &'static str {
self.meta.repository.init().await;
let (motd, motd_len) = motd!("! Local storage is not available in your browser. You will be able to use initiative.sh, but anything you save will not persist beyond this session.");
if self.meta.repository.data_store_enabled() {
&motd[..motd_len]
} else {
motd
}
}
/// The user typed an input and pressed Enter. What happens?
///
/// On success or failure, returns a String that can be displayed back to the user.
pub async fn command(&mut self, input: &str) -> Result<String, String> {
Command::parse_input_irrefutable(input, &self.meta)
.await
.run(input, &mut self.meta)
.await
}
/// The user has updated their input and a new set of suggestions should be populated. This
/// consists of a `Vec` of tuples; the first entry being the text that the user is suggested to
/// type, the second being a brief (1-3--word) description of what that input will do. `Cow` is
/// used here to allow either `String` or `&'static str`, whatever is appropriate to a given
/// case.
///
/// Returns a maximum of 10 results.
pub async fn autocomplete(&self, input: &str) -> Vec<AutocompleteSuggestion> {
let mut suggestions: Vec<_> = Command::autocomplete(input, &self.meta).await;
suggestions.sort_by(|a, b| a.term.cmp_ci(&b.term));
suggestions.truncate(10);
suggestions
}
/// The part of the import flow that occurs after the user selects a file in response to the
/// [`Event::Import`].
pub async fn bulk_import(&mut self, data: BackupData) -> Result<String, String> {
import(&mut self.meta.repository, data)
.await
.map(|stats| stats.to_string())
.map_err(|_| "Failed to import.".to_string())
}
}