Skip to content

Commit c3fec0a

Browse files
authored
feat (cli): Loading animation for deployments and builds (#724)
## Description Added a spinner as sign of loading animation for deployments in CLI ## Related Issues #705 Closes #705 ## Checklist when merging to main <!-- Mark items with "x" when completed --> - [x] No compiler warnings (if applicable) - [x] Code is formatted with `rustfmt` - [x] No useless or dead code (if applicable) - [x] Code is easy to understand - [x] Doc comments are used for all functions, enums, structs, and fields (where appropriate) - [x] All tests pass - [x] Performance has not regressed (assuming change was not to fix a bug) - [x] Version number has been updated in `helix-cli/Cargo.toml` and `helixdb/Cargo.toml` ## Additional Notes Here is a quick video [Screencast from 2025-11-25 14-25-14.webm](https://github.com/user-attachments/assets/c021588a-d56b-4a5c-90ff-b212a41f5f2b) <!-- greptile_comment --> <h2>Greptile Overview</h2> <h3>Greptile Summary</h3> Implemented loading spinner animation for CLI deployment operations to address user feedback that the terminal appeared frozen during builds. **Key changes:** - Added `Spinner` struct in `utils.rs` with async animation using tokio - Integrated spinner around Docker build operations in `build.rs` - Added spinners for both build and deployment phases in `push.rs` with provider-specific messages - Removed redundant status print statements from `docker.rs` to avoid interfering with spinner **Implementation details:** - Spinner runs as a tokio task with braille pattern animation frames - Uses `Arc<Mutex<String>>` for thread-safe message updates - Properly cleans up via Drop trait to prevent resource leaks - Supports dynamic message updates during long operations <details><summary><h3>Important Files Changed</h3></summary> File Analysis | Filename | Score | Overview | |----------|-------|----------| | helix-cli/src/utils.rs | 4/5 | Added `Spinner` struct with async animation loop using tokio, includes proper cleanup via Drop trait | | helix-cli/src/commands/build.rs | 5/5 | Added spinner for Docker image build step, wrapping `build_image` call with start/stop | | helix-cli/src/commands/push.rs | 4/5 | Added spinners for build and deployment phases, with dynamic messages for different cloud providers | | helix-cli/src/docker.rs | 5/5 | Removed status print statements from `build_image`, delegating user feedback to spinner in calling code | </details> </details> <details><summary><h3>Sequence Diagram</h3></summary> ```mermaid sequenceDiagram participant User participant BuildCmd as build.rs participant PushCmd as push.rs participant Spinner participant Docker as docker.rs participant CloudProvider Note over User,CloudProvider: Build Command Flow User->>BuildCmd: helix build BuildCmd->>Spinner: new("DOCKER", "Building...") BuildCmd->>Spinner: start() activate Spinner Note right of Spinner: Tokio task spawned<br/>Animation loop running BuildCmd->>Docker: build_image() Docker-->>BuildCmd: Result BuildCmd->>Spinner: stop() deactivate Spinner Note right of Spinner: Task aborted<br/>Line cleared BuildCmd-->>User: Success message Note over User,CloudProvider: Push Command Flow (Cloud) User->>PushCmd: helix push PushCmd->>Spinner: new("BUILD", "Building instance...") PushCmd->>Spinner: start() activate Spinner PushCmd->>BuildCmd: run() BuildCmd->>Docker: build_image() Docker-->>BuildCmd: Result BuildCmd-->>PushCmd: MetricsData PushCmd->>Spinner: stop() deactivate Spinner PushCmd->>Spinner: new("DEPLOY", "Deploying instance...") PushCmd->>Spinner: start() activate Spinner PushCmd->>Spinner: update("Deploying to Fly.io/ECR/Helix...") Note right of Spinner: Message updates<br/>while animating PushCmd->>CloudProvider: deploy() CloudProvider-->>PushCmd: Result PushCmd->>Spinner: stop() deactivate Spinner PushCmd-->>User: Success message ``` </details> <!-- greptile_other_comments_section --> <!-- /greptile_comment -->
2 parents d5e18f2 + 45187d1 commit c3fec0a

File tree

4 files changed

+92
-5
lines changed

4 files changed

+92
-5
lines changed

helix-cli/src/commands/build.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use crate::metrics_sender::MetricsSender;
44
use crate::project::{ProjectContext, get_helix_repo_cache};
55
use crate::utils::{
66
copy_dir_recursive_excluding, diagnostic_source, helixc_utils::collect_hx_files, print_status,
7-
print_success,
7+
print_success, Spinner,
88
};
99
use eyre::Result;
1010
use std::time::Instant;
@@ -90,7 +90,10 @@ pub async fn run(instance_name: String, metrics_sender: &MetricsSender) -> Resul
9090
DockerManager::check_runtime_available(runtime)?;
9191
let docker = DockerManager::new(&project);
9292

93+
let mut spinner = Spinner::new("DOCKER", "Building Docker image...");
94+
spinner.start();
9395
docker.build_image(&instance_name, instance_config.docker_build_target())?;
96+
spinner.stop();
9497
}
9598

9699
print_success(&format!("Instance '{instance_name}' built successfully"));

helix-cli/src/commands/push.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crate::config::{CloudConfig, InstanceInfo};
66
use crate::docker::DockerManager;
77
use crate::metrics_sender::MetricsSender;
88
use crate::project::ProjectContext;
9-
use crate::utils::{print_status, print_success};
9+
use crate::utils::{print_status, print_success, Spinner};
1010
use eyre::Result;
1111
use std::time::Instant;
1212

