about summary refs log tree commit diff stats
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/colors/Cargo.toml26
-rw-r--r--crates/colors/src/custom.rs65
-rw-r--r--crates/colors/src/lib.rs87
-rw-r--r--crates/colors/src/list.rs223
-rw-r--r--crates/colors/src/support.rs116
-rw-r--r--crates/yt/Cargo.toml1
-rw-r--r--crates/yt/src/config/mod.rs16
-rw-r--r--crates/yt/src/storage/db/insert/playlist.rs3
-rw-r--r--crates/yt/src/videos/mod.rs210
-rw-r--r--crates/yt/tests/_testenv/run.rs8
-rw-r--r--crates/yt/tests/watch/mod.rs12
11 files changed, 727 insertions, 40 deletions
diff --git a/crates/colors/Cargo.toml b/crates/colors/Cargo.toml
new file mode 100644
index 0000000..4edefcf
--- /dev/null
+++ b/crates/colors/Cargo.toml
@@ -0,0 +1,26 @@
+# yt - A fully featured command line YouTube client
+#
+# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+# Copyright (C) 2025 uutils developers
+# SPDX-License-Identifier: MIT
+#
+# This file is part of Yt.
+#
+# You should have received a copy of the License along with this program.
+# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+[package]
+name = "colors"
+authors.workspace = true
+license.workspace = true
+description = "A owo-colors inspired color crate."
+version.workspace = true
+edition.workspace = true
+repository.workspace = true
+rust-version.workspace = true
+publish = false
+
+[dependencies]
+
+[lints]
+workspace = true
diff --git a/crates/colors/src/custom.rs b/crates/colors/src/custom.rs
new file mode 100644
index 0000000..2adcfa9
--- /dev/null
+++ b/crates/colors/src/custom.rs
@@ -0,0 +1,65 @@
+// Taken from <https://github.com/owo-colors/owo-colors/blob/61f8bba2f5f80e9f4fa600fbfdf2c21656f1d523/src/colors/custom.rs>
+// at 2025-07-16T18:05:55 CEST.
+
+const U8_TO_STR: [[u8; 3]; 256] = generate_lookup();
+
+const fn generate_lookup() -> [[u8; 3]; 256] {
+    let mut table = [[0, 0, 0]; 256];
+
+    let mut i = 0;
+    while i < 256 {
+        table[i] = [
+            b'0' + (i / 100) as u8,
+            b'0' + (i / 10 % 10) as u8,
+            b'0' + (i % 10) as u8,
+        ];
+        i += 1;
+    }
+
+    table
+}
+
+#[derive(Clone, Copy)]
+pub(crate) enum Plane {
+    Fg,
+    Bg,
+}
+
+pub(crate) const fn rgb_to_ansi(r: u8, g: u8, b: u8, plane: Plane) -> [u8; 18] {
+    let mut buf = *b"\x1b[p8;2;rrr;ggg;bbb";
+
+    let r = U8_TO_STR[r as usize];
+    let g = U8_TO_STR[g as usize];
+    let b = U8_TO_STR[b as usize];
+
+    // p 2
+    buf[2] = match plane {
+        Plane::Fg => b'3',
+        Plane::Bg => b'4',
+    };
+
+    // r 7
+    buf[7] = r[0];
+    buf[8] = r[1];
+    buf[9] = r[2];
+
+    // g 11
+    buf[11] = g[0];
+    buf[12] = g[1];
+    buf[13] = g[2];
+
+    // b 15
+    buf[15] = b[0];
+    buf[16] = b[1];
+    buf[17] = b[2];
+
+    buf
+}
+
+/// This exists since [`unwrap()`] isn't const-safe (it invokes formatting infrastructure)
+pub(crate) const fn bytes_to_str(bytes: &'static [u8]) -> &'static str {
+    match core::str::from_utf8(bytes) {
+        Ok(o) => o,
+        Err(_e) => panic!("Const parsing &[u8] to a string failed!"),
+    }
+}
diff --git a/crates/colors/src/lib.rs b/crates/colors/src/lib.rs
new file mode 100644
index 0000000..ee2c1f8
--- /dev/null
+++ b/crates/colors/src/lib.rs
@@ -0,0 +1,87 @@
+use std::fmt::{Display, Write};
+
+use crate::{
+    list::{elements, methods},
+    support::{CSE, CSI, elements_inner},
+};
+
+pub mod custom;
+mod list;
+mod support;
+
+#[derive(Debug)]
+pub struct Canvas<I: Display>(I);
+
+impl<I: Display> Colorize for Canvas<I> {
+    fn render_into(self, base: &mut String, use_colors: bool) {
+        write!(base, "{}", self.0).expect("Is written into a string");
+
+        if use_colors {
+            // Reset the color and style, if we used colours.
+            base.write_str(CSI).expect("In-memory write");
+            base.write_str("0").expect("In-memory write");
+            base.write_str(CSE).expect("In-memory write");
+        }
+    }
+}
+
+pub trait IntoCanvas: Display + Sized {
+    fn into_canvas(self) -> Canvas<Self> {
+        Canvas(self)
+    }
+
+    methods! { IntoCanvas }
+}
+
+impl<I: Display> IntoCanvas for I {}
+
+pub trait Colorize: Sized {
+    /// Turn this colorized struct into a string, by writing into the base.
+    fn render_into(self, base: &mut String, use_colors: bool);
+
+    /// Turn this colorized struct into a string for consumption.
+    fn render(self, use_colors: bool) -> String {
+        let mut base = String::new();
+        self.render_into(&mut base, use_colors);
+        base
+    }
+
+    methods! { Colorize }
+}
+
+elements! {}
+
+#[cfg(test)]
+mod tests {
+    use crate::{Colorize, IntoCanvas};
+
+    #[test]
+    fn test_colorize_basic() {
+        let base = "Base".green().render(true);
+        #[rustfmt::skip]
+        let expected = concat!(
+            "\x1b[32m",
+            "Base",
+            "\x1b[0m",
+        );
+
+        assert_eq!(base.as_str(), expected);
+    }
+
+    #[test]
+    fn test_colorize_combo() {
+        let base = "Base".green().on_red().bold().strike_through().render(true);
+
+        #[rustfmt::skip]
+        let expected = concat!(
+            "\x1b[9m",  // strike_through
+            "\x1b[1m",  // bold
+            "\x1b[41m", // on_red
+            "\x1b[32m", // green
+            "Base",
+            "\x1b[0m",
+        );
+
+        assert_eq!(base.as_str(), expected);
+    }
+}
diff --git a/crates/colors/src/list.rs b/crates/colors/src/list.rs
new file mode 100644
index 0000000..ecbe465
--- /dev/null
+++ b/crates/colors/src/list.rs
@@ -0,0 +1,223 @@
+use crate::support::prepend_input;
+
+prepend_input! {
+    crate::support::methods_inner as methods (($tt:tt) -> {$tt}),
+    crate::support::elements_inner as elements,
+
+    <shared_input>
+    {
+        // Colors
+        Black black 30,
+        OnBlack on_black 40,
+
+        Red red 31,
+        OnRed on_red 41,
+
+        Green green 32,
+        OnGreen on_green 42,
+
+        Yellow yellow 33,
+        OnYellow on_yellow 43,
+
+        Blue blue 34,
+        OnBlue on_blue 44,
+
+        Magenta magenta 35,
+        OnMagenta on_magenta 45,
+
+        Cyan cyan 36,
+        OnCyan on_cyan 46,
+
+        White white 37,
+        OnWhite on_white 47,
+
+        Default default 39,
+        OnDefault on_default 49,
+
+        // Bright bright colors
+        BrightBlack bright_black 90,
+        OnBrightBlack on_bright_black 100,
+
+        BrightRed bright_red 91,
+        OnBrightRed on_bright_red 101,
+
+        BrightGreen bright_green 92,
+        OnBrightGreen on_bright_green 102,
+
+        BrightYellow bright_yellow 93,
+        OnBrightYellow on_bright_yellow 103,
+
+        BrightBlue bright_blue 94,
+        OnBrightBlue on_bright_blue 104,
+
+        BrightMagenta bright_magenta 95,
+        OnBrightMagenta on_bright_magenta 105,
+
+        BrightCyan bright_cyan 96,
+        OnBrightCyan on_bright_cyan 106,
+
+        BrightWhite bright_white 97,
+        OnBrightWhite on_bright_white 107,
+
+        // CSS colors
+        // TODO(@bpeetz): Also support background colors with these values. <2025-07-16>
+        AliceBlue alice_blue (240, 248, 255),
+        AntiqueWhite antique_white (250, 235, 215),
+        Aqua aqua (0, 255, 255),
+        Aquamarine aquamarine (127, 255, 212),
+        Azure azure (240, 255, 255),
+        Beige beige (245, 245, 220),
+        Bisque bisque (255, 228, 196),
+        // Black black (0, 0, 0),
+        BlanchedAlmond blanched_almond (255, 235, 205),
+        // Blue blue (0, 0, 255),
+        BlueViolet blue_violet (138, 43, 226),
+        Brown brown (165, 42, 42),
+        BurlyWood burly_wood (222, 184, 135),
+        CadetBlue cadet_blue (95, 158, 160),
+        Chartreuse chartreuse (127, 255, 0),
+        Chocolate chocolate (210, 105, 30),
+        Coral coral (255, 127, 80),
+        CornflowerBlue cornflower_blue (100, 149, 237),
+        Cornsilk cornsilk (255, 248, 220),
+        Crimson crimson (220, 20, 60),
+        DarkBlue dark_blue (0, 0, 139),
+        DarkCyan dark_cyan (0, 139, 139),
+        DarkGoldenRod dark_golden_rod (184, 134, 11),
+        DarkGray dark_gray (169, 169, 169),
+        DarkGrey dark_grey (169, 169, 169),
+        DarkGreen dark_green (0, 100, 0),
+        DarkKhaki dark_khaki (189, 183, 107),
+        DarkMagenta dark_magenta (139, 0, 139),
+        DarkOliveGreen dark_olive_green (85, 107, 47),
+        DarkOrange dark_orange (255, 140, 0),
+        DarkOrchid dark_orchid (153, 50, 204),
+        DarkRed dark_red (139, 0, 0),
+        DarkSalmon dark_salmon (233, 150, 122),
+        DarkSeaGreen dark_sea_green (143, 188, 143),
+        DarkSlateBlue dark_slate_blue (72, 61, 139),
+        DarkSlateGray dark_slate_gray (47, 79, 79),
+        DarkSlateGrey dark_slate_grey (47, 79, 79),
+        DarkTurquoise dark_turquoise (0, 206, 209),
+        DarkViolet dark_violet (148, 0, 211),
+        DeepPink deep_pink (255, 20, 147),
+        DeepSkyBlue deep_sky_blue (0, 191, 255),
+        DimGray dim_gray (105, 105, 105),
+        DimGrey dim_grey (105, 105, 105),
+        DodgerBlue dodger_blue (30, 144, 255),
+        FireBrick fire_brick (178, 34, 34),
+        FloralWhite floral_white (255, 250, 240),
+        ForestGreen forest_green (34, 139, 34),
+        Fuchsia fuchsia (255, 0, 255),
+        Gainsboro gainsboro (220, 220, 220),
+        GhostWhite ghost_white (248, 248, 255),
+        Gold gold (255, 215, 0),
+        GoldenRod golden_rod (218, 165, 32),
+        Gray gray (128, 128, 128),
+        Grey grey (128, 128, 128),
+        // Green green (0, 128, 0),
+        GreenYellow green_yellow (173, 255, 47),
+        HoneyDew honey_dew (240, 255, 240),
+        HotPink hot_pink (255, 105, 180),
+        IndianRed indian_red (205, 92, 92),
+        Indigo indigo (75, 0, 130),
+        Ivory ivory (255, 255, 240),
+        Khaki khaki (240, 230, 140),
+        Lavender lavender (230, 230, 250),
+        LavenderBlush lavender_blush (255, 240, 245),
+        LawnGreen lawn_green (124, 252, 0),
+        LemonChiffon lemon_chiffon (255, 250, 205),
+        LightBlue light_blue (173, 216, 230),
+        LightCoral light_coral (240, 128, 128),
+        LightCyan light_cyan (224, 255, 255),
+        LightGoldenRodYellow light_golden_rod_yellow (250, 250, 210),
+        LightGray light_gray (211, 211, 211),
+        LightGrey light_grey (211, 211, 211),
+        LightGreen light_green (144, 238, 144),
+        LightPink light_pink (255, 182, 193),
+        LightSalmon light_salmon (255, 160, 122),
+        LightSeaGreen light_sea_green (32, 178, 170),
+        LightSkyBlue light_sky_blue (135, 206, 250),
+        LightSlateGray light_slate_gray (119, 136, 153),
+        LightSlateGrey light_slate_grey (119, 136, 153),
+        LightSteelBlue light_steel_blue (176, 196, 222),
+        LightYellow light_yellow (255, 255, 224),
+        Lime lime (0, 255, 0),
+        LimeGreen lime_green (50, 205, 50),
+        Linen linen (250, 240, 230),
+        // Magenta magenta (255, 0, 255),
+        Maroon maroon (128, 0, 0),
+        MediumAquaMarine medium_aqua_marine (102, 205, 170),
+        MediumBlue medium_blue (0, 0, 205),
+        MediumOrchid medium_orchid (186, 85, 211),
+        MediumPurple medium_purple (147, 112, 219),
+        MediumSeaGreen medium_sea_green (60, 179, 113),
+        MediumSlateBlue medium_slate_blue (123, 104, 238),
+        MediumSpringGreen medium_spring_green (0, 250, 154),
+        MediumTurquoise medium_turquoise (72, 209, 204),
+        MediumVioletRed medium_violet_red (199, 21, 133),
+        MidnightBlue midnight_blue (25, 25, 112),
+        MintCream mint_cream (245, 255, 250),
+        MistyRose misty_rose (255, 228, 225),
+        Moccasin moccasin (255, 228, 181),
+        NavajoWhite navajo_white (255, 222, 173),
+        Navy navy (0, 0, 128),
+        OldLace old_lace (253, 245, 230),
+        Olive olive (128, 128, 0),
+        OliveDrab olive_drab (107, 142, 35),
+        Orange orange (255, 165, 0),
+        OrangeRed orange_red (255, 69, 0),
+        Orchid orchid (218, 112, 214),
+        PaleGoldenRod pale_golden_rod (238, 232, 170),
+        PaleGreen pale_green (152, 251, 152),
+        PaleTurquoise pale_turquoise (175, 238, 238),
+        PaleVioletRed pale_violet_red (219, 112, 147),
+        PapayaWhip papaya_whip (255, 239, 213),
+        PeachPuff peach_puff (255, 218, 185),
+        Peru peru (205, 133, 63),
+        Pink pink (255, 192, 203),
+        Plum plum (221, 160, 221),
+        PowderBlue powder_blue (176, 224, 230),
+        Purple purple (128, 0, 128),
+        RebeccaPurple rebecca_purple (102, 51, 153),
+        // Red red (255, 0, 0),
+        RosyBrown rosy_brown (188, 143, 143),
+        RoyalBlue royal_blue (65, 105, 225),
+        SaddleBrown saddle_brown (139, 69, 19),
+        Salmon salmon (250, 128, 114),
+        SandyBrown sandy_brown (244, 164, 96),
+        SeaGreen sea_green (46, 139, 87),
+        SeaShell sea_shell (255, 245, 238),
+        Sienna sienna (160, 82, 45),
+        Silver silver (192, 192, 192),
+        SkyBlue sky_blue (135, 206, 235),
+        SlateBlue slate_blue (106, 90, 205),
+        SlateGray slate_gray (112, 128, 144),
+        SlateGrey slate_grey (112, 128, 144),
+        Snow snow (255, 250, 250),
+        SpringGreen spring_green (0, 255, 127),
+        SteelBlue steel_blue (70, 130, 180),
+        Tan tan (210, 180, 140),
+        Teal teal (0, 128, 128),
+        Thistle thistle (216, 191, 216),
+        Tomato tomato (255, 99, 71),
+        Turquoise turquoise (64, 224, 208),
+        Violet violet (238, 130, 238),
+        Wheat wheat (245, 222, 179),
+        // White white (255, 255, 255),
+        WhiteSmoke white_smoke (245, 245, 245),
+        // Yellow yellow (255, 255, 0),
+        YellowGreen yellow_green (154, 205, 50),
+
+        // Styles
+        Bold bold 1,
+        Dim dim 2,
+        Italic italic 3,
+        Underline underline 4,
+        Blink blink 5,
+        BlinkFast blink_fast 6,
+        Reversed reversed 7,
+        Hidden hidden 8,
+        StrikeThrough strike_through 9,
+    }
+}
diff --git a/crates/colors/src/support.rs b/crates/colors/src/support.rs
new file mode 100644
index 0000000..b42ce5d
--- /dev/null
+++ b/crates/colors/src/support.rs
@@ -0,0 +1,116 @@
+pub(super) const CSI: &str = "\x1b[";
+pub(super) const CSE: &str = "m";
+
+macro_rules! elements_inner {
+    (
+        $(
+            $name:ident $_:ident $number:tt
+        ),*
+        $(,)?
+    ) => {
+        $(
+            #[derive(Debug)]
+            pub struct $name<I: Colorize>(I);
+
+            impl<I: Colorize> Colorize for $name<I> {
+                fn render_into(self, base: &mut String, use_colors: bool) {
+                    elements_inner! {@parse_number $number}
+
+                    if use_colors {
+                        base.write_str(CSI).expect("In-memory write");
+                        base.write_str(NUMBERS).expect("In-memory write");
+                        base.write_str(CSE).expect("In-memory write");
+                    }
+                    self.0.render_into(base, use_colors);
+                    // The canvas is resetting the colours again.
+                }
+            }
+        )*
+    };
+
+    (@parse_number $single:literal) => {
+        const NUMBERS: &str = stringify!($single);
+    };
+    (@parse_number ($red:literal, $green:literal, $blue:literal)) => {
+        const NUMBERS_U8: [u8; 18] = $crate::custom::rgb_to_ansi($red, $green, $blue, $crate::custom::Plane::Fg);
+
+        const NUMBERS: &str = $crate::custom::bytes_to_str(&NUMBERS_U8);
+    }
+}
+pub(super) use elements_inner;
+
+macro_rules! methods_inner {
+    (
+        Colorize
+
+        $(
+            $struct_name:ident $fn_name:ident $_:tt
+        ),*
+        $(,)?
+    ) => {
+        $(
+            fn $fn_name(self) -> $struct_name<Self> {
+                $struct_name(self)
+            }
+        )*
+    };
+    (
+        IntoCanvas
+
+        $(
+            $struct_name:ident $fn_name:ident $_:tt
+        ),*
+        $(,)?
+    ) => {
+        $(
+            fn $fn_name(self) -> $struct_name<Canvas<Self>> {
+                $struct_name(Canvas(self))
+            }
+        )*
+    };
+}
+pub(super) use methods_inner;
+
+macro_rules! prepend_input {
+    (
+        $(
+            $existing_macro_name:path as $new_macro_name:ident $(($macro_rule:tt -> $macro_apply:tt))?
+        ),*
+        $(,)?
+
+        <shared_input>
+        $shared_input:tt
+    ) => {
+        $(
+            prepend_input! {
+                @generate_macro
+                $existing_macro_name as $new_macro_name $(($macro_rule -> $macro_apply))?
+                <shared_input>
+                $shared_input
+            }
+        )*
+    };
+
+    (
+        @generate_macro
+        $existing_macro_name:path as $new_macro_name:ident $((($($macro_rule:tt)*) -> {$($macro_apply:tt)*}))?
+
+        <shared_input>
+        {
+            $(
+                $shared_input:tt
+            )*
+        }
+    ) => {
+        macro_rules! $new_macro_name {
+            ($($($macro_rule)*)?) => {
+                $existing_macro_name! {
+                    $($($macro_apply)*)?
+                    $($shared_input)*
+                }
+            }
+        }
+        pub(super) use $new_macro_name;
+    }
+}
+pub(crate) use prepend_input;
diff --git a/crates/yt/Cargo.toml b/crates/yt/Cargo.toml
index d36751c..54a7ed7 100644
--- a/crates/yt/Cargo.toml
+++ b/crates/yt/Cargo.toml
@@ -30,6 +30,7 @@ chrono = { version = "0.4.41", features = ["now"] }
 chrono-humanize = "0.2.3"
 clap = { version = "4.5.40", features = ["derive"] }
 clap_complete = { version = "4.5.54", features = ["unstable-dynamic"] }
+colors.workspace = true
 futures = "0.3.31"
 owo-colors = "4.2.2"
 regex = "1.11.1"
diff --git a/crates/yt/src/config/mod.rs b/crates/yt/src/config/mod.rs
index adbafdd..52962ab 100644
--- a/crates/yt/src/config/mod.rs
+++ b/crates/yt/src/config/mod.rs
@@ -1,8 +1,20 @@
+use std::sync::atomic::{AtomicBool, Ordering};
+
 use crate::config::support::mk_config;
 
 mod paths;
 mod support;
 
+pub(crate) static SHOULD_DISPLAY_COLOR: AtomicBool = AtomicBool::new(false);
+
+// We need to do both things to comply with what the config expects.
+#[allow(clippy::trivially_copy_pass_by_ref, clippy::unnecessary_wraps)]
+fn set_static_should_display_color(value: &bool) -> anyhow::Result<()> {
+    SHOULD_DISPLAY_COLOR.store(*value, Ordering::Relaxed);
+
+    Ok(())
+}
+
 mk_config! {
     use std::path::PathBuf;
     use std::io::IsTerminal;
@@ -10,6 +22,8 @@ mk_config! {
 
     use crate::shared::bytes::Bytes;
 
+    use super::set_static_should_display_color;
+
     use super::paths::get_config_path;
     use super::paths::get_runtime_path;
     use super::paths::get_data_path;
@@ -27,7 +41,7 @@ mk_config! {
                         .unwrap_or_else(|| std::io::stderr().is_terminal())
                     )
                 )
-            },
+            } => set_static_should_display_color,
         },
         select: SelectConfig = {
             /// The playback speed to use, when it is not overridden.
diff --git a/crates/yt/src/storage/db/insert/playlist.rs b/crates/yt/src/storage/db/insert/playlist.rs
index 2613fb3..dc474ce 100644
--- a/crates/yt/src/storage/db/insert/playlist.rs
+++ b/crates/yt/src/storage/db/insert/playlist.rs
@@ -1,6 +1,7 @@
 use std::{cmp::Ordering, time::Duration};
 
 use anyhow::{Context, Result};
+use colors::Colorize;
 use libmpv2::Mpv;
 use log::{debug, trace};
 
@@ -198,7 +199,7 @@ impl Playlist {
 
         debug!(
             "Setting the watch progress for the current_video '{}' to {}s",
-            current_video.title_fmt_no_color(),
+            current_video.title_fmt().render(false),
             watch_progress.as_secs(),
         );
 
diff --git a/crates/yt/src/videos/mod.rs b/crates/yt/src/videos/mod.rs
index 673d46e..3775e0f 100644
--- a/crates/yt/src/videos/mod.rs
+++ b/crates/yt/src/videos/mod.rs
@@ -9,51 +9,197 @@
 // You should have received a copy of the License along with this program.
 // If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
 
-use anyhow::Result;
-use futures::{TryStreamExt, stream::FuturesUnordered};
+use std::fmt::Write;
 
-pub(crate) mod display;
+use anyhow::{Context, Result};
+use colors::{Colorize, IntoCanvas};
+use url::Url;
 
 use crate::{
     app::App,
-    storage::db::video::{Video, VideoStatusMarker},
+    select::duration::MaybeDuration,
+    storage::db::video::{TimeStamp, Video, VideoStatus},
 };
 
-async fn to_line_display_owned(video: Video, app: &App, format: Option<String>) -> Result<String> {
-    video.to_line_display(app, format).await
+pub(crate) mod format_video;
+
+macro_rules! get {
+    ($value:expr, $key:ident, $name:expr, $code:tt) => {
+        if let Some(value) = &$value.$key {
+            $code(value)
+        } else {
+            concat!("[No ", $name, "]").to_owned()
+        }
+    };
 }
 
-pub(crate) async fn query(
-    app: &App,
-    limit: Option<usize>,
-    search_query: Option<String>,
-    format: Option<String>,
-) -> Result<()> {
-    let all_videos = Video::in_states(app, VideoStatusMarker::ALL).await?;
+pub(crate) trait RenderWithApp: Colorize {
+    fn to_string(self, app: &App) -> String {
+        self.render(app.config.global.display_colors)
+    }
+}
+impl<C: Colorize> RenderWithApp for C {}
+
+impl Video {
+    #[must_use]
+    pub(crate) fn cache_path_fmt(&self) -> impl Colorize {
+        let cache_path = if let VideoStatus::Cached {
+            cache_path,
+            is_focused: _,
+        } = &self.status
+        {
+            cache_path.to_string_lossy().to_string()
+        } else {
+            "[No Cache Path]".to_owned()
+        };
+
+        cache_path.blue().bold()
+    }
 
-    // turn one video to a color display, to pre-warm the hash shrinking cache
-    if let Some(val) = all_videos.first() {
-        val.to_line_display(app, format.clone()).await?;
+    #[must_use]
+    pub(crate) fn description_fmt(&self) -> impl Colorize {
+        get!(
+            self,
+            description,
+            "Description",
+            (|value: &str| value.to_owned())
+        )
+        .into_canvas()
     }
 
-    let limit = limit.unwrap_or(all_videos.len());
+    #[must_use]
+    pub(crate) fn duration_fmt(&self) -> impl Colorize {
+        self.duration.cyan().bold()
+    }
 
-    let all_video_strings: Vec<String> = all_videos
-        .into_iter()
-        .take(limit)
-        .map(|vid| to_line_display_owned(vid, app, format.clone()))
-        .collect::<FuturesUnordered<_>>()
-        .try_collect::<Vec<String>>()
-        .await?;
+    #[must_use]
+    pub(crate) fn watch_progress_fmt(&self) -> impl Colorize {
+        MaybeDuration::from_std(self.watch_progress).cyan().bold()
+    }
 
-    if let Some(query) = search_query {
-        all_video_strings
-            .into_iter()
-            .filter(|video| video.to_lowercase().contains(&query.to_lowercase()))
-            .for_each(|video| println!("{video}"));
-    } else {
-        println!("{}", all_video_strings.join("\n"));
+    pub(crate) async fn extractor_hash_fmt(&self, app: &App) -> Result<impl Colorize> {
+        let hash = self
+            .extractor_hash
+            .as_short_hash(app)
+            .await
+            .with_context(|| {
+                format!(
+                    "Failed to format extractor hash, whilst formatting video: '{}'",
+                    self.title
+                )
+            })?;
+
+        Ok(hash.purple().bold().italic())
     }
 
-    Ok(())
+    #[must_use]
+    pub(crate) fn in_playlist_fmt(&self) -> impl Colorize {
+        let output = match &self.status {
+            VideoStatus::Pick
+            | VideoStatus::Watch
+            | VideoStatus::Watched
+            | VideoStatus::Drop
+            | VideoStatus::Dropped => "Not in the playlist",
+            VideoStatus::Cached { is_focused, .. } => {
+                if *is_focused {
+                    "In the playlist and focused"
+                } else {
+                    "In the playlist"
+                }
+            }
+        };
+        output.yellow().italic()
+    }
+    #[must_use]
+    pub(crate) fn last_status_change_fmt(&self) -> impl Colorize {
+        self.last_status_change.bright_cyan()
+    }
+
+    #[must_use]
+    pub(crate) fn parent_subscription_name_fmt(&self) -> impl Colorize {
+        let psn = get!(
+            self,
+            parent_subscription_name,
+            "author",
+            (|sub: &str| sub.replace('"', "'"))
+        );
+
+        psn.bright_magenta()
+    }
+
+    #[must_use]
+    pub(crate) fn priority_fmt(&self) -> impl Colorize {
+        self.priority.into_canvas()
+    }
+
+    #[must_use]
+    pub(crate) fn publish_date_fmt(&self) -> impl Colorize {
+        let date = get!(
+            self,
+            publish_date,
+            "release date",
+            (|date: &TimeStamp| date.to_string())
+        );
+
+        date.bright_white().bold()
+    }
+
+    #[must_use]
+    pub(crate) fn status_fmt(&self) -> impl Colorize {
+        // TODO: We might support `.trim()`ing that, as the extra whitespace could be bad in the
+        // selection file. <2024-10-07>
+        let status = self.status.as_marker().as_command().to_owned();
+
+        status.red().bold()
+    }
+
+    #[must_use]
+    pub(crate) fn thumbnail_url_fmt(&self) -> impl Colorize {
+        get!(
+            self,
+            thumbnail_url,
+            "thumbnail URL",
+            (|url: &Url| url.to_string())
+        )
+        .into_canvas()
+    }
+
+    #[must_use]
+    pub(crate) fn title_fmt(&self) -> impl Colorize {
+        let title = self.title.replace(['"', '„', '”', '“'], "'");
+
+        title.green().bold()
+    }
+
+    #[must_use]
+    pub(crate) fn url_fmt(&self) -> impl Colorize {
+        let url = self.url.as_str().replace('"', "\\\"");
+
+        url.italic()
+    }
+
+    pub(crate) fn video_options_fmt(&self, app: &App) -> impl Colorize {
+        let video_options = {
+            let mut opts = String::new();
+
+            if let Some(playback_speed) = self.playback_speed {
+                if (playback_speed - app.config.select.playback_speed).abs() > f64::EPSILON {
+                    write!(opts, " --playback-speed '{}'", playback_speed).expect("In-memory");
+                }
+            }
+
+            if let Some(subtitle_langs) = &self.subtitle_langs {
+                if subtitle_langs != &app.config.select.subtitle_langs {
+                    write!(opts, " --subtitle-langs '{}'", subtitle_langs).expect("In-memory");
+                }
+            }
+
+            let opts = opts.trim().to_owned();
+
+            let opts_white = if opts.is_empty() { "" } else { " " };
+            format!("{opts_white}{opts}")
+        };
+
+        video_options.bright_green()
+    }
 }
diff --git a/crates/yt/tests/_testenv/run.rs b/crates/yt/tests/_testenv/run.rs
index 954e112..74f5e86 100644
--- a/crates/yt/tests/_testenv/run.rs
+++ b/crates/yt/tests/_testenv/run.rs
@@ -3,7 +3,7 @@ use std::{
     process::{self, Stdio},
 };
 
-use owo_colors::OwoColorize;
+use colors::{Colorize, IntoCanvas};
 
 use crate::testenv::TestEnv;
 
@@ -161,7 +161,11 @@ impl TestEnv {
 
         cmd.args(args);
 
-        eprintln!("{} `yt {}`", self.name.blue().italic(), args.join(" "));
+        eprintln!(
+            "{} `yt {}`",
+            self.name.blue().italic().render(true),
+            args.join(" ")
+        );
 
         cmd
     }
diff --git a/crates/yt/tests/watch/mod.rs b/crates/yt/tests/watch/mod.rs
index 534c210..9c5a203 100644
--- a/crates/yt/tests/watch/mod.rs
+++ b/crates/yt/tests/watch/mod.rs
@@ -5,7 +5,7 @@ use std::{
     path::PathBuf,
 };
 
-use owo_colors::OwoColorize;
+use colors::{Colorize, IntoCanvas};
 use serde_json::json;
 use yt_dlp::{json_cast, json_get, progress_hook::__priv::vm::common::atomic::Radium};
 
@@ -72,7 +72,11 @@ impl MpvControl {
             .current_request_id
             .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
 
-        eprint!("{} `mpv {}`", self.name.blue().italic(), args.join(" "));
+        eprint!(
+            "{} `mpv {}`",
+            self.name.blue().italic().render(true),
+            args.join(" ")
+        );
 
         writeln!(
             self.stream,
@@ -102,11 +106,11 @@ impl MpvControl {
                             }
                         };
 
-                        eprintln!(", {}: {data}", "output".bright_blue(),);
+                        eprintln!(", {}: {data}", "output".bright_blue().render(true),);
                         return Ok(data);
                     }
 
-                    eprintln!(", {}: {error}", "error".bright_red());
+                    eprintln!(", {}: {error}", "error".bright_red().render(true));
                     return Err(error.to_owned());
                 }
             }