Skip to content

Commit 4ce8f9e

Browse files
authored
Add todo list with pomodoro time tracking (#3)
1 parent 4b9bfa3 commit 4ce8f9e

7 files changed

Lines changed: 535 additions & 30 deletions

File tree

Cargo.lock

Lines changed: 84 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,6 @@ rodio = { version = "0.21.1", features = ["symphonia-mp3", "symphonia-aac"] }
2222
stream-download = { version = "0.23.0", features = ["reqwest-native-tls"] }
2323
tokio = { version = "1", features = ["rt-multi-thread"] }
2424
libc = "0.2"
25+
serde = { version = "1.0", features = ["derive"] }
26+
serde_json = "1.0"
27+
dirs = "6.0"

src/lib.rs

Lines changed: 90 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,14 @@ use ratatui::{
2020
use crate::pomodoro::{Mode, Pomodoro};
2121
use crate::radio::Radio;
2222
use crate::theme::Theme;
23+
use crate::todo::TodoList;
2324
use crate::ui::logo::{self, LOGO_HEIGHT, LOGO_WIDTH};
2425

2526
mod pomodoro;
2627
mod radio;
28+
mod storage;
2729
mod theme;
30+
mod todo;
2831
mod ui;
2932

3033
pub fn run() -> Result<()> {
@@ -51,6 +54,7 @@ fn run_app(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
5154
let mut frame: u64 = 0;
5255
let mut pomo = Pomodoro::new();
5356
let mut radio = Radio::new();
57+
let mut todos = TodoList::load();
5458
let mut last_second = Instant::now();
5559

5660
loop {
@@ -122,7 +126,11 @@ fn run_app(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
122126
};
123127

124128
let pomo_action = if pomo.running { "Pause" } else { "Continue" };
125-
let radio_action = if radio.is_playing() || radio.is_loading() { "stop" } else { "play" };
129+
let radio_action = if radio.is_playing() || radio.is_loading() {
130+
"stop"
131+
} else {
132+
"play"
133+
};
126134

127135
let help = Paragraph::new(Line::from(vec![
128136
Span::styled("q ", Theme::accent()),
@@ -131,18 +139,27 @@ fn run_app(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
131139
Span::styled(format!("{} ", radio_action), Theme::frame()),
132140
Span::styled("←/→ ", Theme::accent()),
133141
Span::styled("station ", Theme::frame()),
142+
Span::styled("t ", Theme::accent()),
143+
Span::styled("todos ", Theme::frame()),
134144
Span::styled("p ", Theme::accent()),
135145
Span::styled("pomo ", Theme::frame()),
136146
Span::styled("space ", Theme::accent()),
137-
Span::styled(format!("{} ", pomo_action), Theme::frame()),
138-
Span::styled("r ", Theme::accent()),
139-
Span::styled("reset", Theme::frame()),
147+
Span::styled(pomo_action, Theme::frame()),
140148
]))
141149
.style(Theme::base());
142150

151+
// Todo list in center area
152+
let todo_area = Rect {
153+
x: area.x + 4,
154+
y: area.y + 3,
155+
width: area.width.saturating_sub(8),
156+
height: area.height.saturating_sub(8),
157+
};
158+
143159
f.render_widget(logo::logo(), logo_area);
144160
f.render_widget(station_line, station_area);
145161
f.render_widget(help, help_area);
162+
todos.draw(f, todo_area);
146163

147164
// Pomodoro on top
148165
if pomo.visible {
@@ -152,7 +169,11 @@ fn run_app(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
152169
Mode::Break => "BREAK",
153170
};
154171
let status = if pomo.running { "▶" } else { "⏸" };
155-
let timer_style = if pomo.running { Theme::accent() } else { Theme::frame() };
172+
let timer_style = if pomo.running {
173+
Theme::accent()
174+
} else {
175+
Theme::frame()
176+
};
156177

157178
let pomo_widget = Paragraph::new(vec![
158179
Line::from(Span::styled(format!("{:02}:{:02}", mm, ss), Theme::hot())),
@@ -176,19 +197,63 @@ fn run_app(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
176197
// input
177198
if event::poll(timeout)? {
178199
if let Event::Key(key) = event::read()? {
179-
match key.code {
180-
KeyCode::Char('q') => {
181-
radio.stop();
182-
return Ok(());
200+
// Todo input mode captures all keys
201+
if todos.input_mode {
202+
match key.code {
203+
KeyCode::Enter => todos.confirm_input(),
204+
KeyCode::Esc => todos.cancel_input(),
205+
KeyCode::Backspace => todos.backspace(),
206+
KeyCode::Char(c) => todos.type_char(c),
207+
_ => {}
208+
}
209+
} else if todos.visible {
210+
// Todo visible - handle todo keys first
211+
match key.code {
212+
KeyCode::Char('q') => {
213+
todos.save();
214+
radio.stop();
215+
return Ok(());
216+
}
217+
KeyCode::Char('t') => todos.toggle_visible(),
218+
KeyCode::Char('n') => todos.enter_input_mode(),
219+
KeyCode::Char('j') => todos.move_down(),
220+
KeyCode::Char('k') => todos.move_up(),
221+
KeyCode::Char('x') => todos.toggle_completed(),
222+
KeyCode::Char('d') => todos.delete_selected(),
223+
KeyCode::Enter => todos.select_for_pomodoro(),
224+
KeyCode::Char('s') => radio.toggle(),
225+
KeyCode::Left => radio.prev_station(),
226+
KeyCode::Right => radio.next_station(),
227+
KeyCode::Char(' ') => {
228+
// Auto-track selected task if none tracked
229+
if todos.active_task.is_none() && !todos.tasks.is_empty() {
230+
todos.select_for_pomodoro();
231+
}
232+
pomo.start_pause();
233+
}
234+
KeyCode::Char('r') => pomo.stop_reset(),
235+
KeyCode::Char('+') => pomo.add_five_minutes(),
236+
KeyCode::Esc => todos.toggle_visible(),
237+
_ => {}
238+
}
239+
} else {
240+
// Normal mode
241+
match key.code {
242+
KeyCode::Char('q') => {
243+
todos.save();
244+
radio.stop();
245+
return Ok(());
246+
}
247+
KeyCode::Char('t') => todos.toggle_visible(),
248+
KeyCode::Char('p') => pomo.toggle_visible(),
249+
KeyCode::Char('s') => radio.toggle(),
250+
KeyCode::Left => radio.prev_station(),
251+
KeyCode::Right => radio.next_station(),
252+
KeyCode::Char(' ') => pomo.start_pause(),
253+
KeyCode::Char('r') => pomo.stop_reset(),
254+
KeyCode::Char('+') => pomo.add_five_minutes(),
255+
_ => {}
183256
}
184-
KeyCode::Char('p') => pomo.toggle_visible(),
185-
KeyCode::Char('s') => radio.toggle(),
186-
KeyCode::Left => radio.prev_station(),
187-
KeyCode::Right => radio.next_station(),
188-
KeyCode::Char(' ') => pomo.start_pause(),
189-
KeyCode::Char('r') => pomo.stop_reset(),
190-
KeyCode::Char('+') => pomo.add_five_minutes(),
191-
_ => {}
192257
}
193258
}
194259
}
@@ -208,6 +273,14 @@ fn run_app(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
208273
radio.set_volume(vol);
209274
}
210275

276+
// Track time on active task
277+
if pomo.running {
278+
if let Some(task_id) = todos.active_task {
279+
todos.add_time(task_id, Duration::from_secs(1));
280+
todos.save_throttled();
281+
}
282+
}
283+
211284
if pomo.tick_1s() {
212285
pomo.play_notification();
213286
// Restore volume after a delay (notification lasts ~1.5s)

src/pomodoro.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ impl Pomodoro {
2626
let break_len = Duration::from_secs(5 * 60);
2727

2828
Self {
29-
visible: false,
29+
visible: true,
3030
running: false,
3131
mode: Mode::Focus,
3232
remaining: focus_len,
@@ -88,7 +88,7 @@ impl Pomodoro {
8888
let Ok(stream) = OutputStreamBuilder::open_default_stream() else {
8989
return;
9090
};
91-
let sink = Sink::connect_new(&stream.mixer());
91+
let sink = Sink::connect_new(stream.mixer());
9292

9393
// Different tones for focus vs break
9494
// Focus starting: lower, calming tone
@@ -106,8 +106,8 @@ impl Pomodoro {
106106
.amplify(0.9);
107107
sink.append(beep);
108108

109-
let silence = rodio::source::Zero::new(1, 44100)
110-
.take_duration(Duration::from_millis(150));
109+
let silence =
110+
rodio::source::Zero::new(1, 44100).take_duration(Duration::from_millis(150));
111111
sink.append(silence);
112112
}
113113

0 commit comments

Comments
 (0)