Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ Additionally, when `enableAutoInstall` is disabled a `completion-script` command
$ example_cli completion-script >> ~/.zshrc
```

When `enableAutoInstall` is enabled, users that prefer to opt out of the automatic installation can do so by running the hidden `disable-completion-auto-install` command. This choice is persisted, so completion files will no longer be installed automatically on command runs:

```bash
$ example_cli disable-completion-auto-install
```

Users can still install completion files manually with `install-completion-files`, which also re-enables the automatic installation.

## Documentation 📝

For an overview of how this package works, check out the [documentation][docs_link].
Expand Down
1 change: 1 addition & 0 deletions lib/src/command_runner/commands/commands.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export 'disable_completion_installation_command.dart';
export 'handle_completion_command.dart';
export 'install_completion_files_command.dart';
export 'print_completion_script_command.dart';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import 'dart:async';

import 'package:args/command_runner.dart';
import 'package:cli_completion/cli_completion.dart';

/// {@template disable_completion_installation_command}
/// A hidden [Command] added by [CompletionCommandRunner] that allows the user
/// to disable the automatic installation of completion files.
///
/// By default, [CompletionCommandRunner] tries to install completion files
/// upon any command run. Running this command persists the user's choice to
/// opt out of that behavior.
///
/// Users can still manually install completion files via the
/// `install-completion-files` command, which also re-enables the automatic
/// installation.
/// {@endtemplate}
class DisableCompletionInstallationCommand<T> extends Command<T> {
/// {@macro disable_completion_installation_command}
DisableCompletionInstallationCommand();

@override
String get description {
return 'Disables the automatic installation of completion files.';
}

/// The string that the user can call to disable the automatic installation
/// of completion files.
static const commandName = 'disable-completion-auto-install';

@override
String get name => commandName;

@override
bool get hidden => true;

@override
CompletionCommandRunner<T> get runner {
return super.runner! as CompletionCommandRunner<T>;
}

@override
FutureOr<T>? run() {
runner.disableAutoInstall();
return null;
}
}
39 changes: 34 additions & 5 deletions lib/src/command_runner/completion_command_runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ import 'package:meta/meta.dart';
/// Adds [InstallCompletionFilesCommand] to enable the user to
/// manually install completion files.
///
/// When [enableAutoInstall] is disabled, it also adds
/// When [enableAutoInstall] is enabled, it also adds
/// [DisableCompletionInstallationCommand] so the user can opt out of the
/// automatic installation of completion files.
///
/// When [enableAutoInstall] is disabled, it instead adds
/// [PrintCompletionScriptCommand] so the user can print the completion script
/// and install it manually.
abstract class CompletionCommandRunner<T> extends CommandRunner<T> {
Expand All @@ -36,10 +40,14 @@ abstract class CompletionCommandRunner<T> extends CommandRunner<T> {
addCommand(InstallCompletionFilesCommand<T>());
addCommand(UnistallCompletionFilesCommand<T>());

// The print completion script command is only useful when the completion
// files are not installed automatically. Otherwise, users should rely on
// the auto installation (or the `install-completion-files` command).
if (!enableAutoInstall) {
if (enableAutoInstall) {
// Allow the user to opt out of the automatic installation of completion
// files that is performed on any command run.
addCommand(DisableCompletionInstallationCommand<T>());
} else {
// The print completion script command is only useful when the completion
// files are not installed automatically. Otherwise, users should rely on
// the auto installation (or the `install-completion-files` command).
addCommand(PrintCompletionScriptCommand<T>());
}
}
Expand Down Expand Up @@ -86,6 +94,7 @@ abstract class CompletionCommandRunner<T> extends CommandRunner<T> {
HandleCompletionRequestCommand.commandName,
InstallCompletionFilesCommand.commandName,
UnistallCompletionFilesCommand.commandName,
DisableCompletionInstallationCommand.commandName,
};

@override
Expand All @@ -94,6 +103,11 @@ abstract class CompletionCommandRunner<T> extends CommandRunner<T> {
if (enableAutoInstall &&
!_reservedCommands.contains(topLevelResults.command?.name)) {
// When auto installing, use error level to display messages.
//
// The installation is skipped if the user has opted out of the automatic
// installation of completion files. This is handled by
// [CompletionInstallation.install] which respects the persisted user
// preference when not forced.
tryInstallCompletionFiles(Level.error);
}

Expand All @@ -113,6 +127,21 @@ abstract class CompletionCommandRunner<T> extends CommandRunner<T> {
}
}

/// Disables the automatic installation of completion files.
///
/// This persists the user's choice so that completion files are no longer
/// automatically installed upon command runs. Users can still manually
/// install completion files via the [InstallCompletionFilesCommand], which
/// also re-enables the automatic installation.
@internal
void disableAutoInstall() {
try {
completionInstallation.setAutoInstallEnabled(enabled: false);
} on Exception catch (e) {
completionInstallationLogger.err(e.toString());
}
}

/// Prints the completion script for the current shell to stdout.
///
/// This is used by [PrintCompletionScriptCommand] to allow users to install
Expand Down
33 changes: 32 additions & 1 deletion lib/src/installer/completion_configuration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ class CompletionConfiguration {
const CompletionConfiguration._({
required this.uninstalls,
required this.installs,
required this.enabled,
});

/// Creates an empty [CompletionConfiguration].
@visibleForTesting
CompletionConfiguration.empty()
: uninstalls = ShellCommandsMap({}),
installs = ShellCommandsMap({});
installs = ShellCommandsMap({}),
enabled = true;

/// Creates a [CompletionConfiguration] from the given [file] content.
///
Expand Down Expand Up @@ -67,6 +69,7 @@ class CompletionConfiguration {
decodedJson,
jsonKey: CompletionConfiguration.installsJsonKey,
),
enabled: _jsonDecodeEnabled(decodedJson),
);
}

Expand All @@ -78,6 +81,10 @@ class CompletionConfiguration {
@visibleForTesting
static const String installsJsonKey = 'installs';

/// The JSON key for the [enabled] field.
@visibleForTesting
static const String enabledJsonKey = 'enabled';

/// Stores those commands that have been manually uninstalled by the user.
///
/// Uninstalls are specific to a given [SystemShell].
Expand All @@ -88,6 +95,15 @@ class CompletionConfiguration {
/// Installed commands are specific to a given [SystemShell].
final ShellCommandsMap installs;

/// Whether the automatic installation of completion files is enabled.
///
/// When set to false, the [CompletionCommandRunner] will not attempt to
/// automatically install completion files upon command runs. Users can
/// still manually install completion files.
///
/// Defaults to true.
final bool enabled;

/// Stores the [CompletionConfiguration] in the given [file].
void writeTo(File file) {
if (!file.existsSync()) {
Expand All @@ -101,6 +117,7 @@ class CompletionConfiguration {
return jsonEncode({
uninstallsJsonKey: _jsonEncodeShellCommandsMap(uninstalls),
installsJsonKey: _jsonEncodeShellCommandsMap(installs),
enabledJsonKey: enabled,
});
}

Expand All @@ -109,14 +126,28 @@ class CompletionConfiguration {
CompletionConfiguration copyWith({
ShellCommandsMap? uninstalls,
ShellCommandsMap? installs,
bool? enabled,
}) {
return CompletionConfiguration._(
uninstalls: uninstalls ?? this.uninstalls,
installs: installs ?? this.installs,
enabled: enabled ?? this.enabled,
);
}
}

/// Decodes the [CompletionConfiguration.enabled] field from the given [json].
///
/// If the value is missing or not a boolean, it defaults to true so that
/// auto installation remains enabled unless the user explicitly disables it.
bool _jsonDecodeEnabled(Map<String, dynamic> json) {
final value = json[CompletionConfiguration.enabledJsonKey];
if (value is bool) {
return value;
}
return true;
}

/// Decodes [ShellCommandsMap] from the given [json].
///
/// If the [json] is not partially or fully valid, it handles issues gracefully
Expand Down
37 changes: 35 additions & 2 deletions lib/src/installer/completion_installation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,31 @@ class CompletionInstallation {
return File(path.join(completionConfigDir.path, 'config.json'));
}

/// Whether the automatic installation of completion files is enabled.
///
/// This reads the user's persisted preference from the
/// [completionConfigurationFile]. It defaults to true when no preference has
/// been persisted.
bool get isAutoInstallEnabled {
return CompletionConfiguration.fromFile(
completionConfigurationFile,
).enabled;
}

/// Persists whether the automatic installation of completion files is
/// [enabled].
///
/// This is used to let the user opt out of the automatic installation of
/// completion files performed on command runs.
void setAutoInstallEnabled({required bool enabled}) {
final completionConfiguration = CompletionConfiguration.fromFile(
completionConfigurationFile,
);
completionConfiguration
.copyWith(enabled: enabled)
.writeTo(completionConfigurationFile);
}

/// Install completion configuration files for a [rootCommand] in the
/// current shell.
///
Expand Down Expand Up @@ -145,6 +170,10 @@ class CompletionInstallation {
command: rootCommand,
systemShell: configuration.shell,
),
// Installing completion files (either automatically or manually)
// implies that the user wants completion, so we re-enable the
// automatic installation in case it was previously disabled.
enabled: true,
)
.writeTo(completionConfigurationFile);
}
Expand Down Expand Up @@ -185,12 +214,16 @@ class CompletionInstallation {
/// Wether the completion configuration files for a [rootCommand] should be
/// installed or not.
///
/// It will return false if the root command is already installed or it
/// has been explicitly uninstalled.
/// It will return false if the user has disabled the automatic installation,
/// if the root command is already installed or if it has been explicitly
/// uninstalled.
bool _shouldInstall(String rootCommand) {
final completionConfiguration = CompletionConfiguration.fromFile(
completionConfigurationFile,
);
if (!completionConfiguration.enabled) {
return false;
}
final systemShell = configuration!.shell;
final isInstalled = completionConfiguration.installs.contains(
command: rootCommand,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import 'package:cli_completion/cli_completion.dart';
import 'package:cli_completion/installer.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart';

class _MockLogger extends Mock implements Logger {}

class _MockCompletionInstallation extends Mock
implements CompletionInstallation {}

class _TestCompletionCommandRunner extends CompletionCommandRunner<int> {
_TestCompletionCommandRunner() : super('test', 'Test command runner');

@override
// Override acceptable for test files
// ignore: overridden_fields
final Logger completionInstallationLogger = _MockLogger();

@override
final CompletionInstallation completionInstallation =
_MockCompletionInstallation();
}

void main() {
group('DisableCompletionInstallationCommand', () {
late _TestCompletionCommandRunner commandRunner;

setUp(() {
commandRunner = _TestCompletionCommandRunner();
});

test('can be instantiated', () {
expect(DisableCompletionInstallationCommand<int>(), isNotNull);
});

test('is hidden', () {
expect(DisableCompletionInstallationCommand<int>().hidden, isTrue);
});

test('description', () {
expect(
DisableCompletionInstallationCommand<int>().description,
'Disables the automatic installation of completion files.',
);
});

test('disables the automatic installation', () async {
await commandRunner.run(['disable-completion-auto-install']);

verify(
() => commandRunner.completionInstallation.setAutoInstallEnabled(
enabled: false,
),
).called(1);
});

test('logs an error when an unknown exception happens', () async {
when(
() => commandRunner.completionInstallation.setAutoInstallEnabled(
enabled: any(named: 'enabled'),
),
).thenThrow(Exception('oops'));

await commandRunner.run(['disable-completion-auto-install']);

verify(
() => commandRunner.completionInstallationLogger.err(any()),
).called(1);
});
});
}
Loading
Loading