commit - /dev/null
commit + 98446bfc5b7e219a4870394b6435b622f4f9ab9d
blob - /dev/null
blob + ea8c4bf7f35f6f77f75d92ad8ce8349f6e81ddba (mode 644)
--- /dev/null
+++ .gitignore
+/target
blob - /dev/null
blob + 1b221707035c9bfee266011dc42c103aca6b83d0 (mode 644)
--- /dev/null
+++ Cargo.lock
+# 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
+[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
+# 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
+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<String>,
+ #[serde(rename = "PRIORITY")]
+ priority: Option<u8>,
+ #[allow(dead_code)]
+ #[serde(rename = "INDENT")]
+ indent: Option<u8>,
+ #[allow(dead_code)]
+ #[serde(rename = "AUTHOR")]
+ author: String,
+ #[allow(dead_code)]
+ #[serde(rename = "RESPONSIBLE")]
+ responsible: Option<String>,
+ #[allow(dead_code)]
+ #[serde(rename = "DATE")]
+ date: Option<String>,
+ #[allow(dead_code)]
+ #[serde(rename = "DATE_LANG")]
+ date_lang: String,
+ #[allow(dead_code)]
+ #[serde(rename = "TIMEZONE")]
+ timezone: String,
+}
+
+fn convert_to_todotxt(tasks: Vec<Record>, 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<dyn Error>> {
+ 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<Record, csv::Error>| 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(())
+}