Skip to content
Merged
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
35 changes: 35 additions & 0 deletions docs/1-essentials/04-console-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,41 @@ Tempest console comes with a range of interactive components that can be used to
Interactive components are only supported on Mac and Linux. On Windows, Tempest will fall back to non-interactive versions of these components.
:::

## Shell completion

Tempest provides shell completion for Zsh and Bash. This allows you to press `Tab` to autocomplete command names and options.

### Installing completions

Run the install command and follow the prompts:

```console
<dim>./</dim>tempest completion:install
```

The installer will detect your current shell, copy the completion script to the appropriate location, and provide instructions for enabling it.

For Zsh, you'll need to ensure the completions directory is in your `fpath` and reload completions:

```zsh
# Add to ~/.zshrc
fpath=(~/.zsh/completions $fpath)
autoload -Uz compinit && compinit
```

For Bash, source the completion file in your `~/.bashrc`:

```bash
source ~/.bash_completion.d/tempest.bash
```

### Additional commands

You may also use these related commands:

- `completion:show` — Output the completion script to stdout (useful for custom installation)
- `completion:uninstall` — Remove the installed completion script

## Middleware

Console middleware can be applied globally or on a per-command basis. Global console middleware will be discovered and applied automatically, by priority order.
Expand Down
31 changes: 31 additions & 0 deletions packages/console/src/Actions/ResolveShell.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Tempest\Console\Actions;

use Tempest\Console\Console;
use Tempest\Console\Enums\Shell;

final readonly class ResolveShell
{
public function __construct(
private Console $console,
) {}

public function __invoke(string $question = 'Which shell?'): ?Shell
{
$detected = Shell::detect();

if ($this->console->supportsPrompting()) {
/** @var Shell */
return $this->console->ask(
question: $question,
options: Shell::class,
default: $detected,
);
}

return $detected;
}
}
98 changes: 98 additions & 0 deletions packages/console/src/Commands/CompletionInstallCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php

declare(strict_types=1);

namespace Tempest\Console\Commands;

use Symfony\Component\Filesystem\Path;
use Tempest\Console\Actions\ResolveShell;
use Tempest\Console\Console;
use Tempest\Console\ConsoleArgument;
use Tempest\Console\ConsoleCommand;
use Tempest\Console\Enums\Shell;
use Tempest\Console\ExitCode;
use Tempest\Support\Filesystem;

use function Tempest\Support\path;

final readonly class CompletionInstallCommand
{
public function __construct(
private Console $console,
private ResolveShell $resolveShell,
) {}

#[ConsoleCommand(
name: 'completion:install',
description: 'Install shell completion for Tempest',
)]
public function __invoke(
#[ConsoleArgument(
description: 'The shell to install completions for (zsh, bash)',
aliases: ['-s'],
)]
?Shell $shell = null,
#[ConsoleArgument(
description: 'Skip confirmation prompts',
aliases: ['-f'],
)]
bool $force = false,
): ExitCode {
$shell ??= ($this->resolveShell)('Which shell do you want to install completions for?');

if ($shell === null) {
$this->console->error('Could not detect shell. Please specify one using the --shell option. Possible values are: zsh, bash.');

return ExitCode::ERROR;
}

$sourcePath = $this->getSourcePath($shell);
$targetDir = $shell->getCompletionsDirectory();
$targetPath = $shell->getInstalledCompletionPath();

if (! Filesystem\is_file($sourcePath)) {
$this->console->error("Completion script not found: {$sourcePath}");

return ExitCode::ERROR;
}

if (! $force) {
$this->console->info("Installing {$shell->value} completions");
$this->console->keyValue('Source', $sourcePath);
$this->console->keyValue('Target', $targetPath);
$this->console->writeln();

if (! $this->console->confirm('Proceed with installation?', default: true)) {
$this->console->warning('Installation cancelled.');

return ExitCode::CANCELLED;
}
}

Filesystem\ensure_directory_exists($targetDir);

if (Filesystem\is_file($targetPath)) {
if (! $force && ! $this->console->confirm('Completion file already exists. Overwrite?', default: false)) {
$this->console->warning('Installation cancelled.');

return ExitCode::CANCELLED;
}
}

Filesystem\copy_file($sourcePath, $targetPath, overwrite: true);
$this->console->success("Installed completion script to: {$targetPath}");

$this->console->writeln();
$this->console->info('Next steps:');
$this->console->instructions($shell->getPostInstallInstructions());

return ExitCode::SUCCESS;
}

