Skip to content

Commit

Permalink
Merge pull request #6 from mark-pitblado/develop
Browse files Browse the repository at this point in the history
Change description to be entered through a seperate text box
  • Loading branch information
mark-pitblado authored Nov 11, 2024
2 parents 8ee19ca + a4b568a commit 3b8f232
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 157 deletions.
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
name = "vikunja-tui"
version = "0.1.0"
edition = "2021"
license = "MIT"
description = "A terminal application to manage tasks in vikunja"
repository = "https://github.com/mark-pitblado/vikunja-tui"
readme = "README.md"

[dependencies]
regex = "1"
Expand Down
8 changes: 8 additions & 0 deletions LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Copyright 2024 Mark Pitblado

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ api_key = "<your-key-here>"
- Add tasks
- Title
- Priority, via ![1-5] (vikunja quick add magic syntax)
- Description, via {} in the add task flow
- Description, via a seperate input box during task creation

## Roadmap

Expand Down
105 changes: 71 additions & 34 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,22 @@ pub struct App {
pub state: ListState,
pub task_detail: Option<TaskDetail>,
pub input_mode: InputMode,
pub active_input: ActiveInput,
pub new_task_title: String,
pub new_task_description: String,
pub page: usize,
pub show_done_tasks: bool,
}

pub enum InputMode {
Normal,
Editing,
Insert,
}
#[derive(PartialEq)]
pub enum ActiveInput {
Title,
Description,
}

