Add progress bar and dynamic panel sizing
All checks were successful
Build and Release / build-and-release (push) Successful in 53s
All checks were successful
Build and Release / build-and-release (push) Successful in 53s
Add progress bar to bottom status bar showing playback progress with gray background fill and metadata text overlay. - Add progress bar to status bar with border gray background - Implement dynamic panel sizing: 80/20 split favoring focused panel - Fix progress bar flashing on track change by resetting position/duration - Remove cache/buffer duration from status display - Reset player position/duration in play() to prevent stale values The progress bar uses a gray background (border color) that fills from left to right as the track plays, with white text for the filled portion and muted text for the unfilled portion.
This commit is contained in:
parent
93741320ac
commit
ccc762419f
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "cm-player"
|
||||
version = "0.1.25"
|
||||
version = "0.1.26"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
||||
82
src/main.rs
82
src/main.rs
@ -219,9 +219,10 @@ fn action_toggle_folder(state: &mut AppState) {
|
||||
}
|
||||
}
|
||||
|
||||
fn action_play_selection(state: &mut AppState, player: &mut player::Player) -> Result<()> {
|
||||
fn action_play_selection(state: &mut AppState, player: &mut player::Player, skip_position_update: &mut bool) -> Result<()> {
|
||||
state.play_selection();
|
||||
if let Some(ref path) = state.current_file {
|
||||
*skip_position_update = true; // Skip position update after track change
|
||||
player.play(path)?;
|
||||
// Explicitly resume playback in case MPV was paused
|
||||
player.resume()?;
|
||||
@ -292,7 +293,7 @@ fn action_remove_from_playlist(state: &mut AppState, player: &mut player::Player
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_navigate_track(state: &mut AppState, player: &mut player::Player, direction: i32) -> Result<()> {
|
||||
fn action_navigate_track(state: &mut AppState, player: &mut player::Player, direction: i32, skip_position_update: &mut bool) -> Result<()> {
|
||||
// direction: 1 for next, -1 for previous
|
||||
let new_index = if direction > 0 {
|
||||
state.playlist_index.saturating_add(1)
|
||||
@ -315,6 +316,7 @@ fn action_navigate_track(state: &mut AppState, player: &mut player::Player, dire
|
||||
PlayerState::Playing => {
|
||||
// Keep playing
|
||||
if let Some(ref path) = state.current_file {
|
||||
*skip_position_update = true; // Skip position update after track change
|
||||
player.play(path)?;
|
||||
player.resume()?;
|
||||
player.update_metadata();
|
||||
@ -324,6 +326,7 @@ fn action_navigate_track(state: &mut AppState, player: &mut player::Player, dire
|
||||
PlayerState::Paused => {
|
||||
// Load but stay paused
|
||||
if let Some(ref path) = state.current_file {
|
||||
*skip_position_update = true; // Skip position update after track change
|
||||
player.play_paused(path)?;
|
||||
player.update_metadata();
|
||||
tracing::info!("{} track (paused): {:?}", track_name, path);
|
||||
@ -344,7 +347,7 @@ fn action_navigate_track(state: &mut AppState, player: &mut player::Player, dire
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_play_from_playlist(state: &mut AppState, player: &mut player::Player, preserve_pause: bool) -> Result<()> {
|
||||
fn action_play_from_playlist(state: &mut AppState, player: &mut player::Player, preserve_pause: bool, skip_position_update: &mut bool) -> Result<()> {
|
||||
state.playlist_index = state.selected_playlist_index;
|
||||
state.current_file = Some(state.playlist[state.playlist_index].clone());
|
||||
|
||||
@ -353,6 +356,7 @@ fn action_play_from_playlist(state: &mut AppState, player: &mut player::Player,
|
||||
match player_state {
|
||||
PlayerState::Playing => {
|
||||
if let Some(ref path) = state.current_file {
|
||||
*skip_position_update = true; // Skip position update after track change
|
||||
player.play(path)?;
|
||||
player.resume()?;
|
||||
player.update_metadata();
|
||||
@ -361,6 +365,7 @@ fn action_play_from_playlist(state: &mut AppState, player: &mut player::Player,
|
||||
}
|
||||
PlayerState::Paused => {
|
||||
if let Some(ref path) = state.current_file {
|
||||
*skip_position_update = true; // Skip position update after track change
|
||||
player.play_paused(path)?;
|
||||
player.update_metadata();
|
||||
tracing::info!("Jumped to track (paused): {:?}", path);
|
||||
@ -368,6 +373,7 @@ fn action_play_from_playlist(state: &mut AppState, player: &mut player::Player,
|
||||
}
|
||||
PlayerState::Stopped => {
|
||||
if let Some(ref path) = state.current_file {
|
||||
*skip_position_update = true; // Skip position update after track change
|
||||
player.play(path)?;
|
||||
player.resume()?;
|
||||
player.update_metadata();
|
||||
@ -378,6 +384,7 @@ fn action_play_from_playlist(state: &mut AppState, player: &mut player::Player,
|
||||
}
|
||||
} else {
|
||||
if let Some(ref path) = state.current_file {
|
||||
*skip_position_update = true; // Skip position update after track change
|
||||
player.play(path)?;
|
||||
// Explicitly resume playback in case MPV was paused
|
||||
player.resume()?;
|
||||
@ -388,11 +395,11 @@ fn action_play_from_playlist(state: &mut AppState, player: &mut player::Player,
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_context_menu_action(menu_type: state::ContextMenuType, selected: usize, state: &mut AppState, player: &mut player::Player) -> Result<()> {
|
||||
fn handle_context_menu_action(menu_type: state::ContextMenuType, selected: usize, state: &mut AppState, player: &mut player::Player, skip_position_update: &mut bool) -> Result<()> {
|
||||
match menu_type {
|
||||
state::ContextMenuType::FilePanel => {
|
||||
match selected {
|
||||
0 => action_play_selection(state, player)?,
|
||||
0 => action_play_selection(state, player, skip_position_update)?,
|
||||
1 => state.add_to_playlist(),
|
||||
_ => {}
|
||||
}
|
||||
@ -434,6 +441,7 @@ fn run_app<B: ratatui::backend::Backend>(
|
||||
let mut metadata_update_counter = 0u32;
|
||||
let mut last_position = 0.0f64;
|
||||
let mut needs_redraw = true;
|
||||
let mut skip_position_update = false;
|
||||
let mut title_bar_area = ratatui::layout::Rect::default();
|
||||
let mut file_panel_area = ratatui::layout::Rect::default();
|
||||
let mut playlist_area = ratatui::layout::Rect::default();
|
||||
@ -470,11 +478,11 @@ fn run_app<B: ratatui::backend::Backend>(
|
||||
state_changed = true;
|
||||
}
|
||||
api::ApiCommand::Next => {
|
||||
action_navigate_track(state, player, 1)?;
|
||||
action_navigate_track(state, player, 1, &mut skip_position_update)?;
|
||||
state_changed = true;
|
||||
}
|
||||
api::ApiCommand::Prev => {
|
||||
action_navigate_track(state, player, -1)?;
|
||||
action_navigate_track(state, player, -1, &mut skip_position_update)?;
|
||||
state_changed = true;
|
||||
}
|
||||
api::ApiCommand::VolumeUp => {
|
||||
@ -531,12 +539,12 @@ fn run_app<B: ratatui::backend::Backend>(
|
||||
match event::read()? {
|
||||
Event::Key(key) => {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
handle_key_event(terminal, state, player, key)?;
|
||||
handle_key_event(terminal, state, player, key, &mut skip_position_update)?;
|
||||
needs_redraw = true;
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse) => {
|
||||
handle_mouse_event(state, mouse, title_bar_area, file_panel_area, playlist_area, player)?;
|
||||
handle_mouse_event(state, mouse, title_bar_area, file_panel_area, playlist_area, player, &mut skip_position_update)?;
|
||||
needs_redraw = true;
|
||||
}
|
||||
_ => {}
|
||||
@ -559,6 +567,7 @@ fn run_app<B: ratatui::backend::Backend>(
|
||||
state.current_position = 0.0;
|
||||
state.current_duration = 0.0;
|
||||
last_position = 0.0;
|
||||
skip_position_update = true; // Skip position update this iteration
|
||||
|
||||
player.play(path)?;
|
||||
player.resume()?;
|
||||
@ -589,21 +598,26 @@ fn run_app<B: ratatui::backend::Backend>(
|
||||
}
|
||||
|
||||
// Update position and duration from player
|
||||
let new_position = player.get_position().unwrap_or(0.0);
|
||||
let new_duration = player.get_duration().unwrap_or(0.0);
|
||||
// Skip this iteration if we just started a new track to avoid stale MPV values
|
||||
if skip_position_update {
|
||||
skip_position_update = false;
|
||||
} else {
|
||||
let new_position = player.get_position().unwrap_or(0.0);
|
||||
let new_duration = player.get_duration().unwrap_or(0.0);
|
||||
|
||||
// Only update if displayed value (rounded to seconds) changed
|
||||
let old_display_secs = last_position as u32;
|
||||
let new_display_secs = new_position as u32;
|
||||
if new_display_secs != old_display_secs {
|
||||
state.current_position = new_position;
|
||||
last_position = new_position;
|
||||
state_changed = true;
|
||||
}
|
||||
// Only update if displayed value (rounded to seconds) changed
|
||||
let old_display_secs = last_position as u32;
|
||||
let new_display_secs = new_position as u32;
|
||||
if new_display_secs != old_display_secs {
|
||||
state.current_position = new_position;
|
||||
last_position = new_position;
|
||||
state_changed = true;
|
||||
}
|
||||
|
||||
if state.current_duration != new_duration {
|
||||
state.current_duration = new_duration;
|
||||
state_changed = true;
|
||||
if state.current_duration != new_duration {
|
||||
state.current_duration = new_duration;
|
||||
state_changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -632,12 +646,12 @@ fn run_app<B: ratatui::backend::Backend>(
|
||||
match event::read()? {
|
||||
Event::Key(key) => {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
handle_key_event(terminal, state, player, key)?;
|
||||
handle_key_event(terminal, state, player, key, &mut skip_position_update)?;
|
||||
needs_redraw = true; // Force redraw after key event
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse) => {
|
||||
handle_mouse_event(state, mouse, title_bar_area, file_panel_area, playlist_area, player)?;
|
||||
handle_mouse_event(state, mouse, title_bar_area, file_panel_area, playlist_area, player, &mut skip_position_update)?;
|
||||
needs_redraw = true; // Force redraw after mouse event
|
||||
}
|
||||
_ => {}
|
||||
@ -652,7 +666,7 @@ fn run_app<B: ratatui::backend::Backend>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_key_event<B: ratatui::backend::Backend>(terminal: &mut Terminal<B>, state: &mut AppState, player: &mut player::Player, key: KeyEvent) -> Result<()> {
|
||||
fn handle_key_event<B: ratatui::backend::Backend>(terminal: &mut Terminal<B>, state: &mut AppState, player: &mut player::Player, key: KeyEvent, skip_position_update: &mut bool) -> Result<()> {
|
||||
// Handle confirmation popup
|
||||
if state.show_refresh_confirm {
|
||||
match key.code {
|
||||
@ -747,7 +761,7 @@ fn handle_key_event<B: ratatui::backend::Backend>(terminal: &mut Terminal<B>, st
|
||||
let menu_type = menu.menu_type;
|
||||
let selected = menu.selected_index;
|
||||
state.context_menu = None;
|
||||
handle_context_menu_action(menu_type, selected, state, player)?;
|
||||
handle_context_menu_action(menu_type, selected, state, player, skip_position_update)?;
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
state.context_menu = None;
|
||||
@ -798,11 +812,11 @@ fn handle_key_event<B: ratatui::backend::Backend>(terminal: &mut Terminal<B>, st
|
||||
}
|
||||
(KeyCode::Char('J'), KeyModifiers::SHIFT) => {
|
||||
// Next track
|
||||
action_navigate_track(state, player, 1)?;
|
||||
action_navigate_track(state, player, 1, skip_position_update)?;
|
||||
}
|
||||
(KeyCode::Char('K'), KeyModifiers::SHIFT) => {
|
||||
// Previous track
|
||||
action_navigate_track(state, player, -1)?;
|
||||
action_navigate_track(state, player, -1, skip_position_update)?;
|
||||
}
|
||||
(KeyCode::Char('d'), KeyModifiers::CONTROL) => {
|
||||
if state.focus_playlist {
|
||||
@ -871,10 +885,10 @@ fn handle_key_event<B: ratatui::backend::Backend>(terminal: &mut Terminal<B>, st
|
||||
(KeyCode::Enter, _) => {
|
||||
if state.focus_playlist {
|
||||
if state.selected_playlist_index < state.playlist.len() {
|
||||
action_play_from_playlist(state, player, false)?;
|
||||
action_play_from_playlist(state, player, false, skip_position_update)?;
|
||||
}
|
||||
} else {
|
||||
action_play_selection(state, player)?;
|
||||
action_play_selection(state, player, skip_position_update)?;
|
||||
}
|
||||
}
|
||||
(KeyCode::Char('s'), _) => {
|
||||
@ -924,7 +938,7 @@ fn handle_key_event<B: ratatui::backend::Backend>(terminal: &mut Terminal<B>, st
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_mouse_event(state: &mut AppState, mouse: MouseEvent, title_bar_area: ratatui::layout::Rect, file_panel_area: ratatui::layout::Rect, playlist_area: ratatui::layout::Rect, player: &mut player::Player) -> Result<()> {
|
||||
fn handle_mouse_event(state: &mut AppState, mouse: MouseEvent, title_bar_area: ratatui::layout::Rect, file_panel_area: ratatui::layout::Rect, playlist_area: ratatui::layout::Rect, player: &mut player::Player, skip_position_update: &mut bool) -> Result<()> {
|
||||
use crossterm::event::MouseButton;
|
||||
use crate::state::ContextMenuType;
|
||||
|
||||
@ -993,7 +1007,7 @@ fn handle_mouse_event(state: &mut AppState, mouse: MouseEvent, title_bar_area: r
|
||||
let menu_type = menu.menu_type;
|
||||
let selected = relative_y;
|
||||
state.context_menu = None;
|
||||
handle_context_menu_action(menu_type, selected, state, player)?;
|
||||
handle_context_menu_action(menu_type, selected, state, player, skip_position_update)?;
|
||||
}
|
||||
return Ok(());
|
||||
} else {
|
||||
@ -1117,7 +1131,7 @@ fn handle_mouse_event(state: &mut AppState, mouse: MouseEvent, title_bar_area: r
|
||||
if item.is_dir {
|
||||
action_toggle_folder(state);
|
||||
} else {
|
||||
action_play_selection(state, player)?;
|
||||
action_play_selection(state, player, skip_position_update)?;
|
||||
}
|
||||
}
|
||||
// Reset click tracking after action
|
||||
@ -1175,7 +1189,7 @@ fn handle_mouse_event(state: &mut AppState, mouse: MouseEvent, title_bar_area: r
|
||||
if is_double_click {
|
||||
// Double click = play the track (preserve pause state)
|
||||
state.selected_playlist_index = actual_track;
|
||||
action_play_from_playlist(state, player, true)?;
|
||||
action_play_from_playlist(state, player, true, skip_position_update)?;
|
||||
// Reset click tracking after action
|
||||
state.last_click_time = None;
|
||||
state.last_click_index = None;
|
||||
|
||||
@ -201,6 +201,9 @@ impl Player {
|
||||
|
||||
pub fn play(&mut self, path: &Path) -> Result<()> {
|
||||
let path_str = path.to_string_lossy();
|
||||
// Reset position/duration before loading new file to avoid showing stale values
|
||||
self.position = 0.0;
|
||||
self.duration = 0.0;
|
||||
self.send_command("loadfile", &[json!(path_str), json!("replace")])?;
|
||||
tracing::info!("Playing: {}", path_str);
|
||||
Ok(())
|
||||
@ -208,6 +211,9 @@ impl Player {
|
||||
|
||||
pub fn play_paused(&mut self, path: &Path) -> Result<()> {
|
||||
let path_str = path.to_string_lossy();
|
||||
// Reset position/duration before loading new file to avoid showing stale values
|
||||
self.position = 0.0;
|
||||
self.duration = 0.0;
|
||||
// Load file but start paused - avoids audio blip when jumping tracks while paused
|
||||
self.send_command("loadfile", &[json!(path_str), json!("replace"), json!({"pause": true})])?;
|
||||
tracing::info!("Playing (paused): {}", path_str);
|
||||
|
||||
152
src/ui/mod.rs
152
src/ui/mod.rs
@ -29,9 +29,16 @@ pub fn render(frame: &mut Frame, state: &mut AppState, player: &mut Player) -> (
|
||||
.split(frame.area());
|
||||
|
||||
// Main content: left (files) | right (status + playlist)
|
||||
// Switch proportions based on focus: 80/20 for focused panel
|
||||
let (left_percent, right_percent) = if state.focus_playlist {
|
||||
(20, 80) // Playlist focused: small file panel, large playlist
|
||||
} else {
|
||||
(80, 20) // File panel focused: large file panel, small playlist
|
||||
};
|
||||
|
||||
let content_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.constraints([Constraint::Percentage(left_percent), Constraint::Percentage(right_percent)])
|
||||
.split(main_chunks[1]);
|
||||
|
||||
render_title_bar(frame, state, player, main_chunks[0]);
|
||||
@ -462,9 +469,21 @@ fn render_title_bar(frame: &mut Frame, state: &AppState, player: &mut Player, ar
|
||||
}
|
||||
|
||||
fn render_status_bar(frame: &mut Frame, state: &AppState, player: &mut Player, area: Rect) {
|
||||
if state.search_mode {
|
||||
// Calculate progress percentage for progress bar
|
||||
let progress_percent = if state.current_duration > 0.0 {
|
||||
(state.current_position / state.current_duration).clamp(0.0, 1.0)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
// If playing and has duration, show progress bar with overlaid text
|
||||
let player_state = player.get_player_state().unwrap_or(PlayerState::Stopped);
|
||||
let show_progress_bar = player_state != PlayerState::Stopped && state.current_duration > 0.0;
|
||||
|
||||
// Determine text content based on mode
|
||||
let status_text = if state.search_mode {
|
||||
// Show search prompt with current query and match count - LEFT aligned
|
||||
let search_text = if state.focus_playlist {
|
||||
if state.focus_playlist {
|
||||
// Searching in playlist
|
||||
if !state.playlist_tab_search_results.is_empty() {
|
||||
format!("/{}_ Playlist Search: {}/{}", state.search_query, state.playlist_tab_search_index + 1, state.playlist_tab_search_results.len())
|
||||
@ -482,28 +501,28 @@ fn render_status_bar(frame: &mut Frame, state: &AppState, player: &mut Player, a
|
||||
} else {
|
||||
format!("/{}_", state.search_query)
|
||||
}
|
||||
};
|
||||
let status_bar = Paragraph::new(search_text)
|
||||
.style(Style::default().fg(Color::White).bg(Theme::background()));
|
||||
frame.render_widget(status_bar, area);
|
||||
}
|
||||
} else if !state.search_matches.is_empty() {
|
||||
// Show search navigation when file search results are active
|
||||
let search_text = format!("/{} Search: {}/{}", state.search_query, state.search_match_index + 1, state.search_matches.len());
|
||||
let status_bar = Paragraph::new(search_text)
|
||||
.style(Style::default().fg(Color::White).bg(Theme::background()));
|
||||
frame.render_widget(status_bar, area);
|
||||
format!("/{} Search: {}/{}", state.search_query, state.search_match_index + 1, state.search_matches.len())
|
||||
} else if !state.playlist_search_matches.is_empty() {
|
||||
// Show search navigation when playlist search results are active
|
||||
let search_text = format!("/{} Playlist Search: {}/{}", state.search_query, state.playlist_search_match_index + 1, state.playlist_search_matches.len());
|
||||
let status_bar = Paragraph::new(search_text)
|
||||
.style(Style::default().fg(Color::White).bg(Theme::background()));
|
||||
frame.render_widget(status_bar, area);
|
||||
format!("/{} Playlist Search: {}/{}", state.search_query, state.playlist_search_match_index + 1, state.playlist_search_matches.len())
|
||||
} else if state.visual_mode {
|
||||
// Show visual mode indicator
|
||||
let visual_text = format!("-- VISUAL -- {} files marked", state.marked_files.len());
|
||||
let status_bar = Paragraph::new(visual_text)
|
||||
.style(Style::default().fg(Theme::foreground()).bg(Theme::background()));
|
||||
format!("-- VISUAL -- {} files marked", state.marked_files.len())
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
// If we have status text (search/visual mode), show it without progress bar
|
||||
if !status_text.is_empty() {
|
||||
let status_bar = Paragraph::new(status_text)
|
||||
.style(Style::default().fg(Color::White).bg(Theme::background()));
|
||||
frame.render_widget(status_bar, area);
|
||||
} else if show_progress_bar {
|
||||
// Show progress bar with metadata text overlay
|
||||
render_progress_bar(frame, state, player, area, progress_percent);
|
||||
} else {
|
||||
// Normal mode: show media metadata if playing
|
||||
// Split into left (artist/album/title) and right (technical info)
|
||||
@ -537,12 +556,6 @@ fn render_status_bar(frame: &mut Frame, state: &AppState, player: &mut Player, a
|
||||
right_parts.push(format!("{} Hz", samplerate));
|
||||
}
|
||||
|
||||
if let Some(cache_dur) = player.cache_duration {
|
||||
if cache_dur > 0.0 {
|
||||
right_parts.push(format!("{:.1}s", cache_dur));
|
||||
}
|
||||
}
|
||||
|
||||
// Create layout for left and right sections
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
@ -575,6 +588,97 @@ fn render_status_bar(frame: &mut Frame, state: &AppState, player: &mut Player, a
|
||||
}
|
||||
}
|
||||
|
||||
fn render_progress_bar(frame: &mut Frame, _state: &AppState, player: &mut Player, area: Rect, progress_percent: f64) {
|
||||
// Get metadata to display
|
||||
let mut left_parts = Vec::new();
|
||||
let mut right_parts = Vec::new();
|
||||
|
||||
// Left side: Artist | Album | Title
|
||||
if let Some(ref artist) = player.artist {
|
||||
left_parts.push(artist.clone());
|
||||
}
|
||||
|
||||
if let Some(ref album) = player.album {
|
||||
left_parts.push(album.clone());
|
||||
}
|
||||
|
||||
if let Some(ref title) = player.media_title {
|
||||
left_parts.push(title.clone());
|
||||
}
|
||||
|
||||
// Right side: Bitrate | Codec | Sample rate | Cache
|
||||
if let Some(bitrate) = player.audio_bitrate {
|
||||
right_parts.push(format!("{:.0} kbps", bitrate));
|
||||
}
|
||||
|
||||
if let Some(ref codec) = player.audio_codec {
|
||||
right_parts.push(codec.to_uppercase());
|
||||
}
|
||||
|
||||
if let Some(samplerate) = player.sample_rate {
|
||||
right_parts.push(format!("{} Hz", samplerate));
|
||||
}
|
||||
|
||||
// Build text parts
|
||||
let left_text = if !left_parts.is_empty() {
|
||||
format!(" {}", left_parts.join(" | "))
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let right_text = if !right_parts.is_empty() {
|
||||
format!("{} ", right_parts.join(" | "))
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
// Calculate filled width based on progress
|
||||
let total_width = area.width as usize;
|
||||
let filled_width = (total_width as f64 * progress_percent) as usize;
|
||||
|
||||
// Build the full line character by character with proper spacing
|
||||
let left_chars: Vec<char> = left_text.chars().collect();
|
||||
let right_chars: Vec<char> = right_text.chars().collect();
|
||||
let right_start_pos = total_width.saturating_sub(right_chars.len());
|
||||
|
||||
// Build spans with progress bar background
|
||||
let mut spans = Vec::new();
|
||||
|
||||
for i in 0..total_width {
|
||||
// Determine which character to show
|
||||
let ch = if i < left_chars.len() {
|
||||
left_chars[i].to_string()
|
||||
} else if i >= right_start_pos && i - right_start_pos < right_chars.len() {
|
||||
right_chars[i - right_start_pos].to_string()
|
||||
} else {
|
||||
" ".to_string()
|
||||
};
|
||||
|
||||
// Apply progress bar background
|
||||
if i < filled_width {
|
||||
// Filled portion - border color background with black text
|
||||
spans.push(Span::styled(
|
||||
ch,
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Theme::border())
|
||||
));
|
||||
} else {
|
||||
// Unfilled portion - normal background
|
||||
spans.push(Span::styled(
|
||||
ch,
|
||||
Style::default()
|
||||
.fg(Theme::muted_text())
|
||||
.bg(Theme::background())
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let progress_line = Line::from(spans);
|
||||
let progress_widget = Paragraph::new(progress_line);
|
||||
frame.render_widget(progress_widget, area);
|
||||
}
|
||||
|
||||
fn render_confirm_popup(frame: &mut Frame, title: &str, message: &str) {
|
||||
// Create centered popup area
|
||||
let area = frame.area();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user