chopstick
All widgets

Move Reminder

Comes with Chopstick

Reminds you to get up and move after 30 minutes of active laptop time. Resets when you're away 5+ minutes; stays quiet otherwise.

Already installed? It's in your widget library.

Type it in the notch's search

  • Reset the move reminder
  • Time until I should move
  • Snooze the move reminder

What it runs

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

widget.json
{
  "id": "move-reminder",
  "name": "Move Reminder",
  "icon": "icon.png",
  "description": "Reminds you to get up and move after 30 minutes of active laptop time. Resets when you're away 5+ minutes; stays quiet otherwise.",
  "initial": "status",
  "watch": [
    {
      "script": "tick.sh",
      "every": 30,
      "as": "sitting"
    }
  ],
  "triggers": [
    {
      "when": {
        "sitting.due": true
      },
      "open": "remind"
    }
  ],
  "states": {
    "status": {
      "open": true,
      "dismiss_after": 6,
      "on_enter": [
        {
          "refresh": "sitting"
        }
      ],
      "compact": {
        "leading": {
          "symbol": "figure.walk"
        },
        "trailing": {
          "text": "{{sitting.remaining | 30}}m"
        }
      },
      "expanded": {
        "size": "compact",
        "header": {
          "symbol": "figure.walk",
          "title": "Move reminder"
        },
        "blocks": [
          {
            "text": "{{sitting.remaining | 30}} min until next reminder",
            "style": "title"
          },
          {
            "note": "{{sitting.minutes | 0}} min active so far"
          }
        ]
      },
      "keys": [
        {
          "key": "esc",
          "do": [
            "dismiss"
          ]
        }
      ]
    },
    "remind": {
      "task": true,
      "open": true,
      "compact": {
        "leading": {
          "symbol": "figure.walk",
          "tint": "green"
        },
        "trailing": {
          "text": "Move"
        }
      },
      "expanded": {
        "size": "compact",
        "header": {
          "symbol": "figure.walk",
          "title": "Time to move",
          "tint": "green"
        },
        "blocks": [
          {
            "text": "You've been on your laptop for {{sitting.minutes | 30}} minutes. Stand up, stretch, walk around.",
            "lines": 3
          },
          {
            "row": [
              {
                "button": {
                  "label": "Done",
                  "style": "filled",
                  "tint": "green",
                  "do": [
                    {
                      "run": "reset.sh"
                    },
                    {
                      "refresh": "sitting"
                    },
                    "dismiss"
                  ]
                }
              },
              {
                "button": {
                  "label": "Snooze 5 min",
                  "style": "plain",
                  "do": [
                    {
                      "run": "snooze.sh"
                    },
                    {
                      "refresh": "sitting"
                    },
                    "dismiss"
                  ]
                }
              }
            ],
            "spacing": "normal"
          }
        ]
      }
    }
  },
  "commands": {
    "reset": {
      "phrase": "Reset the move reminder",
      "description": "Restart the 30-minute timer.",
      "do": [
        {
          "run": "reset.sh"
        },
        {
          "refresh": "sitting"
        },
        {
          "go": "status"
        }
      ]
    },
    "status": {
      "phrase": "Time until I should move", "preview": { "state": "status" },
      "description": "Show how long until the next move reminder.",
      "do": [
        {
          "refresh": "sitting"
        },
        {
          "go": "status"
        }
      ]
    },
    "snooze": {
      "phrase": "Snooze the move reminder",
      "description": "Delay the reminder by 5 minutes.",
      "do": [
        {
          "run": "snooze.sh"
        },
        {
          "refresh": "sitting"
        },
        "dismiss"
      ]
    }
  },
  "preview": {
    "states": [
      "status",
      "remind"
    ],
    "data": {
      "sitting": {
        "minutes": 30,
        "remaining": 12,
        "due": false
      }
    }
  }
}
reset.sh
#!/bin/sh
D="$HOME/.chopstick-move-reminder"; mkdir -p "$D"
echo "0 0 $(date +%s) 0" > "$D/state"
echo '{"minutes":0,"remaining":30,"due":false}'
snooze.sh
#!/bin/sh
D="$HOME/.chopstick-move-reminder"; F="$D/state"; mkdir -p "$D"
now=$(date +%s); active=0; away=0; last=$now; snooze=0
[ -f "$F" ] && read active away last snooze < "$F"
echo "$active $away $now $((now + 300))" > "$F"
tick.sh
#!/bin/sh
# Tracks active laptop time. State file: active away last snooze_until (seconds / epoch).
D="$HOME/.chopstick-move-reminder"; F="$D/state"
mkdir -p "$D"
now=$(date +%s)
active=0; away=0; last=$now; snooze=0
[ -f "$F" ] && read active away last snooze < "$F"
dt=$((now - last)); [ "$dt" -lt 0 ] && dt=0
idle=$(ioreg -c IOHIDSystem | awk '/HIDIdleTime/ {print int($NF/1000000000); exit}')
idle=${idle:-0}
locked=0
ioreg -n Root -d1 | grep -q 'CGSSessionScreenIsLocked.*Yes' && locked=1
if [ "$dt" -gt 120 ]; then
  # gap: Mac was asleep or the widget was off, count it as time away
  away=$((away + dt))
elif [ "$locked" = 1 ] || [ "$idle" -ge 300 ]; then
  away=$((away + dt))
else
  away=0; active=$((active + dt))
fi
if [ "$away" -ge 300 ]; then active=0; snooze=0; fi
echo "$active $away $now $snooze" > "$F"
mins=$((active / 60)); rem=$(( (1800 - active + 59) / 60 )); [ "$rem" -lt 0 ] && rem=0
due=false
[ "$active" -ge 1800 ] && [ "$now" -ge "$snooze" ] && due=true
echo "{\"minutes\":$mins,\"remaining\":$rem,\"due\":$due}"