From 5941a3c75ee970c21f41b61ca2b995a543e8fc5d Mon Sep 17 00:00:00 2001 From: Eike Date: Fri, 19 Jun 2026 16:18:16 +0200 Subject: [PATCH] Dynamically complete job names for `stop` and `logs` commands --- src/cli/cmd/job/logs.rs | 9 +++--- src/cli/cmd/job/stop.rs | 5 ++-- src/cli/complete.rs | 64 ++++++++++++++++++++++++++++++++++++++++- src/httpclient.rs | 5 ++++ src/httpclient/data.rs | 14 ++++++--- 5 files changed, 85 insertions(+), 12 deletions(-) diff --git a/src/cli/cmd/job/logs.rs b/src/cli/cmd/job/logs.rs index 5946f64..aaa5f37 100644 --- a/src/cli/cmd/job/logs.rs +++ b/src/cli/cmd/job/logs.rs @@ -1,20 +1,19 @@ use super::Context; +use crate::cli::complete::complete_job_name; use crate::{cli::sink::Error as SinkError, httpclient}; +use clap::{Parser, ValueHint}; use std::time::Duration; use tokio::signal; use tokio::time::sleep; -use clap::{Parser, ValueHint}; - +use clap_complete::ArgValueCompleter; use snafu::{ResultExt, Snafu}; -/// Listing logs of a jobs. -/// /// List the logs of a job. #[derive(Parser, Debug)] pub struct Input { /// The job name/id to get logs for. - #[arg(value_hint=ValueHint::Other)] + #[arg(value_hint=ValueHint::Other, add = ArgValueCompleter::new(complete_job_name))] pub job_id: String, /// Periodically retrieves logs, it will stop when the job finished. diff --git a/src/cli/cmd/job/stop.rs b/src/cli/cmd/job/stop.rs index de94a4d..89a5214 100644 --- a/src/cli/cmd/job/stop.rs +++ b/src/cli/cmd/job/stop.rs @@ -1,10 +1,11 @@ -use crate::{data::simple_message::SimpleMessage, httpclient}; +use crate::{cli::complete::complete_job_name, data::simple_message::SimpleMessage, httpclient}; use super::Context; use crate::cli::sink::Error as SinkError; use clap::{Parser, ValueHint}; +use clap_complete::ArgValueCompleter; use snafu::{ResultExt, Snafu}; /// Stop a job. @@ -13,7 +14,7 @@ use snafu::{ResultExt, Snafu}; #[derive(Parser, Debug)] pub struct Input { /// The launcher to use for launching the job. - #[arg(value_hint=ValueHint::Other)] + #[arg(value_hint=ValueHint::Other, add = ArgValueCompleter::new(complete_job_name))] pub job_id: String, } diff --git a/src/cli/complete.rs b/src/cli/complete.rs index 47e7062..d4cdd97 100644 --- a/src/cli/complete.rs +++ b/src/cli/complete.rs @@ -5,7 +5,7 @@ use crate::{ data::project_id::ProjectId, httpclient::{ Client, - data::{SessionLauncher, SessionMode}, + data::{SessionLauncher, SessionMode, SessionStartResponse}, }, }; @@ -78,6 +78,35 @@ async fn make_launcher_completion_candidate( cc.help(Some(help)) } +async fn make_job_name_completion_candidate( + client: &Client, + session: &SessionStartResponse, +) -> CompletionCandidate { + let mut help = StyledStr::new(); + let cc = CompletionCandidate::new(session.name.clone()); + + if let Ok(Some(launcher)) = client.get_launcher(&session.launcher_id).await { + help.push_str(&launcher.name); + help.push_str("/"); + help.push_str(session.status.state.to_str()); + } else { + help.push_str(&session.launcher_id); + help.push_str("/"); + help.push_str(session.status.state.to_str()); + eprintln!("Cannot get launcher for {}", &session.launcher_id); + } + + let Ok(Some(project)) = client.get_project_by_id(&session.project_id).await else { + eprintln!("Cannot get project details for: {}", session.project_id); + return cc.help(Some(help)); + }; + + help.push_str(" - "); + help.push_str(&project.name); + + cc.help(Some(help)) +} + async fn resolve_project_id(client: &Client, id: ProjectId) -> Option { match client.get_project(&id).await { Ok(Some(p)) => Some(p.id), @@ -128,3 +157,36 @@ pub fn complete_job_launcher_id(current: &ffi::OsStr) -> Vec Vec { + make_sync_completer(current, async |client, opts| { + let jobs = match client + .list_sessions(Some(SessionMode::NonInteractive)) + .await + { + Err(msg) => { + eprintln!("Completions failed: Error getting list of jobs: {}", msg); + return vec![]; + } + Ok(res) => res, + }; + let mut result: Vec = vec![]; + let project_ctx = opts.get_project_context().ok().flatten(); + let project_id = match project_ctx { + Some(id) => resolve_project_id(&client, id).await, + None => None, + }; + for job in jobs.0.iter().filter(|e| match &project_id { + Some(id) => id == &e.project_id, + None => true, + }) { + let cc = make_job_name_completion_candidate(&client, job).await; + result.push(cc); + } + if result.is_empty() { + eprintln!("No job launchers found."); + } + result + }) +} diff --git a/src/httpclient.rs b/src/httpclient.rs index 8a5394d..b3d1f32 100644 --- a/src/httpclient.rs +++ b/src/httpclient.rs @@ -460,6 +460,11 @@ impl Client { Ok(result) } + pub async fn get_launcher(&self, id: &str) -> Result, Error> { + let path = format!("/api/data/session_launchers/{}", id); + self.json_get_option::(&path).await + } + pub async fn clear_token(&self) -> Result<(), Error> { self.keystore.clear_async().await.context(KeystoreSnafu) } diff --git a/src/httpclient/data.rs b/src/httpclient/data.rs index fd16f47..5fe18e8 100644 --- a/src/httpclient/data.rs +++ b/src/httpclient/data.rs @@ -131,16 +131,22 @@ pub enum SessionState { Succeeded, } -impl fmt::Display for SessionState { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let name = match self { +impl SessionState { + pub fn to_str(&self) -> &'static str { + match self { SessionState::Running => "Running", SessionState::Starting => "Starting", SessionState::Stopping => "Stopping", SessionState::Failed => "Failed", SessionState::Hibernated => "Hibernated", SessionState::Succeeded => "Succeeded", - }; + } + } +} + +impl fmt::Display for SessionState { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let name = self.to_str(); f.write_str(name) } }