chopstick
All widgets

Focus

Comes with Chopstick

Pomodoro focus timer: focus sessions with short breaks and a longer break after every fourth, counting down on the island and calling out each break. Open it to start one.

Already installed? It's in your widget library.

Type it in the notch's search

  • Start focus
  • Focus for minutes minutes
  • Pause focus
  • Resume focus
  • Skip to next phase
  • Stop focus

What it runs

A widget is a folder of plain files. Here is all of its code.

widget.json
{
  "id": "pomodoro",
  "name": "Focus",
  "icon": "icon.png",
  "description": "Pomodoro focus timer: focus sessions with short breaks and a longer break after every fourth, counting down on the island and calling out each break. Open it to start one.",
  "initial": "running",
  "opens_at": "ready",
  "show_when": {
    "session.running": true
  },
  "watch": [
    {
      "script": "state.sh",
      "every": 1,
      "on": [
        "file:state"
      ],
      "as": "session"
    }
  ],
  "triggers": [
    {
      "when": {
        "session.just_changed": true
      },
      "open": "switched"
    }
  ],
  "commands": {
    "start": {
      "phrase": "Start focus",
      "description": "Start a Pomodoro set at session 1.",
      "inputs": {
        "focus_minutes": {
          "type": "number",
          "description": "Focus session length in minutes (default 25)",
          "optional": true
        },
        "break_minutes": {
          "type": "number",
          "description": "Short break length in minutes (default 5; the long break is three times this)",
          "optional": true
        }
      },
      "do": [
        {
          "set": {
            "focus_minutes": "{{input.focus_minutes}}",
            "break_minutes": "{{input.break_minutes}}"
          }
        },
        {
          "run": "start.sh",
          "then": "running"
        },
        {
          "refresh": "session"
        }
      ]
    },
    "focus_for": {
      "phrase": "Focus for {minutes} minutes",
      "description": "Start a Pomodoro set with focus sessions of the given length.",
      "inputs": {
        "minutes": {
          "type": "number",
          "description": "Focus session length in minutes"
        },
        "break_minutes": {
          "type": "number",
          "description": "Short break length in minutes (default 5; the long break is three times this)",
          "optional": true
        }
      },
      "do": [
        {
          "set": {
            "focus_minutes": "{{input.minutes}}",
            "break_minutes": "{{input.break_minutes}}"
          }
        },
        {
          "run": "start.sh",
          "then": "running"
        },
        {
          "refresh": "session"
        }
      ]
    },
    "pause": {
      "phrase": "Pause focus",
      "description": "Pause the running Pomodoro.",
      "do": [
        {
          "set": {
            "control": "pause"
          }
        },
        {
          "run": "control.sh"
        },
        {
          "refresh": "session"
        }
      ]
    },
    "resume": {
      "phrase": "Resume focus",
      "description": "Resume a paused Pomodoro.",
      "do": [
        {
          "set": {
            "control": "resume"
          }
        },
        {
          "run": "control.sh"
        },
        {
          "refresh": "session"
        }
      ]
    },
    "skip": {
      "phrase": "Skip to next phase",
      "description": "End the current focus session or break and start the next one.",
      "do": [
        {
          "set": {
            "control": "skip"
          }
        },
        {
          "run": "control.sh"
        },
        {
          "refresh": "session"
        }
      ]
    },
    "stop": {
      "phrase": "Stop focus",
      "description": "Stop the Pomodoro and hide it.",
      "do": [
        {
          "set": {
            "control": "stop"
          }
        },
        {
          "run": "control.sh"
        },
        {
          "refresh": "session"
        },
        "dismiss"
      ]
    }
  },
  "states": {
    "ready": {
      "open": true,
      "on_enter": [
        {
          "run": "running.sh",
          "then": "running"
        }
      ],
      "compact": {
        "leading": {
          "symbol": "timer",
          "tint": "red"
        },
        "trailing": {
          "text": "Focus"
        }
      },
      "expanded": {
        "size": "compact",
        "header": {
          "symbol": "timer",
          "title": "Focus",
          "tint": "red"
        },
        "blocks": [
          {
            "text": "Four focus sessions with short breaks between them and a long one at the end.",
            "style": "caption",
            "lines": 2
          },
          {
            "row": [
              {
                "button": {
                  "symbol": "play.fill",
                  "label": "25 min",
                  "style": "filled",
                  "tint": "red",
                  "do": [
                    {
                      "set": {
                        "focus_minutes": "25",
                        "break_minutes": "5"
                      }
                    },
                    {
                      "run": "start.sh",
                      "then": "running"
                    },
                    {
                      "refresh": "session"
                    }
                  ]
                }
              },
              {
                "button": {
                  "label": "50 min",
                  "do": [
                    {
                      "set": {
                        "focus_minutes": "50",
                        "break_minutes": "10"
                      }
                    },
                    {
                      "run": "start.sh",
                      "then": "running"
                    },
                    {
                      "refresh": "session"
                    }
                  ]
                }
              },
              "spacer"
            ],
            "spacing": "tight"
          }
        ]
      },
      "keys": [
        {
          "key": "return",
          "label": "Start",
          "do": [
            {
              "set": {
                "focus_minutes": "25",
                "break_minutes": "5"
              }
            },
            {
              "run": "start.sh",
              "then": "running"
            },
            {
              "refresh": "session"
            }
          ]
        },
        {
          "key": "esc",
          "do": [
            "dismiss"
          ]
        }
      ]
    },
    "running": {
      "compact": {
        "leading": {
          "ring": {
            "value": "{{session.elapsed}}",
            "max": "{{session.length}}",
            "tint": "{{session.tint}}"
          }
        },
        "trailing": {
          "text": "{{session.label}}"
        }
      },
      "expanded": {
        "size": "compact",
        "header": {
          "symbol": "timer",
          "title": "{{session.title}}",
          "tint": "{{session.tint}}"
        },
        "blocks": [
          {
            "row": [
              {
                "text": "{{session.label}}",
                "style": "title"
              },
              "spacer",
              {
                "text": "{{session.dots}}",
                "style": "caption",
                "tint": "{{session.tint}}"
              }
            ],
            "align": "center"
          },
          {
            "progress": {
              "value": "{{session.elapsed}}",
              "max": "{{session.length}}",
              "label": "Session {{session.session}} of 4",
              "tint": "{{session.tint}}"
            }
          },
          {
            "row": [
              {
                "button": {
                  "symbol": "{{session.toggle_symbol}}",
                  "label": "{{session.toggle_label}}",
                  "style": "filled",
                  "tint": "{{session.tint}}",
                  "do": [
                    {
                      "set": {
                        "control": "toggle"
                      }
                    },
                    {
                      "run": "control.sh"
                    },
                    {
                      "refresh": "session"
                    }
                  ]
                }
              },
              {
                "button": {
                  "symbol": "forward.end.fill",
                  "label": "Skip",
                  "do": [
                    {
                      "set": {
                        "control": "skip"
                      }
                    },
                    {
                      "run": "control.sh"
                    },
                    {
                      "refresh": "session"
                    }
                  ]
                }
              },
              {
                "button": {
                  "symbol": "stop.fill",
                  "label": "Stop",
                  "do": [
                    {
                      "set": {
                        "control": "stop"
                      }
                    },
                    {
                      "run": "control.sh"
                    },
                    {
                      "refresh": "session"
                    },
                    "dismiss"
                  ]
                }
              }
            ],
            "spacing": "tight"
          }
        ]
      }
    },
    "switched": {
      "open": true,
      "dismiss_after": 8,
      "compact": {
        "leading": {
          "symbol": "bell.fill",
          "tint": "{{session.tint}}"
        },
        "trailing": {
          "text": "{{session.announce}}"
        }
      },
      "expanded": {
        "size": "compact",
        "header": {
          "symbol": "bell.fill",
          "title": "{{session.announce}}",
          "tint": "{{session.tint}}"
        },
        "blocks": [
          {
            "text": "{{session.note}}",
            "style": "body"
          },
          {
            "progress": {
              "value": "{{session.elapsed}}",
              "max": "{{session.length}}",
              "label": "Session {{session.session}} of 4",
              "tint": "{{session.tint}}"
            }
          },
          {
            "row": [
              {
                "button": {
                  "symbol": "{{session.toggle_symbol}}",
                  "label": "{{session.toggle_label}}",
                  "style": "filled",
                  "tint": "{{session.tint}}",
                  "do": [
                    {
                      "set": {
                        "control": "toggle"
                      }
                    },
                    {
                      "run": "control.sh"
                    },
                    {
                      "refresh": "session"
                    }
                  ]
                }
              },
              {
                "button": {
                  "symbol": "forward.end.fill",
                  "label": "Skip",
                  "do": [
                    {
                      "set": {
                        "control": "skip"
                      }
                    },
                    {
                      "run": "control.sh"
                    },
                    {
                      "refresh": "session"
                    }
                  ]
                }
              },
              {
                "button": {
                  "symbol": "stop.fill",
                  "label": "Stop",
                  "do": [
                    {
                      "set": {
                        "control": "stop"
                      }
                    },
                    {
                      "run": "control.sh"
                    },
                    {
                      "refresh": "session"
                    },
                    "dismiss"
                  ]
                }
              }
            ],
            "spacing": "tight"
          }
        ]
      }
    }
  },
  "preview": {
    "states": [
      "ready",
      "running",
      "switched"
    ],
    "data": {
      "session": {
        "running": true,
        "paused": false,
        "just_changed": true,
        "phase": "short",
        "title": "Break",
        "tint": "green",
        "announce": "Break time",
        "note": "Take 5 min away from the screen.",
        "remaining": 272,
        "label": "4:32",
        "elapsed": 28,
        "length": 300,
        "session": 2,
        "dots": "●●○○",
        "toggle_symbol": "pause.fill",
        "toggle_label": "Pause"
      }
    }
  }
}
control.sh
#!/bin/sh
# $CHOPSTICK_CONTROL: pause, resume, toggle (pause/resume, or start when stopped), skip, stop.
. ./lib.sh
A="$CHOPSTICK_CONTROL"
if ! load; then
  [ "$A" = toggle ] && exec sh start.sh
  echo '{"ok":true}'; exit 0
