Commit graph

1066 commits

Author SHA1 Message Date
a255fa47a0
fix(credentials): pack CSES token into single credential entry
Problem: the CSES API token was stored as a separate git credential
entry using `path=api-token`, but many credential helpers (e.g.
`cache`) ignore the `path` field, so the token was lost on
retrieval. The scraper `on_event` callback also called
`git_credential.store` from a `uv.spawn` fast event context,
causing `vim.wait` errors.

Solution: encode the CSES token into the password field as
`password<TAB>token` and decode on retrieval. One credential
entry per platform, no `path` dependency. Wrap `store` calls in
`scraper.lua` with `vim.schedule` to avoid fast event context.
2026-03-07 20:04:28 -05:00
0d06d5cb89
refactor(credentials): add cp.Credentials LuaCATS class 2026-03-07 19:50:34 -05:00
5aa2c024e0
feat(credentials): guard login/submit/logout on credential helper
Problem: if no git credential helper is configured, login and
submit silently fail to persist credentials.

Solution: add `has_helper()` to `git_credential.lua` that checks
`git config credential.helper`. Guard the top of `login()`,
`logout()`, and `submit()` with an early-return error. Add a
healthcheck warning when no helper is configured. Add LuaCATS
annotations to all `git_credential` functions.
2026-03-07 19:50:04 -05:00
8348b6195e
feat: add bar = true to :CP command 2026-03-07 19:46:52 -05:00
5718319413
docs: update requirements and credentials sections
Problem: docs referenced the old file-based credential backend
and `credentials_backend` config option that no longer exist.

Solution: add git 1.7.9+ to optional requirements, update
credentials section to document `git credential` as the sole
storage mechanism, remove `credentials_backend` from config
fields.
2026-03-07 19:46:49 -05:00
2d709ab898
feat(health): split required/optional sections, add git check
Problem: all healthcheck items were under a single `[required]`
section, including optional tools like uv and git.

Solution: move uv/nix/git checks into a new `[optional]` section.
Add git version check asserting >= 1.7.9 with the minimum and
installed versions shown side by side. Show neovim version the
same way.
2026-03-07 19:46:44 -05:00
74f699c5a1
feat(utils): require git in check_required_runtime 2026-03-07 19:46:35 -05:00
d1127f3e9b
refactor: migrate credential consumers to git_credential
Problem: `credentials.lua`, `submit.lua`, and `scraper.lua` all
routed through `cache.get/set/clear_credentials`, which no longer
exists.

Solution: replace all `cache` credential calls with direct
`git_credential.get/store/reject` calls. Remove unused `cache`
imports where applicable.
2026-03-07 19:46:32 -05:00
be1bc2095e
refactor(cache): remove file-based credential storage
Problem: `cache.lua` stored credentials in `_credentials` keys
inside the JSON cache file, with a versioned migration system
(`CACHE_VERSION`) that only existed to move credentials between
formats.

Solution: remove `get_credentials`, `set_credentials`,
`clear_credentials`, the v1→v2 migration, and `CACHE_VERSION`.
The cache file is now a plain JSON blob with no version field.
2026-03-07 19:46:28 -05:00
72c72fbc41
feat(credentials): add git_credential module
Problem: credentials were stored as plaintext JSON in the cache
file, with no integration with system credential managers.

