From 1161296d2742a42215d23964eb61d4137e3a397d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erkan=20I=C5=9EIK?= Date: Wed, 24 Jun 2026 08:09:46 +0300 Subject: [PATCH] feat: add sub-progress bar for rsync file copy progress - InstallMessage: added SubProgress(f32) and CurrentFile(String) variants - GlobalState: added install_sub_progress field - UiSender: added sub_progress() and current_file() helpers - CopyFilesJob: now streams rsync stderr, parses xfr#/to-chk for progress, and sends SubProgress/CurrentFile messages to UI - execution.rs: displays secondary progress bar below the main one during rsync file copy, updates install_current_file in real-time - main.rs: handles new InstallMessage variants in auto-install mode --- src/installer.rs | 2 ++ src/jobs/mod.rs | 76 ++++++++++++++++++++++++++++++++++++++++-- src/main.rs | 2 ++ src/steps/execution.rs | 17 ++++++++++ 4 files changed, 95 insertions(+), 2 deletions(-) diff --git a/src/installer.rs b/src/installer.rs index e8d1536..ef1e25b 100644 --- a/src/installer.rs +++ b/src/installer.rs @@ -207,6 +207,7 @@ pub struct GlobalState { pub current_step: usize, pub available_disks: Vec, pub install_progress: f32, + pub install_sub_progress: f32, pub install_log: Vec, pub install_current_file: Option, pub demo_mode: bool, @@ -274,6 +275,7 @@ impl Default for GlobalState { current_step: 0, available_disks: Vec::new(), install_progress: 0.0, + install_sub_progress: 0.0, install_log: Vec::new(), install_current_file: None, demo_mode: false, diff --git a/src/jobs/mod.rs b/src/jobs/mod.rs index a50deb3..7a9c5de 100644 --- a/src/jobs/mod.rs +++ b/src/jobs/mod.rs @@ -13,6 +13,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc; +use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::process::Command; use rust_i18n::t; @@ -27,6 +28,10 @@ pub enum InstallMessage { Log(String), /// Genel ilerleme; 0.0–1.0 arasında. Progress(f32), + /// Alt ilerleme (ör: rsync içindeki dosya kopyalama); 0.0–1.0 arasında. + SubProgress(f32), + /// Kopyalanmakta olan dosya adı. + CurrentFile(String), /// Tüm işler başarıyla tamamlandı. Done, /// Bir iş başarısız oldu; kurulum durdu. @@ -54,6 +59,14 @@ impl UiSender { pub fn log(&self, s: impl Into) { self.send(InstallMessage::Log(s.into())); } + + pub fn sub_progress(&self, p: f32) { + self.send(InstallMessage::SubProgress(p)); + } + + pub fn current_file(&self, f: impl Into) { + self.send(InstallMessage::CurrentFile(f.into())); + } } // ───────────────────────────────────────────── @@ -179,8 +192,67 @@ impl Job for CopyFilesJob { args.push(self.source.clone()); args.push(self.target.clone()); - let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect(); - run_cmd("rsync", &arg_refs).await?; + if DEMO_MODE.load(Ordering::Relaxed) { + tokio::time::sleep(std::time::Duration::from_secs(3)).await; + return Ok(()); + } + + let mut child = Command::new("rsync") + .args(&args) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn() + .map_err(|e| format!("rsync çalıştırılamadı: {}", e))?; + + let stderr = child.stderr.take() + .ok_or_else(|| "rsync stderr alınamadı".to_string())?; + let reader = BufReader::new(stderr); + let mut lines = reader.lines(); + + while let Some(line) = lines.next_line().await + .map_err(|e| format!("rsync çıktısı okunamadı: {}", e))? + { + let trimmed = line.trim(); + // rsync --info=progress2 format (newlines when piped): + // 1,024 100% 512.00kB/s 0:00:00 (xfr#5, to-chk=99/105) + // Extract xfr# and to-chk for sub-progress + if let Some(xfr_pos) = trimmed.find("xfr#") { + let after_xfr = &trimmed[xfr_pos + 4..]; + let xfr_end = after_xfr.find(|c: char| !c.is_ascii_digit()).unwrap_or(after_xfr.len()); + let xfr: u64 = after_xfr[..xfr_end].parse().unwrap_or(0); + + if let Some(to_chk_pos) = trimmed.find("to-chk=") { + let after_to = &trimmed[to_chk_pos + 7..]; + let slash_pos = after_to.find('/').unwrap_or(after_to.len()); + let total_end = after_to[slash_pos + 1..] + .find(|c: char| !c.is_ascii_digit()) + .unwrap_or(after_to[slash_pos + 1..].len()); + let total: u64 = after_to[slash_pos + 1..][..total_end].parse().unwrap_or(1); + + let p = if total > 0 { + (xfr as f32) / (total as f32) + } else { + 0.0 + }; + ui.sub_progress(p.min(1.0)); + } + } + + // Extract filename: lines starting with a path (not a number/space) + let first_char = trimmed.chars().next(); + if let Some('/') = first_char { + ui.current_file(trimmed.to_string()); + } else if trimmed.starts_with("./") || trimmed.starts_with("..") { + ui.current_file(trimmed.to_string()); + } + } + + let status = child.wait().await + .map_err(|e| format!("rsync beklenemedi: {}", e))?; + + if !status.success() { + return Err(format!("rsync çıkış kodu: {:?}", status.code())); + } Ok(()) } diff --git a/src/main.rs b/src/main.rs index 0bdbcf9..d7a4b7e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -188,6 +188,8 @@ fn run_auto_install(path: &str, demo_mode: bool) -> eframe::Result<()> { print!("\r {}: [{:50}] {:.0}%", t!("progress"), "=".repeat((p * 50.0) as usize), p * 100.0); let _ = std::io::Write::flush(&mut std::io::stdout()); } + Ok(InstallMessage::SubProgress(_)) => {} + Ok(InstallMessage::CurrentFile(_)) => {} Ok(InstallMessage::Done) => { println!("\n\n{}: {}", t!("installation_done"), t!("installation_done_description")); break; diff --git a/src/steps/execution.rs b/src/steps/execution.rs index b328f68..ad8d11d 100644 --- a/src/steps/execution.rs +++ b/src/steps/execution.rs @@ -40,6 +40,7 @@ impl InstallerStep for ExecutionStep { self.slideshow = Some(Slideshow::new(&desktop, state.branding_icons.clone(), state.branding_slides.clone())); state.install_progress = 0.0; + state.install_sub_progress = 0.0; state.install_log.clear(); state.install_current_file = None; state.install_log.push(t!("install_starting").to_string()); @@ -182,6 +183,12 @@ impl InstallerStep for ExecutionStep { Ok(InstallMessage::Progress(p)) => { state.install_progress = p; } + Ok(InstallMessage::SubProgress(p)) => { + state.install_sub_progress = p; + } + Ok(InstallMessage::CurrentFile(f)) => { + state.install_current_file = Some(f); + } Ok(InstallMessage::Done) => { state.install_progress = 1.0; self.finished = true; @@ -305,6 +312,16 @@ impl InstallerStep for ExecutionStep { .animate(running) .desired_height(10.0), ); + + if state.install_sub_progress > 0.0 { + ui.add_space(4.0); + ui.add( + egui::ProgressBar::new(state.install_sub_progress) + .animate(running) + .desired_height(6.0) + .fill(theme::c_accent_dim()), + ); + } }); // Durum etiketi