feat: Add taskwarriortuitask trait

This commit is contained in:
Dheepak Krishnamurthy 2023-09-05 22:44:33 -04:00
parent e09f460e64
commit 18a54f7f03
17 changed files with 462 additions and 167 deletions

View file

@ -1,4 +1,4 @@
max_width = 150
max_width = 120
tab_spaces = 2
group_imports = "StdExternalCrate"
imports_granularity = "Crate"

View file

@ -19,7 +19,10 @@ fn run_pandoc() -> Result<Output, std::io::Error> {
}
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()

View file

@ -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<Option<Action>> {
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::<usize>() 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::<Vec<String>>().join(" ")
task_uuids
.iter()
.map(ToString::to_string)
.collect::<Vec<String>>()
.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::<Vec<String>>().join(" "),
task_uuids
.iter()
.map(ToString::to_string)
.collect::<Vec<String>>()
.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<String, String> {
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::<Vec<String>>().join(" ")
task_uuids
.iter()
.map(ToString::to_string)
.collect::<Vec<String>>()
.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::<Vec<String>>().join(" ")
task_uuids
.iter()
.map(ToString::to_string)
.collect::<Vec<String>>()
.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<task_uuid>[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<task_uuid>[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 {}

View file

@ -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::<Vec<_>>(),
);
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);

View file

@ -100,7 +100,11 @@ impl CompletionList {
state: ListState::default(),
current: String::new(),
pos: 0,
helper: TaskwarriorTuiCompletionHelper { candidates, context, input },
helper: TaskwarriorTuiCompletionHelper {
candidates,
context,
input,
},
}
}

View file

@ -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};

View file

@ -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;
}

View file

@ -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 {

View file

@ -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("<a><b>");
assert_eq!(
@ -217,11 +221,17 @@ mod tests {
]
);
let result = parse_key_sequence("<Ctrl-a>");
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("<Ctrl-Alt-a>");
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("<Ctrl-a").is_err());
@ -229,11 +239,20 @@ mod tests {
#[test]
fn test_simple_keys() {
assert_eq!(parse_key_event("a").unwrap(), KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()));
assert_eq!(
parse_key_event("a").unwrap(),
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty())
);
assert_eq!(parse_key_event("enter").unwrap(), KeyEvent::new(KeyCode::Enter, KeyModifiers::empty()));
assert_eq!(
parse_key_event("enter").unwrap(),
KeyEvent::new(KeyCode::Enter, KeyModifiers::empty())
);
assert_eq!(parse_key_event("esc").unwrap(), KeyEvent::new(KeyCode::Esc, KeyModifiers::empty()));
assert_eq!(
parse_key_event("esc").unwrap(),
KeyEvent::new(KeyCode::Esc, KeyModifiers::empty())
);
}
#[test]
@ -243,9 +262,15 @@ mod tests {
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL)
);
assert_eq!(parse_key_event("alt-enter").unwrap(), KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT));
assert_eq!(
parse_key_event("alt-enter").unwrap(),
KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT)
);
assert_eq!(parse_key_event("shift-esc").unwrap(), KeyEvent::new(KeyCode::Esc, KeyModifiers::SHIFT));
assert_eq!(
parse_key_event("shift-esc").unwrap(),
KeyEvent::new(KeyCode::Esc, KeyModifiers::SHIFT)
);
}
#[test]
@ -274,6 +299,9 @@ mod tests {
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL)
);
assert_eq!(parse_key_event("AlT-eNtEr").unwrap(), KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT));
assert_eq!(
parse_key_event("AlT-eNtEr").unwrap(),
KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT)
);
}
}

View file

@ -1,14 +1,21 @@
use std::ops::{Deref, DerefMut};
use std::{collections::HashMap, error::Error, str};
use std::{
collections::HashMap,
error::Error,
ops::{Deref, DerefMut},
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, SerializeMap, Serializer};
use serde::{
de::{self, Deserialize, Deserializer, MapAccess, Visitor},
ser::{self, Serialize, SerializeMap, Serializer},
};
use crate::keyevent::key_event_to_string;
use crate::{action::Action, keyevent::parse_key_sequence};
use crate::{
action::Action,
keyevent::{key_event_to_string, parse_key_sequence},
};
#[derive(Clone, Debug, Default)]
pub struct KeyMap(pub std::collections::HashMap<Vec<KeyEvent>, 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);

View file

@ -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.")
}

View file

@ -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(())

View file

@ -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::<Vec<&str>>();
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<()> {

View file

@ -445,7 +445,8 @@ 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 {
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())
@ -454,7 +455,8 @@ where
}
}
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())

View file

@ -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::<Vec<_>>().join(","),
Some(v) => v
.iter()
.filter(|t| !self.virtual_tags.contains(t))
.cloned()
.collect::<Vec<_>>()
.join(","),
None => "".to_string(),
},
"recur" => match task.recur() {

24
src/traits.rs Normal file
View file

@ -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);
}
}
}
}

View file

@ -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<PathBuf> = std::env::var(format!("{}_DATA", CRATE_NAME.clone())).ok().map(PathBuf::from);
pub static ref CONFIG_FOLDER: Option<PathBuf> = 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<PathBuf> = std::env::var(format!("{}_DATA", CRATE_NAME.clone()))
.ok()
.map(PathBuf::from);
pub static ref CONFIG_FOLDER: Option<PathBuf> = 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,7 +112,8 @@ 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() {
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,