Solution: add `lua/cp/git_credential.lua` wrapping the
`git credential fill/approve/reject` protocol. Maps each platform
to its host, handles CSES token as a second entry with
`path=api-token`.
2026-03-07 19:46:22 -05:00
936c13d073
doc: update 2026-03-07 19:27:47 -05:00
d1b2117fa2
feat: minor improved security measures 2026-03-07 19:05:43 -05:00
27d7a4e6b5
fix(scraper): minor credential interop 2026-03-07 18:17:13 -05:00
Barrett Ruth
b53c8ca44e
fix(security): harden credential storage and transmission (#369)
Some checks are pending
luarocks / ci (push) Waiting to run
luarocks / publish (push) Blocked by required conditions
## Problem

Credential and cookie files were world-readable (0644), passwords
transited via `CP_CREDENTIALS` env var (visible in `/proc/PID/environ`),
and Kattis/USACO echoed passwords back through stdout unnecessarily.

## Solution

Set 0600 permissions on `cp-nvim.json` and `cookies.json` after every
write, pass credentials via stdin pipe instead of env var, and stop
emitting passwords in ndjson from Kattis/USACO `LoginResult` (CSES token
emission unchanged).
2026-03-07 18:14:34 -05:00
Barrett Ruth
771dbc7753
fix(scrapers): bad credentials detection and error message cleanup (#367)
## Problem

Wrong credentials produced garbled error messages (`"login failed: Login
failed: bad_credentials"`) and stale credentials remained cached after
failure, causing silent re-use on the next invocation.

## Solution

Standardize all scrapers to emit `"bad_credentials"` as a plain error
code, mapped to a human-readable string via `LOGIN_ERRORS` in
`constants.lua`. Fix `credentials.lua` to clear cached credentials on
failure in both the fresh-prompt and re-prompt paths. For AtCoder and
Codeforces, replace `wait_for_url` with `wait_for_function` to detect
the login error element immediately rather than sitting the full 10s
navigation timeout. Add "Remember me" checkbox on Codeforces login.
2026-03-07 17:58:25 -05:00
Barrett Ruth
573b335646
feat: file conflict prompt, empty submit guard, and lint fixes (#366)
## Problem

Loading a problem whose source file already exists silently overwrites
user code. Submitting an empty file sends a blank submission to the
platform. Two ruff lint violations existed in the scrapers.

## Solution

- `setup.lua`: when the target source file exists on the filesystem
(`vim.uv.fs_stat`), show an inline `Overwrite? [y/N]:` prompt. Declining
keeps the existing file open and registers state normally. Skipped when
the file is already loaded in a buffer.
- `submit.lua`: resolve path to absolute, use `vim.uv.fs_stat` to verify
existence, abort with WARN if `stat.size == 0` ("Submit aborted: source
file has no content").
- `scrapers/atcoder.py`: remove unused `pathlib.Path` import (F401).
- `scrapers/base.py`: move local imports to top of file (E402).

Closes #364, #365.
2026-03-07 16:30:51 -05:00
Barrett Ruth
b7ddf4c253
fix(scrapers): cookie fast paths, centralized storage, and reauth hardening (#363)
## Problem

Scraper cookie handling was fragmented across per-platform files with no
shared access, httpx scrapers lacked `checking_login` fast paths on
login, and several re-auth edge cases (CodeChef submit, CF cookie guard,
AtCoder cookie persistence) caused unnecessary full re-logins or silent
failures.

## Solution

Centralize all cookie storage into a single `cookies.json` via helpers
in `base.py`. Add `checking_login` fast paths to `kattis.py` (using the
`x-username` response header as a session probe), `usaco.py`, and
`cses.py` login flows. Fix `kattis.py` submit to emit `checking_login`
only after loading cookies. Remove AtCoder cookie persistence from login
entirely — always do a fresh session. Harden CodeChef and CF reauth
with consistent status logging and cookie guard checks.
2026-03-07 16:10:51 -05:00
Barrett Ruth
564f9286da
fix(scraper): httpx login fast paths, proactive validation, and slug filename fix (#358)
## Problem

httpx scrapers (CSES, Kattis, USACO) always ran full login flows even
with valid cached sessions. \`credentials.lua\` always prompted before
trying cached credentials. \`default_filename\` doubled the slug for
Kattis single-problem mode (e.g. \`addtwonumbersaddtwonumbers.cc\`).

## Solution

Added token/cookie fast paths to CSES \`login()\` and USACO
\`login()\`/\`submit()\`.
Hardened Kattis reactive re-auth trigger to check status code first.
Refactored \`credentials.lua\` to try cached credentials before
prompting.
Fixed \`default_filename\` to not concatenate when \`contest_id ==
problem_id\`.
2026-03-07 15:38:12 -05:00
Barrett Ruth
eb0dea777e
fix(scrapers): login fast paths and re-auth hardening for httpx platforms (#357)
## Problem

On CSES, Kattis, and USACO, `:CP <platform> login` always prompted
for credentials and ran a full web login even when a valid session was
already cached. Submit also had weak stale-session detection.

## Solution

`credentials.lua` now tries cached credentials first before prompting,
delegating fast-path detection to each scraper. CSES `login()` checks
the cached API token and returns immediately if valid. USACO `login()`
and `submit()` call `_check_usaco_login()` upfront. Kattis `submit()`
emits `checking_login` consistently and also triggers re-auth on HTTP
400/403, not just on the `"Request validation failed"` text match.
The premature `Submitting...` log emitted by Lua before the scraper
started is removed — Python's own status events are sufficient.
2026-03-07 02:23:43 -05:00
6e9829a115
fix(race): log initial countdown on start 2026-03-07 00:00:42 -05:00
Barrett Ruth
f59c12a929
fix(race): log initial countdown on start (#356)
## Problem

Starting a race on a contest more than 1 hour away showed no feedback —
`should_notify` only fires at 15-minute intervals, so the first tick
produced no message and the UI appeared frozen during the contest list
fetch.

## Solution

Log the initial countdown immediately after the race timer is set up,
before the first `should_notify` tick fires.
2026-03-06 23:59:56 -05:00
926ca587b8
ci: format
Some checks are pending
luarocks / ci (push) Waiting to run
luarocks / publish (push) Blocked by required conditions
2026-03-06 23:42:45 -05:00
Barrett Ruth
73e5c3f3f8
fix: codechef submit fixes and atcoder cleanup (#355)
## Problem

After the initial CodeChef implementation, submit silently swallowed
"contest not available for submission" errors, and the submit flow used
blind `wait_for_timeout` delays. AtCoder had an unnecessary 500ms settle
delay after file upload.

## Solution

Replace the initial page-load sleep in CodeChef submit with
`wait_for_selector`, replace the 3s post-click sleep with a proper
`wait_for_selector` on the result dialog, and extend the practice
fallback
check to catch both dialog variants. Remove AtCoder's
`BROWSER_SETTLE_DELAY`
and the constant from `timeouts.py`.
2026-03-06 23:40:12 -05:00
Barrett Ruth
3c11d609f5
feat(codechef): implement full CodeChef support (#354)
## Problem

CodeChef had no working login, submit, or contest list. The browser
selectors were wrong, the contest list was missing present/past
contests,
and problem/contest URLs were unset.

## Solution

Fix login and submit selectors for the Drupal-based site. Paginate
`/api/list/contests/past` to collect all 228 Starters, then expand each
parent contest into individual division entries (e.g. `START228 (Div.
4)`).
Add language IDs, correct `url`/`contest_url`/`standings_url` in
metadata,
and make `:CP <platform>` open the contest picker directly.
2026-03-06 23:10:44 -05:00
c7f2af16d4
chore: remove files 2026-03-06 21:35:34 -05:00
Barrett Ruth
291de4e137
fix: expand language IDs, fix AtCoder submit, normalize logging (#353)
## Problem

Language version coverage was incomplete across all platforms, AtCoder
submit used a stale cookie fast-path that caused silent failures, and
raw
`vim.notify` calls throughout the codebase produced inconsistent or
missing `[cp.nvim]:` prefixes.

## Solution

Remove cookie persistence from AtCoder login/submit (always fresh
login),
increase the submit nav timeout to 40s, and switch to in-memory buffer
upload with the correct per-language extension from a full
`_LANGUAGE_ID_EXTENSION`
map covering all 116 AtCoder languages. Expand `LANGUAGE_VERSIONS` in
`constants.lua` with all AtCoder languages, 15 new CF languages with
full
version variants, and 50+ Kattis languages. Fix AtCoder `prolog` ID
(`6079`→`6081`, was Pony) and remove the non-existent `racket` entry.
Replace all raw `vim.notify` calls with `logger.log`. Simplify the
submit
language doc to point at `constants.lua` rather than maintaining a
static table.
2026-03-06 21:35:13 -05:00
Barrett Ruth
1ac521a126
fix: language version IDs, cache crash, and stress session restore (#352)
## Problem

Codeforces language version IDs were wrong (pointed at old compiler
versions), `LANGUAGE_VERSIONS` was incomplete for most platforms, the
contest picker crashed when `supports_countdown` was stored as a
non-table entry, and `:CP stress` restored a broken IO view on exit.

## Solution

Correct CF `programTypeId` values and default to C++20. Add
`LANGUAGE_VERSIONS` entries for all six platforms. Guard
`get_contest_summaries` against non-table cache entries. Call
`ensure_io_view()` after stress session restore. Shorten the stress
terminal buffer name to a readable `term://stress.py` format.
2026-03-06 20:22:28 -05:00
Barrett Ruth
425a8f36e9
fix: complete language version IDs for all platforms (#350)
## Problem

`LANGUAGE_VERSIONS` only covered cpp and python. Several platform IDs
were wrong — CodeChef used `C++ 17`/`Python 3` (correct: `C++`/`PYTH
3`), USACO listed nonexistent c++20/c++23 options, and CSES only had
C++17.

## Solution

Verify every platform's submit page and update all language ID tables.
Add java and rust entries where supported, fix incorrect CodeChef and
USACO IDs, and expand CSES `CSES_LANGUAGES` dict with
C++11/C++20/PyPy3/Java/Rust variants.
2026-03-06 19:28:06 -05:00
Barrett Ruth
2776aaeb21
fix: correct Codeforces language version IDs and default to C++20 (#349)
## Problem

`LANGUAGE_VERSIONS` mapped CF `c++17` to `89` (actually GNU G++20) and
defaulted to `c++17`, which didn't match any CF entry. Helpdocs listed
CF as having only `c++17`.

## Solution

Fix CF version map to `c++17 → 54`, `c++20 → 89`, `c++23 → 91`, add
`pypy3 → 70`. Change `DEFAULT_VERSIONS.cpp` to `c++20` (no behavioral
change — `89` was already the submit default). Update helpdocs. Fix
StyLua formatting in `race.lua`.
2026-03-06 19:01:55 -05:00
Barrett Ruth
8398f3428e
feat(race): notification tiers, drift correction, and T=0 retry (#348)
## Problem

Race countdown had three reliability issues:
- Notified every second regardless of remaining time (noisy for
hours-long waits)
- Never re-fetched start times (stale if contest rescheduled)
- No retry on setup failure at T=0 — if the scraper failed at the most
critical moment, the entire countdown was wasted

## Solution

- **Notification tiers**: >1h every 15m, >5m every 1m, >1m every 10s,
≤60s every 1s
- **Drift correction**: re-fetch contest list every 10 minutes, update
`start_time` if changed
- **T=0 retry**: `race_try_setup` calls `scrape_contest_metadata` with
the new `on_error` callback, retrying up to 15 times at 3s intervals
(~45s). Caches metadata on success, then calls `setup_contest` on the
fast (cached) path. Token guard invalidates stale retries after
cancellation.
- **`on_error` for `scrape_contest_metadata`**: optional 4th param,
backward-compatible
2026-03-06 18:43:12 -05:00
Barrett Ruth
bd0ae25d4b
refactor(race): replace :CP race with --race flag (#347)
## Problem

Race was the only command using `:CP <action> <platform> <contest>`
syntax while every other platform+contest command uses `:CP <platform>
<contest> [flags]`.

## Solution

Make race a `--race` flag on the existing contest setup flow. Remove
`:CP race stop` — starting a new race auto-cancels any active one.
Remove `<Plug>(cp-race-stop)` keymap. Update tab completion and docs
accordingly.
2026-03-06 18:33:42 -05:00
Barrett Ruth
58b6840815
feat: race countdown support and language version selection (#346)
## Problem

Two gaps in the plugin: (1) contest pickers have no way to know whether
a contest supports countdown (race), so the UI can't surface that
affordance; (2) `:CP submit` hardcodes a single language ID per platform
with no way to choose C++ standard or override the platform ID.

## Solution

**Race countdown** (`4e709c8`): Add `supports_countdown` boolean to
`ContestListResult` and wire it through CSES/USACO scrapers, cache, race
module, and pickers.

**Language version selection** (`b90ac67`): Add `LANGUAGE_VERSIONS` and
`DEFAULT_VERSIONS` tables in `constants.lua`. Config gains `version` and
`submit_id` fields validated at setup time. `submit.lua` resolves the
effective config to a platform ID (priority: `submit_id` > `version` >
default). Helpdocs add `*cp-submit-language*` section. Tests cover
`LANGUAGE_IDS` completeness.
2026-03-06 18:18:21 -05:00
Barrett Ruth
592f977296
fix(login): remove cookie fast-path from login subcommand (#344)
## Problem

`:CP <platform> login` short-circuited on cached cookies/tokens — if an
old session was still valid, the new credentials were never tested. The
user got "login successful" even with wrong input. USACO submit also
wasted a roundtrip on `_check_usaco_login` every time.

## Solution

Always validate credentials in the login path. Remove cookie/token
loading from `_login_headless` (AtCoder), `_login_headless_cf` (CF),
`_login_headless_codechef` (CodeChef), and `login` (CSES). For USACO
submit, replace the `_check_usaco_login` roundtrip with cookie trust +
retry-on-auth-failure (the Kattis pattern). Submit paths are unchanged —
cookie fast-paths remain for contest speed.

Closes #331
2026-03-06 17:53:22 -05:00
Barrett Ruth
8465e70772
test: add offline fixture coverage for Kattis and USACO (#342)
## Problem

Kattis and USACO had zero offline test coverage — no fixtures, no
conftest
routers, and no entries in the test matrix. The `precision` field and
error
paths were also unverified across all platforms.

## Solution

Add HTML fixtures for both platforms and wire up `httpx.AsyncClient.get`
routers in `conftest.py` following the existing CSES/CodeChef pattern.
Extend the test matrix from 12 to 23 parametrized cases. Add a dedicated
test for the Kattis contest-vs-slug fallback path (verifying
`contest_url`
and `standings_url`), three parametrized metadata error cases, and a
targeted assertion that `extract_precision` returns a non-`None` float
for
problems with floating-point tolerance hints.

Closes #281.
2026-03-06 16:49:49 -05:00
Barrett Ruth
9727dccc6f
feat(commands): add tab completion for :CP open (#338)
## Problem

`:CP open` accepts `problem`, `contest`, and `standings` but offered no
tab completion for them.

## Solution

Add an `args[2] == 'open'` branch in the `plugin/cp.lua` completer
returning the three valid subcommands.
2026-03-06 16:14:58 -05:00
Barrett Ruth
4ef00afe66
feat(commands): add :CP <platform> signup subcommand (#337)
## Problem

No quick way to reach a platform's registration page from within Neovim.

## Solution

Add `signup` as a platform subcommand that calls `vim.ui.open` on the
platform's registration URL. URLs live in a new `SIGNUP_URLS` table in
`constants.lua`. Works even when the platform is disabled. Tab
completion and vimdoc updated.
2026-03-06 16:14:33 -05:00
Barrett Ruth
e89c57558d
feat(config): merge platform config model and add disabled-platform guard (#336)
## Problem

Supplying any `platforms` table silently dropped all unlisted platforms,
making it easy to accidentally disable platforms. Invoking a disabled
platform produced no user-facing error.

## Solution

Switch to a merge model: all six platforms are enabled by default and
user entries are deep-merged on top. Set a platform key to `false` to
disable it explicitly. Add a `check_platform_enabled` guard in
`handle_command` for contest fetch, login, logout, and race actions.
2026-03-06 16:14:16 -05:00
Barrett Ruth
82640709d6
fix(kattis,usaco): precision, open URLs, and Kattis submit error surface (#335)
## Problem

Kattis and USACO problem tests never extracted float precision, so
epsilon problems got no tolerance. Kattis `scrape_contest_metadata`
omitted `contest_url` and `standings_url`, breaking `:CP open
contest/standings`. Kattis submit always returned success even when the
server responded with an error (e.g. "You need to join the contest").

## Solution

Call `extract_precision` on problem HTML in both scrapers and emit it in
the JSON payload. Set `contest_url` and `standings_url` on Kattis
metadata paths. After Kattis submit, check for `Submission ID:` in the
response and surface the error text if absent.
2026-03-06 15:23:55 -05:00
c4be8b4f9e
fix: remove curl_cffi 2026-03-06 15:05:42 -05:00
Barrett Ruth
0b40e0f33e
fix: replace curl_cffi with scrapling in codeforces metadata (#334)
## Problem

`codeforces.py` used `curl_cffi` to bypass Cloudflare when fetching
contest problem HTML, making it unavailable in the nix python env and
requiring an extra dependency across `pyproject.toml` and `flake.nix`.

## Solution

Rewrite `_fetch_problems_html` to use scrapling `StealthySession` with
`solve_cloudflare=True`, matching the existing CF submit pattern. Extend
`needs_browser` in `scraper.lua` to route CF `metadata` and `tests`
through the FHS env on NixOS. Remove `curl-cffi` from `pyproject.toml`,
`flake.nix`, and test mocks.
2026-03-06 13:25:44 -05:00
Barrett Ruth
6a3d2fe4f8
fix(codechef): rewrite contest list, drop curl_cffi (#333)
## Problem

`scrape_contest_list` made O(N) requests — one per Starters number up to
~200 — to discover division sub-contests via `child_contests`. `run_one`
also fetched problem HTML via `curl_cffi` solely to extract the memory
limit, which is unavailable in the nix python env.

## Solution

Use `/api/list/contests/all` directly: filter to `^START\d+$` codes and
map to `ContestSummary` in a single request. Remove `_fetch_html_sync`,
`MEMORY_LIMIT_RE`, and `_extract_memory_limit`; hardcode `memory_mb =
256.0` and `precision = None` in `run_one`.
2026-03-06 13:25:31 -05:00
Barrett Ruth
ba5ae8df69
fix(kattis): fix nil display_name in contest picker (#332)
## Problem

`ContestSummary.display_name` defaults to `None`, which serializes to
JSON `null` → Lua `vim.NIL`. The contest picker displayed "vim.NIL" for
every entry. Additionally, `start_time` was always stored even when
null, because `vim.NIL` is truthy in Lua.

## Solution

Pass `display_name=name` explicitly in `_parse_contests_page` so JSON
never emits `null`. In `cache.lua` `set_contest_summaries`, coerce
`display_name` via a `~= vim.NIL` guard and apply the same guard before
storing `start_time`.
2026-03-06 13:25:17 -05:00
Barrett Ruth
b6d3df03e3
fix(scrapers): fix Kattis and USACO login and submit (#330)
## Problem

Kattis and USACO login and submit were broken in multiple ways
discovered
during manual end-to-end testing. Neither platform could successfully
authenticate or submit through the plugin.

## Solution

**Kattis:** switch login from `POST /login/email` (requires CSRF fetch)
to
`POST /login` with `script=true` (200 = success, 403 = bad credentials);
remove `_check_kattis_login` entirely since Kattis blocks all GET
requests
from httpx; add submit retry on `"Request validation failed"` to handle
expired sessions; fix language ID `"C++17"` → `"C++"`.

**USACO:** fix login field `user` → `uname`; fix success check to
`code==1`;
fix submit endpoint to `submit-solution.php`, file field to
`sourcefile`,
hidden field extraction off-by-one (`group(2)` → `group(1)`); fix
`_pick_lang_option` loop order (keywords outer, options inner) so
specific
keywords like `"c++17"` match before broad ones like `"c++"`.

**`submit.lua`:** absolutize source file path via `fnamemodify(...,
':p')`
before passing to the scraper — Python is spawned with `cwd=plugin_path`
so relative paths silently fail with `FileNotFoundError`.

**Both platforms:** remove cookie fast path from `login` subcommand so
credentials are always validated, preventing stale cookies from masking
wrong credentials.
2026-03-06 12:38:32 -05:00
Barrett Ruth
543480a4fe
feat: implement login and submit for USACO, Kattis, and CodeChef (#325)
## Problem

Login and submit were stub-only for USACO, Kattis, and CodeChef, leaving
three platforms without a working solve loop.

## Solution

Three commits, one per platform:

**USACO** — httpx-based login via `login-session.php` with cookie cache.
Submit fetches the problem page and parses the form dynamically (action
URL, hidden fields, language select) before POSTing multipart with
`sub_file[]`.

**Kattis** — httpx-based login via `/login/email` (official Kattis CLI
API). Submit is a multipart POST to `/submit`; the `contest` field is
included only when `contest_id != problem_id`. `scrape_contest_list`
URL updated to filter
`kattis_original=on&kattis_recycled=off&user_created=off`.

**CodeChef** — StealthySession browser-based login and submit following
the AtCoder/CF pattern. Login checks `__NEXT_DATA__` for current user
and fills the email/password form. Submit navigates to
`/{contest_id}/submit/{problem_id}`, selects language (standard
`<select>` first, custom dropdown fallback), sets code via
Monaco/CodeMirror/textarea JS, and clicks submit. Retry-on-relogin
pattern matches existing CF behaviour.

Language IDs added to `language_ids.py` for all three platforms.
2026-03-06 00:09:16 -05:00
Barrett Ruth
2d50f0a52a
refactor: remove open_url config option in favour of :CP open (#322)
## Problem

`open_url` automatically opened the browser on contest load and problem
change, which is now redundant with `:CP open`.

## Solution

Remove the `open_url` field from `cp.Config`, its default, its
validation, and the call site in `setup_contest`. Remove documentation
from `cp.nvim.txt`.
2026-03-05 22:55:43 -05:00
Barrett Ruth
401afb205e
feat(commands): document :CP open [problem|contest|standings] (#321)
forgot to document `:CP open`
2026-03-05 19:51:06 -05:00
Barrett Ruth
7bf4cf2538
feat(commands): implement :CP open [problem|contest|standings] (#319)
## Problem

There was no way to open a problem, contest, or standings page in the
browser from within the plugin.

## Solution

Add `contest_url` and `standings_url` to `MetadataResult` and persist
them in the cache. Add `cache.get_open_urls` to resolve all three URLs.
Wire up `:CP open [problem|contest|standings]` in `commands/init.lua`
to call `vim.ui.open`, warning when a URL is unavailable (e.g. CSES
has no standings). Closes #315.
2026-03-05 19:16:05 -05:00
Barrett Ruth
b959f29bd4
fix(scrapers): harden CSES and CF submit edge cases (#295) (#318)
## Problem

CSES `_web_login` used bare dict indexing on the API response, raising
an opaque `KeyError` on missing fields. `_check_token` swallowed all
exceptions as `False`, conflating transient network errors with invalid
tokens. CF persisted cookies unconditionally and silently swallowed
`_solve_turnstile` failures in `submit_action`.

## Solution

CSES API fields now use `.get()` with a descriptive `RuntimeError` on
absence. `_check_token` re-raises `httpx` network/timeout exceptions
so callers see real failures. CF cookie writes are guarded by an
`X-User-Handle` check (the CF auth cookie). `_solve_turnstile` errors
propagate to the outer error handler instead of being silenced.
2026-03-05 19:00:35 -05:00
Barrett Ruth
6c036a7b2e
fix: systematic context guards and full panel lifecycle (#317)
## Problem

The `CONTEST_ACTIONS` list in `handle_command` was a manually maintained
parallel of `ACTIONS` that had already drifted (`pick` was incorrectly
included, breaking `:CP pick` cold). `navigate_problem` only cancelled
the `'run'` panel on `next`/`prev`, leaving interactive and stress
terminals alive and in-flight `run_io_view` callbacks unguarded.
`pick` and `cache` also appeared twice in tab-completion when a contest
was active.

## Solution

Replace `CONTEST_ACTIONS` with a `requires_context` field on
`ParsedCommand`,
set at each parse-site in `parse_command` so the semantics live with the
action definition. Expand `navigate_problem` to call `cancel_io_view()`
and
dispatch `cancel_interactive`/`stress.cancel` for all panel types,
mirroring
the contest-switch logic. Filter already-present `pick` and `cache` from
the
context-conditional completion candidates.
2026-03-05 18:29:41 -05:00
Barrett Ruth
7e7e135681
fix: cancel active process on contest switch (#316)
## Problem

Switching contests while a run, interactive session, or stress test was
active left orphaned callbacks and terminal jobs alive. Running `:CP
run`
twice also let the first run's stale output overwrite the buffer.

## Solution

Replace the `io_view_running` bool in `views.lua` with a generation
counter so concurrent `run_io_view` calls self-cancel via stale-gen
checks in async callbacks. Add `cancel_io_view`, `cancel_interactive`,
and `stress.cancel` for forceful teardown, and call them in
`setup_contest` whenever `is_new_contest` is true.
2026-03-05 17:42:50 -05:00