fi
N=$(date +%s)
if [ "$A" = toggle ]; then if [ -n "$ends" ]; then A=pause; else A=resume; fi; fi
case $A in
  pause) if [ -n "$ends" ]; then left=$((ends - N)); [ "$left" -lt 0 ] && left=0; ends=; save; fi ;;
  resume) if [ -z "$ends" ]; then ends=$((N + left)); left=; save; fi ;;
  skip) advance; if [ -n "$ends" ]; then ends=$((N + $(length))); else left=$(length); fi; save ;;
  stop) rm -f "$F" ;;
  *) echo "unknown control: $A" >&2; exit 1 ;;
esac
echo '{"ok":true}'
lib.sh
#!/bin/sh
# Shared by the other scripts. The truth lives in $CHOPSTICK_STORAGE/state:
# phase (focus|short|long), ends (epoch; empty while paused), left (seconds
# left while paused), session (1-4), focus and brk (lengths in seconds), and
# changed (epoch of the last automatic phase change).
F="$CHOPSTICK_STORAGE/state"

load() { [ -f "$F" ] && . "$F"; }

save() {
  printf 'phase=%s\nends=%s\nleft=%s\nsession=%s\nfocus=%s\nbrk=%s\nchanged=%s\n' \
    "$phase" "$ends" "$left" "$session" "$focus" "$brk" "${changed:-0}" > "$F.tmp" && mv "$F.tmp" "$F"
}

