commit 98446bfc5b7e219a4870394b6435b622f4f9ab9d from: witcher date: Thu Mar 31 21:59:55 2022 UTC inital commit commit - /dev/null commit + 98446bfc5b7e219a4870394b6435b622f4f9ab9d blob - /dev/null blob + ea8c4bf7f35f6f77f75d92ad8ce8349f6e81ddba (mode 644) --- /dev/null +++ .gitignore @@ -0,0 +1 @@ +/target blob - /dev/null blob + 1b221707035c9bfee266011dc42c103aca6b83d0 (mode 644) --- /dev/null +++ Cargo.lock @@ -0,0 +1,214 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "lazy_static", + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time", + "winapi", +] + +[[package]] +name = "csv" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" +dependencies = [ + "bstr", + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" +dependencies = [ + "memchr", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efaa7b300f3b5fe8eb6bf21ce3895e1751d9665086af2d64b42f19701015ff4f" + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "proc-macro2" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "632d02bff7f874a36f33ea8bb416cd484b90cc66c1194b1a1110d067a7013f58" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" + +[[package]] +name = "ryu" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" + +[[package]] +name = "serde" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704df27628939572cd88d33f171cd6f896f4eaca85252c6e0a72d8d8287ee86f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi", + "winapi", +] + +[[package]] +name = "todoist-to-todotxt" +version = "0.1.0" +dependencies = [ + "chrono", + "csv", + "serde", +] + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" blob - /dev/null blob + bce4ce95a6bc1cc78ca0a21258d8f644c8912ffa (mode 644) --- /dev/null +++ Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "todoist-to-todotxt" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +csv = "1.1" +serde = { version = "1", features = ["derive"] } +chrono = "0.4" blob - /dev/null blob + 492dd6e5638c654c306ce109d1a686f6417aebbe (mode 644) --- /dev/null +++ README.md @@ -0,0 +1,52 @@ +# todoist-to-todotxt + +Very crude (read: horrible) [Todoist](todoist.com/) to [Todo.txt](https://github.com/todotxt/todo.txt) converter. +In order to use this, export your Todoist data and unpack the zip-archive to get the csv files. + +Features: + +- Content/Description (converted to a single string) +- Priority +- Due date (with "due:YYYY-MM-DD" key-value tag) +- Project Tag +- Context Tag + +The project tag is derived from the file name. + +This **CAN'T**: + +- convert recurring tasks +- retain information about indentation +- retain author information +- convert timezones +- convert sections + +Patches and discussions are *more* than welcome. This isn't actively developed but if a feature is requested I might be able to implement it, myself. +Of course you're welcome to implement any features yourself and send a patch to the mailing list below. + +## Compiling + +Install the dependencies: + +- Rust + +Then compile `todoist-to-todotxt`: + +``` +cargo build +``` + +## Usage + +``` +todoist-to-todotxt project.csv [...] 1>todo.txt +``` + +Output to `stdout` will be the Todo.txt format. +`stderr` will contain more information, like if a task's due date couldn't be converted due to being a recurring task. + +## Resources + +Send patches and questions to [~witcher/public-inbox@lists.sr.ht](https://lists.sr.ht/~witcher/public-inbox). + +Instructions for preparing a patch are available at [git-send-email.io](https://git-send-email.io/). blob - /dev/null blob + 1359a99bc9ee120b7f3dbcdac3e22cff65baff32 (mode 644) --- /dev/null +++ src/main.rs @@ -0,0 +1,99 @@ +use std::error::Error; + +use chrono::NaiveDate; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +struct Record { + #[serde(rename = "TYPE")] + entry_type: String, + #[serde(rename = "CONTENT")] + content: String, + #[serde(rename = "DESCRIPTION")] + description: Option, + #[serde(rename = "PRIORITY")] + priority: Option, + #[allow(dead_code)] + #[serde(rename = "INDENT")] + indent: Option, + #[allow(dead_code)] + #[serde(rename = "AUTHOR")] + author: String, + #[allow(dead_code)] + #[serde(rename = "RESPONSIBLE")] + responsible: Option, + #[allow(dead_code)] + #[serde(rename = "DATE")] + date: Option, + #[allow(dead_code)] + #[serde(rename = "DATE_LANG")] + date_lang: String, + #[allow(dead_code)] + #[serde(rename = "TIMEZONE")] + timezone: String, +} + +fn convert_to_todotxt(tasks: Vec, project: String) { + for task in tasks { + let mut out = String::new(); + + if let Some(pri) = task.priority { + out.push_str(&format!( + "({})", + std::char::from_u32('A' as u32 + pri as u32 - 1).unwrap() + )); + out.push(' '); + } + + out.push_str(&task.content); + if let Some(desc) = task.description { + out.push(' '); + out.push_str(&desc); + } + out.push_str(&format!(" +{}", project)); + + if let Some(due) = task.date { + // zero pad day if not 2 digits wide + let mut d = if !due.starts_with("[0-9]{2}") { + format!("0{due}") + } else { + due + }; + d.push_str(" 2022"); + + match NaiveDate::parse_from_str(&d, "%d %b %H:%M %Y") { + Ok(_d) => { + out.push_str(" due:"); + out.push_str(&_d.format("%Y-%m-%d").to_string()); + } + Err(_) => eprintln!( + "Attention: Task with content \"{}\" contains recurring task. Manual fix required!", + task.content + ), + } + } + + println!("{}", out); + } +} + +fn main() -> Result<(), Box> { + let args: Vec<_> = std::env::args().skip(1).collect(); + + for arg in args { + let file = std::path::Path::new(&arg); + let _filename = file.file_name().unwrap().to_str().unwrap().to_string(); + let filename = _filename.strip_suffix(".csv").unwrap().to_string(); + + let mut rdr = csv::Reader::from_path(file)?; + let rows: Vec<_> = rdr + .deserialize() + .map(|row: Result| row.unwrap()) + // we don't care about sections, filter out only the tasks + .filter(|row| row.entry_type == *"task") + .collect(); + convert_to_todotxt(rows, filename); + } + + Ok(()) +}