mirror of
https://github.com/kdheepak/taskwarrior-tui.git
synced 2025-08-24 23:46:41 +02:00
feat: Add taskwarriortuitask trait ✨
This commit is contained in:
parent
e09f460e64
commit
18a54f7f03
17 changed files with 462 additions and 167 deletions
|
@ -1,4 +1,4 @@
|
|||
max_width = 150
|
||||
max_width = 120
|
||||
tab_spaces = 2
|
||||
group_imports = "StdExternalCrate"
|
||||
imports_granularity = "Crate"
|
||||
|
|
5
build.rs
5
build.rs
|
@ -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()
|
||||
|
|
305
src/app.rs
305
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<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 {}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -100,7 +100,11 @@ impl CompletionList {
|
|||
state: ListState::default(),
|
||||
current: String::new(),
|
||||
pos: 0,
|
||||
helper: TaskwarriorTuiCompletionHelper { candidates, context, input },
|
||||
helper: TaskwarriorTuiCompletionHelper {
|
||||
candidates,
|
||||
context,
|
||||
input,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
11
src/main.rs
11
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.")
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -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<()> {
|
||||
|
|
18
src/table.rs
18
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())
|
||||
|
|
|
@ -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
24
src/traits.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
34
src/utils.rs
34
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<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,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(())
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue