fix: backup progress window + real-time rsync output

- Replace small popup with big backup progress window (750x500)
- Shows rsync command in header, real-time output in monospace textview
- Progress bar with percentage, Cancel/Close button
- Output with color-coded tags (error=red, success=green, cmd=gray italic)
- Auto-scrolls to bottom during backup
- Fixed live log not updating (stdbuf -oL for unbuffered rsync output)
- Fixed double closing brace compilation error
- Downloaded proper backup icon
This commit is contained in:
x3
2026-09-07 17:37:59 +00:00
parent e2b4bb1413
commit 5422ce17ba
5 changed files with 2900 additions and 60 deletions
+194 -43
View File
@@ -43,42 +43,65 @@ MainWindow::MainWindow(Config& config)
auto* ctx = static_cast<Ctx*>(data);
auto* self = ctx->self;
const RsyncProgress& p = ctx->prog;
if (self->progress_bar_) {
/* Update backup progress window */
if (self->backup_win_) {
if (p.bytes_total > 0) {
double frac = (double)p.bytes_transferred / p.bytes_total;
gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(self->progress_bar_), frac);
}
if (!p.current_file.empty()) {
std::string label = "Syncing: " + p.current_file;
gtk_label_set_text(GTK_LABEL(self->progress_label_), label.c_str());
gtk_label_set_text(GTK_LABEL(self->live_log_label_), label.c_str());
gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(self->backup_progress_), frac);
char pct[32];
snprintf(pct, sizeof(pct), "%.0f%%", frac * 100);
gtk_label_set_text(GTK_LABEL(self->backup_status_label_), pct);
}
if (!p.log_line.empty()) {
const char* tag = nullptr;
if (p.log_line.find("[error]") != std::string::npos) tag = "error";
else if (p.log_line.find("[done]") != std::string::npos) tag = "success";
else if (p.log_line.find("[cmd]") != std::string::npos) tag = "info";
else if (p.log_line.find("[done]") != std::string::npos || p.log_line.find("completed") != std::string::npos) tag = "success";
else if (p.log_line.find("[cmd]") != std::string::npos) {
tag = "cmd";
/* Show the rsync command in the header */
std::string cmd_display = p.log_line;
if (cmd_display.find("[cmd] ") == 0) cmd_display = cmd_display.substr(6);
gtk_label_set_text(GTK_LABEL(self->backup_cmd_label_), cmd_display.c_str());
}
else if (p.log_line.find("[restore") != std::string::npos) tag = "info";
self->append_live_log(p.log_line, tag);
self->append_backup_line(p.log_line, tag);
}
if (p.finished) {
if (self->progress_window_) {
gtk_widget_destroy(self->progress_window_);
self->progress_window_ = nullptr;
}
self->refresh_snapshot_list();
if (p.error) {
self->append_live_log("[ERROR] " + p.error_message, "error");
self->append_backup_line("[ERROR] " + p.error_message, "error");
self->tray_.show_notification("x3-Backup", p.error_message, true);
gtk_label_set_text(GTK_LABEL(self->live_log_label_), "Backup failed");
gtk_label_set_text(GTK_LABEL(self->backup_status_label_), "Failed");
gtk_button_set_label(GTK_BUTTON(self->backup_cancel_btn_), "Close");
} else {
self->append_live_log("[SUCCESS] Backup completed", "success");
self->append_backup_line("[SUCCESS] Backup completed!", "success");
self->tray_.show_notification("x3-Backup", "Backup completed successfully");
gtk_label_set_text(GTK_LABEL(self->live_log_label_), "Backup completed");
gtk_label_set_text(GTK_LABEL(self->backup_status_label_), "Done");
gtk_button_set_label(GTK_BUTTON(self->backup_cancel_btn_), "Close");
}
self->tray_.set_status("x3-Backup - Idle");
gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(self->backup_progress_), 1.0);
self->refresh_snapshot_list();
}
}
/* Also update live log tab */
if (!p.log_line.empty()) {
const char* tag = nullptr;
if (p.log_line.find("[error]") != std::string::npos) tag = "error";
else if (p.log_line.find("[done]") != std::string::npos) tag = "success";
else if (p.log_line.find("[cmd]") != std::string::npos) tag = "info";
else if (p.log_line.find("[restore") != std::string::npos) tag = "info";
self->append_live_log(p.log_line, tag);
}
if (p.finished) {
if (p.error) {
gtk_label_set_text(GTK_LABEL(self->live_log_label_), "Backup failed");
} else {
gtk_label_set_text(GTK_LABEL(self->live_log_label_), "Backup completed");
}
}
delete ctx;
return G_SOURCE_REMOVE;
}, ctx);
@@ -406,12 +429,6 @@ void MainWindow::build_toolbar() {
gtk_box_pack_start(GTK_BOX(tbar), gtk_separator_new(GTK_ORIENTATION_VERTICAL), FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(tbar), mkbtn("Settings", G_CALLBACK(on_settings), this), FALSE, FALSE, 0);
progress_bar_ = gtk_progress_bar_new();
gtk_widget_set_size_request(progress_bar_, 200, -1);
gtk_box_pack_end(GTK_BOX(tbar), progress_bar_, FALSE, FALSE, 0);
progress_label_ = gtk_label_new("");
gtk_box_pack_end(GTK_BOX(tbar), progress_label_, FALSE, FALSE, 4);
gtk_box_pack_start(GTK_BOX(toolbar_), tbar, FALSE, FALSE, 0);
}
@@ -650,34 +667,168 @@ void MainWindow::show_sudo_dialog(std::function<void(const std::string&)> callba
gtk_widget_destroy(dialog);
}
void MainWindow::show_progress_dialog(const std::string& title) {
progress_window_ = gtk_window_new(GTK_WINDOW_POPUP);
gtk_window_set_title(GTK_WINDOW(progress_window_), title.c_str());
gtk_window_set_default_size(GTK_WINDOW(progress_window_), 400, 120);
gtk_window_set_position(GTK_WINDOW(progress_window_), GTK_WIN_POS_CENTER);
gtk_window_set_keep_above(GTK_WINDOW(progress_window_), TRUE);
void MainWindow::open_backup_window(const std::string& description) {
if (backup_win_) {
/* Clear previous output */
GtkTextBuffer* buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(backup_output_view_));
gtk_text_buffer_set_text(buf, "", -1);
gtk_label_set_text(GTK_LABEL(backup_cmd_label_), "");
gtk_label_set_text(GTK_LABEL(backup_status_label_), "Running...");
gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(backup_progress_), 0.0);
gtk_button_set_label(GTK_BUTTON(backup_cancel_btn_), "Cancel");
gtk_widget_show_all(backup_win_);
return;
}
GtkWidget* vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
gtk_widget_set_margin_start(vbox, 16);
gtk_widget_set_margin_top(vbox, 16);
gtk_container_add(GTK_CONTAINER(progress_window_), vbox);
backup_win_ = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_window_set_title(GTK_WINDOW(backup_win_), "x3-Backup - Backup Progress");
gtk_window_set_default_size(GTK_WINDOW(backup_win_), 750, 500);
gtk_window_set_position(GTK_WINDOW(backup_win_), GTK_WIN_POS_CENTER);
gtk_window_set_transient_for(GTK_WINDOW(backup_win_), GTK_WINDOW(window_));
gtk_window_set_modal(GTK_WINDOW(backup_win_), TRUE);
GtkWidget* label = gtk_label_new(title.c_str());
gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 0);
const char* icon_paths[] = {
"icons/x3-backup.png",
"/usr/share/icons/hicolor/256x256/apps/x3-backup.png",
nullptr
};
for (int i = 0; icon_paths[i]; ++i) {
if (g_file_test(icon_paths[i], G_FILE_TEST_EXISTS)) {
gtk_window_set_icon_from_file(GTK_WINDOW(backup_win_), icon_paths[i], nullptr);
break;
}
}
progress_bar_ = gtk_progress_bar_new();
gtk_box_pack_start(GTK_BOX(vbox), progress_bar_, FALSE, FALSE, 0);
GtkWidget* vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
gtk_widget_set_margin_start(vbox, 8);
gtk_widget_set_margin_end(vbox, 8);
gtk_widget_set_margin_top(vbox, 8);
gtk_widget_set_margin_bottom(vbox, 8);
gtk_container_add(GTK_CONTAINER(backup_win_), vbox);
progress_label_ = gtk_label_new("Starting...");
gtk_box_pack_start(GTK_BOX(vbox), progress_label_, FALSE, FALSE, 0);
/* Header */
GtkWidget* hdr_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
GtkWidget* hdr_icon = gtk_image_new_from_icon_name("drive-harddisk", GTK_ICON_SIZE_DIALOG);
gtk_box_pack_start(GTK_BOX(hdr_box), hdr_icon, FALSE, FALSE, 0);
gtk_widget_show_all(progress_window_);
GtkWidget* hdr_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
std::string title_text = description.empty() ? "Creating Backup..." : description;
GtkWidget* hdr_title = gtk_label_new(nullptr);
char title_markup[512];
snprintf(title_markup, sizeof(title_markup), "<b><big>%s</big></big>", title_text.c_str());
gtk_label_set_markup(GTK_LABEL(hdr_title), title_markup);
gtk_widget_set_halign(hdr_title, GTK_ALIGN_START);
gtk_box_pack_start(GTK_BOX(hdr_vbox), hdr_title, FALSE, FALSE, 0);
backup_cmd_label_ = gtk_label_new("Waiting for rsync...");
gtk_widget_set_halign(backup_cmd_label_, GTK_ALIGN_START);
PangoAttrList* mono = pango_attr_list_new();
pango_attr_list_insert(mono, pango_attr_family_new("monospace"));
pango_attr_list_insert(mono, pango_attr_size_new(8000));
gtk_label_set_attributes(GTK_LABEL(backup_cmd_label_), mono);
pango_attr_list_unref(mono);
gtk_box_pack_start(GTK_BOX(hdr_vbox), backup_cmd_label_, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(hdr_box), hdr_vbox, TRUE, TRUE, 0);
gtk_box_pack_start(GTK_BOX(vbox), hdr_box, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(vbox), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), FALSE, FALSE, 0);
/* rsync output */
GtkWidget* output_scroll = gtk_scrolled_window_new(nullptr, nullptr);
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(output_scroll), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
backup_output_view_ = gtk_text_view_new();
gtk_text_view_set_editable(GTK_TEXT_VIEW(backup_output_view_), FALSE);
gtk_text_view_set_monospace(GTK_TEXT_VIEW(backup_output_view_), TRUE);
gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(backup_output_view_), GTK_WRAP_WORD_CHAR);
/* Color tags */
GtkTextBuffer* buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(backup_output_view_));
gtk_text_buffer_create_tag(buf, "error", "foreground", "#e74c3c", "weight", PANGO_WEIGHT_BOLD, nullptr);
gtk_text_buffer_create_tag(buf, "success", "foreground", "#2ecc71", "weight", PANGO_WEIGHT_BOLD, nullptr);
gtk_text_buffer_create_tag(buf, "info", "foreground", "#3498db", nullptr);
gtk_text_buffer_create_tag(buf, "cmd", "foreground", "#95a5a6", "style", PANGO_STYLE_ITALIC, nullptr);
gtk_container_add(GTK_CONTAINER(output_scroll), backup_output_view_);
gtk_box_pack_start(GTK_BOX(vbox), output_scroll, TRUE, TRUE, 0);
/* Progress bar + status */
GtkWidget* prog_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
backup_progress_ = gtk_progress_bar_new();
gtk_widget_set_hexpand(backup_progress_, TRUE);
gtk_box_pack_start(GTK_BOX(prog_box), backup_progress_, TRUE, TRUE, 0);
backup_status_label_ = gtk_label_new("Running...");
gtk_box_pack_start(GTK_BOX(prog_box), backup_status_label_, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(vbox), prog_box, FALSE, FALSE, 0);
/* Buttons */
GtkWidget* btn_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
gtk_widget_set_halign(btn_box, GTK_ALIGN_END);
backup_cancel_btn_ = gtk_button_new_with_label("Cancel");
g_signal_connect(backup_cancel_btn_, "clicked", G_CALLBACK(+[](GtkWidget*, gpointer d) {
auto* self = static_cast<MainWindow*>(d);
const char* label = gtk_button_get_label(GTK_BUTTON(self->backup_cancel_btn_));
if (strcmp(label, "Close") == 0) {
self->close_backup_window();
} else {
self->rsync_.cancel();
gtk_label_set_text(GTK_LABEL(self->backup_status_label_), "Cancelling...");
gtk_button_set_label(GTK_BUTTON(self->backup_cancel_btn_), "Close");
}
}), this);
gtk_box_pack_start(GTK_BOX(btn_box), backup_cancel_btn_, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(vbox), btn_box, FALSE, FALSE, 0);
g_signal_connect(backup_win_, "delete-event", G_CALLBACK(+[](GtkWidget*, GdkEvent*, gpointer d) -> gboolean {
auto* self = static_cast<MainWindow*>(d);
if (self->rsync_.is_running()) {
/* Don't close while running, just hide */
return TRUE;
}
self->close_backup_window();
return TRUE;
}), this);
gtk_widget_show_all(backup_win_);
}
void MainWindow::close_backup_window() {
if (backup_win_) {
gtk_widget_destroy(backup_win_);
backup_win_ = nullptr;
backup_output_view_ = nullptr;
backup_cmd_label_ = nullptr;
backup_progress_ = nullptr;
backup_status_label_ = nullptr;
backup_cancel_btn_ = nullptr;
}
}
void MainWindow::append_backup_line(const std::string& line, const char* tag) {
if (!backup_output_view_) return;
GtkTextBuffer* buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(backup_output_view_));
GtkTextIter end;
gtk_text_buffer_get_end_iter(buf, &end);
if (tag) {
gtk_text_buffer_insert_with_tags_by_name(buf, &end, line.c_str(), -1, tag);
} else {
gtk_text_buffer_insert(buf, &end, line.c_str(), -1);
}
gtk_text_buffer_insert(buf, &end, "\n", -1);
/* Auto-scroll */
GtkTextView* tv = GTK_TEXT_VIEW(backup_output_view_);
GtkTextMark* mark = gtk_text_buffer_create_mark(buf, nullptr, &end, FALSE);
gtk_text_view_scroll_mark_onscreen(tv, mark);
gtk_text_buffer_delete_mark(buf, mark);
}
void MainWindow::on_backup(GtkWidget*, gpointer d) {
auto* self = static_cast<MainWindow*>(d);
self->tray_.set_status("x3-Backup - Creating backup...", true);
self->show_progress_dialog("Creating Snapshot...");
self->open_backup_window("Creating Snapshot...");
gtk_label_set_text(GTK_LABEL(self->live_log_label_), "Backup running...");
g_thread_new("backup", [](gpointer data) -> gpointer {
auto* s = static_cast<MainWindow*>(data);