MilkScript

Automate tasks with MilkScript.

menu

Quickstart: Habit tracker

In this quickstart, you will create a simple habit tracker for your daily, weekly, or monthly habits.

How it works

When you select a repeating task and run the script, it will show the current streak and the days you completed the task.

Set it up

  1. Open the web app or desktop app.
  2. Click the MilkScript button at the top right, then click New...
  3. Copy the code below and paste it into the script editor.
const Status = {
  Hit: '✅',
  Miss: '❌',
  Empty: '⚪️',
};

function* timeline(start, end) {
  for (
    let current = start;
    current.getTime() < end.getTime();
    current.setDate(current.getDate() + 1)
  ) {
    yield current.getTime();
  }

  return end.getTime();
}

function* chunks(array, size) {
  for (let i = 0; i < array.length; i += size) {
    yield array.slice(i, i + size);
  }
}

function getStatus(date, { hits, misses }) {
  return hits.has(date)
    ? Status.Hit
    : misses.has(date)
    ? Status.Miss
    : Status.Empty;
}

function formatDay(date, context) {
  return `\t${getStatus(date, context)}`;
}

function formatWeekHeader(week) {
  return week.map(day =>
        '\t' + new Date(day)
          .toLocaleString('default', { weekday: 'short' })
          .slice(0, 2)
    )
    .join('')
}

function formatWeek(week, i, context) {
  const startOfMonth =
    i === 0
      ? week[week.length - 1]
      : week.find((date) => new Date(date).getDate() === 1);

  return [
    startOfMonth != null
      ? new Date(startOfMonth).toLocaleString('default', { month: 'short' })
      : '   ',
    ...week.map((day) => formatDay(day, context)),
  ].join(' ');
}

function formatYear([year, [firstWeek, ...weeks]], context) {
  return [
    `\t\t\t       ${year}`,
    formatWeekHeader(firstWeek),
    ...[firstWeek, ...weeks].map((week, i) => formatWeek(week, i, context)),
  ].join('\n');
}

function createContext(task) {
  const { min, hits, misses } = task
    .getTaskSeries()
    .getTasks()
    .reduce(
      ({ min, hits, misses }, task) => {
        const completed = task.getCompletedDate()?.setHours(0, 0, 0, 0);
        const due = task.getDueDate()?.setHours(0, 0, 0, 0);

        return {
          min: Math.min(
            min,
            completed ?? Number.MAX_SAFE_INTEGER,
            due ?? Number.MAX_SAFE_INTEGER
          ),
          hits: completed != null && task.getPostponed() === 0 ? new Set([...hits, completed]) : hits,
          misses: new Set(
            [
              ...(due != null && due !== completed ? [...misses, due] : misses),
            ].filter((miss) => miss !== completed)
          ),
        };
      },
      { min: Number.MAX_SAFE_INTEGER, hits: new Set(), misses: new Set() }
    );

  return {
    hits,
    misses,
    start: new Array(7)
      .fill(new Date(min))
      .map(
        (date, i) =>
          new Date(date.getFullYear(), date.getMonth(), date.getDate() - i)
      )
      .find((date) => date.getDay() === 0),
    end: new Date(new Date().setHours(0, 0, 0, 0)),
  };
}

function formatStreaks(days, context) {
  const streaks = days.reduce(
    ([current, ...rest], date) => {
      const dateStatus = getStatus(date, context);

      return dateStatus === Status.Empty
        ? [current, ...rest]
        : dateStatus === Status.Hit
        ? [current + 1, ...rest]
        : current === 0
        ? [current, ...rest]
        : [0, ...[current, ...rest]];
    },
    [0]
  );

  const [latest] = streaks;
  const longest = streaks.reduce(
    (result, streak) => Math.max(result, streak),
    0
  );

  return [`Current streak: ${latest}`, `Longest streak: ${longest}`].join('\n');
}

function formatTask(task) {
  const context = createContext(task);

  const days = Array.from(timeline(context.start, context.end));

  const weeks = Array.from(chunks(days, 7));

  const years = weeks.reduce((result, week) => {
    const year = new Date(week[week.length - 1]).getFullYear();

    return new Map([...result, [year, [...(result.get(year) ?? []), week]]]);
  }, new Map());

  return [
    formatStreaks(days, context),
    ...Array.from(years.entries()).map((year) => formatYear(year, context)),
  ].join('\n\n');
}

const [task] = rtm.getSelectedTasks();

task == null
  ? rtm.newMessage(
      'Oops! There are no selected task.',
      'Please select a repeating task and try again.'
    )
  : !task.isRecurring()
  ? rtm.newMessage(
      'Oops! This task does not repeat.',
      'Please set a repeat interval for the task and try again.'
    )
  : rtm.newFile(
      formatTask(task),
      rtm.MediaType.TEXT,
      `Habit tracker: ${task.getName()}`
    );
  1. At the top left, click Untitled script.
  2. Enter a name for your script (e.g. Habit tracker), then close the script editor.

Try it out

  1. Select a task that repeats every day, week, or month.
  2. Click the MilkScript button at the top right.
  3. Click the recently created script, then click Yes, run script.
  4. When the script execution completes, you'll see the current streak and the progress you're making with the task.