1use crate::command::{AgendaRange, CalendarView, CardinalCommand, ListTarget, SyncTarget};
2use crate::workspace::{View, WorkspaceState};
3
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub enum DomainEffectRequest {
6 ReadMaildir,
7 MoveMessage,
8 SendMail,
9 ReadCalendar,
10 WriteEvent,
11 RunSync(SyncTarget),
12}
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct CommandOutcome {
16 pub state: WorkspaceState,
17 pub effect: Option<DomainEffectRequest>,
18 pub status_message: Option<String>,
19}
20
21pub fn dispatch_command(state: &WorkspaceState, command: &CardinalCommand) -> CommandOutcome {
22 let mut next_state = state.clone();
23 let mut effect = None;
24 let mut status_message = None;
25
26 match command {
27 CardinalCommand::List(target) => match target {
28 ListTarget::Inboxes | ListTarget::Folders => next_state.set_view(View::Inboxes),
29 ListTarget::Mail => {
30 next_state.set_view(View::MailList {
31 mailbox: "mail".to_owned(),
32 });
33 effect = Some(DomainEffectRequest::ReadMaildir);
34 }
35 ListTarget::Unread => {
36 next_state.set_view(View::MailList {
37 mailbox: "unread".to_owned(),
38 });
39 effect = Some(DomainEffectRequest::ReadMaildir);
40 }
41 ListTarget::Flagged => {
42 next_state.set_view(View::MailList {
43 mailbox: "flagged".to_owned(),
44 });
45 effect = Some(DomainEffectRequest::ReadMaildir);
46 }
47 ListTarget::Calendars => {
48 next_state.set_view(View::Calendars);
49 effect = Some(DomainEffectRequest::ReadCalendar);
50 }
51 ListTarget::Invites => {
52 next_state.set_view(View::MailList {
53 mailbox: "invites".to_owned(),
54 });
55 effect = Some(DomainEffectRequest::ReadMaildir);
56 }
57 },
58 CardinalCommand::Calendar(view) => {
59 let range = match view {
60 CalendarView::Today => AgendaRange::Today,
61 CalendarView::Tomorrow => AgendaRange::Tomorrow,
62 CalendarView::Week => AgendaRange::Week,
63 CalendarView::Month => AgendaRange::Default,
64 };
65 next_state.set_view(View::Agenda { range });
66 effect = Some(DomainEffectRequest::ReadCalendar);
67 }
68 CardinalCommand::Agenda(range) => {
69 next_state.set_view(View::Agenda {
70 range: range.clone(),
71 });
72 effect = Some(DomainEffectRequest::ReadCalendar);
73 }
74 CardinalCommand::Compose
75 | CardinalCommand::Reply { .. }
76 | CardinalCommand::Forward { .. } => {
77 next_state.set_view(View::Compose);
78 }
79 CardinalCommand::Send { .. } => {
80 effect = Some(DomainEffectRequest::SendMail);
81 }
82 CardinalCommand::Search { query } => {
83 next_state.set_view(View::SearchResults {
84 query: query.clone(),
85 });
86 }
87 CardinalCommand::Archive
88 | CardinalCommand::Delete
89 | CardinalCommand::Spam
90 | CardinalCommand::Mark(_)
91 | CardinalCommand::Move { .. }
92 | CardinalCommand::Undo => {
93 effect = Some(DomainEffectRequest::MoveMessage);
94 }
95 CardinalCommand::Event(_) | CardinalCommand::Invite(_) => {
96 effect = Some(DomainEffectRequest::WriteEvent);
97 }
98 CardinalCommand::Sync(target) => {
99 effect = Some(DomainEffectRequest::RunSync(target.clone()));
100 status_message = Some("sync queued".to_owned());
101 }
102 CardinalCommand::Open(_)
103 | CardinalCommand::Help
104 | CardinalCommand::Bindings
105 | CardinalCommand::Config
106 | CardinalCommand::Reload
107 | CardinalCommand::Quit => {}
108 }
109
110 CommandOutcome {
111 state: next_state,
112 effect,
113 status_message,
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use crate::command::{CardinalCommand, EventCommand, InviteCommand, MarkState, Selector};
121
122 #[test]
123 fn dispatches_agenda_command_to_typed_view() {
124 let state = WorkspaceState::default();
125 let outcome = dispatch_command(&state, &CardinalCommand::Agenda(AgendaRange::Week));
126
127 assert_eq!(
128 outcome.state.view,
129 View::Agenda {
130 range: AgendaRange::Week
131 }
132 );
133 assert_eq!(outcome.effect, Some(DomainEffectRequest::ReadCalendar));
134 }
135
136 #[test]
137 fn dispatches_sync_command_to_effect() {
138 let state = WorkspaceState::default();
139 let outcome = dispatch_command(&state, &CardinalCommand::Sync(SyncTarget::Mail));
140
141 assert_eq!(
142 outcome.effect,
143 Some(DomainEffectRequest::RunSync(SyncTarget::Mail))
144 );
145 assert_eq!(outcome.status_message, Some("sync queued".to_owned()));
146 }
147
148 #[test]
149 fn dispatches_list_calendars_to_calendar_view() {
150 let state = WorkspaceState::default();
151 let outcome = dispatch_command(&state, &CardinalCommand::List(ListTarget::Calendars));
152
153 assert_eq!(outcome.state.view, View::Calendars);
154 assert_eq!(outcome.effect, Some(DomainEffectRequest::ReadCalendar));
155 }
156
157 #[test]
158 fn dispatches_archive_to_mail_move_effect() {
159 let state = WorkspaceState::default();
160 let outcome = dispatch_command(&state, &CardinalCommand::Archive);
161
162 assert_eq!(outcome.effect, Some(DomainEffectRequest::MoveMessage));
163 }
164
165 #[test]
166 fn dispatches_mail_list_targets_to_maildir_read() {
167 let state = WorkspaceState::default();
168 let list_mail = dispatch_command(&state, &CardinalCommand::List(ListTarget::Mail));
169 let list_unread = dispatch_command(&state, &CardinalCommand::List(ListTarget::Unread));
170 let list_flagged = dispatch_command(&state, &CardinalCommand::List(ListTarget::Flagged));
171 let list_invites = dispatch_command(&state, &CardinalCommand::List(ListTarget::Invites));
172
173 assert_eq!(
174 list_mail.state.view,
175 View::MailList {
176 mailbox: "mail".to_owned()
177 }
178 );
179 assert_eq!(
180 list_unread.state.view,
181 View::MailList {
182 mailbox: "unread".to_owned()
183 }
184 );
185 assert_eq!(
186 list_flagged.state.view,
187 View::MailList {
188 mailbox: "flagged".to_owned()
189 }
190 );
191 assert_eq!(
192 list_invites.state.view,
193 View::MailList {
194 mailbox: "invites".to_owned()
195 }
196 );
197 assert_eq!(list_mail.effect, Some(DomainEffectRequest::ReadMaildir));
198 assert_eq!(list_unread.effect, Some(DomainEffectRequest::ReadMaildir));
199 assert_eq!(list_flagged.effect, Some(DomainEffectRequest::ReadMaildir));
200 assert_eq!(list_invites.effect, Some(DomainEffectRequest::ReadMaildir));
201 }
202
203 #[test]
204 fn dispatches_non_mail_list_targets_without_maildir_effect() {
205 let state = WorkspaceState::default();
206 let list_inboxes = dispatch_command(&state, &CardinalCommand::List(ListTarget::Inboxes));
207 let list_folders = dispatch_command(&state, &CardinalCommand::List(ListTarget::Folders));
208
209 assert_eq!(list_inboxes.state.view, View::Inboxes);
210 assert_eq!(list_folders.state.view, View::Inboxes);
211 assert_eq!(list_inboxes.effect, None);
212 assert_eq!(list_folders.effect, None);
213 }
214
215 #[test]
216 fn dispatches_calendar_views_to_agenda_ranges() {
217 let state = WorkspaceState::default();
218 let today = dispatch_command(&state, &CardinalCommand::Calendar(CalendarView::Today));
219 let tomorrow = dispatch_command(&state, &CardinalCommand::Calendar(CalendarView::Tomorrow));
220 let week = dispatch_command(&state, &CardinalCommand::Calendar(CalendarView::Week));
221 let month = dispatch_command(&state, &CardinalCommand::Calendar(CalendarView::Month));
222
223 assert_eq!(
224 today.state.view,
225 View::Agenda {
226 range: AgendaRange::Today
227 }
228 );
229 assert_eq!(
230 tomorrow.state.view,
231 View::Agenda {
232 range: AgendaRange::Tomorrow
233 }
234 );
235 assert_eq!(
236 week.state.view,
237 View::Agenda {
238 range: AgendaRange::Week
239 }
240 );
241 assert_eq!(
242 month.state.view,
243 View::Agenda {
244 range: AgendaRange::Default
245 }
246 );
247 assert_eq!(today.effect, Some(DomainEffectRequest::ReadCalendar));
248 assert_eq!(tomorrow.effect, Some(DomainEffectRequest::ReadCalendar));
249 assert_eq!(week.effect, Some(DomainEffectRequest::ReadCalendar));
250 assert_eq!(month.effect, Some(DomainEffectRequest::ReadCalendar));
251 }
252
253 #[test]
254 fn dispatches_compose_reply_and_forward_to_compose_view() {
255 let state = WorkspaceState::default();
256 let compose = dispatch_command(&state, &CardinalCommand::Compose);
257 let reply = dispatch_command(&state, &CardinalCommand::Reply { all: true });
258 let forward = dispatch_command(
259 &state,
260 &CardinalCommand::Forward {
261 recipient: "a@example.com".to_owned(),
262 },
263 );
264
265 assert_eq!(compose.state.view, View::Compose);
266 assert_eq!(reply.state.view, View::Compose);
267 assert_eq!(forward.state.view, View::Compose);
268 }
269
270 #[test]
271 fn dispatches_send_to_sendmail_effect() {
272 let state = WorkspaceState::default();
273 let send = dispatch_command(&state, &CardinalCommand::Send { confirm: false });
274 let confirm = dispatch_command(&state, &CardinalCommand::Send { confirm: true });
275
276 assert_eq!(send.effect, Some(DomainEffectRequest::SendMail));
277 assert_eq!(confirm.effect, Some(DomainEffectRequest::SendMail));
278 assert_eq!(send.state, state);
279 }
280
281 #[test]
282 fn dispatches_search_to_search_results_view() {
283 let state = WorkspaceState::default();
284 let outcome = dispatch_command(
285 &state,
286 &CardinalCommand::Search {
287 query: "from:alice".to_owned(),
288 },
289 );
290
291 assert_eq!(
292 outcome.state.view,
293 View::SearchResults {
294 query: "from:alice".to_owned()
295 }
296 );
297 }
298
299 #[test]
300 fn dispatches_all_mail_mutations_to_move_message_effect() {
301 let state = WorkspaceState::default();
302 let delete = dispatch_command(&state, &CardinalCommand::Delete);
303 let spam = dispatch_command(&state, &CardinalCommand::Spam);
304 let mark = dispatch_command(&state, &CardinalCommand::Mark(MarkState::Read));
305 let mv = dispatch_command(
306 &state,
307 &CardinalCommand::Move {
308 target: "archive".to_owned(),
309 },
310 );
311 let undo = dispatch_command(&state, &CardinalCommand::Undo);
312
313 assert_eq!(delete.effect, Some(DomainEffectRequest::MoveMessage));
314 assert_eq!(spam.effect, Some(DomainEffectRequest::MoveMessage));
315 assert_eq!(mark.effect, Some(DomainEffectRequest::MoveMessage));
316 assert_eq!(mv.effect, Some(DomainEffectRequest::MoveMessage));
317 assert_eq!(undo.effect, Some(DomainEffectRequest::MoveMessage));
318 }
319
320 #[test]
321 fn dispatches_noop_commands_without_side_effects() {
322 let state = WorkspaceState::default();
323 let open = dispatch_command(&state, &CardinalCommand::Open(Selector::Index(1)));
324 let help = dispatch_command(&state, &CardinalCommand::Help);
325 let bindings = dispatch_command(&state, &CardinalCommand::Bindings);
326 let config = dispatch_command(&state, &CardinalCommand::Config);
327 let reload = dispatch_command(&state, &CardinalCommand::Reload);
328 let quit = dispatch_command(&state, &CardinalCommand::Quit);
329
330 assert_eq!(open.effect, None);
331 assert_eq!(help.effect, None);
332 assert_eq!(bindings.effect, None);
333 assert_eq!(config.effect, None);
334 assert_eq!(reload.effect, None);
335 assert_eq!(quit.effect, None);
336 assert_eq!(open.state, state);
337 assert_eq!(quit.state, state);
338 }
339
340 #[test]
341 fn dispatches_event_and_invite_to_calendar_write_effect() {
342 let state = WorkspaceState::default();
343 let event_outcome = dispatch_command(&state, &CardinalCommand::Event(EventCommand::New));
344 let invite_outcome =
345 dispatch_command(&state, &CardinalCommand::Invite(InviteCommand::Accept));
346
347 assert_eq!(event_outcome.effect, Some(DomainEffectRequest::WriteEvent));
348 assert_eq!(invite_outcome.effect, Some(DomainEffectRequest::WriteEvent));
349 }
350}