Skip to content

Instantly share code, notes, and snippets.

@davraamides
Last active October 8, 2022 21:16
Show Gist options
  • Save davraamides/487e6d7cabf5f53419a9c337a03f8b37 to your computer and use it in GitHub Desktop.
Save davraamides/487e6d7cabf5f53419a9c337a03f8b37 to your computer and use it in GitHub Desktop.
AppleScript for getting Things tasks from Today list and associated Python script for converting the tasks to a daily schedule report

The two scripts used in my shortcut to create a daily schedule for tasks from Things Today list.

  • Get Today's Tasks.scpt is the AppleScript that gets today's tasks from Things in a format of "title:xxx tags:xxx", one task per line.
  • things_schedule.py is the Python script which parses those tasks and builds a schedule and HTML report and then opens it in the browser.
on run {input, parameters}
set todoList to {}
set text item delimiters to "
"
tell application "Things3"
set theTodos to to dos of list "Today"
repeat with aToDo in theTodos
set theTitle to name of aToDo
set tagNames to tag names of aToDo
set theTask to "title:" & theTitle & " tags:" & tagNames
copy theTask to the end of todoList
end repeat
end tell
set tasks to todoList as text
return tasks
end run
"""
Create a daily schedule report from Things Today tasks
"""
import re
import sys
import datetime
import argparse
import subprocess
from typing import Optional, Tuple, List
NO_TIME_ESTIMATE = -1
DT = datetime.datetime
TD = datetime.timedelta
def main():
parser = argparse.ArgumentParser()
parser.add_argument('-s', '--start', help="Start time for schedule, default to next quarter hour")
parser.add_argument('-d', '--duration', help="Default task duration, default is no duration", default=None, type=int)
parser.add_argument('-b', '--buffer', help="Buffer time between tasks, default is 0 minutes", default=0, type=int)
args = parser.parse_args()
if not args.start:
now = datetime.datetime.now()
delta = 15 - now.minute % 15
start = now + datetime.timedelta(minutes=delta)
else:
s = args.start
if not ':' in s:
s += ':00'
time = datetime.datetime.strptime(s, '%H:%M').time()
today = datetime.date.today()
start = datetime.datetime(today.year, today.month, today.day, time.hour, time.minute)
duration = datetime.timedelta(minutes=args.duration) if args.duration else None
buffer = datetime.timedelta(minutes=args.buffer)
timed_tasks, untimed_tasks = parse_things_tasks(duration)
scheduled_items = build_schedule(timed_tasks, start, buffer)
create_report(scheduled_items, untimed_tasks)
def parse_things_tasks(default_duration: Optional[TD]) -> Tuple[List[Tuple[TD, str]], List[str]]:
timed_tasks, untimed_tasks = [], []
for task in sys.stdin:
m = re.match(r'^title:(.*) tags:(.*)$', task)
if m:
title = m.group(1)
est_mins = parse_estimated_minutes(m.group(2).split(', '))
if est_mins > 0:
timed_tasks.append((datetime.timedelta(minutes=est_mins), title))
elif est_mins == NO_TIME_ESTIMATE and default_duration is not None:
timed_tasks.append((default_duration, title))
else:
untimed_tasks.append(title)
else:
print(f'unmatched: {task}')
return timed_tasks, untimed_tasks
def build_schedule(timed_tasks: List[Tuple[TD, str]], start: DT, buffer: TD) -> List[Tuple[DT, TD, str, str]]:
today = datetime.date.today()
midnight = datetime.datetime(today.year, today.month, today.day, 23, 59)
time = start
schedule = []
while time.date() == today and timed_tasks:
if timed_tasks:
sdt = midnight
dur, text = timed_tasks[0]
# add the task if it will fit in before the next event
if time + dur + buffer <= sdt:
schedule.append((time, dur, text))
timed_tasks.pop(0)
time += dur + buffer
continue
# didn't add either, tick forward by 5 minutes
time += datetime.timedelta(minutes=5)
return schedule
def create_report(scheduled_items, untimed_tasks):
date = datetime.date.today().strftime('%A, %B %-d')
lines = []
for time, dur, text in scheduled_items:
lines.append(TIMED_TASK_TEMPLATE.format(time=format_time(time), duration=format_timedelta(dur), text=text))
scheduled_content = '\n'.join(lines)
lines = []
for text in untimed_tasks:
lines.append(UNTIMED_TASK_TEMPLATE.format(text=text))
other_content = '\n'.join(lines)
filename = '_schedule_.html'
with open(filename, 'w', encoding='utf-8') as f:
f.write(HTML_TEMPLATE.format(date=date, scheduled_content=scheduled_content, other_content=other_content))
subprocess.call(['open', filename])
def parse_estimated_minutes(tags: List[str]) -> int:
for tag in tags:
if re.match(r'\d+m', tag):
return int(tag[:-1])
return NO_TIME_ESTIMATE
def run_command(cmd: List[str], input: str = '') -> Tuple[str, str]:
p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = p.communicate(input.encode('utf8'))
out = stdout.decode('utf8')
err = stderr.decode('utf8')
if p.returncode != 0:
raise ValueError(f"Error running script: rc={p.returncode}, out={out}, err={err}")
return out, err
def format_time(time: DT) -> str:
return time.strftime("%I:%M%p").lower()
def format_timedelta(td: TD) -> str:
hrs, remainder = divmod(td.seconds, 3600)
min, _ = divmod(remainder, 60)
parts = []
if hrs:
parts.append(f'{int(hrs)}h')
if min:
parts.append(f'{int(min)}m')
return ' '.join(parts)
TIMED_TASK_TEMPLATE = """
<tr>
<td><div class="checkbox"></div></td>
<td class="time">{time}</td>
<td class="duration">({duration})</td>
<td class="text">{text}</td>
</tr>
"""
UNTIMED_TASK_TEMPLATE = """
<tr>
<td><div class="checkbox"></div></td>
<td><span class="text">{text}</span></td>
</tr>
"""
HTML_TEMPLATE = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Things Schedule</title>
<style type="text/css">
body {{ font-family: -apple-system, "SegoeUI" }}
table {{ padding-left: 2em; }}
td {{ padding-left: 0.5em; }}
.checkbox {{
display: inline-block;
width: 15px;
height: 15px;
border-radius: 3px;
border: 1px solid gray;
vertical-align: middle;
}}
.time {{ font-weight: bold; }}
.duration {{
opacity: 0.6;
font-style: italic;
font-size: 90%;
text-align: right;
}}
</style>
</head>
<body>
<h1>Schedule for {date}</h1>
<h3>Scheduled Tasks</h3>
<table>
<tbody>
{scheduled_content}
</tbody>
</table>
<h3>Unscheduled Items</h3>
<table>
<tbody>
{other_content}
</tbody>
</table>
</body>
</html>
"""
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment