/* * GnomeSettingsVault - A GNOME configuration backup utility. * Copyright (C) 2026 Nicole Portas, nicole@equalmass.com * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. */ #include "MainWindow.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include namespace fs = std::filesystem; MainWindow::MainWindow() : m_HeaderBar(), m_MainLayout(Gtk::ORIENTATION_VERTICAL, 0), m_Notebook(), m_BottomBox(Gtk::ORIENTATION_HORIZONTAL, 10), m_StatusLabel("Ready."), m_CheckDebugMode("Enable Debug Logging"), m_VBoxBackup(Gtk::ORIENTATION_VERTICAL, 10), m_LabelBackupInstruction("Backup"), m_CheckThemesBackup("GTK Themes"), m_CheckIconsBackup("Icons"), m_CheckWallpapersBackup("Wallpapers"), m_CheckExtensionsBackup("GNOME Extensions"), m_CheckDconfBackup("GNOME Settings"), m_ButtonBackup("Start Backup"), m_VBoxRestore(Gtk::ORIENTATION_VERTICAL, 10), m_LabelRestoreInstruction("Restore from Vault"), m_ButtonSelectVault("Select .gbk Vault File"), m_LabelSelectedVault("No vault selected"), m_CheckThemesRestore("GTK Themes"), m_CheckIconsRestore("Icons"), m_CheckWallpapersRestore("Wallpapers"), m_CheckExtensionsRestore("GNOME Extensions"), m_CheckDconfRestore("GNOME Settings"), m_CheckDryRunRestore("Dry Run"), m_ButtonRestore("Start Restore"), m_WorkerRunning(false), m_JobSuccess(true), m_IsDebugMode(false), m_LastJobWasRestore(false) { m_Dispatcher.connect(sigc::mem_fun(*this, &MainWindow::on_dispatcher_ping)); set_default_size(1000, 700); m_HeaderBar.set_title("GnomeSettingsVault 0.1"); m_HeaderBar.set_show_close_button(true); set_titlebar(m_HeaderBar); m_LabelBackupInstruction.set_use_markup(true); m_LabelRestoreInstruction.set_use_markup(true); m_RefTextBuffer = Gtk::TextBuffer::create(); auto selection_tag = m_RefTextBuffer->create_tag("selection_orange"); selection_tag->property_background() = "#ff8c00"; selection_tag->property_foreground() = "#000000"; m_LogView.set_buffer(m_RefTextBuffer); m_LogView.set_editable(false); m_LogView.set_cursor_visible(true); m_LogView.set_can_focus(true); m_RefTextBuffer->signal_mark_set().connect([this, selection_tag](const Gtk::TextBuffer::iterator& iter, const Glib::RefPtr& mark){ Gtk::TextBuffer::iterator start, end; if (m_RefTextBuffer->get_selection_bounds(start, end)) { m_RefTextBuffer->apply_tag(selection_tag, start, end); } else { m_RefTextBuffer->remove_tag(selection_tag, m_RefTextBuffer->begin(), m_RefTextBuffer->end()); } }); m_LogView.override_background_color(Gdk::RGBA("#1e1e1e")); m_LogView.override_color(Gdk::RGBA("#dcdcdc")); m_RefTextBuffer->create_mark("last_line", m_RefTextBuffer->end()); m_ScrolledWindow.add(m_LogView); m_ScrolledWindow.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_ALWAYS); m_VBoxBackup.set_border_width(20); m_VBoxBackup.pack_start(m_LabelBackupInstruction, Gtk::PACK_SHRINK); m_VBoxBackup.pack_start(m_CheckThemesBackup, Gtk::PACK_SHRINK); m_VBoxBackup.pack_start(m_CheckIconsBackup, Gtk::PACK_SHRINK); m_VBoxBackup.pack_start(m_CheckWallpapersBackup, Gtk::PACK_SHRINK); m_VBoxBackup.pack_start(m_CheckExtensionsBackup, Gtk::PACK_SHRINK); m_VBoxBackup.pack_start(m_CheckDconfBackup, Gtk::PACK_SHRINK); m_VBoxBackup.pack_end(m_ButtonBackup, Gtk::PACK_SHRINK); m_VBoxRestore.set_border_width(20); m_VBoxRestore.pack_start(m_LabelRestoreInstruction, Gtk::PACK_SHRINK); m_VBoxRestore.pack_start(m_ButtonSelectVault, Gtk::PACK_SHRINK); m_VBoxRestore.pack_start(m_LabelSelectedVault, Gtk::PACK_SHRINK); m_VBoxRestore.pack_start(m_CheckThemesRestore, Gtk::PACK_SHRINK); m_VBoxRestore.pack_start(m_CheckIconsRestore, Gtk::PACK_SHRINK); m_VBoxRestore.pack_start(m_CheckWallpapersRestore, Gtk::PACK_SHRINK); m_VBoxRestore.pack_start(m_CheckExtensionsRestore, Gtk::PACK_SHRINK); m_VBoxRestore.pack_start(m_CheckDconfRestore, Gtk::PACK_SHRINK); m_VBoxRestore.pack_start(m_CheckDryRunRestore, Gtk::PACK_SHRINK); m_VBoxRestore.pack_end(m_ButtonRestore, Gtk::PACK_SHRINK); m_Notebook.append_page(m_VBoxBackup, "Backup"); m_Notebook.append_page(m_VBoxRestore, "Restore"); m_BottomBox.set_border_width(10); m_StatusLabel.set_halign(Gtk::ALIGN_START); m_BottomBox.pack_start(m_StatusLabel, Gtk::PACK_EXPAND_WIDGET); m_BottomBox.pack_end(m_CheckDebugMode, Gtk::PACK_SHRINK); m_MainLayout.pack_start(m_Notebook, Gtk::PACK_SHRINK); m_MainLayout.pack_start(m_ScrolledWindow, Gtk::PACK_EXPAND_WIDGET); m_MainLayout.pack_start(m_BottomBox, Gtk::PACK_SHRINK); m_ButtonBackup.signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::on_button_backup_clicked)); m_ButtonRestore.signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::on_button_restore_clicked)); m_ButtonSelectVault.signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::on_button_select_vault_clicked)); add(m_MainLayout); show_all_children(); } MainWindow::~MainWindow() {} void MainWindow::scan_vault_contents(const std::string& path) { struct archive *a = archive_read_new(); struct archive_entry *entry; archive_read_support_format_all(a); archive_read_support_filter_all(a); std::set found; if (archive_read_open_filename(a, path.c_str(), 10240) == ARCHIVE_OK) { while (archive_read_next_header(a, &entry) == ARCHIVE_OK) { std::string p = archive_entry_pathname(entry); if (p.find("themes.tar.gz") != std::string::npos) found.insert("themes"); if (p.find("icons.tar.gz") != std::string::npos) found.insert("icons"); if (p.find("wallpapers.tar.gz") != std::string::npos) found.insert("wallpapers"); if (p.find("extensions.tar.gz") != std::string::npos) found.insert("extensions"); if (p.find("settings.ini") != std::string::npos) found.insert("dconf"); } } archive_read_free(a); m_CheckThemesRestore.set_active(found.count("themes")); m_CheckIconsRestore.set_active(found.count("icons")); m_CheckWallpapersRestore.set_active(found.count("wallpapers")); m_CheckExtensionsRestore.set_active(found.count("extensions")); m_CheckDconfRestore.set_active(found.count("dconf")); } void MainWindow::on_button_select_vault_clicked() { Gtk::FileChooserDialog dialog("Choose vault", Gtk::FILE_CHOOSER_ACTION_OPEN); dialog.set_transient_for(*this); dialog.add_button("_Cancel", Gtk::RESPONSE_CANCEL); dialog.add_button("_Open", Gtk::RESPONSE_OK); if (dialog.run() == Gtk::RESPONSE_OK) { m_SelectedVaultPath = dialog.get_filename(); m_LabelSelectedVault.set_text("Selected: " + fs::path(m_SelectedVaultPath).filename().string()); scan_vault_contents(m_SelectedVaultPath); add_log_ui("Vault loaded."); } } void MainWindow::queue_log(const std::string& message) { // Only write to disk if debug mode is explicitly checked if (m_IsDebugMode) { std::ofstream log_file(std::string(std::getenv("HOME")) + "/GnomeSettingsVault.log", std::ios::app); if (log_file.is_open()) { log_file << message << std::endl; log_file.close(); } } std::lock_guard lock(m_LogMutex); m_LogQueue.push(message); m_Dispatcher.emit(); } void MainWindow::on_dispatcher_ping() { std::lock_guard lock(m_LogMutex); while (!m_LogQueue.empty()) { std::string msg = m_LogQueue.front(); m_LogQueue.pop(); if (msg == "__UNLOCK_UI__") { set_ui_locked(false); // Trigger Logout Prompt if it was a successful restore if (m_JobSuccess && m_LastJobWasRestore) { Gtk::MessageDialog dialog(*this, "Restore Complete", false, Gtk::MESSAGE_QUESTION, Gtk::BUTTONS_YES_NO); dialog.set_secondary_text("Settings and components have been successfully restored.\n\nGNOME needs to restart to apply all changes smoothly. Would you like to log out now?"); if (dialog.run() == Gtk::RESPONSE_YES) { system("gnome-session-quit --logout --no-prompt &"); } } } else { add_log_ui(msg); } } } void MainWindow::add_log_ui(const std::string& message) { m_RefTextBuffer->insert(m_RefTextBuffer->end(), message + "\n"); auto mark = m_RefTextBuffer->get_mark("last_line"); m_RefTextBuffer->move_mark(mark, m_RefTextBuffer->end()); if (m_LogView.get_realized()) m_LogView.scroll_to(mark); } void MainWindow::set_ui_locked(bool locked) { m_ButtonBackup.set_sensitive(!locked); m_ButtonRestore.set_sensitive(!locked); m_ButtonSelectVault.set_sensitive(!locked); m_CheckDebugMode.set_sensitive(!locked); if (!locked) { m_StatusLabel.override_color(m_JobSuccess ? Gdk::RGBA("#44ff44") : Gdk::RGBA("#ff4444")); m_StatusLabel.set_text(m_JobSuccess ? "Job Completed Successfully." : "Job Completed with ERRORS."); } } void MainWindow::run_backup_job(JobConfig config, std::string home_dir) { m_JobSuccess = true; std::error_code ec; queue_log("--- Starting Backup ---"); std::string temp_dir = "/tmp/gvault_pack"; fs::remove_all(temp_dir, ec); fs::create_directories(temp_dir); if (config.themes) if(!create_tar_archive(home_dir + "/.themes", temp_dir + "/themes.tar.gz", false)) m_JobSuccess = false; if (config.icons) if(!create_tar_archive(home_dir + "/.icons", temp_dir + "/icons.tar.gz", false)) m_JobSuccess = false; if (config.wallpapers) { std::string w_dir = get_wallpaper_directory(); if(!w_dir.empty()) if(!create_tar_archive(w_dir, temp_dir + "/wallpapers.tar.gz", false)) m_JobSuccess = false; } if (config.extensions) if(!create_tar_archive(home_dir + "/.local/share/gnome-shell/extensions", temp_dir + "/extensions.tar.gz", false)) m_JobSuccess = false; if (config.dconf) { queue_log("[BACKUP] Exporting GNOME Settings..."); if (system(("dconf dump / > " + temp_dir + "/settings.ini").c_str()) != 0) m_JobSuccess = false; } std::string final_file = home_dir + "/" + config.vault_name + ".gbk"; queue_log("[BACKUP] Compiling final vault..."); if(!create_tar_archive(temp_dir, final_file, true)) m_JobSuccess = false; fs::remove_all(temp_dir, ec); queue_log("__UNLOCK_UI__"); m_WorkerRunning = false; } void MainWindow::on_button_backup_clicked() { Gtk::MessageDialog dialog(*this, "Vault Name", false, Gtk::MESSAGE_QUESTION, Gtk::BUTTONS_OK_CANCEL); Gtk::Entry entry; entry.set_text("GnomeVaultBackup"); dialog.get_content_area()->pack_start(entry, Gtk::PACK_SHRINK); dialog.show_all_children(); if (dialog.run() == Gtk::RESPONSE_OK) { m_IsDebugMode = m_CheckDebugMode.get_active(); m_LastJobWasRestore = false; set_ui_locked(true); m_WorkerRunning = true; m_StatusLabel.override_color(Gdk::RGBA("#ffffff")); m_StatusLabel.set_text("Backup in progress..."); JobConfig cfg; cfg.themes = m_CheckThemesBackup.get_active(); cfg.icons = m_CheckIconsBackup.get_active(); cfg.wallpapers = m_CheckWallpapersBackup.get_active(); cfg.extensions = m_CheckExtensionsBackup.get_active(); cfg.dconf = m_CheckDconfBackup.get_active(); cfg.dry_run = false; cfg.vault_name = entry.get_text(); std::thread(&MainWindow::run_backup_job, this, cfg, std::string(std::getenv("HOME"))).detach(); } } void MainWindow::run_restore_job(JobConfig config, std::string home_dir) { m_JobSuccess = true; std::error_code ec; queue_log("--- Starting Clean Restore ---"); auto nuke_leftovers = [&](const std::string& path) { fs::remove_all(path + "_vault_new", ec); fs::remove_all(path + "_vault_old", ec); }; nuke_leftovers(home_dir + "/.themes"); nuke_leftovers(home_dir + "/.icons"); nuke_leftovers(home_dir + "/.local/share/gnome-shell/extensions"); if (config.extensions && !config.dry_run) { queue_log("[SAFE] Disabling extensions to prevent GNOME crash..."); system("gsettings set org.gnome.shell disable-user-extensions true"); } std::string temp_unpack = "/tmp/gvault_unpack"; fs::remove_all(temp_unpack, ec); fs::create_directories(temp_unpack); queue_log("[RESTORE] Unpacking vault structure..."); if (!extract_tar_archive(config.custom_restore_path, temp_unpack, false, false)) m_JobSuccess = false; std::string work_path = temp_unpack; for (const auto& entry : fs::recursive_directory_iterator(temp_unpack)) { if (entry.path().filename() == "settings.ini") { work_path = entry.path().parent_path().string(); break; } } auto atomic_restore = [&](const std::string& arch_name, const std::string& live_path, const std::string& label) { std::string arch_full = work_path + "/" + arch_name + ".tar.gz"; if (fs::exists(arch_full)) { queue_log("[RESTORE] Swapping " + label + "..."); std::string shadow = live_path + "_vault_new"; std::string old_path = live_path + "_vault_old"; fs::remove_all(shadow, ec); fs::remove_all(old_path, ec); fs::create_directories(shadow); if (extract_tar_archive(arch_full, shadow, config.dry_run, false)) { if (!config.dry_run) { sync(); if (fs::exists(live_path)) { fs::rename(live_path, old_path, ec); if (ec) { queue_log(" !!! Warning: Could not isolate old " + label + ". Retrying via delete."); fs::remove_all(live_path, ec); } } fs::rename(shadow, live_path, ec); if (ec) { queue_log(" !!! ERROR: Rename failed for " + label + "."); m_JobSuccess = false; } else { if (m_IsDebugMode) queue_log(" -> " + label + " swapped successfully."); fs::remove_all(old_path, ec); } } } else m_JobSuccess = false; } }; if (config.themes) atomic_restore("themes", home_dir + "/.themes", "Themes"); if (config.icons) atomic_restore("icons", home_dir + "/.icons", "Icons"); if (config.extensions) atomic_restore("extensions", home_dir + "/.local/share/gnome-shell/extensions", "Extensions"); if (config.wallpapers && fs::exists(work_path + "/wallpapers.tar.gz")) { queue_log("[RESTORE] Restoring Wallpapers..."); if (!extract_tar_archive(work_path + "/wallpapers.tar.gz", "/", config.dry_run, true)) m_JobSuccess = false; } if (config.dconf && fs::exists(work_path + "/settings.ini") && !config.dry_run) { queue_log("[RESTORE] Injecting Dconf settings..."); if (system(("dconf load / < " + work_path + "/settings.ini").c_str()) != 0) m_JobSuccess = false; } if (config.extensions && !config.dry_run) { queue_log("[SAFE] Re-enabling extensions..."); system("gsettings set org.gnome.shell disable-user-extensions false"); } fs::remove_all(temp_unpack, ec); queue_log("__UNLOCK_UI__"); } void MainWindow::on_button_restore_clicked() { if (m_SelectedVaultPath.empty()) return; m_IsDebugMode = m_CheckDebugMode.get_active(); m_LastJobWasRestore = true; set_ui_locked(true); m_WorkerRunning = true; m_StatusLabel.override_color(Gdk::RGBA("#ffffff")); m_StatusLabel.set_text("Restore in progress..."); JobConfig cfg; cfg.themes = m_CheckThemesRestore.get_active(); cfg.icons = m_CheckIconsRestore.get_active(); cfg.wallpapers = m_CheckWallpapersRestore.get_active(); cfg.extensions = m_CheckExtensionsRestore.get_active(); cfg.dconf = m_CheckDconfRestore.get_active(); cfg.dry_run = m_CheckDryRunRestore.get_active(); cfg.custom_restore_path = m_SelectedVaultPath; std::thread(&MainWindow::run_restore_job, this, cfg, std::string(std::getenv("HOME"))).detach(); } bool MainWindow::create_tar_archive(const std::string& source_dir, const std::string& out_filename, bool strip_path) { struct archive *a = archive_write_new(); archive_write_add_filter_gzip(a); archive_write_set_format_pax_restricted(a); archive_write_open_filename(a, out_filename.c_str()); fs::path base_path = fs::absolute(source_dir); bool ok = true; if (m_IsDebugMode) queue_log(" -> Compressing: " + source_dir); for (const auto& dirEntry : fs::recursive_directory_iterator(base_path, fs::directory_options::skip_permission_denied)) { std::string full = dirEntry.path().string(); std::string store = strip_path ? fs::relative(dirEntry.path(), base_path).string() : full; if (store == "." || store == "..") continue; struct archive_entry *entry = archive_entry_new(); archive_entry_set_pathname(entry, store.c_str()); struct stat st; if (lstat(full.c_str(), &st) != 0) { archive_entry_free(entry); ok = false; continue; } archive_entry_copy_stat(entry, &st); if (S_ISLNK(st.st_mode)) { char link_target[4096]; ssize_t len = readlink(full.c_str(), link_target, sizeof(link_target)-1); if (len != -1) { link_target[len]='\0'; archive_entry_set_symlink(entry, link_target); } } archive_write_header(a, entry); if (m_IsDebugMode) queue_log(" -> Packed: " + store); if (S_ISREG(st.st_mode)) { int fd = ::open(full.c_str(), O_RDONLY); if (fd >= 0) { char buff[65536]; ssize_t len; while ((len = ::read(fd, buff, sizeof(buff))) > 0) archive_write_data(a, buff, len); ::close(fd); } else ok = false; } archive_entry_free(entry); } archive_write_close(a); archive_write_free(a); return ok; } bool MainWindow::extract_tar_archive(const std::string& archive_path, const std::string& dest_dir, bool dry_run, bool use_absolute) { struct archive *a = archive_read_new(); archive_read_support_format_all(a); archive_read_support_filter_all(a); if (archive_read_open_filename(a, archive_path.c_str(), 10240) != ARCHIVE_OK) return false; int flags = ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_PERM | ARCHIVE_EXTRACT_UNLINK | ARCHIVE_EXTRACT_SECURE_NODOTDOT; struct archive_entry *entry; bool status = true; while (archive_read_next_header(a, &entry) == ARCHIVE_OK) { std::string stored_path = archive_entry_pathname(entry); if (!use_absolute) { size_t pos = stored_path.find("/.themes/"); if (pos == std::string::npos) pos = stored_path.find("/.icons/"); if (pos == std::string::npos) pos = stored_path.find("/extensions/"); if (pos != std::string::npos) { size_t start = stored_path.find('/', pos + 1); if (start != std::string::npos) stored_path = stored_path.substr(start + 1); } } std::string target = use_absolute ? archive_entry_pathname(entry) : dest_dir + "/" + stored_path; archive_entry_set_pathname(entry, target.c_str()); if (dry_run) { if (m_IsDebugMode) queue_log(" -> [DRY] " + target); archive_read_data_skip(a); } else { if (use_absolute && fs::exists(target) && fs::is_regular_file(target)) { int fd = ::open(target.c_str(), O_WRONLY | O_TRUNC); if (fd >= 0) ::close(fd); } if (archive_read_extract(a, entry, flags) != ARCHIVE_OK) { if (!fs::is_directory(target)) { if (m_IsDebugMode) queue_log(" !!! FAIL: " + std::string(archive_error_string(a)) + " -> " + target); status = false; } } else { if (m_IsDebugMode) queue_log(" -> Restored: " + target); } } } archive_read_close(a); archive_read_free(a); return status; } std::string MainWindow::get_wallpaper_directory() { char buffer[256]; std::string result = ""; FILE* pipe = popen("gsettings get org.gnome.desktop.background picture-uri-dark 2>/dev/null", "r"); if (pipe) { while (fgets(buffer, sizeof(buffer), pipe) != nullptr) result += buffer; pclose(pipe); } size_t file_pos = result.find("file://"); if (file_pos != std::string::npos) { std::string path = result.substr(file_pos + 7); path.erase(std::remove(path.begin(), path.end(), '\''), path.end()); path.erase(std::remove(path.begin(), path.end(), '\n'), path.end()); return fs::path(path).parent_path().string(); } return ""; }