@@ -9,7 +9,7 @@ use std::io::{self, Stdout, Write};
99use std:: sync:: mpsc;
1010use std:: time:: { Duration , Instant } ;
1111
12- use chrono:: Utc ;
12+ use chrono:: { Datelike , Utc } ;
1313use crossterm:: {
1414 cursor:: { Hide , MoveTo , Show } ,
1515 event:: {
@@ -32,13 +32,13 @@ use crate::storage::{AiSettings, Storage};
3232#[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
3333enum Tab {
3434 Checklist ,
35+ Calendar ,
3536 Default ,
3637 Timeline ,
3738 Kanban ,
3839 Settings ,
3940 Suggestions ,
4041}
41-
4242const MODEL_OPTIONS : & [ & str ] = & [
4343 // Anthropic
4444 "claude-opus-4-6" ,
@@ -49,23 +49,23 @@ const MODEL_OPTIONS: &[&str] = &[
4949 "o3" ,
5050 "o4-mini" ,
5151] ;
52-
5352impl Tab {
5453 fn next ( self ) -> Tab {
5554 match self {
56- Tab :: Checklist => Tab :: Default ,
55+ Tab :: Checklist => Tab :: Calendar ,
56+ Tab :: Calendar => Tab :: Default ,
5757 Tab :: Default => Tab :: Timeline ,
5858 Tab :: Timeline => Tab :: Kanban ,
5959 Tab :: Kanban => Tab :: Suggestions ,
6060 Tab :: Suggestions => Tab :: Settings ,
6161 Tab :: Settings => Tab :: Checklist ,
6262 }
6363 }
64-
6564 fn prev ( self ) -> Tab {
6665 match self {
6766 Tab :: Checklist => Tab :: Settings ,
68- Tab :: Default => Tab :: Checklist ,
67+ Tab :: Calendar => Tab :: Checklist ,
68+ Tab :: Default => Tab :: Calendar ,
6969 Tab :: Timeline => Tab :: Default ,
7070 Tab :: Kanban => Tab :: Timeline ,
7171 Tab :: Suggestions => Tab :: Kanban ,
@@ -789,24 +789,30 @@ fn handle_key(app: &mut App, key: KeyEvent) -> io::Result<bool> {
789789 return Ok ( false ) ;
790790 }
791791 KeyCode :: Char ( '2' ) => {
792+ app. tab = Tab :: Calendar ;
793+ app. focus = Focus :: Board ;
794+ app. status = None ;
795+ return Ok ( false ) ;
796+ }
797+ KeyCode :: Char ( '3' ) => {
792798 app. tab = Tab :: Default ;
793799 app. focus = Focus :: Input ;
794800 app. status = None ;
795801 return Ok ( false ) ;
796802 }
797- KeyCode :: Char ( '3 ' ) => {
803+ KeyCode :: Char ( '4 ' ) => {
798804 app. tab = Tab :: Timeline ;
799805 app. focus = Focus :: Board ;
800806 app. status = None ;
801807 return Ok ( false ) ;
802808 }
803- KeyCode :: Char ( '4 ' ) => {
809+ KeyCode :: Char ( '5 ' ) => {
804810 app. tab = Tab :: Kanban ;
805811 app. focus = Focus :: Board ;
806812 app. status = None ;
807813 return Ok ( false ) ;
808814 }
809- KeyCode :: Char ( '5 ' ) => {
815+ KeyCode :: Char ( '6 ' ) => {
810816 app. tab = Tab :: Suggestions ;
811817 app. focus = Focus :: Board ;
812818 app. status = None ;
@@ -837,6 +843,7 @@ fn handle_key(app: &mut App, key: KeyEvent) -> io::Result<bool> {
837843
838844 match app. tab {
839845 Tab :: Checklist => handle_checklist_key ( app, key) ,
846+ Tab :: Calendar => Ok ( false ) ,
840847 Tab :: Default => handle_default_tab_key ( app, key) ,
841848 Tab :: Timeline => handle_timeline_key ( app, key) ,
842849 Tab :: Kanban => handle_kanban_key ( app, key) ,
@@ -863,21 +870,26 @@ fn handle_tabs_key(app: &mut App, key: KeyEvent) -> io::Result<bool> {
863870 app. status = None ;
864871 }
865872 KeyCode :: Char ( '2' ) => {
873+ app. tab = Tab :: Calendar ;
874+ app. focus = Focus :: Board ;
875+ app. status = None ;
876+ }
877+ KeyCode :: Char ( '3' ) => {
866878 app. tab = Tab :: Default ;
867879 app. focus = Focus :: Input ;
868880 app. status = None ;
869881 }
870- KeyCode :: Char ( '3 ' ) => {
882+ KeyCode :: Char ( '4 ' ) => {
871883 app. tab = Tab :: Timeline ;
872884 app. focus = Focus :: Board ;
873885 app. status = None ;
874886 }
875- KeyCode :: Char ( '4 ' ) => {
887+ KeyCode :: Char ( '5 ' ) => {
876888 app. tab = Tab :: Kanban ;
877889 app. focus = Focus :: Board ;
878890 app. status = None ;
879891 }
880- KeyCode :: Char ( '5 ' ) => {
892+ KeyCode :: Char ( '6 ' ) => {
881893 app. tab = Tab :: Suggestions ;
882894 app. focus = Focus :: Board ;
883895 app. status = None ;
@@ -4483,6 +4495,7 @@ fn render(stdout: &mut Stdout, app: &mut App, clear: bool) -> io::Result<()> {
44834495
44844496 match app. tab {
44854497 Tab :: Checklist => render_checklist_tab ( stdout, app, cols, rows) ?,
4498+ Tab :: Calendar => render_calendar_tab ( stdout, app, cols, rows) ?,
44864499 Tab :: Default => render_default_tab ( stdout, app, cols, rows) ?,
44874500 Tab :: Timeline => render_timeline_tab ( stdout, app, cols, rows) ?,
44884501 Tab :: Kanban => render_kanban_tab ( stdout, app, cols, rows) ?,
@@ -4562,10 +4575,11 @@ fn render_tabs(stdout: &mut Stdout, app: &App, cols: u16) -> io::Result<()> {
45624575
45634576 let left_tabs: & [ ( Tab , & str ) ] = & [
45644577 ( Tab :: Checklist , "1 Checklist" ) ,
4565- ( Tab :: Default , "2 Buckets" ) ,
4566- ( Tab :: Timeline , "3 Timeline" ) ,
4567- ( Tab :: Kanban , "4 Kanban" ) ,
4568- ( Tab :: Suggestions , "5 Suggestions" ) ,
4578+ ( Tab :: Calendar , "2 Calendar" ) ,
4579+ ( Tab :: Default , "3 Buckets" ) ,
4580+ ( Tab :: Timeline , "4 Timeline" ) ,
4581+ ( Tab :: Kanban , "5 Kanban" ) ,
4582+ ( Tab :: Suggestions , "6 Suggestions" ) ,
45694583 ] ;
45704584
45714585 for ( tab, label) in left_tabs {
@@ -4870,6 +4884,158 @@ fn checklist_task_order(
48704884 result
48714885}
48724886
4887+ fn render_calendar_tab ( stdout : & mut Stdout , app : & App , cols : u16 , rows : u16 ) -> io:: Result < ( ) > {
4888+ let width = cols as usize ;
4889+ let content_width = width. saturating_sub ( 2 ) ;
4890+ let x = 1u16 ;
4891+ let y_start = 2u16 ;
4892+ let available_rows = rows. saturating_sub ( 7 ) as usize ;
4893+
4894+ let today = chrono:: Local :: now ( ) . date_naive ( ) ;
4895+ let year = today. year ( ) ;
4896+ let month = today. month ( ) ;
4897+
4898+ queue ! (
4899+ stdout,
4900+ MoveTo ( x, y_start) ,
4901+ SetAttribute ( Attribute :: Bold ) ,
4902+ Print ( format!( "{}" , today. format( "%B %Y" ) ) ) ,
4903+ SetAttribute ( Attribute :: Reset )
4904+ ) ?;
4905+
4906+ let weekdays = [ "Mo" , "Tu" , "We" , "Th" , "Fr" , "Sa" , "Su" ] ;
4907+ let cell_w = 4usize ;
4908+ let cal_width = cell_w * 7 ;
4909+ let cal_x = x;
4910+
4911+ queue ! (
4912+ stdout,
4913+ MoveTo ( cal_x, y_start + 1 ) ,
4914+ SetForegroundColor ( Color :: DarkGrey )
4915+ ) ?;
4916+ for wd in & weekdays {
4917+ queue ! ( stdout, Print ( format!( "{:>width$}" , wd, width = cell_w) ) ) ?;
4918+ }
4919+ queue ! ( stdout, ResetColor ) ?;
4920+
4921+ let first_of_month = chrono:: NaiveDate :: from_ymd_opt ( year, month, 1 ) . unwrap ( ) ;
4922+ let days_in_month = if month == 12 {
4923+ chrono:: NaiveDate :: from_ymd_opt ( year + 1 , 1 , 1 )
4924+ } else {
4925+ chrono:: NaiveDate :: from_ymd_opt ( year, month + 1 , 1 )
4926+ }
4927+ . unwrap ( )
4928+ . signed_duration_since ( first_of_month)
4929+ . num_days ( ) as u32 ;
4930+ let start_weekday = first_of_month. weekday ( ) . num_days_from_monday ( ) as usize ;
4931+
4932+ let mut row = 0usize ;
4933+ let mut col = start_weekday;
4934+ for day in 1 ..=days_in_month {
4935+ let date = chrono:: NaiveDate :: from_ymd_opt ( year, month, day) . unwrap ( ) ;
4936+ let y_row = y_start + 2 + row as u16 ;
4937+ if y_row >= y_start + available_rows as u16 {
4938+ break ;
4939+ }
4940+ queue ! ( stdout, MoveTo ( cal_x + ( col * cell_w) as u16 , y_row) ) ?;
4941+ let has_task = app. tasks . iter ( ) . any ( |t| t. due_date == Some ( date) ) ;
4942+ if date == today {
4943+ queue ! (
4944+ stdout,
4945+ SetForegroundColor ( Color :: Black ) ,
4946+ SetBackgroundColor ( Color :: White ) ,
4947+ Print ( format!( "{:>width$}" , day, width = cell_w) ) ,
4948+ ResetColor
4949+ ) ?;
4950+ } else if has_task {
4951+ queue ! (
4952+ stdout,
4953+ SetForegroundColor ( Color :: Cyan ) ,
4954+ Print ( format!( "{:>width$}" , day, width = cell_w) ) ,
4955+ ResetColor
4956+ ) ?;
4957+ } else {
4958+ queue ! ( stdout, Print ( format!( "{:>width$}" , day, width = cell_w) ) ) ?;
4959+ }
4960+ col += 1 ;
4961+ if col >= 7 {
4962+ col = 0 ;
4963+ row += 1 ;
4964+ }
4965+ }
4966+
4967+ let events_x = ( cal_width + 4 ) as u16 + cal_x;
4968+ let events_width = content_width. saturating_sub ( cal_width + 4 ) ;
4969+ if events_width > 10 {
4970+ queue ! (
4971+ stdout,
4972+ MoveTo ( events_x, y_start) ,
4973+ SetAttribute ( Attribute :: Bold ) ,
4974+ Print ( clamp_text( "Tasks with due dates" , events_width) ) ,
4975+ SetAttribute ( Attribute :: Reset )
4976+ ) ?;
4977+
4978+ let mut tasks_with_dates: Vec < & model:: Task > =
4979+ app. tasks . iter ( ) . filter ( |t| t. due_date . is_some ( ) ) . collect ( ) ;
4980+ 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+ }
5015+ if tasks_with_dates. is_empty ( ) {
5016+ queue ! (
5017+ stdout,
5018+ MoveTo ( events_x, y_start + 1 ) ,
5019+ SetForegroundColor ( Color :: DarkGrey ) ,
5020+ Print ( clamp_text( "No tasks with due dates" , events_width) ) ,
5021+ ResetColor
5022+ ) ?;
5023+ }
5024+ }
5025+
5026+ queue ! (
5027+ stdout,
5028+ MoveTo ( x, rows. saturating_sub( 5 ) ) ,
5029+ SetForegroundColor ( Color :: DarkGrey ) ,
5030+ Print ( clamp_text(
5031+ " Calendar view \u{2022} tasks with due dates highlighted in cyan" ,
5032+ content_width,
5033+ ) ) ,
5034+ ResetColor
5035+ ) ?;
5036+ Ok ( ( ) )
5037+ }
5038+
48735039fn render_checklist_tab ( stdout : & mut Stdout , app : & App , cols : u16 , rows : u16 ) -> io:: Result < ( ) > {
48745040 let width = cols as usize ;
48755041 let num_buckets = app. settings . buckets . len ( ) . max ( 1 ) ;
0 commit comments