feat: x3-backup v1.0.0 initial release
GTK3 backup/snapshot tool using rsync backend. Features: - rsync-based snapshots with hardlink delta for space efficiency - Snapshot sidebar with date-sorted list - Full file browser with permissions display (name, size, perms, mtime) - Right-click context menu: Restore file/dir with sudo prompt - System tray icon: backup status, context menu (show/backup/settings/quit) - Menu system: File, Backup, System, Help - Settings dialog: backup destination, source path, retention, exclusions - Cronjob management: install/remove scheduled backups - Live Log tab: real-time rsync output during backup - History Log tab: full log file viewer with color-coded entries - System Backup mode: full root filesystem backup - System Restore mode: restore entire system from snapshot - CLI mode: --backup, --system-backup for cron/headless use - /mnt always excluded from backups - Preserves permissions, ACLs, xattrs (-aAX flags) - Log file: ~/.config/x3-backup/x3-backup.log - Config: ~/.config/x3-backup/settings.json - uninstall.sh for clean removal
This commit is contained in:
@@ -0,0 +1,994 @@
|
||||
#include "main_window.h"
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <sys/stat.h>
|
||||
#include <pwd.h>
|
||||
#include <grp.h>
|
||||
|
||||
static std::string format_size(int64_t bytes) {
|
||||
const char* units[] = {"B", "KB", "MB", "GB", "TB"};
|
||||
int u = 0;
|
||||
double size = bytes;
|
||||
while (size >= 1024 && u < 4) { size /= 1024; u++; }
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "%.1f %s", size, units[u]);
|
||||
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;
|
||||
if (self->progress_bar_) {
|
||||
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());
|
||||
}
|
||||
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 (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->tray_.show_notification("x3-Backup", p.error_message, true);
|
||||
gtk_label_set_text(GTK_LABEL(self->live_log_label_), "Backup failed");
|
||||
} else {
|
||||
self->append_live_log("[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");
|
||||
}
|
||||
self->tray_.set_status("x3-Backup - Idle");
|
||||
}
|
||||
}
|
||||
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();
|
||||
gtk_box_pack_start(GTK_BOX(vbox), toolbar_, FALSE, FALSE, 0);
|
||||
|
||||
build_toolbar();
|
||||
gtk_box_pack_start(GTK_BOX(vbox), 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_top(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);
|
||||
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_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);
|
||||
}
|
||||
|
||||
void 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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
c->self->show_sudo_dialog([c](const std::string& pw) {
|
||||
std::vector<std::string> files = {c->path};
|
||||
c->self->snapshots_.restore_selected(c->self->current_snapshot_, files, pw);
|
||||
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);
|
||||
c->self->show_sudo_dialog([c](const std::string& pw) {
|
||||
std::vector<std::string> files = {c->path};
|
||||
c->self->snapshots_.restore_selected(c->self->current_snapshot_, files, pw);
|
||||
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::show_sudo_dialog(std::function<void(const std::string&)> callback) {
|
||||
GtkWidget* dialog = gtk_dialog_new_with_buttons(
|
||||
"Authentication Required", GTK_WINDOW(window_),
|
||||
(GtkDialogFlags)(GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT),
|
||||
"_Cancel", GTK_RESPONSE_CANCEL,
|
||||
"_OK", 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, 16);
|
||||
gtk_widget_set_margin_end(box, 16);
|
||||
gtk_widget_set_margin_top(box, 16);
|
||||
|
||||
GtkWidget* icon = gtk_image_new_from_icon_name("dialog-password", GTK_ICON_SIZE_DIALOG);
|
||||
gtk_box_pack_start(GTK_BOX(box), icon, FALSE, FALSE, 0);
|
||||
|
||||
GtkWidget* label = gtk_label_new("Root access is required to restore files.\nEnter your sudo password:");
|
||||
gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
|
||||
gtk_box_pack_start(GTK_BOX(box), label, FALSE, FALSE, 0);
|
||||
|
||||
GtkWidget* pass_entry = gtk_entry_new();
|
||||
gtk_entry_set_visibility(GTK_ENTRY(pass_entry), FALSE);
|
||||
gtk_entry_set_placeholder_text(GTK_ENTRY(pass_entry), "Password");
|
||||
gtk_box_pack_start(GTK_BOX(box), pass_entry, FALSE, FALSE, 0);
|
||||
|
||||
gtk_container_add(GTK_CONTAINER(content), box);
|
||||
gtk_widget_show_all(dialog);
|
||||
gtk_widget_grab_focus(pass_entry);
|
||||
|
||||
if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
|
||||
const char* pw = gtk_entry_get_text(GTK_ENTRY(pass_entry));
|
||||
if (pw && *pw) {
|
||||
callback(std::string(pw));
|
||||
}
|
||||
}
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
GtkWidget* label = gtk_label_new(title.c_str());
|
||||
gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 0);
|
||||
|
||||
progress_bar_ = gtk_progress_bar_new();
|
||||
gtk_box_pack_start(GTK_BOX(vbox), progress_bar_, FALSE, FALSE, 0);
|
||||
|
||||
progress_label_ = gtk_label_new("Starting...");
|
||||
gtk_box_pack_start(GTK_BOX(vbox), progress_label_, FALSE, FALSE, 0);
|
||||
|
||||
gtk_widget_show_all(progress_window_);
|
||||
}
|
||||
|
||||
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...");
|
||||
|
||||
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);
|
||||
|
||||
self->show_sudo_dialog([self, file_path](const std::string& pw) {
|
||||
std::vector<std::string> files = {file_path};
|
||||
self->tray_.set_status("x3-Backup - Restoring...", true);
|
||||
self->snapshots_.restore_selected(self->current_snapshot_, files, pw);
|
||||
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->show_sudo_dialog([self](const std::string& pw) {
|
||||
self->snapshots_.restore_full(self->current_snapshot_, pw);
|
||||
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_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 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);
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user