Skip to content

Commit

Permalink
Use dynamic shell completions
Browse files Browse the repository at this point in the history
This replaces the subcommand with an env var to enable completions
  • Loading branch information
LucasPickering committed Oct 4, 2024
1 parent b9d9bd9 commit c95ff95
Show file tree
Hide file tree
Showing 12 changed files with 155 additions and 52 deletions.
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

### Added

- Add `slumber completions` subcommand
- Right now only static completions are available (e.g. subcommands). In the future I am to add dynamic completions based on your collection file (e.g. recipe and profile IDs)
- Add shell completions, accessed by enabling the `COMPLETE` environment variable
- For example, adding `COMPLETE=fish slumber | source` to your `fish.config` will enable completions for fish
- [See docs](https://slumber.lucaspickering.me/book/troubleshooting/shell_completions.html) for more info and a list of supported shells

## [2.1.0] - 2024-09-27

Expand Down
1 change: 0 additions & 1 deletion crates/cli/src/commands.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
pub mod collections;
pub mod completions;
pub mod generate;
pub mod history;
pub mod import;
Expand Down
32 changes: 0 additions & 32 deletions crates/cli/src/commands/completions.rs

This file was deleted.

28 changes: 23 additions & 5 deletions crates/cli/src/commands/history.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
use crate::{util::HeaderDisplay, GlobalArgs, Subcommand};
use crate::{
completions::{complete_profile, complete_recipe},
util::HeaderDisplay,
GlobalArgs, Subcommand,
};
use anyhow::anyhow;
use clap::Parser;
use clap::{Parser, ValueHint};
use clap_complete::ArgValueCompleter;
use dialoguer::console::Style;
use slumber_core::{
collection::{CollectionFile, ProfileId, RecipeId},
Expand All @@ -25,16 +30,29 @@ enum HistorySubcommand {
#[command(visible_alias = "ls")]
List {
/// Recipe to query for
#[clap(add = ArgValueCompleter::new(complete_recipe))]
recipe: RecipeId,

/// Profile to query for. If omitted, query for requests with no
/// profile
#[clap(long = "profile", short)]
#[clap(
long = "profile",
short,
add = ArgValueCompleter::new(complete_profile),
)]
profile: Option<ProfileId>,
},

/// Print an entire request/response by ID
Get { request: RequestId },
/// Print an entire request/response
Get {
// Disable completion for this arg. We could load all the request IDs
// from the DB, but that's not worth the effort since this is an
// unstable command still and people will rarely be typing an ID by
// hand, they'll typically just copy paste
/// ID of the request/response to print
#[clap(value_hint = ValueHint::Other)]
request: RequestId,
},
}

impl Subcommand for HistoryCommand {
Expand Down
18 changes: 15 additions & 3 deletions crates/cli/src/commands/request.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
use crate::{util::HeaderDisplay, GlobalArgs, Subcommand};
use crate::{
completions::{complete_profile, complete_recipe},
util::HeaderDisplay,
GlobalArgs, Subcommand,
};
use anyhow::{anyhow, Context};
use clap::Parser;
use clap::{Parser, ValueHint};
use clap_complete::ArgValueCompleter;
use dialoguer::{Input, Password, Select as DialoguerSelect};
use indexmap::IndexMap;
use itertools::Itertools;
Expand Down Expand Up @@ -59,19 +64,26 @@ pub struct RequestCommand {
#[derive(Clone, Debug, Parser)]
pub struct BuildRequestCommand {
/// ID of the recipe to render into a request
#[clap(add = ArgValueCompleter::new(complete_recipe))]
recipe_id: RecipeId,

/// ID of the profile to pull template values from. If omitted and the
/// collection has default profile defined, use that profile. Otherwise,
/// profile data will not be available.
#[clap(long = "profile", short)]
#[clap(
long = "profile",
short,
add = ArgValueCompleter::new(complete_profile),
)]
profile: Option<ProfileId>,

/// List of key=value template field overrides
#[clap(
long = "override",
short = 'o',
value_parser = parse_key_val::<String, String>,
// There's no reasonable way of doing completions on this, so disable
value_hint = ValueHint::Other,
)]
overrides: Vec<(String, String)>,
}
Expand Down
50 changes: 50 additions & 0 deletions crates/cli/src/completions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//! Shell completion utilities
use clap_complete::CompletionCandidate;
use slumber_core::collection::{Collection, CollectionFile};
use std::{ffi::OsStr, ops::Deref};

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

get_candidates(collection.profiles.keys(), current)
}

