Skip to content

Commit e7e28e8

Browse files
committed
Add completion-cmds for shell completion
Add a new `dist.completion-cmds` option which takes a map of binary names to the subcommand to execute to generate shell completions. This hooks in very easily with the Homebrew `generate_completions_from_executable` helper function. We allow three options for completion generation, `clap-env`, which will use `clap_complete`'s environment variable option to trigger shell completion, or `subcommand`, which specifies the subcommand and parameter format, flag or arg, for the command. All options take an array of shells. ```toml [dist.completion-cmds.mybin] trigger = "clap-env" shells = ["bash","fish"] [dist.completion-cmds.otherbin] trigger.subcommand = {{ name = "completions", format = "flag" }} shells = ["bash","fish","pwsh","zsh"] ```
1 parent 87a615c commit e7e28e8

File tree

12 files changed

+2857
-3
lines changed

12 files changed

+2857
-3
lines changed

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,19 @@ Nothing Yet!
55
# Version 1.0.6 (2025-07-03)
66

77
- Change committer of Homebrew formulas to the owner of GITHUB_TOKEN.
8+
- Add `dist.completion-cmds` option. This takes a map of binaries and a corresponding subcommand which can be executed to generate shell completions. Completions for `bash`, `zsh`, and `fish` will be installed. Currently this only used for Homebrew installs.
9+
10+
Example configs:
11+
12+
```toml
13+
[dist.completion-cmds.mybin]
14+
trigger = "clap-env"
15+
shells = ["bash","fish"]
16+
17+
[dist.completion-cmds.otherbin]
18+
trigger.subcommand = {{ name = "completions", format = "flag" }}
19+
shells = ["bash","fish","pwsh","zsh"]
20+
```
821

922
# Version 1.0.5 (2025-07-02)
1023

cargo-dist/src/backend/installer/homebrew.rs

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
//! Code for generating formula.rb
22
3+
use std::collections::BTreeMap;
4+
35
use axoasset::LocalAsset;
46
use dist_schema::{ChecksumValue, DistManifest, HomebrewPackageName};
57
use serde::Serialize;
@@ -11,7 +13,7 @@ use spdx::{
1113
use super::InstallerInfo;
1214
use crate::{
1315
backend::templates::TEMPLATE_INSTALLER_RB,
14-
config::{ChecksumStyle, LibraryStyle},
16+
config::{ChecksumStyle, CompletionTrigger, LibraryStyle, ParamFormat},
1517
errors::DistResult,
1618
installer::ExecutableZipFragment,
1719
tasks::DistGraph,
@@ -92,8 +94,40 @@ pub(crate) fn write_homebrew_formula(
9294
}
9395
map_fragments!(fragments = (arm64_linux, x86_64_linux, arm64_macos, x86_64_macos));
9496

97+
let mut completions = BTreeMap::new();
98+
for (bin, cmp) in &info.inner.completion_cmds {
99+
let mut shells = "[".to_string();
100+
let mut shell_iter = cmp.shells.iter().peekable();
101+
while let Some(shell) = shell_iter.next() {
102+
shells += &format!(":{shell}");
103+
if shell_iter.peek().is_some() {
104+
shells.push_str(", ");
105+
}
106+
}
107+
shells.push(']');
108+
109+
let (subcommand, format) = match cmp.trigger.clone() {
110+
CompletionTrigger::ClapEnv => (None, ":clap"),
111+
CompletionTrigger::Subcommand { name, format } => match format {
112+
ParamFormat::Arg => (Some(name), ":arg"),
113+
ParamFormat::Flag => (Some(name), ":flag"),
114+
},
115+
};
116+
117+
let completion = match subcommand {
118+
Some(cmd) => format!(" \"{cmd}\", shell_parameter_format: {format}, shells: {shells}"),
119+
None => format!("shell_parameter_format: {format}, shells: {shells}"),
120+
};
121+
122+
completions.insert(bin.clone(), completion);
123+
}
124+
95125
let dest_path = info.inner.dest_path.clone();
96-
let inputs = HomebrewTemplateInputs { info, fragments };
126+
let inputs = HomebrewTemplateInputs {
127+
info,
128+
fragments,
129+
completions,
130+
};
97131

98132
let script = dist
99133
.templates
@@ -109,6 +143,8 @@ struct HomebrewTemplateInputs {
109143

110144
#[serde(flatten)]
111145
fragments: HomebrewFragments<HomebrewFragment>,
146+
147+
completions: BTreeMap<String, String>,
112148
}
113149

114150
#[derive(Debug, Clone, Serialize)]

cargo-dist/src/backend/installer/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use macpkg::PkgInstallerInfo;
1111
use serde::Serialize;
1212

1313
use crate::{
14-
config::{JinjaInstallPathStrategy, LibraryStyle, ZipStyle},
14+
config::{CompletionConfig, JinjaInstallPathStrategy, LibraryStyle, ZipStyle},
1515
platform::{PlatformSupport, RuntimeConditions},
1616
InstallReceipt, ReleaseIdx,
1717
};
@@ -85,6 +85,8 @@ pub struct InstallerInfo {
8585
pub receipt: Option<InstallReceipt>,
8686
/// Aliases to install binaries under
8787
pub bin_aliases: BTreeMap<TripleName, BTreeMap<String, Vec<String>>>,
88+
/// The command to execute to generate shell completion scripts for a given binary
89+
pub completion_cmds: BTreeMap<String, CompletionConfig>,
8890
/// Whether to install generated C dynamic libraries
8991
pub install_libraries: Vec<LibraryStyle>,
9092
/// Platform-specific runtime conditions

cargo-dist/src/config/mod.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,66 @@ pub enum GithubPermission {
4444
Admin,
4545
}
4646

47+
/// Config for generating shell completions
48+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd, Ord, Eq)]
49+
#[serde(rename_all = "kebab-case")]
50+
pub struct CompletionConfig {
51+
/// The method used to trigger shell completions.
52+
pub trigger: CompletionTrigger,
53+
/// The shells completion should be generated for.
54+
pub shells: Vec<Shell>,
55+
}
56+
57+
/// The method used to trigger shell completions
58+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd, Ord, Eq)]
59+
#[serde(rename_all = "kebab-case")]
60+
pub enum CompletionTrigger {
61+
/// Clap completions enabled via the environment, e.g. `COMPLETE=bash foo`.
62+
ClapEnv,
63+
/// The name of the subcommand to generate shell completions and the parameter format it uses.
64+
Subcommand {
65+
/// The name of the completion subcommand.
66+
name: String,
67+
/// The format of the shell-type parameter of the subcommand.
68+
format: ParamFormat,
69+
},
70+
}
71+
72+
/// The format of the parameter that specifies the shell type to the completion subcommand
73+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd, Ord, Eq)]
74+
#[serde(rename_all = "kebab-case")]
75+
pub enum ParamFormat {
76+
/// Arg, e.g. `foo completions bash`.
77+
Arg,
78+
/// Flag, e.g. `foo completions --bash`.
79+
Flag,
80+
}
81+
82+
/// The type of shells that completions may be generated for
83+
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, PartialOrd, Ord, Eq)]
84+
#[serde(rename_all = "kebab-case")]
85+
pub enum Shell {
86+
/// Bash
87+
Bash,
88+
/// Fish
89+
Fish,
90+
/// Powershell
91+
Pwsh,
92+
/// Zsh
93+
Zsh,
94+
}
95+
96+
impl std::fmt::Display for Shell {
97+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98+
match self {
99+
Self::Bash => write!(f, "bash"),
100+
Self::Fish => write!(f, "fish"),
101+
Self::Pwsh => write!(f, "pwsh"),
102+
Self::Zsh => write!(f, "zsh"),
103+
}
104+
}
105+
}
106+
47107
/// Global config for commands
48108
#[derive(Debug, Clone)]
49109
pub struct Config {

cargo-dist/src/config/v0.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,10 @@ pub struct DistMetadata {
490490
#[serde(skip_serializing_if = "Option::is_none")]
491491
pub bin_aliases: Option<SortedMap<String, Vec<String>>>,
492492

493+
/// The command to execute to generate shell completion scripts for a given binary
494+
#[serde(skip_serializing_if = "Option::is_none")]
495+
pub completion_cmds: Option<SortedMap<String, CompletionConfig>>,
496+
493497
/// a prefix to add to the release.yml and tag pattern so that
494498
/// dist can co-exist with other release workflows in complex workspaces
495499
#[serde(skip_serializing_if = "Option::is_none")]
@@ -608,6 +612,7 @@ impl DistMetadata {
608612
github_custom_runners: _,
609613
github_custom_job_permissions: _,
610614
bin_aliases: _,
615+
completion_cmds: _,
611616
tag_namespace: _,
612617
install_updater: _,
613618
always_use_latest_updater: _,
@@ -710,6 +715,7 @@ impl DistMetadata {
710715
github_custom_runners,
711716
github_custom_job_permissions,
712717
bin_aliases,
718+
completion_cmds,
713719
tag_namespace,
714720
install_updater,
715721
always_use_latest_updater,
@@ -894,6 +900,9 @@ impl DistMetadata {
894900
if bin_aliases.is_none() {
895901
bin_aliases.clone_from(&workspace_config.bin_aliases);
896902
}
903+
if completion_cmds.is_none() {
904+
completion_cmds.clone_from(&workspace_config.completion_cmds);
905+
}
897906
if install_updater.is_none() {
898907
*install_updater = workspace_config.install_updater;
899908
}

cargo-dist/src/config/v0_to_v1.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ impl DistMetadata {
7676
github_custom_runners,
7777
github_custom_job_permissions,
7878
bin_aliases,
79+
completion_cmds,
7980
tag_namespace,
8081
install_updater,
8182
always_use_latest_updater,
@@ -324,6 +325,7 @@ impl DistMetadata {
324325
install_success_msg,
325326
install_libraries,
326327
bin_aliases,
328+
completion_cmds,
327329
},
328330
homebrew: homebrew_installer_layer,
329331
msi: msi_installer_layer,

cargo-dist/src/config/v1/installers/mod.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,10 @@ pub struct CommonInstallerLayer {
263263
/// Aliases to install binaries as
264264
#[serde(skip_serializing_if = "Option::is_none")]
265265
pub bin_aliases: Option<SortedMap<String, Vec<String>>>,
266+
267+
/// The command to execute to generate shell completion scripts for a given binary
268+
#[serde(skip_serializing_if = "Option::is_none")]
269+
pub completion_cmds: Option<SortedMap<String, CompletionConfig>>,
266270
}
267271
/// inheritable installer fields (final)
268272
#[derive(Debug, Default, Clone)]
@@ -291,6 +295,9 @@ pub struct CommonInstallerConfig {
291295
/// Aliases to install binaries as
292296
pub bin_aliases: SortedMap<String, Vec<String>>,
293297

298+
/// The command to execute to generate shell completion scripts for a given binary
299+
pub completion_cmds: SortedMap<String, CompletionConfig>,
300+
294301
/// Whether to install an updater program alongside the software
295302
pub install_updater: bool,
296303
}
@@ -302,6 +309,7 @@ impl CommonInstallerConfig {
302309
install_success_msg: "everything's installed!".to_owned(),
303310
install_libraries: Default::default(),
304311
bin_aliases: Default::default(),
312+
completion_cmds: Default::default(),
305313
install_updater: false,
306314
}
307315
}
@@ -315,12 +323,14 @@ impl ApplyLayer for CommonInstallerConfig {
315323
install_success_msg,
316324
install_libraries,
317325
bin_aliases,
326+
completion_cmds,
318327
}: Self::Layer,
319328
) {
320329
self.install_path.apply_val(install_path);
321330
self.install_success_msg.apply_val(install_success_msg);
322331
self.install_libraries.apply_val(install_libraries);
323332
self.bin_aliases.apply_val(bin_aliases);
333+
self.completion_cmds.apply_val(completion_cmds);
324334
}
325335
}
326336
impl ApplyLayer for CommonInstallerLayer {
@@ -332,11 +342,13 @@ impl ApplyLayer for CommonInstallerLayer {
332342
install_success_msg,
333343
install_libraries,
334344
bin_aliases,
345+
completion_cmds,
335346
}: Self::Layer,
336347
) {
337348
self.install_path.apply_opt(install_path);
338349
self.install_success_msg.apply_opt(install_success_msg);
339350
self.install_libraries.apply_opt(install_libraries);
340351
self.bin_aliases.apply_opt(bin_aliases);
352+
self.completion_cmds.apply_opt(completion_cmds);
341353
}
342354
}

cargo-dist/src/init/v0.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ fn get_new_dist_metadata(
240240
github_custom_runners: None,
241241
github_custom_job_permissions: None,
242242
bin_aliases: None,
243+
completion_cmds: None,
243244
tag_namespace: None,
244245
install_updater: None,
245246
always_use_latest_updater: None,
@@ -753,6 +754,7 @@ fn apply_dist_to_metadata(metadata: &mut toml_edit::Item, meta: &DistMetadata) {
753754
github_custom_runners: _,
754755
github_custom_job_permissions: _,
755756
bin_aliases: _,
757+
completion_cmds: _,
756758
system_dependencies: _,
757759
github_build_setup: _,
758760
} = &meta;

cargo-dist/src/tasks/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2034,6 +2034,7 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
20342034
desc,
20352035
receipt: InstallReceipt::from_metadata(&self.inner, release)?,
20362036
bin_aliases,
2037+
completion_cmds: config.completion_cmds.clone(),
20372038
install_libraries: config.install_libraries.clone(),
20382039
runtime_conditions,
20392040
platform_support: None,
@@ -2172,6 +2173,7 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
21722173
desc,
21732174
receipt: None,
21742175
bin_aliases,
2176+
completion_cmds: config.completion_cmds.clone(),
21752177
install_libraries: config.install_libraries.clone(),
21762178
runtime_conditions,
21772179
platform_support: None,
@@ -2277,6 +2279,7 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
22772279
desc,
22782280
receipt: InstallReceipt::from_metadata(&self.inner, release)?,
22792281
bin_aliases,
2282+
completion_cmds: config.completion_cmds.clone(),
22802283
install_libraries: config.install_libraries.clone(),
22812284
runtime_conditions: RuntimeConditions::default(),
22822285
platform_support: None,
@@ -2394,6 +2397,7 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
23942397
desc,
23952398
receipt: None,
23962399
bin_aliases,
2400+
completion_cmds: config.completion_cmds.clone(),
23972401
install_libraries: config.install_libraries.clone(),
23982402
runtime_conditions,
23992403
platform_support: None,

cargo-dist/templates/installer/homebrew.rb.j2

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@ class {{ formula_class }} < Formula
9292

9393
install_binary_aliases!
9494

95+
{%- for binary, completion_cmd in completions|items %}
96+
generate_completions_from_executable(bin/"{{ binary }}",
97+
{{ completion_cmd }})
98+
{%- endfor %}
99+
95100
# Homebrew will automatically install these, so we don't need to do that
96101
doc_files = Dir["README.*", "readme.*", "LICENSE", "LICENSE.*", "CHANGELOG.*"]
97102
leftover_contents = Dir["*"] - doc_files

0 commit comments

Comments
 (0)