Files
x3-backup/src/main_window.cpp
T
x3 4bff4f3db2 refactor: run as root via pkexec, remove sudo handling
- Add privilege escalation at startup (pkexec if not root)
- Remove all sudo password handling (echo password | sudo -S)
- Remove show_sudo_dialog and current_sudo_password_
- Remove --sudo-password CLI option
- All rsync/restore operations run directly as root
- format_size: use GB as default unit for large files
- Remove [cmd] log leaking sudo password
- Simplify snapshot_manager: no sudo_password parameter
- Simplify rsync_manager: no sudo_password parameter
2026-09-08 09:16:31 +00:00

1150 lines
45 KiB
C++

#include "main_window.h"
#include <sstream>
#include <iomanip>
#include <cstring>
#include <fstream>
#include <sys/stat.h>
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<Ctx*>(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), "<b>Snapshots</b>");
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<MainWindow*>(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<MainWindow*>(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<MainWindow*>(d);
self->load_log_history();
}), this);
g_signal_connect(clear_hist_btn, "clicked", G_CALLBACK(+[](GtkWidget*, gpointer d) {
auto* self = static_cast<MainWindow*>(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<MainWindow*>(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<MainWindow*>(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<MainWindow*>(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<RestoreCtx*>(d);
std::vector<std::string> 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<RestoreCtx*>(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<RestoreCtx*>(d);
std::vector<std::string> 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), "<b><big>%s</big></b>", 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<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_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<MainWindow*>(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<MainWindow*>(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<MainWindow*>(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<std::string> 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<MainWindow*>(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<MainWindow*>(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<MainWindow*>(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<MainWindow*>(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<MainWindow*>(d)->navigate_up();
}
void MainWindow::on_show_log(GtkWidget*, gpointer d) {
static_cast<MainWindow*>(d)->show_log_window();
}
void MainWindow::on_system_backup(GtkWidget*, gpointer d) {
auto* self = static_cast<MainWindow*>(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<MainWindow*>(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<MainWindow*>(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<MainWindow*>(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<MainWindow*>(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);
}
}
}