@@ -136,7 +136,11 @@ async fn push_cloud_instance(
136136

137137
let metrics_data = if instance_config.should_build_docker_image() {
138138
// Build happens, get metrics data from build
139-
crate::commands::build::run(instance_name.to_string(), metrics_sender).await?
139+
let mut spinner = Spinner::new("BUILD", "Building instance...");
140+
spinner.start();
141+
let result = crate::commands::build::run(instance_name.to_string(), metrics_sender).await;
142+
spinner.stop();
143+
result?
140144
} else {
141145
// No build, use lightweight parsing
142146
parse_queries_for_metrics(project)?
@@ -149,8 +153,11 @@ async fn push_cloud_instance(
149153
// 3. Triggering deployment on the cloud
150154

151155
let config = project.config.cloud.get(instance_name).unwrap();
156+
let mut deploy_spinner = Spinner::new("DEPLOY", "Deploying instance...");
157+
deploy_spinner.start();
152158
match config {
153159
CloudConfig::FlyIo(config) => {
160+
deploy_spinner.update("Deploying to Fly.io...");
154161
let fly = FlyManager::new(project, config.auth_type.clone()).await?;
155162
let docker = DockerManager::new(project);
156163
// Get the correct image name from docker compose project name
@@ -160,6 +167,7 @@ async fn push_cloud_instance(
160167
.await?;
161168
}
162169
CloudConfig::Ecr(config) => {
170+
deploy_spinner.update("Deploying to ECR...");
163171
let ecr = EcrManager::new(project, config.auth_type.clone()).await?;
164172
let docker = DockerManager::new(project);
165173
// Get the correct image name from docker compose project name
@@ -169,10 +177,12 @@ async fn push_cloud_instance(
169177
.await?;
170178
}
171179
CloudConfig::Helix(_config) => {
180+
deploy_spinner.update("Deploying to Helix...");
172181
let helix = HelixManager::new(project);
173182
helix.deploy(None, instance_name.to_string()).await?;
174183
}
175184
}
185+
deploy_spinner.stop();
176186

177187
print_status("UPLOAD", &format!("Uploading to cluster: {cluster_id}"));
178188

helix-cli/src/docker.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -547,15 +547,14 @@ networks:
547547
self.runtime.label(),
548548
&format!("Building image for instance '{instance_name}'..."),
549549
);
550-
551550
let output = self.run_compose_command(instance_name, vec!["build"])?;
552551

553552
if !output.status.success() {
554553
let stderr = String::from_utf8_lossy(&output.stderr);
555554
return Err(eyre!("{} build failed:\n{stderr}", self.runtime.binary()));
556555
}
557-
558556
print_status(self.runtime.label(), "Image built successfully");
557+
559558
Ok(())
560559
}
561560

helix-cli/src/utils.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ use color_eyre::owo_colors::OwoColorize;
33
use eyre::{Result, eyre};
44
use helix_db::helixc::parser::types::HxFile;
55
use std::{borrow::Cow, fs, path::Path};
6+
use tokio::sync::oneshot;
7+
use tokio::time::Duration;
8+
use std::io::IsTerminal;
9+
610

711
const IGNORES: [&str; 3] = ["target", ".git", ".helix"];
812

@@ -407,3 +411,74 @@ pub mod helixc_utils {
407411
false
408412
}
409413
}
414+
415+
pub struct Spinner {
416+
message: std::sync::Arc<std::sync::Mutex<String>>,
417+
prefix: String,
418+
stop_tx: Option<oneshot::Sender<()>>,
419+
handle: Option<tokio::task::JoinHandle<()>>,
420+
}
421+
422+
impl Spinner {
423+
pub fn new(prefix: &str, message: &str) -> Self {
424+
Self {
425+
message: std::sync::Arc::new(std::sync::Mutex::new(message.to_string())),
426+
prefix: prefix.to_string(),
427+
stop_tx: None,
428+
handle: None,
429+
}
430+
}
431+
// function that starts the spinner
432+
pub fn start(&mut self) {
433+
if !std::io::stdout().is_terminal() {
434+
return; // skip animation for non-interactive terminals
435+
}
436+
let message = self.message.clone();
437+
let prefix = self.prefix.clone();
438+
let (tx, mut rx) = oneshot::channel::<()>();
439+
440+
let handle = tokio::spawn(async move {
441+
let frames = vec!["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
442+
let mut frame_idx = 0;
443+
loop {
444+
if rx.try_recv().is_ok() {
445+
break;
446+
}
447+
let frame = frames[frame_idx % frames.len()];
448+
let msg = message.lock().unwrap().clone();
449+
print!(
450+
"\r{} {frame} {msg}",
451+
format!("[{prefix}]").blue().bold()
452+
);
453+
std::io::Write::flush(&mut std::io::stdout()).unwrap();
454+
frame_idx += 1;
455+
tokio::time::sleep(Duration::from_millis(100)).await;
456+
}
457+
});
458+
self.handle = Some(handle);
459+
self.stop_tx = Some(tx);
460+
}
461+
// function that Stops the spinner
462+
pub fn stop(&mut self) {
463+
if let Some(tx) = self.stop_tx.take() {
464+
let _ = tx.send(());
465+
}
466+
if let Some(handle) = self.handle.take() {
467+
handle.abort();
468+
}
469+
print!("\r");
470+
std::io::Write::flush(&mut std::io::stdout()).unwrap();
471+
}
472+
/// function that updates the message
473+
pub fn update(&mut self, message: &str) {
474+
if let Ok(mut msg) = self.message.lock() {
475+
*msg = message.to_string();
476+
}
477+
}
478+
}
479+
480+
impl Drop for Spinner {
481+
fn drop(&mut self) {
482+
self.stop();
483+
}
484+
}

0 commit comments

Comments
 (0)