Skip to content

Commit 5ca456f

Browse files
author
ComputelessComputer
committed
add calendar tab with month grid and task due date view
1 parent 4afaced commit 5ca456f

1 file changed

Lines changed: 182 additions & 16 deletions

File tree

src/main.rs

Lines changed: 182 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use std::io::{self, Stdout, Write};
99
use std::sync::mpsc;
1010
use std::time::{Duration, Instant};
1111

12-
use chrono::Utc;
12+
use chrono::{Datelike, Utc};
1313
use crossterm::{
1414
cursor::{Hide, MoveTo, Show},
1515
event::{
@@ -32,13 +32,13 @@ use crate::storage::{AiSettings, Storage};
3232
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3333
enum Tab {
3434
Checklist,
35+
Calendar,
3536
Default,
3637
Timeline,
3738
Kanban,
3839
Settings,
3940
Suggestions,
4041
}
41-
4242
const 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-
5352
impl 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+
48735039
fn 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

Comments
 (0)