windscout 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -116,7 +116,7 @@ Run `windscout` with no arguments to open the interactive picker:
116
116
 
117
117
  Your selection is saved to `~/.config/windscout/config.json` (`%USERPROFILE%\.config\windscout\config.json` on Windows) and preselected next time.
118
118
 
119
- After you start, the Start button spins (`[ ⠋ Starting… ]`) until the scan's screen shows, which can take a moment when it turns the firewall on or starts docker. While scanning, WindScout shows a progress bar, elapsed time and remaining time. It also shows what it's doing right now (connecting, measuring latency or measuring speed). The table of working locations refreshes after each country. When the scan finishes, the final report is printed to your normal terminal. It ends with the best connection and the other working ones, each with its `windscribe-cli connect` command. At the prompt, press enter to connect to the best one, type another one's number to connect to that, or `q` to quit.
119
+ After you start, the Start button spins (`[ ⠋ Starting… ]`) until the scan's screen shows, which can take a moment when it turns the firewall on or starts docker. While scanning, WindScout shows a progress bar, elapsed time and remaining time. It also shows what it's doing right now (connecting, measuring latency or measuring speed). The table of working locations refreshes after each country. When the scan finishes, the final report is printed to your normal terminal. It ends with the best connection and the other working ones, each with its `windscribe-cli connect` command. At the prompt, press enter or `0` to connect to the best one, type another one's number and press enter to connect to that, or press `q` to quit. `0` and `q` work right away, without enter.
120
120
 
121
121
  ### Non-interactive
122
122
 
@@ -164,7 +164,7 @@ Press `o` in the picker, or pass `-o`, to choose what a scan tries and in which
164
164
  | default | `default` | the selected countries × protocols, the countries in list order |
165
165
  | by random country | `random` | the same, with the countries shuffled, a new order each run |
