Skip to content

Commit eb2766f

Browse files
committed
Add --verbose flag for latency and server location info
Closes #38
1 parent 2ae8d73 commit eb2766f

5 files changed

Lines changed: 186 additions & 21 deletions

File tree

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,10 @@
4747
"mbps"
4848
],
4949
"dependencies": {
50-
"ink": "^6.3.0",
50+
"ink": "^6.3.1",
5151
"ink-spinner": "^5.0.0",
5252
"meow": "^14.0.0",
53-
"puppeteer": "^24.21.0",
53+
"puppeteer": "^24.22.0",
5454
"react": "^19.1.1",
5555
"unicorn-magic": "^0.3.0"
5656
},

readme.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ $ fast --help
2727
--upload, -u Measure upload speed in addition to download speed
2828
--single-line Reduce spacing and output to a single line
2929
--json JSON output
30+
--verbose Include latency and server location information
3031
3132
Examples
3233
$ fast --upload > file && cat file
@@ -40,6 +41,22 @@ $ fast --help
4041

4142
<img src="screenshot-upload.gif" width="500" height="260">
4243

44+
### Verbose output
45+
46+
Include additional diagnostic information like latency and client location:
47+
48+
```sh
49+
fast --verbose
50+
```
51+
52+
```
53+
72 Mbps
54+
8 Mbps
55+
56+
Latency: 8 ms (unloaded) / 16 ms (loaded)
57+
Client: Osaka, JP • 216.144.245.67
58+
```
59+
4360
### JSON output
4461

4562
The speeds are in Mbps.

source/cli.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const cli = meow(`
1212
--upload, -u Measure upload speed in addition to download speed
1313
--single-line Reduce spacing and output to a single line
1414
--json JSON output
15+
--verbose Include latency and server location information
1516
1617
Examples
1718
$ fast --upload > file && cat file
@@ -32,15 +33,19 @@ const cli = meow(`
3233
json: {
3334
type: 'boolean',
3435
},
36+
verbose: {
37+
type: 'boolean',
38+
},
3539
} as const,
3640
});
3741

3842
function App() {
3943
return (
4044
<Ui
4145
singleLine={cli.flags.singleLine}
42-
upload={cli.flags.upload}
46+
upload={cli.flags.upload || cli.flags.verbose} // eslint-disable-line @typescript-eslint/prefer-nullish-coalescing
4347
json={cli.flags.json}
48+
verbose={cli.flags.verbose}
4449
/>
4550
);
4651
}

source/ui.tsx

Lines changed: 141 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,150 @@ const Speed: React.FC<SpeedComponentProperties> = ({upload, data}) => upload ? (
8686
</>
8787
) : (<DownloadSpeed {...data}/>);
8888

89+
type VerboseInfoProperties = {
90+
readonly data: PartialSpeedData;
91+
readonly singleLine?: boolean;
92+
};
93+
94+
const VerboseInfo: React.FC<VerboseInfoProperties> = ({data, singleLine}) => {
95+
const hasLatencyData = data.latency !== undefined || data.bufferBloat !== undefined;
96+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
97+
const hasClientData = Boolean(data.userLocation || data.userIp);
98+
99+
return (
100+
<>
101+
{!singleLine && <Newline/>}
102+
<Box flexDirection='column'>
103+
<Box>
104+
<Text><FixedSpacer size={4}/></Text>
105+
<Text dimColor>Latency: </Text>
106+
{hasLatencyData ? (
107+
<>
108+
{data.latency !== undefined && (
109+
<>
110+
<Text color='white'>{data.latency}</Text>
111+
<Text dimColor> ms (unloaded)</Text>
112+
</>
113+
)}
114+
{data.latency !== undefined && data.bufferBloat !== undefined && (
115+
<Text dimColor> / </Text>
116+
)}
117+
{data.bufferBloat !== undefined && (
118+
<>
119+
<Text color='white'>{data.bufferBloat}</Text>
120+
<Text dimColor> ms (loaded)</Text>
121+
</>
122+
)}
123+
</>
124+
) : (
125+
<Text dimColor>Measuring...</Text>
126+
)}
127+
</Box>
128+
<Box>
129+
<Text><FixedSpacer size={4}/></Text>
130+
<Text dimColor>Client: </Text>
131+
{hasClientData ? (
132+
<>
133+
{data.userLocation && (
134+
<Text color='white'>{data.userLocation}</Text>
135+
)}
136+
{data.userLocation && data.userIp && (
137+
<Text dimColor></Text>
138+
)}
139+
{data.userIp && (
140+
<Text color='white'>{data.userIp}</Text>
141+
)}
142+
</>
143+
) : (
144+
<Text dimColor>Detecting...</Text>
145+
)}
146+
</Box>
147+
</Box>
148+
</>
149+
);
150+
};
151+
152+
function formatVerboseText(data: PartialSpeedData): string[] {
153+
const lines: string[] = [];
154+
155+
if (data.latency !== undefined || data.bufferBloat !== undefined) {
156+
let latencyLine = 'Latency: ';
157+
if (data.latency !== undefined) {
158+
latencyLine += `${data.latency} ms (unloaded)`;
159+
}
160+
161+
if (data.latency !== undefined && data.bufferBloat !== undefined) {
162+
latencyLine += ' / ';
163+
}
164+
165+
if (data.bufferBloat !== undefined) {
166+
latencyLine += `${data.bufferBloat} ms (loaded)`;
167+
}
168+
169+
lines.push(latencyLine);
170+
}
171+
172+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
173+
if (data.userLocation || data.userIp) {
174+
let clientLine = 'Client: ';
175+
if (data.userLocation) {
176+
clientLine += data.userLocation;
177+
}
178+
179+
if (data.userLocation && data.userIp) {
180+
clientLine += ' • ';
181+
}
182+
183+
if (data.userIp) {
184+
clientLine += data.userIp;
185+
}
186+
187+
lines.push(clientLine);
188+
}
189+
190+
return lines;
191+
}
192+
193+
function createJsonOutput(data: PartialSpeedData, upload: boolean) {
194+
return {
195+
downloadSpeed: convertToMbps(data.downloadSpeed ?? 0, data.downloadUnit ?? 'Mbps'),
196+
uploadSpeed: upload ? convertToMbps(data.uploadSpeed ?? 0, data.uploadUnit ?? 'Mbps') : undefined,
197+
downloadUnit: 'Mbps' as const,
198+
uploadUnit: upload ? 'Mbps' as const : undefined,
199+
downloaded: data.downloaded,
200+
uploaded: data.uploaded,
201+
latency: data.latency,
202+
bufferBloat: data.bufferBloat,
203+
userLocation: data.userLocation,
204+
userIp: data.userIp,
205+
};
206+
}
207+
208+
function formatTextOutput(data: PartialSpeedData, upload: boolean, verbose: boolean): string {
209+
let output = `${data.downloadSpeed ?? 0} ${data.downloadUnit ?? 'Mbps'}`;
210+
211+
if (upload && data.uploadSpeed) {
212+
output += `\n${data.uploadSpeed} ${data.uploadUnit ?? 'Mbps'}`;
213+
}
214+
215+
if (verbose) {
216+
const verboseLines = formatVerboseText(data);
217+
if (verboseLines.length > 0) {
218+
output += '\n\n' + verboseLines.join('\n');
219+
}
220+
}
221+
222+
return output;
223+
}
224+
89225
type FastProperties = {
90226
readonly singleLine?: boolean;
91227
readonly upload?: boolean;
92228
readonly json?: boolean;
229+
readonly verbose?: boolean;
93230
};
94231

