Skip to content

Commit cf0fe94

Browse files
feat: add install triggers (#10)
Co-authored-by: Felix Angelov <[email protected]>
1 parent 8aa223a commit cf0fe94

13 files changed

+529
-43
lines changed

example/lib/src/command_runner.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:args/command_runner.dart';
2+
import 'package:cli_completion/cli_completion.dart';
23
import 'package:example/src/commands/commands.dart';
34
import 'package:mason_logger/mason_logger.dart';
45

@@ -13,7 +14,7 @@ const description = 'Example for cli_completion';
1314
/// $ example_cli --version
1415
/// ```
1516
/// {@endtemplate}
16-
class ExampleCommandRunner extends CommandRunner<int> {
17+
class ExampleCommandRunner extends CompletionCommandRunner<int> {
1718
/// {@macro example_command_runner}
1819
ExampleCommandRunner({
1920
Logger? logger,

example/pubspec.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ environment:
88

99
dependencies:
1010
args: ^2.3.1
11+
cli_completion:
12+
path: ../
1113
mason_logger: ^0.2.0
1214

1315
dev_dependencies:

lib/cli_completion.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/// Contains the completion command runner based elements to add completion to
2+
/// dart command line applications.
3+
library cli_completion;
4+
5+
export 'src/command_runner/commands/commands.dart';
6+
export 'src/command_runner/completion_command_runner.dart';
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export 'handle_completion_command.dart';
2+
export 'install_completion_files_command.dart';
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import 'dart:async';
2+
3+
import 'package:args/command_runner.dart';
4+
import 'package:cli_completion/src/command_runner/completion_command_runner.dart';
5+
6+
import 'package:mason_logger/mason_logger.dart';
7+
8+
/// {@template handle_completion_request_command}
9+
/// A hidden [Command] added by [CompletionCommandRunner] that handles the
10+
/// "completion" sub command.
11+
/// This is called by a shell function when the user presses "tab".
12+
/// Any output to stdout during this call will be interpreted as suggestions
13+
/// for completions.
14+
/// {@endtemplate}
15+
class HandleCompletionRequestCommand<T> extends Command<T> {
16+
/// {@macro handle_completion_request_command}
17+
HandleCompletionRequestCommand(this.logger);
18+
19+
@override
20+
String get description {
21+
return 'Handles shell completion (should never be called manually)';
22+
}
23+
24+
/// The string that the shell will use to call for completion suggestions
25+
static const commandName = 'completion';
26+
27+
@override
28+
String get name => commandName;
29+
30+
@override
31+
bool get hidden => true;
32+
33+
/// The [Logger] used to display the completion suggestions
34+
final Logger logger;
35+
36+
@override
37+
FutureOr<T>? run() {
38+
logger
39+
..info('USA')
40+
..info('Brazil')
41+
..info('Netherlands');
42+
43+
return null;
44+
}
45+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import 'dart:async';
2+
3+
import 'package:args/command_runner.dart';
4+
import 'package:cli_completion/src/command_runner/completion_command_runner.dart';
5+
6+
import 'package:mason_logger/mason_logger.dart';
7+
8+
/// {@template install_completion_command}
9+
/// A hidden [Command] added by [CompletionCommandRunner] that can be used to
10+
/// manually install the completion files
11+
/// (otherwise automatically installed by [CompletionCommandRunner]).
12+
/// {@endtemplate}
13+
///
14+
/// Differently from the auto installation performed by
15+
/// [CompletionCommandRunner] on any command run,
16+
/// this command logs messages during the installation process.
17+
class InstallCompletionFilesCommand<T> extends Command<T> {
18+
/// {@macro install_completion_command}
19+
InstallCompletionFilesCommand() {
20+
argParser.addFlag(
21+
'verbose',
22+
abbr: 'v',
23+
help: 'Verbose output',
24+
negatable: false,
25+
);
26+
}
27+
28+
@override
29+
String get description {
30+
return 'Manually installs completion files for the current shell.';
31+
}
32+
33+
/// The string that the user can call to manually install completion files
34+
static const commandName = 'install-completion-files';
35+
36+
@override
37+
String get name => commandName;
38+
39+
@override
40+
bool get hidden => true;
41+
42+
@override
43+
CompletionCommandRunner<T> get runner {
44+
return super.runner! as CompletionCommandRunner<T>;
45+
}
46+
47+
@override
48+
FutureOr<T>? run() {
49+
final verbose = argResults!['verbose'] as bool;
50+
final level = verbose ? Level.verbose : Level.info;
51+
runner.tryInstallCompletionFiles(level);
52+
return null;
53+
}
54+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import 'dart:async';
2+
3+
import 'package:args/args.dart';
4+
import 'package:args/command_runner.dart';
5+
import 'package:cli_completion/src/command_runner/commands/commands.dart';
6+
import 'package:cli_completion/src/exceptions.dart';
7+
import 'package:cli_completion/src/install/completion_installation.dart';
8+
import 'package:cli_completion/src/system_shell.dart';
9+
import 'package:mason_logger/mason_logger.dart';
10+
import 'package:meta/meta.dart';
11+
12+
/// {@template completion_command_runner}
13+
/// A [CommandRunner] that takes care of installing shell completion scripts
14+
/// and handle completion requests.
15+
/// {@endtemplate}
16+
///
17+
/// It tries to install completion scripts upon any command run.
18+
///
19+
/// Adds [HandleCompletionRequestCommand] to route completion requests to
20+
/// other sub commands.
21+
///
22+
/// Adds [InstallCompletionFilesCommand] to enable the user to
23+
/// manually install completion files.
24+
abstract class CompletionCommandRunner<T> extends CommandRunner<T> {
25+
/// {@macro completion_command_runner}
26+
CompletionCommandRunner(super.executableName, super.description) {
27+
addCommand(HandleCompletionRequestCommand<T>(completionLogger));
28+
addCommand(InstallCompletionFilesCommand<T>());
29+
}
30+
31+
/// The [Logger] used to prompt the completion suggestions.
32+
final Logger completionLogger = Logger();
33+
34+
/// The [Logger] used to display messages during completion installation.
35+
final Logger completionInstallationLogger = Logger();
36+
37+
/// Environment map which can be overridden for testing purposes.
38+
@visibleForTesting
39+
Map<String, String>? environmentOverride;
40+
41+
SystemShell? get _systemShell =>
42+
SystemShell.current(environmentOverride: environmentOverride);
43+
44+
CompletionInstallation? _completionInstallation;
45+
46+
/// The [CompletionInstallation] used to install completion files.
47+
CompletionInstallation get completionInstallation {
48+
var completionInstallation = _completionInstallation;
49+
50+
completionInstallation ??= CompletionInstallation.fromSystemShell(
51+
systemShell: _systemShell,
52+
logger: completionInstallationLogger,
53+
);
54+
55+
return _completionInstallation = completionInstallation;
56+
}
57+
58+
@override
59+
@mustCallSuper
60+
Future<T?> runCommand(ArgResults topLevelResults) async {
61+
final reservedCommands = [
62+
HandleCompletionRequestCommand.commandName,
63+
InstallCompletionFilesCommand.commandName,
64+
];
65+
66+
if (!reservedCommands.contains(topLevelResults.command?.name)) {
67+
// When auto installing, use error level to display messages.
68+
tryInstallCompletionFiles(Level.error);
69+
}
70+
71+
return super.runCommand(topLevelResults);
72+
}
73+
74+
/// Tries to install completion files for the current shell.
75+
@internal
76+
void tryInstallCompletionFiles(Level level) {
77+
try {
78+
completionInstallationLogger.level = level;
79+
completionInstallation.install(executableName);
80+
} on CompletionInstallationException catch (e) {
81+
completionInstallationLogger.err(e.toString());
82+
}
83+
}
84+
}

lib/src/install/completion_installation.dart

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,20 @@ class CompletionInstallation {
2424

2525
/// Creates a [CompletionInstallation] given the current [SystemShell].
2626
factory CompletionInstallation.fromSystemShell({
27-
required SystemShell systemShell,
27+
required SystemShell? systemShell,
2828
required Logger logger,
2929
bool? isWindowsOverride,
3030
Map<String, String>? environmentOverride,
3131
}) {
3232
final isWindows = isWindowsOverride ?? Platform.isWindows;
3333
final environment = environmentOverride ?? Platform.environment;
3434

35+
final configuration = systemShell == null
36+
? null
37+
: ShellCompletionConfiguration.fromSystemShell(systemShell);
38+
3539
return CompletionInstallation(
36-
configuration: ShellCompletionConfiguration.fromSystemShell(systemShell),
40+
configuration: configuration,
3741
logger: logger,
3842
isWindows: isWindows,
3943
environment: environment,
@@ -51,7 +55,7 @@ class CompletionInstallation {
5155
final Map<String, String> environment;
5256

5357
/// The associated [ShellCompletionConfiguration].
54-
final ShellCompletionConfiguration configuration;
58+
final ShellCompletionConfiguration? configuration;
5559

5660
/// Define the [Directory] in which the
5761
/// completion configuration files will be stored.
@@ -71,6 +75,15 @@ class CompletionInstallation {
7175
/// Install completion configuration hooks for a [rootCommand] in the
7276
/// current shell.
7377
void install(String rootCommand) {
78+
final configuration = this.configuration;
79+
80+
if (configuration == null) {
81+
throw CompletionInstallationException(
82+
message: 'Unknown shell.',
83+
rootCommand: rootCommand,
84+
);
85+
}
86+
7487
logger.detail(
7588
'Installing completion for the command $rootCommand '
7689
'on ${configuration.name}',
@@ -87,13 +100,13 @@ class CompletionInstallation {
87100
void createCompletionConfigDir() {
88101
final completionConfigDirPath = completionConfigDir.path;
89102

90-
logger.detail(
103+
logger.info(
91104
'Creating completion configuration directory '
92105
'at $completionConfigDirPath',
93106
);
94107

95108
if (completionConfigDir.existsSync()) {
96-
logger.detail(
109+
logger.warn(
97110
'A ${completionConfigDir.path} directory was already found.',
98111
);
99112
return;
@@ -106,20 +119,21 @@ class CompletionInstallation {
106119
/// identified shell.
107120
@visibleForTesting
108121
void writeCompletionScriptForCommand(String rootCommand) {
122+
final configuration = this.configuration!;
109123
final completionConfigDirPath = completionConfigDir.path;
110124
final commandScriptName = '$rootCommand.${configuration.name}';
111125
final commandScriptPath = path.join(
112126
completionConfigDirPath,
113127
commandScriptName,
114128
);
115-
logger.detail(
129+
logger.info(
116130
'Writing completion script for $rootCommand on $commandScriptPath',
117131
);
118132

119133
final scriptFile = File(commandScriptPath);
120134

121135
if (scriptFile.existsSync()) {
122-
logger.detail(
136+
logger.warn(
123137
'A script file for $rootCommand was already found on '
124138
'$commandScriptPath.',
125139
);
@@ -133,18 +147,19 @@ class CompletionInstallation {
133147
/// [writeCompletionScriptForCommand] the the global completion config file.
134148
@visibleForTesting
135149
void writeCompletionConfigForShell(String rootCommand) {
150+
final configuration = this.configuration!;
136151
final completionConfigDirPath = completionConfigDir.path;
137152

138153
final configPath = path.join(
139154
completionConfigDirPath,
140155
configuration.completionConfigForShellFileName,
141156
);
142-
logger.detail('Adding config for $rootCommand config entry to $configPath');
157+
logger.info('Adding config for $rootCommand config entry to $configPath');
143158

144159
final configFile = File(configPath);
145160

146161
if (!configFile.existsSync()) {
147-
logger.detail('No file found at $configPath, creating one now');
162+
logger.info('No file found at $configPath, creating one now');
148163
configFile.createSync();
149164
}
150165
final commandScriptName = '$rootCommand.${configuration.name}';
@@ -153,7 +168,7 @@ class CompletionInstallation {
153168
configFile.readAsStringSync().contains(commandScriptName);
154169

155170
if (containsLine) {
156-
logger.detail(
171+
logger.warn(
157172
'A config entry for $rootCommand was already found on $configPath.',
158173
);
159174
return;
@@ -167,13 +182,15 @@ class CompletionInstallation {
167182
}
168183

169184
String get _shellRCFilePath =>
170-
_resolveHome(configuration.shellRCFile, environment);
185+
_resolveHome(configuration!.shellRCFile, environment);
171186

172187
/// Write a source to the completion global script in the shell configuration
173188
/// file, which its location is described by the [configuration]
174189
@visibleForTesting
175190
void writeToShellConfigFile(String rootCommand) {
176-
logger.detail(
191+
final configuration = this.configuration!;
192+
193+
logger.info(
177194
'Adding dart cli completion config entry '
178195
'to $_shellRCFilePath',
179196
);
@@ -190,15 +207,15 @@ class CompletionInstallation {
190207
if (!shellRCFile.existsSync()) {
191208
throw CompletionInstallationException(
192209
rootCommand: rootCommand,
193-
message: 'No file found at ${shellRCFile.path}',
210+
message: 'No configuration file found at ${shellRCFile.path}',
194211
);
195212
}
196213

197214
final containsLine =
198215
shellRCFile.readAsStringSync().contains(completionConfigPath);
199216

200217
if (containsLine) {
201-
logger.detail('A completion config entry was found on'
218+
logger.warn('A completion config entry was already found on'
202219
' $_shellRCFilePath.');
203220
return;
204221
}
@@ -235,13 +252,13 @@ class CompletionInstallation {
235252
'''
236253
## [$scriptName]
237254
## $description
238-
${configuration.sourceLineTemplate(scriptPath)}
255+
${configuration!.sourceLineTemplate(scriptPath)}
239256
## [/$scriptName]
240257
241258
''',
242259
);
243260

244-
logger.detail('Added config to $configFilePath');
261+
logger.info('Added config to $configFilePath');
245262
}
246263
}
247264

pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ environment:
77
sdk: ">=2.18.0 <3.0.0"
88

99
dependencies:
10+
args: ^2.3.1
1011
mason_logger: ^0.2.2
1112
meta: ^1.8.0
1213
path: ^1.8.2

0 commit comments

Comments
 (0)