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
use super::{data, error, io, ui, ui::Screen};
use ratatui::prelude::*;
/// The main state of the application.
/// Consists of a select screen that is always existent, a stack of notes the user has navigated through and that he can navigate through by popping, reversing its navigation. Lastly, there is a display screen of the currently displayed note, which should always correspond to the top of the stack.
pub struct App {
// === UI ===
/// The currently displayed UI screen.
select: ui::screen::SelectScreen,
/// The top of the display stack, if present.
display: Option<ui::screen::DisplayScreen>,
/// The ids of note on the display stack
display_stack: Vec<String>,
// === DATA ===
/// Index note data
index: data::NoteIndexContainer,
// === CONFIG ===
/// The file manager this app's screens use to enact the user's file system requests on the file system.
manager: io::FileManager,
/// The HtmlBuider this app's screens use to continuously build html files.
builder: io::HtmlBuilder,
/// The styles used by this app's screens.
styles: ui::UiStyles,
}
impl App {
/// Creates a new application state. This includes
/// - Loading a config file
/// - Indexing notes from the given path
/// - Creating an initial select screen and empty display stack
/// Also returns all errors that happened during creation that did not prevent the creation.
pub fn new(args: crate::Arguments) -> (Self, Vec<error::RucolaError>) {
// Gather errors
let mut errors = vec![];
let (config, vault_path) = match crate::Config::load(args) {
Ok(config_data) => config_data,
Err(e) => {
errors.push(e);
Default::default()
}
};
let styles = match ui::UiStyles::load(&config) {
Ok(config) => config,
Err(e) => {
errors.push(e);
Default::default()
}
};
let builder = io::HtmlBuilder::new(&config, vault_path.clone());
let manager = io::FileManager::new(&config, vault_path.clone());
let tracker = match io::FileTracker::new(&config, vault_path) {
Ok(config) => config,
Err(e) => {
errors.push(e);
Default::default()
}
};
// Index all files in path
let (index, index_errors) = data::NoteIndex::new(tracker, builder.clone());
errors.extend(index_errors);
let index = std::rc::Rc::new(std::cell::RefCell::new(index));
// Initialize app state
(
Self {
select: ui::screen::SelectScreen::new(
index.clone(),
manager.clone(),
builder.clone(),
styles,
config.stats_show,
),
display: None,
display_stack: Vec::new(),
index,
styles,
manager,
builder,
},
errors,
)
}
/// Reads the top of the display stack, creates a new display screen from it and sets that as the currently active display screen.
/// If the display stack is empty, clears the display screen.
fn set_display_to_top(&mut self) -> error::Result<()> {
self.display = match self.display_stack.last() {
Some(id) => Some(ui::screen::DisplayScreen::new(
id,
self.index.clone(),
self.manager.clone(),
self.builder.clone(),
self.styles,
)?),
None => None,
};
Ok(())
}
// Updates the app with the given key.
pub fn update(
&mut self,
key: Option<crossterm::event::KeyEvent>,
) -> error::Result<ui::TerminalMessage> {
// Check for file changes
let mut index = self.index.borrow_mut();
let (modifications, id_changes) = index.handle_file_events()?;
drop(index);
// synchronize display stack with id changes from file events
'changes: for (old_id, maybe_new_id) in id_changes {
// if an id was changed, update all displays referring to it
if let Some(new_id) = maybe_new_id {
for display_id in self.display_stack.iter_mut() {
if *display_id == old_id {
*display_id = new_id;
continue 'changes;
}
}
} else {
// if an id was deleted, remove all such displays from the stack
self.display_stack
.retain(|display_id| *display_id != old_id);
}
}
// remove 'empty' ids, indicating that
self.display_stack
.retain(|display_id| !display_id.is_empty());
if modifications {
// if anything happened in the file system, better refresh the filters
self.select.refresh_env_stats();
// also refresh the display by setting it to none
self.set_display_to_top()?;
}
let key = if let Some(key) = key {
key
} else {
return Ok(ui::TerminalMessage::None);
};
// Update appropriate screen
let msg = if let Some(display) = &mut self.display {
display.update(key)
} else {
self.select.update(key)
};
let msg = msg?;
// Act on the potentially returned message.
match &msg {
// Message that do not modify the app trigger no immediate effect and are later passed up.
ui::Message::None | ui::Message::Quit | ui::Message::OpenExternalCommand(_) => {}
ui::Message::DisplayStackClear => {
// Clear the display stack and remove the current display screen, if there is one.
self.display_stack.clear();
self.display = None;
}
ui::Message::DisplayStackPop => {
// Pop the top of the stack - which should correspond to the currently displayed note.
self.display_stack.pop();
self.set_display_to_top()?;
}
ui::Message::DisplayStackPush(new_id) => {
// Push a new id on top of the display stack.
self.display_stack.push(new_id.clone());
self.set_display_to_top()?;
}
}
Ok(msg.into())
}
pub fn draw(&self, area: Rect, buf: &mut Buffer) {
if let Some(display) = &self.display {
display.draw(area, buf);
} else {
self.select.draw(area, buf);
}
}
}