Skip to main content

rsynko_manager/
transitions.rs

1use crate::*;
2use alux_ext::ext;
3use rsynko_media::portable_user_file_name;
4use std::path::{Path, PathBuf};
5
6/// Derives collection selection and removal from ordered stable identities.
7#[ext(name = CollectionNavigationExt)]
8pub impl<This> This
9where
10    This: NavigationStateAlg + QueueCatalogAlg + QueueRemoveAlg,
11    This::Id: Copy + Eq,
12{
13    /// Selects the adjacent entry with collection-end wraparound.
14    fn select_relative(&mut self, forward: bool) {
15        let ids = self.queue_ids().collect::<Vec<_>>();
16        if ids.is_empty() {
17            self.set_selected_queue_id(None);
18            return;
19        }
20        let current = self
21            .selected_queue_id()
22            .and_then(|selected| ids.iter().position(|id| *id == selected))
23            .unwrap_or(0);
24        let next = if forward {
25            (current + 1) % ids.len()
26        } else if current == 0 {
27            ids.len() - 1
28        } else {
29            current - 1
30        };
31        self.set_selected_queue_id(Some(ids[next]));
32        if matches!(self.page(), ManagerPage::Details(_)) {
33            self.set_page(ManagerPage::Details(ids[next]));
34        }
35    }
36
37    /// Removes selection and chooses its nearest remaining neighbor.
38    fn remove_selected(&mut self) {
39        let ids = self.queue_ids().collect::<Vec<_>>();
40        let Some(selected) = self.selected_queue_id() else {
41            return;
42        };
43        let index = ids.iter().position(|id| *id == selected).unwrap_or(0);
44        self.remove_queue_entry(selected);
45        let remaining = self.queue_ids().collect::<Vec<_>>();
46        let next = remaining
47            .get(index.min(remaining.len().saturating_sub(1)))
48            .copied();
49        self.set_selected_queue_id(next);
50        if matches!(self.page(), ManagerPage::Details(id) | ManagerPage::Formats(id) | ManagerPage::Output(id) | ManagerPage::Log(id) | ManagerPage::Command(id) if id == selected)
51        {
52            self.set_page(ManagerPage::Collection);
53        }
54    }
55}
56
57/// Derives expanded-row cursor movement and normalization.
58#[ext(name = DetailNavigationExt)]
59pub impl<This> This
60where
61    This: NavigationStateAlg + QueueCatalogAlg + DetailSelectionAlg,
62    This::Entry: QueueEntryAlg,
63    This::Id: Copy + Eq,
64{
65    /// Selects an adjacent expanded-row position, including the row itself.
66    fn select_detail(&mut self, forward: bool) {
67        let ManagerPage::Details(id) = self.page() else {
68            return;
69        };
70        let Some(entry) = self.queue_entry(id) else {
71            return;
72        };
73        let controls = entry.detail_controls();
74        if controls.is_empty() {
75            return;
76        }
77        let next = match self
78            .selected_detail_control()
79            .and_then(|selected| controls.iter().position(|control| *control == selected))
80        {
81            None if forward => Some(controls[0]),
82            None => controls.last().copied(),
83            Some(current) if forward && current + 1 == controls.len() => None,
84            Some(current) if forward => Some(controls[current + 1]),
85            Some(0) => None,
86            Some(current) => Some(controls[current - 1]),
87        };
88        self.set_selected_detail_control(next);
89    }
90
91    /// Keeps row focus and replaces a detail cursor that is no longer meaningful.
92    fn normalize_detail_control(&mut self, id: This::Id) {
93        if self.page() != ManagerPage::Details(id) {
94            return;
95        }
96        let Some(entry) = self.queue_entry(id) else {
97            return;
98        };
99        let controls = entry.detail_controls();
100        if self
101            .selected_detail_control()
102            .is_some_and(|selected| !controls.contains(&selected))
103        {
104            self.set_selected_detail_control(controls.first().copied());
105        }
106    }
107}
108
109/// Derives output-name editing from an editable entry and a text draft.
110#[ext(name = OutputEditingExt)]
111pub impl<This> This
112where
113    This: NavigationStateAlg
114        + QueueCatalogAlg
115        + OutputDraftAlg
116        + InputDraftAlg
117        + QueueOutputAlg
118        + QueueSourceAlg
119        + ManagerStatusAlg,
120    This::Entry: QueueEntryAlg,
121    This::Id: Copy,
122{
123    /// Opens input editing only while the selected request still permits its input to change.
124    ///
125    /// A request whose input was already read — extracted, described, named after — no longer
126    /// permits it: changing the input then would make everything derived from it a lie.
127    fn open_input(&mut self) {
128        let Some(id) = self.selected_queue_id() else {
129            return;
130        };
131        let Some(entry) = self.queue_entry(id) else {
132            return;
133        };
134        if entry.is_editable() && entry.output_naming() == OutputNaming::Stated {
135            self.set_input_draft(entry.source().to_owned());
136            self.set_page(ManagerPage::Input(id));
137            self.set_manager_message(None);
138        }
139    }
140
141    /// Applies a non-empty input draft.
142    fn submit_input(&mut self) {
143        let ManagerPage::Input(id) = self.page() else {
144            return;
145        };
146        if self.input_draft().trim().is_empty() {
147            self.set_manager_message(Some("enter an input".to_owned()));
148            return;
149        }
150        self.set_queue_source(id, self.input_draft().trim().to_owned());
151        self.set_page(ManagerPage::Details(id));
152        self.set_manager_message(None);
153    }
154
155    /// Opens output-file-name editing only while the selected request is editable.
156    fn open_output(&mut self) {
157        let Some(id) = self.selected_queue_id() else {
158            return;
159        };
160        let Some(entry) = self.queue_entry(id) else {
161            return;
162        };
163        if entry.is_editable() {
164            self.set_output_draft(
165                entry
166                    .output()
167                    .map_or_else(String::new, |path| path.display().to_string()),
168            );
169            self.set_page(ManagerPage::Output(id));
170            self.set_manager_message(None);
171        }
172    }
173
174    /// Applies a non-empty output draft, as the kind of name the request states it is.
175    fn submit_output(&mut self) {
176        let ManagerPage::Output(id) = self.page() else {
177            return;
178        };
179        let Some(entry) = self.queue_entry(id) else {
180            return;
181        };
182        let naming = entry.output_naming();
183        let extension = entry
184            .output()
185            .and_then(Path::extension)
186            .and_then(std::ffi::OsStr::to_str);
187        if self.output_draft().trim().is_empty() {
188            self.set_manager_message(Some("enter a name".to_owned()));
189            return;
190        }
191        // A stated path is the answer already; making it portable would take its separators away.
192        let output = match naming {
193            OutputNaming::Portable => portable_user_file_name(self.output_draft(), extension),
194            OutputNaming::Stated => PathBuf::from(self.output_draft().trim()),
195        };
196        self.set_queue_output(id, output);
197        self.set_page(ManagerPage::Details(id));
198        self.set_manager_message(None);
199    }
200}
201
202/// Derives the editable format-selection transition.
203#[ext(name = ChoiceEditingExt)]
204pub impl<This> This
205where
206    This: NavigationStateAlg
207        + QueueCatalogAlg
208        + FormatCatalogStateAlg
209        + QueueDryRunAlg
210        + ManagerStatusAlg,
211    This::Entry: QueueEntryAlg,
212    This::Id: Copy,
213{
214    /// Opens the report of what the selected request would do.
215    fn open_report(&mut self) {
216        if let Some(id) = self.selected_queue_id()
217            && self.queue_entry(id).is_some()
218        {
219            self.set_page(ManagerPage::Report(id));
220            self.set_manager_message(None);
221        }
222    }
223
224    /// Opens the record of what was observed about the selected request.
225    fn open_log(&mut self) {
226        if let Some(id) = self.selected_queue_id()
227            && self.queue_entry(id).is_some()
228        {
229            self.set_page(ManagerPage::Log(id));
230            self.set_manager_message(None);
231        }
232    }
233
234    /// Opens the command the selected request would run, stated whole.
235    fn open_command(&mut self) {
236        if let Some(id) = self.selected_queue_id()
237            && self.queue_entry(id).is_some()
238        {
239            self.set_page(ManagerPage::Command(id));
240            self.set_manager_message(None);
241        }
242    }
243
244    /// Turns the rehearsal mode of the selected request on or off.
245    ///
246    /// A request that states no rehearsal mode has none to turn, so the intention denotes nothing.
247    fn toggle_dry_run(&mut self) {
248        if let Some(id) = self.selected_queue_id()
249            && let Some(dry_run) = self.queue_entry(id).and_then(QueueEntryAlg::dry_run)
250        {
251            self.set_queue_dry_run(id, !dry_run);
252            self.set_manager_message(None);
253        }
254    }
255
256    /// Opens format selection only while the selected request is editable.
257    fn open_formats(&mut self) {
258        let Some(id) = self.selected_queue_id() else {
259            return;
260        };
261        if self.queue_entry(id).is_some_and(QueueEntryAlg::is_editable) {
262            self.request_format_catalog(id);
263            self.set_page(ManagerPage::Formats(id));
264            self.set_manager_message(None);
265        } else {
266            self.set_manager_message(Some(
267                "started downloads are immutable; duplicate to change options".to_owned(),
268            ));
269        }
270    }
271}
272
273/// Derives add-source submission from source construction and queue insertion.
274#[ext(name = DraftSubmissionExt)]
275pub impl<This> This
276where
277    This: NavigationStateAlg
278        + QueueCatalogAlg
279        + QueueAppendAlg
280        + DraftStateAlg
281        + ManagerStatusAlg
282        + SourceRequestAlg
283        + SubmissionAlg
284        + MediaOptionsAlg
285        + OutputChoiceAlg,
286    This::Id: Copy,
287{
288    /// Adds requests, focuses the first new identity, and clears stale status.
289    fn add_sources(&mut self, requests: Vec<This::Source>) {
290        let added = self.append_sources(requests);
291        if let Some(added) = added.first().copied() {
292            self.set_selected_queue_id(Some(added));
293        }
294        self.set_manager_message(None);
295    }
296
297    /// Submits non-empty trimmed draft lines as progressive requests.
298    fn submit_draft(&mut self) {
299        let requests = self
300            .draft()
301            .lines()
302            .map(str::trim)
303            .filter(|source| !source.is_empty())
304            .map(|line| self.submitted(line))
305            .collect::<Vec<_>>();
306        if requests.is_empty() {
307            self.set_manager_message(Some("enter at least one source".to_owned()));
308        } else {
309            self.add_sources(requests);
310            self.set_draft(String::new());
311            self.set_page(ManagerPage::Collection);
312        }
313    }
314}
315
316/// Derives lifecycle actions for the selected stable identity.
317#[ext(name = QueueLifecycleExt)]
318pub impl<This> This
319where
320    This: NavigationStateAlg
321        + QueueCatalogAlg
322        + QueuePauseAlg
323        + QueueDuplicateAlg
324        + TransferStateAlg
325        + DetailSelectionAlg
326        + ManagerStatusAlg,
327    This::Entry: QueueEntryAlg,
328    This::Id: Copy + Eq,
329{
330    /// Applies the selected entry's state-dependent Space transition.
331    ///
332    /// Space names a collection entry, so it denotes nothing while another page is current.
333    fn apply_selected_space(&mut self) {
334        if !matches!(
335            self.page(),
336            ManagerPage::Collection | ManagerPage::Details(_)
337        ) {
338            return;
339        }
340        let Some(id) = self.selected_queue_id() else {
341            return;
342        };
343        match self.queue_entry(id).and_then(QueueEntryAlg::space_action) {
344            Some(SpaceAction::Start | SpaceAction::Rehearse) => self.set_waiting(id),
345            Some(SpaceAction::Pause | SpaceAction::Resume) => self.toggle_queue_pause(id),
346            None => {}
347        }
348        self.normalize_detail_control(id);
349    }
350
351    /// Duplicates selection as a fresh editable identity and opens its details.
352    fn duplicate_selected(&mut self) {
353        if let Some(id) = self.selected_queue_id()
354            && let Some(duplicate) = self.duplicate_queue_entry(id)
355        {
356            self.set_selected_queue_id(Some(duplicate));
357            self.set_page(ManagerPage::Details(duplicate));
358            self.set_selected_detail_control(None);
359            self.set_manager_message(Some("source duplicated as an editable download".to_owned()));
360        }
361    }
362
363    /// Restarts the selected failed request with its fixed options.
364    fn restart_selected(&mut self) {
365        if let Some(id) = self.selected_queue_id() {
366            self.restart_waiting(id);
367            self.normalize_detail_control(id);
368            self.set_manager_message(Some("failed download queued again".to_owned()));
369        }
370    }
371}
372
373/// Interprets manager intentions by composing independently derived meanings.
374#[ext(name = ManagerIntentExt)]
375pub impl<This> This
376where
377    This: NavigationStateAlg
378        + QueueCatalogAlg
379        + QueueAppendAlg
380        + QueueRemoveAlg
381        + QueuePauseAlg
382        + QueueDuplicateAlg
383        + QueueFormatEditAlg
384        + FormatCatalogStateAlg
385        + QueueDryRunAlg
386        + RehearsalStateAlg
387        + TransferStateAlg
388        + DetailSelectionAlg
389        + DraftStateAlg
390        + InputDraftAlg
391        + OutputDraftAlg
392        + QueueOutputAlg
393        + QueueSourceAlg
394        + SourceMetadataAlg
395        + TextEditorStateAlg
396        + ManagerStatusAlg
397        + SafeExitAlg
398        + SourceRequestAlg
399        + SubmissionAlg
400        + MediaOptionsAlg
401        + OutputChoiceAlg,
402    This::Entry: QueueEntryAlg + RequestOptionsAlg,
403    This::Id: Copy + Eq,
404{
405    /// Applies one manager intention or external observation.
406    /// Applies one manager intention through the generated fold.
407    fn apply_manager_event(
408        &mut self,
409        event: ManagerIntentOp<This::Id, This::Source, This::Format, This::Change>,
410    ) {
411        event.interpret(&mut IntentDispatch(self));
412    }
413}
414
415/// Applies one manager intention to the state it addresses.
416///
417/// The dispatch is the generated fold, so this states only what each intention *means*.
418struct IntentDispatch<'a, This>(&'a mut This);
419
420impl<This, Id, Source, Format, Change> ManagerIntentInterpreter for IntentDispatch<'_, This>
421where
422    This: ManagerSorts<Id = Id, Source = Source, Format = Format, Change = Change>
423        + NavigationStateAlg
424        + QueueCatalogAlg
425        + QueueAppendAlg
426        + QueueRemoveAlg
427        + QueuePauseAlg
428        + QueueDuplicateAlg
429        + QueueFormatEditAlg
430        + FormatCatalogStateAlg
431        + QueueDryRunAlg
432        + RehearsalStateAlg
433        + TransferStateAlg
434        + DetailSelectionAlg
435        + DraftStateAlg
436        + InputDraftAlg
437        + OutputDraftAlg
438        + QueueOutputAlg
439        + QueueSourceAlg
440        + SourceMetadataAlg
441        + TextEditorStateAlg
442        + ManagerStatusAlg
443        + SafeExitAlg
444        + SourceRequestAlg
445        + SubmissionAlg
446        + MediaOptionsAlg
447        + OutputChoiceAlg,
448    This::Entry: QueueEntryAlg,
449    Id: Copy + Eq,
450{
451    type Id = Id;
452    type Source = Source;
453    type Format = Format;
454    type Change = Change;
455
456    fn add_sources(&mut self, requests: Vec<Self::Source>) {
457        self.0.add_sources(requests);
458    }
459
460    fn set_draft(&mut self, draft: String) {
461        self.0.set_draft(draft);
462    }
463
464    fn insert_text(&mut self, text: String) {
465        self.0.insert_text(&text);
466    }
467
468    fn set_output_draft(&mut self, draft: String) {
469        self.0.set_output_draft(draft);
470    }
471
472    fn set_input_draft(&mut self, draft: String) {
473        self.0.set_input_draft(draft);
474    }
475
476    fn open_input(&mut self) {
477        self.0.open_input();
478    }
479
480    fn submit_input(&mut self) {
481        self.0.submit_input();
482    }
483
484    fn source_metadata(&mut self, id: Self::Id, media_id: String, title: Option<String>) {
485        self.0.apply_source_metadata(id, media_id, title);
486    }
487
488    fn format_catalog(&mut self, id: Self::Id, event: FormatDiscoveryOp<Self::Format>) {
489        self.0.apply_format_catalog_event(id, event);
490    }
491
492    fn rehearsal(&mut self, id: Self::Id, event: RehearsalObservationOp<Self::Change>) {
493        self.0.apply_rehearsal_event(id, event);
494        self.0.normalize_detail_control(id);
495    }
496
497    fn open_report(&mut self) {
498        self.0.open_report();
499    }
500
501    fn toggle_dry_run(&mut self) {
502        self.0.toggle_dry_run();
503    }
504
505    fn transfer(&mut self, id: Self::Id, event: TransferObservationOp) {
506        if matches!(event, TransferObservationOp::Started {}) {
507            self.0.set_manager_message(None);
508        }
509        self.0.apply_transfer_event(id, event);
510        self.0.normalize_detail_control(id);
511    }
512
513    fn open_add_sources(&mut self) {
514        self.0.set_draft(String::new());
515        self.0.set_page(ManagerPage::AddSources);
516    }
517
518    fn open_selected(&mut self) {
519        if let Some(id) = self.0.selected_queue_id() {
520            self.0.set_page(ManagerPage::Details(id));
521            self.0.set_selected_detail_control(None);
522        }
523    }
524
525    fn activate_detail(&mut self) {
526        match self.0.selected_detail_control() {
527            Some(DetailControl::Input) => self.0.open_input(),
528            Some(DetailControl::Output) => self.0.open_output(),
529            Some(DetailControl::Format) => self.0.open_formats(),
530            Some(DetailControl::Restart) => self.0.restart_selected(),
531            Some(DetailControl::Duplicate) => self.0.duplicate_selected(),
532            Some(DetailControl::Log) => self.0.open_log(),
533            Some(DetailControl::Command) => self.0.open_command(),
534            Some(DetailControl::Report) => self.0.open_report(),
535            Some(DetailControl::DryRun) => self.0.toggle_dry_run(),
536            Some(DetailControl::Delete) => self.0.remove_selected(),
537            None => self.0.set_page(ManagerPage::Collection),
538        }
539    }
540
541    fn back(&mut self) {
542        match self.0.page() {
543            ManagerPage::Formats(id)
544            | ManagerPage::Output(id)
545            | ManagerPage::Input(id)
546            | ManagerPage::Log(id)
547            | ManagerPage::Report(id)
548            | ManagerPage::Command(id) => {
549                self.0.set_page(ManagerPage::Details(id));
550            }
551            ManagerPage::Collection | ManagerPage::AddSources | ManagerPage::Details(_) => {
552                self.0.set_page(ManagerPage::Collection);
553            }
554        }
555    }
556
557    fn safe_exit_requested(&mut self) {
558        self.0.request_safe_exit();
559        self.0
560            .set_manager_message(Some("cancelling active downloads".to_owned()));
561    }
562
563    fn select_previous_format(&mut self) {
564        if let ManagerPage::Formats(id) = self.0.page() {
565            self.0.cycle_queue_format(id, false);
566        }
567    }
568
569    fn select_next_format(&mut self) {
570        if let ManagerPage::Formats(id) = self.0.page() {
571            self.0.cycle_queue_format(id, true);
572        }
573    }
574
575    fn submit_draft(&mut self) {
576        self.0.submit_draft();
577    }
578
579    fn delete_before_cursor(&mut self) {
580        self.0.delete_before_cursor();
581    }
582
583    fn delete_at_cursor(&mut self) {
584        self.0.delete_at_cursor();
585    }
586
587    fn move_cursor_left(&mut self) {
588        self.0.move_cursor_left();
589    }
590
591    fn move_cursor_right(&mut self) {
592        self.0.move_cursor_right();
593    }
594
595    fn move_cursor_home(&mut self) {
596        self.0.move_cursor_home();
597    }
598
599    fn move_cursor_end(&mut self) {
600        self.0.move_cursor_end();
601    }
602
603    fn select_previous(&mut self) {
604        self.0.select_relative(false);
605    }
606
607    fn select_next(&mut self) {
608        self.0.select_relative(true);
609    }
610
611    fn select_previous_detail(&mut self) {
612        self.0.select_detail(false);
613    }
614
615    fn select_next_detail(&mut self) {
616        self.0.select_detail(true);
617    }
618
619    fn open_formats(&mut self) {
620        self.0.open_formats();
621    }
622
623    fn open_output(&mut self) {
624        self.0.open_output();
625    }
626
627    fn submit_output(&mut self) {
628        self.0.submit_output();
629    }
630
631    fn apply_selected_space(&mut self) {
632        self.0.apply_selected_space();
633    }
634
635    fn duplicate_selected(&mut self) {
636        self.0.duplicate_selected();
637    }
638
639    fn remove_selected(&mut self) {
640        self.0.remove_selected();
641    }
642
643    fn restart_selected(&mut self) {
644        self.0.restart_selected();
645    }
646}