Skip to content

Commit

Permalink
Add shell completions
Browse files Browse the repository at this point in the history
Closes #6
  • Loading branch information
LucasPickering committed Oct 4, 2024
1 parent c6e5218 commit 246a1cc
Show file tree
Hide file tree
Showing 10 changed files with 173 additions and 26 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## Unreleased - [ReleaseDate]

### Added

- Add shell completions, accessed by enabling the `COMPLETE` environment variable [#6](https://github.com/LucasPickering/env-select/issues/6)
- For example, adding `COMPLETE=fish es | source` to your `fish.config` will enable completions for fish
- [See docs](https://env-select.lucaspickering.me/book/user_guide/shell_completions.html) for more info and a list of supported shells

### Changed

- Upgrade Rust version to 1.80.0
Expand Down
82 changes: 58 additions & 24 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ path = "src/main.rs"

[dependencies]
anyhow = {version = "^1.0.65", features = ["backtrace"]}
clap = {version = "^4.4.0", features = ["derive"]}
clap = {version = "^4.5.19", features = ["derive"]}
clap_complete = {version = "4.5.32", features = ["unstable-dynamic"]}
ctrlc = "^3.2.3"
derive_more = "^0.99.17"
dialoguer = {version = "^0.10.2", default-features = false}
Expand Down
1 change: 1 addition & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
- [Inheritance & Cascading Configs](./user_guide/inheritance.md)
- [Side Effects](./user_guide/side_effects.md)
- [`es run` and Shell Interactions](./user_guide/run_advanced.md)
- [Shell Completions](./user_guide/shell_completions.md)

# API Reference

Expand Down
43 changes: 43 additions & 0 deletions docs/src/user_guide/shell_completions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Shell Completions

Env-select provides tab completions for most shells. For the full list of supported shells, [see the clap docs](https://docs.rs/clap_complete/latest/clap_complete/aot/enum.Shell.html).

> Note: Env-select uses clap's native shell completions, which are still experimental. [This issue](https://github.com/clap-rs/clap/issues/3166) outlines the remaining work to be done.
To source your completions:

**WARNING:** We recommend re-sourcing your completions on upgrade.
These completions work by generating shell code that calls into `your_program` while completing. That interface is unstable and a mismatch between the shell code and `your_program` may result in either invalid completions or no completions being generated.

For this reason, we recommend generating the shell code anew on shell startup so that it is "self-correcting" on shell launch, rather than writing the generated completions to a file.

## Bash

```bash
echo "source <(COMPLETE=bash es)" >> ~/.bashrc
```

## Elvish

```elvish
echo "eval (E:COMPLETE=elvish es | slurp)" >> ~/.elvish/rc.elv
```

## Fish

```fish
echo "source (COMPLETE=fish es | psub)" >> ~/.config/fish/config.fish
```

## Powershell

```powershell
echo "COMPLETE=powershell es | Invoke-Expression" >> $PROFILE
```

## Zsh

````zsh
echo "source <(COMPLETE=zsh es)" >> ~/.zshrc
```
````
4 changes: 4 additions & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::{
commands::{
init::InitCommand, run::RunCommand, set::SetCommand, show::ShowCommand,
},
completions::{complete_application, complete_profile},
config::{Config, Name, Profile},
console::prompt_options,
environment::Environment,
Expand All @@ -14,6 +15,7 @@ use crate::{
GlobalArgs,
};
use clap::Subcommand;
use clap_complete::ArgValueCompleter;
use smol::lock::OnceCell;
use std::path::PathBuf;

Expand Down Expand Up @@ -58,10 +60,12 @@ trait SubcommandTrait {
pub struct Selection {
/// Application to select a profile for. If omitted, an interactive prompt
/// will be shown to select between possible options
#[clap(add = ArgValueCompleter::new(complete_application))]
pub application: Option<Name>,

/// Profile to select. If omitted, an interactive prompt will be shown to
/// select between possible options.
#[clap(add = ArgValueCompleter::new(complete_profile))]
pub profile: Option<Name>,
}

Expand Down
4 changes: 4 additions & 0 deletions src/commands/show.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use crate::{
commands::{CommandContext, SubcommandTrait},
completions::{complete_application, complete_profile},
config::{MapExt, Name},
};
use clap::{Parser, Subcommand};
use clap_complete::ArgValueCompleter;

/// Print configuration and meta information
#[derive(Clone, Debug, Parser)]
Expand All @@ -19,9 +21,11 @@ enum ShowSubcommand {
// are incorrect for this use case
/// Application to show configuration for. If omitted, show all
/// applications.
#[clap(add = ArgValueCompleter::new(complete_application))]
application: Option<Name>,
/// Profile to show configuration for. If omitted, show all profiles
/// for the selected application.
#[clap(add = ArgValueCompleter::new(complete_profile))]
profile: Option<Name>,
},
/// Print the name or path to the shell in use
Expand Down
44 changes: 44 additions & 0 deletions src/completions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use crate::config::{Config, Name};
use clap_complete::CompletionCandidate;
use std::ffi::OsStr;

/// Provide completions for application names
pub fn complete_application(current: &OsStr) -> Vec<CompletionCandidate> {
let Ok(config) = Config::load() else {
return Vec::new();
};

get_candidates(config.applications.keys().map(Name::as_str), current)
}

/// Provide completions for profile names
pub fn complete_profile(current: &OsStr) -> Vec<CompletionCandidate> {
let Ok(config) = Config::load() else {
return Vec::new();
};

// Suggest all profiles for all applications. Ideally we could grab the
// prior argument to tell us what application we're in, but I'm not sure if
// clap exposes that at all
get_candidates(
config
.applications
.values()
.flat_map(|application| application.profiles.keys())
.map(Name::as_str),
current,
)
}

fn get_candidates<'a>(
iter: impl Iterator<Item = &'a str>,
current: &OsStr,
) -> Vec<CompletionCandidate> {
let Some(current) = current.to_str() else {
return Vec::new();
};
// Only include IDs prefixed by the input we've gotten so far
iter.filter(|value| value.starts_with(current))
.map(CompletionCandidate::new)
.collect()
}
6 changes: 6 additions & 0 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,12 @@ impl Config {
}
}

impl Name {
pub fn as_str(&self) -> &str {
self.0.as_str()
}
}

// Validate application/profile name. We do a bit of sanity checking here to
// prevent stuff that might be confusing, or collide with env-select features
impl FromStr for Name {
Expand Down
Loading

0 comments on commit 246a1cc

Please sign in to comment.