# Length of the current phase in seconds; the long break is three short ones.
length() {
  case $phase in
    focus) echo "$focus" ;;
    short) echo "$brk" ;;
    long) echo $((brk * 3)) ;;
  esac
}

# Moves to the next phase: a long break after the 4th focus session.
advance() {
  case $phase in
    focus) if [ "$session" -ge 4 ]; then phase=long; else phase=short; fi ;;
    short) phase=focus; session=$((session + 1)) ;;
    long) phase=focus; session=1 ;;
  esac
}
running.sh
#!/bin/sh
# Succeeds while a set is going (running or paused): opened then, the widget shows it instead of starting one.
[ -f "$CHOPSTICK_STORAGE/state" ]
start.sh
#!/bin/sh
# Starts a new set at session 1. Lengths in (fractional) minutes, from
# $CHOPSTICK_FOCUS_MINUTES and $CHOPSTICK_BREAK_MINUTES; 25 and 5 by default.
. ./lib.sh
secs() { awk -v m="$1" -v d="$2" 'BEGIN { if (m + 0 <= 0) m = d; s = int(m * 60 + 0.5); print (s < 1 ? 1 : s) }'; }
focus=$(secs "$CHOPSTICK_FOCUS_MINUTES" 25)
brk=$(secs "$CHOPSTICK_BREAK_MINUTES" 5)
phase=focus; session=1; left=; changed=0
ends=$(( $(date +%s) + focus ))
save
echo '{"ok":true}'
state.sh
#!/bin/sh
# Prints where the Pomodoro is. Moves on to the next phase when one has
# ended (and saves it), so phases change even while nothing is drawn.
. ./lib.sh
if ! load; then
  echo '{"running":false,"just_changed":false,"title":"Focus","tint":"red","label":"25:00","elapsed":0,"length":1,"session":1,"dots":"○○○○","toggle_symbol":"play.fill","toggle_label":"Start"}'
  exit 0
