1use alux_ext::ext;
2use rsynko_manager::*;
3use std::time::Duration;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum Emphasis {
11 Plain,
13 Name,
15 Heading,
17 Label,
19 Selected,
21 Muted,
23 Running,
25 Held,
27 Finishing,
29 Succeeded,
31 Failed,
33 Safe,
35 Caution,
37}
38
39#[ext(name = PhaseVocabularyExt)]
41pub impl TransferPhase {
42 fn phase_label(self) -> &'static str {
44 match self {
45 Self::Ready => "Ready",
46 Self::Waiting => "Waiting",
47 Self::Rehearsing => "Rehearsing",
48 Self::Extracting => "Extracting",
49 Self::Downloading => "Downloading",
50 Self::Paused => "Paused",
51 Self::Publishing => "Publishing",
52 Self::Complete => "Complete",
53 Self::Failed => "Failed",
54 }
55 }
56
57 fn phase_emphasis(self) -> Emphasis {
59 match self {
60 Self::Ready | Self::Waiting => Emphasis::Plain,
61 Self::Rehearsing | Self::Extracting | Self::Downloading => Emphasis::Running,
62 Self::Paused => Emphasis::Held,
63 Self::Publishing => Emphasis::Finishing,
64 Self::Complete => Emphasis::Succeeded,
65 Self::Failed => Emphasis::Failed,
66 }
67 }
68
69 fn phase_marker(self) -> &'static str {
74 match self {
75 Self::Ready | Self::Paused => "○",
76 Self::Rehearsing => "◌",
77 Self::Waiting
78 | Self::Extracting
79 | Self::Downloading
80 | Self::Publishing
81 | Self::Complete
82 | Self::Failed => "●",
83 }
84 }
85}
86
87#[ext(name = ControlVocabularyExt)]
89pub impl DetailControl {
90 fn control_label(self) -> &'static str {
92 match self {
93 Self::Input => "Input",
94 Self::Output => "File name",
95 Self::Format => "Format",
96 Self::Command => "Command",
97 Self::Restart => "Restart",
98 Self::DryRun => "Dry run",
99 Self::Report => "Report",
100 Self::Log => "Log",
101 Self::Duplicate => "Duplicate",
102 Self::Delete => "Delete",
103 }
104 }
105
106 fn states_value(self) -> bool {
108 matches!(
109 self,
110 Self::Input | Self::Output | Self::Format | Self::Command | Self::Report | Self::Log
111 )
112 }
113}
114
115#[ext(name = StreamVocabularyExt)]
117pub impl MediaStreams {
118 fn streams_label(self) -> &'static str {
120 match self {
121 Self::AudioVideo => "audio + video",
122 Self::Audio => "audio only",
123 Self::Video => "video only",
124 }
125 }
126}
127
128#[ext(name = ChangeVocabularyExt)]
130pub impl ChangeKind {
131 fn change_label(self) -> &'static str {
133 match self {
134 Self::Create => "new",
135 Self::Update => "updated",
136 Self::Delete => "deleted",
137 Self::Unchanged => "unchanged",
138 }
139 }
140
141 fn change_marker(self) -> &'static str {
143 match self {
144 Self::Create => "+",
145 Self::Update => "~",
146 Self::Delete => "-",
147 Self::Unchanged => "=",
148 }
149 }
150
151 fn change_emphasis(self) -> Emphasis {
153 match self {
154 Self::Create => Emphasis::Safe,
155 Self::Update => Emphasis::Caution,
156 Self::Delete => Emphasis::Failed,
157 Self::Unchanged => Emphasis::Muted,
159 }
160 }
161}
162
163#[ext(name = SpaceVocabularyExt)]
165pub impl SpaceAction {
166 fn space_label(self) -> &'static str {
168 match self {
169 Self::Start => "Start",
170 Self::Rehearse => "Dry run",
171 Self::Pause => "Pause",
172 Self::Resume => "Resume",
173 }
174 }
175}
176
177pub const UNSTATED: &str = "—";
179
180#[must_use]
182pub fn bytes_label(bytes: u64) -> String {
183 const UNITS: [&str; 5] = ["B", "KiB", "MiB", "GiB", "TiB"];
185 let mut divisor = 1_u64;
186 let mut unit = 0_usize;
187 while bytes / divisor >= 1024 && unit < UNITS.len() - 1 {
188 divisor = divisor.saturating_mul(1024);
189 unit += 1;
190 }
191 if unit == 0 {
192 format!("{bytes} {}", UNITS[unit])
193 } else {
194 let whole = bytes / divisor;
195 let tenth = bytes % divisor / (divisor / 10);
196 format!("{whole}.{tenth} {}", UNITS[unit])
197 }
198}
199
200#[must_use]
202pub fn duration_label(duration: Duration) -> String {
203 let seconds = duration.as_secs();
204 let hours = seconds / 3600;
205 let minutes = seconds % 3600 / 60;
206 let seconds = seconds % 60;
207 if hours > 0 {
208 format!("{hours}:{minutes:02}:{seconds:02}")
209 } else {
210 format!("{minutes:02}:{seconds:02}")
211 }
212}
213
214#[must_use]
216pub fn elided(text: &str, columns: usize) -> String {
217 if text.chars().count() <= columns {
218 return text.to_owned();
219 }
220 text.chars()
221 .take(columns.saturating_sub(1))
222 .collect::<String>()
223 + "…"
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229 use crate::{
230 ARMED_MODE, CURSOR_MARK, EXPANDED_MARK, Gauge, Key, PATH_SEPARATOR, REHEARSING_MODE,
231 UNSTATED,
232 };
233 use rsynko_manager::{ChangeKind, DetailControl, MediaStreams, SpaceAction, TransferPhase};
234
235 const STATED: [char; 21] = [
244 '█', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '○', '●', '◌', '▸', '▾', '›', '—', '↑', '↓', '←', '→', '\u{26A0}', '\u{FE0F}', ];
252
253 fn stated_words() -> Vec<String> {
255 let phases = TransferPhase::LIFECYCLE
256 .into_iter()
257 .flat_map(|phase| [phase.phase_label(), phase.phase_marker()]);
258 let changes = ChangeKind::REPORTED
259 .into_iter()
260 .flat_map(|kind| [kind.change_label(), kind.change_marker()]);
261 let streams = MediaStreams::OFFERED
262 .into_iter()
263 .map(MediaStreams::streams_label);
264 let controls = [
265 DetailControl::Input,
266 DetailControl::Output,
267 DetailControl::Format,
268 DetailControl::Command,
269 DetailControl::Restart,
270 DetailControl::DryRun,
271 DetailControl::Report,
272 DetailControl::Log,
273 DetailControl::Duplicate,
274 DetailControl::Delete,
275 ]
276 .into_iter()
277 .map(DetailControl::control_label);
278 let spaces = [
279 SpaceAction::Start,
280 SpaceAction::Rehearse,
281 SpaceAction::Pause,
282 SpaceAction::Resume,
283 ]
284 .into_iter()
285 .map(SpaceAction::space_label);
286 phases
287 .chain(changes)
288 .chain(streams)
289 .chain(controls)
290 .chain(spaces)
291 .chain([UNSTATED, REHEARSING_MODE, ARMED_MODE])
292 .map(str::to_owned)
293 .chain(Gauge::LEADING.iter().map(|leading| (*leading).to_owned()))
294 .chain((0..=100_u16).map(|percent| Gauge::of(percent, 8).text()))
295 .chain([Key::Up, Key::Down, Key::Left, Key::Right].map(Key::label))
296 .chain([CURSOR_MARK, EXPANDED_MARK, PATH_SEPARATOR].map(str::to_owned))
297 .collect()
298 }
299
300 #[test]
301 fn every_stated_character_is_one_a_reader_has_been_told_about() {
302 let unexpected = stated_words()
303 .iter()
304 .flat_map(|word| word.chars().collect::<Vec<_>>())
305 .filter(|stated| !stated.is_ascii() && !STATED.contains(stated))
306 .collect::<Vec<_>>();
307
308 assert!(
309 unexpected.is_empty(),
310 "these are stated without having been checked for width: {unexpected:?}"
311 );
312 }
313}