Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions site/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"devDependencies": {
"@astrojs/check": "^0.9.10",
"@axe-core/cli": "^4.13.0",
"chromedriver": "^152.0.3",
"linkedom": "^0.18.13",
"selenium-webdriver": "^4.48.0",
"typescript": "^5.5.0"
Expand Down
12 changes: 8 additions & 4 deletions site/src/content/docs/docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ NetsCLI publishes command-line binaries and desktop installers through GitHub Re

| Platform | Recommended path | Installs |
| --- | --- | --- |
| Windows | `winget install fstubner.netscli` | CLI and TUI |
| Windows | `winget install fstubner.netscli.gui` | Desktop app |
| Windows | `winget install netscli` | CLI and TUI |
| Windows | `winget install netscli-gui` | Desktop app |
| macOS | Homebrew or install script | CLI and TUI |
| Linux | Install script, Homebrew, AUR, or release artifact | CLI and TUI |
| Rust users | `cargo install netscli` | CLI and TUI from crates.io |
Expand All @@ -20,15 +20,19 @@ NetsCLI publishes command-line binaries and desktop installers through GitHub Re
Use winget for the hash-verified install path:

```powershell
winget install fstubner.netscli
winget install netscli
```

The desktop app is distributed separately:

```powershell
winget install fstubner.netscli.gui
winget install netscli-gui
```

Both short names resolve today. The full identifiers are `fstubner.netscli`
and `fstubner.netscli.gui`, and they cannot become ambiguous — use those if a
short name ever matches more than one package in the catalog.

Scoop is also supported, for both the CLI and the desktop app:

```powershell
Expand Down
38 changes: 32 additions & 6 deletions site/src/scripts/landing-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,30 @@ export function initLandingPage(repo: string): void {
never resolves. */
let githubDownloads: number | null = null;
let cratesDownloads: number | null = null;
// Whether each source has finished, which is not the same as whether it
// produced a number. A rate-limited source never sets its count, so
// gating on the counts alone would leave the label waiting forever.
let githubSettled = false;
let cratesSettled = false;
const renderDownloads = () => {
if (githubDownloads === null && cratesDownloads === null) return;
const total = (githubDownloads ?? 0) + (cratesDownloads ?? 0);
// "total" is a claim about both sources, so only make it once both
// have reported. Measured on this page: "Downloads: 178 total" with
// only GitHub in, then "Downloads: 2,635 total" once crates.io
// landed -- same label, same page, a fifteenfold difference. Until
// both are in, the number is a lower bound and now says so.
const complete = githubSettled && cratesSettled
&& githubDownloads !== null && cratesDownloads !== null;
const el = document.getElementById("downloads");
if (el) {
el.textContent = `${fmtDownloads(total)} ${total === 1 ? "download" : "downloads"}`;
el.dataset.totalDownloads = `Downloads: ${fmt(total)} total`;
// fmtDownloads already appends "+" above 1000; don't double it.
const shown = fmtDownloads(total);
const text = complete || shown.endsWith("+") ? shown : `${shown}+`;
el.textContent = `${text} ${total === 1 ? "download" : "downloads"}`;
el.dataset.totalDownloads = complete
? `Downloads: ${fmt(total)} total`
: `Downloads: ${fmt(total)} so far; one source has not reported`;
el.setAttribute("aria-label", el.dataset.totalDownloads);
el.hidden = false;
}
Expand All @@ -102,9 +119,15 @@ export function initLandingPage(repo: string): void {
const n = d?.crate?.downloads;
if (typeof n !== "number") return;
cratesDownloads = n;
renderDownloads();
})
.catch(() => {});
.catch(() => {})
// `finally`, not the success path: a failed or rate-limited fetch
// settles this source too, and the label has to know that to stop
// withholding the word "total".
.finally(() => {
cratesSettled = true;
renderDownloads();
});

fetch(`https://api.github.com/repos/${repo}/releases?per_page=100`)
.then((r) => (r.ok ? r.json() : null))
Expand All @@ -118,7 +141,6 @@ export function initLandingPage(repo: string): void {
),
0,
);
renderDownloads();
const latest = releases.find((release) => !release.draft && !release.prerelease)
?? releases.find((release) => !release.draft);
const versionEl = document.getElementById("latest-version");
Expand All @@ -130,7 +152,11 @@ export function initLandingPage(repo: string): void {
refreshMetricSeparators();
}
})
.catch(() => {});
.catch(() => {})
.finally(() => {
githubSettled = true;
renderDownloads();
});
})();

initCopyButtons();
Expand Down