From ea73f1d3d4c55ffd198387b7cfdf16956f19255c Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Fri, 19 Dec 2025 13:09:26 -0700 Subject: [PATCH] testing --- Cargo.lock | 128 ++++++++++++++++++++++++++++++-- Cargo.toml | 1 + src/app/mod.rs | 8 ++ src/main.rs | 21 +++--- src/ui/mod.rs | 5 ++ src/ui/radarr_ui/library/mod.rs | 10 ++- src/ui/sonarr_ui/history/mod.rs | 9 ++- src/ui/sonarr_ui/library/mod.rs | 6 +- src/ui/widgets/loading_block.rs | 30 +++++++- src/ui/widgets/popup.rs | 39 ++++++---- 10 files changed, 212 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ccbfb24..f23daee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,6 +41,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anpa" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d032745fe46100dbcb28ee6e30f12c4b148786f8889e07cd0a3445eeb54970f" + [[package]] name = "anstream" version = "0.6.21" @@ -217,6 +223,31 @@ dependencies = [ "objc2", ] +[[package]] +name = "bon" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebeb9aaf9329dff6ceb65c689ca3db33dbf15f324909c60e4e5eef5701ce31b1" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77e9d642a7e3a318e37c2c9427b5a6a48aa1ad55dcd986f3034ab2239045a645" +dependencies = [ + "darling 0.21.3", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.111", +] + [[package]] name = "bstr" version = "1.12.1" @@ -376,6 +407,20 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "compact_str" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + [[package]] name = "confy" version = "0.6.1" @@ -459,8 +504,18 @@ version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", ] [[package]] @@ -477,13 +532,38 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.111", +] + [[package]] name = "darling_macro" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core", + "darling_core 0.20.11", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", "quote", "syn 2.0.111", ] @@ -536,7 +616,7 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae5c625eda104c228c06ecaf988d1c60e542176bd7a490e60eeda3493244c0c9" dependencies = [ - "darling", + "darling 0.20.11", "proc-macro2", "quote", "syn 2.0.111", @@ -664,7 +744,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "583f1f514d2754010ff71ed6853068cacbe43cc142cc076aa1b871d9754efc48" dependencies = [ - "darling", + "darling 0.20.11", "quote", "syn 2.0.111", ] @@ -673,7 +753,7 @@ dependencies = [ name = "enum_display_style_derive" version = "0.6.1" dependencies = [ - "darling", + "darling 0.20.11", "quote", "syn 2.0.111", ] @@ -1249,7 +1329,7 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6778b0196eefee7df739db78758e5cf9b37412268bfa5650bfeed028aed20d9c" dependencies = [ - "darling", + "darling 0.20.11", "indoc", "proc-macro2", "quote", @@ -1454,6 +1534,7 @@ dependencies = [ "serial_test", "strum", "strum_macros", + "tachyonfx", "tokio", "tokio-util", "urlencoding", @@ -1477,6 +1558,12 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "micromath" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c8dda44ff03a2f238717214da50f65d5a53b45cd213a7370424ffdb6fae815" + [[package]] name = "mime" version = "0.3.17" @@ -2005,6 +2092,16 @@ dependencies = [ "yansi", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.111", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -2109,7 +2206,7 @@ checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" dependencies = [ "bitflags", "cassowary", - "compact_str", + "compact_str 0.8.1", "crossterm", "indoc", "instability", @@ -2704,6 +2801,21 @@ dependencies = [ "libc", ] +[[package]] +name = "tachyonfx" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de9d53c5afe979f7de9d5ba856c9829b118d6aca6335750201f1ada46bc198e2" +dependencies = [ + "anpa", + "bon", + "compact_str 0.9.0", + "micromath", + "ratatui", + "thiserror 2.0.17", + "unicode-width 0.2.0", +] + [[package]] name = "tempfile" version = "3.23.0" diff --git a/Cargo.toml b/Cargo.toml index 8f4aea4..eb264dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,6 +67,7 @@ openssl = { version = "0.10.70", features = ["vendored"] } veil = "0.2.0" validate_theme_derive = "0.1.0" enum_display_style_derive = "0.1.0" +tachyonfx = { version = "0.21.0", features = ["sendable"] } [dev-dependencies] assert_cmd = "2.0.16" diff --git a/src/app/mod.rs b/src/app/mod.rs index a8de430..d12bf9b 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::path::PathBuf; use std::{fs, process}; +use tachyonfx::{Duration, EffectManager}; use tokio::sync::mpsc::Sender; use tokio_util::sync::CancellationToken; use veil::Redact; @@ -40,6 +41,9 @@ pub struct App<'a> { pub ticks_until_scroll: u64, pub tick_count: u64, pub ui_scroll_tick_count: u64, + pub last_tick: Duration, + pub effects: EffectManager<()>, + pub has_active_effect: bool, pub is_routing: bool, pub is_loading: bool, pub should_refresh: bool, @@ -189,6 +193,7 @@ impl App<'_> { pub fn push_navigation_stack(&mut self, route: Route) { self.navigation_stack.push(route); self.is_routing = true; + self.has_active_effect = false; } pub fn pop_navigation_stack(&mut self) { @@ -237,6 +242,9 @@ impl Default for App<'_> { ticks_until_scroll: 4, tick_count: 0, ui_scroll_tick_count: 0, + last_tick: Duration::ZERO, + effects: EffectManager::default(), + has_active_effect: false, is_loading: false, is_routing: false, should_refresh: false, diff --git a/src/main.rs b/src/main.rs index b611d22..3de2039 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,37 +3,38 @@ extern crate assertables; use anyhow::Result; -use clap::{CommandFactory, Parser, crate_authors, crate_description, crate_name, crate_version}; +use clap::{crate_authors, crate_description, crate_name, crate_version, CommandFactory, Parser}; use clap_complete::generate; use crossterm::execute; use crossterm::terminal::{ - EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode, + disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, }; use log::{debug, error, warn}; use network::NetworkTrait; -use ratatui::Terminal; use ratatui::backend::CrosstermBackend; +use ratatui::Terminal; use reqwest::Client; use std::panic::PanicHookInfo; use std::path::PathBuf; -use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::time::Instant; use std::{io, panic, process}; use tokio::select; use tokio::sync::mpsc::Receiver; -use tokio::sync::{Mutex, mpsc}; +use tokio::sync::{mpsc, Mutex}; use tokio_util::sync::CancellationToken; use utils::{ build_network_client, load_config, start_cli_no_spinner, start_cli_with_spinner, tail_logs, }; -use crate::app::{App, log_and_print_error}; +use crate::app::{log_and_print_error, App}; use crate::cli::Command; -use crate::event::Key; use crate::event::input_event::{Events, InputEvent}; +use crate::event::Key; use crate::network::{Network, NetworkEvent}; use crate::ui::theme::{Theme, ThemeDefinitionsWrapper}; -use crate::ui::{THEME, ui}; +use crate::ui::{ui, THEME}; use crate::utils::load_theme_config; mod app; @@ -242,9 +243,12 @@ async fn start_ui( terminal.hide_cursor()?; let input_events = Events::new(); + let mut last_frame_instant = Instant::now(); loop { let mut app = app.lock().await; + app.last_tick = last_frame_instant.elapsed().into(); + last_frame_instant = Instant::now(); terminal.draw(|f| ui(f, &mut app))?; @@ -256,7 +260,6 @@ async fn start_ui( handlers::handle_events(key, &mut app); } - Some(InputEvent::Tick) => app.on_tick().await, _ => {} } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index d974eee..218ce77 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -92,6 +92,11 @@ pub fn ui(f: &mut Frame<'_>, app: &mut App<'_>) { if app.keymapping_table.is_some() { draw_help_popup(f, app); } + + let area = f.area(); + app + .effects + .process_effects(app.last_tick, f.buffer_mut(), area); } fn draw_header_row(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { diff --git a/src/ui/radarr_ui/library/mod.rs b/src/ui/radarr_ui/library/mod.rs index 2ba46ae..ad0b88d 100644 --- a/src/ui/radarr_ui/library/mod.rs +++ b/src/ui/radarr_ui/library/mod.rs @@ -1,12 +1,11 @@ -use ratatui::Frame; use ratatui::layout::{Constraint, Rect}; use ratatui::widgets::{Cell, Row}; +use ratatui::Frame; use crate::app::App; -use crate::models::Route; use crate::models::radarr_models::Movie; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, LIBRARY_BLOCKS}; -use crate::ui::DrawUi; +use crate::models::Route; use crate::ui::radarr_ui::decorate_with_row_style; use crate::ui::radarr_ui::library::add_movie_ui::AddMovieUi; use crate::ui::radarr_ui::library::delete_movie_ui::DeleteMovieUi; @@ -16,6 +15,7 @@ use crate::ui::utils::{get_width_from_percentage, layout_block_top_border}; use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::popup::{Popup, Size}; +use crate::ui::DrawUi; use crate::utils::{convert_runtime, convert_to_gb}; mod add_movie_ui; @@ -64,7 +64,9 @@ impl DrawUi for LibraryUi { .yes_no_value(app.data.radarr_data.prompt_confirm); f.render_widget( - Popup::new(confirmation_prompt).size(Size::MediumPrompt), + Popup::new(confirmation_prompt) + .size(Size::MediumPrompt) + .app(app), f.area(), ); } diff --git a/src/ui/sonarr_ui/history/mod.rs b/src/ui/sonarr_ui/history/mod.rs index 05c00ef..c6d875e 100644 --- a/src/ui/sonarr_ui/history/mod.rs +++ b/src/ui/sonarr_ui/history/mod.rs @@ -1,19 +1,19 @@ use crate::app::App; -use crate::models::Route; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, HISTORY_BLOCKS}; use crate::models::servarr_models::Language; use crate::models::sonarr_models::{SonarrHistoryEventType, SonarrHistoryItem}; -use crate::ui::DrawUi; +use crate::models::Route; use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{get_width_from_percentage, layout_block_top_border}; use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::message::Message; use crate::ui::widgets::popup::{Popup, Size}; -use ratatui::Frame; +use crate::ui::DrawUi; use ratatui::layout::{Alignment, Constraint, Rect}; use ratatui::style::Style; use ratatui::text::Text; use ratatui::widgets::{Cell, Row}; +use ratatui::Frame; use super::sonarr_ui_utils::{ create_download_failed_history_event_details, @@ -154,5 +154,6 @@ fn draw_history_item_details_popup(f: &mut Frame<'_>, app: &mut App<'_>) { .style(Style::new().secondary()) .alignment(Alignment::Left); - f.render_widget(Popup::new(message).size(Size::NarrowMessage), f.area()); + let widget = Popup::new(message).size(Size::NarrowMessage).app(app); + f.render_widget(widget, f.area()); } diff --git a/src/ui/sonarr_ui/library/mod.rs b/src/ui/sonarr_ui/library/mod.rs index 19569ef..a6e0fc4 100644 --- a/src/ui/sonarr_ui/library/mod.rs +++ b/src/ui/sonarr_ui/library/mod.rs @@ -2,9 +2,9 @@ use add_series_ui::AddSeriesUi; use delete_series_ui::DeleteSeriesUi; use edit_series_ui::EditSeriesUi; use ratatui::{ - Frame, layout::{Constraint, Rect}, widgets::{Cell, Row}, + Frame, }; use series_details_ui::SeriesDetailsUi; @@ -16,15 +16,15 @@ use crate::utils::convert_to_gb; use crate::{ app::App, models::{ - Route, servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, LIBRARY_BLOCKS}, sonarr_models::{Series, SeriesStatus}, + Route, }, ui::{ - DrawUi, styles::ManagarrStyle, utils::{get_width_from_percentage, layout_block_top_border}, widgets::managarr_table::ManagarrTable, + DrawUi, }, }; diff --git a/src/ui/widgets/loading_block.rs b/src/ui/widgets/loading_block.rs index bcac243..76cec5a 100644 --- a/src/ui/widgets/loading_block.rs +++ b/src/ui/widgets/loading_block.rs @@ -1,21 +1,33 @@ +use crate::app::App; use crate::ui::styles::ManagarrStyle; +use derive_setters::Setters; use ratatui::buffer::Buffer; use ratatui::layout::Rect; use ratatui::prelude::Text; +use ratatui::style::Style; use ratatui::widgets::{Block, Paragraph, Widget}; +use tachyonfx::pattern::SweepPattern; +use tachyonfx::{fx, Interpolation}; #[cfg(test)] #[path = "loading_block_tests.rs"] mod loading_block_tests; -pub struct LoadingBlock<'a> { +#[derive(Setters)] +pub struct LoadingBlock<'a, 'b> { is_loading: bool, block: Block<'a>, + #[setters(strip_option)] + app: Option<&'a mut App<'b>>, } -impl<'a> LoadingBlock<'a> { +impl<'a, 'b> LoadingBlock<'a, 'b> { pub fn new(is_loading: bool, block: Block<'a>) -> Self { - Self { is_loading, block } + Self { + is_loading, + block, + app: None, + } } fn render_loading_block(self, area: Rect, buf: &mut Buffer) { @@ -27,10 +39,20 @@ impl<'a> LoadingBlock<'a> { } else { self.block.render(area, buf); } + if let Some(app) = self.app + && !app.has_active_effect { + let color = Style::new().failure().fg.expect("primary fg color is unset"); + let fx = + fx::repeating(fx::paint_fg(color, 1000) + .with_pattern(SweepPattern::left_to_right(10)) + .with_area(area)); + app.effects.add_effect(fx); + app.has_active_effect = true; + } } } -impl Widget for LoadingBlock<'_> { +impl Widget for LoadingBlock<'_, '_> { fn render(self, area: Rect, buf: &mut Buffer) { self.render_loading_block(area, buf); } diff --git a/src/ui/widgets/popup.rs b/src/ui/widgets/popup.rs index b08fe60..25caeb6 100644 --- a/src/ui/widgets/popup.rs +++ b/src/ui/widgets/popup.rs @@ -1,7 +1,10 @@ +use crate::app::App; use crate::ui::utils::{background_block, centered_rect}; +use derive_setters::Setters; use ratatui::buffer::Buffer; use ratatui::layout::{Constraint, Layout, Rect}; use ratatui::widgets::{Block, Clear, Widget}; +use tachyonfx::{fx, Interpolation}; #[cfg(test)] #[path = "popup_tests.rs"] @@ -49,15 +52,25 @@ impl Size { } } -pub struct Popup<'a, T: Widget> { +#[derive(Setters)] +pub struct Popup<'a, 'b, T: Widget> { + #[setters(skip)] widget: T, margin: u16, + #[setters(skip)] percent_x: u16, + #[setters(skip)] percent_y: u16, + #[setters(strip_option)] block: Option>, + #[setters(strip_option)] + app: Option<&'a mut App<'b>>, } -impl<'a, T: Widget> Popup<'a, T> { +impl Popup<'_, '_, T> +where + T: Widget, +{ pub fn new(widget: T) -> Self { Self { widget, @@ -65,6 +78,7 @@ impl<'a, T: Widget> Popup<'a, T> { percent_y: 0, margin: 0, block: None, + app: None } } @@ -81,16 +95,6 @@ impl<'a, T: Widget> Popup<'a, T> { self } - pub fn block(mut self, block: Block<'a>) -> Self { - self.block = Some(block); - self - } - - pub fn margin(mut self, margin: u16) -> Self { - self.margin = margin; - self - } - fn render_popup(self, area: Rect, buf: &mut Buffer) { let mut popup_area = centered_rect(self.percent_x, self.percent_y, area); let height = if popup_area.height < 3 { @@ -114,10 +118,19 @@ impl<'a, T: Widget> Popup<'a, T> { .areas(popup_area); self.widget.render(content_area, buf); + if let Some(app) = self.app + && !app.has_active_effect { + let timer = (100, Interpolation::Linear); + let fx = + fx::coalesce(timer) + .with_area(content_area); + app.effects.add_effect(fx); + app.has_active_effect = true; + } } } -impl Widget for Popup<'_, T> { +impl Widget for Popup<'_, '_, T> { fn render(self, area: Rect, buf: &mut Buffer) { self.render_popup(area, buf); }