Providing support for Projects Pane

Signed-off-by: lkadalski <kadalski.lukasz@gmail.com>
This commit is contained in:
lkadalski 2021-10-11 12:51:49 +02:00
parent e587e36dd8
commit 75217bf266
14 changed files with 1620 additions and 1255 deletions

8
Cargo.lock generated
View file

@ -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",

View file

@ -14,6 +14,8 @@ A Terminal User Interface for [Taskwarrior](https://taskwarrior.org/).
Showcase of features: <https://youtu.be/0ZdkfNrIAcw>
[![](https://img.youtube.com/vi/0ZdkfNrIAcw/0.jpg)](https://www.youtube.com/watch?v=0ZdkfNrIAcw)
**User Interface**
![](https://user-images.githubusercontent.com/1813121/113251568-bdef2380-927f-11eb-8cb6-5d95b00eee53.gif)

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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$}",

View file

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