fi
N=$(date +%s)
if [ -n "$ends" ] && [ "$ends" -le "$N" ]; then
  while [ "$ends" -le "$N" ]; do advance; ends=$((ends + $(length))); done
  changed=$N
  save
  afplay /System/Library/Sounds/Glass.aiff >/dev/null 2>&1 &
fi
L=$(length)
if [ -n "$ends" ]; then R=$((ends - N)); paused=false; sym=pause.fill; tl=Pause
else R=$left; paused=true; sym=play.fill; tl=Resume; fi
M=$(( (L + 59) / 60 ))
case $phase in
  focus) title=Focus; tint=red; ann="Back to focus"; note="Session $session of 4, $M min." ;;
  short) title=Break; tint=green; ann="Break time"; note="Take $M min away from the screen." ;;
  long) title="Long break"; tint=cyan; ann="Break time"; note="Set done. Take $M min off." ;;
esac
[ "$paused" = true ] && title="$title · Paused"
jc=false; [ $((N - ${changed:-0})) -lt 6 ] && jc=true
dots=""; i=1; while [ $i -le 4 ]; do if [ $i -le "$session" ]; then dots="${dots}●"; else dots="${dots}○"; fi; i=$((i + 1)); done
printf '{"running":true,"paused":%s,"just_changed":%s,"phase":"%s","title":"%s","tint":"%s","announce":"%s","note":"%s","remaining":%d,"label":"%d:%02d","elapsed":%d,"length":%d,"session":%d,"dots":"%s","toggle_symbol":"%s","toggle_label":"%s"}\n' \
  "$paused" "$jc" "$phase" "$title" "$tint" "$ann" "$note" "$R" $((R / 60)) $((R % 60)) $((L - R)) "$L" "$session" "$dots" "$sym" "$tl"