diff --git a/README.md b/README.md index 2b1da34..e16b8af 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # vikunja-tui +[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-%23FE5196?logo=conventionalcommits&logoColor=white)](https://conventionalcommits.org) + This is a simple terminal user interface for [vikunja](https://vikunja.io). The purpose is to allow users to manage tasks from the terminal, using their own API key. This project is not managed or affiliated with the Vikunja team. ## Setup diff --git a/src/api.rs b/src/api.rs index 42b9795..1eab872 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,4 +1,4 @@ -use crate::models::{Label, Task, TaskDetail}; +use crate::models::{Task, TaskDetail}; use reqwest::Client; use std::collections::HashMap; diff --git a/src/app.rs b/src/app.rs index c470bdb..efa4bc3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -11,6 +11,7 @@ pub struct App { pub input_mode: InputMode, pub new_task_title: String, pub page: usize, + pub show_done_tasks: bool, } pub enum InputMode { @@ -33,9 +34,25 @@ impl App { input_mode: InputMode::Normal, new_task_title: String::new(), page: 1, + show_done_tasks: false, } } + pub async fn refresh_tasks( + &mut self, + instance_url: &str, + api_key: &str, + ) -> Result<(), Box> { + let new_tasks = fetch_tasks(instance_url, api_key, self.page).await?; + if self.show_done_tasks { + self.tasks = new_tasks; + } else { + self.tasks = new_tasks.into_iter().filter(|task| !task.done).collect(); + } + self.state.select(Some(0)); + Ok(()) + } + pub fn next_page(&mut self) { self.page += 1; } @@ -101,17 +118,21 @@ impl App { KeyCode::Char('n') => { // Next page self.next_page(); - if let Ok(new_tasks) = fetch_tasks(instance_url, api_key, self.page).await { - self.tasks = new_tasks.into_iter().filter(|task| !task.done).collect(); - self.state.select(Some(0)); + if let Err(err) = self.refresh_tasks(instance_url, api_key).await { + eprintln!("Error fetching tasks: {}", err); } } KeyCode::Char('p') => { // Previous page self.previous_page(); - if let Ok(new_tasks) = fetch_tasks(instance_url, api_key, self.page).await { - self.tasks = new_tasks.into_iter().filter(|task| !task.done).collect(); - self.state.select(Some(0)); + if let Err(err) = self.refresh_tasks(instance_url, api_key).await { + eprintln!("Error fetching tasks: {}", err); + } + } + KeyCode::Char('t') => { + self.show_done_tasks = !self.show_done_tasks; + if let Err(err) = self.refresh_tasks(instance_url, api_key).await { + eprintln!("Error fetching tasks: {}", err); } } KeyCode::Char('a') => { diff --git a/src/main.rs b/src/main.rs index f3e8eba..9f79278 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,8 @@ mod app; mod models; mod ui; +use crate::api::fetch_tasks; + use app::App; use crossterm::{ execute, @@ -47,10 +49,14 @@ async fn main() -> Result<(), Box> { let instance_url = config.vikunja.instance_url; let api_key = config.vikunja.api_key; - let all_tasks = api::fetch_tasks(&instance_url, &api_key, 1).await?; + let show_done_tasks = false; - let incomplete_tasks: Vec = - all_tasks.into_iter().filter(|task| !task.done).collect(); + let tasks = fetch_tasks(&instance_url, &api_key, 1).await?; + let tasks = if show_done_tasks { + tasks + } else { + tasks.into_iter().filter(|task| !task.done).collect() + }; enable_raw_mode()?; let mut stdout = io::stdout(); @@ -60,7 +66,7 @@ async fn main() -> Result<(), Box> { terminal.hide_cursor()?; - let app = App::new(incomplete_tasks); + let app = App::new(tasks); let res = run_app(&mut terminal, app, &instance_url, &api_key).await; diff --git a/src/models.rs b/src/models.rs index 8b85d12..c9f242d 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,7 +1,7 @@ use serde::Deserialize; // Task struct -#[derive(Deserialize, Debug)] +#[derive(Clone, Deserialize, Debug)] pub struct Task { pub id: u64, pub title: String, diff --git a/src/ui.rs b/src/ui.rs index 046d516..d6137a5 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,7 +1,6 @@ use crate::app::{App, InputMode}; -use crate::models::{Label, Task}; use ansi_parser::{AnsiParser, Output}; -use crossterm::event::{self, Event as CEvent, KeyCode}; +use crossterm::event::{self, Event as CEvent}; use ratatui::{ backend::Backend, layout::{Alignment, Constraint, Direction, Layout, Rect}, @@ -70,6 +69,8 @@ fn get_legend(input_mode: &InputMode) -> Text<'static> { Span::raw(": Next Page "), Span::styled(" p ", Style::default().fg(Color::Red)), Span::raw(": Previous Page "), + Span::styled(" t ", Style::default().fg(Color::Red)), + Span::raw(": Toggle Done "), Span::styled(" Enter ", Style::default().fg(Color::Red)), Span::raw(": View Details "), Span::styled(" a ", Style::default().fg(Color::Red)), @@ -119,16 +120,32 @@ pub async fn run_app( ) .split(body_chunk); + let task_title = if app.show_done_tasks { + "Tasks (All)" + } else { + "Tasks (Undone)" + }; + // Left panel: Task list let tasks_widget = if !app.tasks.is_empty() { let tasks: Vec = app .tasks .iter() - .map(|task| ListItem::new(Line::from(task.title.clone()))) + .map(|task| { + let content = if task.done { + vec![ + Span::styled("DONE ", Style::default().fg(Color::Green)), + Span::raw(&task.title), + ] + } else { + vec![Span::raw(&task.title)] + }; + ListItem::new(Line::from(content)) + }) .collect(); List::new(tasks) - .block(Block::default().borders(Borders::ALL).title("Tasks")) + .block(Block::default().borders(Borders::ALL).title(task_title)) .highlight_style( Style::default() .fg(Color::Green) @@ -136,8 +153,8 @@ pub async fn run_app( ) .highlight_symbol(">> ") } else { - List::new(vec![ListItem::new("No incomplete tasks available")]) - .block(Block::default().borders(Borders::ALL).title("Tasks")) + List::new(vec![ListItem::new("No tasks available")]) + .block(Block::default().borders(Borders::ALL).title(task_title)) }; f.render_stateful_widget(tasks_widget, chunks[0], &mut app.state);