windscout 0.7.0 → 0.9.0
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 +89 -16
- package/dist/background.js +131 -0
- package/dist/cli.js +180 -121
- package/dist/config.js +44 -5
- package/dist/docker.js +123 -25
- package/dist/measure.js +12 -0
- package/dist/probe.js +50 -24
- package/dist/scan.js +8 -16
- package/dist/time.js +5 -0
- package/dist/tui/connect.js +29 -0
- package/dist/tui/picker.js +80 -63
- package/dist/tui/progress.js +6 -4
- package/dist/tui/report.js +14 -4
- package/dist/tui/results.js +454 -0
- package/dist/tui/text.js +19 -0
- package/dist/watch.js +454 -0
- package/dist/windscribe.js +58 -10
- package/dist/worker.js +8 -6
- package/package.json +1 -1
package/dist/tui/picker.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { loadDockerWireGuard, MIN_TIMEOUT, saveConfig, SPEED_TEST } from "../config.js";
|
|
1
|
+
import { loadDockerWireGuard, MIN_HOLD, MIN_TIMEOUT, saveConfig, SPEED_TEST } from "../config.js";
|
|
2
2
|
import { dockerState, loginSeconds, notInContainers, workerCount } from "../docker.js";
|
|
3
|
-
import {
|
|
3
|
+
import { steadySeconds } from "../probe.js";
|
|
4
4
|
import { attemptSeconds, estimate, ORDERS, plan } from "../scan.js";
|
|
5
|
-
import { duration } from "../time.js";
|
|
5
|
+
import { ago, duration } from "../time.js";
|
|
6
6
|
import { canUpdate, checkUpdate, versionLines } from "../update.js";
|
|
7
7
|
import { COUNTRIES, protocolPorts, PROTOCOLS, withPorts } from "../windscribe.js";
|
|
8
8
|
import { countryName, flag } from "./country.js";
|
|
@@ -20,17 +20,13 @@ const needsLast = (o) => o === "last-first" || o === "last-only";
|
|
|
20
20
|
// only the last results: the countries, protocols and ports are greyed out, and their keys and clicks say why
|
|
21
21
|
const SKIPPED = "skipped with only the last results: o changes the scanning type";
|
|
22
22
|
const flip = (set, value) => (set.has(value) ? set.delete(value) : set.add(value));
|
|
23
|
-
// "5m ago", "3h ago", "2 days ago"
|
|
24
|
-
const ago = (ms) => {
|
|
25
|
-
const m = Math.max(0, Math.round((Date.now() - ms) / 60000)), h = Math.round(m / 60), d = Math.round(h / 24);
|
|
26
|
-
return m < 1 ? "just now" : m < 60 ? `${m}m ago` : h < 48 ? `${h}h ago` : `${d} days ago`;
|
|
27
|
-
};
|
|
28
23
|
/** Resolves to the choice (also saved for next time), null when the user quits, or "update" for u */
|
|
29
|
-
export function pick(saved, { timeout, speedTest, firewall, order, stopAtFirst, allPorts, docker, last }) {
|
|
24
|
+
export function pick(saved, { timeout, speedTest, firewall, order, stopAtFirst, hold, allPorts, docker, last }) {
|
|
30
25
|
const selected = new Set(saved?.countries ?? []);
|
|
31
26
|
const protos = new Set(saved?.protocols.length ? saved.protocols : PROTOCOLS);
|
|
32
27
|
let cursor = 0, top = 0, message = "", query = "", searching = false;
|
|
33
|
-
|
|
28
|
+
const typed = { timeout: String(timeout), hold: String(hold) }; // the settings in seconds, as typed
|
|
29
|
+
let editing = null; // the one being typed
|
|
34
30
|
let dockerAt = null, inDialog = false; // docker's state once checked; a docker dialog has the screen
|
|
35
31
|
let portLists = null; // each protocol's ports, once the app said
|
|
36
32
|
let starting = -1; // once start is pressed: the start button's spinner frame, until the next screen shows
|
|
@@ -41,12 +37,15 @@ export function pick(saved, { timeout, speedTest, firewall, order, stopAtFirst,
|
|
|
41
37
|
};
|
|
42
38
|
const ports = (p) => (allPorts && portLists?.get(p)?.length) || 1; // attempts per country for the protocol
|
|
43
39
|
const perCountry = (ps) => [...ps].reduce((n, p) => n + ports(p), 0);
|
|
44
|
-
|
|
40
|
+
const wireguard = loadDockerWireGuard(); // in docker containers, as the last parallel scan found
|
|
41
|
+
// what start scans, in its order: as the CLI plans it, so the estimate counts the same attempts. A parallel scan is the
|
|
42
|
+
// containers' alone: the protocols they can't run are skipped
|
|
45
43
|
const todo = () => {
|
|
46
44
|
const ps = PROTOCOLS.filter((p) => protos.has(p));
|
|
47
|
-
|
|
45
|
+
const all = plan(order, COUNTRIES.filter((c) => selected.has(c)), allPorts && portLists ? withPorts(ps, portLists) : ps, last?.results, portLists ?? undefined);
|
|
46
|
+
const cant = notInContainers(docker ? all.map((a) => a.protocol) : [], wireguard);
|
|
47
|
+
return { list: all.filter((a) => !cant.has(a.protocol)), skipped: [...new Set([...cant].map((p) => p.split(":")[0]))] };
|
|
48
48
|
};
|
|
49
|
-
const wireguard = loadDockerWireGuard(); // in docker containers, as the last parallel scan found
|
|
50
49
|
enterScreen();
|
|
51
50
|
return new Promise((resolve) => {
|
|
52
51
|
const render = () => {
|
|
@@ -54,24 +53,25 @@ export function pick(saved, { timeout, speedTest, firewall, order, stopAtFirst,
|
|
|
54
53
|
return;
|
|
55
54
|
const rows = out.rows || 24;
|
|
56
55
|
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
56
|
const off = order === "last-only"; // only the saved connections: the countries, protocols and ports are skipped
|
|
66
57
|
const missing = needsLast(order) && !last; // a type by the last result, and no saved scan: start is off
|
|
67
|
-
timeout = Number(
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
58
|
+
timeout = Number(typed.timeout);
|
|
59
|
+
hold = Number(typed.hold);
|
|
60
|
+
const { list, skipped } = todo(), n = list.length;
|
|
61
|
+
// only the last results lists the countries it scans, all selected; the user's own selection is back with the other types
|
|
62
|
+
const matches = off ? [...new Set(list.map((a) => a.country))] : visible();
|
|
63
|
+
const height = Math.max(3, rows - 25 - version.length);
|
|
64
|
+
if (!off) { // so the selection's list keeps its cursor and scroll for then
|
|
65
|
+
cursor = Math.max(0, Math.min(cursor, matches.length - 1));
|
|
66
|
+
if (cursor < top)
|
|
67
|
+
top = cursor;
|
|
68
|
+
if (cursor >= top + height)
|
|
69
|
+
top = cursor - height + 1;
|
|
70
|
+
top = Math.max(0, Math.min(top, matches.length - height));
|
|
71
|
+
}
|
|
72
|
+
const from = off ? 0 : top; // the first row shown
|
|
73
|
+
const parallel = docker && dockerAt === "ready" && n > 0;
|
|
74
|
+
const slots = parallel ? workerCount(n) : 1;
|
|
75
75
|
const login = parallel ? loginSeconds(slots) : 0; // the containers log in before the scan starts
|
|
76
76
|
const protoLine = PROTOCOLS.map((p, i) => `${dim(under(String(i + 1)))} ${protos.has(p) ? (off ? dim : green)("● " + p) : dim("○ " + p)}`).join(" ");
|
|
77
77
|
const listed = portLists && [...portLists.values()].some((l) => l.length), each = perCountry(protos);
|
|
@@ -90,13 +90,14 @@ export function pick(saved, { timeout, speedTest, firewall, order, stopAtFirst,
|
|
|
90
90
|
const fwLine = `${dim(under("f"))} ${firewall ? green("● " + fwLabel) : dim("○ " + fwLabel)} ${dim(docker
|
|
91
91
|
? "not used with docker"
|
|
92
92
|
: firewall ? "only VPN traffic gets through" : "off: apps may leak in between")}`;
|
|
93
|
-
|
|
94
|
-
const
|
|
93
|
+
// a setting in seconds: as typed, with the cursor while typing, yellow when it's under its least
|
|
94
|
+
const field = (k, least) => editing === k ? bold(typed[k]) + cyan("▏") : (Number(typed[k]) < least ? yellow : bold)(typed[k] || "?");
|
|
95
|
+
const timeoutLine = `${dim(under("t"))} timeout ${field("timeout", MIN_TIMEOUT)} s ${dim(`per connection · at least ${MIN_TIMEOUT}`)}`;
|
|
95
96
|
const dockerLabel = "parallel scan in docker containers";
|
|
96
97
|
const dockerLine = `${dim(under("d"))} ${docker ? green("● " + dockerLabel) : dim("○ " + dockerLabel)} ${dim(!docker
|
|
97
98
|
? "off: one attempt at a time"
|
|
98
|
-
: dockerAt === "ready" ? `${slots} at a time
|
|
99
|
-
|
|
99
|
+
: (dockerAt === "ready" ? `${slots} at a time` : dockerAt ? "docker isn't ready: start sets it up" : "checking docker…")
|
|
100
|
+
+ (skipped.length ? ` · ${skipped.join(" and ")} skipped` : ""))}`;
|
|
100
101
|
const saves = last ? plan("last-only", [], [], last.results, portLists ?? undefined).length : 0; // the saved connections, each once
|
|
101
102
|
const firsts = list.filter((a) => a.saved).length; // those this scan tries first
|
|
102
103
|
const orderLine = `${dim(under("o"))} scanning type ${bold(ORDER_NAMES[order])} ${dim(missing
|
|
@@ -106,26 +107,29 @@ export function pick(saved, { timeout, speedTest, firewall, order, stopAtFirst,
|
|
|
106
107
|
: order === "last-first" ? `${firsts} of ${saves} saved selected · ${ago(last.at)}`
|
|
107
108
|
: `${saves} saved working connection${saves === 1 ? "" : "s"} · ${ago(last.at)}`)}`;
|
|
108
109
|
const stopLabel = "stop at the first working connection";
|
|
109
|
-
// the best saved connection goes first
|
|
110
|
-
const lead = list[0]?.saved
|
|
110
|
+
// the best saved connection goes first: when it still holds up, the scan ends there
|
|
111
|
+
const lead = list[0]?.saved ? list[0] : null;
|
|
111
112
|
const stopLine = `${dim(under("w"))} ${stopAtFirst ? green("● " + stopLabel) : dim("○ " + stopLabel)} ${dim(!stopAtFirst
|
|
112
113
|
? "off: tries every attempt"
|
|
113
|
-
: lead ? `≈ ${duration(login +
|
|
114
|
-
:
|
|
114
|
+
: lead ? `≈ ${duration(login + steadySeconds(hold))} if ${lead.country} ${lead.protocol} still holds up`
|
|
115
|
+
: `≈ ${duration(steadySeconds(hold))} of checks per working one`)}`;
|
|
116
|
+
const holdLine = `${dim(under("h"))} hold ${field("hold", MIN_HOLD)} s ${dim(stopAtFirst
|
|
117
|
+
? `each working one stays connected this long · at least ${MIN_HOLD}`
|
|
118
|
+
: "not used: stop at the first is off")}`;
|
|
115
119
|
const search = off
|
|
116
|
-
? yellow("countries
|
|
120
|
+
? yellow("the last results' countries: only these are scanned")
|
|
117
121
|
: searching ? `${cyan("🔍")} ${bold(query)}${cyan("▏")}`
|
|
118
122
|
: query ? `🔍 ${query}` : dim("🔍 press / to search");
|
|
119
|
-
const shown = matches.slice(
|
|
120
|
-
const range = matches.length > height ? `${
|
|
123
|
+
const shown = matches.slice(from, from + height);
|
|
124
|
+
const range = matches.length > height ? `${from + 1}–${from + shown.length} of ${matches.length}` : `${matches.length} countries`;
|
|
121
125
|
const body = shown.map((code, j) => {
|
|
122
126
|
const current = !off && top + j === cursor;
|
|
123
|
-
const mark = selected.has(code) ? (off ? dim : green)("●") : dim("○");
|
|
127
|
+
const mark = off || selected.has(code) ? (off ? dim : green)("●") : dim("○");
|
|
124
128
|
const name = `${flag(code)} ${countryName(code)}`;
|
|
125
129
|
return [`${current ? cyan("❯") : " "} ${mark}`, current ? bold(code) : dim(code), current ? bold(cyan(name)) : off ? dim(name) : name];
|
|
126
130
|
});
|
|
127
131
|
if (!body.length)
|
|
128
|
-
body.push([" ", "", dim("no country matches")]);
|
|
132
|
+
body.push([" ", "", dim(off ? "no saved scan yet" : "no country matches")]);
|
|
129
133
|
const buttons = [
|
|
130
134
|
["start", starting < 0 ? "▶ Start" : `${spin(starting++)} Starting…`],
|
|
131
135
|
["all", under("A") + (query ? "ll shown" : "ll")],
|
|
@@ -133,15 +137,15 @@ export function pick(saved, { timeout, speedTest, firewall, order, stopAtFirst,
|
|
|
133
137
|
["quit", under("Q") + "uit"],
|
|
134
138
|
];
|
|
135
139
|
const buttonLine = buttons.map(([id, label]) => (id === "start" && n && !missing ? bold(cyan(`[ ${label} ]`)) : dim(`[ ${label} ]`))).join(" ");
|
|
136
|
-
const settings = [protoLine, portsLine, ...(SPEED_TEST ? [speedLine] : []), fwLine, timeoutLine, dockerLine, orderLine, stopLine];
|
|
140
|
+
const settings = [protoLine, portsLine, ...(SPEED_TEST ? [speedLine] : []), fwLine, timeoutLine, dockerLine, orderLine, stopLine, holdLine];
|
|
137
141
|
const table = grid([{ title: "" }, { title: "Code" }, { title: "Country" }], body, { caption: search, captionRight: dim(range), minWidth: Math.max(...settings.map(vw)) + 4 });
|
|
138
142
|
const width = vw(table[0]);
|
|
139
143
|
const head = box(settings, {
|
|
140
144
|
title: bold(cyan("WindScout")),
|
|
141
|
-
right: `${(off ? dim : bold)(String(selected.size))}${dim(`/${COUNTRIES.length} selected`)}`,
|
|
145
|
+
right: `${(off ? dim : bold)(String(off ? matches.length : selected.size))}${dim(`/${COUNTRIES.length} selected`)}`,
|
|
142
146
|
width: width - 4,
|
|
143
147
|
});
|
|
144
|
-
const total = login + estimate(
|
|
148
|
+
const total = login + estimate(n, timeout, slots);
|
|
145
149
|
const costs = dim(`(~${attemptSeconds(timeout)}s each${slots > 1 ? `, ${slots} at a time` : ""}${login ? `, + ~${duration(login)} login` : ""})`);
|
|
146
150
|
const lines = [
|
|
147
151
|
...head,
|
|
@@ -151,15 +155,18 @@ export function pick(saved, { timeout, speedTest, firewall, order, stopAtFirst,
|
|
|
151
155
|
"",
|
|
152
156
|
centerIn(missing
|
|
153
157
|
? yellow("no saved scan yet, so start is off: press o for another scanning type")
|
|
154
|
-
: !n ? yellow(
|
|
155
|
-
:
|
|
156
|
-
|
|
158
|
+
: !n ? yellow(skipped.length ? `the docker containers can't run ${skipped.join(" or ")}: d turns docker off`
|
|
159
|
+
: `select at least one country and one protocol${last ? ", or o for only the last results" : ""}`)
|
|
160
|
+
: stopAtFirst // it can end at any attempt, and each working one adds its checks (the w line)
|
|
161
|
+
? `up to ${bold(String(n))} attempts · ${bold("≈ " + duration(total))} if none connects ${costs}`
|
|
157
162
|
: `${bold(String(n))} attempts · estimated ${bold("≈ " + duration(total))} ${costs}`, width),
|
|
158
|
-
centerIn(dim(editing
|
|
159
|
-
? "type the seconds
|
|
160
|
-
:
|
|
161
|
-
? "type to
|
|
162
|
-
:
|
|
163
|
+
centerIn(dim(editing === "hold"
|
|
164
|
+
? "type the seconds each working connection has to stay connected · enter/esc done"
|
|
165
|
+
: editing
|
|
166
|
+
? "type the seconds to wait for each connection · enter/esc done"
|
|
167
|
+
: searching
|
|
168
|
+
? "type to filter · ↑↓ move · enter/esc done"
|
|
169
|
+
: `↑↓ move · space select · a/n ${query ? "all/none shown · esc clear" : "all/none · / search"} · enter start · q quit`), width),
|
|
163
170
|
centerIn(dim(searching || editing ? "" : `1-${PROTOCOLS.length} protocols · p ports${SPEED_TEST ? " · s speed" : ""} · f firewall · t timeout · d docker · o type · w first`), width),
|
|
164
171
|
];
|
|
165
172
|
if (message)
|
|
@@ -184,7 +191,7 @@ export function pick(saved, { timeout, speedTest, firewall, order, stopAtFirst,
|
|
|
184
191
|
});
|
|
185
192
|
hit = {
|
|
186
193
|
protoRow: row(protoLine), protoHits, portsRow: row(portsLine), speedRow: SPEED_TEST ? row(speedLine) : -1,
|
|
187
|
-
firewallRow: row(fwLine), timeoutRow: row(timeoutLine), dockerRow: row(dockerLine), orderRow: row(orderLine), stopRow: row(stopLine),
|
|
194
|
+
firewallRow: row(fwLine), timeoutRow: row(timeoutLine), dockerRow: row(dockerLine), orderRow: row(orderLine), stopRow: row(stopLine), holdRow: row(holdLine),
|
|
188
195
|
searchRow: at.top + head.length + 1,
|
|
189
196
|
firstRow: at.top + head.length + 5, shown: shown.length,
|
|
190
197
|
buttonRow: at.top + head.length + table.length + 1, buttonHits,
|
|
@@ -207,7 +214,7 @@ export function pick(saved, { timeout, speedTest, firewall, order, stopAtFirst,
|
|
|
207
214
|
const start = () => {
|
|
208
215
|
if (needsLast(order) && !last)
|
|
209
216
|
return false; // start is off: the warning under the table says why
|
|
210
|
-
if (!todo().length) {
|
|
217
|
+
if (!todo().list.length) {
|
|
211
218
|
message = "nothing to scan yet";
|
|
212
219
|
return false;
|
|
213
220
|
}
|
|
@@ -215,6 +222,10 @@ export function pick(saved, { timeout, speedTest, firewall, order, stopAtFirst,
|
|
|
215
222
|
message = `the timeout must be at least ${MIN_TIMEOUT} seconds`;
|
|
216
223
|
return false;
|
|
217
224
|
}
|
|
225
|
+
if (stopAtFirst && !(hold >= MIN_HOLD)) {
|
|
226
|
+
message = `the hold must be at least ${MIN_HOLD} seconds`;
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
218
229
|
// keep the user's order of preference stable: list order
|
|
219
230
|
const choice = {
|
|
220
231
|
countries: COUNTRIES.filter((c) => selected.has(c)),
|
|
@@ -223,6 +234,7 @@ export function pick(saved, { timeout, speedTest, firewall, order, stopAtFirst,
|
|
|
223
234
|
firewall,
|
|
224
235
|
order,
|
|
225
236
|
stopAtFirst,
|
|
237
|
+
hold,
|
|
226
238
|
allPorts,
|
|
227
239
|
timeout,
|
|
228
240
|
docker,
|
|
@@ -271,11 +283,13 @@ export function pick(saved, { timeout, speedTest, firewall, order, stopAtFirst,
|
|
|
271
283
|
else if (off && (onCountries || y === hit.protoRow || y === hit.portsRow))
|
|
272
284
|
message = SKIPPED;
|
|
273
285
|
else if (y === hit.searchRow)
|
|
274
|
-
searching = true, editing =
|
|
286
|
+
searching = true, editing = null;
|
|
275
287
|
else if (y === hit.timeoutRow)
|
|
276
|
-
editing =
|
|
288
|
+
editing = "timeout", searching = false;
|
|
289
|
+
else if (y === hit.holdRow)
|
|
290
|
+
editing = "hold", searching = false;
|
|
277
291
|
else {
|
|
278
|
-
searching = editing =
|
|
292
|
+
searching = false, editing = null; // clicking anywhere else ends typing, like leaving a text field
|
|
279
293
|
if (y >= hit.firstRow && y < hit.firstRow + hit.shown) {
|
|
280
294
|
cursor = top + (y - hit.firstRow);
|
|
281
295
|
const c = matches[cursor];
|
|
@@ -341,12 +355,13 @@ export function pick(saved, { timeout, speedTest, firewall, order, stopAtFirst,
|
|
|
341
355
|
else if (k === "pagedown")
|
|
342
356
|
cursor = Math.min(matches.length - 1, cursor + 10);
|
|
343
357
|
else if (editing) {
|
|
358
|
+
const text = typed[editing];
|
|
344
359
|
if (k === "return" || k === "escape")
|
|
345
|
-
editing =
|
|
360
|
+
editing = null;
|
|
346
361
|
else if (k === "backspace")
|
|
347
|
-
|
|
348
|
-
else if (/^[0-9]$/.test(str ?? "") &&
|
|
349
|
-
|
|
362
|
+
typed[editing] = text.slice(0, -1);
|
|
363
|
+
else if (/^[0-9]$/.test(str ?? "") && text.length < 4)
|
|
364
|
+
typed[editing] = text === "0" ? str : text + str;
|
|
350
365
|
}
|
|
351
366
|
else if (searching) {
|
|
352
367
|
// typing goes into the search box; letters are not shortcuts here
|
|
@@ -382,7 +397,9 @@ export function pick(saved, { timeout, speedTest, firewall, order, stopAtFirst,
|
|
|
382
397
|
else if (k === "f")
|
|
383
398
|
firewall = !firewall;
|
|
384
399
|
else if (k === "t")
|
|
385
|
-
editing =
|
|
400
|
+
editing = "timeout", typed.timeout = "";
|
|
401
|
+
else if (k === "h")
|
|
402
|
+
editing = "hold", typed.hold = "";
|
|
386
403
|
else if (k === "d")
|
|
387
404
|
return void toggleDocker();
|
|
388
405
|
else if (k === "o")
|
package/dist/tui/progress.js
CHANGED
|
@@ -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 =
|
|
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(
|
|
64
|
-
const head = ` ${cyan(spinner)} ${
|
|
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/dist/tui/report.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
// The final report, printed to the normal screen so it stays in the scrollback.
|
|
2
|
-
import { sortOk } from "../probe.js";
|
|
2
|
+
import { sortOk, target } from "../probe.js";
|
|
3
|
+
import { ago } from "../time.js";
|
|
3
4
|
import { CLI } from "../windscribe.js";
|
|
4
5
|
import { flag } from "./country.js";
|
|
5
6
|
import { matrix, resultTable } from "./tables.js";
|
|
6
|
-
import { bold, cyan, dim, fmt, green, vw, yellow } from "./text.js";
|
|
7
|
+
import { bold, cyan, dim, fmt, green, red, vw, yellow } from "./text.js";
|
|
7
8
|
import { box, grid } from "./widgets.js";
|
|
8
|
-
// nickname pins the exact datacenter
|
|
9
|
-
export const target = (r) => r.location.split(" - ").at(-1) || r.query;
|
|
10
9
|
export const command = (r) => `${CLI} connect "${target(r)}" ${r.protocol}`;
|
|
11
10
|
/** Prints the report; returns the connections it numbered: the best (#0), then the others (#1…) */
|
|
12
11
|
export function report(results, protocols, { all, speedTest }) {
|
|
@@ -62,3 +61,14 @@ export function report(results, protocols, { all, speedTest }) {
|
|
|
62
61
|
console.log(rest.join("\n"));
|
|
63
62
|
return [b, ...others];
|
|
64
63
|
}
|
|
64
|
+
/** Prints the list as the results screen left it, best first, as windscout connect will try it: on the way out, when it
|
|
65
|
+
* changed there (a test, a drop, a background check) */
|
|
66
|
+
export function printList(list) {
|
|
67
|
+
if (!list.length)
|
|
68
|
+
return;
|
|
69
|
+
const speeds = list.some((e) => e.speedMbps !== null);
|
|
70
|
+
console.log(grid([{ title: "#", align: "right" }, { title: "Location" }, { title: "Protocol" }, { title: "Latency", align: "right" },
|
|
71
|
+
...(speeds ? [{ title: "Speed", align: "right" }] : []), { title: "Last check" }], list.map((e, i) => [dim(String(i + 1)), `${flag(e.query)} ${e.location || e.query}`, e.protocol,
|
|
72
|
+
e.latencyMs === null ? "-" : `${fmt(e.latencyMs, 0)} ms`, ...(speeds ? [e.speedMbps === null ? "-" : `${fmt(e.speedMbps, 1)} Mbps`] : []),
|
|
73
|
+
e.failed ? red(`✗ ${e.failed} · ${ago(e.failedAt ?? Date.now())}`) : e.checked ? green(`✓ ${ago(e.checked)}`) : dim("✓ in the scan")]), { caption: `${bold("Your connections now")} ${dim("· saved: windscout connect tries them in this order")}`, minWidth: 64 }).join("\n"));
|
|
74
|
+
}
|