1use crate::{DownloadEvent, DownloadOptions, Format, FormatCatalog, FormatChoice, role_accepts};
2use crate::{RsyncSyntax, SyncCommand};
3use rsynko_manager::*;
4use rsynko_media::portable_file_name;
5use rsynko_rsync::{RsyncEndpointExt, SyncCommandExt, SyncCommandViewAlg, SyncMode, SyncProfile, sync_profile};
6use rsynko_ui::{DiscoveryState, FormatChoiceViewAlg, FormatLabelExt, FormatRolesExt};
7use std::ops::Not;
8use std::path::{Path, PathBuf};
9use std::time::Duration;
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
13pub struct QueueId(pub u64);
14
15#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
17pub struct MemoryManager;
18
19#[derive(Clone, Debug, PartialEq, Eq)]
21pub struct SourceRequest {
22 pub source: String,
24 pub output: Option<PathBuf>,
26 pub options: DownloadOptions,
28}
29
30impl SourceRequest {
31 #[must_use]
33 pub fn new(source: impl Into<String>, output: Option<PathBuf>, options: DownloadOptions) -> Self {
34 Self { source: source.into(), output, options }
35 }
36}
37
38impl ManagerSorts for MemoryManager {
39 type Id = ();
40 type Source = SourceRequest;
41 type Options = DownloadOptions;
42 type Output = Option<PathBuf>;
43 type Format = Format;
44 type Change = PlannedChange;
45 type Entry = ();
46 type Downloads = ManagerState;
47}
48
49impl ManagerSorts for ManagerState {
50 type Id = QueueId;
51 type Source = SourceRequest;
52 type Options = DownloadOptions;
53 type Output = Option<PathBuf>;
54 type Format = Format;
55 type Change = PlannedChange;
56 type Entry = QueueEntry;
57 type Downloads = ();
58}
59
60impl DownloadsAlg for MemoryManager {
61 fn empty_downloads(&self) -> Self::Downloads {
62 ManagerState::downloads()
63 }
64}
65
66impl SourceRequestAlg for MemoryManager {
67 fn source(&self, input: impl Into<String>, output: Self::Output, options: Self::Options) -> Self::Source {
68 SourceRequest::new(input, output, options)
69 }
70}
71
72impl MediaOptionsAlg for MemoryManager {
73 fn progressive(&self) -> Self::Options {
74 DownloadOptions::progressive()
75 }
76
77 fn audio(&self) -> Self::Options {
78 DownloadOptions::audio()
79 }
80
81 fn video(&self) -> Self::Options {
82 DownloadOptions::video()
83 }
84}
85
86impl SourceRecognitionAlg for MemoryManager {
87 fn recognizes_source(&self, line: &str) -> bool {
88 recognized(line.trim())
89 }
90}
91
92impl SubmissionAlg for MemoryManager {
93 fn submitted(&self, line: &str) -> Self::Source {
94 submitted(line)
95 }
96}
97
98impl OutputChoiceAlg for MemoryManager {
99 fn suggested_output(&self) -> Self::Output {
100 None
101 }
102
103 fn exact_output(&self, path: impl Into<PathBuf>) -> Self::Output {
104 Some(path.into())
105 }
106}
107
108impl SourceRequestAlg for ManagerState {
109 fn source(&self, input: impl Into<String>, output: Self::Output, options: Self::Options) -> Self::Source {
110 SourceRequest::new(input, output, options)
111 }
112}
113
114impl MediaOptionsAlg for ManagerState {
115 fn progressive(&self) -> Self::Options {
116 DownloadOptions::progressive()
117 }
118
119 fn audio(&self) -> Self::Options {
120 DownloadOptions::audio()
121 }
122
123 fn video(&self) -> Self::Options {
124 DownloadOptions::video()
125 }
126}
127
128impl SourceRecognitionAlg for ManagerState {
129 fn recognizes_source(&self, line: &str) -> bool {
130 recognized(line.trim())
131 }
132}
133
134impl SubmissionAlg for ManagerState {
135 fn submitted(&self, line: &str) -> Self::Source {
136 submitted(line)
137 }
138}
139
140impl OutputChoiceAlg for ManagerState {
141 fn suggested_output(&self) -> Self::Output {
142 None
143 }
144
145 fn exact_output(&self, path: impl Into<PathBuf>) -> Self::Output {
146 Some(path.into())
147 }
148}
149
150#[derive(Clone, Debug, PartialEq, Eq)]
152pub struct TransferState {
153 phase: TransferPhase,
154 destination: Option<PathBuf>,
155 downloaded: u64,
156 total: Option<u64>,
157 elapsed: Duration,
158 terminal: Option<DownloadEvent>,
159 summary: Option<String>,
160 detail: Option<String>,
161 pause_supported: bool,
162 resume_phase: Option<TransferPhase>,
163}
164
165impl TransferState {
166 fn ready() -> Self {
167 Self {
168 phase: TransferPhase::Ready,
169 destination: None,
170 downloaded: 0,
171 total: None,
172 elapsed: Duration::ZERO,
173 terminal: None,
174 summary: None,
175 detail: None,
176 pause_supported: false,
177 resume_phase: None,
178 }
179 }
180
181 #[must_use]
183 pub const fn phase(&self) -> TransferPhase {
184 self.phase
185 }
186
187 #[must_use]
189 pub fn destination(&self) -> Option<&Path> {
190 self.destination.as_deref()
191 }
192
193 #[must_use]
195 pub const fn downloaded(&self) -> u64 {
196 self.downloaded
197 }
198
199 #[must_use]
201 pub const fn total(&self) -> Option<u64> {
202 self.total
203 }
204
205 #[must_use]
207 pub const fn elapsed(&self) -> Duration {
208 self.elapsed
209 }
210
211 #[must_use]
213 pub const fn terminal(&self) -> Option<&DownloadEvent> {
214 self.terminal.as_ref()
215 }
216
217 #[must_use]
219 pub fn summary(&self) -> Option<&str> {
220 self.summary.as_deref()
221 }
222
223 #[must_use]
225 pub fn detail(&self) -> Option<&str> {
226 self.detail.as_deref()
227 }
228}
229
230impl TransferViewAlg for TransferState {
231 fn transferred(&self) -> u64 {
232 self.downloaded
233 }
234
235 fn transfer_total(&self) -> Option<u64> {
236 self.total
237 }
238
239 fn transfer_elapsed(&self) -> Duration {
240 self.elapsed
241 }
242
243 fn transfer_complete(&self) -> bool {
244 self.phase == TransferPhase::Complete
245 }
246
247 fn transfer_destination(&self) -> Option<&Path> {
248 self.destination()
249 }
250
251 fn transfer_summary(&self) -> Option<&str> {
252 self.summary()
253 }
254
255 fn transfer_detail(&self) -> Option<&str> {
256 self.detail()
257 }
258}
259
260#[derive(Clone, Debug, PartialEq)]
262pub struct QueueEntry {
263 id: QueueId,
264 request: SourceRequest,
265 transfer: TransferState,
266 formats: FormatCatalog,
267 media_id: Option<String>,
268 title: Option<String>,
269 output_custom: bool,
270 log: Vec<String>,
271 rehearsal: Rehearsal,
272}
273
274#[derive(Clone, Debug, PartialEq, Eq)]
276pub enum Rehearsal {
277 Unrehearsed,
279 Rehearsing,
281 Reported(Vec<PlannedChange>),
283 Failed(String),
285}
286
287#[derive(Clone, Debug, PartialEq, Eq, derive_new::new)]
289pub struct PlannedChange {
290 pub path: String,
292 pub kind: ChangeKind,
294 pub size: Option<u64>,
296}
297
298impl PlannedChangeAlg for PlannedChange {
299 fn change_path(&self) -> &str {
300 &self.path
301 }
302
303 fn change_kind(&self) -> ChangeKind {
304 self.kind
305 }
306
307 fn change_size(&self) -> Option<u64> {
308 self.size
309 }
310}
311
312impl QueueEntry {
313 #[must_use]
315 pub const fn id(&self) -> QueueId {
316 self.id
317 }
318
319 #[must_use]
321 pub fn source(&self) -> &str {
322 &self.request.source
323 }
324
325 #[must_use]
327 pub fn output(&self) -> Option<&Path> {
328 self.request.output.as_deref()
329 }
330
331 #[must_use]
333 pub fn title(&self) -> Option<&str> {
334 self.title.as_deref()
335 }
336
337 #[must_use]
339 pub const fn transfer(&self) -> &TransferState {
340 &self.transfer
341 }
342
343 #[must_use]
345 pub const fn options(&self) -> &DownloadOptions {
346 &self.request.options
347 }
348
349 #[must_use]
351 pub const fn formats(&self) -> &FormatCatalog {
352 &self.formats
353 }
354
355 #[must_use]
357 pub fn is_editable(&self) -> bool {
358 self.transfer.phase == TransferPhase::Ready
359 }
360
361 #[must_use]
363 pub fn detail_controls(&self) -> Vec<DetailControl> {
364 let rehearsed = matches!(self.rehearsal, Rehearsal::Unrehearsed).not().then_some(DetailControl::Report);
365 let rehearsal = self.request.options.dry_run().is_some().then_some(DetailControl::DryRun);
366 let commanded = self.stated_command().is_some().then_some(DetailControl::Command);
368 match self.transfer.phase {
369 TransferPhase::Ready => [
370 (self.output_naming() == OutputNaming::Stated).then_some(DetailControl::Input),
372 Some(DetailControl::Output),
373 (self.request.options.media_streams().is_some() || self.request.options.profile().is_some())
375 .then_some(DetailControl::Format),
376 commanded,
377 rehearsed,
378 Some(DetailControl::Log),
379 rehearsal,
380 Some(DetailControl::Duplicate),
381 Some(DetailControl::Delete),
382 ]
383 .into_iter()
384 .flatten()
385 .collect(),
386 TransferPhase::Failed => [
388 commanded,
389 rehearsed,
390 Some(DetailControl::Log),
391 Some(DetailControl::Restart),
392 Some(DetailControl::Duplicate),
393 Some(DetailControl::Delete),
394 ]
395 .into_iter()
396 .flatten()
397 .collect(),
398 TransferPhase::Rehearsing
399 | TransferPhase::Waiting
400 | TransferPhase::Extracting
401 | TransferPhase::Downloading
402 | TransferPhase::Paused
403 | TransferPhase::Publishing
404 | TransferPhase::Complete => [
405 commanded,
406 rehearsed,
407 Some(DetailControl::Log),
408 Some(DetailControl::Duplicate),
409 Some(DetailControl::Delete),
410 ]
411 .into_iter()
412 .flatten()
413 .collect(),
414 }
415 }
416
417 #[must_use]
419 pub fn space_action(&self) -> Option<SpaceAction> {
420 match self.transfer.phase {
421 TransferPhase::Ready if self.output().is_some() => Some(match self.dry_run() {
422 Some(true) => SpaceAction::Rehearse,
423 Some(false) | None => SpaceAction::Start,
424 }),
425 TransferPhase::Extracting | TransferPhase::Downloading if self.transfer.pause_supported => {
426 Some(SpaceAction::Pause)
427 }
428 TransferPhase::Paused => Some(SpaceAction::Resume),
429 TransferPhase::Ready
430 | TransferPhase::Waiting
431 | TransferPhase::Rehearsing
432 | TransferPhase::Extracting
433 | TransferPhase::Downloading
434 | TransferPhase::Publishing
435 | TransferPhase::Complete
436 | TransferPhase::Failed => None,
437 }
438 }
439
440 #[must_use]
447 pub fn transfer_command(&self) -> Option<SyncCommand> {
448 let dry_run = self.request.options.dry_run()?;
449 let destination = self.output()?;
450 let mode = SyncMode::transfer().rehearsed(dry_run).profiled(self.request.options.profile()?);
451 Some(RsyncSyntax.transfer_command(
452 &RsyncSyntax.read_endpoint(&self.request.source),
453 &RsyncSyntax.read_endpoint(&destination.display().to_string()),
454 mode,
455 ))
456 }
457
458 #[must_use]
460 pub fn stated_command(&self) -> Option<String> {
461 let command = self.transfer_command()?;
462 Some(format!(
463 "{} {}",
464 RsyncSyntax.command_program(&command),
465 RsyncSyntax.command_arguments(&command).collect::<Vec<_>>().join(" ")
466 ))
467 }
468
469 pub fn log(&self) -> impl DoubleEndedIterator<Item = &str> {
471 self.log.iter().map(String::as_str)
472 }
473
474 fn note(&mut self, note: impl Into<String>) {
476 self.log.push(note.into());
477 }
478
479 fn forget_rehearsal(&mut self) {
484 if matches!(self.rehearsal, Rehearsal::Unrehearsed) {
485 return;
486 }
487 self.rehearsal = Rehearsal::Unrehearsed;
488 self.note("the report was forgotten: it no longer states what would happen");
489 }
490
491 #[must_use]
493 pub fn label(&self) -> &str {
494 self.title.as_deref().filter(|title| !title.is_empty()).unwrap_or(&self.request.source)
495 }
496}
497
498impl TransferViewAlg for QueueEntry {
499 fn transferred(&self) -> u64 {
500 self.transfer.transferred()
501 }
502
503 fn transfer_total(&self) -> Option<u64> {
504 self.transfer.transfer_total()
505 }
506
507 fn transfer_elapsed(&self) -> Duration {
508 self.transfer.transfer_elapsed()
509 }
510
511 fn transfer_complete(&self) -> bool {
512 self.transfer.transfer_complete()
513 }
514
515 fn transfer_destination(&self) -> Option<&Path> {
516 self.transfer.transfer_destination()
517 }
518
519 fn transfer_summary(&self) -> Option<&str> {
520 self.transfer.transfer_summary()
521 }
522
523 fn transfer_detail(&self) -> Option<&str> {
524 self.transfer.transfer_detail()
525 }
526}
527
528impl QueueEntryAlg for QueueEntry {
529 fn label(&self) -> &str {
530 self.label()
531 }
532
533 fn source(&self) -> &str {
534 self.source()
535 }
536
537 fn phase(&self) -> TransferPhase {
538 self.transfer.phase()
539 }
540
541 fn dry_run(&self) -> Option<bool> {
542 self.request.options.dry_run()
543 }
544
545 fn output_naming(&self) -> OutputNaming {
546 if self.request.options.profile().is_some() { OutputNaming::Stated } else { OutputNaming::Portable }
548 }
549
550 fn stated_command(&self) -> Option<String> {
551 self.stated_command()
552 }
553
554 fn output(&self) -> Option<&Path> {
555 self.output()
556 }
557
558 fn is_editable(&self) -> bool {
559 self.is_editable()
560 }
561
562 fn detail_controls(&self) -> Vec<DetailControl> {
563 self.detail_controls()
564 }
565
566 fn download_log(&self) -> impl Iterator<Item = &str> {
567 self.log()
568 }
569
570 fn space_action(&self) -> Option<SpaceAction> {
571 self.space_action()
572 }
573}
574
575impl FormatChoiceViewAlg for QueueEntry {
576 type Format = Format;
577
578 fn discovery(&self) -> DiscoveryState<'_> {
579 match &self.formats {
580 FormatCatalog::Unknown => DiscoveryState::Unrequested,
581 FormatCatalog::Waiting => DiscoveryState::Waiting,
582 FormatCatalog::Inspecting => DiscoveryState::Inspecting,
583 FormatCatalog::Available(_) => DiscoveryState::Described,
584 FormatCatalog::Failed(message) => DiscoveryState::Failed(message),
585 }
586 }
587
588 fn described_formats(&self) -> impl Iterator<Item = &Self::Format> {
589 self.formats.available().unwrap_or_default().iter()
590 }
591}
592
593impl RehearsalViewAlg for QueueEntry {
594 type Change = PlannedChange;
595
596 fn rehearsal(&self) -> RehearsalState<'_> {
597 match &self.rehearsal {
598 Rehearsal::Unrehearsed => RehearsalState::Unrehearsed,
599 Rehearsal::Rehearsing => RehearsalState::Rehearsing,
600 Rehearsal::Reported(_) => RehearsalState::Reported,
601 Rehearsal::Failed(message) => RehearsalState::Failed(message),
602 }
603 }
604
605 fn planned_changes(&self) -> impl Iterator<Item = &Self::Change> {
606 match &self.rehearsal {
607 Rehearsal::Reported(changes) => changes.as_slice(),
608 Rehearsal::Unrehearsed | Rehearsal::Rehearsing | Rehearsal::Failed(_) => &[],
609 }
610 .iter()
611 }
612}
613
614impl RequestOptionsAlg for QueueEntry {
615 fn performer(&self) -> Performer {
616 self.request.options.performer()
617 }
618
619 fn media_streams(&self) -> Option<MediaStreams> {
620 self.request.options.media_streams()
621 }
622
623 fn chosen_choice(&self) -> Option<&str> {
624 if let Some(profile) = self.request.options.profile() {
626 return Some(sync_profile::to(profile));
627 }
628 match self.request.options.format() {
629 FormatChoice::Best => None,
630 FormatChoice::Id(id) => Some(id),
631 }
632 }
633
634 fn selectable_choices(&self) -> impl Iterator<Item = &str> {
635 let ways = self
636 .request
637 .options
638 .profile()
639 .map(|_| sync_profile::ALL)
640 .unwrap_or_default()
641 .iter()
642 .copied()
643 .map(sync_profile::to)
644 .collect::<Vec<_>>();
645 ways.into_iter().chain(self.formats.available().unwrap_or_default().iter().map(|format| format.id.as_str()))
646 }
647
648 fn choice_summary(&self, choice: &str) -> Option<&str> {
649 sync_profile::from(choice).map(SyncProfile::summary)
652 }
653}
654
655#[derive(Clone, Debug, PartialEq)]
657pub struct ManagerState {
658 page: ManagerPage<QueueId>,
659 queue: Vec<QueueEntry>,
660 selected: Option<QueueId>,
661 detail_control: Option<DetailControl>,
662 draft: String,
663 draft_cursor: usize,
664 output_draft: String,
665 output_draft_cursor: usize,
666 input_draft: String,
667 input_draft_cursor: usize,
668 message: Option<String>,
669 exit_requested: bool,
670 next_id: u64,
671}
672
673impl ManagerState {
674 #[must_use]
676 pub const fn downloads() -> Self {
677 Self {
678 page: ManagerPage::Collection,
679 queue: Vec::new(),
680 selected: None,
681 detail_control: None,
682 draft: String::new(),
683 draft_cursor: 0,
684 output_draft: String::new(),
685 output_draft_cursor: 0,
686 input_draft: String::new(),
687 input_draft_cursor: 0,
688 message: None,
689 exit_requested: false,
690 next_id: 0,
691 }
692 }
693
694 #[must_use]
696 pub const fn page(&self) -> ManagerPage<QueueId> {
697 self.page
698 }
699
700 #[must_use]
702 pub fn queue(&self) -> &[QueueEntry] {
703 &self.queue
704 }
705
706 #[must_use]
708 pub const fn selected_id(&self) -> Option<QueueId> {
709 self.selected
710 }
711
712 #[must_use]
714 pub const fn detail_control(&self) -> Option<DetailControl> {
715 self.detail_control
716 }
717
718 #[must_use]
720 pub fn selected(&self) -> Option<&QueueEntry> {
721 let selected = self.selected?;
722 self.entry(selected)
723 }
724
725 #[must_use]
727 pub fn entry(&self, id: QueueId) -> Option<&QueueEntry> {
728 self.queue.iter().find(|entry| entry.id == id)
729 }
730
731 #[must_use]
733 pub fn draft(&self) -> &str {
734 &self.draft
735 }
736
737 #[must_use]
739 pub fn output_draft(&self) -> &str {
740 &self.output_draft
741 }
742
743 #[must_use]
745 pub fn message(&self) -> Option<&str> {
746 self.message.as_deref()
747 }
748
749 pub fn waiting(&self) -> impl Iterator<Item = &QueueEntry> {
751 self.queue.iter().filter(|entry| entry.transfer.phase == TransferPhase::Waiting)
752 }
753
754 #[must_use]
756 pub fn first_waiting_format_catalog(&self) -> Option<&QueueEntry> {
757 self.queue.iter().find(|entry| entry.formats == FormatCatalog::Waiting)
758 }
759
760 #[must_use]
762 pub fn has_active_transfer(&self) -> bool {
763 self.queue.iter().any(|entry| {
764 matches!(
765 entry.transfer.phase,
766 TransferPhase::Extracting
767 | TransferPhase::Downloading
768 | TransferPhase::Paused
769 | TransferPhase::Publishing
770 )
771 })
772 }
773}
774
775impl DownloadCollectionAlg for ManagerState {
776 fn add_sources(mut self, sources: impl IntoIterator<Item = Self::Source>) -> Self {
777 let requests: Vec<Self::Source> = sources.into_iter().collect();
778 self.apply_manager_event(ManagerIntentOp::AddSources { requests });
779 self
780 }
781}
782
783impl NavigationStateAlg for ManagerState {
784 fn page(&self) -> ManagerPage<Self::Id> {
785 self.page
786 }
787
788 fn set_page(&mut self, page: ManagerPage<Self::Id>) {
789 self.page = page;
790 }
791}
792
793impl QueueCatalogAlg for ManagerState {
794 fn queue_ids(&self) -> impl Iterator<Item = Self::Id> {
795 self.queue.iter().map(|entry| entry.id)
796 }
797
798 fn selected_queue_id(&self) -> Option<Self::Id> {
799 self.selected
800 }
801
802 fn set_selected_queue_id(&mut self, selected: Option<Self::Id>) {
803 self.selected = selected;
804 }
805
806 fn queue_entry(&self, id: Self::Id) -> Option<&Self::Entry> {
807 self.entry(id)
808 }
809}
810
811impl QueueAppendAlg for ManagerState {
812 fn append_sources(&mut self, requests: Vec<Self::Source>) -> Vec<Self::Id> {
813 requests
814 .into_iter()
815 .map(|request| {
816 let id = QueueId(self.next_id);
817 self.next_id = self.next_id.saturating_add(1);
818 let output_custom = request.output.is_some();
819 let request_source = request.source.clone();
820 let formats = if request.options.performer() == Performer::Retrieval {
821 FormatCatalog::Waiting
822 } else {
823 FormatCatalog::Unknown
824 };
825 self.queue.push(QueueEntry {
826 id,
827 request,
828 transfer: TransferState::ready(),
829 formats,
830 media_id: None,
831 title: None,
832 output_custom,
833 log: vec![format!("added {}", request_source)],
834 rehearsal: Rehearsal::Unrehearsed,
835 });
836 refresh_default_output(self.queue.last_mut().expect("the request was appended"));
837 id
838 })
839 .collect()
840 }
841}
842
843impl QueueRemoveAlg for ManagerState {
844 fn remove_queue_entry(&mut self, id: Self::Id) {
845 self.queue.retain(|entry| entry.id != id);
846 }
847}
848
849impl QueuePauseAlg for ManagerState {
850 fn toggle_queue_pause(&mut self, id: Self::Id) {
851 let Some(entry) = self.queue.iter_mut().find(|entry| entry.id == id) else {
852 return;
853 };
854 match entry.space_action() {
855 Some(SpaceAction::Pause) => {
856 entry.transfer.resume_phase = Some(entry.transfer.phase);
857 entry.transfer.phase = TransferPhase::Paused;
858 }
859 Some(SpaceAction::Resume) => {
860 entry.transfer.phase = entry.transfer.resume_phase.take().unwrap_or(TransferPhase::Downloading);
861 }
862 Some(SpaceAction::Start | SpaceAction::Rehearse) | None => {}
864 }
865 }
866}
867
868impl QueueDuplicateAlg for ManagerState {
869 fn duplicate_queue_entry(&mut self, id: Self::Id) -> Option<Self::Id> {
870 let entry = self.queue.iter().find(|entry| entry.id == id)?.clone();
871 let source = entry.source().to_owned();
872 let options = entry.request.options.clone().rehearsing();
874 let output = entry.request.options.profile().and(entry.request.output.clone());
877 let stated = output.is_some();
878 let formats = match entry.formats {
879 FormatCatalog::Available(formats) => FormatCatalog::Available(formats),
880 FormatCatalog::Unknown | FormatCatalog::Waiting | FormatCatalog::Inspecting | FormatCatalog::Failed(_) => {
881 FormatCatalog::Unknown
882 }
883 };
884 let duplicate = QueueId(self.next_id);
885 self.next_id = self.next_id.saturating_add(1);
886 self.queue.push(QueueEntry {
887 id: duplicate,
888 request: SourceRequest::new(source, output, options),
889 transfer: TransferState::ready(),
890 formats,
891 media_id: entry.media_id,
892 title: entry.title,
893 output_custom: stated,
894 log: vec![format!("duplicated from download {}", entry.id.0)],
895 rehearsal: Rehearsal::Unrehearsed,
896 });
897 refresh_default_output(self.queue.last_mut().expect("duplicate was appended"));
898 Some(duplicate)
899 }
900}
901
902impl QueueFormatEditAlg for ManagerState {
903 fn cycle_queue_format(&mut self, id: Self::Id, forward: bool) {
904 let Some(entry) = self.queue.iter_mut().find(|entry| entry.id == id) else {
905 return;
906 };
907 if !entry.is_editable() {
908 return;
909 }
910 if entry.request.options.profile().is_some() {
912 let ways = sync_profile::ALL;
913 let current = entry
914 .request
915 .options
916 .profile()
917 .and_then(|profile| ways.iter().position(|way| *way == profile))
918 .unwrap_or(0);
919 let next = if forward {
920 (current + 1) % ways.len()
921 } else if current == 0 {
922 ways.len() - 1
923 } else {
924 current - 1
925 };
926 entry.request.options = entry.request.options.clone().with_profile(ways[next]);
927 entry.forget_rehearsal();
928 return;
929 }
930 let offered = entry.request.options.media_streams().map(|_| entry.offered_streams()).unwrap_or_default();
933 let choices: Vec<DownloadOptions> = offered
934 .into_iter()
935 .map(|streams| entry.request.options.clone().with_media_streams(streams))
936 .chain(
937 entry
938 .formats
939 .available()
940 .unwrap_or_default()
941 .iter()
942 .map(|format| entry.request.options.clone().with_format(FormatChoice::Id(format.id.clone()))),
943 )
944 .collect();
945 let current = choices.iter().position(|choice| *choice == entry.request.options).unwrap_or(0);
946 let next = if forward {
947 (current + 1) % choices.len()
948 } else if current == 0 {
949 choices.len() - 1
950 } else {
951 current - 1
952 };
953 entry.request.options = choices[next].clone();
954 refresh_default_output(entry);
955 }
956}
957
958impl FormatCatalogStateAlg for ManagerState {
959 fn request_format_catalog(&mut self, id: Self::Id) {
960 if let Some(entry) = self.queue.iter_mut().find(|entry| entry.id == id)
961 && entry.is_editable()
962 && entry.request.options.performer() == Performer::Retrieval
964 && matches!(
965 entry.formats,
966 FormatCatalog::Unknown | FormatCatalog::Failed(_)
967 )
968 {
969 entry.formats = FormatCatalog::Waiting;
970 }
971 }
972
973 fn apply_format_catalog_event(&mut self, id: Self::Id, event: FormatDiscoveryOp<Self::Format>) {
974 let Some(entry) = self.queue.iter_mut().find(|entry| entry.id == id) else {
975 return;
976 };
977 entry.formats = match event {
978 FormatDiscoveryOp::Started {} => {
979 entry.note("inspecting source formats");
980 FormatCatalog::Inspecting
981 }
982 FormatDiscoveryOp::Available { formats } => {
983 entry.note(format!("extraction described {} formats", formats.len()));
984 for format in &formats {
985 entry.note(described_format(format));
986 }
987 FormatCatalog::Available(formats)
988 }
989 FormatDiscoveryOp::Failed { message } => {
990 entry.note(format!("format inspection failed: {message}"));
991 FormatCatalog::Failed(message)
992 }
993 };
994 if matches!(entry.request.options.format(), FormatChoice::Best)
999 && let Some(asked) = entry.request.options.media_streams()
1000 {
1001 let offered = entry.offered_streams();
1002 if let Some(carried) = offered.first().copied() {
1003 if !offered.contains(&asked) {
1004 entry.request.options = entry.request.options.clone().with_media_streams(carried);
1005 }
1006 } else if let Some(named) =
1007 entry.formats.available().and_then(|described| described.first()).map(|format| format.id.clone())
1008 {
1009 entry.request.options = entry.request.options.clone().with_format(FormatChoice::Id(named));
1010 }
1011 }
1012 refresh_default_output(entry);
1013 }
1014}
1015
1016impl OutputDraftAlg for ManagerState {
1017 fn output_draft(&self) -> &str {
1018 &self.output_draft
1019 }
1020
1021 fn set_output_draft(&mut self, draft: String) {
1022 self.output_draft_cursor = draft.len();
1023 self.output_draft = draft;
1024 }
1025}
1026
1027impl InputDraftAlg for ManagerState {
1028 fn input_draft(&self) -> &str {
1029 &self.input_draft
1030 }
1031
1032 fn set_input_draft(&mut self, draft: String) {
1033 self.input_draft_cursor = draft.len();
1034 self.input_draft = draft;
1035 }
1036}
1037
1038impl QueueSourceAlg for ManagerState {
1039 fn set_queue_source(&mut self, id: Self::Id, source: String) {
1040 if let Some(entry) = self.queue.iter_mut().find(|entry| entry.id == id)
1041 && entry.is_editable()
1042 {
1043 entry.note(format!("input changed to {source}"));
1044 entry.request.source = source;
1045 entry.forget_rehearsal();
1046 refresh_default_output(entry);
1047 }
1048 }
1049}
1050
1051impl QueueOutputAlg for ManagerState {
1052 fn set_queue_output(&mut self, id: Self::Id, output: PathBuf) {
1053 if let Some(entry) = self.queue.iter_mut().find(|entry| entry.id == id)
1054 && entry.is_editable()
1055 {
1056 entry.request.output = Some(output);
1057 entry.output_custom = true;
1058 entry.forget_rehearsal();
1059 }
1060 }
1061}
1062
1063impl DownloadLogAlg for ManagerState {
1064 fn note_download(&mut self, id: Self::Id, note: String) {
1065 if let Some(entry) = self.queue.iter_mut().find(|entry| entry.id == id) {
1066 entry.note(note);
1067 }
1068 }
1069}
1070
1071impl SourceMetadataAlg for ManagerState {
1072 fn apply_source_metadata(&mut self, id: Self::Id, media_id: String, title: Option<String>) {
1073 if let Some(entry) = self.queue.iter_mut().find(|entry| entry.id == id) {
1074 entry.note(match &title {
1075 Some(title) => format!("extracted {media_id}: {title}"),
1076 None => format!("extracted {media_id}"),
1077 });
1078 entry.media_id = Some(media_id);
1079 entry.title = title;
1080 refresh_default_output(entry);
1081 }
1082 }
1083}
1084
1085fn refresh_default_output(entry: &mut QueueEntry) {
1086 if entry.output_custom {
1087 return;
1088 }
1089 if entry.request.options.performer() == Performer::Program {
1093 let source = RsyncSyntax.read_endpoint(&entry.request.source);
1094 entry.request.output = Some(PathBuf::from(RsyncSyntax.endpoint_name(&source)));
1095 return;
1096 }
1097 let Some(media_id) = entry.media_id.as_deref() else {
1098 return;
1099 };
1100 entry.request.output = Some(portable_file_name(entry.title.as_deref(), media_id, selected_extension(entry)));
1101}
1102
1103fn selected_extension(entry: &QueueEntry) -> Option<&str> {
1104 let formats = entry.formats.available()?;
1105 match entry.request.options.format() {
1106 FormatChoice::Best => formats
1107 .iter()
1108 .rev()
1109 .find(|format| entry.request.options.media_streams().is_some_and(|streams| role_accepts(streams, format)))
1110 .and_then(Format::extension),
1111 FormatChoice::Id(id) => formats.iter().find(|format| format.id == *id).and_then(Format::extension),
1112 }
1113}
1114
1115impl TransferStateAlg for ManagerState {
1116 fn set_waiting(&mut self, id: Self::Id) {
1117 if let Some(entry) = self.queue.iter_mut().find(|entry| entry.id == id)
1118 && entry.is_editable()
1119 && entry.output().is_some()
1120 {
1121 entry.transfer = TransferState::ready();
1122 entry.transfer.phase = TransferPhase::Waiting;
1123 }
1124 }
1125
1126 fn restart_waiting(&mut self, id: Self::Id) {
1127 if let Some(entry) = self.queue.iter_mut().find(|entry| entry.id == id)
1128 && entry.transfer.phase == TransferPhase::Failed
1129 {
1130 entry.transfer = TransferState::ready();
1131 entry.transfer.phase = TransferPhase::Waiting;
1132 }
1133 }
1134
1135 fn apply_transfer_event(&mut self, id: Self::Id, event: TransferObservationOp) {
1136 let Some(entry) = self.queue.iter_mut().find(|entry| entry.id == id) else {
1137 return;
1138 };
1139 event.interpret(&mut EntryObservation(entry));
1140 }
1141}
1142
1143struct EntryObservation<'a>(&'a mut QueueEntry);
1147
1148impl TransferObservationInterpreter for EntryObservation<'_> {
1149 fn pause_capability(&mut self, supported: bool) {
1150 self.0.transfer.pause_supported = supported;
1151 }
1152
1153 fn started(&mut self) {
1154 self.0.note("retrieval started");
1155 self.0.transfer.phase = TransferPhase::Extracting;
1156 }
1157
1158 fn elapsed(&mut self, elapsed: Duration) {
1159 self.0.transfer.elapsed = elapsed;
1160 }
1161
1162 fn progress(&mut self, destination: PathBuf, downloaded: u64, total: Option<u64>) {
1163 let observed_phase = if total.is_some_and(|total| downloaded >= total) {
1164 TransferPhase::Publishing
1165 } else {
1166 TransferPhase::Downloading
1167 };
1168 if self.0.transfer.phase == TransferPhase::Paused {
1169 self.0.transfer.resume_phase = Some(observed_phase);
1170 } else {
1171 self.0.transfer.phase = observed_phase;
1172 }
1173 self.0.transfer.destination = Some(destination);
1174 self.0.transfer.downloaded = downloaded;
1175 self.0.transfer.total = total;
1176 }
1177
1178 fn completed(&mut self, destination: PathBuf, bytes: u64) {
1179 self.0.note(format!("published {} ({bytes} bytes)", destination.display()));
1180 self.0.transfer.phase = TransferPhase::Complete;
1181 self.0.transfer.terminal = Some(DownloadEvent::Succeeded { destination, bytes });
1182 }
1183
1184 fn failed(&mut self, destination: PathBuf, message: String) {
1185 self.0.note(format!("retrieval failed at {}: {message}", destination.display()));
1186 self.0.transfer.phase = TransferPhase::Failed;
1187 self.0.transfer.terminal = Some(DownloadEvent::Failed { destination, message });
1188 }
1189
1190 fn program_failed(&mut self, summary: String, detail: String) {
1191 self.0.note(format!("{summary}: {detail}"));
1192 self.0.transfer.phase = TransferPhase::Failed;
1193 self.0.transfer.summary = Some(summary);
1194 self.0.transfer.detail = Some(detail);
1195 }
1196}
1197
1198impl ManagerState {
1199 pub fn transfer_request(&mut self, id: QueueId) {
1204 if let Some(entry) = self.queue.iter_mut().find(|entry| entry.id == id) {
1205 entry.request.options = DownloadOptions::transfer();
1206 }
1207 }
1208}
1209
1210impl QueueDryRunAlg for ManagerState {
1211 fn set_queue_dry_run(&mut self, id: Self::Id, dry_run: bool) {
1212 if let Some(entry) = self.queue.iter_mut().find(|entry| entry.id == id)
1213 && entry.is_editable()
1214 {
1215 entry.request.options.set_dry_run(dry_run);
1216 entry.note(if dry_run {
1217 "dry run enabled: the next run will state what it would do and change nothing"
1218 } else {
1219 "dry run disabled: the next run will perform the transfer"
1220 });
1221 }
1222 }
1223}
1224
1225impl RehearsalStateAlg for ManagerState {
1226 fn apply_rehearsal_event(&mut self, id: Self::Id, event: RehearsalObservationOp<Self::Change>) {
1227 let Some(entry) = self.queue.iter_mut().find(|entry| entry.id == id) else {
1228 return;
1229 };
1230 match event {
1231 RehearsalObservationOp::Started {} => {
1232 entry.note("rehearsing what the transfer would do");
1233 entry.transfer.phase = TransferPhase::Rehearsing;
1234 entry.rehearsal = Rehearsal::Rehearsing;
1235 }
1236 RehearsalObservationOp::Reported { changes } => {
1237 let altered = changes.iter().filter(|change| change.kind.alters()).count();
1238 entry.note(format!("the rehearsal stated {altered} of {} paths would change", changes.len()));
1239 entry.transfer.phase = TransferPhase::Ready;
1241 entry.rehearsal = Rehearsal::Reported(changes);
1242 }
1243 RehearsalObservationOp::Failed { message } => {
1244 entry.note(format!("rehearsal failed: {message}"));
1245 entry.transfer.phase = TransferPhase::Ready;
1246 entry.rehearsal = Rehearsal::Failed(message);
1247 }
1248 }
1249 }
1250}
1251
1252impl DetailSelectionAlg for ManagerState {
1253 fn selected_detail_control(&self) -> Option<DetailControl> {
1254 self.detail_control
1255 }
1256
1257 fn set_selected_detail_control(&mut self, control: Option<DetailControl>) {
1258 self.detail_control = control;
1259 }
1260}
1261
1262impl DraftStateAlg for ManagerState {
1263 fn draft(&self) -> &str {
1264 &self.draft
1265 }
1266
1267 fn set_draft(&mut self, draft: String) {
1268 self.draft_cursor = draft.len();
1269 self.draft = draft;
1270 }
1271}
1272
1273impl TextEditorStateAlg for ManagerState {
1274 fn active_text_editor(&self) -> Option<(&str, usize)> {
1275 match self.page {
1276 ManagerPage::AddSources => Some((&self.draft, self.draft_cursor)),
1277 ManagerPage::Output(_) => Some((&self.output_draft, self.output_draft_cursor)),
1278 ManagerPage::Input(_) => Some((&self.input_draft, self.input_draft_cursor)),
1279 ManagerPage::Collection
1280 | ManagerPage::Details(_)
1281 | ManagerPage::Formats(_)
1282 | ManagerPage::Log(_)
1283 | ManagerPage::Report(_)
1284 | ManagerPage::Command(_) => None,
1285 }
1286 }
1287
1288 fn set_active_text_editor(&mut self, text: String, cursor: usize) {
1289 let mut cursor = cursor.min(text.len());
1290 while !text.is_char_boundary(cursor) {
1291 cursor = cursor.saturating_sub(1);
1292 }
1293 match self.page {
1294 ManagerPage::AddSources => {
1295 self.draft = text;
1296 self.draft_cursor = cursor;
1297 }
1298 ManagerPage::Output(_) => {
1299 self.output_draft = text;
1300 self.output_draft_cursor = cursor;
1301 }
1302 ManagerPage::Input(_) => {
1303 self.input_draft = text;
1304 self.input_draft_cursor = cursor;
1305 }
1306 ManagerPage::Collection
1307 | ManagerPage::Details(_)
1308 | ManagerPage::Formats(_)
1309 | ManagerPage::Log(_)
1310 | ManagerPage::Report(_)
1311 | ManagerPage::Command(_) => {}
1312 }
1313 }
1314}
1315
1316impl ManagerStatusAlg for ManagerState {
1317 fn manager_message(&self) -> Option<&str> {
1318 self.message()
1319 }
1320
1321 fn set_manager_message(&mut self, message: Option<String>) {
1322 self.message = message;
1323 }
1324}
1325
1326impl SafeExitAlg for ManagerState {
1327 fn exit_requested(&self) -> bool {
1328 self.exit_requested
1329 }
1330
1331 fn request_safe_exit(&mut self) {
1332 self.exit_requested = true;
1333 }
1334}
1335
1336fn submitted(line: &str) -> SourceRequest {
1343 let trimmed = line.trim();
1344 if recognized(trimmed) {
1347 return SourceRequest::new(trimmed, None, DownloadOptions::progressive());
1348 }
1349 if let Some((source, destination)) = RsyncSyntax.read_transfer(line) {
1350 return SourceRequest::new(
1351 RsyncSyntax.endpoint_text(&source),
1352 Some(PathBuf::from(RsyncSyntax.endpoint_text(&destination))),
1353 DownloadOptions::transfer(),
1354 );
1355 }
1356 SourceRequest::new(trimmed, None, DownloadOptions::transfer())
1357}
1358
1359const RETRIEVED_SCHEMES: [&str; 3] = ["https://", "http://", "fixture://"];
1365
1366fn recognized(line: &str) -> bool {
1372 RETRIEVED_SCHEMES.iter().any(|scheme| line.starts_with(scheme))
1373}
1374
1375fn described_format(format: &Format) -> String {
1377 format!(" {}", format.format_label())
1378}