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:
x3
2026-09-07 17:20:41 +00:00
commit c8ef700612
20 changed files with 2535 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
# Autotools generated
/configure
/Makefile
/Makefile.in
/src/Makefile
/src/Makefile.in
/build-aux/
autom4te.cache/
config.log
config.status
*.o
*.d
x3-backup
.deps/
# Editor
*~
*.swp
.vscode/
.idea/
# Build
build/
obj/
+15
View File
@@ -0,0 +1,15 @@
SUBDIRS = src
EXTRA_DIST = desktop/x3-backup.desktop icons
install-data-local:
$(MKDIR_P) $(DESTDIR)$(datadir)/applications
$(INSTALL_DATA) desktop/x3-backup.desktop $(DESTDIR)$(datadir)/applications/x3-backup.desktop
$(MKDIR_P) $(DESTDIR)$(datadir)/icons/hicolor/256x256/apps
$(INSTALL_DATA) icons/x3-backup.png $(DESTDIR)$(datadir)/icons/hicolor/256x256/apps/x3-backup.png
uninstall-local:
rm -f $(DESTDIR)$(datadir)/applications/x3-backup.desktop
rm -f $(DESTDIR)$(datadir)/icons/hicolor/256x256/apps/x3-backup.png
CLEANFILES = *~
+19
View File
@@ -0,0 +1,19 @@
AC_INIT([x3-backup], [1.0.0], [x3@sysdaemon.xyz])
AC_CONFIG_SRCDIR([src/main.cpp])
AC_CONFIG_AUX_DIR([build-aux])
AM_INIT_AUTOMAKE([foreign -Wall -Werror subdir-objects])
AC_PROG_CXX
AX_CXX_COMPILE_STDCXX([17], [noext], [mandatory])
PKG_CHECK_MODULES([GTK], [gtk+-3.0 >= 3.24])
PKG_CHECK_MODULES([JSON_GLIB], [json-glib-1.0])
AC_CHECK_PROG([RSYNC], [rsync], [yes], [no])
AS_IF([test "x$RSYNC" = "xno"], [AC_MSG_ERROR([rsync is required])])
AC_CHECK_PROG([CRONTAB], [crontab], [yes], [no])
AC_CHECK_PROG([PKEXEC], [pkexec], [yes], [no])
AC_CONFIG_FILES([Makefile src/Makefile])
AC_OUTPUT
+9
View File
@@ -0,0 +1,9 @@
[Desktop Entry]
Type=Application
Name=x3-Backup
Comment=GTK3 Backup/Snapshot Tool using rsync
Exec=x3-backup
Icon=x3-backup
Terminal=false
Categories=System;Utility;
Keywords=backup;snapshot;rsync;restore;
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

