From 7ebafec3c0cd0047be50e4b4f8877283aa793ebc Mon Sep 17 00:00:00 2001 From: Ruben Rosario Date: Sun, 21 Jun 2026 18:01:49 +0100 Subject: [PATCH] Calendar panel: full-width layout with 4 weekly columns - Moved Calendar from body left column (8 lines) to full-width row between body and status bar (12 lines) - Calendar splits into 4 horizontal panels, each showing one week starting from Monday of the current week - Day headers in Cyan (Yellow for today), events in White - Removed old date-grouped event list rendering - Body layout simplified to single horizontal split (Tasks | Detail) --- src/ui/components.rs | 146 +++++++++++++++++++++++++++---------------- src/ui/mod.rs | 29 ++++----- 2 files changed, 104 insertions(+), 71 deletions(-) diff --git a/src/ui/components.rs b/src/ui/components.rs index 48cad86..9bcc08f 100644 --- a/src/ui/components.rs +++ b/src/ui/components.rs @@ -1,3 +1,5 @@ +use chrono::Datelike; + use ratatui::style::{Color, Modifier, Style}; use ratatui::text::{Line, Span, Text}; use ratatui::widgets::{Block, Borders, Clear, List, ListItem, Paragraph, Tabs, Wrap}; @@ -600,70 +602,104 @@ pub fn render_calendar_panel( area: Rect, events: &[CalendarEvent], focused: bool, - scroll: u16, + _scroll: u16, ) { - let block = Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(if focused { FOCUS_COLOR } else { Color::DarkGray })) - .title(" Calendar ") - .title_alignment(Alignment::Left); - - if events.is_empty() { - let paragraph = Paragraph::new(Text::from(Line::from(Span::styled( - " No upcoming events ", - Style::default().fg(Color::DarkGray), - )))) - .block(block) - .alignment(Alignment::Center); - frame.render_widget(paragraph, area); + if area.width < 20 || area.height < 3 { return; } - let mut lines: Vec = Vec::new(); - let mut current_date: Option = None; + let today = chrono::Local::now().naive_local().date(); + let weekday = today.weekday().num_days_from_monday(); + let this_monday = today - chrono::Duration::days(weekday as i64); - for event in events { - if let Some(start) = event.start { - let event_date = start.date(); - if Some(event_date) != current_date { - current_date = Some(event_date); - let day_header = format!( - " --- {} {} --- ", - event_date.format("%A"), - event_date.format("%d/%m"), - ); - lines.push(Line::from(Span::styled( - day_header, - Style::default().fg(Color::Gray), - ))); + let cols = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Ratio(1, 4); 4]) + .split(area); + + let has_events = events.iter().any(|e| { + e.start.map_or(false, |s| { + let d = s.date(); + d >= this_monday && d < this_monday + chrono::Duration::days(28) + }) + }); + + for week_idx in 0..4 { + let week_start = this_monday + chrono::Duration::weeks(week_idx as i64); + let week_title = format!(" W/C {} ", week_start.format("%d/%m")); + let col_area = cols[week_idx]; + + let border = if focused { FOCUS_COLOR } else { Color::DarkGray }; + let block = Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(border)) + .title(week_title) + .title_alignment(Alignment::Left); + + if !has_events { + let msg = if week_idx == 0 { + " No upcoming events " + } else { + "" + }; + let paragraph = Paragraph::new(Text::from(Line::from(Span::styled( + msg, + Style::default().fg(Color::DarkGray), + )))) + .block(block); + frame.render_widget(paragraph, col_area); + continue; + } + + let inner = block.inner(col_area); + let inner_h = inner.height as usize; + + let mut lines: Vec = Vec::new(); + + for day_offset in 0..7 { + let day = week_start + chrono::Duration::days(day_offset); + + let day_style = if day == today { + Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD) + } else { + Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD) + }; + let day_label = format!( + " {} {}", + match day.weekday() { + chrono::Weekday::Mon => "Mon", + chrono::Weekday::Tue => "Tue", + chrono::Weekday::Wed => "Wed", + chrono::Weekday::Thu => "Thu", + chrono::Weekday::Fri => "Fri", + chrono::Weekday::Sat => "Sat", + chrono::Weekday::Sun => "Sun", + }, + day.format("%d/%m") + ); + lines.push(Line::from(Span::styled(day_label, day_style))); + + if lines.len() >= inner_h { + break; } - let time_str = start.format("%H:%M").to_string(); - let summary = &event.summary; - let line_text = if summary.len() > 30 { - format!(" {} {:.30}…", time_str, summary) - } else { - format!(" {} {}", time_str, summary) - }; - lines.push(Line::from(Span::styled( - line_text, - Style::default().fg(DETAIL_COLOR), - ))); + for event in events.iter().filter(|e| e.start.map_or(false, |s| s.date() == day)) { + let time_str = event.start.map(|s| s.format("%H:%M").to_string()).unwrap_or_default(); + let line_text = format!(" {} {}", time_str, event.summary); + lines.push(Line::from(Span::styled( + line_text, + Style::default().fg(DETAIL_COLOR), + ))); + + if lines.len() >= inner_h { + break; + } + } } + + let paragraph = Paragraph::new(Text::from(lines)).block(block); + frame.render_widget(paragraph, col_area); } - - let inner_h = (area.height as usize).saturating_sub(2); - let visible_lines: Vec = lines - .iter() - .skip(scroll as usize) - .take(inner_h) - .cloned() - .collect(); - - let paragraph = Paragraph::new(Text::from(visible_lines)) - .block(block) - .scroll((0, 0)); - frame.render_widget(paragraph, area); } /// Simple word wrap: splits text at word boundaries to fit max_width chars per line diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 2ec552a..a726970 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -61,13 +61,15 @@ pub fn draw(frame: &mut Frame, view: AppView) { .constraints([ Constraint::Length(3), Constraint::Min(0), + Constraint::Length(12), Constraint::Length(1), ]) .split(area); let tabs_area = main_layout[0]; let body_area = main_layout[1]; - let status_area = main_layout[2]; + let calendar_area = main_layout[2]; + let status_area = main_layout[3]; let is_tabs_focused = view.focus == Focus::Tabs; render_tabs_bar(frame, tabs_area, view.lists, view.selected_list, is_tabs_focused); @@ -77,30 +79,16 @@ pub fn draw(frame: &mut Frame, view: AppView) { .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) .split(body_area); - let left_col = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Min(0), Constraint::Length(8)]) - .split(body_layout[0]); - let is_task_list_focused = view.focus == Focus::TaskList; render_task_list( frame, - left_col[0], + body_layout[0], view.tasks, view.selected_task, is_task_list_focused, view.task_list_scroll, ); - let is_calendar_focused = view.focus == Focus::Calendar; - render_calendar_panel( - frame, - left_col[1], - view.calendar_events, - is_calendar_focused, - view.calendar_scroll, - ); - let is_detail_focused = view.focus == Focus::Detail; render_detail( frame, @@ -110,6 +98,15 @@ pub fn draw(frame: &mut Frame, view: AppView) { view.detail_scroll, ); + let is_calendar_focused = view.focus == Focus::Calendar; + render_calendar_panel( + frame, + calendar_area, + view.calendar_events, + is_calendar_focused, + view.calendar_scroll, + ); + render_status_bar(frame, status_area, view.network_status, view.sync_stats); if let Some(popup) = view.show_popup {