impl App {
Expand All @@ -33,7 +41,9 @@ impl App {
state,
task_detail: None,
input_mode: InputMode::Normal,
active_input: ActiveInput::Title,
new_task_title: String::new(),
new_task_description: String::new(),
page: 1,
show_done_tasks: false,
}
Expand Down Expand Up @@ -139,6 +149,8 @@ impl App {
KeyCode::Char('a') => {
self.input_mode = InputMode::Editing;
self.new_task_title.clear();
self.new_task_description.clear();
self.active_input = ActiveInput::Title;
}
KeyCode::Enter => {
if let Err(err) = self.select_task(instance_url, api_key).await {
Expand All @@ -147,47 +159,72 @@ impl App {
}
_ => {}
},
InputMode::Editing => {
match key.code {
KeyCode::Enter => {
if !self.new_task_title.trim().is_empty() {
let parsed_task = parse_task_input(&self.new_task_title);

if let Err(err) = create_new_task(
instance_url,
api_key,
&parsed_task.title,
parsed_task.description.as_deref(),
parsed_task.priority,
)
.await
{
eprintln!("Error creating task: {}", err);
} else {
// Refresh the task list
if let Ok(all_tasks) =
fetch_tasks(instance_url, api_key, self.page).await
{
self.tasks =
all_tasks.into_iter().filter(|task| !task.done).collect();
self.state.select(Some(0));
}
}
InputMode::Editing => match key.code {
KeyCode::Char('i') => {
self.input_mode = InputMode::Insert;
}
KeyCode::Tab => {
self.active_input = match self.active_input {
ActiveInput::Title => ActiveInput::Description,
ActiveInput::Description => ActiveInput::Title,
};
}
KeyCode::Enter => {
if self.new_task_title.trim().is_empty() {
eprintln!("Task title cannot be empty.");
} else {
let parsed_task = parse_task_input(&self.new_task_title);

let description = if self.new_task_description.trim().is_empty() {
None
} else {
Some(self.new_task_description.as_str())
};

if let Err(err) = create_new_task(
instance_url,
api_key,
&parsed_task.title,
description,
parsed_task.priority,
)
.await
{
eprintln!("Error creating new task: {}", err);
} else if let Err(err) = self.refresh_tasks(instance_url, api_key).await {
eprintln!("Error fetching tasks: {}", err);
}
self.new_task_title.clear();
self.new_task_description.clear();
self.input_mode = InputMode::Normal;
}
KeyCode::Char(c) => {
self.new_task_title.push(c); // Handle character input
}
KeyCode::Backspace => {
self.new_task_title.pop(); // Handle backspace
}
KeyCode::Esc => {
self.new_task_title.clear();
self.new_task_description.clear();
self.input_mode = InputMode::Normal;
}
_ => {}
},
InputMode::Insert => match key.code {
KeyCode::Char(c) => match self.active_input {
ActiveInput::Title => self.new_task_title.push(c),
ActiveInput::Description => self.new_task_description.push(c),
},
KeyCode::Backspace => match self.active_input {
ActiveInput::Title => {
self.new_task_title.pop();
}
KeyCode::Esc => {
self.input_mode = InputMode::Normal; // Exit editing mode
ActiveInput::Description => {
self.new_task_description.pop();
}
_ => {}
},
KeyCode::Esc => {
self.input_mode = InputMode::Editing;
}
}
_ => {}
},
}
Ok(false)
}
Expand Down
4 changes: 0 additions & 4 deletions src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@ pub struct Task {
// TaskDetail struct with description
#[derive(Deserialize, Debug)]
pub struct TaskDetail {
pub id: u64,
pub title: String,
pub done: bool,
pub due_date: Option<String>,
pub labels: Option<Vec<Label>>,
pub priority: Option<i32>,
Expand All @@ -23,6 +20,5 @@ pub struct TaskDetail {
// Label struct
#[derive(Deserialize, Debug)]
pub struct Label {
pub id: u64,
pub title: String,
}
76 changes: 2 additions & 74 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,13 @@ use regex::Regex;
#[derive(Debug, PartialEq)]
pub struct ParsedTask {
pub title: String,
pub description: Option<String>,
pub priority: Option<u8>,
}

pub fn parse_task_input(input: &str) -> ParsedTask {
let priority_re = Regex::new(r"!(\d+)\s*").unwrap();
let description_re = Regex::new(r"\{([^}]*)\}").unwrap();

let mut priority = None;
let mut description = None;

// Priority
for caps in priority_re.captures_iter(input) {
Expand All @@ -25,90 +22,27 @@ pub fn parse_task_input(input: &str) -> ParsedTask {
}
}

// Description
let mut title = input.to_string();
if let Some(caps) = description_re.captures(&title) {
if let Some(desc_match) = caps.get(1) {
description = Some(desc_match.as_str().trim().to_string());
}
}

title = description_re.replace_all(&title, "").to_string();

title = priority_re.replace_all(&title, "").to_string();
let title = priority_re.replace_all(input, "").into_owned();

let title = Regex::new(r"\s+")
.unwrap()
.replace_all(&title, " ")
.trim()
.to_string();

ParsedTask {
title,
priority,
description,
}
ParsedTask { title, priority }
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_parse_with_description() {
let input = "Implement feature X {This is the description of the task}";
let expected = ParsedTask {
title: "Implement feature X".to_string(),
description: Some("This is the description of the task".to_string()),
priority: None,
};
let result = parse_task_input(input);
assert_eq!(result, expected);
}

#[test]
fn test_parse_with_description_and_priority() {
let input = "Fix bug in module !2 {Critical issue that needs immediate attention}";
let expected = ParsedTask {
title: "Fix bug in module".to_string(),
description: Some("Critical issue that needs immediate attention".to_string()),
priority: Some(2),
};
let result = parse_task_input(input);
assert_eq!(result, expected);
}

#[test]
fn test_parse_with_description_and_priority_in_any_order() {
let input = "{Detailed description here} Update documentation !3";
let expected = ParsedTask {
title: "Update documentation".to_string(),
description: Some("Detailed description here".to_string()),
priority: Some(3),
};
let result = parse_task_input(input);
assert_eq!(result, expected);
}

#[test]
fn test_parse_with_multiple_descriptions() {
let input = "Task title {First description} {Second description}";
let expected = ParsedTask {
title: "Task title".to_string(),
description: Some("First description".to_string()),
priority: None,
};
let result = parse_task_input(input);
assert_eq!(result, expected);
}

#[test]
fn test_parse_with_priority_in_middle() {
let input = "Update !4 software documentation";
let expected = ParsedTask {
title: "Update software documentation".to_string(),
priority: Some(4),
description: None,
};
let result = parse_task_input(input);
assert_eq!(result, expected);
Expand All @@ -120,7 +54,6 @@ mod tests {
let expected = ParsedTask {
title: "Fix bugs in the code".to_string(),
priority: Some(2),
description: None,
};
let result = parse_task_input(input);
assert_eq!(result, expected);
Expand All @@ -132,7 +65,6 @@ mod tests {
let expected = ParsedTask {
title: "Write tests for the parser".to_string(),
priority: Some(3),
description: None,
};
let result = parse_task_input(input);
assert_eq!(result, expected);
Expand All @@ -144,7 +76,6 @@ mod tests {
let expected = ParsedTask {
title: "Deploy to production".to_string(),
priority: Some(5),
description: None,
};
let result = parse_task_input(input);
assert_eq!(result, expected);
Expand All @@ -156,7 +87,6 @@ mod tests {
let expected = ParsedTask {
title: "Prepare presentation slides".to_string(),
priority: Some(2),
description: None,
};
let result = parse_task_input(input);
assert_eq!(result, expected);
Expand All @@ -168,7 +98,6 @@ mod tests {
let expected = ParsedTask {
title: "Organize team building event".to_string(),
priority: Some(1),
description: None,
};
let result = parse_task_input(input);
assert_eq!(result, expected);
Expand All @@ -180,7 +109,6 @@ mod tests {
let expected = ParsedTask {
title: "Check logs immediately".to_string(),
priority: None,
description: None,
};
let result = parse_task_input(input);
assert_eq!(result, expected);
Expand Down
Loading

0 comments on commit 3b8f232

Please sign in to comment.