#include "main_window.h" #include #include #include #include #include static std::string format_size(int64_t bytes) { char buf[64]; if (bytes < 1024) snprintf(buf, sizeof(buf), "%ld B", (long)bytes); else if (bytes < 1024LL * 1024) snprintf(buf, sizeof(buf), "%.1f KB", (double)bytes / 1024); else if (bytes < 1024LL * 1024 * 1024) snprintf(buf, sizeof(buf), "%.1f MB", (double)bytes / (1024 * 1024)); else snprintf(buf, sizeof(buf), "%.2f GB", (double)bytes / (1024LL * 1024 * 1024)); return buf; } static std::string mode_string(mode_t mode) { char buf[11]; buf[0] = S_ISDIR(mode) ? 'd' : '-'; buf[1] = (mode & S_IRUSR) ? 'r' : '-'; buf[2] = (mode & S_IWUSR) ? 'w' : '-'; buf[3] = (mode & S_IXUSR) ? 'x' : '-'; buf[4] = (mode & S_IRGRP) ? 'r' : '-'; buf[5] = (mode & S_IWGRP) ? 'w' : '-'; buf[6] = (mode & S_IXGRP) ? 'x' : '-'; buf[7] = (mode & S_IROTH) ? 'r' : '-'; buf[8] = (mode & S_IWOTH) ? 'w' : '-'; buf[9] = (mode & S_IXOTH) ? 'x' : '-'; buf[10] = 0; return buf; } MainWindow::MainWindow(Config& config) : config_(config), rsync_(config), snapshots_(config, rsync_) { rsync_.on_progress = [this](const RsyncProgress& p) { struct Ctx { MainWindow* self; RsyncProgress prog; }; Ctx* ctx = new Ctx{this, p}; g_idle_add([](gpointer data) -> gboolean { auto* ctx = static_cast(data); auto* self = ctx->self; const RsyncProgress& p = ctx->prog; /* Update backup progress window */ if (self->backup_win_) { if (p.estimated_total_size > 0) { double frac = (p.estimated_total_size > 0) ? (double)p.bytes_transferred / p.estimated_total_size : 0; if (frac > 1.0) frac = 1.0; 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); char size_info[128]; snprintf(size_info, sizeof(size_info), "%s / %s", format_size(p.bytes_transferred).c_str(), format_size(p.estimated_total_size).c_str()); gtk_label_set_text(GTK_LABEL(self->backup_size_label_), size_info); } if (p.estimated_remaining > 0 && p.estimated_remaining < 86400 * 365) { std::string time_str = self->rsync_.format_time_remaining(p.estimated_remaining); gtk_label_set_text(GTK_LABEL(self->backup_time_label_), time_str.c_str()); } else if (p.elapsed_seconds > 0) { char elapsed_buf[64]; int mins = (int)(p.elapsed_seconds / 60); int secs = (int)(p.elapsed_seconds) % 60; snprintf(elapsed_buf, sizeof(elapsed_buf), "Elapsed: %dm %02ds", mins, secs); gtk_label_set_text(GTK_LABEL(self->backup_time_label_), elapsed_buf); } 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 || 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"; else if (p.log_line.find("[info]") != std::string::npos) tag = "info"; self->append_backup_line(p.log_line, tag); } if (p.finished) { if (p.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->backup_status_label_), "Failed"); gtk_button_set_label(GTK_BUTTON(self->backup_cancel_btn_), "Close"); } else { self->append_backup_line("[SUCCESS] Backup completed!", "success"); self->tray_.show_notification("x3-Backup", "Backup completed successfully"); 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); }; tray_.on_show = [this]() { show(); }; tray_.on_quit = [this]() { gtk_main_quit(); }; tray_.on_quick_backup = [this]() { start_quick_backup(); }; tray_.on_settings = [this]() { SettingsDialog dlg(GTK_WINDOW(window_), config_); dlg.run(); }; build_ui(); } MainWindow::~MainWindow() {} void MainWindow::build_ui() { window_ = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_window_set_title(GTK_WINDOW(window_), "x3-Backup"); gtk_window_set_default_size(GTK_WINDOW(window_), 1100, 700); g_signal_connect(window_, "delete-event", G_CALLBACK(on_tray_close), this); 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(window_), icon_paths[i], nullptr); break; } } GtkWidget* vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); gtk_container_add(GTK_CONTAINER(window_), vbox); build_menu(); GtkWidget* btn_toolbar = build_toolbar(); gtk_box_pack_start(GTK_BOX(vbox), toolbar_, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(vbox), btn_toolbar, FALSE, FALSE, 0); GtkWidget* paned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL); gtk_box_pack_start(GTK_BOX(vbox), paned, TRUE, TRUE, 0); /* Left: snapshot list */ GtkWidget* left_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); gtk_widget_set_size_request(left_box, 300, -1); gtk_paned_pack1(GTK_PANED(paned), left_box, FALSE, FALSE); GtkWidget* left_header = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); gtk_widget_set_margin_start(left_header, 8); gtk_widget_set_margin_end(left_header, 4); gtk_widget_set_margin_top(left_header, 6); gtk_widget_set_margin_bottom(left_header, 6); GtkWidget* left_title = gtk_label_new(NULL); gtk_label_set_markup(GTK_LABEL(left_title), "Snapshots"); gtk_widget_set_halign(left_title, GTK_ALIGN_START); gtk_box_pack_start(GTK_BOX(left_header), left_title, TRUE, TRUE, 0); GtkWidget* refresh_btn = gtk_button_new_from_icon_name("view-refresh", GTK_ICON_SIZE_MENU); gtk_widget_set_tooltip_text(refresh_btn, "Refresh snapshot list"); g_signal_connect(refresh_btn, "clicked", G_CALLBACK(+[](GtkWidget*, gpointer d) { static_cast(d)->refresh_snapshot_list(); }), this); gtk_box_pack_end(GTK_BOX(left_header), refresh_btn, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(left_box), left_header, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(left_box), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), FALSE, FALSE, 0); GtkWidget* snap_scroll = gtk_scrolled_window_new(nullptr, nullptr); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(snap_scroll), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); snapshot_listbox_ = gtk_list_box_new(); gtk_list_box_set_selection_mode(GTK_LIST_BOX(snapshot_listbox_), GTK_SELECTION_SINGLE); g_signal_connect(snapshot_listbox_, "row-selected", G_CALLBACK(on_snapshot_selected), this); gtk_container_add(GTK_CONTAINER(snap_scroll), snapshot_listbox_); gtk_box_pack_start(GTK_BOX(left_box), snap_scroll, TRUE, TRUE, 0); refresh_snapshot_list(); /* Right: notebook with file browser + log tabs */ GtkWidget* right_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); gtk_paned_pack2(GTK_PANED(paned), right_box, TRUE, TRUE); notebook_ = gtk_notebook_new(); gtk_box_pack_start(GTK_BOX(right_box), notebook_, TRUE, TRUE, 0); /* ── Tab 1: File Browser ── */ GtkWidget* browser_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); GtkWidget* nav_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4); gtk_widget_set_margin_start(nav_box, 4); gtk_widget_set_margin_top(nav_box, 4); gtk_widget_set_margin_bottom(nav_box, 4); GtkWidget* up_btn = gtk_button_new_from_icon_name("go-up", GTK_ICON_SIZE_MENU); g_signal_connect(up_btn, "clicked", G_CALLBACK(on_navigate_up), this); gtk_box_pack_start(GTK_BOX(nav_box), up_btn, FALSE, FALSE, 0); path_entry_ = gtk_entry_new(); gtk_editable_set_editable(GTK_EDITABLE(path_entry_), FALSE); gtk_widget_set_hexpand(path_entry_, TRUE); gtk_box_pack_start(GTK_BOX(nav_box), path_entry_, TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(browser_box), nav_box, FALSE, FALSE, 0); nav_label_ = gtk_label_new("Select a snapshot to browse"); gtk_widget_set_halign(nav_label_, GTK_ALIGN_START); gtk_widget_set_margin_start(nav_label_, 8); gtk_box_pack_start(GTK_BOX(browser_box), nav_label_, FALSE, FALSE, 0); /* File tree */ file_store_ = gtk_list_store_new(5, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING); file_tree_ = gtk_tree_view_new_with_model(GTK_TREE_MODEL(file_store_)); g_object_unref(file_store_); GtkCellRenderer* name_renderer = gtk_cell_renderer_text_new(); GtkTreeViewColumn* name_col = gtk_tree_view_column_new_with_attributes("Name", name_renderer, "text", col_name, nullptr); gtk_tree_view_column_set_sort_column_id(name_col, col_name); gtk_tree_view_append_column(GTK_TREE_VIEW(file_tree_), name_col); GtkCellRenderer* size_renderer = gtk_cell_renderer_text_new(); GtkTreeViewColumn* size_col = gtk_tree_view_column_new_with_attributes("Size", size_renderer, "text", col_size, nullptr); gtk_tree_view_column_set_sort_column_id(size_col, col_size); gtk_tree_view_append_column(GTK_TREE_VIEW(file_tree_), size_col); GtkCellRenderer* perm_renderer = gtk_cell_renderer_text_new(); GtkTreeViewColumn* perm_col = gtk_tree_view_column_new_with_attributes("Permissions", perm_renderer, "text", col_permissions, nullptr); gtk_tree_view_append_column(GTK_TREE_VIEW(file_tree_), perm_col); GtkCellRenderer* mtime_renderer = gtk_cell_renderer_text_new(); GtkTreeViewColumn* mtime_col = gtk_tree_view_column_new_with_attributes("Modified", mtime_renderer, "text", col_mtime, nullptr); gtk_tree_view_append_column(GTK_TREE_VIEW(file_tree_), mtime_col); g_signal_connect(file_tree_, "row-activated", G_CALLBACK(on_file_row_activated), this); g_signal_connect(file_tree_, "button-press-event", G_CALLBACK(on_file_button_press), this); GtkWidget* tree_scroll = gtk_scrolled_window_new(nullptr, nullptr); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(tree_scroll), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); gtk_container_add(GTK_CONTAINER(tree_scroll), file_tree_); gtk_box_pack_start(GTK_BOX(browser_box), tree_scroll, TRUE, TRUE, 0); gtk_notebook_append_page(GTK_NOTEBOOK(notebook_), browser_box, gtk_label_new("Files")); /* ── Tab 2: Live Log ── */ GtkWidget* live_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4); gtk_widget_set_margin_start(live_box, 4); gtk_widget_set_margin_top(live_box, 4); live_log_label_ = gtk_label_new("No backup running"); gtk_widget_set_halign(live_log_label_, GTK_ALIGN_START); gtk_widget_set_margin_start(live_log_label_, 4); PangoAttrList* mono_attrs = pango_attr_list_new(); pango_attr_list_insert(mono_attrs, pango_attr_family_new("monospace")); gtk_label_set_attributes(GTK_LABEL(live_log_label_), mono_attrs); pango_attr_list_unref(mono_attrs); gtk_box_pack_start(GTK_BOX(live_box), live_log_label_, FALSE, FALSE, 0); GtkWidget* live_scroll = gtk_scrolled_window_new(nullptr, nullptr); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(live_scroll), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); live_log_textview_ = gtk_text_view_new(); gtk_text_view_set_editable(GTK_TEXT_VIEW(live_log_textview_), FALSE); gtk_text_view_set_monospace(GTK_TEXT_VIEW(live_log_textview_), TRUE); GtkTextBuffer* live_buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(live_log_textview_)); gtk_text_buffer_create_tag(live_buf, "normal", nullptr); gtk_text_buffer_create_tag(live_buf, "error", "foreground", "#e74c3c", nullptr); gtk_text_buffer_create_tag(live_buf, "success", "foreground", "#2ecc71", nullptr); gtk_text_buffer_create_tag(live_buf, "info", "foreground", "#3498db", nullptr); gtk_text_buffer_create_tag(live_buf, "cmd", "foreground", "#95a5a6", "style", PANGO_STYLE_ITALIC, nullptr); gtk_container_add(GTK_CONTAINER(live_scroll), live_log_textview_); gtk_box_pack_start(GTK_BOX(live_box), live_scroll, TRUE, TRUE, 0); GtkWidget* live_toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4); GtkWidget* clear_live_btn = gtk_button_new_with_label("Clear"); g_signal_connect(clear_live_btn, "clicked", G_CALLBACK(+[](GtkWidget*, gpointer d) { auto* self = static_cast(d); GtkTextBuffer* buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(self->live_log_textview_)); gtk_text_buffer_set_text(buf, "", -1); gtk_label_set_text(GTK_LABEL(self->live_log_label_), "No backup running"); }), this); gtk_box_pack_start(GTK_BOX(live_toolbar), clear_live_btn, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(live_box), live_toolbar, FALSE, FALSE, 0); gtk_notebook_append_page(GTK_NOTEBOOK(notebook_), live_box, gtk_label_new("Live Log")); /* ── Tab 3: History Log ── */ GtkWidget* hist_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4); gtk_widget_set_margin_start(hist_box, 4); gtk_widget_set_margin_top(hist_box, 4); GtkWidget* hist_label = gtk_label_new("x3-Backup Log History"); gtk_widget_set_halign(hist_label, GTK_ALIGN_START); gtk_widget_set_margin_start(hist_label, 4); PangoAttrList* mono2 = pango_attr_list_new(); pango_attr_list_insert(mono2, pango_attr_family_new("monospace")); gtk_label_set_attributes(GTK_LABEL(hist_label), mono2); pango_attr_list_unref(mono2); gtk_box_pack_start(GTK_BOX(hist_box), hist_label, FALSE, FALSE, 0); backlog_scrolled_ = gtk_scrolled_window_new(nullptr, nullptr); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(backlog_scrolled_), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); backlog_textview_ = gtk_text_view_new(); gtk_text_view_set_editable(GTK_TEXT_VIEW(backlog_textview_), FALSE); gtk_text_view_set_monospace(GTK_TEXT_VIEW(backlog_textview_), TRUE); gtk_container_add(GTK_CONTAINER(backlog_scrolled_), backlog_textview_); gtk_box_pack_start(GTK_BOX(hist_box), backlog_scrolled_, TRUE, TRUE, 0); GtkWidget* hist_toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4); GtkWidget* refresh_hist_btn = gtk_button_new_with_label("Refresh"); GtkWidget* clear_hist_btn = gtk_button_new_with_label("Clear"); g_signal_connect(refresh_hist_btn, "clicked", G_CALLBACK(+[](GtkWidget*, gpointer d) { auto* self = static_cast(d); self->load_log_history(); }), this); g_signal_connect(clear_hist_btn, "clicked", G_CALLBACK(+[](GtkWidget*, gpointer d) { auto* self = static_cast(d); GtkTextBuffer* buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(self->backlog_textview_)); gtk_text_buffer_set_text(buf, "", -1); }), this); gtk_box_pack_start(GTK_BOX(hist_toolbar), refresh_hist_btn, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(hist_toolbar), clear_hist_btn, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(hist_box), hist_toolbar, FALSE, FALSE, 0); gtk_notebook_append_page(GTK_NOTEBOOK(notebook_), hist_box, gtk_label_new("History Log")); log_path_ = std::string(g_get_home_dir()) + "/.config/x3-backup/x3-backup.log"; build_statusbar(); gtk_box_pack_start(GTK_BOX(vbox), statusbar_, FALSE, FALSE, 0); } void MainWindow::build_menu() { toolbar_ = gtk_menu_bar_new(); /* File menu */ GtkWidget* file_menu = gtk_menu_new(); GtkWidget* file_item = gtk_menu_item_new_with_label("File"); gtk_menu_item_set_submenu(GTK_MENU_ITEM(file_item), file_menu); GtkWidget* new_backup = gtk_menu_item_new_with_label("New Backup"); g_signal_connect(new_backup, "activate", G_CALLBACK(on_new_backup), this); gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), new_backup); GtkWidget* sys_backup = gtk_menu_item_new_with_label("System Backup (full)"); g_signal_connect(sys_backup, "activate", G_CALLBACK(on_system_backup), this); gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), sys_backup); gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), gtk_separator_menu_item_new()); GtkWidget* settings = gtk_menu_item_new_with_label("Settings"); g_signal_connect(settings, "activate", G_CALLBACK(on_settings), this); gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), settings); GtkWidget* show_log = gtk_menu_item_new_with_label("Show Log"); g_signal_connect(show_log, "activate", G_CALLBACK(on_show_log), this); gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), show_log); gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), gtk_separator_menu_item_new()); GtkWidget* quit = gtk_menu_item_new_with_label("Quit"); g_signal_connect(quit, "activate", G_CALLBACK(on_quit), this); gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), quit); gtk_menu_shell_append(GTK_MENU_SHELL(toolbar_), file_item); /* Backup menu */ GtkWidget* backup_menu = gtk_menu_new(); GtkWidget* backup_item = gtk_menu_item_new_with_label("Backup"); gtk_menu_item_set_submenu(GTK_MENU_ITEM(backup_item), backup_menu); GtkWidget* run_backup = gtk_menu_item_new_with_label("Run Backup Now"); g_signal_connect(run_backup, "activate", G_CALLBACK(on_backup), this); gtk_menu_shell_append(GTK_MENU_SHELL(backup_menu), run_backup); GtkWidget* restore = gtk_menu_item_new_with_label("Restore Selected"); g_signal_connect(restore, "activate", G_CALLBACK(on_restore), this); gtk_menu_shell_append(GTK_MENU_SHELL(backup_menu), restore); gtk_menu_shell_append(GTK_MENU_SHELL(backup_menu), gtk_separator_menu_item_new()); GtkWidget* del_snap = gtk_menu_item_new_with_label("Delete Snapshot"); g_signal_connect(del_snap, "activate", G_CALLBACK(on_delete_snapshot), this); gtk_menu_shell_append(GTK_MENU_SHELL(backup_menu), del_snap); gtk_menu_shell_append(GTK_MENU_SHELL(toolbar_), backup_item); /* System menu */ GtkWidget* sys_menu = gtk_menu_new(); GtkWidget* sys_item = gtk_menu_item_new_with_label("System"); gtk_menu_item_set_submenu(GTK_MENU_ITEM(sys_item), sys_menu); GtkWidget* sys_restore = gtk_menu_item_new_with_label("Restore Full System"); g_signal_connect(sys_restore, "activate", G_CALLBACK(on_system_restore), this); gtk_menu_shell_append(GTK_MENU_SHELL(sys_menu), sys_restore); gtk_menu_shell_append(GTK_MENU_SHELL(sys_menu), gtk_separator_menu_item_new()); GtkWidget* install_cron = gtk_menu_item_new_with_label("Install Cronjob"); g_signal_connect(install_cron, "activate", G_CALLBACK(on_install_cron), this); gtk_menu_shell_append(GTK_MENU_SHELL(sys_menu), install_cron); GtkWidget* remove_cron = gtk_menu_item_new_with_label("Remove Cronjob"); g_signal_connect(remove_cron, "activate", G_CALLBACK(on_remove_cron), this); gtk_menu_shell_append(GTK_MENU_SHELL(sys_menu), remove_cron); gtk_menu_shell_append(GTK_MENU_SHELL(toolbar_), sys_item); /* Help menu */ GtkWidget* help_menu = gtk_menu_new(); GtkWidget* help_item = gtk_menu_item_new_with_label("Help"); gtk_menu_item_set_submenu(GTK_MENU_ITEM(help_item), help_menu); GtkWidget* about = gtk_menu_item_new_with_label("About"); g_signal_connect(about, "activate", G_CALLBACK(on_about), this); gtk_menu_shell_append(GTK_MENU_SHELL(help_menu), about); gtk_menu_shell_append(GTK_MENU_SHELL(toolbar_), help_item); } GtkWidget* MainWindow::build_toolbar() { GtkWidget* tbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4); gtk_widget_set_margin_start(tbar, 6); gtk_widget_set_margin_end(tbar, 6); gtk_widget_set_margin_top(tbar, 4); gtk_widget_set_margin_bottom(tbar, 4); auto mkbtn = [](const char* label, GCallback cb, gpointer data) -> GtkWidget* { GtkWidget* b = gtk_button_new_with_label(label); g_signal_connect(b, "clicked", cb, data); return b; }; gtk_box_pack_start(GTK_BOX(tbar), mkbtn("New Backup", G_CALLBACK(on_new_backup), this), FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(tbar), mkbtn("Restore", G_CALLBACK(on_restore), this), FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(tbar), mkbtn("Delete", G_CALLBACK(on_delete_snapshot), this), FALSE, FALSE, 0); 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); return tbar; } void MainWindow::build_statusbar() { statusbar_ = gtk_statusbar_new(); } void MainWindow::refresh_snapshot_list() { GList* children = gtk_container_get_children(GTK_CONTAINER(snapshot_listbox_)); for (GList* l = children; l; l = l->next) gtk_widget_destroy(GTK_WIDGET(l->data)); g_list_free(children); auto snapshots = snapshots_.list_snapshots(); for (const auto& s : snapshots) { GtkWidget* row_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2); gtk_widget_set_margin_start(row_box, 8); gtk_widget_set_margin_end(row_box, 8); gtk_widget_set_margin_top(row_box, 6); gtk_widget_set_margin_bottom(row_box, 6); GtkWidget* name_label = gtk_label_new(s.display_name.c_str()); gtk_widget_set_halign(name_label, GTK_ALIGN_START); PangoAttrList* attrs = pango_attr_list_new(); pango_attr_list_insert(attrs, pango_attr_weight_new(PANGO_WEIGHT_BOLD)); gtk_label_set_attributes(GTK_LABEL(name_label), attrs); pango_attr_list_unref(attrs); gtk_box_pack_start(GTK_BOX(row_box), name_label, FALSE, FALSE, 0); std::string detail; if (!s.description.empty()) detail = s.description; else detail = "Size: " + format_size(s.size_bytes); GtkWidget* detail_label = gtk_label_new(detail.c_str()); gtk_widget_set_halign(detail_label, GTK_ALIGN_START); gtk_label_set_ellipsize(GTK_LABEL(detail_label), PANGO_ELLIPSIZE_END); gtk_box_pack_start(GTK_BOX(row_box), detail_label, FALSE, FALSE, 0); gtk_list_box_insert(GTK_LIST_BOX(snapshot_listbox_), row_box, -1); } gtk_widget_show_all(snapshot_listbox_); } void MainWindow::on_snapshot_selected(GtkListBox*, GtkListBoxRow* row, gpointer d) { auto* self = static_cast(d); if (!row) return; int idx = gtk_list_box_row_get_index(row); auto snapshots = self->snapshots_.list_snapshots(); if (idx < 0 || idx >= (int)snapshots.size()) return; self->current_snapshot_ = snapshots[idx].path; self->current_path_ = ""; self->path_stack_.clear(); self->browse_path(self->current_snapshot_, ""); } void MainWindow::browse_path(const std::string& snapshot, const std::string& path) { current_snapshot_ = snapshot; current_path_ = path; std::string display = path.empty() ? "/" : path; gtk_entry_set_text(GTK_ENTRY(path_entry_), display.c_str()); std::string info = "Browsing: " + snapshot; if (!path.empty()) info += "/" + path; gtk_label_set_text(GTK_LABEL(nav_label_), info.c_str()); gtk_list_store_clear(file_store_); auto entries = snapshots_.browse_directory(snapshot, path); for (const auto& e : entries) { GtkTreeIter iter; gtk_list_store_append(file_store_, &iter); gtk_list_store_set(file_store_, &iter, col_name, e.name.c_str(), col_size, (e.is_dir ? "-" : format_size(e.size).c_str()), col_type, e.is_dir ? "dir" : "file", col_permissions, mode_string(e.mode).c_str(), col_mtime, "", -1); } } void MainWindow::navigate_up() { if (current_path_.empty()) return; auto pos = current_path_.rfind('/'); if (pos == std::string::npos || pos == 0) { path_stack_.pop_back(); if (path_stack_.empty()) { current_path_ = ""; } else { current_path_ = path_stack_.back(); path_stack_.pop_back(); } } else { path_stack_.push_back(current_path_); current_path_ = current_path_.substr(0, pos); } browse_path(current_snapshot_, current_path_); } void MainWindow::on_file_row_activated(GtkTreeView* tree, GtkTreePath* path, GtkTreeViewColumn*, gpointer d) { auto* self = static_cast(d); if (self->current_snapshot_.empty()) return; GtkTreeModel* model = gtk_tree_view_get_model(tree); GtkTreeIter iter; if (!gtk_tree_model_get_iter(model, &iter, path)) return; gchar* name = nullptr; gchar* type = nullptr; gtk_tree_model_get(model, &iter, 0, &name, 2, &type, -1); if (type && strcmp(type, "dir") == 0) { std::string new_path = self->current_path_.empty() ? std::string(name) : self->current_path_ + "/" + name; self->path_stack_.push_back(self->current_path_); self->browse_path(self->current_snapshot_, new_path); } g_free(name); g_free(type); } gboolean MainWindow::on_file_button_press(GtkWidget*, GdkEventButton* event, gpointer d) { if (event->type != GDK_BUTTON_PRESS || event->button != 3) return FALSE; auto* self = static_cast(d); if (self->current_snapshot_.empty()) return FALSE; GtkTreeSelection* sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(self->file_tree_)); GtkTreeModel* model = nullptr; GtkTreeIter iter; if (!gtk_tree_selection_get_selected(sel, &model, &iter)) return FALSE; gchar* name = nullptr; gchar* type = nullptr; gtk_tree_model_get(model, &iter, 0, &name, 2, &type, -1); std::string file_path = self->current_path_.empty() ? std::string(name) : self->current_path_ + "/" + name; GtkWidget* menu = gtk_menu_new(); /* Restore item */ GtkWidget* restore_item = gtk_menu_item_new_with_label("Restore"); struct RestoreCtx { MainWindow* self; std::string path; }; auto* ctx = new RestoreCtx{self, file_path}; g_signal_connect_data(restore_item, "activate", G_CALLBACK(+[](GtkWidget*, gpointer d) { auto* c = static_cast(d); std::vector files = {c->path}; c->self->snapshots_.restore_selected(c->self->current_snapshot_, files); c->self->tray_.show_notification("x3-Backup", "File restored: " + c->path); delete c; }), ctx, nullptr, (GConnectFlags)0); gtk_menu_shell_append(GTK_MENU_SHELL(menu), restore_item); /* Info */ if (type && strcmp(type, "file") == 0) { GtkWidget* info_item = gtk_menu_item_new_with_label("Properties"); g_signal_connect_data(info_item, "activate", G_CALLBACK(+[](GtkWidget*, gpointer d) { auto* c = static_cast(d); GtkWidget* dlg = gtk_message_dialog_new(nullptr, GTK_DIALOG_MODAL, GTK_MESSAGE_INFO, GTK_BUTTONS_OK, "File: %s", c->path.c_str()); gtk_dialog_run(GTK_DIALOG(dlg)); gtk_widget_destroy(dlg); delete c; }), ctx, nullptr, (GConnectFlags)0); gtk_menu_shell_append(GTK_MENU_SHELL(menu), info_item); } else { g_free(name); g_free(type); /* For dirs, free old ctx and create restore for dir */ delete ctx; ctx = new RestoreCtx{self, file_path}; g_signal_connect_data(restore_item, "activate", G_CALLBACK(+[](GtkWidget*, gpointer d) { auto* c = static_cast(d); std::vector files = {c->path}; c->self->snapshots_.restore_selected(c->self->current_snapshot_, files); c->self->tray_.show_notification("x3-Backup", "Directory restored: " + c->path); delete c; }), ctx, nullptr, (GConnectFlags)0); } gtk_widget_show_all(menu); gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent*)event); g_free(name); g_free(type); return 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_label_set_text(GTK_LABEL(backup_time_label_), ""); gtk_label_set_text(GTK_LABEL(backup_size_label_), ""); 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; } 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); 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; } } 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); /* 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); 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), "%s", 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); /* Time and size info */ GtkWidget* info_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 16); backup_time_label_ = gtk_label_new(""); gtk_widget_set_halign(backup_time_label_, GTK_ALIGN_START); PangoAttrList* info_attrs = pango_attr_list_new(); pango_attr_list_insert(info_attrs, pango_attr_family_new("monospace")); gtk_label_set_attributes(GTK_LABEL(backup_time_label_), info_attrs); gtk_box_pack_start(GTK_BOX(info_box), backup_time_label_, TRUE, TRUE, 0); backup_size_label_ = gtk_label_new(""); gtk_widget_set_halign(backup_size_label_, GTK_ALIGN_END); gtk_label_set_attributes(GTK_LABEL(backup_size_label_), info_attrs); pango_attr_list_unref(info_attrs); gtk_box_pack_start(GTK_BOX(info_box), backup_size_label_, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(vbox), info_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(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(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_time_label_ = nullptr; backup_size_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_tag_table_lookup(gtk_text_buffer_get_tag_table(buf), tag)) { gtk_text_buffer_insert_with_tags_by_name(buf, &end, line.c_str(), -1, tag, nullptr); } 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(d); self->tray_.set_status("x3-Backup - Creating backup...", true); 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(data); s->snapshots_.create_snapshot(""); return nullptr; }, self); } void MainWindow::start_quick_backup() { on_backup(nullptr, this); } void MainWindow::on_restore(GtkWidget*, gpointer d) { auto* self = static_cast(d); if (self->current_snapshot_.empty()) { GtkWidget* msg = gtk_message_dialog_new(GTK_WINDOW(self->window_), GTK_DIALOG_MODAL, GTK_MESSAGE_WARNING, GTK_BUTTONS_OK, "Please select a snapshot first."); gtk_dialog_run(GTK_DIALOG(msg)); gtk_widget_destroy(msg); return; } GtkTreeSelection* sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(self->file_tree_)); GtkTreeModel* model = nullptr; GtkTreeIter iter; if (!gtk_tree_selection_get_selected(sel, &model, &iter)) { GtkWidget* msg = gtk_message_dialog_new(GTK_WINDOW(self->window_), GTK_DIALOG_MODAL, GTK_MESSAGE_WARNING, GTK_BUTTONS_OK, "Please select a file or directory to restore."); gtk_dialog_run(GTK_DIALOG(msg)); gtk_widget_destroy(msg); return; } gchar* name = nullptr; gtk_tree_model_get(model, &iter, 0, &name, -1); std::string file_path = self->current_path_.empty() ? std::string(name) : self->current_path_ + "/" + name; g_free(name); std::vector files = {file_path}; self->tray_.set_status("x3-Backup - Restoring...", true); self->snapshots_.restore_selected(self->current_snapshot_, files); self->tray_.set_status("x3-Backup - Idle"); self->tray_.show_notification("x3-Backup", "Restored: " + file_path); } void MainWindow::on_delete_snapshot(GtkWidget*, gpointer d) { auto* self = static_cast(d); if (self->current_snapshot_.empty()) return; GtkWidget* dialog = gtk_message_dialog_new(GTK_WINDOW(self->window_), GTK_DIALOG_MODAL, GTK_MESSAGE_WARNING, GTK_BUTTONS_YES_NO, "Delete this snapshot? This cannot be undone."); if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_YES) { std::string name = self->current_snapshot_; self->snapshots_.delete_snapshot(name); self->current_snapshot_ = ""; self->refresh_snapshot_list(); self->tray_.show_notification("x3-Backup", "Snapshot deleted"); } gtk_widget_destroy(dialog); } void MainWindow::on_new_backup(GtkWidget*, gpointer d) { auto* self = static_cast(d); GtkWidget* dialog = gtk_dialog_new_with_buttons( "New Backup", GTK_WINDOW(self->window_), (GtkDialogFlags)(GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT), "_Cancel", GTK_RESPONSE_CANCEL, "_Create", GTK_RESPONSE_ACCEPT, nullptr); GtkWidget* content = gtk_dialog_get_content_area(GTK_DIALOG(dialog)); GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8); gtk_widget_set_margin_start(box, 12); gtk_widget_set_margin_end(box, 12); gtk_widget_set_margin_top(box, 12); GtkWidget* label = gtk_label_new("Description (optional):"); gtk_widget_set_halign(label, GTK_ALIGN_START); gtk_box_pack_start(GTK_BOX(box), label, FALSE, FALSE, 0); GtkWidget* desc_entry = gtk_entry_new(); gtk_entry_set_placeholder_text(GTK_ENTRY(desc_entry), "My backup"); gtk_box_pack_start(GTK_BOX(box), desc_entry, FALSE, FALSE, 0); gtk_container_add(GTK_CONTAINER(content), box); gtk_widget_show_all(dialog); if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) { const char* desc = gtk_entry_get_text(GTK_ENTRY(desc_entry)); std::string description = (desc && *desc) ? desc : ""; self->on_backup(nullptr, self); } gtk_widget_destroy(dialog); } void MainWindow::on_settings(GtkWidget*, gpointer d) { auto* self = static_cast(d); SettingsDialog dlg(GTK_WINDOW(self->window_), self->config_); if (dlg.run()) { self->refresh_snapshot_list(); } } void MainWindow::on_quit(GtkWidget*, gpointer) { gtk_main_quit(); } void MainWindow::on_about(GtkWidget*, gpointer d) { auto* self = static_cast(d); gtk_show_about_dialog(GTK_WINDOW(self->window_), "program-name", "x3-Backup", "version", "1.0.0", "comments", "GTK3 backup/snapshot tool using rsync", "copyright", "2026 x3", "website", "https://git.sysdaemon.xyz/x3/x3-backup", "license-type", GTK_LICENSE_GPL_3_0, nullptr); } void MainWindow::on_navigate_up(GtkWidget*, gpointer d) { static_cast(d)->navigate_up(); } void MainWindow::on_show_log(GtkWidget*, gpointer d) { static_cast(d)->show_log_window(); } void MainWindow::on_system_backup(GtkWidget*, gpointer d) { auto* self = static_cast(d); GtkWidget* dialog = gtk_message_dialog_new(GTK_WINDOW(self->window_), GTK_DIALOG_MODAL, GTK_MESSAGE_WARNING, GTK_BUTTONS_YES_NO, "This will create a full system backup.\n" "Files in /mnt and other exclude paths will be skipped.\n\nContinue?"); if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_YES) { self->start_quick_backup(); } gtk_widget_destroy(dialog); } void MainWindow::on_system_restore(GtkWidget*, gpointer d) { auto* self = static_cast(d); GtkWidget* dialog = gtk_message_dialog_new(GTK_WINDOW(self->window_), GTK_DIALOG_MODAL, GTK_MESSAGE_WARNING, GTK_BUTTONS_YES_NO, "This will restore the ENTIRE system from a snapshot.\n" "All current files will be overwritten!\n\n" "Are you sure you want to continue?"); if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_YES) { if (self->current_snapshot_.empty()) { GtkWidget* msg = gtk_message_dialog_new(GTK_WINDOW(self->window_), GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "Please select a snapshot first."); gtk_dialog_run(GTK_DIALOG(msg)); gtk_widget_destroy(msg); gtk_widget_destroy(dialog); return; } self->snapshots_.restore_full(self->current_snapshot_); self->tray_.show_notification("x3-Backup", "Full system restore completed"); } gtk_widget_destroy(dialog); } void MainWindow::install_cronjob() { if (config_.settings().cron_schedule.empty()) return; std::string label = config_.settings().cron_label; std::string schedule = config_.settings().cron_schedule; std::string cmd = "$(which x3-backup) --backup &"; std::string crontab_cmd = "(crontab -l 2>/dev/null | grep -v '" + label + "'; echo '" + schedule + " " + cmd + " # " + label + "') | crontab -"; system(crontab_cmd.c_str()); } void MainWindow::remove_cronjob() { std::string label = config_.settings().cron_label; std::string cmd = "crontab -l 2>/dev/null | grep -v '" + label + "' | crontab -"; system(cmd.c_str()); } void MainWindow::on_install_cron(GtkWidget*, gpointer d) { auto* self = static_cast(d); self->install_cronjob(); GtkWidget* msg = gtk_message_dialog_new(GTK_WINDOW(self->window_), GTK_DIALOG_MODAL, GTK_MESSAGE_INFO, GTK_BUTTONS_OK, "Cronjob installed: %s\nSchedule: %s", self->config_.settings().cron_label.c_str(), self->config_.settings().cron_schedule.c_str()); gtk_dialog_run(GTK_DIALOG(msg)); gtk_widget_destroy(msg); } void MainWindow::on_remove_cron(GtkWidget*, gpointer d) { auto* self = static_cast(d); self->remove_cronjob(); GtkWidget* msg = gtk_message_dialog_new(GTK_WINDOW(self->window_), GTK_DIALOG_MODAL, GTK_MESSAGE_INFO, GTK_BUTTONS_OK, "Cronjob removed."); gtk_dialog_run(GTK_DIALOG(msg)); gtk_widget_destroy(msg); } gboolean MainWindow::on_tray_close(GtkWidget*, GdkEvent*, gpointer d) { auto* self = static_cast(d); self->hide(); return TRUE; } void MainWindow::run() { gtk_widget_show_all(window_); tray_.set_status("x3-Backup - Idle"); gtk_main(); } void MainWindow::show() { gtk_widget_show_all(window_); gtk_window_present(GTK_WINDOW(window_)); } void MainWindow::hide() { gtk_widget_hide(window_); } bool MainWindow::is_visible() const { return gtk_widget_get_visible(window_); } void MainWindow::append_live_log(const std::string& line, const char* tag) { if (!live_log_textview_) return; GtkTextBuffer* buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(live_log_textview_)); GtkTextIter end; gtk_text_buffer_get_end_iter(buf, &end); if (tag && gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(buf), tag)) { gtk_text_buffer_insert_with_tags_by_name(buf, &end, line.c_str(), -1, tag, nullptr); } else { gtk_text_buffer_insert(buf, &end, line.c_str(), -1); } gtk_text_buffer_insert(buf, &end, "\n", -1); /* Auto-scroll to bottom */ GtkTextView* tv = GTK_TEXT_VIEW(live_log_textview_); 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::load_log_history() { if (!backlog_textview_ || log_path_.empty()) return; std::ifstream f(log_path_); if (!f.is_open()) return; GtkTextBuffer* buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(backlog_textview_)); gtk_text_buffer_set_text(buf, "", -1); std::string line; while (std::getline(f, line)) { GtkTextIter end; gtk_text_buffer_get_end_iter(buf, &end); const char* tag = nullptr; if (line.find("[ERROR]") != std::string::npos || line.find("[CRIT]") != std::string::npos) tag = "error"; else if (line.find("[done]") != std::string::npos || line.find("completed") != std::string::npos) tag = "success"; else if (line.find("[info]") != std::string::npos || line.find("===") != std::string::npos) tag = "info"; if (tag) { gtk_text_buffer_insert_with_tags_by_name(buf, &end, line.c_str(), -1, tag, nullptr); } else { gtk_text_buffer_insert(buf, &end, line.c_str(), -1); } gtk_text_buffer_insert(buf, &end, "\n", -1); } /* Create color tags if not yet */ GtkTextBuffer* lbuf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(live_log_textview_)); if (!gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(lbuf), "error")) { gtk_text_buffer_create_tag(lbuf, "error", "foreground", "#e74c3c", nullptr); gtk_text_buffer_create_tag(lbuf, "success", "foreground", "#2ecc71", nullptr); gtk_text_buffer_create_tag(lbuf, "info", "foreground", "#3498db", nullptr); } if (!gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(buf), "error")) { gtk_text_buffer_create_tag(buf, "error", "foreground", "#e74c3c", nullptr); gtk_text_buffer_create_tag(buf, "success", "foreground", "#2ecc71", nullptr); gtk_text_buffer_create_tag(buf, "info", "foreground", "#3498db", nullptr); } } void MainWindow::show_log_window() { load_log_history(); /* Switch to history log tab */ if (notebook_) { int n = gtk_notebook_get_n_pages(GTK_NOTEBOOK(notebook_)); if (n >= 3) { gtk_notebook_set_current_page(GTK_NOTEBOOK(notebook_), 2); } } }