private function getSourcePath(Shell $shell): string
{
return Path::canonicalize(
path(__DIR__, '..', $shell->getSourceFilename())->toString(),
);
}
}
63 changes: 63 additions & 0 deletions packages/console/src/Commands/CompletionShowCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

declare(strict_types=1);

namespace Tempest\Console\Commands;

use Symfony\Component\Filesystem\Path;
use Tempest\Console\Actions\ResolveShell;
use Tempest\Console\Console;
use Tempest\Console\ConsoleArgument;
use Tempest\Console\ConsoleCommand;
use Tempest\Console\Enums\Shell;
use Tempest\Console\ExitCode;
use Tempest\Support\Filesystem;

use function Tempest\Support\path;

final readonly class CompletionShowCommand
{
public function __construct(
private Console $console,
private ResolveShell $resolveShell,
) {}

#[ConsoleCommand(
name: 'completion:show',
description: 'Output the shell completion script to stdout',
)]
public function __invoke(
#[ConsoleArgument(
description: 'The shell to show completions for (zsh, bash)',
aliases: ['-s'],
)]
?Shell $shell = null,
): ExitCode {
$shell ??= ($this->resolveShell)('Which shell completion script do you want to see?');

if ($shell === null) {
$this->console->error('Could not detect shell. Please specify one using the --shell option. Possible values are: zsh, bash.');

return ExitCode::ERROR;
}

$sourcePath = $this->getSourcePath($shell);

if (! Filesystem\is_file($sourcePath)) {
$this->console->error("Completion script not found: {$sourcePath}");

return ExitCode::ERROR;
}

$this->console->writeRaw(Filesystem\read_file($sourcePath));

return ExitCode::SUCCESS;
}

private function getSourcePath(Shell $shell): string
{
return Path::canonicalize(
path(__DIR__, '..', $shell->getSourceFilename())->toString(),
);
}
}
76 changes: 76 additions & 0 deletions packages/console/src/Commands/CompletionUninstallCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

declare(strict_types=1);

namespace Tempest\Console\Commands;

use Tempest\Console\Actions\ResolveShell;
use Tempest\Console\Console;
use Tempest\Console\ConsoleArgument;
use Tempest\Console\ConsoleCommand;
use Tempest\Console\Enums\Shell;
use Tempest\Console\ExitCode;
use Tempest\Support\Filesystem;

final readonly class CompletionUninstallCommand
{
public function __construct(
private Console $console,
private ResolveShell $resolveShell,
) {}

#[ConsoleCommand(
name: 'completion:uninstall',
description: 'Uninstall shell completion for Tempest',
)]
public function __invoke(
#[ConsoleArgument(
description: 'The shell to uninstall completions for (zsh, bash)',
aliases: ['-s'],
)]
?Shell $shell = null,
#[ConsoleArgument(
description: 'Skip confirmation prompts',
aliases: ['-f'],
)]
bool $force = false,
): ExitCode {
$shell ??= ($this->resolveShell)('Which shell completions do you want to uninstall?');

if ($shell === null) {
$this->console->error('Could not detect shell. Please specify one using the --shell option. Possible values are: zsh, bash.');

return ExitCode::ERROR;
}

$targetPath = $shell->getInstalledCompletionPath();

if (! Filesystem\is_file($targetPath)) {
$this->console->warning("Completion file not found: {$targetPath}");
$this->console->info('Nothing to uninstall.');

return ExitCode::SUCCESS;
}

if (! $force) {
$this->console->info("Uninstalling {$shell->value} completions");
$this->console->keyValue('File', $targetPath);
$this->console->writeln();

if (! $this->console->confirm('Proceed with uninstallation?', default: true)) {
$this->console->warning('Uninstallation cancelled.');

return ExitCode::CANCELLED;
}
}

Filesystem\delete_file($targetPath);
$this->console->success("Removed completion script: {$targetPath}");

$this->console->writeln();
$this->console->info('Remember to remove any related lines from your shell configuration:');
$this->console->keyValue('Config file', $shell->getRcFile());

return ExitCode::SUCCESS;
}
}
Loading