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
This commit is contained in:
Erkan IŞIK
2026-06-24 08:09:46 +03:00
parent 35ddd0e933
commit 1161296d27
4 changed files with 95 additions and 2 deletions
+2
View File
@@ -207,6 +207,7 @@ pub struct GlobalState {
pub current_step: usize, pub current_step: usize,
pub available_disks: Vec<DiskInfo>, pub available_disks: Vec<DiskInfo>,
pub install_progress: f32, pub install_progress: f32,
pub install_sub_progress: f32,
pub install_log: Vec<String>, pub install_log: Vec<String>,
pub install_current_file: Option<String>, pub install_current_file: Option<String>,
pub demo_mode: bool, pub demo_mode: bool,
@@ -274,6 +275,7 @@ impl Default for GlobalState {
current_step: 0, current_step: 0,
available_disks: Vec::new(), available_disks: Vec::new(),
install_progress: 0.0, install_progress: 0.0,
install_sub_progress: 0.0,
install_log: Vec::new(), install_log: Vec::new(),
install_current_file: None, install_current_file: None,
demo_mode: false, demo_mode: false,
+74 -2
View File
@@ -13,6 +13,7 @@
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc; use std::sync::mpsc;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::Command; use tokio::process::Command;
use rust_i18n::t; use rust_i18n::t;
@@ -27,6 +28,10 @@ pub enum InstallMessage {
Log(String), Log(String),
/// Genel ilerleme; 0.01.0 arasında. /// Genel ilerleme; 0.01.0 arasında.
Progress(f32), Progress(f32),
/// Alt ilerleme (ör: rsync içindeki dosya kopyalama); 0.01.0 arasında.
SubProgress(f32),
/// Kopyalanmakta olan dosya adı.
CurrentFile(String),
/// Tüm işler başarıyla tamamlandı. /// Tüm işler başarıyla tamamlandı.
Done, Done,
/// Bir iş başarısız oldu; kurulum durdu. /// Bir iş başarısız oldu; kurulum durdu.
@@ -54,6 +59,14 @@ impl UiSender {
pub fn log(&self, s: impl Into<String>) { pub fn log(&self, s: impl Into<String>) {
self.send(InstallMessage::Log(s.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<String>) {
self.send(InstallMessage::CurrentFile(f.into()));
}
} }
// ───────────────────────────────────────────── // ─────────────────────────────────────────────
@@ -179,8 +192,67 @@ impl Job for CopyFilesJob {
args.push(self.source.clone()); args.push(self.source.clone());
args.push(self.target.clone()); args.push(self.target.clone());
let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect(); if DEMO_MODE.load(Ordering::Relaxed) {
run_cmd("rsync", &arg_refs).await?; 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(()) Ok(())
} }
+2
View File
@@ -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); print!("\r {}: [{:50}] {:.0}%", t!("progress"), "=".repeat((p * 50.0) as usize), p * 100.0);
let _ = std::io::Write::flush(&mut std::io::stdout()); let _ = std::io::Write::flush(&mut std::io::stdout());
} }
Ok(InstallMessage::SubProgress(_)) => {}
Ok(InstallMessage::CurrentFile(_)) => {}
Ok(InstallMessage::Done) => { Ok(InstallMessage::Done) => {
println!("\n\n{}: {}", t!("installation_done"), t!("installation_done_description")); println!("\n\n{}: {}", t!("installation_done"), t!("installation_done_description"));
break; break;
+17
View File
@@ -40,6 +40,7 @@ impl InstallerStep for ExecutionStep {
self.slideshow = Some(Slideshow::new(&desktop, state.branding_icons.clone(), state.branding_slides.clone())); self.slideshow = Some(Slideshow::new(&desktop, state.branding_icons.clone(), state.branding_slides.clone()));
state.install_progress = 0.0; state.install_progress = 0.0;
state.install_sub_progress = 0.0;
state.install_log.clear(); state.install_log.clear();
state.install_current_file = None; state.install_current_file = None;
state.install_log.push(t!("install_starting").to_string()); state.install_log.push(t!("install_starting").to_string());
@@ -182,6 +183,12 @@ impl InstallerStep for ExecutionStep {
Ok(InstallMessage::Progress(p)) => { Ok(InstallMessage::Progress(p)) => {
state.install_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) => { Ok(InstallMessage::Done) => {
state.install_progress = 1.0; state.install_progress = 1.0;
self.finished = true; self.finished = true;
@@ -305,6 +312,16 @@ impl InstallerStep for ExecutionStep {
.animate(running) .animate(running)
.desired_height(10.0), .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 // Durum etiketi