/* This file is part of DarkFi (https://dark.fi) * * Copyright (C) 2020-2024 Dyne.org foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ use std::{cmp::Ordering, fmt::Write, str::FromStr}; use prettytable::{ format::{consts::FORMAT_NO_COLSEP, FormatBuilder, LinePosition, LineSeparator}, row, table, Cell, Row, Table, }; use textwrap::fill; use darkfi::{ util::time::{timestamp_to_date, DateFormat}, Result, }; use crate::{ primitives::{State, TaskInfo}, TaskEvent, }; pub fn print_task_list(tasks: Vec, ws: String) -> Result<()> { let mut tasks = tasks; let mut table = Table::new(); table.set_format( FormatBuilder::new() .padding(1, 1) .separators(&[LinePosition::Title], LineSeparator::new('-', ' ', ' ', ' ')) .build(), ); table.set_titles(row!["ID", "Title", "Tags", "Project", "Assigned", "Due", "Rank"]); // group tasks by state. tasks.sort_by_key(|task| task.state.clone()); // sort tasks by there rank only if they are not stopped. tasks.sort_by(|a, b| { if a.state != "stop" && b.state != "stop" { b.rank.partial_cmp(&a.rank).unwrap() } else { // because sort_by does not reorder equal elements Ordering::Equal } }); let mut min_rank = None; let mut max_rank = None; if let Some(first) = tasks.first() { max_rank = first.rank; } if let Some(last) = tasks.last() { min_rank = last.rank; } for task in tasks { let state = State::from_str(&task.state.clone())?; let (max_style, min_style, mid_style, gen_style) = if state.is_start() { ("bFg", "Fc", "Fg", "Fg") } else if state.is_pause() { ("iFYBd", "iFYBd", "iFYBd", "iFYBd") } else if state.is_stop() { ("Fr", "Fr", "Fr", "Fr") } else { ("", "", "", "") }; let rank = if let Some(r) = task.rank { r.to_string() } else { "".to_string() }; let mut print_tags = vec![]; for tag in &task.tags { let t = tag.replace('+', ""); print_tags.push(t) } let due_ = match task.due { Some(ts) => ts.0, None => 0, }; table.add_row(Row::new(vec![ Cell::new(&task.id.to_string()).style_spec(gen_style), Cell::new(&task.title).style_spec(gen_style), Cell::new(&print_tags.join(", ")).style_spec(gen_style), Cell::new(&task.project.join(", ")).style_spec(gen_style), Cell::new(&task.assign.join(", ")).style_spec(gen_style), Cell::new(×tamp_to_date(due_, DateFormat::Date)).style_spec(gen_style), if task.rank == max_rank { Cell::new(&rank).style_spec(max_style) } else if task.rank == min_rank { Cell::new(&rank).style_spec(min_style) } else { Cell::new(&rank).style_spec(mid_style) }, ])); } let workspace = format!("Workspace: {}", ws); let mut ws_table = table!([workspace]); ws_table.set_format( FormatBuilder::new() .padding(1, 1) .separators(&[LinePosition::Bottom], LineSeparator::new('-', ' ', ' ', ' ')) .build(), ); if unsafe { libc::isatty(libc::STDOUT_FILENO) } == 1 { ws_table.printstd(); table.printstd(); } else { for row in table.row_iter() { for cell in row.iter() { print!("{}\t", cell.get_content()); } println!(); } } Ok(()) } pub fn taskinfo_table(taskinfo: TaskInfo) -> Result { let due_ = match taskinfo.due { Some(ts) => ts.0, None => 0, }; let due = timestamp_to_date(due_, DateFormat::Date); let created_at = timestamp_to_date(taskinfo.created_at.0, DateFormat::DateTime); let rank = if let Some(r) = taskinfo.rank { r.to_string() } else { "".to_string() }; let mut table = table!( [Bd => "ref_id", &taskinfo.ref_id], ["workspace", &taskinfo.workspace], [Bd =>"id", &taskinfo.id.to_string()], ["owner", &taskinfo.owner], [Bd =>"title", &taskinfo.title], ["tags", &taskinfo.tags.join(", ")], [Bd =>"desc", &taskinfo.desc.to_string()], ["assign", taskinfo.assign.join(", ")], [Bd =>"project", taskinfo.project.join(", ")], ["due", due], [Bd =>"rank", rank], ["created_at", created_at], [Bd =>"current_state", &taskinfo.state]); table.set_format( FormatBuilder::new() .padding(1, 1) .separators(&[LinePosition::Title], LineSeparator::new('-', ' ', ' ', ' ')) .build(), ); table.set_titles(row!["Name", "Value"]); Ok(table) } pub fn events_table(taskinfo: TaskInfo) -> Result
{ let (events, timestamps) = &events_as_string(taskinfo.events); let mut events_table = table!([events, timestamps]); events_table.set_format(*FORMAT_NO_COLSEP); events_table.set_titles(row!["Events"]); Ok(events_table) } pub fn comments_table(taskinfo: TaskInfo) -> Result
{ let (events, timestamps) = &comments_as_string(taskinfo.events); let mut comments_table = table!([events, timestamps]); comments_table.set_format(*FORMAT_NO_COLSEP); comments_table.set_titles(row!["Comments"]); Ok(comments_table) } pub fn print_task_info(taskinfo: TaskInfo) -> Result<()> { let table = taskinfo_table(taskinfo.clone())?; table.printstd(); let events_table = events_table(taskinfo.clone())?; events_table.printstd(); let comments_table = comments_table(taskinfo)?; comments_table.printstd(); println!(); Ok(()) } pub fn events_as_string(events: Vec) -> (String, String) { let mut events_str = String::new(); let mut timestamps_str = String::new(); let width = 50; for event in events { if event.action.as_str() == "comment" { continue } writeln!(timestamps_str, "{}", event.timestamp).unwrap(); match event.action.as_str() { "title" => { writeln!(events_str, "- {} changed title to {}", event.author, event.content) .unwrap(); } "rank" => { writeln!(events_str, "- {} changed rank to {}", event.author, event.content) .unwrap(); } "state" => { writeln!(events_str, "- {} changed state to {}", event.author, event.content) .unwrap(); } "assign" => { writeln!(events_str, "- {} assigned {}", event.author, event.content).unwrap(); } "project" => { writeln!(events_str, "- {} changed project to {}", event.author, event.content) .unwrap(); } "tags" => { writeln!(events_str, "- {} changed tags to {}", event.author, event.content) .unwrap(); } "due" => { writeln!( events_str, "- {} changed due date to {}", event.author, timestamp_to_date(event.content.parse::().unwrap_or(0), DateFormat::Date) ) .unwrap(); } "desc" => { // wrap long description let ev_content = fill(&event.content, textwrap::Options::new(width).subsequent_indent(" ")); // skip wrapped lines to align timestamp with the first line for _ in 1..ev_content.lines().count() { writeln!(timestamps_str, " ").unwrap(); } writeln!(events_str, "- {} changed description to: {}", event.author, ev_content) .unwrap(); } _ => {} } } (events_str, timestamps_str) } pub fn comments_as_string(events: Vec) -> (String, String) { let mut events_str = String::new(); let mut timestamps_str = String::new(); let width = 50; for event in events { if event.action.as_str() != "comment" { continue } writeln!(timestamps_str, "{}", event.timestamp).unwrap(); if event.action.as_str() == "comment" { // wrap long comments let ev_content = fill(&event.content, textwrap::Options::new(width).subsequent_indent(" ")); // skip wrapped lines to align timestamp with the first line for _ in 1..ev_content.lines().count() { writeln!(timestamps_str, " ").unwrap(); } writeln!(events_str, "{}> {}", event.author, ev_content).unwrap(); } } (events_str, timestamps_str) }