Skip to content

Commit bf76412

Browse files
author
Jochum van der Ploeg
authored
feat: add thank you message (#814)
1 parent 3f2c098 commit bf76412

File tree

6 files changed

+250
-0
lines changed

6 files changed

+250
-0
lines changed

e2e/helpers/command_helper.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ void Function() withRunner(
4242
final commandRunner = VeryGoodCommandRunner(
4343
logger: logger,
4444
pubUpdater: pubUpdater,
45+
environment: {'CI': 'true'},
4546
);
4647

4748
when(() => progress.complete(any())).thenAnswer((_) {

lib/src/command_runner.dart

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ import 'package:args/command_runner.dart';
33
import 'package:cli_completion/cli_completion.dart';
44
import 'package:mason/mason.dart' hide packageVersion;
55
import 'package:meta/meta.dart';
6+
import 'package:path/path.dart' as path;
67
import 'package:pub_updater/pub_updater.dart';
8+
import 'package:universal_io/io.dart';
79
import 'package:very_good_cli/src/commands/commands.dart';
10+
import 'package:very_good_cli/src/logger_extension.dart';
811
import 'package:very_good_cli/src/pub_license/pub_license.dart';
912
import 'package:very_good_cli/src/version.dart';
1013

@@ -19,9 +22,11 @@ class VeryGoodCommandRunner extends CompletionCommandRunner<int> {
1922
VeryGoodCommandRunner({
2023
Logger? logger,
2124
PubUpdater? pubUpdater,
25+
Map<String, String>? environment,
2226
@visibleForTesting PubLicense? pubLicense,
2327
}) : _logger = logger ?? Logger(),
2428
_pubUpdater = pubUpdater ?? PubUpdater(),
29+
_environment = environment ?? Platform.environment,
2530
super('very_good', '🦄 A Very Good Command-Line Interface') {
2631
argParser
2732
..addFlag(
@@ -45,6 +50,16 @@ class VeryGoodCommandRunner extends CompletionCommandRunner<int> {
4550
final Logger _logger;
4651
final PubUpdater _pubUpdater;
4752

53+
/// Map of environments information.
54+
Map<String, String> get environment => environmentOverride ?? _environment;
55+
final Map<String, String> _environment;
56+
57+
/// Boolean for checking if windows, which can be overridden for
58+
/// testing purposes.
59+
@visibleForTesting
60+
bool? isWindowsOverride;
61+
bool get _isWindows => isWindowsOverride ?? Platform.isWindows;
62+
4863
@override
4964
void printUsage() => _logger.info(usage);
5065

@@ -115,6 +130,7 @@ class VeryGoodCommandRunner extends CompletionCommandRunner<int> {
115130
if (topLevelResults.command?.name != UpdateCommand.commandName) {
116131
await _checkForUpdates();
117132
}
133+
_showThankYou();
118134
return exitCode;
119135
}
120136

@@ -134,4 +150,39 @@ Run ${lightCyan.wrap('very_good update')} to update''',
134150
}
135151
} catch (_) {}
136152
}
153+
154+
void _showThankYou() {
155+
if (environment.containsKey('CI')) return;
156+
157+
final versionFile = File(
158+
path.join(_configDir.path, 'version'),
159+
)..createSync(recursive: true);
160+
161+
if (versionFile.readAsStringSync() == packageVersion) return;
162+
versionFile.writeAsStringSync(packageVersion);
163+
164+
_logger.wrap(
165+
lightMagenta.wrap('''
166+
167+
Thank you for using Very Good Ventures open source tools!
168+
Don't forget to fill out this form to get information on future updates and releases here: ${lightBlue.wrap(link(uri: Uri.parse('https://verygood.ventures/open-source/cli/subscribe-latest-tool-updates')))}'''),
169+
print: _logger.info,
170+
);
171+
}
172+
173+
Directory get _configDir {
174+
if (_isWindows) {
175+
// Use localappdata on windows
176+
final localAppData = environment['LOCALAPPDATA']!;
177+
return Directory(path.join(localAppData, 'VeryGoodCLI'));
178+
} else {
179+
// Try using XDG config folder
180+
var dirPath = environment['XDG_CONFIG_HOME'];
181+
// Fallback to $HOME if not following XDG specification
182+
if (dirPath == null || dirPath.isEmpty) {
183+
dirPath = environment['HOME'];
184+
}
185+
return Directory(path.join(dirPath!, '.very_good_cli'));
186+
}
187+
}
137188
}

lib/src/logger_extension.dart

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,41 @@
11
import 'package:mason_logger/mason_logger.dart';
2+
import 'package:universal_io/io.dart';
23

34
/// Extension on the Logger class for custom styled logging.
45
extension LoggerX on Logger {
56
/// Log a message in the "created" style of the CLI.
67
void created(String message) {
78
info(lightCyan.wrap(styleBold.wrap(message)));
89
}
10+
11+
/// Wrap the [text] to fit perfectly within the width of the terminal when
12+
/// [print]ed.
13+
///
14+
/// To overwrite the width you can use [length].
15+
void wrap(
16+
String? text, {
17+
required void Function(String?) print,
18+
int? length,
19+
}) {
20+
final maxLength = length ?? stdout.terminalColumns;
21+
for (final sentence in text?.split('/n') ?? <String>[]) {
22+
final words = sentence.split(' ');
23+
24+
final currentLine = StringBuffer();
25+
for (final word in words) {
26+
// Replace all ANSI sequences so we can get the true character length.
27+
final charLength = word
28+
.replaceAll(RegExp('\x1B(?:[@-Z\\-_]|[[0-?]*[ -/]*[@-~])'), '')
29+
.length;
30+
31+
if (currentLine.length + charLength > maxLength) {
32+
print(currentLine.toString());
33+
currentLine.clear();
34+
}
35+
currentLine.write('$word ');
36+
}
37+
38+
print(currentLine.toString());
39+
}
40+
}
941
}

test/helpers/command_helper.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ void Function() withRunner(
4747
final commandRunner = VeryGoodCommandRunner(
4848
logger: logger,
4949
pubUpdater: pubUpdater,
50+
environment: {'CI': 'true'},
5051
pubLicense: pubLicense,
5152
);
5253

test/src/command_runner_test.dart

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ class _MockPubUpdater extends Mock implements PubUpdater {}
1616

1717
class _MockProgress extends Mock implements Progress {}
1818

19+
class _MockDirectory extends Mock implements Directory {}
20+
21+
class _MockFile extends Mock implements File {}
22+
23+
class _MockStdout extends Mock implements Stdout {}
24+
1925
const expectedUsage = [
2026
'🦄 A Very Good Command-Line Interface\n'
2127
'\n'
@@ -71,6 +77,7 @@ void main() {
7177
commandRunner = VeryGoodCommandRunner(
7278
logger: logger,
7379
pubUpdater: pubUpdater,
80+
environment: {'CI': 'true'},
7481
);
7582
});
7683

@@ -179,6 +186,114 @@ void main() {
179186
expect(result, equals(ExitCode.success.code));
180187
});
181188

189+
group('_showThankYou', () {
190+
late Directory cliCache;
191+
late File versionFile;
192+
late Stdout stdout;
193+
194+
setUp(() {
195+
cliCache = _MockDirectory();
196+
when(() => cliCache.path).thenReturn('/users/test');
197+
198+
versionFile = _MockFile();
199+
when(() => versionFile.readAsStringSync()).thenReturn('0.0.0');
200+
201+
stdout = _MockStdout();
202+
when(() => stdout.supportsAnsiEscapes).thenReturn(true);
203+
when(() => stdout.terminalColumns).thenReturn(30);
204+
});
205+
206+
test('shows message when version changed', () async {
207+
commandRunner.environmentOverride = {
208+
'HOME': '/users/test',
209+
};
210+
211+
await IOOverrides.runZoned(
212+
() async {
213+
final result = await commandRunner.run([]);
214+
expect(result, equals(ExitCode.success.code));
215+
216+
verifyInOrder([
217+
() => logger.info('\nThank you for using Very Good '),
218+
() => logger.info('Ventures open source '),
219+
() => logger.info("tools!\nDon't forget to fill "),
220+
() => logger.info('out this form to get '),
221+
() => logger.info('information on future updates '),
222+
() => logger.info('and releases here: '),
223+
() => logger.info(
224+
any(
225+
that: contains(
226+
'https://verygood.ventures/open-source/cli/subscribe-latest-tool-updates',
227+
),
228+
),
229+
),
230+
]);
231+
232+
verify(
233+
() => versionFile.createSync(
234+
recursive: any(that: isTrue, named: 'recursive'),
235+
),
236+
).called(1);
237+
verify(() => versionFile.readAsStringSync()).called(1);
238+
verify(
239+
() => versionFile
240+
.writeAsStringSync(any(that: equals(packageVersion))),
241+
).called(1);
242+
},
243+
createDirectory: (path) => cliCache,
244+
createFile: (path) => versionFile,
245+
stdout: () => stdout,
246+
);
247+
});
248+
249+
test('cache inside XDG directory', () async {
250+
commandRunner.environmentOverride = {
251+
'HOME': '/users/test',
252+
'XDG_CONFIG_HOME': '/users/test/.xdg',
253+
};
254+
255+
final xdgCache = _MockDirectory();
256+
when(() => xdgCache.path).thenReturn('/users/test/.xdg');
257+
258+
await IOOverrides.runZoned(
259+
() async {
260+
final result = await commandRunner.run([]);
261+
expect(result, equals(ExitCode.success.code));
262+
263+
verifyNever(() => cliCache.path);
264+
verify(() => xdgCache.path).called(1);
265+
},
266+
createDirectory: (path) =>
267+
path.contains('.xdg') ? xdgCache : cliCache,
268+
createFile: (path) => versionFile,
269+
stdout: () => stdout,
270+
);
271+
});
272+
273+
test('cache inside local APP_DATA on windows', () async {
274+
commandRunner
275+
..environmentOverride = {'LOCALAPPDATA': '/C/Users/test'}
276+
..isWindowsOverride = true;
277+
278+
final windowsCache = _MockDirectory();
279+
when(() => windowsCache.path).thenReturn('/C/Users/test');
280+
281+
await IOOverrides.runZoned(
282+
() async {
283+
final result = await commandRunner.run([]);
284+
expect(result, equals(ExitCode.success.code));
285+
286+
verifyNever(() => cliCache.path);
287+
verify(() => windowsCache.path).called(1);
288+
},
289+
createDirectory: (path) =>
290+
path.startsWith('/C/') ? windowsCache : cliCache,
291+
createFile: (path) => versionFile,
292+
stdout: () => stdout,
293+
);
294+
});
295+
});
296+
182297
group('--help', () {
183298
test('outputs usage', () async {
184299
final result = await commandRunner.run(['--help']);
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import 'package:mason/mason.dart';
2+
import 'package:mocktail/mocktail.dart';
3+
import 'package:test/test.dart';
4+
import 'package:very_good_cli/src/logger_extension.dart';
5+
6+
class _MockLogger extends Mock implements Logger {}
7+
8+
void main() {
9+
group('LoggerX', () {
10+
late Logger logger;
11+
12+
setUp(() {
13+
logger = _MockLogger();
14+
});
15+
16+
test('created', () {
17+
logger.created('test');
18+
19+
verify(
20+
() => logger.info(
21+
any(that: equals(lightCyan.wrap(styleBold.wrap('test')))),
22+
),
23+
).called(1);
24+
});
25+
26+
group('wrap', () {
27+
test('normally across two separate prints', () {
28+
logger.wrap('1 2 3 4 5 1 2 3 4 5', print: logger.info, length: 10);
29+
30+
verifyInOrder([
31+
() => logger.info(any(that: equals('1 2 3 4 5 '))),
32+
() => logger.info(any(that: equals('1 2 3 4 5 '))),
33+
]);
34+
});
35+
36+
test('across two separate prints while not adding in ANSI encoding', () {
37+
logger.wrap(
38+
'${lightCyan.wrap('1 2 3 4 5')} 1 2 3 4 5',
39+
print: logger.info,
40+
length: 10,
41+
);
42+
43+
verifyInOrder([
44+
() => logger.info(any(that: equals(lightCyan.wrap('1 2 3 4 5 ')))),
45+
() => logger.info(any(that: equals('1 2 3 4 5 '))),
46+
]);
47+
});
48+
});
49+
});
50+
}

0 commit comments

Comments
 (0)