+22
View File
@@ -0,0 +1,22 @@
bin_PROGRAMS = x3-backup
x3_backup_SOURCES = \
main.cpp \
config.cpp \
rsync_manager.cpp \
snapshot_manager.cpp \
main_window.cpp \
settings_dialog.cpp \
system_tray.cpp
x3_backup_CXXFLAGS = \
-std=c++17 \
$(GTK_CFLAGS) \
$(JSON_GLIB_CFLAGS) \
-DLOCALEDIR=\"$(localedir)\" \
-DBINDIR=\"$(bindir)\"
x3_backup_LDADD = \
$(GTK_LIBS) \
$(JSON_GLIB_LIBS) \
-lstdc++fs
+135
View File
@@ -0,0 +1,135 @@
#include "config.h"
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <json-glib/json-glib.h>
Config::Config() {
const char* home = getenv("HOME");
if (!home) home = ".";
config_dir_ = std::string(home) + "/.config/x3-backup";
ensure_dirs();
}
void Config::ensure_dirs() {
std::string cmd = "mkdir -p \"" + config_dir_ + "\"";
system(cmd.c_str());
}
void Config::load() {
std::string path = config_file();
FILE* f = fopen(path.c_str(), "r");
if (!f) return;
fseek(f, 0, SEEK_END);
long len = ftell(f);
fseek(f, 0, SEEK_SET);
char* buf = new char[len + 1];
fread(buf, 1, len, f);
buf[len] = 0;
fclose(f);
JsonParser* parser = json_parser_new();
GError* err = nullptr;
if (!json_parser_load_from_data(parser, buf, len, &err)) {
g_warning("Config parse error: %s", err->message);
g_error_free(err);
g_object_unref(parser);
delete[] buf;
return;
}
delete[] buf;
JsonObject* root = json_node_get_object(json_parser_get_root(parser));
if (json_object_has_member(root, "backup_dest"))
settings_.backup_dest = json_object_get_string_member(root, "backup_dest");
if (json_object_has_member(root, "source_path"))
settings_.source_path = json_object_get_string_member(root, "source_path");
if (json_object_has_member(root, "preserve_permissions"))
settings_.preserve_permissions = json_object_get_boolean_member(root, "preserve_permissions");
if (json_object_has_member(root, "preserve_acls"))
settings_.preserve_acls = json_object_get_boolean_member(root, "preserve_acls");
if (json_object_has_member(root, "preserve_xattrs"))
settings_.preserve_xattrs = json_object_get_boolean_member(root, "preserve_xattrs");
if (json_object_has_member(root, "delete_removed"))
settings_.delete_removed = json_object_get_boolean_member(root, "delete_removed");
if (json_object_has_member(root, "retention_days"))
settings_.retention_days = (int)json_object_get_int_member(root, "retention_days");
if (json_object_has_member(root, "retention_count"))
settings_.retention_count = (int)json_object_get_int_member(root, "retention_count");
if (json_object_has_member(root, "cron_enabled"))
settings_.cron_enabled = json_object_get_boolean_member(root, "cron_enabled");
if (json_object_has_member(root, "cron_schedule"))
settings_.cron_schedule = json_object_get_string_member(root, "cron_schedule");
if (json_object_has_member(root, "cron_label"))
settings_.cron_label = json_object_get_string_member(root, "cron_label");
if (json_object_has_member(root, "exclude_patterns")) {
JsonArray* arr = json_object_get_array_member(root, "exclude_patterns");
settings_.exclude_patterns.clear();
for (guint i = 0; i < json_array_get_length(arr); i++) {
settings_.exclude_patterns.push_back(json_array_get_string_element(arr, i));
}
}
g_object_unref(parser);
}
void Config::save() {
JsonBuilder* builder = json_builder_new();
json_builder_begin_object(builder);
json_builder_set_member_name(builder, "backup_dest");
json_builder_add_string_value(builder, settings_.backup_dest.c_str());
json_builder_set_member_name(builder, "source_path");
json_builder_add_string_value(builder, settings_.source_path.c_str());
json_builder_set_member_name(builder, "preserve_permissions");
json_builder_add_boolean_value(builder, settings_.preserve_permissions);
json_builder_set_member_name(builder, "preserve_acls");
json_builder_add_boolean_value(builder, settings_.preserve_acls);
json_builder_set_member_name(builder, "preserve_xattrs");
json_builder_add_boolean_value(builder, settings_.preserve_xattrs);
json_builder_set_member_name(builder, "delete_removed");
json_builder_add_boolean_value(builder, settings_.delete_removed);
json_builder_set_member_name(builder, "retention_days");
json_builder_add_int_value(builder, settings_.retention_days);
json_builder_set_member_name(builder, "retention_count");
json_builder_add_int_value(builder, settings_.retention_count);
json_builder_set_member_name(builder, "cron_enabled");
json_builder_add_boolean_value(builder, settings_.cron_enabled);
json_builder_set_member_name(builder, "cron_schedule");
json_builder_add_string_value(builder, settings_.cron_schedule.c_str());
json_builder_set_member_name(builder, "cron_label");
json_builder_add_string_value(builder, settings_.cron_label.c_str());
json_builder_set_member_name(builder, "exclude_patterns");
json_builder_begin_array(builder);
for (const auto& e : settings_.exclude_patterns) {
json_builder_add_string_value(builder, e.c_str());
}
json_builder_end_array(builder);
json_builder_end_object(builder);
JsonGenerator* gen = json_generator_new();
json_generator_set_root(gen, json_builder_get_root(builder));
json_generator_set_pretty(gen, TRUE);
json_generator_set_indent(gen, 2);
std::string path = config_file();
json_generator_to_file(gen, path.c_str(), nullptr);
g_object_unref(gen);
g_object_unref(builder);
}
+44
View File
@@ -0,0 +1,44 @@
#pragma once
#include <string>
#include <vector>
#include <gtk/gtk.h>
struct BackupSettings {
std::string backup_dest = "/mnt/backups";
std::string source_path = "/";
std::vector<std::string> exclude_patterns = {"/mnt", "/proc", "/sys", "/dev", "/run", "/tmp", "/lost+found"};
bool preserve_permissions = true;
bool preserve_acls = true;
bool preserve_xattrs = true;
bool delete_removed = true;
int retention_days = 30;
int retention_count = 10;
bool cron_enabled = false;
std::string cron_schedule = "0 2 * * *";
std::string cron_label = "x3-backup-auto";
};
class Config {
public:
Config();
void load();
void save();
BackupSettings& settings() { return settings_; }
const BackupSettings& settings() const { return settings_; }
std::string config_dir() const { return config_dir_; }
std::string config_file() const { return config_dir_ + "/settings.json"; }
std::string snapshots_dir() const { return settings_.backup_dest + "/snapshots"; }
std::string metadata_dir() const { return settings_.backup_dest + "/metadata"; }
bool is_system_backup_mode() const { return system_backup_mode_; }
void set_system_backup_mode(bool v) { system_backup_mode_ = v; }
private:
BackupSettings settings_;
std::string config_dir_;
bool system_backup_mode_ = false;
void ensure_dirs();
};
+143
View File
@@ -0,0 +1,143 @@
#include <gtk/gtk.h>
#include <glib.h>
#include <cstdlib>
#include <cstring>
#include <cstdio>
#include <ctime>
#include <fstream>
#include <string>
#include "config.h"
#include "rsync_manager.h"
#include "snapshot_manager.h"
#include "main_window.h"
static FILE* log_file = nullptr;
static bool log_to_stdout = false;
static std::string get_log_path() {
const char* home = getenv("HOME");
if (!home) home = ".";
std::string dir = std::string(home) + "/.config/x3-backup";
std::string cmd = "mkdir -p \"" + dir + "\"";
(void)system(cmd.c_str());
return dir + "/x3-backup.log";
}
static void log_handler(const gchar* domain, GLogLevelFlags level, const gchar* message, gpointer) {
const char* level_str = "INFO";
if (level & G_LOG_LEVEL_WARNING) level_str = "WARN";
if (level & G_LOG_LEVEL_CRITICAL) level_str = "CRIT";
if (level & G_LOG_LEVEL_ERROR) level_str = "ERROR";
time_t now = time(nullptr);
struct tm* t = localtime(&now);
char timebuf[32];
strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S", t);
char line[2048];
snprintf(line, sizeof(line), "[%s] [%s] %s: %s\n", timebuf, level_str, domain ? domain : "app", message);
if (log_to_stdout) {
fputs(line, stdout);
fflush(stdout);
}
if (log_file) {
fputs(line, log_file);
fflush(log_file);
}
}
static void setup_logging(bool cli_mode) {
const char* env_log = g_getenv("X3_BACKUP_LOG");
if (env_log && strcmp(env_log, "1") == 0) {
log_to_stdout = true;
}
if (cli_mode) {
log_to_stdout = true;
}
std::string path = get_log_path();
log_file = fopen(path.c_str(), "a");
if (!log_file) {
g_warning("Cannot open log file: %s", path.c_str());
}
g_log_set_handler("x3-backup", (GLogLevelFlags)(G_LOG_LEVEL_MASK), log_handler, nullptr);
g_log_set_handler(nullptr, (GLogLevelFlags)(G_LOG_LEVEL_MASK), log_handler, nullptr);
g_message("=== x3-backup started ===");
g_message("Log file: %s", path.c_str());
}
int main(int argc, char* argv[]) {
gtk_init(&argc, &argv);
bool system_mode = false;
bool backup_now = false;
bool show_help = false;
bool cli_mode = false;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--system-backup") == 0) {
system_mode = true;
backup_now = true;
cli_mode = true;
} else if (strcmp(argv[i], "--backup") == 0) {
backup_now = true;
cli_mode = true;
} else if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
show_help = true;
} else if (strcmp(argv[i], "--log") == 0) {
log_to_stdout = true;
}
}
setup_logging(cli_mode);
if (show_help) {
g_print("x3-Backup - GTK3 Backup/Snapshot Tool\n\n");
g_print("Usage: x3-backup [OPTIONS]\n\n");
g_print("Options:\n");
g_print(" --backup Create a backup immediately and exit\n");
g_print(" --system-backup Create a full system backup and exit\n");
g_print(" --log Print log to stdout\n");
g_print(" --help, -h Show this help\n\n");
g_print("Without options, x3-backup starts in GUI mode with system tray.\n\n");
g_print("Log file: ~/.config/x3-backup/x3-backup.log\n");
return 0;
}
Config config;
config.load();
if (backup_now) {
if (system_mode) {
config.set_system_backup_mode(true);
config.settings().source_path = "/";
g_message("System backup mode: backing up entire system");
}
RsyncManager rsync(config);
SnapshotManager snapshots(config, rsync);
g_message("Starting backup...");
snapshots.cleanup_old_snapshots();
std::string ts = snapshots.create_snapshot(system_mode ? "System backup" : "Manual backup");
if (!ts.empty()) {
g_message("Backup completed: %s", ts.c_str());
} else {
g_warning("Backup failed!");
return 1;
}
return 0;
}
MainWindow window(config);
window.run();
g_message("=== x3-backup exiting ===");
if (log_file) fclose(log_file);
return 0;
}
+994
View File
@@ -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);
}
}
}
+106
View File
@@ -0,0 +1,106 @@
#pragma once
#include <gtk/gtk.h>
#include <string>
#include <vector>
#include <fstream>
#include "config.h"
#include "rsync_manager.h"
#include "snapshot_manager.h"
#include "system_tray.h"
#include "settings_dialog.h"
class MainWindow {
public:
MainWindow(Config& config);
~MainWindow();
GtkWidget* widget() const { return window_; }
void run();
void show();
void hide();
bool is_visible() const;
void start_quick_backup();
SystemTray& tray() { return tray_; }
private:
void build_ui();
void build_menu();
void build_toolbar();
void build_sidebar();
void build_file_browser();
void build_statusbar();
void refresh_snapshot_list();
void load_selected_snapshot();
void browse_path(const std::string& snapshot, const std::string& path);
void navigate_up();
void on_backup_clicked();
void on_restore_clicked();
void on_delete_snapshot_clicked();
void on_new_backup_clicked();
void show_sudo_dialog(std::function<void(const std::string&)> callback);
void show_progress_dialog(const std::string& title);
void install_cronjob();
void remove_cronjob();
void show_log_window();
void append_live_log(const std::string& line, const char* tag = nullptr);
void load_log_history();
static void on_backup(GtkWidget* w, gpointer d);
static void on_restore(GtkWidget* w, gpointer d);
static void on_delete_snapshot(GtkWidget* w, gpointer d);
static void on_new_backup(GtkWidget* w, gpointer d);
static void on_settings(GtkWidget* w, gpointer d);
static void on_quit(GtkWidget* w, gpointer d);
static void on_about(GtkWidget* w, gpointer d);
static void on_show_log(GtkWidget* w, gpointer d);
static void on_snapshot_selected(GtkListBox* box, GtkListBoxRow* row, gpointer d);
static void on_file_row_activated(GtkTreeView* tree, GtkTreePath* path, GtkTreeViewColumn* col, gpointer d);
static gboolean on_file_button_press(GtkWidget* widget, GdkEventButton* event, gpointer d);
static void on_navigate_up(GtkWidget* w, gpointer d);
static void on_system_backup(GtkWidget* w, gpointer d);
static void on_system_restore(GtkWidget* w, gpointer d);
static void on_install_cron(GtkWidget* w, gpointer d);
static void on_remove_cron(GtkWidget* w, gpointer d);
static gboolean on_tray_close(GtkWidget* widget, GdkEvent* event, gpointer d);
Config& config_;
RsyncManager rsync_;
SnapshotManager snapshots_;
SystemTray tray_;
GtkWidget* window_ = nullptr;
GtkWidget* notebook_ = nullptr;
GtkWidget* snapshot_listbox_ = nullptr;
GtkWidget* file_tree_ = nullptr;
GtkWidget* path_entry_ = nullptr;
GtkWidget* statusbar_ = nullptr;
GtkWidget* progress_bar_ = nullptr;
GtkWidget* progress_label_ = nullptr;
GtkWidget* progress_window_ = nullptr;
GtkWidget* nav_label_ = nullptr;
GtkListStore* file_store_ = nullptr;
GtkWidget* toolbar_ = nullptr;
GtkWidget* log_window_ = nullptr;
GtkWidget* log_textview_ = nullptr;
guint log_timer_ = 0;
GtkWidget* live_log_textview_ = nullptr;
GtkWidget* live_log_label_ = nullptr;
GtkWidget* backlog_textview_ = nullptr;
GtkWidget* backlog_scrolled_ = nullptr;
std::string log_path_;
std::string current_snapshot_;
std::string current_path_;
std::vector<std::string> path_stack_;
int col_name = 0, col_size = 1, col_type = 2, col_permissions = 3, col_mtime = 4;
};
+265
View File
@@ -0,0 +1,265 @@
#include "rsync_manager.h"
#include <cstdio>
#include <cstring>
#include <sstream>
#include <regex>
#include <unistd.h>
#include <sys/wait.h>
RsyncManager::RsyncManager(Config& config) : config_(config) {}
std::vector<std::string> RsyncManager::build_exclude_args() {
std::vector<std::string> args;
for (const auto& e : config_.settings().exclude_patterns) {
args.push_back("--exclude=" + e);
}
return args;
}
bool RsyncManager::create_snapshot(const std::string& snapshot_name,
const std::string& source,
const std::string& dest,
const std::string& link_dest) {
if (running_) return false;
cancel_requested_ = false;
running_ = true;
{
std::lock_guard<std::mutex> lock(log_mutex_);
log_buffer_.clear();
}
std::string target = dest + "/" + snapshot_name;
std::string cmd = "rsync -aAX --info=progress2";
if (config_.settings().delete_removed)
cmd += " --delete";
if (!link_dest.empty())
cmd += " --link-dest=\"" + link_dest + "\"";
for (const auto& e : config_.settings().exclude_patterns) {
cmd += " --exclude=\"" + e + "\"";
}
cmd += " \"" + source + "/\" \"" + target + "/\" 2>&1";
g_message("rsync cmd: %s", cmd.c_str());
if (on_progress) {
RsyncProgress p;
p.log_line = "[cmd] " + cmd;
on_progress(p);
}
FILE* pipe = popen(cmd.c_str(), "r");
if (!pipe) {
running_ = false;
return false;
}
RsyncProgress prog;
char buf[4096];
while (fgets(buf, sizeof(buf), pipe)) {
if (cancel_requested_) break;
std::string line(buf);
/* Remove trailing newline */
if (!line.empty() && line.back() == '\n') line.pop_back();
if (!line.empty() && line.back() == '\r') line.pop_back();
{
std::lock_guard<std::mutex> lock(log_mutex_);
log_buffer_.push_back(line);
}
parse_progress_line(line, prog);
prog.log_line = line;
if (on_progress) on_progress(prog);
}
int status = pclose(pipe);
running_ = false;
if (cancel_requested_) {
if (on_progress) {
RsyncProgress p;
p.error = true;
p.error_message = "Cancelled by user";
p.finished = true;
p.log_line = "[cancelled] Operation cancelled by user";
on_progress(p);
}
return false;
}
if (WEXITSTATUS(status) != 0) {
if (on_progress) {
RsyncProgress p;
p.error = true;
p.error_message = "rsync exited with status " + std::to_string(WEXITSTATUS(status));
p.finished = true;
p.log_line = "[error] rsync exited with status " + std::to_string(WEXITSTATUS(status));
on_progress(p);
}
return false;
}
if (on_progress) {
prog.finished = true;
prog.log_line = "[done] Snapshot created: " + snapshot_name;
on_progress(prog);
}
return true;
}
bool RsyncManager::restore_files(const std::string& snapshot_path,
const std::vector<std::string>& files,
const std::string& restore_dest,
const std::string& sudo_password) {
if (running_) return false;
cancel_requested_ = false;
running_ = true;
{
std::lock_guard<std::mutex> lock(log_mutex_);
log_buffer_.clear();
}
bool success = true;
int total = files.size();
int current = 0;
for (const auto& f : files) {
if (cancel_requested_) break;
current++;
std::string src = snapshot_path + "/" + f;
std::string dst = restore_dest + "/" + f;
std::string cmd = "echo '" + sudo_password + "' | sudo -S rsync -aAX \"" + src + "\" \"" + dst + "\" 2>&1";
g_message("restore cmd: %s", cmd.c_str());
if (on_progress) {
RsyncProgress p;
p.log_line = "[restore] (" + std::to_string(current) + "/" + std::to_string(total) + ") " + f;
p.files_transferred = current;
p.files_total = total;
on_progress(p);
}
FILE* pipe = popen(cmd.c_str(), "r");
if (pipe) {
char rbuf[512];
while (fgets(rbuf, sizeof(rbuf), pipe)) {
std::string line(rbuf);
if (!line.empty() && line.back() == '\n') line.pop_back();
{
std::lock_guard<std::mutex> lock(log_mutex_);
log_buffer_.push_back(line);
}
}
int status = pclose(pipe);
if (WEXITSTATUS(status) != 0) {
success = false;
g_warning("Failed to restore: %s", f.c_str());
if (on_progress) {
RsyncProgress p;
p.log_line = "[error] Failed to restore: " + f;
on_progress(p);
}
}
}
}
running_ = false;
if (on_progress) {
RsyncProgress p;
p.finished = true;
p.log_line = success ? "[done] Restore completed" : "[done] Restore completed with errors";
p.error = !success;
on_progress(p);
}
return success;
}
bool RsyncManager::restore_full_snapshot(const std::string& snapshot_path,
const std::string& restore_dest,
const std::string& sudo_password) {
if (running_) return false;
cancel_requested_ = false;
running_ = true;
{
std::lock_guard<std::mutex> lock(log_mutex_);
log_buffer_.clear();
}
std::string cmd = "echo '" + sudo_password + "' | sudo -S rsync -aAX --delete \""
+ snapshot_path + "/\" \"" + restore_dest + "/\" 2>&1";
g_message("full restore cmd: %s", cmd.c_str());
if (on_progress) {
RsyncProgress p;
p.log_line = "[restore-full] Restoring entire system from " + snapshot_path;
on_progress(p);
}
FILE* pipe = popen(cmd.c_str(), "r");
if (!pipe) {
running_ = false;
return false;
}
char buf[4096];
while (fgets(buf, sizeof(buf), pipe)) {
if (cancel_requested_) break;
std::string line(buf);
if (!line.empty() && line.back() == '\n') line.pop_back();
{
std::lock_guard<std::mutex> lock(log_mutex_);
log_buffer_.push_back(line);
}
if (on_progress) {
RsyncProgress p;
p.log_line = line;
on_progress(p);
}
}
int status = pclose(pipe);
running_ = false;
if (on_progress) {
RsyncProgress p;
p.finished = true;
p.error = (WEXITSTATUS(status) != 0);
if (p.error) p.error_message = "Restore failed with status " + std::to_string(WEXITSTATUS(status));
p.log_line = p.error ? "[error] Full restore failed" : "[done] Full system restore completed";
on_progress(p);
}
return WEXITSTATUS(status) == 0;
}
void RsyncManager::cancel() {
cancel_requested_ = true;
}
void RsyncManager::parse_progress_line(const std::string& line, RsyncProgress& prog) {
std::regex re_pct(R"((\d+)%)");
std::regex re_speed(R"(([\d.]+)[kmgt]?/s)");
std::regex re_file(R"(\s(\S+)$)");
std::smatch m;
if (std::regex_search(line, m, re_pct)) {
prog.bytes_transferred = std::stoll(m[1].str());
}
if (std::regex_search(line, m, re_speed)) {
prog.speed = (int)std::stod(m[1].str());
}
}
+57
View File
@@ -0,0 +1,57 @@
#pragma once
#include <string>
#include <vector>
#include <functional>
#include <atomic>
#include <thread>
#include <mutex>
#include "config.h"
struct RsyncProgress {
int64_t files_transferred = 0;
int64_t files_total = 0;
int64_t bytes_transferred = 0;
int64_t bytes_total = 0;
int speed = 0;
std::string current_file;
std::string log_line;
bool finished = false;
bool error = false;
std::string error_message;
};
class RsyncManager {
public:
RsyncManager(Config& config);
bool create_snapshot(const std::string& snapshot_name,
const std::string& source,
const std::string& dest,
const std::string& link_dest = "");
bool restore_files(const std::string& snapshot_path,
const std::vector<std::string>& files,
const std::string& restore_dest,
const std::string& sudo_password);
bool restore_full_snapshot(const std::string& snapshot_path,
const std::string& restore_dest,
const std::string& sudo_password);
void cancel();
bool is_running() const { return running_; }
std::function<void(const RsyncProgress&)> on_progress;
private:
Config& config_;
std::atomic<bool> running_{false};
std::atomic<bool> cancel_requested_{false};
std::thread worker_;
std::mutex log_mutex_;
std::vector<std::string> log_buffer_;
std::vector<std::string> build_exclude_args();
bool run_rsync(const std::vector<std::string>& args);
void parse_progress_line(const std::string& line, RsyncProgress& prog);
};
+193
View File
@@ -0,0 +1,193 @@
#include "settings_dialog.h"
#include <sstream>
SettingsDialog::SettingsDialog(GtkWindow* parent, Config& config)
: parent_(parent), config_(config) {}
void SettingsDialog::populate_excludes() {
GtkTextBuffer* buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(exclude_textview_));
std::string text;
for (const auto& e : config_.settings().exclude_patterns) {
if (!text.empty()) text += "\n";
text += e;
}
gtk_text_buffer_set_text(buf, text.c_str(), -1);
}
void SettingsDialog::save_excludes() {
GtkTextBuffer* buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(exclude_textview_));
GtkTextIter start, end;
gtk_text_buffer_get_bounds(buf, &start, &end);
char* text = gtk_text_buffer_get_text(buf, &start, &end, FALSE);
config_.settings().exclude_patterns.clear();
std::istringstream iss(text);
std::string line;
while (std::getline(iss, line)) {
if (!line.empty()) {
config_.settings().exclude_patterns.push_back(line);
}
}
g_free(text);
}
bool SettingsDialog::run() {
GtkWidget* dialog = gtk_dialog_new_with_buttons(
"Settings", parent_,
(GtkDialogFlags)(GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT),
"_Cancel", GTK_RESPONSE_CANCEL,
"_Save", GTK_RESPONSE_ACCEPT,
nullptr);
GtkWidget* content = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
GtkWidget* notebook = gtk_notebook_new();
gtk_widget_set_margin_start(notebook, 8);
gtk_widget_set_margin_end(notebook, 8);
gtk_widget_set_margin_top(notebook, 8);
/* ── General Tab ── */
GtkWidget* gen_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
gtk_widget_set_margin_start(gen_box, 8);
gtk_widget_set_margin_end(gen_box, 8);
gtk_widget_set_margin_top(gen_box, 8);
GtkWidget* dest_hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
GtkWidget* dest_label = gtk_label_new("Backup Destination:");
gtk_box_pack_start(GTK_BOX(dest_hbox), dest_label, FALSE, FALSE, 0);
dest_entry_ = gtk_entry_new();
gtk_entry_set_text(GTK_ENTRY(dest_entry_), config_.settings().backup_dest.c_str());
gtk_widget_set_hexpand(dest_entry_, TRUE);
gtk_box_pack_start(GTK_BOX(dest_hbox), dest_entry_, TRUE, TRUE, 0);
GtkWidget* dest_btn = gtk_button_new_with_label("Browse...");
g_signal_connect(dest_btn, "clicked", G_CALLBACK(+[](GtkWidget*, gpointer d) {
auto* self = static_cast<SettingsDialog*>(d);
GtkWidget* fc = gtk_file_chooser_dialog_new("Select Backup Destination",
self->parent_, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER,
"_Cancel", GTK_RESPONSE_CANCEL, "_Select", GTK_RESPONSE_ACCEPT, nullptr);
if (gtk_dialog_run(GTK_DIALOG(fc)) == GTK_RESPONSE_ACCEPT) {
char* path = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(fc));
if (path) {
gtk_entry_set_text(GTK_ENTRY(self->dest_entry_), path);
g_free(path);
}
}
gtk_widget_destroy(fc);
}), this);
gtk_box_pack_start(GTK_BOX(dest_hbox), dest_btn, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(gen_box), dest_hbox, FALSE, FALSE, 0);
GtkWidget* src_hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
GtkWidget* src_label = gtk_label_new("Source Path:");
gtk_box_pack_start(GTK_BOX(src_hbox), src_label, FALSE, FALSE, 0);
source_entry_ = gtk_entry_new();
gtk_entry_set_text(GTK_ENTRY(source_entry_), config_.settings().source_path.c_str());
gtk_widget_set_hexpand(source_entry_, TRUE);
gtk_box_pack_start(GTK_BOX(src_hbox), source_entry_, TRUE, TRUE, 0);
gtk_box_pack_start(GTK_BOX(gen_box), src_hbox, FALSE, FALSE, 0);
GtkWidget* ret_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
GtkWidget* ret_days_label = gtk_label_new("Keep snapshots (days):");
gtk_box_pack_start(GTK_BOX(ret_box), ret_days_label, FALSE, FALSE, 0);
retention_days_spin_ = gtk_spin_button_new_with_range(0, 365, 1);
gtk_spin_button_set_value(GTK_SPIN_BUTTON(retention_days_spin_), config_.settings().retention_days);
gtk_box_pack_start(GTK_BOX(ret_box), retention_days_spin_, FALSE, FALSE, 0);
GtkWidget* ret_count_label = gtk_label_new("Max snapshots:");
gtk_box_pack_start(GTK_BOX(ret_box), ret_count_label, FALSE, FALSE, 0);
retention_count_spin_ = gtk_spin_button_new_with_range(0, 100, 1);
gtk_spin_button_set_value(GTK_SPIN_BUTTON(retention_count_spin_), config_.settings().retention_count);
gtk_box_pack_start(GTK_BOX(ret_box), retention_count_spin_, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(gen_box), ret_box, FALSE, FALSE, 0);
delete_check_ = gtk_check_button_new_with_label("Delete files from backup that are no longer in source");
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(delete_check_), config_.settings().delete_removed);
gtk_box_pack_start(GTK_BOX(gen_box), delete_check_, FALSE, FALSE, 0);
gtk_notebook_append_page(GTK_NOTEBOOK(notebook), gen_box, gtk_label_new("General"));
/* ── Permissions Tab ── */
GtkWidget* perm_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
gtk_widget_set_margin_start(perm_box, 8);
gtk_widget_set_margin_top(perm_box, 8);
acl_check_ = gtk_check_button_new_with_label("Preserve ACLs (Access Control Lists)");
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(acl_check_), config_.settings().preserve_acls);
gtk_box_pack_start(GTK_BOX(perm_box), acl_check_, FALSE, FALSE, 0);
xattr_check_ = gtk_check_button_new_with_label("Preserve extended attributes (xattrs)");
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(xattr_check_), config_.settings().preserve_xattrs);
gtk_box_pack_start(GTK_BOX(perm_box), xattr_check_, FALSE, FALSE, 0);
gtk_notebook_append_page(GTK_NOTEBOOK(notebook), perm_box, gtk_label_new("Permissions"));
/* ── Excludes Tab ── */
GtkWidget* excl_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
gtk_widget_set_margin_start(excl_box, 8);
gtk_widget_set_margin_top(excl_box, 8);
GtkWidget* excl_label = gtk_label_new("Exclude patterns (one per line):");
gtk_box_pack_start(GTK_BOX(excl_box), excl_label, FALSE, FALSE, 0);
GtkWidget* scroll = gtk_scrolled_window_new(nullptr, nullptr);
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
gtk_widget_set_size_request(scroll, 400, 200);
exclude_textview_ = gtk_text_view_new();
gtk_text_view_set_monospace(GTK_TEXT_VIEW(exclude_textview_), TRUE);
gtk_container_add(GTK_CONTAINER(scroll), exclude_textview_);
gtk_box_pack_start(GTK_BOX(excl_box), scroll, TRUE, TRUE, 0);
populate_excludes();
gtk_notebook_append_page(GTK_NOTEBOOK(notebook), excl_box, gtk_label_new("Excludes"));
/* ── Cronjob Tab ── */
GtkWidget* cron_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
gtk_widget_set_margin_start(cron_box, 8);
gtk_widget_set_margin_top(cron_box, 8);
cron_enabled_check_ = gtk_check_button_new_with_label("Enable scheduled backups (cronjob)");
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(cron_enabled_check_), config_.settings().cron_enabled);
gtk_box_pack_start(GTK_BOX(cron_box), cron_enabled_check_, FALSE, FALSE, 0);
GtkWidget* sched_hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
GtkWidget* sched_label = gtk_label_new("Schedule (crontab format):");
gtk_box_pack_start(GTK_BOX(sched_hbox), sched_label, FALSE, FALSE, 0);
cron_schedule_entry_ = gtk_entry_new();
gtk_entry_set_text(GTK_ENTRY(cron_schedule_entry_), config_.settings().cron_schedule.c_str());
gtk_entry_set_placeholder_text(GTK_ENTRY(cron_schedule_entry_), "0 2 * * *");
gtk_widget_set_size_request(cron_schedule_entry_, 200, -1);
gtk_box_pack_start(GTK_BOX(sched_hbox), cron_schedule_entry_, FALSE, FALSE, 0);
GtkWidget* cron_help = gtk_label_new(NULL);
gtk_label_set_markup(GTK_LABEL(cron_help),
"<small>Examples: <i>0 2 * * *</i> = daily at 2am, "
"<i>0 */6 * * *</i> = every 6h, "
"<i>0 2 * * 0</i> = Sundays at 2am</small>");
gtk_widget_set_halign(cron_help, GTK_ALIGN_START);
gtk_box_pack_start(GTK_BOX(cron_box), sched_hbox, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(cron_box), cron_help, FALSE, FALSE, 0);
gtk_notebook_append_page(GTK_NOTEBOOK(notebook), cron_box, gtk_label_new("Schedule"));
gtk_box_pack_start(GTK_BOX(content), notebook, TRUE, TRUE, 0);
gtk_widget_show_all(dialog);
bool saved = false;
if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
config_.settings().backup_dest = gtk_entry_get_text(GTK_ENTRY(dest_entry_));
config_.settings().source_path = gtk_entry_get_text(GTK_ENTRY(source_entry_));
config_.settings().retention_days = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(retention_days_spin_));
config_.settings().retention_count = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(retention_count_spin_));
config_.settings().delete_removed = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(delete_check_));
config_.settings().preserve_acls = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(acl_check_));
config_.settings().preserve_xattrs = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(xattr_check_));
config_.settings().cron_enabled = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(cron_enabled_check_));
config_.settings().cron_schedule = gtk_entry_get_text(GTK_ENTRY(cron_schedule_entry_));
save_excludes();
config_.save();
saved = true;
}
gtk_widget_destroy(dialog);
return saved;
}
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include <gtk/gtk.h>
#include "config.h"
class SettingsDialog {
public:
SettingsDialog(GtkWindow* parent, Config& config);
bool run();
private:
GtkWindow* parent_;
Config& config_;
GtkWidget* dest_entry_;
GtkWidget* source_entry_;
GtkWidget* retention_days_spin_;
GtkWidget* retention_count_spin_;
GtkWidget* delete_check_;
GtkWidget* acl_check_;
GtkWidget* xattr_check_;
GtkWidget* cron_enabled_check_;
GtkWidget* cron_schedule_entry_;
GtkWidget* exclude_textview_;
void populate_excludes();
void save_excludes();
};
+259
View File
@@ -0,0 +1,259 @@
#include "snapshot_manager.h"
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <sstream>
#include <dirent.h>
#include <sys/stat.h>
#include <json-glib/json-glib.h>
#include <algorithm>
#include <unistd.h>
SnapshotManager::SnapshotManager(Config& config, RsyncManager& rsync)
: config_(config), rsync_(rsync) {}
std::string SnapshotManager::get_timestamp() {
time_t now = time(nullptr);
struct tm* t = localtime(&now);
char buf[64];
strftime(buf, sizeof(buf), "%Y-%m-%d_%H-%M-%S", t);
return std::string(buf);
}
std::string SnapshotManager::create_snapshot(const std::string& description) {
std::string ts = get_timestamp();
std::string snapshots_dir = config_.snapshots_dir();
std::string metadata_dir = config_.metadata_dir();
std::string cmd1 = "mkdir -p \"" + snapshots_dir + "\"";
std::string cmd2 = "mkdir -p \"" + metadata_dir + "\"";
system(cmd1.c_str());
system(cmd2.c_str());
/* Find latest snapshot for hardlink delta */
std::string link_dest = "";
auto snapshots = list_snapshots();
if (!snapshots.empty()) {
link_dest = snapshots[0].path;
}
rsync_.on_progress = on_progress;
bool ok = rsync_.create_snapshot(ts, config_.settings().source_path, snapshots_dir, link_dest);
if (ok) {
SnapshotInfo info;
info.name = ts;
info.timestamp = ts;
info.path = snapshots_dir + "/" + ts;
info.description = description;
save_metadata(info);
}
return ok ? ts : "";
}
void SnapshotManager::delete_snapshot(const std::string& name) {
std::string path = config_.snapshots_dir() + "/" + name;
std::string cmd = "rm -rf \"" + path + "\"";
system(cmd.c_str());
std::string meta = config_.metadata_dir() + "/" + name + ".json";
std::string meta_cmd = "rm -f \"" + meta + "\"";
system(meta_cmd.c_str());
}
std::vector<SnapshotInfo> SnapshotManager::list_snapshots() {
std::vector<SnapshotInfo> result;
std::string dir = config_.snapshots_dir();
DIR* d = opendir(dir.c_str());
if (!d) return result;
struct dirent* entry;
while ((entry = readdir(d)) != nullptr) {
if (entry->d_name[0] == '.') continue;
if (entry->d_type != DT_DIR) continue;
SnapshotInfo info;
info.name = entry->d_name;
info.timestamp = entry->d_name;
info.path = dir + "/" + entry->d_name;
/* Format display name */
std::string ts = info.timestamp;
if (ts.size() >= 19) {
info.display_name = ts.substr(0, 10) + " " + ts.substr(11, 8);
} else {
info.display_name = ts;
}
load_metadata(info);
result.push_back(info);
}
closedir(d);
std::sort(result.begin(), result.end(), [](const SnapshotInfo& a, const SnapshotInfo& b) {
return a.timestamp > b.timestamp;
});
return result;
}
SnapshotInfo SnapshotManager::get_snapshot_info(const std::string& name) {
SnapshotInfo info;
info.name = name;
info.path = config_.snapshots_dir() + "/" + name;
info.timestamp = name;
load_metadata(info);
return info;
}
std::vector<FileEntry> SnapshotManager::browse_directory(const std::string& snapshot_path,
const std::string& subpath) {
std::vector<FileEntry> result;
std::string full = snapshot_path;
if (!subpath.empty()) full += "/" + subpath;
DIR* d = opendir(full.c_str());
if (!d) return result;
struct dirent* entry;
while ((entry = readdir(d)) != nullptr) {
if (entry->d_name[0] == '.' && (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0))
continue;
FileEntry fe;
fe.name = entry->d_name;
fe.is_dir = (entry->d_type == DT_DIR);
fe.full_path = subpath.empty() ? fe.name : subpath + "/" + fe.name;
std::string file_path = full + "/" + entry->d_name;
struct stat st;
if (stat(file_path.c_str(), &st) == 0) {
fe.size = st.st_size;
fe.mode = st.st_mode;
fe.uid = st.st_uid;
fe.gid = st.st_gid;
fe.mtime = st.st_mtime;
}
result.push_back(fe);
}
closedir(d);
std::sort(result.begin(), result.end(), [](const FileEntry& a, const FileEntry& b) {
if (a.is_dir != b.is_dir) return a.is_dir > b.is_dir;
return a.name < b.name;
});
return result;
}
bool SnapshotManager::restore_selected(const std::string& snapshot_path,
const std::vector<std::string>& files,
const std::string& sudo_password) {
return rsync_.restore_files(snapshot_path, files, "/", sudo_password);
}
bool SnapshotManager::restore_full(const std::string& snapshot_path,
const std::string& sudo_password) {
return rsync_.restore_full_snapshot(snapshot_path, "/", sudo_password);
}
void SnapshotManager::cleanup_old_snapshots() {
auto snapshots = list_snapshots();
int count = 0;
for (int i = snapshots.size() - 1; i >= 0; i--) {
bool should_delete = false;
if (config_.settings().retention_count > 0 &&
(int)snapshots.size() - count > config_.settings().retention_count) {
should_delete = true;
}
if (config_.settings().retention_days > 0) {
struct tm tm = {};
std::string ts = snapshots[i].timestamp;
if (ts.size() >= 19) {
sscanf(ts.c_str(), "%d-%d-%d_%d-%d-%d",
&tm.tm_year, &tm.tm_mon, &tm.tm_mday,
&tm.tm_hour, &tm.tm_min, &tm.tm_sec);
tm.tm_year -= 1900;
tm.tm_mon -= 1;
time_t snap_time = mktime(&tm);
double diff = difftime(time(nullptr), snap_time);
if (diff > config_.settings().retention_days * 86400.0) {
should_delete = true;
}
}
}
if (should_delete) {
delete_snapshot(snapshots[i].name);
} else {
count++;
}
}
}
void SnapshotManager::save_metadata(const SnapshotInfo& info) {
std::string path = config_.metadata_dir() + "/" + info.name + ".json";
JsonBuilder* builder = json_builder_new();
json_builder_begin_object(builder);
json_builder_set_member_name(builder, "name");
json_builder_add_string_value(builder, info.name.c_str());
json_builder_set_member_name(builder, "timestamp");
json_builder_add_string_value(builder, info.timestamp.c_str());
json_builder_set_member_name(builder, "description");
json_builder_add_string_value(builder, info.description.c_str());
json_builder_set_member_name(builder, "size_bytes");
json_builder_add_int_value(builder, info.size_bytes);
json_builder_set_member_name(builder, "file_count");
json_builder_add_int_value(builder, info.file_count);
json_builder_end_object(builder);
JsonGenerator* gen = json_generator_new();
json_generator_set_root(gen, json_builder_get_root(builder));
json_generator_set_pretty(gen, TRUE);
json_generator_to_file(gen, path.c_str(), nullptr);
g_object_unref(gen);
g_object_unref(builder);
}
void SnapshotManager::load_metadata(SnapshotInfo& info) {
std::string path = config_.metadata_dir() + "/" + info.name + ".json";
FILE* f = fopen(path.c_str(), "r");
if (!f) return;
fseek(f, 0, SEEK_END);
long len = ftell(f);
fseek(f, 0, SEEK_SET);
char* buf = new char[len + 1];
fread(buf, 1, len, f);
buf[len] = 0;
fclose(f);
JsonParser* parser = json_parser_new();
GError* err = nullptr;
if (json_parser_load_from_data(parser, buf, len, &err)) {
JsonObject* root = json_node_get_object(json_parser_get_root(parser));
if (json_object_has_member(root, "description"))
info.description = json_object_get_string_member(root, "description");
if (json_object_has_member(root, "size_bytes"))
info.size_bytes = json_object_get_int_member(root, "size_bytes");
if (json_object_has_member(root, "file_count"))
info.file_count = (int)json_object_get_int_member(root, "file_count");
}
if (err) g_error_free(err);
g_object_unref(parser);
delete[] buf;
}
+60
View File
@@ -0,0 +1,60 @@
#pragma once
#include <string>
#include <vector>
#include <ctime>
#include <gtk/gtk.h>
#include "config.h"
#include "rsync_manager.h"
struct SnapshotInfo {
std::string name;
std::string timestamp;
std::string display_name;
std::string description;
int64_t size_bytes = 0;
int file_count = 0;
std::string path;
};
struct FileEntry {
std::string name;
std::string full_path;
bool is_dir = false;
int64_t size = 0;
mode_t mode = 0;
uid_t uid = 0;
gid_t gid = 0;
time_t mtime = 0;
};
class SnapshotManager {
public:
SnapshotManager(Config& config, RsyncManager& rsync);
std::string create_snapshot(const std::string& description = "");
void delete_snapshot(const std::string& name);
std::vector<SnapshotInfo> list_snapshots();
SnapshotInfo get_snapshot_info(const std::string& name);
std::vector<FileEntry> browse_directory(const std::string& snapshot_path,
const std::string& subpath = "");
bool restore_selected(const std::string& snapshot_path,
const std::vector<std::string>& files,
const std::string& sudo_password);
bool restore_full(const std::string& snapshot_path,
const std::string& sudo_password);
void cleanup_old_snapshots();
std::function<void(const RsyncProgress&)> on_progress;
private:
Config& config_;
RsyncManager& rsync_;
std::string get_timestamp();
void save_metadata(const SnapshotInfo& info);
void load_metadata(SnapshotInfo& info);
};
+100
View File
@@ -0,0 +1,100 @@
#include "system_tray.h"
SystemTray::SystemTray() : icon_(nullptr), menu_(nullptr) {
icon_ = gtk_status_icon_new();
gtk_status_icon_set_tooltip_text(icon_, "x3-Backup - Idle");
gtk_status_icon_set_visible(icon_, TRUE);
g_signal_connect(icon_, "activate", G_CALLBACK(on_status_icon_activate), this);
g_signal_connect(icon_, "button-press-event", G_CALLBACK(on_status_icon_button_press), this);
set_icon("drive-harddisk");
}
SystemTray::~SystemTray() {
if (icon_) {
gtk_status_icon_set_visible(icon_, FALSE);
g_object_unref(icon_);
icon_ = nullptr;
}
if (menu_) {
gtk_widget_destroy(menu_);
menu_ = nullptr;
}
}
void SystemTray::set_status(const std::string& tooltip, bool backup_running) {
if (!icon_) return;
gtk_status_icon_set_tooltip_text(icon_, tooltip.c_str());
if (backup_running) {
set_icon("preferences-system");
} else {
set_icon("drive-harddisk");
}
}
void SystemTray::set_icon(const char* icon_name) {
if (!icon_) return;
gtk_status_icon_set_from_icon_name(icon_, icon_name);
}
void SystemTray::show_notification(const std::string& summary, const std::string& body, bool is_error) {
GNotification* notification = g_notification_new(summary.c_str());
g_notification_set_body(notification, body.c_str());
g_notification_set_priority(notification, G_NOTIFICATION_PRIORITY_NORMAL);
g_application_send_notification(g_application_get_default(), nullptr, notification);
g_object_unref(notification);
}
void SystemTray::on_status_icon_activate(GtkStatusIcon*, gpointer data) {
auto* self = static_cast<SystemTray*>(data);
if (self->on_show) self->on_show();
}
gboolean SystemTray::on_status_icon_button_press(GtkStatusIcon*, GdkEventButton* event, gpointer data) {
auto* self = static_cast<SystemTray*>(data);
if (event->button != 3) return FALSE;
if (self->menu_) {
gtk_widget_destroy(self->menu_);
self->menu_ = nullptr;
}
self->menu_ = gtk_menu_new();
GtkWidget* show_item = gtk_menu_item_new_with_label("Show x3-Backup");
g_signal_connect(show_item, "activate", G_CALLBACK(+[](GtkWidget*, gpointer d) {
auto* s = static_cast<SystemTray*>(d);
if (s->on_show) s->on_show();
}), self);
gtk_menu_shell_append(GTK_MENU_SHELL(self->menu_), show_item);
GtkWidget* backup_item = gtk_menu_item_new_with_label("Quick Backup");
g_signal_connect(backup_item, "activate", G_CALLBACK(+[](GtkWidget*, gpointer d) {
auto* s = static_cast<SystemTray*>(d);
if (s->on_quick_backup) s->on_quick_backup();
}), self);
gtk_menu_shell_append(GTK_MENU_SHELL(self->menu_), backup_item);
GtkWidget* settings_item = gtk_menu_item_new_with_label("Settings");
g_signal_connect(settings_item, "activate", G_CALLBACK(+[](GtkWidget*, gpointer d) {
auto* s = static_cast<SystemTray*>(d);
if (s->on_settings) s->on_settings();
}), self);
gtk_menu_shell_append(GTK_MENU_SHELL(self->menu_), settings_item);
gtk_menu_shell_append(GTK_MENU_SHELL(self->menu_), gtk_separator_menu_item_new());
GtkWidget* quit_item = gtk_menu_item_new_with_label("Quit");
g_signal_connect(quit_item, "activate", G_CALLBACK(+[](GtkWidget*, gpointer d) {
auto* s = static_cast<SystemTray*>(d);
if (s->on_quit) s->on_quit();
}), self);
gtk_menu_shell_append(GTK_MENU_SHELL(self->menu_), quit_item);
gtk_widget_show_all(self->menu_);
gtk_menu_popup_at_pointer(GTK_MENU(self->menu_), (GdkEvent*)event);
return TRUE;
}
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include <gtk/gtk.h>
#include <string>
#include <functional>
class SystemTray {
public:
SystemTray();
~SystemTray();
void set_status(const std::string& tooltip, bool backup_running = false);
void set_icon(const char* icon_name);
void show_notification(const std::string& summary, const std::string& body, bool is_error = false);
bool is_visible() const { return icon_ != nullptr; }
std::function<void()> on_show;
std::function<void()> on_quit;
std::function<void()> on_quick_backup;
std::function<void()> on_settings;
private:
GtkStatusIcon* icon_;
GtkWidget* menu_;
static void on_status_icon_activate(GtkStatusIcon* icon, gpointer data);
static gboolean on_status_icon_button_press(GtkStatusIcon* icon, GdkEventButton* event, gpointer data);
};
Executable
+36
View File
@@ -0,0 +1,36 @@
#!/bin/bash
set -e
echo "=== x3-Backup Uninstall ==="
# Remove installed files
echo "Removing installed files..."
if [ -f /usr/local/bin/x3-backup ]; then
sudo rm -f /usr/local/bin/x3-backup
echo " Removed /usr/local/bin/x3-backup"
fi
if [ -f /usr/local/share/applications/x3-backup.desktop ]; then
sudo rm -f /usr/local/share/applications/x3-backup.desktop
echo " Removed desktop file"
fi
if [ -f /usr/local/share/icons/hicolor/256x256/apps/x3-backup.png ]; then
sudo rm -f /usr/local/share/icons/hicolor/256x256/apps/x3-backup.png
echo " Removed icon"
fi
# Remove cronjob
echo "Removing cronjob..."
crontab -l 2>/dev/null | grep -v 'x3-backup-auto' | crontab - 2>/dev/null || true
echo " Cronjob removed"
# Ask about config and data
echo ""
echo "Config and backup data in ~/.config/x3-backup/ were NOT removed."
echo "To remove config: rm -rf ~/.config/x3-backup"
echo ""
echo "Backup snapshots were NOT removed."
echo "To remove backups: check your configured backup destination"
echo ""
echo "Done."