diff --git a/src/main.rs b/src/main.rs
@@ -198,9 +198,11 @@ fn run_app(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> Result<()>
Ok(event) => {
let actions = match event {
Event::Key(key) => {
- // On key press during splash: jump to fade-out phase
+ // A key during the splash dismisses it outright — the
+ // hint promises the app, not another half second of
+ // animation.
if ui.show_splash {
- ui.begin_splash_fade_out();
+ ui.dismiss_splash();
vec![]
} else {
// Handle queue selection directly for playlist focus
diff --git a/src/ui/mod.rs b/src/ui/mod.rs
@@ -135,24 +135,11 @@ impl Ui {
}
}
- /// Skip the splash forward to the start of its fade-out, if it has not got
- /// there already. Only a keypress does this: the timeline is otherwise
- /// fixed, and the fade-out still plays so the skip is not a hard cut.
- pub fn begin_splash_fade_out(&mut self) {
- if !self.show_splash {
- return;
- }
- let Some(start) = self.splash_start else { return };
- if start.elapsed().as_secs_f32() >= SPLASH_FADE_OUT_AT {
- return;
- }
- // checked_sub because Instant is CLOCK_MONOTONIC: started within
- // SPLASH_FADE_OUT_AT of boot, plain `-` panics.
- if let Some(t) = std::time::Instant::now()
- .checked_sub(std::time::Duration::from_secs_f32(SPLASH_FADE_OUT_AT))
- {
- self.splash_start = Some(t);
- }
+ /// Drop the splash immediately. Only a keypress does this; left alone the
+ /// timeline always runs in full.
+ pub fn dismiss_splash(&mut self) {
+ self.show_splash = false;
+ self.splash_start = None;
}
pub fn render(&mut self, frame: &mut Frame, app: &App) {
@@ -314,39 +301,23 @@ mod tests {
)
}
- fn elapsed(ui: &Ui) -> f32 {
- ui.splash_start.map(|s| s.elapsed().as_secs_f32()).unwrap_or(0.0)
- }
-
#[test]
- fn a_keypress_skips_the_splash_hold() {
+ fn a_keypress_drops_the_splash_immediately() {
let mut ui = ui();
assert!(ui.show_splash);
- assert!(elapsed(&ui) < SPLASH_FADE_OUT_AT);
- ui.begin_splash_fade_out();
+ ui.dismiss_splash();
- // Straight to the fade-out rather than sitting through the hold.
- assert!(elapsed(&ui) >= SPLASH_FADE_OUT_AT);
- assert!(elapsed(&ui) < SPLASH_END, "the fade-out still plays");
- }
-
- #[test]
- fn skipping_twice_does_not_cut_the_fade_out_short() {
- let mut ui = ui();
- ui.begin_splash_fade_out();
- let first = elapsed(&ui);
- ui.begin_splash_fade_out();
- assert!(elapsed(&ui) >= first, "must not jump past the end");
- assert!(elapsed(&ui) < SPLASH_END);
+ // No trailing fade: the hint says the key skips to the app.
+ assert!(!ui.show_splash);
+ assert!(ui.splash_start.is_none());
}
#[test]
- fn skipping_a_dismissed_splash_is_a_no_op() {
+ fn dismissing_twice_is_harmless() {
let mut ui = ui();
- ui.show_splash = false;
- ui.splash_start = None;
- ui.begin_splash_fade_out();
- assert!(ui.splash_start.is_none());
+ ui.dismiss_splash();
+ ui.dismiss_splash();
+ assert!(!ui.show_splash);
}
}
diff --git a/src/ui/widgets/about_modal.rs b/src/ui/widgets/about_modal.rs
@@ -37,6 +37,9 @@ const LOGO: [&str; 5] = [
];
const LOGO_DISPLAY_W: u16 = 35;
+/// Shown at the bottom of the splash so the skip is discoverable.
+const SKIP_HINT: &str = "Press any key to skip";
+
/// Fill area with dark background + scattered music notes
fn render_background(buf: &mut Buffer, area: Rect) {
let bg = Style::default().bg(BG_DARK).fg(BG_DARK);
@@ -349,13 +352,71 @@ pub fn render_splash_screen(frame: &mut Frame, area: Rect, _theme: &Theme, opaci
// --- Tagline (faded) ---
let tagline = "Terminal music, your way";
- let tag_w = tagline.len() as u16;
if cur_y < area.y + area.height {
- let x = center_x(area.x, area.width, tag_w);
+ let x = center_x(area.x, area.width, tagline.len() as u16);
buf.set_string(
x, cur_y, tagline,
Style::default().fg(fade_color(NEON_CYAN, opacity)).bg(BG_DARK),
);
}
+
+ // --- Skip hint, pinned to the bottom ---
+ if area.height > content_h + 2 {
+ let y = area.y + area.height - 2;
+ let x = center_x(area.x, area.width, SKIP_HINT.len() as u16);
+ buf.set_string(
+ x, y, SKIP_HINT,
+ // Dimmer than the rest so it reads as a footnote, and it fades with
+ // everything else rather than popping in at full strength.
+ Style::default()
+ .fg(fade_color(Color::Rgb(120, 130, 150), opacity))
+ .bg(BG_DARK)
+ .add_modifier(Modifier::ITALIC),
+ );
+ }
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use ratatui::backend::TestBackend;
+ use ratatui::Terminal;
+
+ fn rows(w: u16, h: u16) -> Vec<String> {
+ let mut terminal = Terminal::new(TestBackend::new(w, h)).unwrap();
+ terminal
+ .draw(|f| render_splash_screen(f, f.area(), &Theme::default(), 1.0))
+ .unwrap();
+ let buf = terminal.backend().buffer();
+ (0..h)
+ .map(|y| {
+ (0..w)
+ .map(|x| buf.cell((x, y)).map(|c| c.symbol()).unwrap_or(" "))
+ .collect::<String>()
+ })
+ .collect()
+ }
+
+ #[test]
+ fn the_skip_hint_sits_below_the_logo_without_covering_it() {
+ let rows = rows(80, 24);
+ let hint = rows
+ .iter()
+ .position(|r| r.contains(SKIP_HINT))
+ .expect("hint is drawn");
+ let tagline = rows
+ .iter()
+ .position(|r| r.contains("Terminal music, your way"))
+ .expect("tagline is drawn");
+ assert!(hint > tagline, "hint must not land on the centred block");
+ assert!(hint < rows.len(), "hint stays on screen");
+ }
+
+ #[test]
+ fn a_terminal_too_short_for_the_hint_drops_it_rather_than_overlapping() {
+ // Nine rows of content plus the hint's own two do not fit in eleven.
+ assert!(!rows(60, 11).iter().any(|r| r.contains(SKIP_HINT)));
+ assert!(rows(60, 12).iter().any(|r| r.contains(SKIP_HINT)));
+ }
+}
|