Skip to content

Commit 4da149d

Browse files
Add docker run image:tag completion support (#229)
Co-authored-by: Oz <oz-agent@warp.dev>
1 parent 31f3da5 commit 4da149d

2 files changed

Lines changed: 116 additions & 81 deletions

File tree

command-signatures/json/docker.json

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -267,8 +267,9 @@
267267
"description": "Create a new container",
268268
"args": [
269269
{
270-
"name": "container",
271-
"generatorName": "all_local_images",
270+
"name": "image",
271+
"description": "The Docker image to use",
272+
"generatorName": "image_with_tags",
272273
"skipGeneratorValidation": false
273274
},
274275
{
@@ -1099,7 +1100,8 @@
10991100
"description": "Show the history of an image",
11001101
"args": {
11011102
"name": "image",
1102-
"generatorName": "all_local_images",
1103+
"description": "The Docker image to use",
1104+
"generatorName": "image_with_tags",
11031105
"skipGeneratorValidation": false
11041106
},
11051107
"options": [
@@ -1632,7 +1634,8 @@
16321634
"description": "Remove one or more images",
16331635
"args": {
16341636
"name": "image",
1635-
"generatorName": "all_local_images",
1637+
"description": "The Docker image to use",
1638+
"generatorName": "image_with_tags",
16361639
"skipGeneratorValidation": false,
16371640
"isVariadic": true
16381641
},
@@ -2351,7 +2354,7 @@
23512354
{
23522355
"name": "image",
23532356
"description": "The Docker image to use",
2354-
"generatorName": "docker_image_with_tag_and_size",
2357+
"generatorName": "image_with_tags",
23552358
"skipGeneratorValidation": false
23562359
},
23572360
{
@@ -2364,7 +2367,8 @@
23642367
"description": "Save one or more images to a tar archive (streamed to STDOUT by default)",
23652368
"args": {
23662369
"name": "image",
2367-
"generatorName": "all_local_images",
2370+
"description": "The Docker image to use",
2371+
"generatorName": "image_with_tags",
23682372
"skipGeneratorValidation": false
23692373
},
23702374
"options": [
@@ -2423,7 +2427,8 @@
24232427
"description": "View the packaged-based Software Bill Of Materials (SBOM) for an image",
24242428
"args": {
24252429
"name": "image",
2426-
"generatorName": "all_local_images",
2430+
"description": "The Docker image to use",
2431+
"generatorName": "image_with_tags",
24272432
"skipGeneratorValidation": false
24282433
},
24292434
"options": [
@@ -3111,8 +3116,9 @@
31113116
"description": "Create a new container",
31123117
"args": [
31133118
{
3114-
"name": "container",
3115-
"generatorName": "all_local_images",
3119+
"name": "image",
3120+
"description": "The Docker image to use",
3121+
"generatorName": "image_with_tags",
31163122
"skipGeneratorValidation": false
31173123
},
31183124
{
@@ -4881,7 +4887,7 @@
48814887
{
48824888
"name": "image",
48834889
"description": "The Docker image to use",
4884-
"generatorName": "docker_image_with_tag_and_size"
4890+
"generatorName": "image_with_tags"
48854891
},
48864892
{
48874893
"name": "command"
@@ -5663,7 +5669,8 @@
56635669
"description": "Show the history of an image",
56645670
"args": {
56655671
"name": "image",
5666-
"generatorName": "all_local_images",
5672+
"description": "The Docker image to use",
5673+
"generatorName": "image_with_tags",
56675674
"skipGeneratorValidation": false
56685675
},
56695676
"options": [
@@ -5735,7 +5742,8 @@
57355742
"description": "Display detailed information on one or more images",
57365743
"args": {
57375744
"name": "image",
5738-
"generatorName": "all_local_images",
5745+
"description": "The Docker image to use",
5746+
"generatorName": "image_with_tags",
57395747
"skipGeneratorValidation": false,
57405748
"isVariadic": true
57415749
},
@@ -5916,7 +5924,8 @@
59165924
"description": "Remove one or more images",
59175925
"args": {
59185926
"name": "image",
5919-
"generatorName": "all_local_images",
5927+
"description": "The Docker image to use",
5928+
"generatorName": "image_with_tags",
59205929
"skipGeneratorValidation": false,
59215930
"isVariadic": true
59225931
},
@@ -5939,7 +5948,8 @@
59395948
"description": "Save one or more images to a tar archive (streamed to STDOUT by default)",
59405949
"args": {
59415950
"name": "image",
5942-
"generatorName": "all_local_images",
5951+
"description": "The Docker image to use",
5952+
"generatorName": "image_with_tags",
59435953
"skipGeneratorValidation": false
59445954
},
59455955
"options": [
@@ -6779,7 +6789,8 @@
67796789
"args": [
67806790
{
67816791
"name": "image",
6782-
"generatorName": "all_local_images",
6792+
"description": "The Docker image to use",
6793+
"generatorName": "image_with_tags",
67836794
"skipGeneratorValidation": false
67846795
},
67856796
{
@@ -8759,7 +8770,8 @@
87598770
"description": "Remove trust for an image",
87608771
"args": {
87618772
"name": "image",
8762-
"generatorName": "all_local_images",
8773+
"description": "The Docker image to use",
8774+
"generatorName": "image_with_tags",
87638775
"skipGeneratorValidation": false
87648776
},
87658777
"options": [
@@ -8777,7 +8789,8 @@
87778789
"description": "Sign an image",
87788790
"args": {
87798791
"name": "image",
8780-
"generatorName": "all_local_images",
8792+
"description": "The Docker image to use",
8793+
"generatorName": "image_with_tags",
87818794
"skipGeneratorValidation": false
87828795
},
87838796
"options": [

command-signatures/src/generators/docker.rs

Lines changed: 86 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,73 @@ use warp_completion_metadata::{
55
GeneratorResultsCollector, IconType, Suggestion, TemplateFilter,
66
};
77

8+
/// Post-processes the output for the `image_with_tags` generator.
9+
/// Handles two output modes:
10+
/// - JSON mode (normal): lines are JSON objects from `docker images --format '{{ json . }}'`
11+
/// - Tag mode: lines are space-separated `image:tag ID Size` from a filtered `docker image ls`
12+
fn post_process_image_with_tags(output: &str) -> GeneratorResults {
13+
if output.trim().is_empty() {
14+
return GeneratorResults::default();
15+
}
16+
17+
let first_char = output.trim_start().chars().next().unwrap_or(' ');
18+
19+
if first_char == '{' {
20+
// JSON mode (normal): suggest repository names with metadata
21+
output
22+
.lines()
23+
.filter_map(|line| {
24+
let docker_image_output: Result<DockerImageOutput> = serde_json::from_str(line);
25+
if let Ok(docker_image_output) = docker_image_output {
26+
if let (Some(repo), Some(size), Some(tag), Some(id)) = (
27+
docker_image_output.repository,
28+
docker_image_output.size,
29+
docker_image_output.tag,
30+
docker_image_output.id,
31+
) {
32+
Some(
33+
Suggestion::with_description(
34+
repo,
35+
format!("{}@{} - {}", id, tag, size),
36+
)
37+
.with_icon(IconType::DockerImage),
38+
)
39+
} else {
40+
None
41+
}
42+
} else {
43+
log::info!(
44+
"Unable to deserialize docker image output with err {:?}",
45+
docker_image_output.err().unwrap()
46+
);
47+
None
48+
}
49+
})
50+
.collect_unordered_results()
51+
} else {
52+
// Tag mode: lines like "nginx:latest abc123 52MB"
53+
output
54+
.lines()
55+
.filter(|line| !line.is_empty() && !line.contains("<none>"))
56+
.filter_map(|line| {
57+
let parts: Vec<&str> = line.splitn(3, ' ').collect();
58+
let image_tag = parts.first()?;
59+
if parts.len() >= 3 {
60+
Some(
61+
Suggestion::with_description(
62+
*image_tag,
63+
format!("{} - {}", parts[1], parts[2]),
64+
)
65+
.with_icon(IconType::DockerImage),
66+
)
67+
} else {
68+
Some(Suggestion::new(*image_tag).with_icon(IconType::DockerImage))
69+
}
70+
})
71+
.collect_unordered_results()
72+
}
73+
}
74+
875
#[derive(Debug, serde::Deserialize)]
976
#[serde(rename_all = "PascalCase")]
1077
struct DockerContainerOutput {
@@ -384,72 +451,27 @@ pub fn generator() -> CommandSignatureGenerators {
384451
),
385452
)
386453
.add_generator(
387-
"run_images",
388-
Generator::script(
389-
CommandBuilder::single_command("docker images --format '{{ json . }}'"),
390-
|output| {
391-
if output.trim().is_empty() {
392-
return GeneratorResults::default();
393-
}
454+
"image_with_tags",
455+
Generator::command_from_tokens(
456+
|tokens, has_trailing_whitespace, _| {
457+
let last_token = if has_trailing_whitespace {
458+
""
459+
} else {
460+
tokens.last().copied().unwrap_or("")
461+
};
394462

395-
output
396-
.lines()
397-
.filter_map(|image| {
398-
let docker_image_output: Result<DockerImageOutput> =
399-
serde_json::from_str(image);
400-
if let Ok(docker_image_output) = docker_image_output {
401-
if let (Some(repo), Some(size), Some(tag), Some(id)) = (
402-
docker_image_output.repository,
403-
docker_image_output.size,
404-
docker_image_output.tag,
405-
docker_image_output.id,
406-
) {
407-
Some(
408-
Suggestion::with_description(
409-
repo,
410-
format!("{}@{} -{}", id, tag, size),
411-
)
412-
.with_icon(IconType::DockerImage),
413-
)
414-
} else {
415-
None
416-
}
417-
} else {
418-
log::info!(
419-
"Unable to deserialize docker image output with err {:?}",
420-
docker_image_output.err().unwrap()
421-
);
422-
None
423-
}
424-
})
425-
.collect_unordered_results()
426-
},
427-
),
428-
)
429-
.add_generator(
430-
"docker_image_with_tag_and_size",
431-
Generator::script(
432-
CommandBuilder::single_command(
433-
"docker images --format '{{.Repository}} {{.Size}} {{.Tag}} {{.ID}}'",
434-
),
435-
|output| {
436-
output
437-
.split('\n')
438-
.filter_map(|line| {
439-
let words: Vec<&str> = line.split(' ').collect();
440-
(words.len() >= 4).then(|| {
441-
let id = words[1];
442-
let tag = words[2];
443-
let size = words[3];
444-
Suggestion::with_description(
445-
words[0],
446-
format!("{}@{} - {}", id, tag, size),
447-
)
448-
.with_icon(IconType::DockerImage)
449-
})
450-
})
451-
.collect_unordered_results()
463+
if let Some(colon_pos) = last_token.rfind(':') {
464+
let image = &last_token[..colon_pos];
465+
if !image.is_empty() {
466+
return CommandBuilder::single_command(format!(
467+
"docker image ls '{}' --format '{{{{.Repository}}}}:{{{{.Tag}}}} {{{{.ID}}}} {{{{.Size}}}}'" ,
468+
image
469+
));
470+
}
471+
}
472+
CommandBuilder::single_command("docker images --format '{{ json . }}'")
452473
},
474+
post_process_image_with_tags,
453475
),
454476
)
455477
.add_filter(

0 commit comments

Comments
 (0)