mirror of
https://github.com/kdheepak/taskwarrior-tui.git
synced 2025-08-24 14:36:42 +02:00
Providing support for Projects Pane
Signed-off-by: lkadalski <kadalski.lukasz@gmail.com>
This commit is contained in:
parent
e587e36dd8
commit
75217bf266
14 changed files with 1620 additions and 1255 deletions
8
Cargo.lock
generated
8
Cargo.lock
generated
|
@ -1093,9 +1093,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.9"
|
||||
version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
|
||||
checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
@ -1356,9 +1356,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
|||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.74"
|
||||
version = "1.0.78"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c"
|
||||
checksum = "a4eac2e6c19f5c3abc0c229bea31ff0b9b091c7b14990e8924b92902a303a0c0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
|
@ -14,6 +14,8 @@ A Terminal User Interface for [Taskwarrior](https://taskwarrior.org/).
|
|||
|
||||
Showcase of features: <https://youtu.be/0ZdkfNrIAcw>
|
||||
|
||||
[](https://www.youtube.com/watch?v=0ZdkfNrIAcw)
|
||||
|
||||
**User Interface**
|
||||
|
||||

|
||||
|
|
5
build.rs
5
build.rs
|
@ -1,5 +1,8 @@
|
|||
#![allow(dead_code)]
|
||||
use clap_generate::{generate_to, generators::*};
|
||||
use clap_generate::{
|
||||
generate_to,
|
||||
generators::{Bash, Fish, PowerShell, Zsh},
|
||||
};
|
||||
|
||||
include!("src/cli.rs");
|
||||
|
||||
|
|
2287
src/app.rs
2287
src/app.rs
File diff suppressed because it is too large
Load diff
|
@ -35,12 +35,12 @@ impl<'a> Default for Calendar<'a> {
|
|||
let month = Local::today().month();
|
||||
Calendar {
|
||||
block: None,
|
||||
style: Default::default(),
|
||||
style: Style::default(),
|
||||
months_per_row: 0,
|
||||
year,
|
||||
month,
|
||||
date_style: vec![],
|
||||
today_style: Default::default(),
|
||||
today_style: Style::default(),
|
||||
title_background_color: Color::Reset,
|
||||
}
|
||||
}
|
||||
|
@ -88,20 +88,7 @@ impl<'a> Calendar<'a> {
|
|||
|
||||
impl<'a> Widget for Calendar<'a> {
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer) {
|
||||
let month_names = [
|
||||
Month::January.name(),
|
||||
Month::February.name(),
|
||||
Month::March.name(),
|
||||
Month::April.name(),
|
||||
Month::May.name(),
|
||||
Month::June.name(),
|
||||
Month::July.name(),
|
||||
Month::August.name(),
|
||||
Month::September.name(),
|
||||
Month::October.name(),
|
||||
Month::November.name(),
|
||||
Month::December.name(),
|
||||
];
|
||||
let month_names = Self::generate_month_names();
|
||||
buf.set_style(area, self.style);
|
||||
|
||||
let area = match self.block.take() {
|
||||
|
@ -131,12 +118,12 @@ impl<'a> Widget for Calendar<'a> {
|
|||
let first = NaiveDate::from_ymd(year, i + 1, 1);
|
||||
(
|
||||
first,
|
||||
first - Duration::days(first.weekday().num_days_from_sunday() as i64),
|
||||
first - Duration::days(i64::from(first.weekday().num_days_from_sunday())),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut startm = 0_usize;
|
||||
let mut start_m = 0_usize;
|
||||
if self.months_per_row > area.width as usize / 8 / 3 || self.months_per_row == 0 {
|
||||
self.months_per_row = area.width as usize / 8 / 3;
|
||||
}
|
||||
|
@ -146,27 +133,27 @@ impl<'a> Widget for Calendar<'a> {
|
|||
let x = area.x;
|
||||
let s = format!("{year:^width$}", year = year, width = area.width as usize);
|
||||
|
||||
let mut year = 0;
|
||||
let mut new_year = 0;
|
||||
let style = Style::default().add_modifier(Modifier::UNDERLINED);
|
||||
if self.year + year as i32 == today.year() {
|
||||
if self.year + new_year as i32 == today.year() {
|
||||
buf.set_string(x, y, &s, self.today_style.add_modifier(Modifier::UNDERLINED));
|
||||
} else {
|
||||
buf.set_string(x, y, &s, style);
|
||||
}
|
||||
|
||||
let startx = (area.width - 3 * 7 * self.months_per_row as u16 - self.months_per_row as u16) / 2;
|
||||
let start_x = (area.width - 3 * 7 * self.months_per_row as u16 - self.months_per_row as u16) / 2;
|
||||
y += 2;
|
||||
loop {
|
||||
let endm = std::cmp::min(startm + self.months_per_row, 12);
|
||||
let mut x = area.x + startx;
|
||||
for (c, d) in days.iter_mut().enumerate().take(endm).skip(startm) {
|
||||
if c > startm {
|
||||
let endm = std::cmp::min(start_m + self.months_per_row, 12);
|
||||
let mut x = area.x + start_x;
|
||||
for (c, d) in days.iter_mut().enumerate().take(endm).skip(start_m) {
|
||||
if c > start_m {
|
||||
x += 1;
|
||||
}
|
||||
let m = d.0.month() as usize;
|
||||
let s = format!("{:^20}", month_names[m - 1]);
|
||||
let style = Style::default().bg(self.title_background_color);
|
||||
if m == today.month() as usize && self.year + year as i32 == today.year() {
|
||||
if m == today.month() as usize && self.year + new_year as i32 == today.year() {
|
||||
buf.set_string(x, y, &s, self.today_style);
|
||||
} else {
|
||||
buf.set_string(x, y, &s, style);
|
||||
|
@ -174,8 +161,8 @@ impl<'a> Widget for Calendar<'a> {
|
|||
x += s.len() as u16 + 1;
|
||||
}
|
||||
y += 1;
|
||||
let mut x = area.x + startx;
|
||||
for d in days.iter_mut().take(endm).skip(startm) {
|
||||
let mut x = area.x + start_x;
|
||||
for d in days.iter_mut().take(endm).skip(start_m) {
|
||||
let m = d.0.month() as usize;
|
||||
let style = Style::default().bg(self.title_background_color);
|
||||
buf.set_string(
|
||||
|
@ -189,12 +176,12 @@ impl<'a> Widget for Calendar<'a> {
|
|||
y += 1;
|
||||
loop {
|
||||
let mut moredays = false;
|
||||
let mut x = area.x + startx;
|
||||
for c in startm..endm {
|
||||
if c > startm {
|
||||
let mut x = area.x + start_x;
|
||||
for c in start_m..endm {
|
||||
if c > start_m {
|
||||
x += 1;
|
||||
}
|
||||
let d = &mut days[c + year * 12];
|
||||
let d = &mut days[c + new_year * 12];
|
||||
for _ in 0..7 {
|
||||
let s = if d.0.month() == d.1.month() {
|
||||
format!("{:>2}", d.1.day())
|
||||
|
@ -204,7 +191,7 @@ impl<'a> Widget for Calendar<'a> {
|
|||
let mut style = Style::default();
|
||||
let index = self.date_style.iter().position(|(date, style)| d.1 == *date);
|
||||
if let Some(i) = index {
|
||||
style = self.date_style[i].1
|
||||
style = self.date_style[i].1;
|
||||
}
|
||||
if d.1 == Local::today().naive_local() {
|
||||
buf.set_string(x, y, s, self.today_style);
|
||||
|
@ -221,21 +208,21 @@ impl<'a> Widget for Calendar<'a> {
|
|||
break;
|
||||
}
|
||||
}
|
||||
startm += self.months_per_row;
|
||||
start_m += self.months_per_row;
|
||||
y += 2;
|
||||
if y + 8 > area.height {
|
||||
break;
|
||||
} else if startm >= 12 {
|
||||
startm = 0;
|
||||
year += 1;
|
||||
} else if start_m >= 12 {
|
||||
start_m = 0;
|
||||
new_year += 1;
|
||||
days.append(
|
||||
&mut months
|
||||
.iter()
|
||||
.map(|i| {
|
||||
let first = NaiveDate::from_ymd(self.year + year as i32, i + 1, 1);
|
||||
let first = NaiveDate::from_ymd(self.year + new_year as i32, i + 1, 1);
|
||||
(
|
||||
first,
|
||||
first - Duration::days(first.weekday().num_days_from_sunday() as i64),
|
||||
first - Duration::days(i64::from(first.weekday().num_days_from_sunday())),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
|
@ -244,12 +231,12 @@ impl<'a> Widget for Calendar<'a> {
|
|||
let x = area.x;
|
||||
let s = format!(
|
||||
"{year:^width$}",
|
||||
year = self.year as usize + year,
|
||||
year = self.year as usize + new_year,
|
||||
width = area.width as usize
|
||||
);
|
||||
let mut style = Style::default().add_modifier(Modifier::UNDERLINED);
|
||||
if self.year + year as i32 == today.year() {
|
||||
style = style.add_modifier(Modifier::BOLD)
|
||||
if self.year + new_year as i32 == today.year() {
|
||||
style = style.add_modifier(Modifier::BOLD);
|
||||
}
|
||||
buf.set_string(x, y, &s, style);
|
||||
y += 1;
|
||||
|
@ -258,3 +245,23 @@ impl<'a> Widget for Calendar<'a> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Calendar<'a> {
|
||||
fn generate_month_names() -> [&'a str; 12] {
|
||||
let month_names = [
|
||||
Month::January.name(),
|
||||
Month::February.name(),
|
||||
Month::March.name(),
|
||||
Month::April.name(),
|
||||
Month::May.name(),
|
||||
Month::June.name(),
|
||||
Month::July.name(),
|
||||
Month::August.name(),
|
||||
Month::September.name(),
|
||||
Month::October.name(),
|
||||
Month::November.name(),
|
||||
Month::December.name(),
|
||||
];
|
||||
month_names
|
||||
}
|
||||
}
|
||||
|
|
|
@ -157,11 +157,7 @@ impl CompletionList {
|
|||
}
|
||||
|
||||
pub fn selected(&self) -> Option<String> {
|
||||
if let Some(i) = self.state.selected() {
|
||||
self.get(i)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
self.state.selected().and_then(|i| self.get(i))
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
|
|
|
@ -74,8 +74,8 @@ impl Config {
|
|||
let bool_collection = Self::get_bool_collection();
|
||||
|
||||
let enabled = true;
|
||||
let obfuscate = bool_collection.get("obfuscate").cloned().unwrap_or(false);
|
||||
let print_empty_columns = bool_collection.get("print_empty_columns").cloned().unwrap_or(false);
|
||||
let obfuscate = bool_collection.get("obfuscate").copied().unwrap_or(false);
|
||||
let print_empty_columns = bool_collection.get("print_empty_columns").copied().unwrap_or(false);
|
||||
|
||||
let color = Self::get_color_collection(data);
|
||||
let filter = Self::get_filter(data, report)?;
|
||||
|
@ -384,14 +384,14 @@ impl Config {
|
|||
let data = Self::get_config("rule.precedence.color", data)
|
||||
.context("Unable to parse `task show rule.precedence.color`.")
|
||||
.unwrap();
|
||||
data.split(',').map(|s| s.to_string()).collect::<Vec<_>>()
|
||||
data.split(',').map(ToString::to_string).collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
fn get_uda_priority_values(data: &str) -> Vec<String> {
|
||||
let data = Self::get_config("uda.priority.values", data)
|
||||
.context("Unable to parse `task show uda.priority.values`.")
|
||||
.unwrap();
|
||||
data.split(',').map(|s| s.to_string()).collect::<Vec<_>>()
|
||||
data.split(',').map(ToString::to_string).collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
fn get_filter(data: &str, report: &str) -> Result<String> {
|
||||
|
@ -471,7 +471,7 @@ impl Config {
|
|||
fn get_uda_selection_indicator(data: &str) -> String {
|
||||
let indicator = Self::get_config("uda.taskwarrior-tui.selection.indicator", data);
|
||||
match indicator {
|
||||
None => "• ".to_string(),
|
||||
None => "\u{2022} ".to_string(),
|
||||
Some(indicator) => format!("{} ", indicator),
|
||||
}
|
||||
}
|
||||
|
@ -479,7 +479,7 @@ impl Config {
|
|||
fn get_uda_mark_indicator(data: &str) -> String {
|
||||
let indicator = Self::get_config("uda.taskwarrior-tui.mark.indicator", data);
|
||||
match indicator {
|
||||
None => "✔ ".to_string(),
|
||||
None => "\u{2714} ".to_string(),
|
||||
Some(indicator) => format!("{} ", indicator),
|
||||
}
|
||||
}
|
||||
|
@ -495,7 +495,7 @@ impl Config {
|
|||
fn get_uda_mark_highlight_indicator(data: &str) -> String {
|
||||
let indicator = Self::get_config("uda.taskwarrior-tui.mark-selection.indicator", data);
|
||||
match indicator {
|
||||
None => "⦿ ".to_string(),
|
||||
None => "\u{29bf} ".to_string(),
|
||||
Some(indicator) => format!("{} ", indicator),
|
||||
}
|
||||
}
|
||||
|
@ -503,7 +503,7 @@ impl Config {
|
|||
fn get_uda_unmark_highlight_indicator(data: &str) -> String {
|
||||
let indicator = Self::get_config("uda.taskwarrior-tui.unmark-selection.indicator", data);
|
||||
match indicator {
|
||||
None => "⦾ ".to_string(),
|
||||
None => "\u{29be} ".to_string(),
|
||||
Some(indicator) => format!("{} ", indicator),
|
||||
}
|
||||
}
|
||||
|
|
14
src/event.rs
14
src/event.rs
|
@ -17,7 +17,7 @@ use std::time::{Duration, Instant};
|
|||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, PartialOrd)]
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, PartialOrd, Eq)]
|
||||
pub enum Key {
|
||||
CtrlBackspace,
|
||||
CtrlDelete,
|
||||
|
@ -61,7 +61,13 @@ pub struct Events {
|
|||
|
||||
impl Events {
|
||||
pub fn with_config(config: EventConfig) -> Events {
|
||||
use crossterm::event::{KeyCode::*, KeyModifiers};
|
||||
use crossterm::event::{
|
||||
KeyCode::{
|
||||
BackTab, Backspace, Char, Delete, Down, End, Enter, Esc, Home, Insert, Left, Null, PageDown, PageUp,
|
||||
Right, Tab, Up, F,
|
||||
},
|
||||
KeyModifiers,
|
||||
};
|
||||
let tick_rate = config.tick_rate;
|
||||
let (tx, rx) = unbounded::<Event<Key>>();
|
||||
task::spawn_local(async move {
|
||||
|
@ -131,13 +137,13 @@ impl Events {
|
|||
self.rx.recv().await
|
||||
}
|
||||
|
||||
pub fn leave_tui_mode(&self, terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) {
|
||||
pub fn leave_tui_mode(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) {
|
||||
disable_raw_mode().unwrap();
|
||||
execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture).unwrap();
|
||||
terminal.show_cursor().unwrap();
|
||||
}
|
||||
|
||||
pub fn enter_tui_mode(&self, terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) {
|
||||
pub fn enter_tui_mode(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) {
|
||||
execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture).unwrap();
|
||||
enable_raw_mode().unwrap();
|
||||
terminal.resize(terminal.size().unwrap()).unwrap();
|
||||
|
|
|
@ -97,31 +97,31 @@ impl KeyConfig {
|
|||
}
|
||||
|
||||
pub fn update(&mut self, data: &str) -> Result<()> {
|
||||
let quit = self.get_config("uda.taskwarrior-tui.keyconfig.quit", data);
|
||||
let refresh = self.get_config("uda.taskwarrior-tui.keyconfig.refresh", data);
|
||||
let go_to_bottom = self.get_config("uda.taskwarrior-tui.keyconfig.go-to-bottom", data);
|
||||
let go_to_top = self.get_config("uda.taskwarrior-tui.keyconfig.go-to-top", data);
|
||||
let down = self.get_config("uda.taskwarrior-tui.keyconfig.down", data);
|
||||
let up = self.get_config("uda.taskwarrior-tui.keyconfig.up", data);
|
||||
let page_down = self.get_config("uda.taskwarrior-tui.keyconfig.page-down", data);
|
||||
let page_up = self.get_config("uda.taskwarrior-tui.keyconfig.page-up", data);
|
||||
let delete = self.get_config("uda.taskwarrior-tui.keyconfig.delete", data);
|
||||
let done = self.get_config("uda.taskwarrior-tui.keyconfig.done", data);
|
||||
let start_stop = self.get_config("uda.taskwarrior-tui.keyconfig.start-stop", data);
|
||||
let select = self.get_config("uda.taskwarrior-tui.keyconfig.select", data);
|
||||
let select_all = self.get_config("uda.taskwarrior-tui.keyconfig.select-all", data);
|
||||
let undo = self.get_config("uda.taskwarrior-tui.keyconfig.undo", data);
|
||||
let edit = self.get_config("uda.taskwarrior-tui.keyconfig.edit", data);
|
||||
let modify = self.get_config("uda.taskwarrior-tui.keyconfig.modify", data);
|
||||
let shell = self.get_config("uda.taskwarrior-tui.keyconfig.shell", data);
|
||||
let log = self.get_config("uda.taskwarrior-tui.keyconfig.log", data);
|
||||
let add = self.get_config("uda.taskwarrior-tui.keyconfig.add", data);
|
||||
let annotate = self.get_config("uda.taskwarrior-tui.keyconfig.annotate", data);
|
||||
let filter = self.get_config("uda.taskwarrior-tui.keyconfig.filter", data);
|
||||
let zoom = self.get_config("uda.taskwarrior-tui.keyconfig.zoom", data);
|
||||
let context_menu = self.get_config("uda.taskwarrior-tui.keyconfig.context-menu", data);
|
||||
let next_tab = self.get_config("uda.taskwarrior-tui.keyconfig.next-tab", data);
|
||||
let previous_tab = self.get_config("uda.taskwarrior-tui.keyconfig.previous-tab", data);
|
||||
let quit = Self::get_config("uda.taskwarrior-tui.keyconfig.quit", data);
|
||||
let refresh = Self::get_config("uda.taskwarrior-tui.keyconfig.refresh", data);
|
||||
let go_to_bottom = Self::get_config("uda.taskwarrior-tui.keyconfig.go-to-bottom", data);
|
||||
let go_to_top = Self::get_config("uda.taskwarrior-tui.keyconfig.go-to-top", data);
|
||||
let down = Self::get_config("uda.taskwarrior-tui.keyconfig.down", data);
|
||||
let up = Self::get_config("uda.taskwarrior-tui.keyconfig.up", data);
|
||||
let page_down = Self::get_config("uda.taskwarrior-tui.keyconfig.page-down", data);
|
||||
let page_up = Self::get_config("uda.taskwarrior-tui.keyconfig.page-up", data);
|
||||
let delete = Self::get_config("uda.taskwarrior-tui.keyconfig.delete", data);
|
||||
let done = Self::get_config("uda.taskwarrior-tui.keyconfig.done", data);
|
||||
let start_stop = Self::get_config("uda.taskwarrior-tui.keyconfig.start-stop", data);
|
||||
let select = Self::get_config("uda.taskwarrior-tui.keyconfig.select", data);
|
||||
let select_all = Self::get_config("uda.taskwarrior-tui.keyconfig.select-all", data);
|
||||
let undo = Self::get_config("uda.taskwarrior-tui.keyconfig.undo", data);
|
||||
let edit = Self::get_config("uda.taskwarrior-tui.keyconfig.edit", data);
|
||||
let modify = Self::get_config("uda.taskwarrior-tui.keyconfig.modify", data);
|
||||
let shell = Self::get_config("uda.taskwarrior-tui.keyconfig.shell", data);
|
||||
let log = Self::get_config("uda.taskwarrior-tui.keyconfig.log", data);
|
||||
let add = Self::get_config("uda.taskwarrior-tui.keyconfig.add", data);
|
||||
let annotate = Self::get_config("uda.taskwarrior-tui.keyconfig.annotate", data);
|
||||
let filter = Self::get_config("uda.taskwarrior-tui.keyconfig.filter", data);
|
||||
let zoom = Self::get_config("uda.taskwarrior-tui.keyconfig.zoom", data);
|
||||
let context_menu = Self::get_config("uda.taskwarrior-tui.keyconfig.context-menu", data);
|
||||
let next_tab = Self::get_config("uda.taskwarrior-tui.keyconfig.next-tab", data);
|
||||
let previous_tab = Self::get_config("uda.taskwarrior-tui.keyconfig.previous-tab", data);
|
||||
|
||||
self.quit = quit.unwrap_or(self.quit);
|
||||
self.refresh = refresh.unwrap_or(self.refresh);
|
||||
|
@ -190,14 +190,12 @@ impl KeyConfig {
|
|||
}
|
||||
}
|
||||
|
||||
fn get_config(&self, config: &str, data: &str) -> Option<Key> {
|
||||
fn get_config(config: &str, data: &str) -> Option<Key> {
|
||||
for line in data.split('\n') {
|
||||
if line.starts_with(config) {
|
||||
let line = line.trim_start_matches(config).trim_start().trim_end().to_string();
|
||||
if line.len() == 1 {
|
||||
return Some(Key::Char(line.chars().next().unwrap()));
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
} else if line.starts_with(&config.replace('-', "_")) {
|
||||
let line = line
|
||||
|
@ -207,8 +205,6 @@ impl KeyConfig {
|
|||
.to_string();
|
||||
if line.len() == 1 {
|
||||
return Some(Key::Char(line.chars().next().unwrap()));
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
68
src/main.rs
68
src/main.rs
|
@ -2,6 +2,31 @@
|
|||
#![allow(unused_imports)]
|
||||
#![allow(unused_variables)]
|
||||
|
||||
use std::env;
|
||||
use std::error::Error;
|
||||
use std::io::{self, Write};
|
||||
use std::panic;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use async_std::prelude::*;
|
||||
use async_std::sync::{Arc, Mutex};
|
||||
use async_std::task;
|
||||
use crossterm::{
|
||||
cursor,
|
||||
event::{DisableMouseCapture, EnableMouseCapture, EventStream},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use tui::{backend::CrosstermBackend, Terminal};
|
||||
|
||||
use app::{Mode, TaskwarriorTui};
|
||||
|
||||
use crate::app::Action;
|
||||
use crate::event::{Event, EventConfig, Events, Key};
|
||||
use crate::keyconfig::KeyConfig;
|
||||
|
||||
mod app;
|
||||
mod calendar;
|
||||
mod cli;
|
||||
|
@ -12,41 +37,22 @@ mod event;
|
|||
mod help;
|
||||
mod history;
|
||||
mod keyconfig;
|
||||
mod pane;
|
||||
mod table;
|
||||
mod task_report;
|
||||
|
||||
use crate::event::{Event, EventConfig, Events, Key};
|
||||
use anyhow::Result;
|
||||
use std::env;
|
||||
use std::error::Error;
|
||||
use std::io::{self, Write};
|
||||
use std::panic;
|
||||
use std::time::Duration;
|
||||
|
||||
use async_std::prelude::*;
|
||||
use async_std::sync::{Arc, Mutex};
|
||||
use async_std::task;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
|
||||
use crossterm::{
|
||||
cursor,
|
||||
event::{DisableMouseCapture, EnableMouseCapture, EventStream},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use tui::{backend::CrosstermBackend, Terminal};
|
||||
|
||||
use app::{AppMode, TaskwarriorTuiApp};
|
||||
|
||||
/// # Panics
|
||||
/// Will panic if could not obtain terminal
|
||||
pub fn setup_terminal() -> Terminal<CrosstermBackend<io::Stdout>> {
|
||||
enable_raw_mode().unwrap();
|
||||
enable_raw_mode().expect("Running not in terminal");
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen).unwrap();
|
||||
execute!(stdout, Clear(ClearType::All)).unwrap();
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
Terminal::new(backend).unwrap()
|
||||
}
|
||||
|
||||
/// # Panics
|
||||
/// Will panic if could not `disable_raw_mode`
|
||||
pub fn destruct_terminal() {
|
||||
disable_raw_mode().unwrap();
|
||||
execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture).unwrap();
|
||||
|
@ -72,7 +78,7 @@ async fn tui_main(_config: &str, report: &str) -> Result<()> {
|
|||
better_panic::Settings::auto().create_panic_handler()(panic_info);
|
||||
}));
|
||||
|
||||
let maybeapp = TaskwarriorTuiApp::new(report);
|
||||
let maybeapp = TaskwarriorTui::new(report);
|
||||
if maybeapp.is_err() {
|
||||
destruct_terminal();
|
||||
return Err(maybeapp.err().unwrap());
|
||||
|
@ -103,9 +109,9 @@ async fn tui_main(_config: &str, report: &str) -> Result<()> {
|
|||
|| input == app.keyconfig.shortcut7
|
||||
|| input == app.keyconfig.shortcut8
|
||||
|| input == app.keyconfig.shortcut9)
|
||||
&& app.mode == AppMode::TaskReport
|
||||
&& app.mode == Mode::Tasks(Action::Report)
|
||||
{
|
||||
events.leave_tui_mode(&mut terminal);
|
||||
Events::leave_tui_mode(&mut terminal);
|
||||
}
|
||||
|
||||
let r = app.handle_input(input);
|
||||
|
@ -120,10 +126,10 @@ async fn tui_main(_config: &str, report: &str) -> Result<()> {
|
|||
|| input == app.keyconfig.shortcut7
|
||||
|| input == app.keyconfig.shortcut8
|
||||
|| input == app.keyconfig.shortcut9)
|
||||
&& app.mode == AppMode::TaskReport
|
||||
|| app.mode == AppMode::TaskError
|
||||
&& app.mode == Mode::Tasks(Action::Report)
|
||||
|| app.mode == Mode::Tasks(Action::Error)
|
||||
{
|
||||
events.enter_tui_mode(&mut terminal);
|
||||
Events::enter_tui_mode(&mut terminal);
|
||||
}
|
||||
if r.is_err() {
|
||||
destruct_terminal();
|
||||
|
|
28
src/pane/mod.rs
Normal file
28
src/pane/mod.rs
Normal file
|
@ -0,0 +1,28 @@
|
|||
use anyhow::Result;
|
||||
|
||||
use crate::app::{Action, Mode, TaskwarriorTui};
|
||||
use crate::event::Key;
|
||||
use clap::App;
|
||||
use std::ops::Index;
|
||||
|
||||
pub mod project;
|
||||
|
||||
pub trait Pane {
|
||||
fn handle_input(app: &mut TaskwarriorTui, input: Key) -> Result<()>;
|
||||
fn change_focus_to_left_pane(app: &mut TaskwarriorTui) {
|
||||
match app.mode {
|
||||
Mode::Tasks(_) => {}
|
||||
Mode::Projects => app.mode = Mode::Tasks(Action::Report),
|
||||
Mode::Calendar => {
|
||||
app.mode = Mode::Projects;
|
||||
}
|
||||
}
|
||||
}
|
||||
fn change_focus_to_right_pane(app: &mut TaskwarriorTui) {
|
||||
match app.mode {
|
||||
Mode::Tasks(_) => app.mode = Mode::Projects,
|
||||
Mode::Projects => app.mode = Mode::Calendar,
|
||||
Mode::Calendar => {}
|
||||
}
|
||||
}
|
||||
}
|
203
src/pane/project.rs
Normal file
203
src/pane/project.rs
Normal file
|
@ -0,0 +1,203 @@
|
|||
// Based on https://gist.github.com/diwic/5c20a283ca3a03752e1a27b0f3ebfa30
|
||||
// See https://old.reddit.com/r/rust/comments/4xneq5/the_calendar_example_challenge_ii_why_eddyb_all/
|
||||
|
||||
use anyhow::Context as AnyhowContext;
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::fmt;
|
||||
|
||||
const COL_WIDTH: usize = 21;
|
||||
const PROJECT_HEADER: &str = "Name";
|
||||
const REMAINING_TASK_HEADER: &str = "Remaining";
|
||||
const AVG_AGE_HEADER: &str = "Avg age";
|
||||
const COMPLETE_HEADER: &str = "Complete";
|
||||
|
||||
use chrono::{Datelike, Duration, Local, Month, NaiveDate, NaiveDateTime, TimeZone};
|
||||
|
||||
use tui::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::{Color, Modifier, Style},
|
||||
symbols,
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
|
||||
use crate::app::{Action, Mode, TaskwarriorTui};
|
||||
use crate::event::Key;
|
||||
use crate::pane::Pane;
|
||||
use crate::table::TableState;
|
||||
use itertools::Itertools;
|
||||
use std::cmp::min;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::error::Error;
|
||||
use std::process::{Command, Output};
|
||||
use task_hookrs::project::Project;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct ProjectsState {
|
||||
pub(crate) list: Vec<Project>,
|
||||
pub table_state: TableState,
|
||||
pub current_selection: usize,
|
||||
pub marked: HashSet<Project>,
|
||||
pub report_height: u16,
|
||||
pub columns: Vec<String>,
|
||||
pub rows: Vec<ProjectDetails>,
|
||||
}
|
||||
|
||||
pub struct ProjectDetails {
|
||||
name: Project,
|
||||
remaining: usize,
|
||||
avg_age: String,
|
||||
complete: String,
|
||||
}
|
||||
impl ProjectsState {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
list: Vec::default(),
|
||||
table_state: TableState::default(),
|
||||
current_selection: 0,
|
||||
marked: HashSet::default(),
|
||||
report_height: 0,
|
||||
columns: vec![
|
||||
PROJECT_HEADER.to_string(),
|
||||
REMAINING_TASK_HEADER.to_string(),
|
||||
AVG_AGE_HEADER.to_string(),
|
||||
COMPLETE_HEADER.to_string(),
|
||||
],
|
||||
rows: vec![],
|
||||
}
|
||||
}
|
||||
fn pattern_by_marked(app: &mut TaskwarriorTui) -> String {
|
||||
let mut project_pattern = String::new();
|
||||
if !app.projects.marked.is_empty() {
|
||||
for (idx, project) in app.projects.marked.clone().iter().enumerate() {
|
||||
let mut input: String = String::from(project);
|
||||
if input.as_str() == "(none)" {
|
||||
input = " ".to_string();
|
||||
}
|
||||
if idx > 0 {
|
||||
project_pattern = format!("{} or project:{}", project_pattern, input);
|
||||
} else {
|
||||
project_pattern = format!("\'(project:{}", input);
|
||||
}
|
||||
}
|
||||
project_pattern = format!("{})\'", project_pattern);
|
||||
}
|
||||
project_pattern
|
||||
}
|
||||
pub fn toggle_mark(&mut self) {
|
||||
if !self.list.is_empty() {
|
||||
let selected = self.current_selection;
|
||||
if !self.marked.insert(self.list[selected].clone()) {
|
||||
self.marked.remove(self.list[selected].as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn simplified_view(&mut self) -> (Vec<Vec<String>>, Vec<String>) {
|
||||
let rows = self
|
||||
.rows
|
||||
.iter()
|
||||
.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)
|
||||
}
|
||||
|
||||
pub fn update_data(&mut self) -> Result<()> {
|
||||
self.list.clear();
|
||||
self.rows.clear();
|
||||
let output = Command::new("task")
|
||||
.arg("summary")
|
||||
.output()
|
||||
.context("Unable to run `task summary`")
|
||||
.unwrap();
|
||||
let data = String::from_utf8_lossy(&output.stdout);
|
||||
for line in data.split('\n').into_iter().skip(3) {
|
||||
if line.is_empty() {
|
||||
break;
|
||||
}
|
||||
let row: Vec<String> = line
|
||||
.split(' ')
|
||||
.map(str::trim)
|
||||
.map(str::trim_start)
|
||||
.filter(|x| !x.is_empty())
|
||||
.map(ToString::to_string)
|
||||
.collect();
|
||||
|
||||
self.rows.push(ProjectDetails {
|
||||
name: (&row[0]).parse()?,
|
||||
remaining: (&row[1]).parse()?,
|
||||
avg_age: (&row[2]).parse()?,
|
||||
complete: (&row[3]).parse()?,
|
||||
});
|
||||
}
|
||||
self.list = self.rows.iter().map(|x| x.name.clone()).collect_vec();
|
||||
Ok(())
|
||||
}
|
||||
fn update_table_state(&mut self) {
|
||||
self.table_state.select(Some(self.current_selection));
|
||||
if self.marked.is_empty() {
|
||||
self.table_state.single_selection();
|
||||
}
|
||||
self.table_state.clear();
|
||||
for project in &self.marked {
|
||||
let index = self.list.iter().position(|x| x == project);
|
||||
self.table_state.mark(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Pane for ProjectsState {
|
||||
fn handle_input(app: &mut TaskwarriorTui, input: Key) -> Result<()> {
|
||||
if input == app.keyconfig.quit || input == Key::Ctrl('c') {
|
||||
app.should_quit = true;
|
||||
} else if input == app.keyconfig.next_tab {
|
||||
Self::change_focus_to_right_pane(app);
|
||||
} else if input == app.keyconfig.previous_tab {
|
||||
Self::change_focus_to_left_pane(app);
|
||||
} else if input == Key::Down || input == app.keyconfig.down {
|
||||
self::focus_on_next_project(app);
|
||||
} else if input == Key::Up || input == app.keyconfig.up {
|
||||
self::focus_on_previous_project(app);
|
||||
} else if input == app.keyconfig.select {
|
||||
self::update_task_filter_by_selection(app)?;
|
||||
}
|
||||
app.projects.update_table_state();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_on_next_project(app: &mut TaskwarriorTui) {
|
||||
if app.projects.current_selection < app.projects.list.len() - 1 {
|
||||
app.projects.current_selection += 1;
|
||||
app.projects.table_state.select(Some(app.projects.current_selection));
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_on_previous_project(app: &mut TaskwarriorTui) {
|
||||
if app.projects.current_selection >= 1 {
|
||||
app.projects.current_selection -= 1;
|
||||
app.projects.table_state.select(Some(app.projects.current_selection));
|
||||
}
|
||||
}
|
||||
|
||||
fn update_task_filter_by_selection(app: &mut TaskwarriorTui) -> Result<()> {
|
||||
app.projects.table_state.multiple_selection();
|
||||
let last_project_pattern = ProjectsState::pattern_by_marked(app);
|
||||
app.projects.toggle_mark();
|
||||
let new_project_pattern = ProjectsState::pattern_by_marked(app);
|
||||
let current_filter = app.filter.as_str();
|
||||
app.filter_history_context.add(current_filter);
|
||||
|
||||
let mut filter = current_filter.replace(&last_project_pattern, "");
|
||||
filter = format!("{}{}", filter, new_project_pattern);
|
||||
app.filter.update(filter.as_str(), filter.len());
|
||||
app.update(true)?;
|
||||
Ok(())
|
||||
}
|
23
src/table.rs
23
src/table.rs
|
@ -1,6 +1,6 @@
|
|||
use cassowary::{
|
||||
strength::{MEDIUM, REQUIRED, WEAK},
|
||||
WeightedRelation::*,
|
||||
WeightedRelation::{EQ, GE, LE},
|
||||
{Expression, Solver},
|
||||
};
|
||||
use std::collections::HashSet;
|
||||
|
@ -340,7 +340,7 @@ where
|
|||
}
|
||||
Constraint::Min(v) => variables[i] | GE(WEAK) | f64::from(v),
|
||||
Constraint::Max(v) => variables[i] | LE(WEAK) | f64::from(v),
|
||||
})
|
||||
});
|
||||
}
|
||||
solver
|
||||
.add_constraint(
|
||||
|
@ -354,7 +354,7 @@ where
|
|||
for &(var, value) in solver.fetch_changes() {
|
||||
let index = var_indices[&var];
|
||||
let value = if value.is_sign_negative() { 0 } else { value as u16 };
|
||||
solved_widths[index] = value
|
||||
solved_widths[index] = value;
|
||||
}
|
||||
|
||||
let mut y = table_area.top();
|
||||
|
@ -399,7 +399,7 @@ where
|
|||
|
||||
let highlight_symbol = match state.mode {
|
||||
TableMode::MultipleSelection => {
|
||||
let s = self.highlight_symbol.unwrap_or("•").trim_end();
|
||||
let s = self.highlight_symbol.unwrap_or("\u{2022}").trim_end();
|
||||
format!("{} ", s)
|
||||
}
|
||||
TableMode::SingleSelection => self.highlight_symbol.unwrap_or("").to_string(),
|
||||
|
@ -407,7 +407,7 @@ where
|
|||
|
||||
let mark_symbol = match state.mode {
|
||||
TableMode::MultipleSelection => {
|
||||
let s = self.mark_symbol.unwrap_or("✔").trim_end();
|
||||
let s = self.mark_symbol.unwrap_or("\u{2714}").trim_end();
|
||||
format!("{} ", s)
|
||||
}
|
||||
TableMode::SingleSelection => self.highlight_symbol.unwrap_or("").to_string(),
|
||||
|
@ -422,12 +422,12 @@ where
|
|||
};
|
||||
|
||||
let mark_highlight_symbol = {
|
||||
let s = self.mark_highlight_symbol.unwrap_or("⦿").trim_end();
|
||||
let s = self.mark_highlight_symbol.unwrap_or("\u{29bf}").trim_end();
|
||||
format!("{} ", s)
|
||||
};
|
||||
|
||||
let unmark_highlight_symbol = {
|
||||
let s = self.unmark_highlight_symbol.unwrap_or("⦾").trim_end();
|
||||
let s = self.unmark_highlight_symbol.unwrap_or("\u{29be}").trim_end();
|
||||
format!("{} ", s)
|
||||
};
|
||||
|
||||
|
@ -437,7 +437,7 @@ where
|
|||
let remaining = (table_area.bottom() - y) as usize;
|
||||
|
||||
// Make sure the table shows the selected item
|
||||
state.offset = if let Some(s) = selected {
|
||||
state.offset = selected.map_or(0, |s| {
|
||||
if s >= remaining + state.offset - 1 {
|
||||
s + 1 - remaining
|
||||
} else if s < state.offset {
|
||||
|
@ -445,9 +445,7 @@ where
|
|||
} else {
|
||||
state.offset
|
||||
}
|
||||
} else {
|
||||
0
|
||||
};
|
||||
});
|
||||
for (i, row) in self.rows.skip(state.offset).take(remaining).enumerate() {
|
||||
let (data, style, symbol) = match row {
|
||||
Row::Data(d) | Row::StyledData(d, _)
|
||||
|
@ -491,8 +489,7 @@ where
|
|||
);
|
||||
if c == header_index {
|
||||
let symbol = match state.mode {
|
||||
TableMode::SingleSelection => &symbol,
|
||||
TableMode::MultipleSelection => &symbol,
|
||||
TableMode::SingleSelection | TableMode::MultipleSelection => &symbol,
|
||||
};
|
||||
format!(
|
||||
"{symbol}{elt:>width$}",
|
||||
|
|
|
@ -31,9 +31,8 @@ pub fn vague_format_date_time(from_dt: NaiveDateTime, to_dt: NaiveDateTime) -> S
|
|||
return format!("{}{}h", minus, seconds / 60 / 60);
|
||||
} else if seconds >= 60 {
|
||||
return format!("{}{}min", minus, seconds / 60);
|
||||
} else {
|
||||
return format!("{}{}s", minus, seconds);
|
||||
}
|
||||
return format!("{}{}s", minus, seconds);
|
||||
}
|
||||
|
||||
pub struct TaskReportTable {
|
||||
|
@ -85,7 +84,7 @@ impl TaskReportTable {
|
|||
labels: vec![],
|
||||
columns: vec![],
|
||||
tasks: vec![vec![]],
|
||||
virtual_tags: virtual_tags.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
|
||||
virtual_tags: virtual_tags.iter().map(ToString::to_string).collect::<Vec<_>>(),
|
||||
description_width: 100,
|
||||
};
|
||||
task_report_table.export_headers(Some(data), report)?;
|
||||
|
@ -96,15 +95,14 @@ impl TaskReportTable {
|
|||
self.columns = vec![];
|
||||
self.labels = vec![];
|
||||
|
||||
let data = match data {
|
||||
Some(s) => s.to_string(),
|
||||
None => {
|
||||
let output = Command::new("task")
|
||||
.arg("show")
|
||||
.arg(format!("report.{}.columns", report))
|
||||
.output()?;
|
||||
String::from_utf8_lossy(&output.stdout).into_owned()
|
||||
}
|
||||
let data = if let Some(s) = data {
|
||||
s.to_string()
|
||||
} else {
|
||||
let output = Command::new("task")
|
||||
.arg("show")
|
||||
.arg(format!("report.{}.columns", report))
|
||||
.output()?;
|
||||
String::from_utf8_lossy(&output.stdout).into_owned()
|
||||
};
|
||||
|
||||
for line in data.split('\n') {
|
||||
|
@ -132,7 +130,7 @@ impl TaskReportTable {
|
|||
}
|
||||
|
||||
if self.labels.is_empty() {
|
||||
for label in self.columns.iter() {
|
||||
for label in &self.columns {
|
||||
let label = label.split('.').collect::<Vec<&str>>()[0];
|
||||
let label = if label == "id" { "ID" } else { label };
|
||||
let mut c = label.chars();
|
||||
|
@ -162,18 +160,17 @@ impl TaskReportTable {
|
|||
let s = self.get_string_attribute(name, task, tasks);
|
||||
item.push(s);
|
||||
}
|
||||
self.tasks.push(item)
|
||||
self.tasks.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn simplify_table(&mut self) -> (Vec<Vec<String>>, Vec<String>) {
|
||||
// find which columns are empty
|
||||
let null_columns_len;
|
||||
if !self.tasks.is_empty() {
|
||||
null_columns_len = self.tasks[0].len();
|
||||
} else {
|
||||
if self.tasks.is_empty() {
|
||||
return (vec![], vec![]);
|
||||
}
|
||||
null_columns_len = self.tasks[0].len();
|
||||
|
||||
let mut null_columns = vec![0; null_columns_len];
|
||||
for task in &self.tasks {
|
||||
|
@ -190,7 +187,7 @@ impl TaskReportTable {
|
|||
.iter()
|
||||
.enumerate()
|
||||
.filter(|&(i, _)| null_columns[i] != 0)
|
||||
.map(|(_, e)| e.to_owned())
|
||||
.map(|(_, e)| e.clone())
|
||||
.collect();
|
||||
tasks.push(t);
|
||||
}
|
||||
|
@ -201,7 +198,7 @@ impl TaskReportTable {
|
|||
.iter()
|
||||
.enumerate()
|
||||
.filter(|&(i, _)| null_columns[i] != 0)
|
||||
.map(|(_, e)| e.to_owned())
|
||||
.map(|(_, e)| e.clone())
|
||||
.collect();
|
||||
|
||||
(tasks, headers)
|
||||
|
@ -214,11 +211,7 @@ impl TaskReportTable {
|
|||
Some(v) => vague_format_date_time(Local::now().naive_utc(), NaiveDateTime::new(v.date(), v.time())),
|
||||
None => "".to_string(),
|
||||
},
|
||||
"until" => match task.until() {
|
||||
Some(v) => vague_format_date_time(Local::now().naive_utc(), NaiveDateTime::new(v.date(), v.time())),
|
||||
None => "".to_string(),
|
||||
},
|
||||
"until.remaining" => match task.until() {
|
||||
"until" | "until.remaining" => match task.until() {
|
||||
Some(v) => vague_format_date_time(Local::now().naive_utc(), NaiveDateTime::new(v.date(), v.time())),
|
||||
None => "".to_string(),
|
||||
},
|
||||
|
@ -233,7 +226,7 @@ impl TaskReportTable {
|
|||
"status.short" => task.status().to_string().chars().next().unwrap().to_string(),
|
||||
"status" => task.status().to_string(),
|
||||
"priority" => match task.priority() {
|
||||
Some(p) => p.to_owned(),
|
||||
Some(p) => p.clone(),
|
||||
None => "".to_string(),
|
||||
},
|
||||
"project" => match task.project() {
|
||||
|
@ -258,10 +251,10 @@ impl TaskReportTable {
|
|||
let mut dt = vec![];
|
||||
for u in v {
|
||||
if let Some(t) = tasks.iter().find(|t| t.uuid() == u) {
|
||||
dt.push(t.id().unwrap())
|
||||
dt.push(t.id().unwrap());
|
||||
}
|
||||
}
|
||||
join(dt.iter().map(|i| i.to_string()), " ")
|
||||
join(dt.iter().map(ToString::to_string), " ")
|
||||
}
|
||||
}
|
||||
None => "".to_string(),
|
||||
|
@ -299,16 +292,18 @@ impl TaskReportTable {
|
|||
None => "".to_string(),
|
||||
},
|
||||
"description.count" => {
|
||||
let c = match task.annotations() {
|
||||
Some(a) => format!("[{}]", a.len()),
|
||||
None => format!(""),
|
||||
let c = if let Some(a) = task.annotations() {
|
||||
format!("[{}]", a.len())
|
||||
} else {
|
||||
format!("")
|
||||
};
|
||||
format!("{} {}", task.description().to_string(), c)
|
||||
}
|
||||
"description.truncated_count" => {
|
||||
let c = match task.annotations() {
|
||||
Some(a) => format!(" [{}]", a.len()),
|
||||
None => format!(""),
|
||||
let c = if let Some(a) = task.annotations() {
|
||||
format!("[{}]", a.len())
|
||||
} else {
|
||||
format!("")
|
||||
};
|
||||
let d = task.description().to_string();
|
||||
let mut available_width = self.description_width;
|
||||
|
@ -318,7 +313,7 @@ impl TaskReportTable {
|
|||
let (d, _) = d.unicode_truncate(available_width);
|
||||
let mut d = d.to_string();
|
||||
if d != *task.description() {
|
||||
d = format!("{}…", d);
|
||||
d = format!("{}\u{2026}", d);
|
||||
}
|
||||
format!("{}{}", d, c)
|
||||
}
|
||||
|
@ -328,12 +323,11 @@ impl TaskReportTable {
|
|||
let (d, _) = d.unicode_truncate(available_width);
|
||||
let mut d = d.to_string();
|
||||
if d != *task.description() {
|
||||
d = format!("{}…", d);
|
||||
d = format!("{}\u{2026}", d);
|
||||
}
|
||||
d
|
||||
}
|
||||
"description.desc" => task.description().to_string(),
|
||||
"description" => task.description().to_string(),
|
||||
"description.desc" | "description" => task.description().to_string(),
|
||||
"urgency" => match &task.urgency() {
|
||||
Some(f) => format!("{:.2}", *f),
|
||||
None => "0.00".to_string(),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue