Skip to content

Commit 246a1cc

Browse files
Add shell completions
Closes #6
1 parent c6e5218 commit 246a1cc

File tree

10 files changed

+173
-26
lines changed

10 files changed

+173
-26
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
## Unreleased - [ReleaseDate]
44

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

713
- Upgrade Rust version to 1.80.0

Cargo.lock

+58-24
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ path = "src/main.rs"
1616

1717
[dependencies]
1818
anyhow = {version = "^1.0.65", features = ["backtrace"]}
19-
clap = {version = "^4.4.0", features = ["derive"]}
19+
clap = {version = "^4.5.19", features = ["derive"]}
20+
clap_complete = {version = "4.5.32", features = ["unstable-dynamic"]}
2021
ctrlc = "^3.2.3"
2122
derive_more = "^0.99.17"
2223
dialoguer = {version = "^0.10.2", default-features = false}

docs/src/SUMMARY.md

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
- [Inheritance & Cascading Configs](./user_guide/inheritance.md)
1616
- [Side Effects](./user_guide/side_effects.md)
1717
- [`es run` and Shell Interactions](./user_guide/run_advanced.md)
18+
- [Shell Completions](./user_guide/shell_completions.md)
1819

1920
# API Reference
2021

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Shell Completions
2+
3+
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).
4+
5+
> 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.
6+
7+
To source your completions:
8+
9+
**WARNING:** We recommend re-sourcing your completions on upgrade.
10+
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.
11+
12+
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.
13+
14+
## Bash
15+
16+
```bash
17+
echo "source <(COMPLETE=bash es)" >> ~/.bashrc
18+
```
19+
20+
## Elvish
21+
22+
```elvish
23+
echo "eval (E:COMPLETE=elvish es | slurp)" >> ~/.elvish/rc.elv
24+
```
25+
26+
## Fish
27+
28+
```fish
29+
echo "source (COMPLETE=fish es | psub)" >> ~/.config/fish/config.fish
30+
```
31+
32+
## Powershell
33+
34+
```powershell
35+
echo "COMPLETE=powershell es | Invoke-Expression" >> $PROFILE
36+
```
37+
38+
## Zsh
39+
40+
````zsh
41+
echo "source <(COMPLETE=zsh es)" >> ~/.zshrc
42+
```
43+
````

src/commands/mod.rs

+4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use crate::{
66
commands::{
77
init::InitCommand, run::RunCommand, set::SetCommand, show::ShowCommand,
88
},
9+
completions::{complete_application, complete_profile},
910
config::{Config, Name, Profile},
1011
console::prompt_options,
1112
environment::Environment,
@@ -14,6 +15,7 @@ use crate::{
1415
GlobalArgs,
1516
};
1617
use clap::Subcommand;
18+
use clap_complete::ArgValueCompleter;
1719
use smol::lock::OnceCell;
1820
use std::path::PathBuf;
1921

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

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

src/commands/show.rs

+4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
use crate::{
22
commands::{CommandContext, SubcommandTrait},
3+
completions::{complete_application, complete_profile},
34
config::{MapExt, Name},
45
};
56
use clap::{Parser, Subcommand};
7+
use clap_complete::ArgValueCompleter;
68

79
/// Print configuration and meta information
810
#[derive(Clone, Debug, Parser)]
@@ -19,9 +21,11 @@ enum ShowSubcommand {
1921
// are incorrect for this use case
2022
/// Application to show configuration for. If omitted, show all
2123
/// applications.
24+
#[clap(add = ArgValueCompleter::new(complete_application))]
2225
application: Option<Name>,
2326
/// Profile to show configuration for. If omitted, show all profiles
2427
/// for the selected application.
28+
#[clap(add = ArgValueCompleter::new(complete_profile))]
2529
profile: Option<Name>,
2630
},
2731
/// Print the name or path to the shell in use

src/completions.rs

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
use crate::config::{Config, Name};
2+
use clap_complete::CompletionCandidate;
3+
use std::ffi::OsStr;
4+
5+
/// Provide completions for application names
6+
pub fn complete_application(current: &OsStr) -> Vec<CompletionCandidate> {
7+
let Ok(config) = Config::load() else {
8+
return Vec::new();
9+
};
10+
11+
get_candidates(config.applications.keys().map(Name::as_str), current)
12+
}
13+
14+
/// Provide completions for profile names
15+
pub fn complete_profile(current: &OsStr) -> Vec<CompletionCandidate> {
16+
let Ok(config) = Config::load() else {
17+
return Vec::new();
18+
};
19+
20+
// Suggest all profiles for all applications. Ideally we could grab the
21+
// prior argument to tell us what application we're in, but I'm not sure if
22+
// clap exposes that at all
23+
get_candidates(
24+
config
25+
.applications
26+
.values()
27+
.flat_map(|application| application.profiles.keys())
28+
.map(Name::as_str),
29+
current,
30+
)
31+
}
32+
33+
fn get_candidates<'a>(
34+
iter: impl Iterator<Item = &'a str>,
35+
current: &OsStr,
36+
) -> Vec<CompletionCandidate> {
37+
let Some(current) = current.to_str() else {
38+
return Vec::new();
39+
};
40+
// Only include IDs prefixed by the input we've gotten so far
41+
iter.filter(|value| value.starts_with(current))
42+
.map(CompletionCandidate::new)
43+
.collect()
44+
}

src/config/mod.rs

+6
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,12 @@ impl Config {
229229
}
230230
}
231231

232+
impl Name {
233+
pub fn as_str(&self) -> &str {
234+
self.0.as_str()
235+
}
236+
}
237+
232238
// Validate application/profile name. We do a bit of sanity checking here to
233239
// prevent stuff that might be confusing, or collide with env-select features
234240
impl FromStr for Name {

0 commit comments

Comments
 (0)