Skip to content

Download control: sizes and speed, ids and cancel, pause and resume - #17

Merged
iStorry merged 5 commits into
mainfrom
feat/download-control
Oct 20, 2026
Merged

iStorry merged 5 commits into
mainfrom
feat/download-control

Conversation

@iStorry

@iStorry iStorry commented Oct 20, 2026

Copy link
Copy Markdown
Owner

The first half of download control: sizes and speed, a download id that follows its list item, cancel, and pause and resume. Each feature reaches the checker, interpreter, compiled apps, the runtime, the LSP, the highlighters, the reference docs, fixtures/downloader.nf and the roadmap. Several connections per file, choosing the file name, and headers and retries come in a follow-up.

What's in it

1. Bytes, size and speed (e72d2ed)

  • download(url, to:, progress: $p, status: $s, received: $r, total: $t, speed: $v). received, total and speed are optional int bindings.
  • total is 0 until the server gives the size. It is set to the file's size once the file is saved.
  • speed is in bytes per second, averaged over the last 2 seconds. The bindings update at most 10 times a second.
  • New built-in format_bytes(n) -> string, for example 1.5 KB. It counts in 1000s, like macOS and browsers. It only reads, so the UI can call it.
  • The fields a download can bind are now one table, DownloadField in nifty-runtime. The checker, interpreter, compiled runtime and codegen all read it. Compiled apps pass one DownloadDesc.

2. Download ids and cancel (c67ec14)

  • download(…, id: $items[last].id) writes an id such as download-3. cancel_download(id) stops the download, removes the partial file and sets status to Cancelled.
  • id: is required whenever a download writes into a list item. This is syntax decision row 24.
  • Fixes the list-position bug: a download's reports now follow the item holding its id, even when earlier rows are removed or the list is rebuilt. Removing the item, or overwriting its id, cancels the download.
  • Each download has a coordinator thread, plus a connection thread whose output the coordinator can close at any time. Cancel and pause never wait on the network.
  • nf dev reload stops every download and removes its partial file before the new program starts.
  • Pins the Zed grammar to 7f40a5b, the merge of UI elements: progress, scroll, controls, images, menus, windows from code and drops #16.

3. Pause and resume (476fe9f)

  • pause_download(id) keeps the partial file and sets status to Paused.
  • resume_download(id) sends Range: bytes=N-.
    • A 206 answer with a Content-Range continues from there.
    • A server that sends the whole file again restarts from zero; the docs say so.
    • A 206 without a Content-Range fails rather than guessing.
  • Downloads are written to name.part and renamed to the first free final name once saved, so a leftover file is clearly unfinished.

The compiled app interface (ABI_VERSION) is now 4.

Design decisions to review

  1. Ids: the id is a string, not a handle type.
  2. Removing a row cancels its download.
  3. nf dev reload stops downloads rather than carrying them into the new program.
  4. Quitting on macOS leaves .part files behind, because AppKit quits with exit(). Fixing it needs an on-quit hook in both backends.
  5. Unfinished files are named name.part until saved.
  6. total is 0 when the size is unknown, rather than a special value.
  7. format_bytes counts in 1000s. Windows Explorer counts in 1024s; the docs say so.
  8. The fixture's Cancel, Pause and Resume buttons appear by comparing status to the literal strings "Downloading…" and "Paused". Enums would make this cleaner.

Checked

  • make check passes: fmt, clippy (-D warnings) on macOS and with --target x86_64-pc-windows-msvc, build, cargo test --workspace, the tree-sitter corpus (32/32) and the VS Code grammar test.
  • The tests run against a local HTTP server and are deterministic: the server stalls a response until the test lets it go, with no sleeps. They cover:
    • with and without Content-Length
    • cancel, including while paused
    • reports following their row after an earlier row is removed, and removing a row cancelling its download
    • reload removing the partial file
    • resume sending range: bytes=4-
    • a server without range support restarting and still producing the correct file
  • The interpreter and compiled tests both run fixtures/downloader.nf.
  • Nothing platform-specific was checked by hand on macOS; only the automated tests ran.

Not yet checked by hand

macOS:

  • Download, cancel, pause and resume a real file in fixtures/downloader.nf. Check the sizes and speed it shows.

Windows:

  • The same run through fixtures/downloader.nf.
  • Cancel and resume while another program, such as an antivirus scan or Explorer's preview pane, has the .part file open.
  • .part files are removed when the app quits.
  • Renaming the .part file over the placeholder when a download is saved, including when a file with that name appeared during the download.

Follow-ups

  • Several connections per file, choosing the file name, and request headers with retries.
  • docs/src/guide/bindings.md still says only text_field and checkbox take bindings.

iStorry and others added 5 commits October 20, 2026 17:33
…bytes

download takes optional received:, total: and speed: bindings next to
progress: and status:. The fields a download can bind are one table,
DownloadField in nifty-runtime, which the checker, the interpreter, the
compiled runtime and codegen all read; a report says which fields it
writes, so each runner applies it the same way. Progress is reported at
most every 100 ms, with the speed averaged over the last two seconds.

format_bytes(n) is the first built-in that gives a value; it only reads,
so the UI may call it. Sizes use units of 1000, as macOS and browsers do.

Compiled apps now pass a download as one DownloadDesc with a list of
bindings, so ABI_VERSION goes to 3.
download takes id: $string, and writes a process-wide unique id such as
download-3 into it. cancel_download(id) stops that download and removes its
partial file; status becomes Cancelled. A download reporting into a list
item must bind id: in that item. Before each report and after each action,
the runtime finds the item holding the id and moves the bindings that share
its path, so removing or rebuilding rows no longer sends reports to the
wrong row; a download whose item is gone is cancelled. The search is shared
by the interpreter and compiled apps through nifty-download's StateReader.

Each download now runs as a coordinator thread that handles one ordered
stream of events: its connection's progress and the app's commands. The
connection does the blocking network reads and writes the file through an
Outlet the coordinator can close at any time, so cancelling never waits on
the network. Dropping Downloads, as nf dev does on reload, stops every
download and removes its partial file before returning.

The test server serves many requests and can stall mid-body until the
test resumes it, so tests cancel and reorder rows without sleeping.

Also drops the roadmap's in-progress entry for #16, now merged, and pins
the Zed grammar to 7f40a5b. ABI_VERSION goes to 4.
pause_download(id) closes the download's connection and keeps its partial
file; status becomes Paused. resume_download(id) opens a new connection
asking for the rest with Range: bytes=N-. A server that answers 206 with a
Content-Range continues from there; one that ignores the range sends the
whole file, and the download starts over from zero. A 206 without a
Content-Range fails rather than guessing.

A download now writes to name.part and takes its final name, the first
free one from its copy number on, once saved, so a file left behind when
macOS ends the app mid-download reads as unfinished. Each connection's
events carry its id, and the outlet counts what it wrote, so a pause knows
exactly what is on disk even with events from the old connection still
queued.

The test server answers range requests unless told not to.
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
@iStorry
iStorry merged commit f2a6803 into main Oct 20, 2026
5 checks passed
@iStorry
iStorry deleted the feat/download-control branch October 20, 2026 09:01
@iStorry
iStorry restored the feat/download-control branch October 20, 2026 09:01
@iStorry
iStorry deleted the feat/download-control branch October 21, 2026 08:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant