Skip to content

Instantly share code, notes, and snippets.

@shunf4
Last active October 12, 2021 13:31
Show Gist options
  • Select an option

  • Save shunf4/567fc3ea49389ae876d8d0090c9542f8 to your computer and use it in GitHub Desktop.

Select an option

Save shunf4/567fc3ea49389ae876d8d0090c9542f8 to your computer and use it in GitHub Desktop.
TODO.TXT to VTODO ICS (iCalendar) file
<?php
// CB's Todo.txt to ics script
// from https://chrisbeckstrom.com/2018/06/07/how-to-generate-a-calendar-from-your-todo-txt-file/
// shunf4 modified
// goes through todo.txt and generates a calendar
// 2018-06-06
//
// HOW TO USE THIS
// this script parses a file in the todo.txt format and generates an ics file
// the ics file will have a series of VTODO
// you can load this calendar into email or calendar application that supports
// to-do lists (tasks) (like Fastmail's calendar) or other systems
//
// WHY?
// I love todo.txt but sometimes it's tough to get a picture of what I really
// need to do. Sticking my dated tasks on a calendar really helps me!
// inspiration from here:
// https://gist.github.com/jakebellacera/635416
// hide all errors, we don't want them mucking up our ics output
error_reporting(0);
ini_set('display_errors', 0);
if ($_SERVER['REQUEST_METHOD'] !== "GET") {
http_response_code(405);
die("Not Allowed");
}
// where does the todo.txt file live?
$todopath = "/path/to/TODO.TXT";
// timezone of date strings in $todopath
$todoTimezone = new DateTimeZone('+0800');
// load the todo file, give us an error if we can't open it
$todofile = fopen($todopath, "r") or die("Unable to open todo file!");
// calendar file top stuff
$ical = "BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//hacksw/handcal//NONSGML v1.0//EN";
// iterate through each line of the file
while(!feof($todofile)) {
$line = fgets($todofile);
$trimmedLine = trim($line);
if ($trimmedLine === "") {
continue;
}
//print "line contains DUE! doing stuff <br>";
// explode the string into an array
$array = explode(" ", $line);
// iterate through each word and figure out what it is
// i.e. a due date, a context, a project, threshold, hidden, etc.
$done = false;
$indexOfFirstDate = 0;
$indexOfSecondDate = -10;
$dateDone = "";
$dateDoneOrig = "";
$dateAdded = "";
$dateAddedOrig = "";
$dateDue = "";
$categories = "";
$priority = "";
$priorityOrig = "";
$url = "";
$project = "";
$processingAdvancedWords = true;
foreach ($array as $i=>&$word){
$word = trim($word, "\r\n");
$word = str_replace("\r", "", $word);
$word = str_replace("\n", " ", $word);
$tryDate = DateTime::createFromFormat('!Y-m-d', $word, $todoTimezone);
$isCurrentWordAdvancedWord = false;
if ($tryDate !== FALSE) {
$tryDate->setTimezone(new DateTimeZone("UTC"));
//echo "---this word is a date! <br>";
if ($i === $indexOfFirstDate) {
$dateAdded = $tryDate->format("Ymd\THis\Z");
$dateAddedOrig = $word;
$indexOfSecondDate = $indexOfFirstDate + 1;
$isCurrentWordAdvancedWord = true;
} elseif ($i === $indexOfSecondDate) {
$dateDone = $dateAdded;
$dateDoneOrig = $dateAddedOrig;
$dateAdded = $tryDate->format("Ymd\THis\Z");
$dateAddedOrig = $word;
$isCurrentWordAdvancedWord = true;
}
}
$trimmedWord = trim($word);
if ($trimmedWord === "x" && $i === 0) {
$done = true;
$indexOfFirstDate += 1;
$indexOfSecondDate += 1;
$isCurrentWordAdvancedWord = true;
}
if ($processingAdvancedWords && strlen($trimmedWord) === 3 && substr($trimmedWord, 0, 1) === "(" && substr($trimmedWord, 2, 1) === ")" && substr($trimmedWord, 1, 1) >= "A" && substr($trimmedWord, 1, 1) <= "Z") {
$priorityOrig = $trimmedWord;
$priority = strval(ord(substr($trimmedWord, 1, 1)) - ord("A") + 1);
$indexOfFirstDate += 1;
$indexOfSecondDate += 1;
$isCurrentWordAdvancedWord = true;
}
//print "value: $value <br>";
// IF WORD CONTAINS "due:"
if (strpos($word, "due:") === 0) {
$due = substr($word, 4);
$due = trim($due);
$dateDue = DateTime::createFromFormat('!Y-m-d', $due, $todoTimezone)->setTimezone(new DateTimeZone("UTC"))->format("Ymd\THis\Z");
}
// IF WORD CONTAINS "@"
if (strpos($word, "@") === 0) {
//echo "--- this word has a context! <br>";
$context = str_replace("\n","",$word);
}
// IF WORD CONTAINS "+"
if (strpos($word, "+") === 0) {
//echo "--- this word has a project! <br>";
$project = $word;
$projectName = substr($project, 1);
$categories .= "CATEGORIES:$projectName\n";
}
// IF WORD CONTAINS "t:"
if (strpos($word, "t:") === 0) {
//echo "--- this word contains t: <br>";
$threshold = $word;
}
// IF WORD CONTAINS "h:"
if (strpos($word, "h:") === 0) {
//echo "--- this word contains h: <br>";
$hide = $word;
}
// IS THIS A URL?
if (strpos($word, "http://") === 0 || strpos($word, "https://") === 0) {
//echo "--- this word is a URL!<br>";
$url = $word;
}
if ($isCurrentWordAdvancedWord === false) {
$processingAdvancedWords = false;
}
}
// put together a summary
// summary: the name of the calendar event
// description: more info when you click on the calendar event
// BUILD THE SUMMARY
// +projectname something something
$summary = $line;
if (strpos($summary, "x ") === 0) {
$summary = substr($summary, 2);
}
// get rid of due date
$summary = (str_replace("due:".$due,"",$summary));
// get rid of the added&done date
$summary = (str_replace($dateAddedOrig,"",$summary));
$summary = (str_replace($dateDoneOrig,"",$summary));
$summary = (str_replace($priorityOrig,"",$summary));
// get rid of the context
// $summary = (str_replace($context,"",$summary));
// get rid of the threshold
$summary = (str_replace($threshold,"",$summary));
// get rid of the URL
$summary = (str_replace($url,"",$summary));
// remove project....
$summary = (str_replace($project,"",$summary));
//... so we can make it first
if ($project !== "") {
$summary = $project . " " . $summary;
}
// remove newlines
$summary = (str_replace("\n","",$summary));
// remove double spaces
$summary = (str_replace(" "," ",$summary));
$summary = implode(" ", array_filter(explode(" ", $summary), function($x) { return $x !== ""; }));
$summaryBase64 = base64_encode($summary);
//print "summary: $summary <br>";
// create the ICS entry
$ical = $ical . "
BEGIN:VTODO
SUMMARY:$summary
DESCRIPTION:$trimmedLine
UID:$summaryBase64
";
$ical .= $categories;
if ($url !== "") {
$ical = $ical . "URL:$url\n";
}
if ($dateDone !== "") {
$ical = $ical . "COMPLETED:$dateDone\n";
$ical = $ical . "DTSTAMP:$dateDone\n";
$ical = $ical . "LAST-MODIFIED:$dateDone\n";
}
if ($dateAdded !== "") {
$ical = $ical . "CREATED:$dateAdded\n";
$ical = $ical . "DTSTART:$dateAdded\n";
if ($dateDone === "") {
$ical = $ical . "DTSTAMP:$dateAdded\n";
$ical = $ical . "LAST-MODIFIED:$dateAdded\n";
}
}
if ($dateDue !== "") {
$ical = $ical . "DUE:$dateDue\n";
}
if ($done) {
$ical = $ical . "STATUS:COMPLETED\n";
}
if ($priority !== "") {
$ical = $ical . "PRIORITY:$priority\n";
}
$ical .= "END:VTODO\n";
}
fclose($todofile); // close the todofile
$ical = $ical . "
END:VCALENDAR";
// comment these headers out to load the text in a web browser
// uncomment to generate an ics file when loading this script
header('Content-Type: text/calendar; charset=utf-8');
header('Content-Disposition: attachment; filename=invite.ics');
echo $ical;
exit;
?>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment