ommp.git

ommp

~ Oh My Music Player — a standalone terminal music player built with Rust and ratatui
git clone https://github.com/debuman/ommp.git

src/ui/widgets/status_bar.rs (6579B)

      1 use ratatui::layout::{Constraint, Direction, Layout, Rect, Alignment};
      2 use ratatui::style::{Color, Modifier, Style};
      3 use ratatui::text::{Line, Span};
      4 use ratatui::widgets::{Block, Borders, Paragraph};
      5 use ratatui::Frame;
      6 
      7 use crate::app::App;
      8 use crate::app::state::{PlayState, SyncState};
      9 use crate::ui::theme::Theme;
     10 use crate::ui::text::format_time;
     11 
     12 pub fn render_status_bar(frame: &mut Frame, area: Rect, app: &App, theme: &Theme) {
     13     let block = if let Some(err) = &app.audio_error {
     14         // Nothing will ever play; without this the UI shows a play state and a
     15         // moving progress bar over silence.
     16         Block::default()
     17             .borders(Borders::ALL)
     18             .border_style(Style::default().fg(Color::Rgb(255, 90, 90)))
     19             .title(format!(" [NO AUDIO] {} ", err))
     20             .title_style(
     21                 Style::default()
     22                     .fg(Color::Rgb(255, 90, 90))
     23                     .add_modifier(Modifier::BOLD),
     24             )
     25     } else if app.sync_state == SyncState::Scanning {
     26         Block::default()
     27             .borders(Borders::ALL)
     28             .border_style(Style::default().fg(Color::Rgb(255, 200, 80)))
     29             .title(" [SYNCING] ")
     30             .title_style(Style::default().fg(Color::Rgb(255, 200, 80)).add_modifier(Modifier::BOLD))
     31     } else {
     32         Block::default()
     33             .borders(Borders::ALL)
     34             .border_style(Style::default().fg(theme.border_unfocused))
     35     };
     36 
     37     let inner = block.inner(area);
     38     frame.render_widget(block, area);
     39 
     40     let cols = Layout::default()
     41         .direction(Direction::Horizontal)
     42         .constraints([
     43             Constraint::Percentage(25),
     44             Constraint::Percentage(50),
     45             Constraint::Percentage(25),
     46         ])
     47         .split(inner);
     48 
     49     // Left: Play state + time
     50     let state_icon = match app.playback.state {
     51         PlayState::Playing => "\u{F04B}",  // nf-fa-play
     52         PlayState::Paused => "\u{F04C}",   // nf-fa-pause
     53         PlayState::Stopped => "\u{F04D}",  // nf-fa-stop
     54     };
     55 
     56     let pos = format_time(app.playback.position_secs);
     57     let dur = format_time(app.playback.duration_secs);
     58 
     59     let bitrate = app
     60         .current_track()
     61         .and_then(|t| t.bitrate)
     62         .map(|b| format!(" ({}kbps)", b))
     63         .unwrap_or_default();
     64 
     65     let state_color = match app.playback.state {
     66         PlayState::Playing => Color::Rgb(80, 255, 120),   // bright green
     67         PlayState::Paused => Color::Rgb(255, 200, 80),    // amber
     68         PlayState::Stopped => Color::Rgb(255, 100, 100),  // soft red
     69     };
     70 
     71     let left_line1 = Line::from(vec![
     72         Span::styled(
     73             format!(" {} {}", state_icon, match app.playback.state {
     74                 PlayState::Playing => "Playing",
     75                 PlayState::Paused => "Paused",
     76                 PlayState::Stopped => "Stopped",
     77             }),
     78             Style::default().fg(state_color).add_modifier(Modifier::BOLD),
     79         ),
     80     ]);
     81 
     82     let left_line2 = Line::from(vec![
     83         Span::styled(
     84             format!(" {}/{}{}", pos, dur, bitrate),
     85             Style::default().fg(Color::Gray),
     86         ),
     87     ]);
     88 
     89     let left = Paragraph::new(vec![left_line1, left_line2]);
     90     frame.render_widget(left, cols[0]);
     91 
     92     // Center: Track info
     93     let (title, artist_album) = if let Some(track) = app.current_track() {
     94         (
     95             track.title.clone(),
     96             format!("{} - {}", track.display_artist(), track.display_album()),
     97         )
     98     } else {
     99         ("No track playing".to_string(), String::new())
    100     };
    101 
    102     let center_line1 = Line::from(Span::styled(
    103         title,
    104         Style::default().fg(Color::Rgb(100, 220, 255)).add_modifier(Modifier::BOLD),
    105     )).alignment(Alignment::Center);
    106 
    107     let center_line2 = Line::from(Span::styled(
    108         artist_album,
    109         Style::default().fg(Color::Rgb(200, 170, 255)),
    110     )).alignment(Alignment::Center);
    111 
    112     let center = Paragraph::new(vec![center_line1, center_line2]);
    113     frame.render_widget(center, cols[1]);
    114 
    115     // Right: Volume + shuffle/repeat
    116     let vol_pct = (app.playback.volume * 100.0) as u8;
    117 
    118     let shuffle_style = if app.playback.shuffle {
    119         Style::default().fg(Color::Rgb(100, 220, 255)).add_modifier(Modifier::BOLD)
    120     } else {
    121         Style::default().fg(Color::DarkGray)
    122     };
    123 
    124     let repeat_style = match app.playback.repeat {
    125         crate::app::state::RepeatMode::Off => Style::default().fg(Color::DarkGray),
    126         crate::app::state::RepeatMode::All => Style::default().fg(Color::Rgb(120, 255, 180)).add_modifier(Modifier::BOLD),
    127         crate::app::state::RepeatMode::One => Style::default().fg(Color::Rgb(255, 220, 100)).add_modifier(Modifier::BOLD),
    128     };
    129 
    130     // Volume staircase with gradient: green → yellow → orange → red
    131     const STEPS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
    132     let vol_colors: [Color; 8] = [
    133         Color::Rgb(80, 200, 120),  // green
    134         Color::Rgb(120, 220, 100), // green-yellow
    135         Color::Rgb(180, 230, 80),  // yellow-green
    136         Color::Rgb(230, 220, 60),  // yellow
    137         Color::Rgb(255, 190, 50),  // amber
    138         Color::Rgb(255, 150, 40),  // orange
    139         Color::Rgb(255, 110, 50),  // red-orange
    140         Color::Rgb(255, 70, 70),   // red
    141     ];
    142     let filled = (vol_pct as u16 * 8 / 100).min(8) as usize;
    143     let mut vol_spans = Vec::with_capacity(10);
    144     for (i, &ch) in STEPS.iter().enumerate() {
    145         let style = if i < filled {
    146             Style::default().fg(vol_colors[i]).add_modifier(Modifier::BOLD)
    147         } else {
    148             Style::default().fg(Color::Indexed(238))
    149         };
    150         vol_spans.push(Span::styled(String::from(ch), style));
    151     }
    152     vol_spans.push(Span::styled(format!(" {}% ", vol_pct), Style::default().fg(Color::White)));
    153 
    154     let right_line1 = Line::from(vol_spans).alignment(Alignment::Right);
    155 
    156     let is_bookmarked = app.queue.current_index
    157         .and_then(|qi| app.queue.tracks.get(qi))
    158         .is_some_and(|&ti| app.is_in_playlist(ti));
    159     let bookmark_style = if is_bookmarked {
    160         Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
    161     } else {
    162         Style::default().fg(Color::DarkGray)
    163     };
    164 
    165     let right_line2 = Line::from(vec![
    166         Span::styled("\u{F005} ", bookmark_style),  // nf-fa-star
    167         Span::styled("\u{F074} ", shuffle_style),   // nf-fa-random
    168         Span::styled(format!("{} ", app.playback.repeat.symbol()), repeat_style),
    169     ]).alignment(Alignment::Right);
    170 
    171     let right = Paragraph::new(vec![right_line1, right_line2]);
    172     frame.render_widget(right, cols[2]);
    173 }
    174