166
166
  | by last success result first | `last-first` | the same, but the [last result](#last-result-rescan-and-connect)'s working connections among them go first, best first and on their own port, then the rest. With one country selected, only that country is scanned, its saved connections first. The line shows how many saved connections are in your selection (`1 of 7 saved selected · 3h ago`) |
167
- | only the last results | `last-only` | only the last result's working connections, each on its own protocol and port. The countries, protocols and all ports are skipped, so the picker greys them out and locks them (your selection stays for next time). With `-o last-only`, `-c` isn't needed |
167
+ | only the last results | `last-only` | only the last result's working connections, each on its own protocol and port. The countries, protocols and all ports are skipped, so the picker greys them out and locks them. Its country list shows just the countries this scan tries, all selected; your own selection comes back when you change the type, and stays for next time. With `-o last-only`, `-c` isn't needed |
168
168
 
169
169
  The two types by the last result need a saved scan. Until there is one, choosing either turns `[ ▶ Start ]` off (greyed out), and a warning takes the estimate's place: `no saved scan yet, so start is off: press o for another scanning type`. Without the picker, `-o last-first` and `-o last-only` stop with an error instead. The estimate counts each saved connection once: `tcp` and `tcp:443` are the same attempt when 443 is tcp's default port.
170
170
 
@@ -198,7 +198,7 @@ Both modes show an **availability grid**: one row per country, and ✓ / ✗ for
198
198
  ╰─────────────────────────┴────────────────────────┴─────────┴───────────┴─────╯
199
199
  ```
200
200
 
201
- While scanning, press `tab` to switch between the availability grid and the speed or latency ranking. Press `p` (or `space`) to pause: the current attempt finishes, the VPN disconnects, and the scan waits until you press `p` again. Paused time doesn't count toward elapsed or remaining. In connect-only mode the scan starts on the availability grid.
201
+ While scanning, press `tab` to switch between the availability grid and the speed or latency ranking. Press `p` (or `space`) to pause: the current attempt finishes, the VPN disconnects, and the scan waits until you press `p` again. Paused time doesn't count toward elapsed or remaining. The scan starts on the ranking (*Working so far*) until you switch: then the next scans start on the table you left it on (saved to `~/.config/windscout/view.json`).
202
202
 
203
203
  ## All ports
204
204
 
@@ -248,7 +248,7 @@ Then the first parallel scan builds a local image, `windscout-worker`, with Node
248
248
 
249
249
  The mirrors come from [Mirava](https://github.com/MiravaOrg/Mirava)'s list of mirrors in Iran; they come and go, and that's where to look for new ones.
250
250
 
251
- **Kernel check:** each container first checks that docker's Linux kernel gives it what Windscribe needs: a tunnel device (tun) and a firewall (nftables). If not, the scan stops and says so, rather than showing every location as blocked. Windscribe's WireGuard also needs nftables' `fib` in its firewall table, which Docker Desktop's own Linux VM lacks (on macOS, and on Windows without WSL 2). There, WireGuard runs on this computer instead (see below).
251
+ **Kernel check:** each container first checks that docker's Linux kernel gives it what Windscribe needs: a tunnel device (tun) and a firewall (nftables). If not, the scan stops and says so, rather than showing every location as blocked. Windscribe's WireGuard also needs nftables' `fib` in its firewall table, which Docker Desktop's own Linux VM lacks (on macOS, and on Windows without WSL 2). There, a parallel scan skips WireGuard (see below).
252
252
 
253
253
  **Login:** the containers' Windscribe apps log in with the session that the Windscribe app on this computer is logged in with, so they never log in with your password. Windscribe locks an account that logs in many times at once, and a login per container would do that. WindScout reads the session from the app's settings (`~/.config/Windscribe/Windscribe2.conf` on Linux, the `com.windscribe.Windscribe2` defaults on macOS, `HKEY_CURRENT_USER\Software\Windscribe\Windscribe2` in the registry on Windows) and hands it to each container on its standard input, not on a command line, where other programs could see it. Each container's docker volume (`windscout-1`, `windscout-2`, …) keeps its app's settings for the next scans, with the WireGuard keys it registered with your account. A volume that another account used starts over. If you log out of the Windscribe app, the containers' session ends too, and the next scan gives them the new one.
254
254
 
@@ -260,7 +260,7 @@ WindScout 0.6.0 asked for your username and password instead, and could save the
260
260
 
261
261
  **Differences from a normal scan:**
262
262
 
263
- - IKEv2, and WireGuard when the kernel check finds it can't run in the containers, are tried on this computer once the containers are done, one at a time like a normal scan. The scan screen shows them on a line of their own (`this computer`), and the picker's estimate counts them. Windscribe's Linux app has no IKEv2.
263
+ - The containers do the whole scan: nothing runs on this computer. What they can't run is skipped: IKEv2 (Windscribe's Linux app has none), and WireGuard when the kernel check finds it can't run in the containers. The picker's docker line says so (`ikev2 skipped`), its estimate leaves them out, and a line after the scan lists them. To try them, scan without docker. If every container stops, the scan ends there, with what they found, and says why.
264
264
  - This computer's own Windscribe connection is disconnected at the start and stays off while the containers scan, since it would carry their traffic too.
265
265
  - No firewall: "block internet while scanning" isn't used, since it would block the containers' traffic too.
266
266
 
package/dist/cli.js CHANGED
@@ -49,10 +49,14 @@ Examples: windscout -c DE,NL,TR -p stealth,tcp:443
49
49
  windscout connect`;
50
50
  // "DE, NL,,TR" → ["DE", "NL", "TR"]
51
51
  const list = (s) => s.split(",").map((x) => x.trim()).filter(Boolean);
52
- const ask = (question) => new Promise((resolve) => {
52
+ /** The typed answer; one of instant is taken as soon as it's the whole answer, without enter */
53
+ const ask = (question, instant = []) => new Promise((resolve) => {
53
54
  const rl = createInterface({ input: process.stdin, output: out });
55
+ // runs after readline's own keypress listener, so rl.line has the key in it
56
+ const onKey = () => instant.includes(rl.line.toLowerCase()) && (out.write("\n"), resolve(rl.line), rl.close());
57
+ process.stdin.on("keypress", onKey);
54
58
  rl.on("SIGINT", () => rl.close()); // Ctrl-C / Ctrl-D: no answer, null
55
- rl.on("close", () => resolve(null));
59
+ rl.on("close", () => (process.stdin.off("keypress", onKey), resolve(null)));
56
60
  rl.question(question, (a) => (resolve(a.trim()), rl.close()));
57
61
  });
58
62
  /** Runs a result's connect command and checks it as the scan does: connected, on its port, with internet through it.
@@ -66,11 +70,12 @@ async function connectTo(r, timeout) {
66
70
  await disconnect();
67
71
  return false;
68
72
  }
69
- /** Asks which numbered connection to make (enter: the best), then runs its command and waits for it to connect */
73
+ /** Asks which numbered connection to make (enter: the best), then runs its command and waits for it to connect.
74
+ * 0 and q need no enter; the other numbers do, since 1 may be the start of 10 */
70
75
  async function offerConnect(choices, timeout) {
71
76
  const others = choices.length > 2 ? `, 1-${choices.length - 1} = other` : choices.length > 1 ? ", 1 = other" : "";
72
77
  for (;;) {
73
- const a = await ask(`\nconnect to # (enter or 0 = best${others}, q = quit): `);
78
+ const a = await ask(`\nconnect to # (enter or 0 = best${others}, q = quit): `, ["0", "q"]);
74
79
  if (a === null || /^q(uit)?$/i.test(a))
75
80
  return;
76
81
  const r = a === "" ? choices[0] : /^\d+$/.test(a) ? choices[Number(a)] : undefined;
@@ -218,34 +223,45 @@ async function main() {
218
223
  const attempts = plan(order, countries, protocols, fromLast?.results, lists);
219
224
  // the tables' columns: the protocols asked for, and the saved connections' own (only those, when only they are scanned)
220
225
  const scanned = [...new Set([...(order === "last-only" ? [] : protocols), ...attempts.map((x) => x.protocol)])];
221
- let probers = [probe], after; // after: what runs here once the containers are done
222
- const later = notInContainers(scanned, loadDockerWireGuard()); // as far as the last parallel scan knows
223
- const inContainers = attempts.filter((x) => !later.has(x.protocol)).length;
224
- if (docker && inContainers) {
225
- const started = await startParallel(inContainers);
226
+ // a parallel scan is the containers' alone: the protocols they can't run (IKEv2, WireGuard on some kernels) aren't tried
227
+ let probers = [probe], skipped = new Set();
228
+ const inContainers = () => attempts.filter((x) => !skipped.has(x.protocol));
229
+ const nothingLeft = () => new Error(`the docker containers can't run ${[...skipped].join(" or ")}, and nothing else is selected:`
230
+ + " scan without --docker (d in the picker)");
231
+ if (docker && attempts.length) {
232
+ skipped = notInContainers(scanned, loadDockerWireGuard()); // as far as the last parallel scan knows
233
+ if (!inContainers().length)
234
+ throw nothingLeft();
235
+ const started = await startParallel(inContainers().length);
226
236
  if (!started)
227
237
  return leaveScreen();
228
238
  saveDockerWireGuard(started.wireguard);
229
239
  probers = started.probers;
230
- after = { prober: probe, protocols: notInContainers(scanned, started.wireguard), name: "this computer" };
240
+ skipped = notInContainers(scanned, started.wireguard); // their kernel may not run WireGuard after all, or may now
241
+ if (!inContainers().length) {
242
+ await stopWorkers();
243
+ throw nothingLeft();
244
+ }
231
245
  await windscribe("disconnect"); // a VPN connection here would carry the containers' traffic too
232
246
  }
233
- const scan = new Scan(countries, scanned, { timeout, speedTest, only: attempts, stopAtFirst }, probers, after);
247
+ const scan = new Scan(countries, scanned.filter((p) => !skipped.has(p)), { timeout, speedTest, only: inContainers(), stopAtFirst }, probers);
234
248
  // not with containers: their traffic goes out through this computer, and the firewall would block it
235
- const blocking = firewall && !after && (await blockInternet());
249
+ const blocking = firewall && !docker && (await blockInternet());
236
250
  const view = tty ? showProgress(scan, blocking) : logProgress(scan);
237
251
  await scan.run(view);
238
- if (after)
252
+ if (docker)
239
253
  await stopWorkers();
240
254
  await restoreFirewall();
241
255
  view.close();
242
256
  for (const e of new Set(scan.errors))
243
257
  console.error(yellow(e));
258
+ if (skipped.size)
259
+ console.error(yellow(`not tried: ${[...skipped].join(", ")}, which the docker containers can't run (a scan without --docker can)`));
244
260
  if (scan.found)
245
261
  console.log(green("stopped at the first connection that held up (a speed test, then staying up)"));
246
262
  else if (scan.stopping)
247
263
  console.log(yellow("interrupted, showing partial results"));
248
- const choices = report(scan.results, scanned, { all: a.all, speedTest: speedTest || stopAtFirst }); // stop at the first measures speed
264
+ const choices = report(scan.results, scan.protocols, { all: a.all, speedTest: speedTest || stopAtFirst }); // stop at the first measures speed
249
265
  // its working connections replace the saved ones it tried, so what stopped working drops out; the saved ones it didn't try
250
266
  // (other countries, or what a stopped scan didn't get to) stay, after its own; a scan where nothing worked keeps the last
251
267
  const tried = new Set(scan.results.filter((r) => r.status !== "interrupted").map((r) => `${r.query} ${r.protocol}`));
package/dist/config.js CHANGED
@@ -1,4 +1,5 @@
1
- // The picker's selection, what the last parallel scan found its containers can do, and the last scan's working results, remembered between runs.
1
+ // The picker's selection, what the last parallel scan found its containers can do, the last scan's working results, and the
2
+ // scan screen's table, remembered between runs.
2
3
  import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
4
  import { homedir } from "node:os";
4
5
  import { dirname, join } from "node:path";
@@ -11,6 +12,7 @@ export const MIN_TIMEOUT = 10; // seconds; shorter cuts off connections that wou
11
12
  export const CONFIG = join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "windscout", "config.json");
12
13
  const DOCKER_FILE = join(dirname(CONFIG), "docker.json"); // what the last parallel scan found its containers can do
13
14
  export const LAST_FILE = join(dirname(CONFIG), "last.json"); // the last scan's working connections
15
+ const VIEW_FILE = join(dirname(CONFIG), "view.json"); // the table the scan screen was left on
14
16
  /** The saved choice, minus anything this version doesn't offer; null on first run or an unreadable file */
15
17
  export function loadConfig() {
16
18
  try {
@@ -54,6 +56,22 @@ export function saveDockerWireGuard(wireguard) {
54
56
  }
55
57
  catch { } // not being able to remember isn't worth failing the scan
56
58
  }
59
+ /** The table the last scan was left on; the ranking until the user switches */
60
+ export function loadView() {
61
+ try {
62
+ return JSON.parse(readFileSync(VIEW_FILE, "utf8")).view === "availability" ? "availability" : "ranking";
63
+ }
64
+ catch {
65
+ return "ranking";
66
+ }
67
+ }
68
+ export function saveView(view) {
69
+ try {
70
+ mkdirSync(dirname(VIEW_FILE), { recursive: true });
71
+ writeFileSync(VIEW_FILE, JSON.stringify({ view }) + "\n");
72
+ }
73
+ catch { } // not being able to remember isn't worth failing the scan
74
+ }
57
75
  /** The last scan that found something; null when there's none or the file is unreadable */
58
76
  export function loadLast() {
59
77
  try {
package/dist/docker.js CHANGED
@@ -29,7 +29,7 @@ const REGISTRIES = [...(process.env.WINDSCOUT_REGISTRY_MIRRORS?.split(",").map((
29
29
  // openresolv: the app sets the tunnel's DNS through resolvconf; without it, DNS goes nowhere once connected.
30
30
  // The CMD first checks the kernel (WSL's builds tun and parts of nftables as modules): no tun or nftables, no tunnel, so
31
31
  // the container stops saying so instead of scanning. The helper's WireGuard routing also puts a fib rule in an inet table,
32
- // which Docker Desktop's own kernel (macOS, Windows without WSL) can't run: WS_WIREGUARD=0 then, and it runs on this computer.
32
+ // which Docker Desktop's own kernel (macOS, Windows without WSL) can't run: WS_WIREGUARD=0 then, and a parallel scan skips it.
33
33
  // No systemd in a container: start the helper (root) like its unit would; the worker starts the app once it has the session
34
34
  const DOCKERFILE = `FROM ${BASE_TAG}
35
35
  ENV LANG=C.UTF-8
@@ -107,8 +107,8 @@ export async function dockerState() {
107
107
  const WORKER_MEMORY = 250e6; // bytes per container: Windscribe's app and helper, a tunnel and node take ~120 MB
108
108
  // ponytail: past this, more containers mostly add load on the one uplink, and WireGuard keys on the account; raise if real scans say so
109
109
  const MAX_WORKERS = 16;
110
- /** Protocols the containers can't do, which a parallel scan runs on this computer after them: IKEv2 (Windscribe's Linux app
111
- * has none), and WireGuard when their kernel can't run its routing (see DOCKERFILE) */
110
+ /** Protocols the containers can't do, which a parallel scan skips: IKEv2 (Windscribe's Linux app has none), and WireGuard
111
+ * when their kernel can't run its routing (see DOCKERFILE) */
112
112
  export const notInContainers = (protocols, wireguard) => new Set(protocols.filter((p) => /^ikev2(:|$)/.test(p) || (!wireguard && /^wireguard(:|$)/.test(p))));
113
113
  /** Containers for this many attempts, when docker can run at most `most`: one per attempt; past that, the fewest that need no
114
114
  * more rounds (17 attempts, 16 at most: 9 containers make 2 rounds, as 16 would), since fewer log in sooner and register fewer keys */
package/dist/scan.js CHANGED
@@ -29,7 +29,6 @@ export class Scan {
29
29
  protocols;
30
30
  options;
31
31
  probers;
32
- after;
33
32
  results = [];
34
33
  finished = []; // countries with every attempt done: the views refresh their tables per country
35
34
  errors = []; // why slots stopped early
@@ -46,16 +45,13 @@ export class Scan {
46
45
  idle = 0; // seconds spent paused
47
46
  resume = () => { };
48
47
  unpaused = Promise.resolve();
49
- constructor(countries, protocols, options, probers = [probe], after) {
48
+ constructor(countries, protocols, options, probers = [probe]) {
50
49
  this.countries = countries;
51
50
  this.protocols = protocols;
52
51
  this.options = options;
53
52
  this.probers = probers;
54
- this.after = after;
55
53
  this.total = this.attempts().length;
56
54
  this.slots = probers.map(() => ({ current: null, step: "", attemptStart: now() }));
57
- if (after)
58
- this.slots.push({ current: null, step: "", attemptStart: now(), name: after.name });
59
55
  }
60
56
  /** Seconds spent scanning, pauses not counted */
61
57
  elapsed() {
@@ -87,15 +83,12 @@ export class Scan {
87
83
  const left = new Map(); // attempts to go, per country
88
84
  for (const { country } of queue)
89
85
  left.set(country, (left.get(country) ?? 0) + 1);
90
- const later = this.after?.protocols ?? new Set();
91
- await Promise.all(this.probers.map((prober, i) => this.work(prober, this.slots[i], (p) => !later.has(p), queue, left, hooks)));
92
- // then the rest, also what slots whose container stopped left over
93
- if (this.after)
94
- await this.work(this.after.prober, this.slots[this.probers.length], () => true, queue, left, hooks);
86
+ // when every slot's container stops, what they left isn't tried: scan.errors says why
87
+ await Promise.all(this.probers.map((prober, i) => this.work(prober, this.slots[i], queue, left, hooks)));
95
88
  return this.results;
96
89
  }
97
- /** One slot: takes the attempts it can make from the queue, one at a time, until there are none */
98
- async work(prober, slot, can, queue, left, hooks) {
90
+ /** One slot: takes the attempts from the queue, one at a time, until there are none */
91
+ async work(prober, slot, queue, left, hooks) {
99
92
  const { timeout, speedTest, stopAtFirst = false } = this.options;
100
93
  for (;;) {
101
94
  while (this.paused && !this.stopping) {
@@ -105,10 +98,9 @@ export class Scan {
105
98
  }
106
99
  await this.unpaused;
107
100
  }
108
- const i = this.stopping ? -1 : queue.findIndex((a) => can(a.protocol));
109
- if (i < 0)
101
+ const next = this.stopping ? undefined : queue.shift();
102
+ if (!next)
110
103
  return;
111
- const [next] = queue.splice(i, 1);
112
104
  const { country, protocol } = next;
113
105
  Object.assign(slot, { current: next, step: "", attemptStart: now() });
114
106
  hooks.attempt?.(country, protocol);
@@ -41,12 +41,15 @@ export function pick(saved, { timeout, speedTest, firewall, order, stopAtFirst,
41
41
  };
42
42
  const ports = (p) => (allPorts && portLists?.get(p)?.length) || 1; // attempts per country for the protocol
43
43
  const perCountry = (ps) => [...ps].reduce((n, p) => n + ports(p), 0);
44
- // what start scans, in its order: as the CLI plans it, so the estimate counts the same attempts
44
+ const wireguard = loadDockerWireGuard(); // in docker containers, as the last parallel scan found
45
+ // what start scans, in its order: as the CLI plans it, so the estimate counts the same attempts. A parallel scan is the
46
+ // containers' alone: the protocols they can't run are skipped
45
47
  const todo = () => {
46
48
  const ps = PROTOCOLS.filter((p) => protos.has(p));
47
- return plan(order, COUNTRIES.filter((c) => selected.has(c)), allPorts && portLists ? withPorts(ps, portLists) : ps, last?.results, portLists ?? undefined);
49
+ const all = plan(order, COUNTRIES.filter((c) => selected.has(c)), allPorts && portLists ? withPorts(ps, portLists) : ps, last?.results, portLists ?? undefined);
50
+ const cant = notInContainers(docker ? all.map((a) => a.protocol) : [], wireguard);
51
+ return { list: all.filter((a) => !cant.has(a.protocol)), skipped: [...new Set([...cant].map((p) => p.split(":")[0]))] };
48
52
  };
49
- const wireguard = loadDockerWireGuard(); // in docker containers, as the last parallel scan found
50
53
  enterScreen();
51
54
  return new Promise((resolve) => {
52
55
  const render = () => {
@@ -54,24 +57,24 @@ export function pick(saved, { timeout, speedTest, firewall, order, stopAtFirst,
54
57
  return;
55
58
  const rows = out.rows || 24;
56
59
  const version = versionLines(true);
57
- const matches = visible();
58
- const height = Math.max(3, rows - 24 - version.length);
59
- cursor = Math.max(0, Math.min(cursor, matches.length - 1));
60
- if (cursor < top)
61
- top = cursor;
62
- if (cursor >= top + height)
63
- top = cursor - height + 1;
64
- top = Math.max(0, Math.min(top, matches.length - height));
65
60
  const off = order === "last-only"; // only the saved connections: the countries, protocols and ports are skipped
66
61
  const missing = needsLast(order) && !last; // a type by the last result, and no saved scan: start is off
67
62
  timeout = Number(seconds);
68
- const list = todo(), n = list.length;
69
- const base = (p) => p.split(":")[0]; // "wireguard:443" → "wireguard"
70
- const scanned = new Set(list.map((a) => base(a.protocol)));
71
- const here = PROTOCOLS.filter((p) => docker && scanned.has(p) && notInContainers(PROTOCOLS, wireguard).has(p)); // after the containers
72
- const inContainers = list.filter((a) => !here.includes(base(a.protocol))).length;
73
- const parallel = docker && dockerAt === "ready" && inContainers > 0;
74
- const slots = parallel ? workerCount(inContainers) : 1;
63
+ const { list, skipped } = todo(), n = list.length;
64
+ // only the last results lists the countries it scans, all selected; the user's own selection is back with the other types
65
+ const matches = off ? [...new Set(list.map((a) => a.country))] : visible();
66
+ const height = Math.max(3, rows - 24 - version.length);
67
+ if (!off) { // so the selection's list keeps its cursor and scroll for then
68
+ cursor = Math.max(0, Math.min(cursor, matches.length - 1));
69
+ if (cursor < top)
70
+ top = cursor;
71
+ if (cursor >= top + height)
72
+ top = cursor - height + 1;
73
+ top = Math.max(0, Math.min(top, matches.length - height));
74
+ }
75
+ const from = off ? 0 : top; // the first row shown
76
+ const parallel = docker && dockerAt === "ready" && n > 0;
77
+ const slots = parallel ? workerCount(n) : 1;
75
78
  const login = parallel ? loginSeconds(slots) : 0; // the containers log in before the scan starts
76
79
  const protoLine = PROTOCOLS.map((p, i) => `${dim(under(String(i + 1)))} ${protos.has(p) ? (off ? dim : green)("● " + p) : dim("○ " + p)}`).join(" ");
77
80
  const listed = portLists && [...portLists.values()].some((l) => l.length), each = perCountry(protos);
@@ -95,8 +98,8 @@ export function pick(saved, { timeout, speedTest, firewall, order, stopAtFirst,
95
98
  const dockerLabel = "parallel scan in docker containers";
96
99
  const dockerLine = `${dim(under("d"))} ${docker ? green("● " + dockerLabel) : dim("○ " + dockerLabel)} ${dim(!docker
97
100
  ? "off: one attempt at a time"
98
- : dockerAt === "ready" ? `${slots} at a time${here.length ? ` · ${here.join(" and ")} here, after them` : ""}`
99
- : dockerAt ? "docker isn't ready: start sets it up" : "checking docker…")}`;
101
+ : (dockerAt === "ready" ? `${slots} at a time` : dockerAt ? "docker isn't ready: start sets it up" : "checking docker…")
102
+ + (skipped.length ? ` · ${skipped.join(" and ")} skipped` : ""))}`;
100
103
  const saves = last ? plan("last-only", [], [], last.results, portLists ?? undefined).length : 0; // the saved connections, each once
101
104
  const firsts = list.filter((a) => a.saved).length; // those this scan tries first
102
105
  const orderLine = `${dim(under("o"))} scanning type ${bold(ORDER_NAMES[order])} ${dim(missing
@@ -106,26 +109,26 @@ export function pick(saved, { timeout, speedTest, firewall, order, stopAtFirst,
106
109
  : order === "last-first" ? `${firsts} of ${saves} saved selected · ${ago(last.at)}`
107
110
  : `${saves} saved working connection${saves === 1 ? "" : "s"} · ${ago(last.at)}`)}`;
108
111
  const stopLabel = "stop at the first working connection";
109
- // the best saved connection goes first (unless it waits for the containers): when it still holds up, the scan ends there
110
- const lead = list[0]?.saved && !here.includes(base(list[0].protocol)) ? list[0] : null;
112
+ // the best saved connection goes first: when it still holds up, the scan ends there
113
+ const lead = list[0]?.saved ? list[0] : null;
111
114
  const stopLine = `${dim(under("w"))} ${stopAtFirst ? green("● " + stopLabel) : dim("○ " + stopLabel)} ${dim(!stopAtFirst
112
115
  ? "off: tries every attempt"
113
116
  : lead ? `≈ ${duration(login + STEADY_SECONDS)} if ${lead.country} ${lead.protocol} still holds up`
114
117
  : "after a speed and stability check")}`;
115
118
  const search = off
116
- ? yellow("countries skipped: only the last results are scanned")
119
+ ? yellow("the last results' countries: only these are scanned")
117
120
  : searching ? `${cyan("🔍")} ${bold(query)}${cyan("▏")}`
118
121
  : query ? `🔍 ${query}` : dim("🔍 press / to search");
119
- const shown = matches.slice(top, top + height);
120
- const range = matches.length > height ? `${top + 1}–${top + shown.length} of ${matches.length}` : `${matches.length} countries`;
122
+ const shown = matches.slice(from, from + height);
123
+ const range = matches.length > height ? `${from + 1}–${from + shown.length} of ${matches.length}` : `${matches.length} countries`;
121
124
  const body = shown.map((code, j) => {
122
125
  const current = !off && top + j === cursor;
123
- const mark = selected.has(code) ? (off ? dim : green)("●") : dim("○");
126
+ const mark = off || selected.has(code) ? (off ? dim : green)("●") : dim("○");
124
127
  const name = `${flag(code)} ${countryName(code)}`;
125
128
  return [`${current ? cyan("❯") : " "} ${mark}`, current ? bold(code) : dim(code), current ? bold(cyan(name)) : off ? dim(name) : name];
126
129
  });
127
130
  if (!body.length)
128
- body.push([" ", "", dim("no country matches")]);
131
+ body.push([" ", "", dim(off ? "no saved scan yet" : "no country matches")]);
129
132
  const buttons = [
130
133
  ["start", starting < 0 ? "▶ Start" : `${spin(starting++)} Starting…`],
131
134
  ["all", under("A") + (query ? "ll shown" : "ll")],
@@ -138,10 +141,10 @@ export function pick(saved, { timeout, speedTest, firewall, order, stopAtFirst,
138
141
  const width = vw(table[0]);
139
142
  const head = box(settings, {
140
143
  title: bold(cyan("WindScout")),
141
- right: `${(off ? dim : bold)(String(selected.size))}${dim(`/${COUNTRIES.length} selected`)}`,
144
+ right: `${(off ? dim : bold)(String(off ? matches.length : selected.size))}${dim(`/${COUNTRIES.length} selected`)}`,
142
145
  width: width - 4,
143
146
  });
144
- const total = login + estimate(inContainers, timeout, slots) + estimate(n - inContainers, timeout);
147
+ const total = login + estimate(n, timeout, slots);
145
148
  const costs = dim(`(~${attemptSeconds(timeout)}s each${slots > 1 ? `, ${slots} at a time` : ""}${login ? `, + ~${duration(login)} login` : ""})`);
146
149
  const lines = [
147
150
  ...head,
@@ -151,7 +154,8 @@ export function pick(saved, { timeout, speedTest, firewall, order, stopAtFirst,
151
154
  "",
152
155
  centerIn(missing
153
156
  ? yellow("no saved scan yet, so start is off: press o for another scanning type")
154
- : !n ? yellow(`select at least one country and one protocol${last ? ", or o for only the last results" : ""}`)
157
+ : !n ? yellow(skipped.length ? `the docker containers can't run ${skipped.join(" or ")}: d turns docker off`
158
+ : `select at least one country and one protocol${last ? ", or o for only the last results" : ""}`)
155
159
  : stopAtFirst // it can end at any attempt: this is when none holds up
156
160
  ? `up to ${bold(String(n))} attempts · at most ${bold("≈ " + duration(total))} ${costs}`
157
161
  : `${bold(String(n))} attempts · estimated ${bold("≈ " + duration(total))} ${costs}`, width),
@@ -207,7 +211,7 @@ export function pick(saved, { timeout, speedTest, firewall, order, stopAtFirst,
207
211
  const start = () => {
208
212
  if (needsLast(order) && !last)
209
213
  return false; // start is off: the warning under the table says why
210
- if (!todo().length) {
214
+ if (!todo().list.length) {
211
215
  message = "nothing to scan yet";
212
216
  return false;
213
217
  }
@@ -1,3 +1,4 @@
1
+ import { loadView, saveView } from "../config.js";
1
2
  import { sortOk } from "../probe.js";
2
3
  import { attemptSeconds } from "../scan.js";
3
4
  import { duration, now } from "../time.js";
@@ -8,13 +9,13 @@ import { matrix, resultTable } from "./tables.js";
8
9
  import { draw, enterScreen, input, leaveScreen, out, repoLink } from "./terminal.js";
9
10
  import { bold, cyan, dim, green, red, vw, yellow } from "./text.js";
10
11
  import { bar, box, centerIn, spin } from "./widgets.js";
11
- /** Progress bar, current attempt, results so far. tab switches the table, p pauses, q stops (twice: quits).
12
+ /** Progress bar, current attempt, results so far. tab switches the table (the next scans start on it), p pauses, q stops (twice: quits).
12
13
  * blocking: we turned the firewall on for the scan */
13
14
  export function showProgress(scan, blocking) {
14
15
  const { protocols, options: { timeout } } = scan;
15
16
  const speedTest = scan.options.speedTest || Boolean(scan.options.stopAtFirst); // stop at the first measures speed too
16
17
  let quitting = false; // q pressed: a second one quits right away
17
- let view = speedTest ? "ranking" : "availability"; // tab switches
18
+ let view = loadView(); // tab switches, and saves it for the next scans
18
19
  let frame = 0;
19
20
  const render = () => {
20
21
  const rows = out.rows || 24;
@@ -60,8 +61,8 @@ export function showProgress(scan, blocking) {
60
61
  const pausing = paused && !waiting && frame % 8 < 5;
61
62
  const slotLine = (s) => {
62
63
  if (!s.current)
63
- return s.step ? ` ${red("✗")} ${dim(clip(s.step, inner - 3))}` : dim(s.name ? ` · ${s.name}: after the containers` : " · idle");
64
- const head = ` ${cyan(spinner)} ${s.name ? dim(`${s.name}: `) : ""}${bold(attempt(s))} `, time = ` · ${duration(now() - s.attemptStart)}`;
64
+ return s.step ? ` ${red("✗")} ${dim(clip(s.step, inner - 3))}` : dim(" · idle");
65
+ const head = ` ${cyan(spinner)} ${bold(attempt(s))} `, time = ` · ${duration(now() - s.attemptStart)}`;
65
66
  return head + dim(clip(s.step, inner - vw(head) - time.length) + time);
66
67
  };
67
68
  const panel = box([
@@ -98,6 +99,7 @@ export function showProgress(scan, blocking) {
98
99
  input.on("keypress", async (_str, key = {}) => {
99
100
  if (key.name === "tab") {
100
101
  view = view === "ranking" ? "availability" : "ranking";
102
+ saveView(view);
101
103
  render();
102
104
  }
103
105
  else if (key.name === "p" || key.name === "space") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "windscout",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Find which Windscribe locations and protocols work from your network, ranked by speed (unofficial)",
5
5
  "type": "module",
6
6
  "bin": {