use alux_ext::ext;
use rsynko_manager::*;
use std::time::Duration;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Emphasis {
Plain,
Name,
Heading,
Label,
Selected,
Muted,
Running,
Held,
Finishing,
Succeeded,
Failed,
Safe,
Caution,
}
#[ext(name = PhaseVocabularyExt)]
pub impl TransferPhase {
fn phase_label(self) -> &'static str {
match self {
Self::Ready => "Ready",
Self::Waiting => "Waiting",
Self::Rehearsing => "Rehearsing",
Self::Extracting => "Extracting",
Self::Downloading => "Downloading",
Self::Paused => "Paused",
Self::Publishing => "Publishing",
Self::Complete => "Complete",
Self::Failed => "Failed",
}
}
fn phase_emphasis(self) -> Emphasis {
match self {
Self::Ready | Self::Waiting => Emphasis::Plain,
Self::Rehearsing | Self::Extracting | Self::Downloading => Emphasis::Running,
Self::Paused => Emphasis::Held,
Self::Publishing => Emphasis::Finishing,
Self::Complete => Emphasis::Succeeded,
Self::Failed => Emphasis::Failed,
}
}
fn phase_marker(self) -> &'static str {
match self {
Self::Ready | Self::Paused => "○",
Self::Rehearsing => "◌",
Self::Waiting | Self::Extracting | Self::Downloading | Self::Publishing | Self::Complete | Self::Failed => {
"●"
}
}
}
}
#[ext(name = ControlVocabularyExt)]
pub impl DetailControl {
fn control_label(self) -> &'static str {
match self {
Self::Input => "Input",
Self::Output => "File name",
Self::Format => "Format",
Self::Command => "Command",
Self::Restart => "Restart",
Self::DryRun => "Dry run",
Self::Report => "Report",
Self::Log => "Log",
Self::Duplicate => "Duplicate",
Self::Delete => "Delete",
}
}
fn states_value(self) -> bool {
matches!(self, Self::Input | Self::Output | Self::Format | Self::Command | Self::Report | Self::Log)
}
}
#[ext(name = StreamVocabularyExt)]
pub impl MediaStreams {
fn streams_label(self) -> &'static str {
match self {
Self::AudioVideo => "audio + video",
Self::Audio => "audio only",
Self::Video => "video only",
}
}
}
#[ext(name = ChangeVocabularyExt)]
pub impl ChangeKind {
fn change_label(self) -> &'static str {
match self {
Self::Create => "new",
Self::Update => "updated",
Self::Delete => "deleted",
Self::Unchanged => "unchanged",
}
}
fn change_marker(self) -> &'static str {
match self {
Self::Create => "+",
Self::Update => "~",
Self::Delete => "-",
Self::Unchanged => "=",
}
}
fn change_emphasis(self) -> Emphasis {
match self {
Self::Create => Emphasis::Safe,
Self::Update => Emphasis::Caution,
Self::Delete => Emphasis::Failed,
Self::Unchanged => Emphasis::Muted,
}
}
}
#[ext(name = SpaceVocabularyExt)]
pub impl SpaceAction {
fn space_label(self) -> &'static str {
match self {
Self::Start => "Start",
Self::Rehearse => "Dry run",
Self::Pause => "Pause",
Self::Resume => "Resume",
}
}
}
pub const UNSTATED: &str = "—";
#[must_use]
pub fn bytes_label(bytes: u64) -> String {
const UNITS: [&str; 5] = ["B", "KiB", "MiB", "GiB", "TiB"];
let mut divisor = 1_u64;
let mut unit = 0_usize;
while bytes / divisor >= 1024 && unit < UNITS.len() - 1 {
divisor = divisor.saturating_mul(1024);
unit += 1;
}
if unit == 0 {
format!("{bytes} {}", UNITS[unit])
} else {
let whole = bytes / divisor;
let tenth = bytes % divisor / (divisor / 10);
format!("{whole}.{tenth} {}", UNITS[unit])
}
}
#[must_use]
pub fn duration_label(duration: Duration) -> String {
let seconds = duration.as_secs();
let hours = seconds / 3600;
let minutes = seconds % 3600 / 60;
let seconds = seconds % 60;
if hours > 0 { format!("{hours}:{minutes:02}:{seconds:02}") } else { format!("{minutes:02}:{seconds:02}") }
}
#[must_use]
pub fn elided(text: &str, columns: usize) -> String {
if text.chars().count() <= columns {
return text.to_owned();
}
text.chars().take(columns.saturating_sub(1)).collect::<String>() + "…"
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ARMED_MODE, CURSOR_MARK, EXPANDED_MARK, Gauge, Key, PATH_SEPARATOR, REHEARSING_MODE, UNSTATED};
use rsynko_manager::{ChangeKind, DetailControl, MediaStreams, SpaceAction, TransferPhase};
const STATED: [char; 21] = [
'█', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '○', '●', '◌', '▸', '▾', '›', '—', '↑', '↓', '←', '→', '\u{26A0}', '\u{FE0F}', ];
fn stated_words() -> Vec<String> {
let phases = TransferPhase::LIFECYCLE.into_iter().flat_map(|phase| [phase.phase_label(), phase.phase_marker()]);
let changes = ChangeKind::REPORTED.into_iter().flat_map(|kind| [kind.change_label(), kind.change_marker()]);
let streams = MediaStreams::OFFERED.into_iter().map(MediaStreams::streams_label);
let controls = [
DetailControl::Input,
DetailControl::Output,
DetailControl::Format,
DetailControl::Command,
DetailControl::Restart,
DetailControl::DryRun,
DetailControl::Report,
DetailControl::Log,
DetailControl::Duplicate,
DetailControl::Delete,
]
.into_iter()
.map(DetailControl::control_label);
let spaces = [SpaceAction::Start, SpaceAction::Rehearse, SpaceAction::Pause, SpaceAction::Resume]
.into_iter()
.map(SpaceAction::space_label);
phases
.chain(changes)
.chain(streams)
.chain(controls)
.chain(spaces)
.chain([UNSTATED, REHEARSING_MODE, ARMED_MODE])
.map(str::to_owned)
.chain(Gauge::LEADING.iter().map(|leading| (*leading).to_owned()))
.chain((0..=100_u16).map(|percent| Gauge::of(percent, 8).text()))
.chain([Key::Up, Key::Down, Key::Left, Key::Right].map(Key::label))
.chain([CURSOR_MARK, EXPANDED_MARK, PATH_SEPARATOR].map(str::to_owned))
.collect()
}
#[test]
fn every_stated_character_is_one_a_reader_has_been_told_about() {
let unexpected = stated_words()
.iter()
.flat_map(|word| word.chars().collect::<Vec<_>>())
.filter(|stated| !stated.is_ascii() && !STATED.contains(stated))
.collect::<Vec<_>>();
assert!(unexpected.is_empty(), "these are stated without having been checked for width: {unexpected:?}");
}
}