diff --git a/Cargo.lock b/Cargo.lock index b8948a44..91edf028 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -769,15 +769,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "0.4.6" @@ -1492,7 +1483,7 @@ dependencies = [ "dotenv", "env_logger", "failure", - "itertools 0.8.2", + "itertools", "lazy_static", "log", "percent-encoding 1.0.1", @@ -1924,15 +1915,13 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] name = "tui" -version = "0.9.5" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9533d39bef0ae8f510e8a99d78702e68d1bbf0b98a78ec9740509d287010ae1e" +checksum = "a977b0bb2e2033a6fef950f218f13622c3c34e59754b704ce3492dedab1dfe95" dependencies = [ "bitflags", "cassowary", "crossterm", - "either", - "itertools 0.9.0", "unicode-segmentation", "unicode-width", ] diff --git a/Cargo.toml b/Cargo.toml index 10f725d2..9e30dfa9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ license = "MIT OR Apache-2.0" [dependencies] rspotify = "0.10.0" -tui = { version = "0.9.5", features = ["crossterm"], default-features = false } +tui = { version = "0.10.0", features = ["crossterm"], default-features = false } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_yaml = "0.8" diff --git a/src/ui/audio_analysis.rs b/src/ui/audio_analysis.rs index 609986f1..307c3ee4 100644 --- a/src/ui/audio_analysis.rs +++ b/src/ui/audio_analysis.rs @@ -4,7 +4,8 @@ use tui::{ backend::Backend, layout::{Constraint, Direction, Layout}, style::Style, - widgets::{BarChart, Block, Borders, Paragraph, Text}, + text::{Span, Spans}, + widgets::{BarChart, Block, Borders, Paragraph}, Frame, }; const PITCHES: [&str; 12] = [ @@ -24,10 +25,12 @@ where .split(f.size()); let analysis_block = Block::default() - .title("Analysis") + .title(Span::styled( + "Analysis", + Style::default().fg(app.user_config.theme.inactive), + )) .borders(Borders::ALL) - .border_style(Style::default().fg(app.user_config.theme.inactive)) - .title_style(Style::default().fg(app.user_config.theme.inactive)); + .border_style(Style::default().fg(app.user_config.theme.inactive)); let white = Style::default().fg(app.user_config.theme.text); let gray = Style::default().fg(app.user_config.theme.inactive); @@ -38,20 +41,17 @@ where let bar_chart_block = Block::default() .borders(Borders::ALL) .style(white) - .title(bar_chart_title) - .title_style(gray) + .title(Span::styled(bar_chart_title, gray)) .border_style(gray); - let analysis_text = [Text::raw("No analysis available")]; let empty_analysis_block = || { - Paragraph::new(analysis_text.iter()) - .block(analysis_block) + Paragraph::new("No analysis available") + .block(analysis_block.clone()) .style(Style::default().fg(app.user_config.theme.text)) }; - let pitch_text = [Text::raw("No pitch information available")]; let empty_pitches_block = || { - Paragraph::new(pitch_text.iter()) - .block(bar_chart_block) + Paragraph::new("No pitch information available") + .block(bar_chart_block.clone()) .style(Style::default().fg(app.user_config.theme.text)) }; @@ -76,24 +76,24 @@ where .find(|section| section.start >= progress_seconds); if let (Some(segment), Some(section)) = (segment, section) { - let texts = [ - Text::raw(format!( - "Tempo: {} (confidence {:.0}%)\n", + let texts = vec![ + Spans::from(format!( + "Tempo: {} (confidence {:.0}%)", section.tempo, section.tempo_confidence * 100.0 )), - Text::raw(format!( - "Key: {} (confidence {:.0}%)\n", + Spans::from(format!( + "Key: {} (confidence {:.0}%)", PITCHES.get(section.key as usize).unwrap_or(&PITCHES[0]), section.key_confidence * 100.0 )), - Text::raw(format!( - "Time Signature: {}/4 (confidence {:.0}%)\n", + Spans::from(format!( + "Time Signature: {}/4 (confidence {:.0}%)", section.time_signature, section.time_signature_confidence * 100.0 )), ]; - let p = Paragraph::new(texts.iter()) + let p = Paragraph::new(texts) .block(analysis_block) .style(Style::default().fg(app.user_config.theme.text)); f.render_widget(p, chunks[0]); @@ -118,7 +118,7 @@ where .block(bar_chart_block) .data(&data) .bar_width(width as u16) - .style(Style::default().fg(app.user_config.theme.analysis_bar)) + .bar_style(Style::default().fg(app.user_config.theme.analysis_bar)) .value_style( Style::default() .fg(app.user_config.theme.analysis_bar_text) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 547ae52c..2d76b1e8 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -15,7 +15,8 @@ use tui::{ backend::Backend, layout::{Alignment, Constraint, Direction, Layout, Rect}, style::{Modifier, Style}, - widgets::{Block, Borders, Clear, Gauge, List, ListState, Paragraph, Row, Table, Text}, + text::{Span, Spans, Text}, + widgets::{Block, Borders, Clear, Gauge, List, ListItem, ListState, Paragraph, Row, Table, Wrap}, Frame, }; use util::{ @@ -95,8 +96,7 @@ where Block::default() .borders(Borders::ALL) .style(white) - .title("Help (press to go back)") - .title_style(gray) + .title(Span::styled("Help (press to go back)", gray)) .border_style(gray), ) .style(Style::default().fg(app.user_config.theme.text)) @@ -125,12 +125,14 @@ where ); let input_string: String = app.input.iter().collect(); - let lines = [Text::raw(&input_string)]; - let input = Paragraph::new(lines.iter()).block( + let lines = Text::from((&input_string).as_str()); + let input = Paragraph::new(lines).block( Block::default() .borders(Borders::ALL) - .title("Search") - .title_style(get_color(highlight_state, app.user_config.theme)) + .title(Span::styled( + "Search", + get_color(highlight_state, app.user_config.theme), + )) .border_style(get_color(highlight_state, app.user_config.theme)), ); f.render_widget(input, chunks[0]); @@ -143,13 +145,12 @@ where }; let block = Block::default() - .title("Help") + .title(Span::styled("Help", Style::default().fg(help_block_text.0))) .borders(Borders::ALL) - .border_style(Style::default().fg(help_block_text.0)) - .title_style(Style::default().fg(help_block_text.0)); + .border_style(Style::default().fg(help_block_text.0)); - let lines = [Text::raw(help_block_text.1)]; - let help = Paragraph::new(lines.iter()) + let lines = Text::from(help_block_text.1); + let help = Paragraph::new(lines) .block(block) .style(Style::default().fg(help_block_text.0)); f.render_widget(help, chunks[1]); @@ -770,7 +771,14 @@ where { let chunks = Layout::default() .direction(Direction::Vertical) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .constraints( + [ + Constraint::Percentage(50), + Constraint::Percentage(25), + Constraint::Percentage(25), + ] + .as_ref(), + ) .margin(1) .split(layout_chunk); @@ -813,8 +821,10 @@ where let title_block = Block::default() .borders(Borders::ALL) - .title(&title) - .title_style(get_color(highlight_state, app.user_config.theme)) + .title(Span::styled( + &title, + get_color(highlight_state, app.user_config.theme), + )) .border_style(get_color(highlight_state, app.user_config.theme)); f.render_widget(title_block, layout_chunk); @@ -843,35 +853,38 @@ where PlayingItem::Episode(episode) => format!("{} - {}", episode.name, episode.show.name), }; - let lines = [Text::styled( + let lines = Text::from(Span::styled( play_bar_text, Style::default().fg(app.user_config.theme.playbar_text), - )]; + )); - let artist = Paragraph::new(lines.iter()) + let artist = Paragraph::new(lines) .style(Style::default().fg(app.user_config.theme.playbar_text)) .block( - Block::default().title(&track_name).title_style( + Block::default().title(Span::styled( + &track_name, Style::default() .fg(app.user_config.theme.selected) - .modifier(Modifier::BOLD), - ), + .add_modifier(Modifier::BOLD), + )), ); f.render_widget(artist, chunks[0]); let perc = get_track_progress_percentage(app.song_progress_ms, duration_ms); let song_progress_label = display_track_progress(app.song_progress_ms, duration_ms); let song_progress = Gauge::default() - .block(Block::default().title("")) - .style( + .gauge_style( Style::default() .fg(app.user_config.theme.playbar_progress) .bg(app.user_config.theme.playbar_background) - .modifier(Modifier::ITALIC | Modifier::BOLD), + .add_modifier(Modifier::ITALIC | Modifier::BOLD), ) .percent(perc) - .label(&song_progress_label); - f.render_widget(song_progress, chunks[1]); + .label(Span::styled( + &song_progress_label, + Style::default().fg(app.user_config.theme.playbar_progress_text), + )); + f.render_widget(song_progress, chunks[2]); } } } @@ -886,10 +899,10 @@ where .margin(5) .split(f.size()); - let playing_text = vec![ - Text::raw("Api response: "), - Text::styled(&app.api_error, Style::default().fg(app.user_config.theme.error_text)), - Text::styled( + let playing_text = Spans::from(vec![ + Span::raw("Api response: "), + Span::styled(&app.api_error, Style::default().fg(app.user_config.theme.error_text)), + Span::styled( " If you are trying to play a track, please check that @@ -899,24 +912,26 @@ If you are trying to play a track, please check that ", Style::default().fg(app.user_config.theme.text), ), - Text::styled(" + Span::styled(" Hint: a playback device must be either an official spotify client or a light weight alternative such as spotifyd ", Style::default().fg(app.user_config.theme.hint)), - Text::styled( + Span::styled( "\nPress to return", Style::default().fg(app.user_config.theme.inactive), ), - ]; + ]); - let playing_paragraph = Paragraph::new(playing_text.iter()) - .wrap(true) + let playing_paragraph = Paragraph::new(playing_text) + .wrap(Wrap { trim: true }) .style(Style::default().fg(app.user_config.theme.text)) .block( Block::default() .borders(Borders::ALL) - .title("Error") - .title_style(Style::default().fg(app.user_config.theme.error_border)) + .title(Span::styled( + "Error", + Style::default().fg(app.user_config.theme.error_border), + )) .border_style(Style::default().fg(app.user_config.theme.error_border)), ); f.render_widget(playing_paragraph, chunks[0]); @@ -939,9 +954,11 @@ where ); let welcome = Block::default() - .title("Welcome!") + .title(Span::styled( + "Welcome!", + get_color(highlight_state, app.user_config.theme), + )) .borders(Borders::ALL) - .title_style(get_color(highlight_state, app.user_config.theme)) .border_style(get_color(highlight_state, app.user_config.theme)); f.render_widget(welcome, layout_chunk); @@ -955,28 +972,29 @@ where changelog.replace("\n## [Unreleased]\n", "") }; - let top_text = vec![Text::styled( - BANNER, - Style::default().fg(app.user_config.theme.banner), - )]; + // Banner text with correct styling + let mut top_text = Text::from(BANNER); + top_text.patch_style(Style::default().fg(app.user_config.theme.banner)); - let bottom_text = vec![ - Text::raw("\nPlease report any bugs or missing features to https://github.com/Rigellute/spotify-tui\n\n"), - Text::raw(clean_changelog) - ]; + let bottom_text_raw = format!( + "{}{}", + "\nPlease report any bugs or missing features to https://github.com/Rigellute/spotify-tui\n\n", + clean_changelog + ); + let bottom_text = Text::from(bottom_text_raw.as_str()); // Contains the banner - let top_text = Paragraph::new(top_text.iter()) + let top_text = Paragraph::new(top_text) .style(Style::default().fg(app.user_config.theme.text)) .block(Block::default()); f.render_widget(top_text, chunks[0]); // CHANGELOG - let bottom_text = Paragraph::new(bottom_text.iter()) + let bottom_text = Paragraph::new(bottom_text) .style(Style::default().fg(app.user_config.theme.text)) .block(Block::default()) - .wrap(true) - .scroll(app.home_scroll); + .wrap(Wrap { trim: false }) + .scroll((app.home_scroll, 0)); f.render_widget(bottom_text, chunks[1]); } @@ -995,17 +1013,19 @@ fn draw_not_implemented_yet( current_route.hovered_block == block, ); let display_block = Block::default() - .title(title) + .title(Span::styled( + title, + get_color(highlight_state, app.user_config.theme), + )) .borders(Borders::ALL) - .title_style(get_color(highlight_state, app.user_config.theme)) .border_style(get_color(highlight_state, app.user_config.theme)); - let text = vec![Text::raw("Not implemented yet!")]; + let text = Text::from("Not implemented yet!"); - let not_implemented = Paragraph::new(text.iter()) + let not_implemented = Paragraph::new(text) .style(Style::default().fg(app.user_config.theme.text)) .block(display_block) - .wrap(true); + .wrap(Wrap { trim: true }); f.render_widget(not_implemented, layout_chunk); } @@ -1122,39 +1142,41 @@ where .margin(5) .split(f.size()); - let device_instructions = [ - Text::raw("To play tracks, please select a device. "), - Text::raw("Use `j/k` or up/down arrow keys to move up and down and to select. "), - Text::raw("Your choice here will be cached so you can jump straight back in when you next open `spotify-tui`. "), - Text::raw("You can change the playback device at any time by pressing `d`."), - ]; + let device_instructions = Spans::from(vec![ + Span::raw("To play tracks, please select a device. "), + Span::raw("Use `j/k` or up/down arrow keys to move up and down and to select. "), + Span::raw("Your choice here will be cached so you can jump straight back in when you next open `spotify-tui`. "), + Span::raw("You can change the playback device at any time by pressing `d`."), + ]); - let instructions = Paragraph::new(device_instructions.iter()) + let instructions = Paragraph::new(device_instructions) .style(Style::default().fg(app.user_config.theme.text)) - .wrap(true) + .wrap(Wrap { trim: true }) .block( - Block::default() - .borders(Borders::NONE) - .title("Welcome to spotify-tui!") - .title_style( - Style::default() - .fg(app.user_config.theme.active) - .modifier(Modifier::BOLD), - ), + Block::default().borders(Borders::NONE).title(Span::styled( + "Welcome to spotify-tui!", + Style::default() + .fg(app.user_config.theme.active) + .add_modifier(Modifier::BOLD), + )), ); f.render_widget(instructions, chunks[0]); - let no_device_message = vec![Text::raw("No devices found: Make sure a device is active")]; + let no_device_message = Span::raw("No devices found: Make sure a device is active"); - let items: Box> = match &app.devices { + let items = match &app.devices { Some(items) => { if items.devices.is_empty() { - Box::new(no_device_message.into_iter()) + vec![ListItem::new(no_device_message)] } else { - Box::new(items.devices.iter().map(|device| Text::raw(&device.name))) + items + .devices + .iter() + .map(|device| ListItem::new(Span::raw(&device.name))) + .collect() } } - None => Box::new(no_device_message.into_iter()), + None => vec![ListItem::new(no_device_message)], }; let mut state = ListState::default(); @@ -1162,16 +1184,18 @@ where let list = List::new(items) .block( Block::default() - .title("Devices") + .title(Span::styled( + "Devices", + Style::default().fg(app.user_config.theme.active), + )) .borders(Borders::ALL) - .title_style(Style::default().fg(app.user_config.theme.active)) .border_style(Style::default().fg(app.user_config.theme.inactive)), ) .style(Style::default().fg(app.user_config.theme.text)) .highlight_style( Style::default() .fg(app.user_config.theme.active) - .modifier(Modifier::BOLD), + .add_modifier(Modifier::BOLD), ); f.render_stateful_widget(list, chunks[1], &mut state); } @@ -1359,16 +1383,26 @@ fn draw_selectable_list( let mut state = ListState::default(); state.select(selected_index); - let list = List::new(items.iter().map(|i| Text::raw(i.as_ref()))) + let lst_items: Vec = items + .iter() + .map(|i| ListItem::new(Span::raw(i.as_ref()))) + .collect(); + + //TODO + let list = List::new(lst_items) .block( Block::default() - .title(title) + .title(Span::styled( + title, + get_color(highlight_state, app.user_config.theme), + )) .borders(Borders::ALL) - .title_style(get_color(highlight_state, app.user_config.theme)) .border_style(get_color(highlight_state, app.user_config.theme)), ) .style(Style::default().fg(app.user_config.theme.text)) - .highlight_style(get_color(highlight_state, app.user_config.theme).modifier(Modifier::BOLD)); + .highlight_style( + get_color(highlight_state, app.user_config.theme).add_modifier(Modifier::BOLD), + ); f.render_stateful_widget(list, layout_chunk, &mut state); } @@ -1403,13 +1437,16 @@ where // suggestion: possibly put this as part of // app.dialog, but would have to introduce lifetime - let text = [ - Text::raw("Are you sure you want to delete\nthe playlist: "), - Text::styled(playlist.as_str(), Style::default().modifier(Modifier::BOLD)), - Text::raw("?"), - ]; + let text = Spans::from(vec![ + Span::raw("Are you sure you want to delete\nthe playlist: "), + Span::styled( + playlist.as_str(), + Style::default().add_modifier(Modifier::BOLD), + ), + Span::raw("?"), + ]); - let text = Paragraph::new(text.iter()).alignment(Alignment::Center); + let text = Paragraph::new(text).alignment(Alignment::Center); f.render_widget(text, vchunks[0]); @@ -1419,8 +1456,8 @@ where .constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)].as_ref()) .split(vchunks[1]); - let ok_text = [Text::raw("Ok")]; - let ok = Paragraph::new(ok_text.iter()) + let ok_text = Span::raw("Ok"); + let ok = Paragraph::new(ok_text) .style(Style::default().fg(if app.confirm { app.user_config.theme.hovered } else { @@ -1430,8 +1467,8 @@ where f.render_widget(ok, hchunks[0]); - let cancel_text = [Text::raw("Cancel")]; - let cancel = Paragraph::new(cancel_text.iter()) + let cancel_text = Span::raw("Cancel"); + let cancel = Paragraph::new(cancel_text) .style(Style::default().fg(if app.confirm { app.user_config.theme.inactive } else { @@ -1455,7 +1492,8 @@ fn draw_table( ) where B: Backend, { - let selected_style = get_color(highlight_state, app.user_config.theme).modifier(Modifier::BOLD); + let selected_style = + get_color(highlight_state, app.user_config.theme).add_modifier(Modifier::BOLD); let track_playing_index = app.current_playback_context.to_owned().and_then(|ctx| { ctx.item.and_then(|item| match item { @@ -1493,7 +1531,7 @@ fn draw_table( formatted_row[title_idx] = format!("▶ {}", &formatted_row[title_idx]); style = Style::default() .fg(app.user_config.theme.active) - .modifier(Modifier::BOLD); + .add_modifier(Modifier::BOLD); } } } @@ -1528,8 +1566,10 @@ fn draw_table( Block::default() .borders(Borders::ALL) .style(Style::default().fg(app.user_config.theme.text)) - .title(title) - .title_style(get_color(highlight_state, app.user_config.theme)) + .title(Span::styled( + title, + get_color(highlight_state, app.user_config.theme), + )) .border_style(get_color(highlight_state, app.user_config.theme)), ) .style(Style::default().fg(app.user_config.theme.text)) diff --git a/src/user_config.rs b/src/user_config.rs index 36b58484..f76c6ea4 100644 --- a/src/user_config.rs +++ b/src/user_config.rs @@ -22,6 +22,7 @@ pub struct UserTheme { pub inactive: Option, pub playbar_background: Option, pub playbar_progress: Option, + pub playbar_progress_text: Option, pub playbar_text: Option, pub selected: Option, pub text: Option, @@ -40,6 +41,7 @@ pub struct Theme { pub inactive: Color, pub playbar_background: Color, pub playbar_progress: Color, + pub playbar_progress_text: Color, pub playbar_text: Color, pub selected: Color, pub text: Color, @@ -59,6 +61,7 @@ impl Default for Theme { inactive: Color::Gray, playbar_background: Color::Black, playbar_progress: Color::LightCyan, + playbar_progress_text: Color::LightCyan, playbar_text: Color::White, selected: Color::LightCyan, text: Color::White, @@ -337,6 +340,7 @@ impl UserConfig { to_theme_item!(inactive); to_theme_item!(playbar_background); to_theme_item!(playbar_progress); + to_theme_item!(playbar_progress_text); to_theme_item!(playbar_text); to_theme_item!(selected); to_theme_item!(text);