From 2987f2c8b79cfb17e3f188059d6830bf8a8dbad2 Mon Sep 17 00:00:00 2001 From: Mark Pitblado Date: Wed, 6 Nov 2024 06:43:21 -0800 Subject: [PATCH 1/3] feat: add description parsing --- src/parser.rs | 77 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 2 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index 6fb0628..6935c55 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3,14 +3,18 @@ use regex::Regex; #[derive(Debug, PartialEq)] pub struct ParsedTask { pub title: String, + pub description: Option, pub priority: Option, } 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) { if let Some(priority_match) = caps.get(1) { if let Ok(p) = priority_match.as_str().parse::() { @@ -21,7 +25,17 @@ pub fn parse_task_input(input: &str) -> ParsedTask { } } - let title = priority_re.replace_all(input, ""); + // 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 = Regex::new(r"\s+") .unwrap() @@ -29,19 +43,72 @@ pub fn parse_task_input(input: &str) -> ParsedTask { .trim() .to_string(); - ParsedTask { title, priority } + ParsedTask { + title, + priority, + description, + } } #[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); @@ -53,6 +120,7 @@ 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); @@ -64,6 +132,7 @@ 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); @@ -75,6 +144,7 @@ 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); @@ -86,6 +156,7 @@ 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); @@ -97,6 +168,7 @@ 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); @@ -108,6 +180,7 @@ 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); From a92eaa2286f9c9c847bf6e6e5cd6c165c7a92774 Mon Sep 17 00:00:00 2001 From: Mark Pitblado Date: Wed, 6 Nov 2024 06:49:11 -0800 Subject: [PATCH 2/3] feat: finish description implementation --- src/api.rs | 5 +++++ src/app.rs | 3 +-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/api.rs b/src/api.rs index 711ef29..d501e6a 100644 --- a/src/api.rs +++ b/src/api.rs @@ -49,6 +49,7 @@ pub async fn create_new_task( instance_url: &str, api_key: &str, task_title: &str, + description: Option<&str>, priority: Option, ) -> Result<(), Box> { let client = Client::new(); @@ -58,6 +59,10 @@ pub async fn create_new_task( "title": task_title }); + if let Some(desc) = description { + task_data["description"] = json!(desc); + } + if let Some(priority_value) = priority { task_data["priority"] = json!(priority_value); } diff --git a/src/app.rs b/src/app.rs index 75feda2..9a229e5 100644 --- a/src/app.rs +++ b/src/app.rs @@ -151,14 +151,13 @@ impl App { match key.code { KeyCode::Enter => { if !self.new_task_title.trim().is_empty() { - // Use the parser to extract the task title, priority, and label titles let parsed_task = parse_task_input(&self.new_task_title); - // Create the new task with the parsed title, priority, and labels if let Err(err) = create_new_task( instance_url, api_key, &parsed_task.title, + parsed_task.description.as_deref(), parsed_task.priority, ) .await From 86db899593abca2f3c5d6f8ce2333a6490c465c9 Mon Sep 17 00:00:00 2001 From: Mark Pitblado Date: Wed, 6 Nov 2024 07:11:37 -0800 Subject: [PATCH 3/3] feat: dynamically adjust task entry box height enable the task entry box to automatically expand if the user enters text that should span across lines --- README.md | 1 + src/ui.rs | 47 ++++++++++++++++++++++++++++++++++++----------- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 7037ece..5061140 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ api_key = "" - Add tasks - Title - Priority, via ![1-5] (vikunja quick add magic syntax) + - Description, via {} in the add task flow ## Roadmap diff --git a/src/ui.rs b/src/ui.rs index 2f1c8b7..d362d8e 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -12,14 +12,14 @@ use ratatui::{ use std::io; use std::time::Duration; -fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { +fn centered_rect_absolute(width: u16, height: u16, r: Rect) -> Rect { let popup_layout = Layout::default() .direction(Direction::Vertical) .constraints( [ - Constraint::Percentage((100 - percent_y) / 2), - Constraint::Percentage(percent_y), - Constraint::Percentage((100 - percent_y) / 2), + Constraint::Length((r.height.saturating_sub(height)) / 2), + Constraint::Length(height), + Constraint::Length((r.height.saturating_sub(height) + 1) / 2), ] .as_ref(), ) @@ -29,9 +29,9 @@ fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { .direction(Direction::Horizontal) .constraints( [ - Constraint::Percentage((100 - percent_x) / 2), - Constraint::Percentage(percent_x), - Constraint::Percentage((100 - percent_x) / 2), + Constraint::Length((r.width.saturating_sub(width)) / 2), + Constraint::Length(width), + Constraint::Length((r.width.saturating_sub(width) + 1) / 2), ] .as_ref(), ) @@ -243,16 +243,32 @@ pub async fn run_app( } } InputMode::Editing => { - let popup_area = centered_rect(60, 10, body_chunk); + let popup_width_percentage = 60; + let popup_width = (size.width * popup_width_percentage / 100).saturating_sub(2); + + let lines_required = calculate_wrapped_lines(&app.new_task_title, popup_width); + + let min_required_height = 1; + + let required_height = std::cmp::max(lines_required as u16, min_required_height); + + let popup_height = required_height + 2; + + let max_popup_height = size.height - 2; + let popup_height = std::cmp::min(popup_height, max_popup_height); + + let popup_area = + centered_rect_absolute(popup_width + 2, popup_height, body_chunk); let popup_block = Block::default() - .title("Enter New Task Title (Press Enter to Submit)") + .title("Enter New Task (Press Enter to Submit)") .borders(Borders::ALL) .style(Style::default().fg(Color::Green)); - let input = Paragraph::new(Text::from(app.new_task_title.as_str())) + let input = Paragraph::new(app.new_task_title.as_str()) .style(Style::default().fg(Color::White)) - .block(popup_block); + .block(popup_block) + .wrap(Wrap { trim: false }); f.render_widget(Clear, popup_area); f.render_widget(input, popup_area); @@ -279,3 +295,12 @@ pub async fn run_app( } } } + +fn calculate_wrapped_lines(text: &str, max_width: u16) -> usize { + let mut line_count = 0; + for line in text.lines() { + let line_width = line.chars().count() as u16; + line_count += ((line_width + max_width - 1) / max_width) as usize; + } + line_count +}