Metadata-Version: 2.5
Name: intervals-icu-pause-split
Version: 0.1.0
Summary: Split intervals.icu activities into moving and pause intervals, with a preview before anything is written
Project-URL: Homepage, https://github.com/bartTC/intervals-icu-pause-split
Project-URL: Issues, https://github.com/bartTC/intervals-icu-pause-split/issues
Author: Martin Mahner
License-Expression: MIT
License-File: LICENSE
Keywords: bikepacking,cycling,hiking,intervals,intervals.icu,laps,pause
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: End Users/Desktop
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Utilities
Requires-Python: >=3.11
Requires-Dist: httpx>=0.27
Requires-Dist: python-dotenv>=1.0
Requires-Dist: rich>=13.7
Description-Content-Type: text/markdown

# intervals-icu-pause-split

Splits an [intervals.icu](https://intervals.icu) activity into moving legs
and pauses, so a café stop, a lunch break on a hike or a night in a hotel no
longer drags down the averages of the leg it sits in. Works for rides, hikes,
walks and runs alike.

```
uvx intervals-icu-pause-split https://intervals.icu/activities/i87942121 --api-key=YOUR_KEY
```

That is the whole install. `uvx` fetches it, runs it, and throws it away again.
Pass the activity id or just paste its URL.

## What it looks like

A gravel ride on Rügen. intervals.icu had detected 97 intervals on its own:
power surges of a few seconds, with "recovery" in between, which says nothing
about how the ride actually went.

```
❯ uvx intervals-icu-pause-split --edges 5km https://intervals.icu/activities/i87942121

╭────────────────── intervals.icu · i87942121 ──────────────────╮
│ Sassnitz Gravel/Offroad-Radfahren                             │
│ GravelRide · Sat 10:58, 2025-05-17 · 96.9 km · 5:33 h elapsed │
╰───────────────────────────────────────────────────────────────╯
  Pause: at least 5 min below 3 km/h or not recording; stops less than 1 min apart are merged
  Warmup: first 5 km · Cooldown: last 5 km

Speed     │▄▅▆▇▇▇▇▆▅▃▄▄▅▆▆▄█▇███▅▇▇▁ ▅▇▆▇▇▇▇▇▇▆▇▆▇▆▆▆▄▆▄  ▄▇▇▆▇█▇▆▇▇▇█▇▇█▇▆▇▇▆▅▄▅▆▆▅█▅▅▅▅▇▇▆▅▆██▇██│
Now       │╎░╎╎╎╎█╎█╎╎█░░█████░░█░░░░░░░░░░░░░░░░░░░░░█░░░░░█╎░░░░░░█░░░█░░░░░░╎░█░╎█░░░░░╎█╎░╎╎╎█░│
New       │████████████████████████░░░█████████████████░░░█████████████████████████████████████████│
          └┴───────────────┴──────────────┴───────────────┴───────────────┴───────────────┴────────┘
           11:00           12:00          13:00           14:00           15:00           16:00
           ██ moving   ░ pause   ╎ shorter pause   ▁…█ speed up to 27 km/h   · one column ≈ 3 min

Now  97 intervals, 88 not shown
 #  Type      Label  Start  Duration  Distance  km/h    W
 1  RECOVERY  —      10:58     1 min    0.4 km     —    —
 2  WORK      —      11:00     3 min    0.7 km  11.3  255
 3  RECOVERY  —      11:04     5 min    1.5 km     —    —
 4  WORK      —      11:09       6 s    0.0 km  17.3  510
 5  RECOVERY  —      11:09     1 min    0.6 km     —    —
 6  WORK      —      11:11       5 s    0.0 km  20.8  418
 …  …         …          …         …         …     …    …
95  RECOVERY  —      16:23     1 min    0.7 km     —    —
96  WORK      —      16:25      10 s    0.1 km  27.1  468
97  RECOVERY  —      16:25     6 min    2.8 km     —    —

New
#  Type      Label     Start  Duration  Distance  km/h    W
1  WORK      Warmup    10:58    17 min    5.0 km  17.1  195
2  WORK      Leg 1     11:16    1:13 h   21.0 km  19.6  133
3  RECOVERY  —         12:29    10 min    0.0 km     —    —
4  WORK      Leg 2     12:40    1:06 h   22.0 km  19.8  148
5  RECOVERY  —         13:47    11 min    0.0 km     —    —
6  WORK      Leg 3     13:58    2:20 h   43.9 km  19.4  140
7  WORK      Cooldown  16:19    12 min    5.0 km  23.5   99
  5 legs · 5:11 h · 96.9 km   |   2 pauses · 21 min

  Replace the 97 intervals with these 7? [y/n] (n):
```

`Now` is what the activity has, `New` what it will get: 5 km out of town and
5 km back in as warmup and cooldown, three legs, and the two real stops
between them. A long `Now` list is cut down to its first and last few rows;
the timeline above it always shows everything.

The timeline is drawn to scale. A pause shorter than one column still shows up,
as a thin `╎`, so a short stop stays visible even on a two-day bikepacking
trip without looking longer than it was. In the terminal, consecutive legs alternate between
green and cyan.

## How pauses are found

- **Standing still:** slower than `--stop-speed`: 1 km/h on foot (hike, walk,
  run), 3 km/h for everything else. A steep climb on foot easily drops below
  3 km/h, while GPS drift at a real stop stays under 1 km/h.
- **Not recording:** auto-pause leaves no samples, just a jump in the time
  stream. That counts as standing still too.
- **Merged:** stops with less than `--merge` (60 s) of moving between them are
  one pause. Wheeling the bike from the café back to the road does not split it
  in two.
- **Long enough:** a pause needs `--min-pause` (5 min) of actual standstill. The
  moving between merged stops does not count towards that, so stop-and-go
  through town stays moving.

Getting out of town and back in can be split off as well: `--edges 10m` turns
the first and last ten minutes into `Warmup` and `Cooldown` intervals. Give a
distance instead with `--edges 5km`, or two values to set start and end
separately: `--edges 3km,10m`. A distance ignores the time spent at traffic
lights, so it tends to match "until I'm out of town" better.

## What gets written

Only the moving legs, as labelled WORK intervals. intervals.icu fills the
gaps between them with RECOVERY intervals by itself. It does not accept
RECOVERY intervals through the API (they come back as WORK), and it drops any
label on them, which is why the pauses in the table have none.

Writing replaces **all** intervals on the activity. Before that, the current
ones are saved to `~/.local/state/intervals-icu-pause-split/backups/` (or
`$XDG_STATE_HOME`), and the tool prints the command that puts them back:

```
uvx intervals-icu-pause-split i87942121 --restore ~/.local/state/intervals-icu-pause-split/backups/i87942121-20261006-044729.json
```

## Usage

```
uvx intervals-icu-pause-split i87942121                   # preview, then ask before writing
uvx intervals-icu-pause-split i87942121 --dry-run         # preview, never write
uvx intervals-icu-pause-split i87942121 --min-pause 10m   # only the longer stops
uvx intervals-icu-pause-split i87942121 --edges 10m       # warmup and cooldown of 10 minutes
uvx intervals-icu-pause-split i87942121 --edges 3km,10m   # 3 km warmup, 10 minute cooldown
uvx intervals-icu-pause-split i87942121 --label Etappe    # "Etappe 1", "Etappe 2", …
uvx intervals-icu-pause-split i87942121 --yes             # write without asking
```

Durations are written as `90s`, `5m` or `1.5h`; a bare number means minutes.
Distances for `--edges` are written in `km` (`0.5km`, not `500m`, because `m`
already means minutes).
Nothing is written without a confirmation prompt unless you pass `--yes`.

### Credentials

The API key sits at the bottom of <https://intervals.icu/settings>. Provide it
by flag, by environment variable, or in a `.env` file next to where you run it:

```
INTERVALS_ICU_API_KEY=your_key_here
```

### Options

| Flag | Default | Meaning |
| ---- | ------- | ------- |
| `--api-key` | `$INTERVALS_ICU_API_KEY` | Your intervals.icu API key |
| `--min-pause` | `5m` | Minimum standstill for a pause |
| `--merge` | `60s` | Stops with less moving than this in between count as one pause |
| `--stop-speed` | `1` on foot, `3` otherwise | Below this speed (km/h) you count as standing still |
| `--edges` | | Split off the first and last stretch as `Warmup` and `Cooldown`, as a duration (`10m`) or a distance (`3km`); one value for both ends or two, comma-separated, for start and end |
| `--label` | `Leg` | Label prefix for the moving intervals |
| `--width` | terminal width | Width of the timeline |
| `--restore` | | Put back the intervals from a backup file |
| `--dry-run` | | Show the preview, never write |
| `--yes` | | Skip the confirmation prompt |

## Development

```
just test                      # run the suite with coverage
just coverage                  # the same, as a browsable HTML report
just check i87942121          # dry run against your own account
just run i87942121            # run for real, with the confirmation prompt
just build                     # build the distribution
```

Releases are cut by tagging:

```
git tag v0.1.0 && git push --tags
```

CI runs the tests, refuses a tag that disagrees with the version in
`pyproject.toml`, and publishes to PyPI through Trusted Publishing, so no API
token is stored in the repository.

## License

MIT
