initiative_core/app/mod.rs
1pub use command::{
2 AppCommand, Autocomplete, AutocompleteSuggestion, Command, CommandAlias, CommandMatches,
3 ContextAwareParse, Runnable,
4};
5pub use meta::AppMeta;
6
7mod command;
8mod meta;
9
10use crate::storage::backup::{import, BackupData};
11use crate::utils::CaseInsensitiveStr;
12use initiative_macros::motd;
13
14/// The application wrapper. Its inner [`AppMeta`] object holds metadata associated with the
15/// application, including ephemeral storage of journal entries and the object representing the
16/// underlying data storage.
17///
18/// The main methods of interest are [`App::command`], called when a command is run by the user,
19/// and [`App::autocomplete`], called to get a list of suggestions for a given input.
20#[derive(Debug)]
21pub struct App {
22 meta: AppMeta,
23}
24
25/// An event that can occur while the app is running that may require special handling by the UI.
26#[derive(Debug)]
27pub enum Event {
28 /// The user typed the `export` command and the journal backup is ready to download.
29 Export(BackupData),
30
31 /// The user typed the `import` command and should be prompted to select a file to import.
32 Import,
33}
34
35impl App {
36 pub fn new(meta: AppMeta) -> App {
37 App { meta }
38 }
39
40 /// Initialize a running application. This is done as a separate step from the constructor
41 /// because it runs asynchronously. Its purpose, in turn, is to trigger the underlying data
42 /// store to initialize, which may involve opening a database connection.
43 pub async fn init(&mut self) -> &'static str {
44 self.meta.repository.init().await;
45 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.");
46
47 if self.meta.repository.data_store_enabled() {
48 &motd[..motd_len]
49 } else {
50 motd
51 }
52 }
53
54 /// The user typed an input and pressed Enter. What happens?
55 ///
56 /// On success or failure, returns a String that can be displayed back to the user.
57 pub async fn command(&mut self, input: &str) -> Result<String, String> {
58 Command::parse_input_irrefutable(input, &self.meta)
59 .await
60 .run(input, &mut self.meta)
61 .await
62 }
63
64 /// The user has updated their input and a new set of suggestions should be populated. This
65 /// consists of a `Vec` of tuples; the first entry being the text that the user is suggested to
66 /// type, the second being a brief (1-3--word) description of what that input will do. `Cow` is
67 /// used here to allow either `String` or `&'static str`, whatever is appropriate to a given
68 /// case.
69 ///
70 /// Returns a maximum of 10 results.
71 pub async fn autocomplete(&self, input: &str) -> Vec<AutocompleteSuggestion> {
72 let mut suggestions: Vec<_> = Command::autocomplete(input, &self.meta).await;
73 suggestions.sort_by(|a, b| a.term.cmp_ci(&b.term));
74 suggestions.truncate(10);
75 suggestions
76 }
77
78 /// The part of the import flow that occurs after the user selects a file in response to the
79 /// [`Event::Import`].
80 pub async fn bulk_import(&mut self, data: BackupData) -> Result<String, String> {
81 import(&mut self.meta.repository, data)
82 .await
83 .map(|stats| stats.to_string())
84 .map_err(|_| "Failed to import.".to_string())
85 }
86}