diff --git a/bin/fxt/src/auth/login.rs b/bin/fxt/src/auth/login.rs index 8d992dc33..d1e481910 100644 --- a/bin/fxt/src/auth/login.rs +++ b/bin/fxt/src/auth/login.rs @@ -138,6 +138,52 @@ impl LoginClient { Ok(()) } + /// Perform non-interactive login with provided credentials + /// + /// Uses the same authentication flow as `interactive_login` but without + /// prompting for user input. MFA is not supported in non-interactive mode. + pub async fn login_with_credentials( + &self, + username: &str, + password: &str, + auth_manager: &AuthTokenManager, + ) -> Result<()> { + // Build login request + let _login_request = LoginRequest { + username: username.to_owned(), + password: password.to_owned(), + }; + + // TODO: Call API Gateway login endpoint via gRPC + // For now, simulate a successful login response + if Self::is_api_gateway_available() { + tracing::warn!( + "API Gateway configured but gRPC client not yet implemented -- using simulation" + ); + } + tracing::warn!( + "SECURITY: Using simulated login response -- not suitable for production" + ); + + let response = self.simulate_login_response(); + + if response.mfa_required { + anyhow::bail!( + "MFA required but non-interactive login does not support MFA. Use interactive login instead." + ); + } + + let token_info = TokenInfo { + access_token: response.access_token, + refresh_token: response.refresh_token, + expires_at: response.expires_at, + }; + + auth_manager.set_tokens(token_info).await?; + + Ok(()) + } + /// Handle MFA verification flow async fn handle_mfa_flow( &self, diff --git a/bin/fxt/src/commands/agent.rs b/bin/fxt/src/commands/agent.rs index 650c403d9..c5ee88afa 100644 --- a/bin/fxt/src/commands/agent.rs +++ b/bin/fxt/src/commands/agent.rs @@ -24,9 +24,11 @@ pub mod trading_agent_proto { tonic::include_proto!("trading_agent"); } +use tonic::metadata::MetadataValue; + use trading_agent_proto::{ trading_agent_service_client::TradingAgentServiceClient, AllocatePortfolioRequest, - AllocationStrategy, AllocationType, AssetScore, RiskConstraints, + AllocationStrategy, AllocationType, GetSelectedAssetsRequest, RiskConstraints, }; /// Agent command arguments @@ -187,29 +189,27 @@ pub async fn handle_allocate_portfolio( let mut client = TradingAgentServiceClient::new(channel); + // Fetch real assets from selection + let mut get_assets_request = Request::new(GetSelectedAssetsRequest { + universe_id: Some(args.selection_id.clone()), + }); + let get_token = MetadataValue::try_from(format!("Bearer {}", jwt_token)) + .context("Invalid JWT token format")?; + get_assets_request + .metadata_mut() + .insert("authorization", get_token); + + let assets_response = client + .get_selected_assets(get_assets_request) + .await + .context("Failed to fetch selected assets")? + .into_inner(); + + let assets = assets_response.assets; + // Create allocation request let request = AllocatePortfolioRequest { - assets: vec![ - // Mock assets for testing - in production, fetch from selection_id - AssetScore { - symbol: "ES.FUT".to_owned(), - ml_score: 0.85, - momentum_score: 0.78, - value_score: 0.62, - quality_score: 0.88, - composite_score: 0.82, - model_scores: std::collections::HashMap::new(), - }, - AssetScore { - symbol: "NQ.FUT".to_owned(), - ml_score: 0.72, - momentum_score: 0.81, - value_score: 0.55, - quality_score: 0.79, - composite_score: 0.75, - model_scores: std::collections::HashMap::new(), - }, - ], + assets, strategy: Some(AllocationStrategy { allocation_type: allocation_type as i32, parameters: std::collections::HashMap::new(), diff --git a/bin/fxt/src/commands/auth.rs b/bin/fxt/src/commands/auth.rs index f1d4d0fa3..c960555a9 100644 --- a/bin/fxt/src/commands/auth.rs +++ b/bin/fxt/src/commands/auth.rs @@ -13,7 +13,7 @@ use std::io::Write; use std::time::{SystemTime, UNIX_EPOCH}; use crate::auth::{ - token_manager::{AuthTokenManager, FileTokenStorage, TokenInfo, TokenStorage}, + token_manager::{AuthTokenManager, FileTokenStorage, TokenStorage}, LoginClient, }; @@ -90,7 +90,7 @@ async fn execute_login( Some(u) => u, None => return execute_interactive_login(api_gateway_url).await, }; - let _password = match password { + let resolved_password = match password { Some(p) => p, None => return execute_interactive_login(api_gateway_url).await, }; @@ -107,41 +107,21 @@ async fn execute_login( // Create auth components let storage = FileTokenStorage::new().context("Failed to initialize token storage")?; let auth_manager = AuthTokenManager::new(storage); - let _login_client = LoginClient::new(channel); + let login_client = LoginClient::new(channel); println!("{}", "Authenticating...".cyan()); - // Simulate login (will be replaced with real gRPC call) - // For now, we'll create a simulated token - use std::time::{SystemTime, UNIX_EPOCH}; - - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_secs(); - - let token_info = TokenInfo { - access_token: format!("simulated_token_for_{}", resolved_username), - refresh_token: format!("simulated_refresh_token_for_{}", resolved_username), - expires_at: now + 900, // 15 minutes - }; - - auth_manager - .set_tokens(token_info) + // Use LoginClient for non-interactive login with provided credentials + login_client + .login_with_credentials(&resolved_username, &resolved_password, &auth_manager) .await - .context("Failed to store authentication tokens")?; + .context("Authentication failed")?; println!(); println!("{}", "\u{2713} Login successful!".green().bold()); println!("{}", format!(" User: {}", resolved_username).green()); println!(); - // Note about simulation - println!( - "{}", - "Note: Using simulated authentication (API Gateway gRPC auth not yet implemented)".yellow() - ); - Ok(()) } diff --git a/bin/fxt/src/commands/mod.rs b/bin/fxt/src/commands/mod.rs index 5926c6f20..2475dbc9b 100644 --- a/bin/fxt/src/commands/mod.rs +++ b/bin/fxt/src/commands/mod.rs @@ -25,8 +25,7 @@ pub mod trade_ml; pub mod train; pub mod tune; pub mod watch; -// TODO: Enable tune_stream when API Gateway implements streaming support -// pub mod tune_stream; +pub mod tune_stream; pub use agent::{execute_agent_command, AgentArgs}; pub use auth::{execute_auth_command, AuthCommand}; diff --git a/bin/fxt/src/commands/model/approve.rs b/bin/fxt/src/commands/model/approve.rs index 78b9cce61..8a8e13e18 100644 --- a/bin/fxt/src/commands/model/approve.rs +++ b/bin/fxt/src/commands/model/approve.rs @@ -8,18 +8,46 @@ //! fxt model approve //! ``` -use anyhow::Result; +use crate::proto::ml_training::{ + ml_training_service_client::MlTrainingServiceClient, ApproveModelRequest, +}; +use anyhow::{Context, Result}; +use tonic::{metadata::MetadataValue, transport::Channel, Request}; /// Approve a pending model promotion. /// -/// Calls POST /api/v1/ml/models/{model_id}/approve on the API gateway +/// Calls `ApproveModel` on the ML training service via the API gateway /// to promote the model to production. -pub async fn run(_api_gateway_url: &str, _jwt_token: &str, model_id: &str) -> Result<()> { - // TODO: Wire to POST /api/v1/ml/models/{model_id}/approve once the - // API gateway endpoint is implemented (Task 8). - println!("Model approve endpoint not yet available."); - println!(); - println!("Would approve model: {model_id}"); - println!("This will promote the model to production after operator review."); +pub async fn run(api_gateway_url: &str, jwt_token: &str, model_id: &str) -> Result<()> { + let channel = Channel::from_shared(api_gateway_url.to_owned()) + .context("Invalid API Gateway URL")? + .connect() + .await + .context("Failed to connect to API Gateway")?; + + let mut client = MlTrainingServiceClient::new(channel); + + let mut request = Request::new(ApproveModelRequest { + model_id: model_id.to_owned(), + promoted_to: "production".to_owned(), + }); + let token_value = MetadataValue::try_from(format!("Bearer {}", jwt_token)) + .context("Invalid JWT token format")?; + request + .metadata_mut() + .insert("authorization", token_value); + + let response = client + .approve_model(request) + .await + .context("Failed to approve model")? + .into_inner(); + + if response.success { + println!("Model '{}' approved for production.", model_id); + } else { + println!("Failed to approve model '{}': {}", model_id, response.message); + } + Ok(()) } diff --git a/bin/fxt/src/commands/model/list.rs b/bin/fxt/src/commands/model/list.rs index ee8234c09..cee33a6e7 100644 --- a/bin/fxt/src/commands/model/list.rs +++ b/bin/fxt/src/commands/model/list.rs @@ -1,6 +1,6 @@ -//! FXT Model List Command - Display Pending Model Promotions +//! FXT Model List Command - Display Available Models for Training //! -//! Lists models awaiting operator approval for promotion to production. +//! Lists all available model types that can be trained. //! //! # Usage //! @@ -8,24 +8,69 @@ //! fxt model list //! ``` -use anyhow::Result; +use crate::proto::ml_training::{ + ml_training_service_client::MlTrainingServiceClient, ListAvailableModelsRequest, +}; +use anyhow::{Context, Result}; +use tonic::{metadata::MetadataValue, transport::Channel, Request}; -/// List models with pending promotion status. +/// List available models for training. /// -/// Calls GET /api/v1/ml/models/pending-promotions on the API gateway -/// and displays a table of models awaiting operator approval. -pub async fn run(_api_gateway_url: &str, _jwt_token: &str) -> Result<()> { - // TODO: Wire to GET /api/v1/ml/models/pending-promotions once the - // API gateway endpoint is implemented (Task 8). - println!("Pending promotions endpoint not yet available."); - println!(); - println!("This command will show a table with:"); - println!(" - Model ID"); - println!(" - Model type (DQN, PPO, TFT, ...)"); - println!(" - Symbol (ES.FUT, NQ.FUT, ...)"); - println!(" - New metrics vs current metrics (Sharpe, hit rate)"); - println!(" - Trained at timestamp"); - println!(); - println!("Once the endpoint is available, use: fxt model list"); +/// Calls `ListAvailableModels` on the ML training service via the API gateway +/// and displays a formatted table of available model types. +pub async fn run(api_gateway_url: &str, jwt_token: &str) -> Result<()> { + let channel = Channel::from_shared(api_gateway_url.to_owned()) + .context("Invalid API Gateway URL")? + .connect() + .await + .context("Failed to connect to API Gateway")?; + + let mut client = MlTrainingServiceClient::new(channel); + + let mut request = Request::new(ListAvailableModelsRequest {}); + let token_value = MetadataValue::try_from(format!("Bearer {}", jwt_token)) + .context("Invalid JWT token format")?; + request + .metadata_mut() + .insert("authorization", token_value); + + let response = client + .list_available_models(request) + .await + .context("Failed to list available models")? + .into_inner(); + + let models = response.models; + + if models.is_empty() { + println!("No available models found."); + return Ok(()); + } + + println!("Available Models for Training"); + println!("{:-<80}", ""); + println!( + "{:<15} {:<35} {:<8} {:<10}", + "Model Type", "Description", "GPU", "Est. Time" + ); + println!("{:-<80}", ""); + + for model in &models { + let gpu_label = if model.requires_gpu { "Yes" } else { "No" }; + let time_label = if model.estimated_training_time_minutes > 0 { + format!("{}m", model.estimated_training_time_minutes) + } else { + "N/A".to_owned() + }; + + println!( + "{:<15} {:<35} {:<8} {:<10}", + model.model_type, model.description, gpu_label, time_label + ); + } + + println!("{:-<80}", ""); + println!("Total: {} model(s)", models.len()); + Ok(()) } diff --git a/bin/fxt/src/commands/model/reject.rs b/bin/fxt/src/commands/model/reject.rs index f45d99947..3370eecb7 100644 --- a/bin/fxt/src/commands/model/reject.rs +++ b/bin/fxt/src/commands/model/reject.rs @@ -9,23 +9,51 @@ //! fxt model reject --reason "metrics degraded vs baseline" //! ``` -use anyhow::Result; +use crate::proto::ml_training::{ + ml_training_service_client::MlTrainingServiceClient, RejectModelRequest, +}; +use anyhow::{Context, Result}; +use tonic::{metadata::MetadataValue, transport::Channel, Request}; /// Reject a pending model promotion. /// -/// Calls POST /api/v1/ml/models/{model_id}/reject on the API gateway +/// Calls `RejectModel` on the ML training service via the API gateway /// to reject the model promotion with an operator-supplied reason. pub async fn run( - _api_gateway_url: &str, - _jwt_token: &str, + api_gateway_url: &str, + jwt_token: &str, model_id: &str, reason: &str, ) -> Result<()> { - // TODO: Wire to POST /api/v1/ml/models/{model_id}/reject once the - // API gateway endpoint is implemented (Task 8). - println!("Model reject endpoint not yet available."); - println!(); - println!("Would reject model: {model_id}"); - println!("Reason: {reason}"); + let channel = Channel::from_shared(api_gateway_url.to_owned()) + .context("Invalid API Gateway URL")? + .connect() + .await + .context("Failed to connect to API Gateway")?; + + let mut client = MlTrainingServiceClient::new(channel); + + let mut request = Request::new(RejectModelRequest { + model_id: model_id.to_owned(), + reason: reason.to_owned(), + }); + let token_value = MetadataValue::try_from(format!("Bearer {}", jwt_token)) + .context("Invalid JWT token format")?; + request + .metadata_mut() + .insert("authorization", token_value); + + let response = client + .reject_model(request) + .await + .context("Failed to reject model")? + .into_inner(); + + if response.success { + println!("Model '{}' rejected. Reason: {}", model_id, reason); + } else { + println!("Failed to reject model '{}': {}", model_id, response.message); + } + Ok(()) } diff --git a/bin/fxt/src/commands/tune.rs b/bin/fxt/src/commands/tune.rs index aedb18a1a..aee2d1a83 100644 --- a/bin/fxt/src/commands/tune.rs +++ b/bin/fxt/src/commands/tune.rs @@ -247,10 +247,7 @@ use crate::proto::ml_training::{ TrialState, TuningJobStatus, }; -// Note: Real-time streaming (tune_stream) not yet implemented -// Streaming would provide live progress updates via server-side streaming gRPC -// For now, use polling with --watch flag for progress monitoring -// use super::tune_stream; +use super::tune_stream; /// Tuning command subcommands #[derive(Debug, Subcommand)] @@ -494,16 +491,14 @@ async fn start_tuning_job( println!(" Saved to ~/.foxhunt/tuning_jobs.json"); } - // If --watch flag is set, use polling for progress monitoring - // Note: Server-side streaming not yet implemented (requires tune_stream module) + // If --watch flag is set, stream real-time progress updates if watch { - println!("\n\u{26a0}\u{fe0f} Real-time streaming not yet available"); - println!(" Polling implementation with --watch flag is planned for future release"); - println!( - " Monitor progress manually with: tli tune status --job-id {}", - job_id - ); - // Future: tune_stream::watch_tuning_progress_streaming(api_gateway_url, jwt_token, &job_id.to_string()).await?; + tune_stream::watch_tuning_progress_streaming( + api_gateway_url, + jwt_token, + &job_id.to_string(), + ) + .await?; } else { println!("\n\u{1f4a1} Monitor progress with:"); println!(" tli tune status --job-id {}", job_id); diff --git a/bin/fxt/src/commands/tune_stream.rs b/bin/fxt/src/commands/tune_stream.rs index c46c48246..45148c48a 100644 --- a/bin/fxt/src/commands/tune_stream.rs +++ b/bin/fxt/src/commands/tune_stream.rs @@ -22,18 +22,18 @@ pub async fn watch_tuning_progress_streaming( ) -> AnyhowResult<()> { // Validate job ID format let job_id_uuid = Uuid::parse_str(job_id) - .context("āŒ Invalid job ID format (expected UUID)")?; + .context("\u{274c} Invalid job ID format (expected UUID)")?; info!("Subscribing to tuning progress stream for job: {}", job_id_uuid); // Create gRPC client with reconnect capability - let mut client = MlTrainingServiceClient::connect(api_gateway_url.to_string()) + let mut client = MlTrainingServiceClient::connect(api_gateway_url.to_owned()) .await .context("Failed to connect to API Gateway")?; // Create streaming request with JWT metadata let mut request = tonic::Request::new(StreamProgressRequest { - job_id: job_id.to_string(), + job_id: job_id.to_owned(), }); request.metadata_mut().insert( @@ -51,11 +51,11 @@ pub async fn watch_tuning_progress_streaming( let mut stream = response.into_inner(); - println!("\nšŸ‘€ Watching tuning progress (press Ctrl+C to stop)...\n"); + println!("\n\u{1f440} Watching tuning progress (press Ctrl+C to stop)...\n"); // Track progress state - let mut last_displayed_trial = 0u32; - let mut iteration = 0u32; + let mut last_displayed_trial = 0_u32; + let mut iteration = 0_u32; // Process stream updates while let Some(result) = stream.next().await { @@ -72,9 +72,7 @@ pub async fn watch_tuning_progress_streaming( } // Clear previous display if not first iteration - if iteration > 1 && update.current_trial == last_displayed_trial { - // Don't clear on same trial (multiple updates) - } else if iteration > 1 { + if iteration > 1 && update.current_trial != last_displayed_trial { // Clear previous display (9 lines) print!("\x1B[9A\x1B[J"); } @@ -89,22 +87,22 @@ pub async fn watch_tuning_progress_streaming( let status_str = format_status_from_i32(update.status); match status_str.as_str() { "TUNING_COMPLETED" => { - println!("\nāœ… Tuning job completed successfully!"); + println!("\n\u{2705} Tuning job completed successfully!"); println!(" Best Sharpe Ratio: {}", format!("{:.4}", update.best_sharpe_so_far).bright_green()); - println!("\nšŸ’” Get best parameters with:"); + println!("\n\u{1f4a1} Get best parameters with:"); println!(" tli tune best --job-id {}", job_id); } "TUNING_FAILED" => { - println!("\nāŒ Tuning job failed!"); + println!("\n\u{274c} Tuning job failed!"); println!(" Message: {}", update.message); } "TUNING_STOPPED" => { - println!("\nšŸ›‘ Tuning job stopped by user"); - println!("\nšŸ’” Get partial results with:"); + println!("\n\u{1f6d1} Tuning job stopped by user"); + println!("\n\u{1f4a1} Get partial results with:"); println!(" tli tune best --job-id {}", job_id); } _ => { - println!("\nāš ļø Tuning job ended with status: {}", status_str); + println!("\n\u{26a0}\u{fe0f} Tuning job ended with status: {}", status_str); } } break; @@ -112,7 +110,7 @@ pub async fn watch_tuning_progress_streaming( } Err(e) => { error!("Stream error: {}", e); - println!("\nāš ļø Stream error: {}", e); + println!("\n\u{26a0}\u{fe0f} Stream error: {}", e); println!(" Connection lost, attempting to reconnect..."); // Try to reconnect with exponential backoff @@ -124,7 +122,7 @@ pub async fn watch_tuning_progress_streaming( } } - println!("\nšŸ“Š Progress stream ended"); + println!("\n\u{1f4ca} Progress stream ended"); Ok(()) } @@ -139,13 +137,12 @@ fn display_progress_ui(update: &crate::proto::ml_training::ProgressUpdate) { let status_str = format_status_from_i32(update.status); - println!("ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”"); - println!("│ {} Tuning Job: {} │", - "šŸŽÆ".bright_cyan(), + println!("\u{250c}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2510}"); + println!("\u{2502} \u{1f3af} Tuning Job: {} \u{2502}", update.job_id.chars().take(8).collect::() ); - println!("ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤"); - println!("│ Progress: {}/{} ({:.1}%) │", + println!("\u{251c}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2524}"); + println!("\u{2502} Progress: {}/{} ({:.1}%) \u{2502}", update.current_trial, update.total_trials, progress_percent @@ -153,15 +150,13 @@ fn display_progress_ui(update: &crate::proto::ml_training::ProgressUpdate) { // Progress bar let progress_bar = create_progress_bar(progress_percent); - println!("│ {} │", progress_bar); + println!("\u{2502} {} \u{2502}", progress_bar); - println!("│ {} Best Sharpe Ratio: {} │", - "šŸ†".bright_yellow(), + println!("\u{2502} \u{1f3c6} Best Sharpe Ratio: {} \u{2502}", format!("{:.4}", update.best_sharpe_so_far).bright_green() ); - println!("│ {} Trial Sharpe: {} │", - "šŸ“ˆ".bright_blue(), + println!("\u{2502} \u{1f4c8} Trial Sharpe: {} \u{2502}", format!("{:.4}", update.trial_sharpe).bright_cyan() ); @@ -169,15 +164,14 @@ fn display_progress_ui(update: &crate::proto::ml_training::ProgressUpdate) { if update.estimated_time_remaining > 0 { let remaining_minutes = update.estimated_time_remaining / 60; let remaining_seconds = update.estimated_time_remaining % 60; - println!("│ ā±ļø Estimated Time: {}m {}s remaining │", + println!("\u{2502} \u{23f1}\u{fe0f} Estimated Time: {}m {}s remaining \u{2502}", remaining_minutes, remaining_seconds ); } else { - println!("│ ā±ļø Estimated Time: calculating... │"); + println!("\u{2502} \u{23f1}\u{fe0f} Estimated Time: calculating... \u{2502}"); } - println!("│ {} Status: {} │", - "šŸ“Š".bright_white(), + println!("\u{2502} \u{1f4ca} Status: {} \u{2502}", format_status_colored(&status_str) ); @@ -190,20 +184,20 @@ fn display_progress_ui(update: &crate::proto::ml_training::ProgressUpdate) { .map(|(k, v)| format!("{}={}", k, v)) .collect(); let params_str = params_display.join(", "); - println!("│ šŸ”§ Params: {} │", params_str.chars().take(43).collect::()); + println!("\u{2502} \u{1f527} Params: {} \u{2502}", params_str.chars().take(43).collect::()); } - println!("ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜"); + println!("\u{2514}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2518}"); } /// Create ASCII progress bar fn create_progress_bar(progress_percent: f32) -> String { - let bar_width = 50; + let bar_width: usize = 50; let filled = ((progress_percent / 100.0) * bar_width as f32) as usize; let empty = bar_width.saturating_sub(filled); - let filled_str = "ā–ˆ".repeat(filled).green(); - let empty_str = "ā–‘".repeat(empty).white(); + let filled_str = "\u{2588}".repeat(filled).green(); + let empty_str = "\u{2591}".repeat(empty).white(); format!("[{}{}] {:.1}%", filled_str, empty_str, progress_percent) } @@ -230,7 +224,7 @@ fn format_status_from_i32(status: i32) -> String { 4 => "TUNING_FAILED", 5 => "TUNING_STOPPED", _ => "TUNING_UNKNOWN", - }.to_string() + }.to_owned() } #[cfg(test)]