From 18a54f7f0378f4386b083063b3d88ac02bfc6310 Mon Sep 17 00:00:00 2001 From: Dheepak Krishnamurthy Date: Tue, 5 Sep 2023 22:44:33 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20Add=20taskwarriortuitask=20trait=20?= =?UTF-8?q?=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .rustfmt.toml | 2 +- build.rs | 5 +- src/app.rs | 305 ++++++++++++++++++++++++++++++-------------- src/calendar.rs | 15 ++- src/completion.rs | 6 +- src/config.rs | 15 ++- src/history.rs | 7 +- src/keyconfig.rs | 6 +- src/keyevent.rs | 48 +++++-- src/keymap.rs | 50 ++++++-- src/main.rs | 11 +- src/pane/context.rs | 21 ++- src/pane/project.rs | 13 +- src/table.rs | 18 +-- src/task_report.rs | 49 ++++++- src/traits.rs | 24 ++++ src/utils.rs | 34 +++-- 17 files changed, 462 insertions(+), 167 deletions(-) create mode 100644 src/traits.rs diff --git a/.rustfmt.toml b/.rustfmt.toml index db49549..ef5985c 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -1,4 +1,4 @@ -max_width = 150 +max_width = 120 tab_spaces = 2 group_imports = "StdExternalCrate" imports_granularity = "Crate" diff --git a/build.rs b/build.rs index fdba145..02f2e60 100644 --- a/build.rs +++ b/build.rs @@ -19,7 +19,10 @@ fn run_pandoc() -> Result { } fn get_commit_hash() { - let git_output = std::process::Command::new("git").args(["rev-parse", "--git-dir"]).output().ok(); + let git_output = std::process::Command::new("git") + .args(["rev-parse", "--git-dir"]) + .output() + .ok(); let git_dir = git_output.as_ref().and_then(|output| { std::str::from_utf8(&output.stdout) .ok() diff --git a/src/app.rs b/src/app.rs index 5a14d7f..f03503b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -58,6 +58,7 @@ use crate::{ table::{Row, Table, TableMode, TableState}, task_report::TaskReportTable, trace_dbg, + traits::TaskwarriorTuiTask, tui::{self, Event}, ui, utils::{self, get_data_dir}, @@ -255,7 +256,8 @@ impl TaskwarriorTui { .output() .context("Unable to run `task --version`")?; - let task_version = Versioning::new(String::from_utf8_lossy(&output.stdout).trim()).ok_or(anyhow!("Unable to get version string"))?; + let task_version = + Versioning::new(String::from_utf8_lossy(&output.stdout).trim()).ok_or(anyhow!("Unable to get version string"))?; let (w, h) = crossterm::terminal::size().unwrap_or((50, 15)); @@ -372,9 +374,9 @@ impl TaskwarriorTui { } pub fn update(&mut self, action: Action) -> Result> { - match action { - Action::Quit => self.should_quit = true, - _ => {} + if let Action::Quit = action { + self.should_quit = true; + return Ok(None); } Ok(None) } @@ -384,7 +386,10 @@ impl TaskwarriorTui { } pub fn get_context(&mut self) -> Result<()> { - let output = std::process::Command::new("task").arg("_get").arg("rc.context").output()?; + let output = std::process::Command::new("task") + .arg("_get") + .arg("rc.context") + .output()?; self.current_context = String::from_utf8_lossy(&output.stdout).to_string(); self.current_context = self.current_context.strip_suffix('\n').unwrap_or("").to_string(); @@ -465,7 +470,8 @@ impl TaskwarriorTui { let area = centered_rect(f.size(), 50, 50); f.render_widget(Clear, area); let t = format!("{}", self.current_selection); - let p = Paragraph::new(Text::from(t)).block(Block::default().borders(Borders::ALL).border_type(BorderType::Rounded)); + let p = + Paragraph::new(Text::from(t)).block(Block::default().borders(Borders::ALL).border_type(BorderType::Rounded)); f.render_widget(p, area); } @@ -557,7 +563,10 @@ impl TaskwarriorTui { f, rects[1], "Press any key to continue.", - (Span::styled("Error", Style::default().add_modifier(Modifier::BOLD)), None), + ( + Span::styled("Error", Style::default().add_modifier(Modifier::BOLD)), + None, + ), 0, false, self.error.clone(), @@ -567,7 +576,12 @@ impl TaskwarriorTui { let rect = centered_rect(f.size(), 90, 60); f.render_widget(Clear, rect); let p = Paragraph::new(Text::from(text)) - .block(Block::default().borders(Borders::ALL).border_type(BorderType::Rounded).title(title)) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .title(title), + ) .wrap(Wrap { trim: true }); f.render_widget(p, rect); // draw error pop up @@ -597,7 +611,10 @@ impl TaskwarriorTui { f, rects[1], self.command.value(), - (Span::styled("Jump to Task", Style::default().add_modifier(Modifier::BOLD)), None), + ( + Span::styled("Jump to Task", Style::default().add_modifier(Modifier::BOLD)), + None, + ), position, true, self.error.clone(), @@ -654,7 +671,10 @@ impl TaskwarriorTui { f, rects[1], self.command.value(), - (Span::styled("Shell Command", Style::default().add_modifier(Modifier::BOLD)), None), + ( + Span::styled("Shell Command", Style::default().add_modifier(Modifier::BOLD)), + None, + ), position, true, self.error.clone(), @@ -885,7 +905,13 @@ impl TaskwarriorTui { let area = centered_rect(f.size(), percent_x, percent_y); - f.render_widget(Clear, area.inner(&Margin { vertical: 0, horizontal: 0 })); + f.render_widget( + Clear, + area.inner(&Margin { + vertical: 0, + horizontal: 0, + }), + ); let (contexts, headers) = self.get_all_contexts(); @@ -918,7 +944,10 @@ impl TaskwarriorTui { Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .title(Line::from(vec![Span::styled("Context", Style::default().add_modifier(Modifier::BOLD))])), + .title(Line::from(vec![Span::styled( + "Context", + Style::default().add_modifier(Modifier::BOLD), + )])), ) .header_style( self @@ -968,7 +997,12 @@ impl TaskwarriorTui { rect.height = std::cmp::min(area.height / 2, self.completion_list.len() as u16 + 2); rect.width = std::cmp::min( area.width / 2, - self.completion_list.max_width().unwrap_or(40).try_into().unwrap_or(area.width / 2), + self + .completion_list + .max_width() + .unwrap_or(40) + .try_into() + .unwrap_or(area.width / 2), ); rect.y = rect.y.saturating_sub(rect.height); if cursor_position as u16 + rect.width >= area.width { @@ -994,7 +1028,10 @@ impl TaskwarriorTui { ) { // f.render_widget(Clear, rect); if cursor { - f.set_cursor(std::cmp::min(rect.x + position as u16, rect.x + rect.width.saturating_sub(2)), rect.y + 1); + f.set_cursor( + std::cmp::min(rect.x + position as u16, rect.x + rect.width.saturating_sub(2)), + rect.y + 1, + ); } let rects = Layout::default() .direction(Direction::Vertical) @@ -1034,7 +1071,9 @@ impl TaskwarriorTui { None => "Loading task details ...".to_string(), }; self.task_details_scroll = std::cmp::min( - (data.lines().count() as u16).saturating_sub(rect.height).saturating_add(2), + (data.lines().count() as u16) + .saturating_sub(rect.height) + .saturating_add(2), self.task_details_scroll, ); let p = Paragraph::new(Text::from(&data[..])) @@ -1092,7 +1131,12 @@ impl TaskwarriorTui { for tag_name in virtual_tag_names_in_precedence.iter().rev() { if tag_name == "uda." || tag_name == "priority" { if let Some(p) = task.priority() { - let s = self.config.color.get(&format!("color.uda.priority.{}", p)).copied().unwrap_or_default(); + let s = self + .config + .color + .get(&format!("color.uda.priority.{}", p)) + .copied() + .unwrap_or_default(); style = style.patch(s.0); } } else if tag_name == "tag." { @@ -1105,7 +1149,12 @@ impl TaskwarriorTui { } } else if tag_name == "project." { if let Some(p) = task.project() { - let s = self.config.color.get(&format!("color.project.{}", p)).copied().unwrap_or_default(); + let s = self + .config + .color + .get(&format!("color.project.{}", p)) + .copied() + .unwrap_or_default(); style = style.patch(s.0); } } else if task @@ -1151,7 +1200,10 @@ impl TaskwarriorTui { // now start trimming while (widths.iter().sum::() as u16) >= maximum_column_width - (headers.len()) as u16 { - let index = widths.iter().position(|i| i == widths.iter().max().unwrap_or(&0)).unwrap_or_default(); + let index = widths + .iter() + .position(|i| i == widths.iter().max().unwrap_or(&0)) + .unwrap_or_default(); if widths[index] == 1 { break; } @@ -1636,7 +1688,10 @@ impl TaskwarriorTui { debug!("Unable to parse output: {:?}", data); } } else { - self.error = Some(format!("Cannot run `{:?}` - ({}) error:\n{}", &task, output.status, error)); + self.error = Some(format!( + "Cannot run `{:?}` - ({}) error:\n{}", + &task, output.status, error + )); } Ok(()) @@ -1653,7 +1708,9 @@ impl TaskwarriorTui { .arg("rc._forcecolor=off"); // .arg("rc.verbose:override=false"); - if let Some(args) = shlex::split(format!(r#"rc.report.{}.filter='{}'"#, self.report, self.filter.value().trim()).trim()) { + if let Some(args) = + shlex::split(format!(r#"rc.report.{}.filter='{}'"#, self.report, self.filter.value().trim()).trim()) + { for arg in args { task.arg(arg); } @@ -1695,7 +1752,10 @@ impl TaskwarriorTui { debug!("Unable to parse output: {:?}", data); } } else { - self.error = Some(format!("Cannot run `{:?}` - ({}) error:\n{}", &task, output.status, error)); + self.error = Some(format!( + "Cannot run `{:?}` - ({}) error:\n{}", + &task, output.status, error + )); } Ok(()) @@ -1722,7 +1782,11 @@ impl TaskwarriorTui { } pub fn task_subprocess(&mut self) -> Result<(), String> { - let task_uuids = if self.tasks.is_empty() { vec![] } else { self.selected_task_uuids() }; + let task_uuids = if self.tasks.is_empty() { + vec![] + } else { + self.selected_task_uuids() + }; let shell = self.command.value(); @@ -1745,7 +1809,14 @@ impl TaskwarriorTui { Ok(o) => { let output = String::from_utf8_lossy(&o.stdout); if !output.is_empty() { - Err(format!("Shell command `{}` ran successfully but printed the following output:\n\n{}\n\nSuppress output of shell commands to prevent the error prompt from showing up.", shell, output)) + Err(format!( + r#"Shell command `{}` ran successfully but printed the following output: + + {} + + Suppress output of shell commands to prevent the error prompt from showing up."#, + shell, output + )) } else { Ok(()) } @@ -1781,10 +1852,16 @@ impl TaskwarriorTui { let output = command.output(); match output { Ok(_) => Ok(()), - Err(_) => Err(format!("Cannot run `task log {}`. Check documentation for more information", shell)), + Err(_) => Err(format!( + "Cannot run `task log {}`. Check documentation for more information", + shell + )), } } - None => Err(format!("Unable to run `{:?}`: shlex::split(`{}`) failed.", command, shell)), + None => Err(format!( + "Unable to run `{:?}`: shlex::split(`{}`) failed.", + command, shell + )), } } @@ -1819,7 +1896,11 @@ impl TaskwarriorTui { pub fn task_shortcut(&mut self, s: usize) -> Result<(), String> { // self.pause_tui().await.unwrap(); - let task_uuids = if self.tasks.is_empty() { vec![] } else { self.selected_task_uuids() }; + let task_uuids = if self.tasks.is_empty() { + vec![] + } else { + self.selected_task_uuids() + }; let shell = &self.config.uda_shortcuts[s]; @@ -1831,7 +1912,11 @@ impl TaskwarriorTui { let shell = format!( "{} {}", shell, - task_uuids.iter().map(ToString::to_string).collect::>().join(" ") + task_uuids + .iter() + .map(ToString::to_string) + .collect::>() + .join(" ") ); let shell = shellexpand::tilde(&shell).into_owned(); @@ -1861,10 +1946,16 @@ impl TaskwarriorTui { Err(s) => Err(format!("`{}` failed to wait with output: {}", shell, s)), } } - Err(err) => Err(format!("`{}` failed: Unable to spawn shortcut number {} - Error: {}", shell, s, err)), + Err(err) => Err(format!( + "`{}` failed: Unable to spawn shortcut number {} - Error: {}", + shell, s, err + )), } } - None => Err(format!("Unable to run shortcut number {}: shlex::split(`{}`) failed.", s, shell)), + None => Err(format!( + "Unable to run shortcut number {}: shlex::split(`{}`) failed.", + s, shell + )), }; if task_uuids.len() == 1 { @@ -1964,7 +2055,11 @@ impl TaskwarriorTui { } Err(_) => Err(format!( "Cannot run `task {} annotate {}`. Check documentation for more information", - task_uuids.iter().map(ToString::to_string).collect::>().join(" "), + task_uuids + .iter() + .map(ToString::to_string) + .collect::>() + .join(" "), shell )), } @@ -2010,12 +2105,17 @@ impl TaskwarriorTui { Err(e) => Err(format!("Cannot run `{:?}`: {}", command, e)), } } - None => Err(format!("Unable to run `{:?}`: shlex::split(`{}`) failed.", command, shell)), + None => Err(format!( + "Unable to run `{:?}`: shlex::split(`{}`) failed.", + command, shell + )), } } pub fn task_virtual_tags(task_uuid: Uuid) -> Result { - let output = std::process::Command::new("task").arg(format!("{}", task_uuid)).output(); + let output = std::process::Command::new("task") + .arg(format!("{}", task_uuid)) + .output(); match output { Ok(output) => { @@ -2034,7 +2134,10 @@ impl TaskwarriorTui { task_uuid )) } - Err(_) => Err(format!("Cannot run `task {}`. Check documentation for more information", task_uuid)), + Err(_) => Err(format!( + "Cannot run `task {}`. Check documentation for more information", + task_uuid + )), } } @@ -2047,13 +2150,19 @@ impl TaskwarriorTui { for task_uuid in &task_uuids { let mut command = "start"; - for tag in TaskwarriorTui::task_virtual_tags(*task_uuid).unwrap_or_default().split(' ') { + for tag in TaskwarriorTui::task_virtual_tags(*task_uuid) + .unwrap_or_default() + .split(' ') + { if tag == "ACTIVE" { command = "stop"; } } - let output = std::process::Command::new("task").arg(task_uuid.to_string()).arg(command).output(); + let output = std::process::Command::new("task") + .arg(task_uuid.to_string()) + .arg(command) + .output(); if output.is_err() { return Err(format!("Error running `task {}` for task `{}`.", command, task_uuid)); } @@ -2094,7 +2203,10 @@ impl TaskwarriorTui { .output(); if output.is_err() { - return Err(format!("Error running `task modify {}` for task `{}`.", tag_to_set, task_uuid,)); + return Err(format!( + "Error running `task modify {}` for task `{}`.", + tag_to_set, task_uuid, + )); } } } @@ -2130,7 +2242,11 @@ impl TaskwarriorTui { Ok(_) => Ok(()), Err(_) => Err(format!( "Cannot run `task delete` for tasks `{}`. Check documentation for more information", - task_uuids.iter().map(ToString::to_string).collect::>().join(" ") + task_uuids + .iter() + .map(ToString::to_string) + .collect::>() + .join(" ") )), }; self.current_selection_uuid = None; @@ -2158,7 +2274,11 @@ impl TaskwarriorTui { Ok(_) => Ok(()), Err(_) => Err(format!( "Cannot run `task done` for task `{}`. Check documentation for more information", - task_uuids.iter().map(ToString::to_string).collect::>().join(" ") + task_uuids + .iter() + .map(ToString::to_string) + .collect::>() + .join(" ") )), }; self.current_selection_uuid = None; @@ -2167,12 +2287,17 @@ impl TaskwarriorTui { } pub fn task_undo(&mut self) -> Result<(), String> { - let output = std::process::Command::new("task").arg("rc.confirmation=off").arg("undo").output(); + let output = std::process::Command::new("task") + .arg("rc.confirmation=off") + .arg("undo") + .output(); match output { Ok(output) => { let data = String::from_utf8_lossy(&output.stdout); - let re = Regex::new(r"(?P[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})").unwrap(); + let re = + Regex::new(r"(?P[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})") + .unwrap(); if let Some(caps) = re.captures(&data) { if let Ok(uuid) = Uuid::parse_str(&caps["task_uuid"]) { self.current_selection_uuid = Some(uuid); @@ -2195,7 +2320,10 @@ impl TaskwarriorTui { let task_id = self.tasks[selected].id().unwrap_or_default(); let task_uuid = *self.tasks[selected].uuid(); - let r = std::process::Command::new("task").arg(format!("{}", task_uuid)).arg("edit").spawn(); + let r = std::process::Command::new("task") + .arg(format!("{}", task_uuid)) + .arg("edit") + .spawn(); let r = match r { Ok(child) => { @@ -2244,7 +2372,7 @@ impl TaskwarriorTui { for l_i in 0..tasks.len() { let default_deps = vec![]; let deps = tasks[l_i].depends().unwrap_or(&default_deps).clone(); - add_tag(&mut tasks[l_i], "UNBLOCKED".to_string()); + tasks[l_i].add_tag("UNBLOCKED".to_string()); for dep in deps { for r_i in 0..tasks.len() { if tasks[r_i].uuid() == &dep { @@ -2255,9 +2383,9 @@ impl TaskwarriorTui { && r_status != &TaskStatus::Completed && r_status != &TaskStatus::Deleted { - remove_tag(&mut tasks[l_i], "UNBLOCKED"); - add_tag(&mut tasks[l_i], "BLOCKED".to_string()); - add_tag(&mut tasks[r_i], "BLOCKING".to_string()); + tasks[l_i].remove_tag("UNBLOCKED"); + tasks[l_i].add_tag("BLOCKED".to_string()); + tasks[r_i].add_tag("BLOCKING".to_string()); } break; } @@ -2269,45 +2397,45 @@ impl TaskwarriorTui { // TODO: support all virtual tags that taskwarrior supports for task in tasks.iter_mut() { match task.status() { - TaskStatus::Waiting => add_tag(task, "WAITING".to_string()), - TaskStatus::Completed => add_tag(task, "COMPLETED".to_string()), - TaskStatus::Pending => add_tag(task, "PENDING".to_string()), - TaskStatus::Deleted => add_tag(task, "DELETED".to_string()), + TaskStatus::Waiting => task.add_tag("WAITING".to_string()), + TaskStatus::Completed => task.add_tag("COMPLETED".to_string()), + TaskStatus::Pending => task.add_tag("PENDING".to_string()), + TaskStatus::Deleted => task.add_tag("DELETED".to_string()), TaskStatus::Recurring => (), } if task.start().is_some() { - add_tag(task, "ACTIVE".to_string()); + task.add_tag("ACTIVE".to_string()); } if task.scheduled().is_some() { - add_tag(task, "SCHEDULED".to_string()); + task.add_tag("SCHEDULED".to_string()); } if task.parent().is_some() { - add_tag(task, "INSTANCE".to_string()); + task.add_tag("INSTANCE".to_string()); } if task.until().is_some() { - add_tag(task, "UNTIL".to_string()); + task.add_tag("UNTIL".to_string()); } if task.annotations().is_some() { - add_tag(task, "ANNOTATED".to_string()); + task.add_tag("ANNOTATED".to_string()); } let virtual_tags = self.task_report_table.virtual_tags.clone(); if task.tags().is_some() && task.tags().unwrap().iter().any(|s| !virtual_tags.contains(s)) { - add_tag(task, "TAGGED".to_string()); + task.add_tag("TAGGED".to_string()); } if !task.uda().is_empty() { - add_tag(task, "UDA".to_string()); + task.add_tag("UDA".to_string()); } if task.mask().is_some() { - add_tag(task, "TEMPLATE".to_string()); + task.add_tag("TEMPLATE".to_string()); } if task.project().is_some() { - add_tag(task, "PROJECT".to_string()); + task.add_tag("PROJECT".to_string()); } if task.priority().is_some() { - add_tag(task, "PRIORITY".to_string()); + task.add_tag("PRIORITY".to_string()); } if task.recur().is_some() { - add_tag(task, "RECURRING".to_string()); + task.add_tag("RECURRING".to_string()); let r = task.recur().unwrap(); } if let Some(d) = task.due() { @@ -2319,24 +2447,24 @@ impl TaskwarriorTui { let now = TimeZone::from_utc_datetime(now.offset(), &now.naive_utc()); let d = d.clone(); if (reference - chrono::Duration::nanoseconds(1)).month() == now.month() { - add_tag(task, "MONTH".to_string()); + task.add_tag("MONTH".to_string()); } if (reference - chrono::Duration::nanoseconds(1)).month() % 4 == now.month() % 4 { - add_tag(task, "QUARTER".to_string()); + task.add_tag("QUARTER".to_string()); } if reference.year() == now.year() { - add_tag(task, "YEAR".to_string()); + task.add_tag("YEAR".to_string()); } match get_date_state(&d, self.config.due) { DateState::EarlierToday | DateState::LaterToday => { - add_tag(task, "DUE".to_string()); - add_tag(task, "TODAY".to_string()); - add_tag(task, "DUETODAY".to_string()); + task.add_tag("DUE".to_string()); + task.add_tag("TODAY".to_string()); + task.add_tag("DUETODAY".to_string()); } DateState::AfterToday => { - add_tag(task, "DUE".to_string()); + task.add_tag("DUE".to_string()); if reference.date_naive() == (now + chrono::Duration::days(1)).date_naive() { - add_tag(task, "TOMORROW".to_string()); + task.add_tag("TOMORROW".to_string()); } } _ => (), @@ -2350,7 +2478,7 @@ impl TaskwarriorTui { let now = Local::now().naive_utc(); let d = NaiveDateTime::new(d.date(), d.time()); if d < now { - add_tag(task, "OVERDUE".to_string()); + task.add_tag("OVERDUE".to_string()); } } } @@ -2528,7 +2656,9 @@ impl TaskwarriorTui { if let Some(tags) = task.tags() { for tag in tags { if !virtual_tags.contains(tag) { - self.completion_list.insert(("tag".to_string(), format!("tag:{}", &tag))); + self + .completion_list + .insert(("tag".to_string(), format!("tag:{}", &tag))); } } } @@ -2554,22 +2684,30 @@ impl TaskwarriorTui { } for task in tasks { if let Some(date) = task.due() { - self.completion_list.insert(("due".to_string(), get_formatted_datetime(date))); + self + .completion_list + .insert(("due".to_string(), get_formatted_datetime(date))); } } for task in tasks { if let Some(date) = task.wait() { - self.completion_list.insert(("wait".to_string(), get_formatted_datetime(date))); + self + .completion_list + .insert(("wait".to_string(), get_formatted_datetime(date))); } } for task in tasks { if let Some(date) = task.scheduled() { - self.completion_list.insert(("scheduled".to_string(), get_formatted_datetime(date))); + self + .completion_list + .insert(("scheduled".to_string(), get_formatted_datetime(date))); } } for task in tasks { if let Some(date) = task.end() { - self.completion_list.insert(("end".to_string(), get_formatted_datetime(date))); + self + .completion_list + .insert(("end".to_string(), get_formatted_datetime(date))); } } } @@ -2604,24 +2742,5 @@ impl TaskwarriorTui { } } -pub fn handle_movement(linebuffer: &mut Input, input: KeyEvent) { - linebuffer.handle_event(&crossterm::event::Event::Key(input)); -} - -pub fn add_tag(task: &mut Task, tag: String) { - match task.tags_mut() { - Some(t) => t.push(tag), - None => task.set_tags(Some(vec![tag])), - } -} - -pub fn remove_tag(task: &mut Task, tag: &str) { - if let Some(t) = task.tags_mut() { - if let Some(index) = t.iter().position(|x| *x == tag) { - t.remove(index); - } - } -} - #[cfg(test)] mod tests {} diff --git a/src/calendar.rs b/src/calendar.rs index 04d5056..bd06510 100644 --- a/src/calendar.rs +++ b/src/calendar.rs @@ -7,7 +7,9 @@ const COL_WIDTH: usize = 21; use std::cmp::min; -use chrono::{format::Fixed, DateTime, Datelike, Duration, FixedOffset, Local, Month, NaiveDate, NaiveDateTime, TimeZone}; +use chrono::{ + format::Fixed, DateTime, Datelike, Duration, FixedOffset, Local, Month, NaiveDate, NaiveDateTime, TimeZone, +}; use ratatui::{ buffer::Buffer, layout::Rect, @@ -228,13 +230,20 @@ impl<'a> Widget for Calendar<'a> { .iter() .map(|i| { let first = NaiveDate::from_ymd_opt(self.year + new_year as i32, i + 1, 1).unwrap(); - (first, first - Duration::days(i64::from(first.weekday().num_days_from_sunday()))) + ( + first, + first - Duration::days(i64::from(first.weekday().num_days_from_sunday())), + ) }) .collect::>(), ); let x = area.x; - let s = format!("{year:^width$}", year = self.year as usize + new_year, width = area.width as usize); + let s = format!( + "{year:^width$}", + year = self.year as usize + new_year, + width = area.width as usize + ); let mut style = Style::default().add_modifier(Modifier::UNDERLINED); if self.year + new_year as i32 == today.year() { style = style.add_modifier(Modifier::BOLD); diff --git a/src/completion.rs b/src/completion.rs index 858c3d8..3e413b8 100644 --- a/src/completion.rs +++ b/src/completion.rs @@ -100,7 +100,11 @@ impl CompletionList { state: ListState::default(), current: String::new(), pos: 0, - helper: TaskwarriorTuiCompletionHelper { candidates, context, input }, + helper: TaskwarriorTuiCompletionHelper { + candidates, + context, + input, + }, } } diff --git a/src/config.rs b/src/config.rs index 0ff12f1..5947811 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,3 +1,7 @@ +use std::{collections::HashMap, error::Error, path::PathBuf, str}; + +use color_eyre::eyre::{eyre, Context, Result}; +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use figment::{ providers::{Env, Format, Serialized, Toml}, Figment, @@ -6,13 +10,10 @@ use ratatui::{ style::{Color, Modifier, Style}, symbols::line::DOUBLE_VERTICAL, }; -use std::{collections::HashMap, error::Error, path::PathBuf, str}; - -use color_eyre::eyre::{eyre, Context, Result}; - -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use serde::de::{self, Deserialize, Deserializer, MapAccess, Visitor}; -use serde::ser::{self, Serialize, Serializer}; +use serde::{ + de::{self, Deserialize, Deserializer, MapAccess, Visitor}, + ser::{self, Serialize, Serializer}, +}; use serde_derive::{Deserialize, Serialize}; use crate::{action::Action, keyevent::parse_key_sequence, keymap::KeyMap, utils::get_config_dir}; diff --git a/src/history.rs b/src/history.rs index 01b3cd7..5f9bc17 100644 --- a/src/history.rs +++ b/src/history.rs @@ -19,7 +19,8 @@ impl HistoryContext { pub fn new(filename: &str, data_path: PathBuf) -> Self { let history = DefaultHistory::new(); - std::fs::create_dir_all(&data_path).unwrap_or_else(|_| panic!("Unable to create configuration directory in {:?}", &data_path)); + std::fs::create_dir_all(&data_path) + .unwrap_or_else(|_| panic!("Unable to create configuration directory in {:?}", &data_path)); let data_path = data_path.join(filename); @@ -78,7 +79,9 @@ impl HistoryContext { } else { let hi = self.history_index().unwrap(); - if hi == self.history.len().saturating_sub(1) && dir == SearchDirection::Forward || hi == 0 && dir == SearchDirection::Reverse { + if hi == self.history.len().saturating_sub(1) && dir == SearchDirection::Forward + || hi == 0 && dir == SearchDirection::Reverse + { return None; } diff --git a/src/keyconfig.rs b/src/keyconfig.rs index f90b137..1484fb1 100644 --- a/src/keyconfig.rs +++ b/src/keyconfig.rs @@ -204,7 +204,11 @@ impl KeyConfig { error!("Found multiple characters in {} for {}", line, config); } } else if line.starts_with(&config.replace('-', "_")) { - let line = line.trim_start_matches(&config.replace('-', "_")).trim_start().trim_end().to_string(); + let line = line + .trim_start_matches(&config.replace('-', "_")) + .trim_start() + .trim_end() + .to_string(); if has_just_one_char(&line) { return Some(KeyCode::Char(line.chars().next().unwrap())); } else { diff --git a/src/keyevent.rs b/src/keyevent.rs index 46cbe62..ce24b39 100644 --- a/src/keyevent.rs +++ b/src/keyevent.rs @@ -186,9 +186,10 @@ pub fn key_event_to_string(event: KeyEvent) -> String { #[cfg(test)] mod tests { - use super::*; use pretty_assertions::assert_eq; + use super::*; + fn test_event_to_string() { let event = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL); println!("{}", key_event_to_string(event)); // Outputs: ctrl-a @@ -197,7 +198,10 @@ mod tests { #[test] fn test_single_key_sequence() { let result = parse_key_sequence("a"); - assert_eq!(result.unwrap(), vec![KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty())]); + assert_eq!( + result.unwrap(), + vec![KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty())] + ); let result = parse_key_sequence(""); assert_eq!( @@ -217,11 +221,17 @@ mod tests { ] ); let result = parse_key_sequence(""); - assert_eq!(result.unwrap(), vec![KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL),]); + assert_eq!( + result.unwrap(), + vec![KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL),] + ); let result = parse_key_sequence(""); assert_eq!( result.unwrap(), - vec![KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL | KeyModifiers::ALT),] + vec![KeyEvent::new( + KeyCode::Char('a'), + KeyModifiers::CONTROL | KeyModifiers::ALT + ),] ); assert!(parse_key_sequence("Ctrl-a>").is_err()); assert!(parse_key_sequence(", Action>); @@ -96,7 +103,11 @@ impl<'de> Deserialize<'de> for KeyMap { if let Some(old_action) = keymap.insert(key_sequence, action.clone()) { if old_action != action { - return Err(format!("Found a {:?} for both {:?} and {:?}", key_sequence_str, old_action, action)).map_err(de::Error::custom); + return Err(format!( + "Found a {:?} for both {:?} and {:?}", + key_sequence_str, old_action, action + )) + .map_err(de::Error::custom); } } } @@ -110,14 +121,21 @@ impl<'de> Deserialize<'de> for KeyMap { #[cfg(test)] mod validate_tests { - use super::*; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; + use super::*; + #[test] fn test_no_conflict() { let mut map = std::collections::HashMap::new(); - map.insert(vec![KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty())], Action::Quit); - map.insert(vec![KeyEvent::new(KeyCode::Char('b'), KeyModifiers::empty())], Action::Quit); + map.insert( + vec![KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty())], + Action::Quit, + ); + map.insert( + vec![KeyEvent::new(KeyCode::Char('b'), KeyModifiers::empty())], + Action::Quit, + ); let keymap = KeyMap(map); assert!(keymap.validate().is_ok()); @@ -126,7 +144,10 @@ mod validate_tests { #[test] fn test_conflict_prefix() { let mut map = std::collections::HashMap::new(); - map.insert(vec![KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty())], Action::Quit); + map.insert( + vec![KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty())], + Action::Quit, + ); map.insert( vec![ KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()), @@ -142,7 +163,10 @@ mod validate_tests { #[test] fn test_no_conflict_different_modifiers() { let mut map = std::collections::HashMap::new(); - map.insert(vec![KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL)], Action::Quit); + map.insert( + vec![KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL)], + Action::Quit, + ); map.insert(vec![KeyEvent::new(KeyCode::Char('a'), KeyModifiers::ALT)], Action::Quit); let keymap = KeyMap(map); diff --git a/src/main.rs b/src/main.rs index 9fce2ea..ad60a85 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,6 +18,7 @@ mod pane; mod scrollbar; mod table; mod task_report; +mod traits; mod tui; mod ui; mod utils; @@ -70,7 +71,10 @@ async fn main() -> Result<()> { if let Some(e) = taskrc { if env::var("TASKRC").is_err() { // if environment variable is not set, this env::var returns an error - env::set_var("TASKRC", absolute_path(PathBuf::from(e)).expect("Unable to get path for taskrc")) + env::set_var( + "TASKRC", + absolute_path(PathBuf::from(e)).expect("Unable to get path for taskrc"), + ) } else { log::warn!("TASKRC environment variable cannot be set.") } @@ -79,7 +83,10 @@ async fn main() -> Result<()> { if let Some(e) = taskdata { if env::var("TASKDATA").is_err() { // if environment variable is not set, this env::var returns an error - env::set_var("TASKDATA", absolute_path(PathBuf::from(e)).expect("Unable to get path for taskdata")) + env::set_var( + "TASKDATA", + absolute_path(PathBuf::from(e)).expect("Unable to get path for taskdata"), + ) } else { log::warn!("TASKDATA environment variable cannot be set.") } diff --git a/src/pane/context.rs b/src/pane/context.rs index 3a190f7..786f786 100644 --- a/src/pane/context.rs +++ b/src/pane/context.rs @@ -65,7 +65,12 @@ impl ContextsState { Self { table_state: TableState::default(), report_height: 0, - columns: vec![NAME.to_string(), TYPE.to_string(), DEFINITION.to_string(), ACTIVE.to_string()], + columns: vec![ + NAME.to_string(), + TYPE.to_string(), + DEFINITION.to_string(), + ACTIVE.to_string(), + ], rows: vec![], } } @@ -114,7 +119,12 @@ impl ContextsState { let definition = line.replacen(name, "", 1); let definition = definition.replacen(typ, "", 1); let definition = definition.strip_suffix(active).unwrap_or_default(); - let context = ContextDetails::new(name.to_string(), definition.trim().to_string(), active.to_string(), typ.to_string()); + let context = ContextDetails::new( + name.to_string(), + definition.trim().to_string(), + active.to_string(), + typ.to_string(), + ); self.rows.push(context); } if self.rows.iter().any(|r| r.active != "no") { @@ -125,7 +135,12 @@ impl ContextsState { } else { self.rows.insert( 0, - ContextDetails::new("none".to_string(), "".to_string(), "yes".to_string(), "read".to_string()), + ContextDetails::new( + "none".to_string(), + "".to_string(), + "yes".to_string(), + "read".to_string(), + ), ); } Ok(()) diff --git a/src/pane/project.rs b/src/pane/project.rs index 0b7252a..2940070 100644 --- a/src/pane/project.rs +++ b/src/pane/project.rs @@ -104,7 +104,14 @@ impl ProjectsState { let rows = self .rows .iter() - .map(|c| vec![c.name.clone(), c.remaining.to_string(), c.avg_age.to_string(), c.complete.clone()]) + .map(|c| { + vec![ + c.name.clone(), + c.remaining.to_string(), + c.avg_age.to_string(), + c.complete.clone(), + ] + }) .collect(); let headers = self.columns.clone(); (rows, headers) @@ -112,7 +119,9 @@ impl ProjectsState { pub fn last_line(&self, line: &str) -> bool { let words = line.trim().split(' ').map(|s| s.trim()).collect::>(); - return words.len() == 2 && words[0].chars().map(|c| c.is_numeric()).all(|b| b) && (words[1] == "project" || words[1] == "projects"); + return words.len() == 2 + && words[0].chars().map(|c| c.is_numeric()).all(|b| b) + && (words[1] == "project" || words[1] == "projects"); } pub fn update_data(&mut self) -> Result<()> { diff --git a/src/table.rs b/src/table.rs index b38b600..9777fac 100644 --- a/src/table.rs +++ b/src/table.rs @@ -445,16 +445,18 @@ where }); for (i, row) in self.rows.skip(state.offset).take(remaining).enumerate() { let (data, style, symbol) = match row { - Row::Data(d) | Row::StyledData(d, _) if Some(i) == state.current_selection().map(|s| s - state.offset) => match state.mode { - TableMode::MultipleSelection => { - if state.marked.contains(&(i + state.offset)) { - (d, highlight_style, mark_highlight_symbol.to_string()) - } else { - (d, highlight_style, unmark_highlight_symbol.to_string()) + Row::Data(d) | Row::StyledData(d, _) if Some(i) == state.current_selection().map(|s| s - state.offset) => { + match state.mode { + TableMode::MultipleSelection => { + if state.marked.contains(&(i + state.offset)) { + (d, highlight_style, mark_highlight_symbol.to_string()) + } else { + (d, highlight_style, unmark_highlight_symbol.to_string()) + } } + TableMode::SingleSelection => (d, highlight_style, highlight_symbol.to_string()), } - TableMode::SingleSelection => (d, highlight_style, highlight_symbol.to_string()), - }, + } Row::Data(d) => { if state.marked.contains(&(i + state.offset)) { (d, default_style, mark_symbol.to_string()) diff --git a/src/task_report.rs b/src/task_report.rs index ec8a48b..e2b9923 100644 --- a/src/task_report.rs +++ b/src/task_report.rs @@ -38,37 +38,67 @@ pub fn vague_format_date_time(from_dt: NaiveDateTime, to_dt: NaiveDateTime, with if seconds >= 60 * 60 * 24 * 365 { return if with_remainder { - format!("{}{}y{}mo", minus, seconds / year, (seconds - year * (seconds / year)) / month) + format!( + "{}{}y{}mo", + minus, + seconds / year, + (seconds - year * (seconds / year)) / month + ) } else { format!("{}{}y", minus, seconds / year) }; } else if seconds >= 60 * 60 * 24 * 90 { return if with_remainder { - format!("{}{}mo{}w", minus, seconds / month, (seconds - month * (seconds / month)) / week) + format!( + "{}{}mo{}w", + minus, + seconds / month, + (seconds - month * (seconds / month)) / week + ) } else { format!("{}{}mo", minus, seconds / month) }; } else if seconds >= 60 * 60 * 24 * 14 { return if with_remainder { - format!("{}{}w{}d", minus, seconds / week, (seconds - week * (seconds / week)) / day) + format!( + "{}{}w{}d", + minus, + seconds / week, + (seconds - week * (seconds / week)) / day + ) } else { format!("{}{}w", minus, seconds / week) }; } else if seconds >= 60 * 60 * 24 { return if with_remainder { - format!("{}{}d{}h", minus, seconds / day, (seconds - day * (seconds / day)) / hour) + format!( + "{}{}d{}h", + minus, + seconds / day, + (seconds - day * (seconds / day)) / hour + ) } else { format!("{}{}d", minus, seconds / day) }; } else if seconds >= 60 * 60 { return if with_remainder { - format!("{}{}h{}min", minus, seconds / hour, (seconds - hour * (seconds / hour)) / minute) + format!( + "{}{}h{}min", + minus, + seconds / hour, + (seconds - hour * (seconds / hour)) / minute + ) } else { format!("{}{}h", minus, seconds / hour) }; } else if seconds >= 60 { return if with_remainder { - format!("{}{}min{}s", minus, seconds / minute, (seconds - minute * (seconds / minute))) + format!( + "{}{}min{}s", + minus, + seconds / minute, + (seconds - minute * (seconds / minute)) + ) } else { format!("{}{}min", minus, seconds / minute) }; @@ -365,7 +395,12 @@ impl TaskReportTable { None => "".to_string(), }, "tags" => match task.tags() { - Some(v) => v.iter().filter(|t| !self.virtual_tags.contains(t)).cloned().collect::>().join(","), + Some(v) => v + .iter() + .filter(|t| !self.virtual_tags.contains(t)) + .cloned() + .collect::>() + .join(","), None => "".to_string(), }, "recur" => match task.recur() { diff --git a/src/traits.rs b/src/traits.rs new file mode 100644 index 0000000..28d8ca9 --- /dev/null +++ b/src/traits.rs @@ -0,0 +1,24 @@ +use task_hookrs::task::Task; + +pub trait TaskwarriorTuiTask { + fn add_tag(&mut self, tag: String); + + fn remove_tag(&mut self, tag: &str); +} + +impl TaskwarriorTuiTask for Task { + fn add_tag(&mut self, tag: String) { + match self.tags_mut() { + Some(t) => t.push(tag), + None => self.set_tags(Some(vec![tag])), + } + } + + fn remove_tag(&mut self, tag: &str) { + if let Some(t) = self.tags_mut() { + if let Some(index) = t.iter().position(|x| *x == tag) { + t.remove(index); + } + } + } +} diff --git a/src/utils.rs b/src/utils.rs index 9840e14..b3f00c3 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -24,15 +24,22 @@ use directories::ProjectDirs; use lazy_static::lazy_static; use tracing::error; use tracing_error::ErrorLayer; -use tracing_subscriber::{self, filter::EnvFilter, prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt, Layer}; +use tracing_subscriber::{ + self, filter::EnvFilter, prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt, Layer, +}; use crate::tui::Tui; lazy_static! { pub static ref CRATE_NAME: String = env!("CARGO_CRATE_NAME").to_uppercase().to_string(); - pub static ref DATA_FOLDER: Option = std::env::var(format!("{}_DATA", CRATE_NAME.clone())).ok().map(PathBuf::from); - pub static ref CONFIG_FOLDER: Option = std::env::var(format!("{}_CONFIG", CRATE_NAME.clone())).ok().map(PathBuf::from); - pub static ref GIT_COMMIT_HASH: String = std::env::var(format!("{}_GIT_INFO", CRATE_NAME.clone())).unwrap_or_else(|_| String::from("Unknown")); + pub static ref DATA_FOLDER: Option = std::env::var(format!("{}_DATA", CRATE_NAME.clone())) + .ok() + .map(PathBuf::from); + pub static ref CONFIG_FOLDER: Option = std::env::var(format!("{}_CONFIG", CRATE_NAME.clone())) + .ok() + .map(PathBuf::from); + pub static ref GIT_COMMIT_HASH: String = + std::env::var(format!("{}_GIT_INFO", CRATE_NAME.clone())).unwrap_or_else(|_| String::from("Unknown")); pub static ref LOG_FILE: String = format!("{}.log", CRATE_NAME.to_lowercase()); } @@ -105,15 +112,16 @@ pub fn initialize_logging(directory: PathBuf) -> Result<()> { .with(tui_logger::tracing_subscriber_layer()) .with(ErrorLayer::default()) .init(); - let default_level = std::env::var("RUST_LOG").map_or(log::LevelFilter::Info, |val| match val.to_lowercase().as_str() { - "off" => log::LevelFilter::Off, - "error" => log::LevelFilter::Error, - "warn" => log::LevelFilter::Warn, - "info" => log::LevelFilter::Info, - "debug" => log::LevelFilter::Debug, - "trace" => log::LevelFilter::Trace, - _ => log::LevelFilter::Info, - }); + let default_level = + std::env::var("RUST_LOG").map_or(log::LevelFilter::Info, |val| match val.to_lowercase().as_str() { + "off" => log::LevelFilter::Off, + "error" => log::LevelFilter::Error, + "warn" => log::LevelFilter::Warn, + "info" => log::LevelFilter::Info, + "debug" => log::LevelFilter::Debug, + "trace" => log::LevelFilter::Trace, + _ => log::LevelFilter::Info, + }); tui_logger::set_default_level(default_level); Ok(())