/// Provide completions for recipe IDs
pub fn complete_recipe(current: &OsStr) -> Vec<CompletionCandidate> {
let Ok(collection) = load_collection() else {
return Vec::new();
};

get_candidates(
collection
.recipes
.iter()
// Include recipe IDs only. Folder IDs are never passed to the CLI
.filter_map(|(_, node)| Some(&node.recipe()?.id)),
current,
)
}

fn load_collection() -> anyhow::Result<Collection> {
// For now we just lean on the default collection paths. In the future we
// should be able to look for a --file arg in the command and use that path
let path = CollectionFile::try_path(None, None)?;
Collection::load(&path)
}

fn get_candidates<'a, T: 'a + Deref<Target = String>>(
iter: impl Iterator<Item = &'a T>,
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(|value| CompletionCandidate::new(value.as_str()))
.collect()
}
18 changes: 12 additions & 6 deletions crates/cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@
//! do so at your own risk of breakage.
mod commands;
mod completions;
mod util;

use crate::commands::{
collections::CollectionsCommand, completions::CompletionsCommand,
generate::GenerateCommand, history::HistoryCommand, import::ImportCommand,
new::NewCommand, request::RequestCommand, show::ShowCommand,
collections::CollectionsCommand, generate::GenerateCommand,
history::HistoryCommand, import::ImportCommand, new::NewCommand,
request::RequestCommand, show::ShowCommand,
};
use clap::Parser;
use clap::{CommandFactory, Parser};
use clap_complete::CompleteEnv;
use std::{path::PathBuf, process::ExitCode};

const COMMAND_NAME: &str = "slumber";
Expand All @@ -37,6 +39,12 @@ pub struct Args {
}

impl Args {
/// Check if we're in shell completion mode, which is set via the `COMPLETE`
/// env var. If so, this will print completions then exit the process
pub fn complete() {
CompleteEnv::with_factory(Args::command).complete();
}

/// Alias for [clap::Parser::parse]
pub fn parse() -> Self {
<Self as Parser>::parse()
Expand All @@ -57,7 +65,6 @@ pub struct GlobalArgs {
#[derive(Clone, Debug, clap::Subcommand)]
pub enum CliCommand {
Collections(CollectionsCommand),
Completions(CompletionsCommand),
Generate(GenerateCommand),
History(HistoryCommand),
Import(ImportCommand),
Expand All @@ -71,7 +78,6 @@ impl CliCommand {
pub async fn execute(self, global: GlobalArgs) -> anyhow::Result<ExitCode> {
match self {
Self::Collections(command) => command.execute(global).await,
Self::Completions(command) => command.execute(global).await,
Self::Generate(command) => command.execute(global).await,
Self::History(command) => command.execute(global).await,
Self::Import(command) => command.execute(global).await,
Expand Down
5 changes: 3 additions & 2 deletions crates/core/src/collection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ pub use cereal::HasId;
pub use models::*;
pub use recipe_tree::*;

use anyhow::anyhow;
use anyhow::{anyhow, Context};
use itertools::Itertools;
use std::{
env,
Expand Down Expand Up @@ -159,7 +159,8 @@ async fn load_collection(path: PathBuf) -> anyhow::Result<Collection> {
// tokio::fs for this but that just uses std::fs underneath anyway.
task::spawn_blocking(move || Collection::load(&path))
.await
.expect("TODO")
// This error only occurs if the task panics
.context("Error parsing collection")?
}

#[cfg(test)]
Expand Down
1 change: 1 addition & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,5 @@

- [Logs](./troubleshooting/logs.md)
- [Lost Request History](./troubleshooting/lost_history.md)
- [Shell Completions](./troubleshooting/shell_completions.md)
- [TLS Certificate Errors](./troubleshooting/tls.md)
2 changes: 1 addition & 1 deletion docs/src/install.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Install

See [installation instructions](/artifacts)
See [installation instructions](/artifacts). Optionally, after installation you can [enable shell completions](./troubleshooting/shell_completions.md).
46 changes: 46 additions & 0 deletions docs/src/troubleshooting/shell_completions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Shell Completions

Slumber 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: Slumber 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 slumber)" >> ~/.bashrc
```

## Elvish

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

## Fish

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

## Powershell

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

## Zsh

````zsh
echo "source <(COMPLETE=zsh slumber)" >> ~/.zshrc
```
````
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use tracing_subscriber::{filter::Targets, fmt::format::FmtSpan, prelude::*};
#[tokio::main]
async fn main() -> anyhow::Result<ExitCode> {
// Global initialization
Args::complete(); // If COMPLETE var is enabled, process will stop here
let args = Args::parse();

initialize_tracing(args.subcommand.is_some());
Expand Down

0 comments on commit c95ff95

Please sign in to comment.