95-
const Ui: React.FC<FastProperties> = ({singleLine, upload, json}) => {
232+
const Ui: React.FC<FastProperties> = ({singleLine, upload, json, verbose}) => {
96233
const [error, setError] = useState('');
97234
const [data, setData] = useState<PartialSpeedData>({});
98235
const [isDone, setIsDone] = useState(false);
@@ -137,29 +274,14 @@ const Ui: React.FC<FastProperties> = ({singleLine, upload, json}) => {
137274
}
138275

139276
if (json) {
140-
const jsonData = {
141-
downloadSpeed: convertToMbps(data.downloadSpeed ?? 0, data.downloadUnit ?? 'Mbps'),
142-
uploadSpeed: upload ? convertToMbps(data.uploadSpeed ?? 0, data.uploadUnit ?? 'Mbps') : undefined,
143-
downloadUnit: 'Mbps' as const,
144-
uploadUnit: upload ? 'Mbps' as const : undefined,
145-
downloaded: data.downloaded,
146-
uploaded: data.uploaded,
147-
latency: data.latency,
148-
bufferBloat: data.bufferBloat,
149-
userLocation: data.userLocation,
150-
userIp: data.userIp,
151-
};
152-
277+
const jsonData = createJsonOutput(data, Boolean(upload));
153278
write(JSON.stringify(jsonData, (_key, value) =>
154279
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
155280
value === undefined ? undefined : value,
156281
'\t',
157282
));
158283
} else if (!process.stdout.isTTY) {
159-
write(`${data.downloadSpeed ?? 0} ${data.downloadUnit ?? 'Mbps'}`);
160-
if (upload && data.uploadSpeed) {
161-
write(`\n${data.uploadSpeed} ${data.uploadUnit ?? 'Mbps'}`);
162-
}
284+
write(formatTextOutput(data, Boolean(upload), Boolean(verbose)));
163285
}
164286

165287
exit();
@@ -189,6 +311,7 @@ const Ui: React.FC<FastProperties> = ({singleLine, upload, json}) => {
189311
{isDone && <Text><FixedSpacer size={4}/></Text>}
190312
{Object.keys(data).length > 0 && <Speed upload={upload} data={data}/>}
191313
</Box>
314+
{verbose && <VerboseInfo data={data} singleLine={singleLine}/>}
192315
<Spacer singleLine={singleLine}/>
193316
</>
194317
);

test.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,23 @@ test('json upload output', async t => {
5151
t.is(typeof data.downloadSpeed, 'number');
5252
t.is(typeof data.uploadSpeed, 'number');
5353
});
54+
55+
test('verbose flag', async t => {
56+
const {stdout} = await execa('node', ['./distribution/cli.js', '--verbose'], {timeout: 90_000});
57+
58+
// Should contain speed measurement
59+
t.regex(stdout, /\d+(?:\.\d+)?\s+[MGKB]bps/);
60+
61+
// Should contain latency information
62+
t.regex(stdout, /Latency:/);
63+
t.regex(stdout, /\d+\s+ms/);
64+
65+
// Should contain client information
66+
t.regex(stdout, /Client:/);
67+
});
68+
69+
test('verbose flag in help', async t => {
70+
const {stdout} = await execa('node', ['./distribution/cli.js', '--help']);
71+
t.true(stdout.includes('--verbose'));
72+
t.true(stdout.includes('Include latency and server location information'));
73+
});

0 commit comments

Comments
 (0)