Skip to content

Commit

Permalink
Add max_comment_length to config
Browse files Browse the repository at this point in the history
  • Loading branch information
alanvardy committed Jan 5, 2025
1 parent 502dc68 commit 05ec318
Show file tree
Hide file tree
Showing 10 changed files with 158 additions and 40 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Unreleased (on main branch only)

- Render attachment URLs in comments
- Make `max_comment_length` configurable

## 2025-01-02 v0.6.26

Expand Down
11 changes: 11 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- [Values](#values)
- [disable_links](#disable_links)
- [last_version_check](#last_version_check)
- [max_comment_length](#max_comment_length)
- [next_id](#next_id)
- [path](#path)
- [natural_language_only](#natural_language_only)
Expand Down Expand Up @@ -66,6 +67,16 @@ If true, disables OSC8 linking and just displays plain text

Holds a string date, i.e. `"2023-08-30"` representing the last time crates.io was checked for the latest `tod` version. Tod will check crates.io a maximum of once per day.

### max_comment_length

```
type: nullable positive integer
default: null
possible_values: Any positive integer or null
```

The maximum number of characters that will be printed in total when showing comments.

### next_id

```
Expand Down
8 changes: 8 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ use tokio::fs;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::sync::mpsc::UnboundedSender;

const MAX_COMMENT_LENGTH: u32 = 500;

#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Completed {
count: u32,
Expand Down Expand Up @@ -45,6 +47,8 @@ pub struct Config {
#[serde(default)]
pub disable_links: bool,
pub completed: Option<Completed>,
// Maximum length for printing comments
pub max_comment_length: Option<u32>,
pub verbose: Option<bool>,
/// Don't ask for sections
pub no_sections: Option<bool>,
Expand Down Expand Up @@ -108,6 +112,9 @@ impl Default for SortValue {
}
}
impl Config {
pub fn max_comment_length(self: &Config) -> u32 {
self.max_comment_length.unwrap_or(MAX_COMMENT_LENGTH)
}
pub async fn reload_projects(self: &mut Config) -> Result<String, Error> {
let all_projects = todoist::projects(self).await?;
let current_projects = self.projects.clone().unwrap_or_default();
Expand Down Expand Up @@ -255,6 +262,7 @@ impl Config {
natural_language_only: None,
mock_string: None,
mock_select: None,
max_comment_length: None,
verbose: None,
internal: Internal { tx: Some(tx) },
args: Args {
Expand Down
14 changes: 13 additions & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use std::{fmt::Display, num::ParseIntError};
use std::{
fmt::Display,
num::{ParseIntError, TryFromIntError},
};

use crate::color;
use homedir::GetHomeError;
Expand Down Expand Up @@ -32,6 +35,15 @@ impl From<std::io::Error> for Error {
}
}

impl From<TryFromIntError> for Error {
fn from(value: TryFromIntError) -> Self {
Self {
source: String::from("TryFromIntError"),
message: format!("{value}"),
}
}
}

impl From<JoinError> for Error {
fn from(value: JoinError) -> Self {
Self {
Expand Down
20 changes: 17 additions & 3 deletions src/filters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ use crate::{
};

pub async fn edit_task(config: &Config, filter: String) -> Result<String, Error> {
let tasks = todoist::tasks_for_filter(config, &filter).await?;
let tasks = todoist::tasks_for_filters(config, &filter)
.await?
.into_iter()
.flat_map(|(_, tasks)| tasks.to_owned())
.collect::<Vec<Task>>();

let task = input::select(input::TASK, tasks, config.mock_select)?;

Expand Down Expand Up @@ -52,15 +56,25 @@ pub async fn next_task(config: Config, filter: &str) -> Result<String, Error> {
}

async fn fetch_next_task(config: &Config, filter: &str) -> Result<Option<(Task, usize)>, Error> {
let tasks = todoist::tasks_for_filter(config, filter).await?;
let tasks = todoist::tasks_for_filters(config, filter)
.await?
.into_iter()
.flat_map(|(_, tasks)| tasks.to_owned())
.collect::<Vec<Task>>();

let tasks = tasks::sort_by_value(tasks, config);

Ok(tasks.first().map(|task| (task.to_owned(), tasks.len())))
}

/// Put dates on all tasks without dates
pub async fn schedule(config: &Config, filter: &String, sort: &SortOrder) -> Result<String, Error> {
let tasks = todoist::tasks_for_filter(config, filter).await?;
let tasks = todoist::tasks_for_filters(config, filter)
.await?
.into_iter()
.flat_map(|(_, tasks)| tasks.to_owned())
.collect::<Vec<Task>>();

let tasks = tasks::sort(tasks, config, sort);

if tasks.is_empty() {
Expand Down
56 changes: 36 additions & 20 deletions src/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,26 +28,26 @@ impl Display for Flag {

/// Get a list of all tasks
pub async fn view(config: &Config, flag: Flag, sort: &SortOrder) -> Result<String, Error> {
let tasks = match flag.clone() {
Flag::Project(project) => todoist::tasks_for_project(config, &project).await?,
Flag::Filter(filter) => todoist::tasks_for_filter(config, &filter).await?,
let list_of_tasks = match flag.clone() {
Flag::Project(project) => vec![(
project.name.clone(),
todoist::tasks_for_project(config, &project).await?,
)],
Flag::Filter(filter) => todoist::tasks_for_filters(config, &filter).await?,
};

let empty_text = format!("No tasks for {flag}");
let title = format!("Tasks for {flag}");

if tasks.is_empty() {
return Ok(empty_text);
}

let mut buffer = String::new();
buffer.push_str(&color::green_string(&title));
buffer.push('\n');

for task in tasks::sort(tasks, config, sort) {
let text = task.fmt(config, FormatType::List, true, false).await?;
for (query, tasks) in list_of_tasks {
let title = format!("Tasks for {query}");
buffer.push('\n');
buffer.push_str(&color::green_string(&title));
buffer.push('\n');
buffer.push_str(&text);
for task in tasks::sort(tasks, config, sort) {
let text = task.fmt(config, FormatType::List, true, false).await?;
buffer.push('\n');
buffer.push_str(&text);
}
}
Ok(buffer)
}
Expand All @@ -60,7 +60,11 @@ pub async fn prioritize(config: &Config, flag: Flag, sort: &SortOrder) -> Result
.into_iter()
.filter(|task| task.priority == Priority::None)
.collect::<Vec<Task>>(),
Flag::Filter(filter) => todoist::tasks_for_filter(config, &filter).await?,
Flag::Filter(filter) => todoist::tasks_for_filters(config, &filter)
.await?
.iter()
.flat_map(|(_, tasks)| tasks.to_owned())
.collect::<Vec<Task>>(),
};

let empty_text = format!("No tasks for {flag}");
Expand Down Expand Up @@ -90,7 +94,11 @@ pub async fn timebox(config: &Config, flag: Flag, sort: &SortOrder) -> Result<St
.into_iter()
.filter(|task| task.duration.is_none())
.collect::<Vec<Task>>(),
Flag::Filter(filter) => todoist::tasks_for_filter(config, &filter).await?,
Flag::Filter(filter) => todoist::tasks_for_filters(config, &filter)
.await?
.into_iter()
.flat_map(|(_, tasks)| tasks.to_owned())
.collect::<Vec<Task>>(),
};

let empty_text = format!("No tasks for {flag}");
Expand Down Expand Up @@ -122,7 +130,11 @@ pub async fn process(config: &Config, flag: Flag, sort: &SortOrder) -> Result<St
tasks::filter_not_in_future(tasks, config)?
}

Flag::Filter(filter) => todoist::tasks_for_filter(config, &filter).await?,
Flag::Filter(filter) => todoist::tasks_for_filters(config, &filter)
.await?
.into_iter()
.flat_map(|(_, tasks)| tasks.to_owned())
.collect::<Vec<Task>>(),
};
let tasks = tasks::reject_parent_tasks(tasks, config).await;

Expand Down Expand Up @@ -156,7 +168,11 @@ pub async fn label(
) -> Result<String, Error> {
let tasks = match flag.clone() {
Flag::Project(project) => todoist::tasks_for_project(config, &project).await?,
Flag::Filter(filter) => todoist::tasks_for_filter(config, &filter).await?,
Flag::Filter(filter) => todoist::tasks_for_filters(config, &filter)
.await?
.into_iter()
.flat_map(|(_, tasks)| tasks.to_owned())
.collect::<Vec<Task>>(),
};

let empty_text = format!("No tasks for {flag}");
Expand Down Expand Up @@ -474,7 +490,7 @@ mod tests {
.await
.unwrap();

assert!(tasks.contains("Tasks for 'today'"));
assert!(tasks.contains("Tasks for today"));
mock.assert();
}

Expand Down
12 changes: 6 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ struct ListView {
project: Option<String>,

#[arg(short, long)]
/// The filter containing the tasks
/// The filter containing the tasks. Can add multiple filters separated by commas.
filter: Option<String>,

#[arg(short = 't', long, default_value_t = SortOrder::Datetime)]
Expand All @@ -334,7 +334,7 @@ struct ListProcess {
project: Option<String>,

#[arg(short, long)]
/// The filter containing the tasks
/// The filter containing the tasks. Can add multiple filters separated by commas.
filter: Option<String>,

#[arg(short = 't', long, default_value_t = SortOrder::Value)]
Expand All @@ -349,7 +349,7 @@ struct ListTimebox {
project: Option<String>,

#[arg(short, long)]
/// The filter containing the tasks, does not filter out tasks with durations unless specified in filter
/// The filter containing the tasks, does not filter out tasks with durations unless specified in filter. Can add multiple filters separated by commas.
filter: Option<String>,

#[arg(short = 't', long, default_value_t = SortOrder::Value)]
Expand All @@ -364,7 +364,7 @@ struct ListPrioritize {
project: Option<String>,

#[arg(short, long)]
/// The filter containing the tasks
/// The filter containing the tasks. Can add multiple filters separated by commas.
filter: Option<String>,

#[arg(short = 't', long, default_value_t = SortOrder::Value)]
Expand All @@ -375,7 +375,7 @@ struct ListPrioritize {
#[derive(Parser, Debug, Clone)]
struct ListLabel {
#[arg(short, long)]
/// The filter containing the tasks
/// The filter containing the tasks. Can add multiple filters separated by commas.
filter: Option<String>,

#[arg(short, long)]
Expand All @@ -398,7 +398,7 @@ struct ListSchedule {
project: Option<String>,

#[arg(short, long)]
/// The filter containing the tasks
/// The filter containing the tasks. Can add multiple filters separated by commas.
filter: Option<String>,

#[arg(short, long, default_value_t = false)]
Expand Down
29 changes: 25 additions & 4 deletions src/tasks/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ use supports_hyperlinks::Stream;
use super::{priority, DateTimeInfo, Duration, Task, Unit};
use crate::{color, config::Config, error::Error, projects::Project, time, todoist};

const MAX_COMMENT_LENGTH: usize = 500;

pub fn content(task: &Task, config: &Config) -> String {
let content = match task.priority {
priority::Priority::Low => color::blue_string(&task.content),
Expand Down Expand Up @@ -146,9 +144,10 @@ pub async fn comments(config: &Config, task: &Task) -> Result<String, Error> {
comments.reverse();
let comments = comments.join("\n\n");
let mut formatted_string = format!("\n\n{comment_icon} Comments {comment_icon}\n\n{comments}");
let max_comment_length: usize = config.max_comment_length().try_into()?;

if formatted_string.len() > MAX_COMMENT_LENGTH {
formatted_string.truncate(MAX_COMMENT_LENGTH);
if formatted_string.len() > max_comment_length {
formatted_string.truncate(max_comment_length);
formatted_string.push_str("[TRUNCATED]");
};

Expand All @@ -157,6 +156,8 @@ pub async fn comments(config: &Config, task: &Task) -> Result<String, Error> {

#[cfg(test)]
mod tests {
use crate::test;

use super::*;
use pretty_assertions::assert_eq;

Expand All @@ -176,4 +177,24 @@ mod tests {
String::from("\x1B]8;;https://app.todoist.com/app/task/1\x1B\\[link]\x1B]8;;\x1B\\")
)
}

#[tokio::test]
async fn test_comments() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("GET", "/rest/v2/comments/?task_id=222")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(test::responses::comments())
.create_async()
.await;

let config = test::fixtures::config().await.mock_url(server.url());
let task = test::fixtures::task();

let comments = comments(&config, &task).await.unwrap();

assert_matches!(comments.as_str(), "\n\n★ Comments ★\n\nPosted 2016-09-22 00:00:00 PDT\nAttachment \u{1b}]8;;https://s3.amazonaws.com/domorebe[TRUNCATED]");
mock.expect(1);
}
}
19 changes: 19 additions & 0 deletions src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ pub mod fixtures {
disable_links: false,
completed: None,
bell_on_success: false,
max_comment_length: Some(100),
bell_on_failure: true,
internal: Internal { tx: tx() },
projects: Some(vec![Project {
Expand Down Expand Up @@ -335,6 +336,24 @@ pub mod responses {
)
}

pub fn comments() -> String {
String::from(
"[{
\"content\": \"Need one bottle of milk\",
\"id\": \"2992679862\",
\"posted_at\": \"2016-09-22T07:00:00.000000Z\",
\"project_id\": null,
\"task_id\": \"2995104339\",
\"attachment\": {
\"file_name\": \"File.pdf\",
\"file_type\": \"application/pdf\",
\"file_url\": \"https://s3.amazonaws.com/domorebetter/Todoist+Setup+Guide.pdf\",
\"resource_type\": \"file\"
}
}]",
)
}

pub fn user() -> String {
String::from(
"\
Expand Down
Loading

0 comments on commit 05ec318

Please sign in to comment.