diff --git a/src/app.rs b/src/app.rs index 9a229e5..da9639e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -10,7 +10,9 @@ pub struct App { pub state: ListState, pub task_detail: Option, 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, } @@ -18,6 +20,12 @@ pub struct App { pub enum InputMode { Normal, Editing, + Insert, +} +#[derive(PartialEq)] +pub enum ActiveInput { + Title, + Description, } impl App { @@ -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, } @@ -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 { @@ -147,47 +159,74 @@ 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) } diff --git a/src/ui.rs b/src/ui.rs index d362d8e..de6c064 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,9 +1,9 @@ -use crate::app::{App, InputMode}; +use crate::app::{ActiveInput, App, InputMode}; use ansi_parser::{AnsiParser, Output}; -use crossterm::event::{self, Event as CEvent}; +use crossterm::event::{self, Event as CEvent, KeyCode}; use ratatui::{ backend::Backend, - layout::{Alignment, Constraint, Direction, Layout, Rect}, + layout::{Alignment, Constraint, Direction, Layout, Margin, Rect}, style::{Color, Modifier, Style}, text::{Line, Span, Text}, widgets::{Block, Borders, Clear, List, ListItem, Paragraph, Wrap}, @@ -12,29 +12,31 @@ use ratatui::{ use std::io; use std::time::Duration; +fn get_cursor_position(input: &str, area: Rect) -> (u16, u16) { + let lines: Vec<&str> = input.lines().collect(); + let last_line = lines.last().unwrap_or(&""); + let x = area.x + last_line.chars().count() as u16 + 1; + let y = area.y + lines.len() as u16 - 1 + 1; + (x, y) +} + fn centered_rect_absolute(width: u16, height: u16, r: Rect) -> Rect { let popup_layout = Layout::default() .direction(Direction::Vertical) - .constraints( - [ - Constraint::Length((r.height.saturating_sub(height)) / 2), - Constraint::Length(height), - Constraint::Length((r.height.saturating_sub(height) + 1) / 2), - ] - .as_ref(), - ) + .constraints([ + Constraint::Length((r.height.saturating_sub(height)) / 2u16), + Constraint::Length(height), + Constraint::Length((r.height.saturating_sub(height) + 1u16) / 2u16), + ]) .split(r); Layout::default() .direction(Direction::Horizontal) - .constraints( - [ - Constraint::Length((r.width.saturating_sub(width)) / 2), - Constraint::Length(width), - Constraint::Length((r.width.saturating_sub(width) + 1) / 2), - ] - .as_ref(), - ) + .constraints([ + Constraint::Length((r.width.saturating_sub(width)) / 2u16), + Constraint::Length(width), + Constraint::Length((r.width.saturating_sub(width) + 1u16) / 2u16), + ]) .split(popup_layout[1])[1] } @@ -61,27 +63,24 @@ fn get_legend(input_mode: &InputMode) -> Text<'static> { InputMode::Normal => Text::from(Line::from(vec![ Span::styled(" q ", Style::default().fg(Color::Red)), Span::raw(": Quit "), - Span::styled(" j ", Style::default().fg(Color::Red)), - Span::raw(": Down "), - Span::styled(" k ", Style::default().fg(Color::Red)), - Span::raw(": Up "), - Span::styled(" n ", Style::default().fg(Color::Red)), - 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 "), + // ... other keys ... Span::styled(" a ", Style::default().fg(Color::Red)), Span::raw(": Add Task "), ])), InputMode::Editing => Text::from(Line::from(vec![ + Span::styled(" i ", Style::default().fg(Color::Red)), + Span::raw(": Insert "), + Span::styled(" Tab ", Style::default().fg(Color::Red)), + Span::raw(": Switch Input "), Span::styled(" Enter ", Style::default().fg(Color::Red)), Span::raw(": Submit "), Span::styled(" Esc ", Style::default().fg(Color::Red)), Span::raw(": Cancel "), ])), + InputMode::Insert => Text::from(Line::from(vec![ + Span::styled(" Esc ", Style::default().fg(Color::Red)), + Span::raw(": Exit Insert Mode "), + ])), } } @@ -99,7 +98,7 @@ pub async fn run_app( let chunks = Layout::default() .direction(Direction::Vertical) .margin(0) - .constraints([Constraint::Min(0), Constraint::Length(2)].as_ref()) + .constraints([Constraint::Min(0), Constraint::Length(2u16)]) .split(size); let body_chunk = chunks[0]; @@ -109,9 +108,7 @@ pub async fn run_app( InputMode::Normal => { let chunks = Layout::default() .direction(Direction::Horizontal) - .constraints( - [Constraint::Percentage(65), Constraint::Percentage(35)].as_ref(), - ) + .constraints([Constraint::Percentage(65), Constraint::Percentage(35)]) .split(body_chunk); let task_title = if app.show_done_tasks { @@ -157,10 +154,9 @@ pub async fn run_app( let detail_block = Block::default().borders(Borders::ALL).title("Task Details"); if let Some(ref detail) = app.task_detail { - // Initialize lines with 'static lifetime let mut lines: Vec> = Vec::new(); - // due_date + // Due date let due_date = match &detail.due_date { Some(date) if date != "0001-01-01T00:00:00Z" => date.clone(), _ => "No due date".to_string(), @@ -173,7 +169,7 @@ pub async fn run_app( Span::raw(due_date), ])); - // priority + // Priority let priority_str = match detail.priority { Some(p) => p.to_string(), None => "No priority".to_string(), @@ -186,7 +182,7 @@ pub async fn run_app( Span::raw(priority_str), ])); - // labels + // Labels lines.push(Line::from(vec![Span::styled( "Labels: ", Style::default().add_modifier(Modifier::BOLD), @@ -197,7 +193,7 @@ pub async fn run_app( let mut label_spans: Vec> = Vec::new(); for (i, label) in labels.iter().enumerate() { if i > 0 { - label_spans.push(Span::raw(" ".to_string())); + label_spans.push(Span::raw(" ")); } label_spans.push(Span::styled( format!(" {} ", label.title), @@ -211,7 +207,7 @@ pub async fn run_app( } } - // description + // Description lines.push(Line::from(vec![Span::styled( "Description: ", Style::default().add_modifier(Modifier::BOLD), @@ -242,36 +238,95 @@ pub async fn run_app( f.render_widget(paragraph, chunks[1]); } } - InputMode::Editing => { - let popup_width_percentage = 60; - let popup_width = (size.width * popup_width_percentage / 100).saturating_sub(2); + InputMode::Editing | InputMode::Insert => { + let popup_width_percentage = 60u16; + let popup_width = + (size.width * popup_width_percentage / 100u16).saturating_sub(2u16); - let lines_required = calculate_wrapped_lines(&app.new_task_title, popup_width); + // Calculate the required heights for the input boxes + let title_lines_required = + calculate_wrapped_lines(&app.new_task_title, popup_width); + let description_lines_required = + calculate_wrapped_lines(&app.new_task_description, popup_width); - let min_required_height = 1; + let title_height = std::cmp::max(title_lines_required as u16, 1u16); + let description_height = std::cmp::max(description_lines_required as u16, 2u16); // At least 2 lines tall - let required_height = std::cmp::max(lines_required as u16, min_required_height); + let total_height = title_height + description_height + 6u16; // +6 for borders and titles - 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 max_popup_height = size.height - 2u16; + let popup_height = std::cmp::min(total_height, max_popup_height); let popup_area = - centered_rect_absolute(popup_width + 2, popup_height, body_chunk); + centered_rect_absolute(popup_width + 2u16, popup_height, body_chunk); let popup_block = Block::default() - .title("Enter New Task (Press Enter to Submit)") + .title("Enter New Task (Press Enter to Submit, Tab to Switch)") .borders(Borders::ALL) .style(Style::default().fg(Color::Green)); - let input = Paragraph::new(app.new_task_title.as_str()) + // Split the popup area vertically for the two input boxes + let input_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(title_height + 2u16), // +2 for borders + Constraint::Length(description_height + 2u16), // +2 for borders + ]) + .split(popup_area.inner(Margin { + vertical: 1u16, + horizontal: 1u16, + })); // Adjust for popup_block borders + + // Title input box + let title_block = Block::default().borders(Borders::ALL).title("Title").style( + if app.active_input == ActiveInput::Title { + Style::default().fg(Color::Yellow) + } else { + Style::default() + }, + ); + + let title_paragraph = Paragraph::new(app.new_task_title.as_str()) .style(Style::default().fg(Color::White)) - .block(popup_block) + .block(title_block) + .wrap(Wrap { trim: false }); + + // Description input box + let description_block = Block::default() + .borders(Borders::ALL) + .title("Description") + .style(if app.active_input == ActiveInput::Description { + Style::default().fg(Color::Yellow) + } else { + Style::default() + }); + + let description_paragraph = Paragraph::new(app.new_task_description.as_str()) + .style(Style::default().fg(Color::White)) + .block(description_block) .wrap(Wrap { trim: false }); f.render_widget(Clear, popup_area); - f.render_widget(input, popup_area); + f.render_widget(popup_block, popup_area); + + f.render_widget(title_paragraph, input_chunks[0]); + f.render_widget(description_paragraph, input_chunks[1]); + + // Set cursor position + match app.active_input { + ActiveInput::Title => { + // Calculate cursor position in title input + let cursor_position = + get_cursor_position(&app.new_task_title, input_chunks[0]); + f.set_cursor(cursor_position.0, cursor_position.1); + } + ActiveInput::Description => { + // Calculate cursor position in description input + let cursor_position = + get_cursor_position(&app.new_task_description, input_chunks[1]); + f.set_cursor(cursor_position.0, cursor_position.1); + } + } } } @@ -300,7 +355,8 @@ 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; + let total_width = line_width + max_width - 1u16; + line_count += (total_width / max_width) as usize; } line_count }