11mod ai;
2+ mod calendar;
23mod cli;
34mod llm;
45mod mail;
@@ -279,6 +280,10 @@ struct App {
279280 suggestions_rx : Option < mpsc:: Receiver < EmailEvent > > ,
280281 task_email_map : std:: collections:: HashMap < Uuid , String > ,
281282
283+ calendar_events : Vec < calendar:: CalendarEvent > ,
284+ calendar_loading : bool ,
285+ calendar_rx : Option < mpsc:: Receiver < Vec < calendar:: CalendarEvent > > > ,
286+
282287 esc_count : u8 ,
283288 esc_last : Instant ,
284289}
@@ -301,6 +306,19 @@ impl Drop for TerminalGuard {
301306 }
302307}
303308
309+ fn spawn_calendar_fetch ( ) -> mpsc:: Receiver < Vec < calendar:: CalendarEvent > > {
310+ let ( tx, rx) = mpsc:: channel ( ) ;
311+ std:: thread:: spawn ( move || match calendar:: get_upcoming_events ( 14 ) {
312+ Ok ( events) => {
313+ let _ = tx. send ( events) ;
314+ }
315+ Err ( _) => {
316+ let _ = tx. send ( Vec :: new ( ) ) ;
317+ }
318+ } ) ;
319+ rx
320+ }
321+
304322fn spawn_email_poller ( settings : AiSettings ) -> mpsc:: Receiver < EmailEvent > {
305323 let ( tx, rx) = mpsc:: channel ( ) ;
306324 std:: thread:: spawn ( move || {
@@ -509,6 +527,9 @@ fn main() -> io::Result<()> {
509527 suggestions_selected : 0 ,
510528 suggestions_rx : None ,
511529 task_email_map : std:: collections:: HashMap :: new ( ) ,
530+ calendar_events : Vec :: new ( ) ,
531+ calendar_loading : false ,
532+ calendar_rx : None ,
512533 esc_count : 0 ,
513534 esc_last : Instant :: now ( ) ,
514535 } ;
@@ -545,6 +566,10 @@ fn run_app(stdout: &mut Stdout, app: &mut App) -> io::Result<()> {
545566 needs_redraw = true ;
546567 }
547568
569+ if poll_calendar ( app) {
570+ needs_redraw = true ;
571+ }
572+
548573 if archive_check. elapsed ( ) >= Duration :: from_secs ( 60 ) {
549574 if auto_archive_tasks ( & mut app. tasks ) {
550575 persist ( app) ;
@@ -792,6 +817,10 @@ fn handle_key(app: &mut App, key: KeyEvent) -> io::Result<bool> {
792817 app. tab = Tab :: Calendar ;
793818 app. focus = Focus :: Board ;
794819 app. status = None ;
820+ if !app. calendar_loading && app. calendar_events . is_empty ( ) {
821+ app. calendar_loading = true ;
822+ app. calendar_rx = Some ( spawn_calendar_fetch ( ) ) ;
823+ }
795824 return Ok ( false ) ;
796825 }
797826 KeyCode :: Char ( '3' ) => {
@@ -873,6 +902,10 @@ fn handle_tabs_key(app: &mut App, key: KeyEvent) -> io::Result<bool> {
873902 app. tab = Tab :: Calendar ;
874903 app. focus = Focus :: Board ;
875904 app. status = None ;
905+ if !app. calendar_loading && app. calendar_events . is_empty ( ) {
906+ app. calendar_loading = true ;
907+ app. calendar_rx = Some ( spawn_calendar_fetch ( ) ) ;
908+ }
876909 }
877910 KeyCode :: Char ( '3' ) => {
878911 app. tab = Tab :: Default ;
@@ -3986,6 +4019,18 @@ fn poll_suggestions(app: &mut App) -> bool {
39864019 has_new
39874020}
39884021
4022+ fn poll_calendar ( app : & mut App ) -> bool {
4023+ if let Some ( rx) = & app. calendar_rx {
4024+ if let Ok ( events) = rx. try_recv ( ) {
4025+ app. calendar_events = events;
4026+ app. calendar_loading = false ;
4027+ app. calendar_rx = None ;
4028+ return true ;
4029+ }
4030+ }
4031+ false
4032+ }
4033+
39894034/// Apply a TaskUpdate to a task, returning true if anything changed.
39904035fn apply_update (
39914036 task : & mut Task ,
@@ -4967,59 +5012,96 @@ fn render_calendar_tab(stdout: &mut Stdout, app: &App, cols: u16, rows: u16) ->
49675012 let events_x = ( cal_width + 4 ) as u16 + cal_x;
49685013 let events_width = content_width. saturating_sub ( cal_width + 4 ) ;
49695014 if events_width > 10 {
5015+ let mut y_cur = y_start;
5016+
5017+ if !app. calendar_events . is_empty ( ) {
5018+ queue ! (
5019+ stdout,
5020+ MoveTo ( events_x, y_cur) ,
5021+ SetAttribute ( Attribute :: Bold ) ,
5022+ Print ( clamp_text( "Upcoming events" , events_width) ) ,
5023+ SetAttribute ( Attribute :: Reset )
5024+ ) ?;
5025+ y_cur += 1 ;
5026+ for evt in & app. calendar_events {
5027+ if y_cur >= y_start + available_rows as u16 {
5028+ break ;
5029+ }
5030+ let entry = format ! ( "{} • {} [{}]" , evt. start_date, evt. title, evt. calendar_name) ;
5031+ queue ! (
5032+ stdout,
5033+ MoveTo ( events_x, y_cur) ,
5034+ SetForegroundColor ( Color :: Magenta ) ,
5035+ Print ( clamp_text( & entry, events_width) ) ,
5036+ ResetColor
5037+ ) ?;
5038+ y_cur += 1 ;
5039+ }
5040+ y_cur += 1 ;
5041+ } else if app. calendar_loading {
5042+ queue ! (
5043+ stdout,
5044+ MoveTo ( events_x, y_cur) ,
5045+ SetForegroundColor ( Color :: DarkGrey ) ,
5046+ Print ( clamp_text( "Loading calendar events..." , events_width) ) ,
5047+ ResetColor
5048+ ) ?;
5049+ y_cur += 2 ;
5050+ }
5051+
49705052 queue ! (
49715053 stdout,
4972- MoveTo ( events_x, y_start ) ,
5054+ MoveTo ( events_x, y_cur ) ,
49735055 SetAttribute ( Attribute :: Bold ) ,
49745056 Print ( clamp_text( "Tasks with due dates" , events_width) ) ,
49755057 SetAttribute ( Attribute :: Reset )
49765058 ) ?;
4977-
5059+ y_cur += 1 ;
49785060 let mut tasks_with_dates: Vec < & model:: Task > =
49795061 app. tasks . iter ( ) . filter ( |t| t. due_date . is_some ( ) ) . collect ( ) ;
49805062 tasks_with_dates. sort_by_key ( |t| t. due_date ) ;
4981-
4982- for ( i, task) in tasks_with_dates. iter ( ) . enumerate ( ) {
4983- let y_row = y_start + 1 + i as u16 ;
4984- if y_row >= y_start + available_rows as u16 {
4985- break ;
4986- }
4987- let date_str = task
4988- . due_date
4989- . map ( |d| d. format ( "%m/%d" ) . to_string ( ) )
4990- . unwrap_or_default ( ) ;
4991- let short_id = task. id . to_string ( ) . chars ( ) . take ( 8 ) . collect :: < String > ( ) ;
4992- let status_icon = match task. progress {
4993- model:: Progress :: Done => "\u{2713} " ,
4994- model:: Progress :: InProgress => "\u{25d0} " ,
4995- model:: Progress :: Todo => "\u{25cb} " ,
4996- model:: Progress :: Backlog => "\u{b7} " ,
4997- model:: Progress :: Archived => "\u{2298} " ,
4998- } ;
4999- let entry = format ! ( "{} {} {} {}" , date_str, status_icon, short_id, task. title) ;
5000- let status_color = match task. progress {
5001- model:: Progress :: Done => Color :: DarkGrey ,
5002- model:: Progress :: InProgress => Color :: Yellow ,
5003- model:: Progress :: Todo => Color :: Blue ,
5004- model:: Progress :: Backlog => Color :: DarkGrey ,
5005- model:: Progress :: Archived => Color :: DarkGrey ,
5006- } ;
5007- queue ! (
5008- stdout,
5009- MoveTo ( events_x, y_row) ,
5010- SetForegroundColor ( status_color) ,
5011- Print ( clamp_text( & entry, events_width) ) ,
5012- ResetColor
5013- ) ?;
5014- }
50155063 if tasks_with_dates. is_empty ( ) {
50165064 queue ! (
50175065 stdout,
5018- MoveTo ( events_x, y_start + 1 ) ,
5066+ MoveTo ( events_x, y_cur ) ,
50195067 SetForegroundColor ( Color :: DarkGrey ) ,
50205068 Print ( clamp_text( "No tasks with due dates" , events_width) ) ,
50215069 ResetColor
50225070 ) ?;
5071+ } else {
5072+ for task in & tasks_with_dates {
5073+ if y_cur >= y_start + available_rows as u16 {
5074+ break ;
5075+ }
5076+ let date_str = task
5077+ . due_date
5078+ . map ( |d| d. format ( "%m/%d" ) . to_string ( ) )
5079+ . unwrap_or_default ( ) ;
5080+ let short_id = task. id . to_string ( ) . chars ( ) . take ( 8 ) . collect :: < String > ( ) ;
5081+ let status_icon = match task. progress {
5082+ model:: Progress :: Done => "\u{2713} " ,
5083+ model:: Progress :: InProgress => "\u{25d0} " ,
5084+ model:: Progress :: Todo => "\u{25cb} " ,
5085+ model:: Progress :: Backlog => "\u{b7} " ,
5086+ model:: Progress :: Archived => "\u{2298} " ,
5087+ } ;
5088+ let entry = format ! ( "{} {} {} {}" , date_str, status_icon, short_id, task. title) ;
5089+ let status_color = match task. progress {
5090+ model:: Progress :: Done => Color :: DarkGrey ,
5091+ model:: Progress :: InProgress => Color :: Yellow ,
5092+ model:: Progress :: Todo => Color :: Blue ,
5093+ model:: Progress :: Backlog => Color :: DarkGrey ,
5094+ model:: Progress :: Archived => Color :: DarkGrey ,
5095+ } ;
5096+ queue ! (
5097+ stdout,
5098+ MoveTo ( events_x, y_cur) ,
5099+ SetForegroundColor ( status_color) ,
5100+ Print ( clamp_text( & entry, events_width) ) ,
5101+ ResetColor
5102+ ) ?;
5103+ y_cur += 1 ;
5104+ }
50235105 }
50245106 }
50255107
0 commit comments