/ src / main.rs
main.rs
  1  use dtasks::{DurationsConf, Task, TimerEnds, hms_to_seconds, seconds_to_hms};
  2  use std::collections::HashMap;
  3  use std::io::Write;
  4  use std::io::{self, Read, stdout};
  5  use std::sync::mpsc::{self, Receiver};
  6  use std::thread;
  7  use std::time::Duration;
  8  
  9  // NOTE:
 10  // Guideline: Follow the Chapter 12 rustbook development procedure.
 11  // After rebuild on a crates base to increase Code Quality making it
 12  // more scalable and maintainable. And last, do some implementations
 13  // for learning concepts such as WASM, libp2p and concurrency.
 14  
 15  // TODO:
 16  // - [ ] Reorganize modules of lib crate
 17  // - [ ] Refactor and cleanup Tasks
 18  // - [ ] create logic for export and import trasks to filesystem
 19  // - [ ] allow numbers to add and remove tasks and select task to work on
 20  // - [ ] fix panics with error handling
 21  // - [ ] Add functions for the menu
 22  // - [ ] Add implementation for update tasks struct data.
 23  // work_on_task():
 24  // - [ ] refactor to manage breaks properly
 25  // - [ ] Add section if breaks are required, create as tasks::new_break and the logic
 26  // list_tasks():
 27  // - [ ] Add display trait for tasks
 28  // - [ ] Do with closures if better
 29  //
 30  // HACK: Future Development
 31  // * Use clap to work as autocomplete terminal (VIM Bindings Optional)
 32  // * Implement a format trait or Substitute time std with chrono
 33  // * WASM support and a web interface
 34  // * libp2p integration for handling it as a DAPP
 35  
 36  fn main() {
 37      let mut tasks = HashMap::new();
 38      tasks.insert("test".to_string(), Task::new());
 39  
 40      let mut input = String::new();
 41  
 42      print_tasks_menu();
 43  
 44      loop {
 45          print!("main> ");
 46          stdout().flush().expect("could not clean");
 47          input.clear();
 48  
 49          io::stdin()
 50              .read_line(&mut input)
 51              .expect("Failed to read line");
 52          match input.as_str() {
 53              "a\n" => add_task(&mut tasks),
 54              "r\n" => rm_task(&mut tasks),
 55              "l\n" => list_tasks(&tasks),
 56              "w\n" => work_on_task(&mut tasks),
 57              "?\n" => print_tasks_menu(),
 58              "q\n" => break,
 59              _ => {
 60                  print!("{esc}[2J{esc}[1;1H", esc = 27 as char);
 61                  println!("Unkown Input");
 62                  println!("press ? for help");
 63                  continue;
 64              }
 65          }
 66      }
 67  }
 68  
 69  fn print_tasks_menu() {
 70      print!("{esc}[2J{esc}[1;1H", esc = 27 as char);
 71      println!("Select one of the following options");
 72      println!("a: adds/edits a task");
 73      println!("r: removes a task");
 74      println!("l: lists the tasks");
 75      println!("w: work on a task");
 76      println!("q: quit the program");
 77      println!("?: for help (this menu)");
 78      println!("");
 79  }
 80  fn print_task_menu() {
 81      print!("{esc}[2J{esc}[1;1H", esc = 27 as char);
 82      println!("input one of the following options");
 83      println!("d: display the current timer");
 84      println!("l: loop display the current timer");
 85      println!("s: start,stop,resume the timer");
 86      println!("n: next timer");
 87      println!("p: previous timer");
 88      println!("q: quit this task");
 89      println!("?: for help (this menu)");
 90      println!("");
 91  }
 92  
 93  fn add_task(tasks: &mut HashMap<String, Task>) {
 94      print!("{esc}[2J{esc}[1;1H", esc = 27 as char);
 95      let mut task_name = String::new();
 96      let mut num_pomodoros = String::new();
 97  
 98      // some variables for custom pomodoros
 99      let mut duration = DurationsConf::new();
100      let mut opt = String::new();
101      let mut pomodoro: u64 = 25;
102      let mut short_break: u64 = 5;
103      let mut long_break: u64 = 15;
104  
105      println!("Input the task name:");
106      io::stdin()
107          .read_line(&mut task_name)
108          .expect("Failed to read line");
109      task_name = task_name.trim().to_string();
110  
111      println!("Input how many pomodoros:");
112      io::stdin()
113          .read_line(&mut num_pomodoros)
114          .expect("Failed to read line");
115      let num_pomodoros: usize = match num_pomodoros.trim().parse() {
116          Ok(num) => num,
117          Err(_) => 1, // 1 pomo as default if error
118      };
119  
120      println!("Set custom pomodoro timer? [y/n]:");
121  
122      io::stdin()
123          .read_line(&mut opt)
124          .expect("Failed to read line");
125      match opt.as_str() {
126          "y\n" => {
127              println!("input minutes for pomodoro:");
128              stdout().flush().expect("could not clean");
129              opt.clear();
130              io::stdin()
131                  .read_line(&mut opt)
132                  .expect("Failed to read line");
133              pomodoro = match opt.trim().parse() {
134                  Ok(num) => num,
135                  Err(_) => 25, // 1 pomo as default if error
136              };
137              println!("input minutes for short break:");
138              stdout().flush().expect("could not clean");
139              opt.clear();
140              io::stdin()
141                  .read_line(&mut opt)
142                  .expect("Failed to read line");
143              short_break = match opt.trim().parse() {
144                  Ok(num) => num,
145                  Err(_) => 5, // 1 pomo as default if error
146              };
147              if num_pomodoros > 1 {
148                  println!("input minutes for long break:");
149                  stdout().flush().expect("could not clean");
150                  opt.clear();
151                  io::stdin()
152                      .read_line(&mut opt)
153                      .expect("Failed to read line");
154                  long_break = match opt.trim().parse() {
155                      Ok(num) => num,
156                      Err(_) => 15, // 1 pomo as default if error
157                  };
158                  duration.update_vals(
159                      num_pomodoros,
160                      hms_to_seconds(0, pomodoro, 0),
161                      hms_to_seconds(0, short_break, 0),
162                      hms_to_seconds(0, long_break, 0),
163                  );
164              } else {
165                  // No long break neded
166                  long_break = 0;
167                  duration.update_vals(
168                      num_pomodoros,
169                      hms_to_seconds(0, pomodoro, 0),
170                      hms_to_seconds(0, short_break, 0),
171                      hms_to_seconds(0, long_break, 0),
172                  );
173              }
174          }
175          _ => {
176              println!("pressed other, so settiing pomodoro to default time");
177              duration.update_vals(
178                  num_pomodoros,
179                  hms_to_seconds(0, pomodoro, 0),
180                  hms_to_seconds(0, short_break, 0),
181                  hms_to_seconds(0, long_break, 0),
182              );
183              println!("press ? for help");
184          }
185      }
186  
187      // ISSUE: Check for entry.or_insert for error handling
188      tasks.insert(task_name.to_string(), Task::build(0, duration));
189  
190      list_tasks(tasks);
191  }
192  
193  fn rm_task(tasks: &mut HashMap<String, Task>) {
194      list_tasks(tasks);
195      let mut task_name = String::new();
196  
197      println!("Input the task name to remove it:");
198      io::stdin()
199          .read_line(&mut task_name)
200          .expect("Failed to read line");
201      task_name = task_name.trim().to_string();
202  
203      // TODO: Simplify
204      if let Some(tsk) = tasks.remove(&mut task_name) {
205          list_tasks(tasks);
206          match tsk.get_pomodoro_ends_as_secs() {
207              Ok(secs) => {
208                  let (hh, mm, ss) = seconds_to_hms(secs);
209                  println!(
210                      "{task_name}: Duration: {hh}:{mm}:{ss} ToDo: {} Done:{} REMOVED",
211                      tsk.get_pomodoro_todo(),
212                      tsk.get_pomodoro_done()
213                  );
214              }
215              Err(e) => {
216                  println!(
217                      "{task_name}: Duration: {e}  ToDo: {} Done:{} REMOVED ",
218                      tsk.get_pomodoro_todo(),
219                      tsk.get_pomodoro_done()
220                  );
221              }
222          }
223      } else {
224          //Do error handling
225          println!("there is not such task");
226      }
227  }
228  
229  fn list_tasks(tasks: &HashMap<String, Task>) {
230      print!("{esc}[2J{esc}[1;1H", esc = 27 as char);
231      if tasks.len() > 0 {
232          println!("Tasks:");
233          for task in tasks.keys() {
234              if let Some(tsk) = tasks.get(task) {
235                  match tsk.get_pomodoro_ends_as_secs() {
236                      Ok(secs) => {
237                          let (hh, mm, ss) = seconds_to_hms(secs);
238                          println!(
239                              "{task}: Duration: {hh}:{mm}:{ss} ToDo: {} Done:{}",
240                              tsk.get_pomodoro_todo(),
241                              tsk.get_pomodoro_done()
242                          );
243                      }
244                      Err(e) => {
245                          println!(
246                              "{task}: Duration: {e}  ToDo: {} Done:{}",
247                              tsk.get_pomodoro_todo(),
248                              tsk.get_pomodoro_done()
249                          );
250                      }
251                  }
252              }
253          }
254      } else {
255          println!("Not added tasks, press a for adding one!");
256      }
257      println!("");
258  }
259  
260  fn work_on_task(tasks: &mut HashMap<String, Task>) {
261      list_tasks(tasks);
262      print!("Input the task name to work on: ");
263      let mut task_name = String::new();
264      // ToDO handle input errors
265      print!("> ");
266      stdout().flush().expect("could not clean");
267      task_name.clear();
268  
269      io::stdin()
270          .read_line(&mut task_name)
271          .expect("Failed to read line");
272      task_name = task_name.trim().to_string();
273  
274      if let Some(task) = tasks.get_mut(&task_name) {
275          if task.get_pomodoro_todo() > 0 {
276              print_task_menu();
277              loop {
278                  let mut input = String::new();
279                  print!("working on {task_name}> ");
280                  stdout().flush().expect("could not clean");
281                  input.clear();
282  
283                  io::stdin()
284                      .read_line(&mut input)
285                      .expect("Failed to read line");
286  
287                  match input.as_str() {
288                      "d\n" => {
289                          print!("{esc}[2J{esc}[1;1H", esc = 27 as char);
290                          print_timer(&task_name, task);
291                      }
292                      "l\n" => {
293                          let input_rx = spawn_input_listener();
294                          loop {
295                              print!("{esc}[2J{esc}[1;1H", esc = 27 as char);
296                              print_timer(&task_name, task);
297                              println!("Press any key (then Enter) to stop the display loop.");
298                              // Check if we received the interrupt signal.
299                              // `try_recv` is non‑blocking; it returns immediately.
300                              if let Ok(_) = input_rx.try_recv() {
301                                  println!("Got a key press → stopping pomodoro display");
302                                  break;
303                              }
304                              // Avoid a busy‑spin – sleep a little.
305                              thread::sleep(Duration::from_millis(200));
306                              // if task is ended no need to keep the display loop
307                              if task.is_ended() {
308                                  println!("Current timer is ended, loop display stopped.");
309                                  break;
310                              }
311                          }
312                      }
313                      "s\n" => {
314                          print!("{esc}[2J{esc}[1;1H", esc = 27 as char);
315                          let _ = task.start_pause_pomodoro();
316                          print_timer(&task_name, task);
317                      }
318                      "n\n" => {
319                          print!("{esc}[2J{esc}[1;1H", esc = 27 as char);
320                          task.next_pomodoro();
321                          print_timer(&task_name, task);
322                      }
323                      "p\n" => {
324                          print!("{esc}[2J{esc}[1;1H", esc = 27 as char);
325                          task.previous_pomodoro();
326                          print_timer(&task_name, task);
327                      }
328                      "?\n" => print_task_menu(),
329                      "q\n" => break,
330                      _ => {
331                          print!("{esc}[2J{esc}[1;1H", esc = 27 as char);
332                          println!("Unkown Input");
333                          println!("press ? for help");
334                          continue;
335                      }
336                  }
337              }
338          } else {
339              print!("{esc}[2J{esc}[1;1H", esc = 27 as char);
340              println!("the current task has no pomodoros todo")
341          }
342      } else {
343          print!("{esc}[2J{esc}[1;1H", esc = 27 as char);
344          println!("There is not a task named: {task_name}")
345      }
346  }
347  
348  pub fn print_timer(task_name: &str, task: &mut Task) {
349      let (timer_state, timer_time, paused_time, idx, timer_type) = task.get_pomodoro_data();
350      let timer_type = match timer_type {
351          TimerEnds::Pomodoro(_) => "Pomorodo",
352          TimerEnds::ShortBreak(_) => "Short Break",
353          TimerEnds::LongBreak(_) => "Long Break",
354      };
355      print!("{esc}[2J{esc}[1;1H", esc = 27 as char);
356      println!("\t----- {} -----", task_name);
357      println!("Timer: {}", timer_type);
358      println!("State: {}", timer_state);
359      let (mut hh, mut mm, mut ss) = seconds_to_hms(timer_time);
360      // TODO: set this as a Display trait of type hh_mm_ss and verify conditional logic
361      if hh == 0 {
362          println!("Time to finish: {mm}:{ss}");
363      } else {
364          println!("Time to finish: {hh}:{mm}:{ss}");
365      }
366      (hh, mm, ss) = seconds_to_hms(paused_time);
367      if hh == 0 {
368          println!("Paused Time: {mm}:{ss}");
369      } else {
370          println!("Paused Time: {hh}:{mm}:{ss}");
371      }
372      println!("Cycle: {}/{}", idx + 1, task.get_task_cycle().len());
373      // TODO: better information of working tasks and timer_type
374      // perhaps add into task menu?
375      // match timer_type {
376      //    "Pomodoro" => println!("Doing:{}",),
377      //    "Short Break" => println!()
378      //}
379  }
380  
381  /// Starts a background thread that reads *one* byte from stdin and
382  /// sends a message on the channel when something is typed.
383  ///
384  /// The channel is unbounded – we only care about the *first* key press.
385  fn spawn_input_listener() -> Receiver<()> {
386      let (tx, rx) = mpsc::channel::<()>();
387  
388      thread::spawn(move || {
389          // `stdin()` gives us a handle that is buffered by default.
390          // We want to read a single byte, so we lock it and read directly.
391          let stdin = io::stdin();
392          let mut handle = stdin.lock();
393  
394          // This will block until the user presses a key and hits <Enter>.
395          // `read_exact` guarantees we get exactly one byte.
396          let mut buf = [0u8; 1];
397          let _ = handle.read_exact(&mut buf);
398  
399          // Send the signal. If the receiver has dropped (e.g. main finished)
400          // we just ignore the error.
401          let _ = tx.send(());
402      });
403  
404      rx
405  }
406  
407  // Structural Changes ToDo and Ideas:
408  
409  //enum TaskOption {
410  //    Start,
411  //    Stop,
412  //    Pause(Task),
413  //    Update(Task),
414  //}
415  
416  //struct Tasks(HashMap<String, Task>);
417  //
418  //impl Tasks {
419  //    fn Add(mut self, Task) {
420  //    }
421  //    fn Remove(mut self, Task) {
422  //    }
423  //    fn List(&self) {
424  //    }
425  //}