From 9cc7e98c52cba10184e0debaaddbdcc6f784b686 Mon Sep 17 00:00:00 2001 From: vikdevelop Date: Wed, 18 Mar 2026 16:41:26 +0100 Subject: [PATCH 01/13] Add support for Niri WM and LXQt DE --- src/globals.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/globals.py b/src/globals.py index 1d0dbda9..4228833d 100644 --- a/src/globals.py +++ b/src/globals.py @@ -83,6 +83,8 @@ def get_app_environment(): "MATE": {"de_name": "MATE", "dirs": [(f"{home}/.config/caja", "caja")]}, "Deepin": {"de_name": "Deepin", "dirs": [(f"{home}/.config/deepin", "deepin"), (f"{home}/.local/share/deepin", "deepin-data")]}, "Hyprland": {"de_name": "Hyprland", "dirs": [(f"{home}/.config/hypr", "hypr")]}, + "LXQt": {"de_name": "LXQt", "dirs": [(f"{home}/.config/lxqt", "lxqt")]}, + "niri": {"de_name": "Niri", "dirs": [(f"{home}/.config/niri", "niri")]}, "KDE": {"de_name": "KDE Plasma"}, } From ccf500b6a188ea37cc5329b052a5b36e3085166b Mon Sep 17 00:00:00 2001 From: vikdevelop Date: Thu, 19 Mar 2026 18:02:19 +0100 Subject: [PATCH 02/13] First implementation of #521 --- po/savedesktop.pot | 16 +++ src/core/de_config.py | 2 +- src/core/flatpaks_installer.py | 108 ++++++++++------- src/gui/flatpaks_installer_window.py | 168 +++++++++++++++++++++++++++ src/gui/meson.build | 3 +- src/savedesktop.in | 8 ++ 6 files changed, 262 insertions(+), 43 deletions(-) create mode 100644 src/gui/flatpaks_installer_window.py diff --git a/po/savedesktop.pot b/po/savedesktop.pot index c761ecc8..0a50f639 100644 --- a/po/savedesktop.pot +++ b/po/savedesktop.pot @@ -108,6 +108,22 @@ msgstr "" msgid "Keep existing Flatpak apps and data" msgstr "" +#: src/gui/flatpaks_installer_window.py +msgid "Installing your Flatpak apps..." +msgstr "" + +#: src/gui/flatpaks_installer_window.py +msgid "You can continue using your computer; everything runs in the background. Once the installation of your Flatpak apps and, if applicable, your user data is complete, a message will appear here and you’ll also see a system notification. You can also show the progress below." +msgstr "" + +#: src/gui/flatpaks_installer_window.py +msgid "Your apps have been installed!" +msgstr "" + +#: src/gui/flatpaks_installer_window.py +msgid "This window will be closed automatically in 2 minutes." +msgstr "" + #: src/gui/templates/shortcuts_window.ui:50 #: src/gui/more_options_dialog.py msgid "More options" diff --git a/src/core/de_config.py b/src/core/de_config.py index 46d3ec43..0291dd5a 100644 --- a/src/core/de_config.py +++ b/src/core/de_config.py @@ -105,7 +105,7 @@ def create_flatpak_autostart(): "[Desktop Entry]\n" "Name=SaveDesktop (Flatpak Apps installer)\n" "Type=Application\n" - f"Exec=python3 {CACHE}/workspace/flatpaks_installer.py\n" + f"Exec=flatpak run io.github.vikdevelop.SaveDesktop --show-flatpaks-installer & python3 -u {CACHE}/workspace/flatpaks_installer.py > {CACHE}/workspace/log.pipe 2>&1\n" ) print("[OK] Created Flatpak autostart") diff --git a/src/core/flatpaks_installer.py b/src/core/flatpaks_installer.py index 190f3e9e..d7244d0d 100644 --- a/src/core/flatpaks_installer.py +++ b/src/core/flatpaks_installer.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -import os, subprocess, shutil, json +import os, subprocess, shutil, json, time from pathlib import Path CACHE_FLATPAK = f"{Path.home()}/.var/app/io.github.vikdevelop.SaveDesktop/cache/tmp/workspace" @@ -35,22 +35,21 @@ # Restore Flatpak user data archive if copy_data: - print("[DEBUG] Copying the Flatpak's user data to ~/.var/app") + print("[COPY] Copying the Flatpak's user data to ~/.var/app", flush=True) if os.path.exists(f"app"): - print("Copying the Flatpak apps' user data to the ~/.var/app directory (backward compatibility mode)") os.system(f"cp -au ./app/ ~/.var/") archive_file = "Flatpak_Apps/flatpak-apps-data.tgz" if os.path.exists("Flatpak_Apps/flatpak-apps-data.tgz") else "flatpak-apps-data.tgz" if os.path.exists(archive_file): - tar_cmd = ["tar", "-xzvf", archive_file, "-C", f"{Path.home()}/.var"] + tar_cmd = ["tar", "-xzf", archive_file, "-C", f"{Path.home()}/.var"] for d_app in disabled_flatpaks: tar_cmd.extend([f"--exclude={d_app}", f"--exclude=app/{d_app}"]) subprocess.run(tar_cmd) + print("✔ Copied Flatpak's user data", flush=True) # Load Flatpaks from bash scripts if install_flatpaks: - print("[DEBUG] Scanning the Bash scripts...") if os.path.exists("Flatpak_Apps"): installed_flatpaks_files = ['Flatpak_Apps/installed_flatpaks.sh', 'Flatpak_Apps/installed_user_flatpaks.sh'] else: @@ -59,56 +58,84 @@ desired_flatpaks = {} for file in installed_flatpaks_files: if os.path.exists(file): - print(f"[DEBUG] Reading: {file}") with open(file, 'r') as f: - for line in f: - if 'flatpak' in line and 'install' in line: - parts = line.split() - - app_id = None - for part in parts: - if "." in part and not part.startswith("-"): - app_id = part - break - - if not app_id: - continue - - if app_id in disabled_flatpaks: - print(f"[SKIP] Disabled app: {app_id}") - continue - - method = "--user" if "--user" in parts else "--system" - desired_flatpaks[app_id] = method - print(f"[DEBUG] Added to the installation queue: {app_id} ({method})") - + # Read all lines into memory + lines = f.readlines() + + # The file is safely closed here, we process the list in memory + for line in lines: + if 'flatpak' in line and 'install' in line: + parts = line.split() + + app_id = None + for part in parts: + # Look for something that looks like an app ID (e.g. com.obsproject.Studio) + if "." in part and not part.startswith("-"): + app_id = part + break + + if not app_id: + continue + + if app_id in disabled_flatpaks: + print(f"[SKIP] Disabled app: {app_id}", flush=True) + continue + + # Determine if it's a user or system install + method = "--user" if "--user" in parts else "--system" + desired_flatpaks[app_id] = method + + # Count the actual valid apps we found, not just raw lines in the file + apps_count = len(desired_flatpaks) if not desired_flatpaks: - print("[DEBUG] The installation queue is empty. It couldn't find any valid Flatpaks to install.") + print("Installation queue is empty.", flush=True) + else: + print(f"Installing {apps_count} apps", flush=True) - # Get currently installed Flatpaks + # Get currently installed Flatpaks first, before doing any math system_flatpak_dir = '/var/lib/flatpak/app' user_flatpak_dir = os.path.expanduser('~/.local/share/flatpak/app') installed_apps = {} + for directory, method in [(system_flatpak_dir, '--system'), (user_flatpak_dir, '--user')]: if os.path.exists(directory): for app in os.listdir(directory): if os.path.isdir(os.path.join(directory, app)): installed_apps[app] = method - for app, method in desired_flatpaks.items(): - if app not in installed_apps: - print(f"[INSTALL] Installing {app} ({method})") + # Filter out the apps that are already on the system + apps_to_install = {app: method for app, method in desired_flatpaks.items() if app not in installed_apps} + apps_count = len(apps_to_install) + + # Print the ones we are skipping just so the user knows + for app in desired_flatpaks: + if app in installed_apps: + print(f"[INFO] {app} is already available in the system.", flush=True) + + # Now handle the actual installation queue + if apps_count == 0: + print("Installation queue is empty. Nothing new to install.", flush=True) + else: + print(f"Installing {apps_count} new apps", flush=True) + + # Start the installation loop only for the missing apps + for i, (app, method) in enumerate(apps_to_install.items(), start=1): + print(f"↓ Installing {app} ({i}/{apps_count})", flush=True) + + # Ensure the user repo exists if we are doing a user install if method == '--user': os.system("flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo") + + # Run the actual Flatpak install command subprocess.run(['flatpak', 'install', method, 'flathub', app, '-y']) - else: - print(f"[INFO] {app} is available in the system.") + + print("✔ All apps have been installed", flush=True) # Remove Flatpaks, which are not in the list (only if it's allowed by the user) if not keep_flatpaks: for app, method in installed_apps.items(): if app not in desired_flatpaks: - print(f"[REMOVE] {method.title()} Flatpak: {app}") + print(f"[REMOVE] {method.title()} Flatpak: {app}", flush=True) subprocess.run(['flatpak', 'uninstall', method, app, '--delete-data', '-y']) # Remove orphaned ~/.var/app directories @@ -116,17 +143,16 @@ if user_var_app.exists(): for app_dir in user_var_app.iterdir(): if app_dir.is_dir() and app_dir.name not in desired_flatpaks: - print(f"[REMOVE] Orphaned Flatpak user data: {app_dir.name}") + print(f"[REMOVE] Orphaned Flatpak user data: {app_dir.name}", flush=True) shutil.rmtree(app_dir) + print("[OK] All useless apps have been removed", flush=True) + + print("✔ All operations have been completed successfully.") else: - print("Nothing to do.") + print("Nothing to do.", flush=True) # Remove the autostart file after finishing the operations autostart_file = f"{Path.home()}/.config/autostart/io.github.vikdevelop.SaveDesktop.Flatpak.desktop" if os.path.exists(autostart_file): os.remove(autostart_file) - -# Remove the cache dir after finishing the operations -if os.path.exists(CACHE_FLATPAK): - shutil.rmtree(CACHE_FLATPAK) diff --git a/src/gui/flatpaks_installer_window.py b/src/gui/flatpaks_installer_window.py new file mode 100644 index 00000000..ea095488 --- /dev/null +++ b/src/gui/flatpaks_installer_window.py @@ -0,0 +1,168 @@ +import sys, gi, threading, time, shutil, os +gi.require_version('Gtk', '4.0') +gi.require_version('Adw', '1') +from gi.repository import Gtk, Adw, Gio, GLib +from savedesktop.globals import * + +class FlatpaksWindow(Adw.ApplicationWindow): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.set_title(_("Installing your Flatpak apps...")) + + # header bar and toolbarview + self.headerbar = Adw.HeaderBar.new() + self.toolbarview = Adw.ToolbarView.new() + self.toolbarview.add_top_bar(self.headerbar) + self.set_content(self.toolbarview) + + self.set_size_request(360, 600) + + # primary layout + self.scrolled = Gtk.ScrolledWindow() + self.scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) + self.scrolled.set_vexpand(True) + self.scrolled.set_hexpand(True) + self.toolbarview.set_content(self.scrolled) + + self.winBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) + self.winBox.set_valign(Gtk.Align.FILL) + self.winBox.set_halign(Gtk.Align.FILL) + self.winBox.set_vexpand(True) + self.winBox.set_hexpand(True) + self.scrolled.set_child(self.winBox) + + # Status page + self.status_page = Adw.StatusPage.new() + self.status_page.set_icon_name("preferences-desktop-apps-symbolic") + self.status_page.set_description(_("You can continue using your computer; everything runs in the background. Once the installation of your Flatpak apps and, if applicable, your user data is complete, a message will appear here and you’ll also see a system notification. You can also show the progress below.")) + self.winBox.append(self.status_page) + + # Create TextView for logging + self.text_view = Gtk.TextView() + self.text_view.set_editable(False) + self.text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) + + # Wrapper ScrolledWindow for the logs + self.log_scroller = Gtk.ScrolledWindow() + self.log_scroller.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) + + self.log_scroller.set_min_content_height(100) + self.log_scroller.set_hexpand(True) + self.log_scroller.set_vexpand(True) + + # Put the TextView inside this new ScrolledWindow + self.log_scroller.set_child(self.text_view) + + # Append the ScrolledWindow to the main UI box + self.winBox.append(self.log_scroller) + + thread = threading.Thread(target=self.read_pipe_background, daemon=True) + thread.start() + + def read_pipe_background(self): + log_path = os.path.expanduser(f"{CACHE}/workspace/log.pipe") + print(f"[THREAD] Looking for log file at: {log_path}") + + # 1. Wait patiently until the logic script actually creates the file + while not os.path.exists(log_path): + print(f"[THREAD] Still waiting for {log_path} to exist...") + time.sleep(0.5) + + print("[THREAD] File found! Opening it now...") + + # 2. Open the regular file ONCE and read it continuously + try: + with open(log_path, 'r') as log_file: + while True: + line = log_file.readline() + + if not line: + # Check if the host script finished and deleted the file + if not os.path.exists(log_path): + print("[THREAD] File was deleted. Killing thread cleanly.") + break # Kill the thread + + # If file exists but no new data, chill for 100ms + time.sleep(0.1) + continue + + # Send the new line to the main UI thread + GLib.idle_add(self.add_text_to_ui, line) + + # 3. Explicit kill switch: FIXED TO MATCH YOUR REAL STRING + if "✔ All operations have been completed successfully." in line: + print("[THREAD] Success string found. Stopping background read.") + break + + except Exception as e: + print(f"[THREAD] FATAL ERROR: {e}") + + def add_text_to_ui(self, text): + buffer = self.text_view.get_buffer() + end_iter = buffer.get_end_iter() + + # 1. Check for completion and trigger events + if "✔ All operations have been completed successfully." in text: + self.send_completion_notification() + cleanup_thread = threading.Thread(target=self.cache_cleanup) + cleanup_thread.start() + + # Insert the text into the view + buffer.insert(end_iter, text) + + # 2. Autoscroll to the bottom + new_end_iter = buffer.get_end_iter() + mark = buffer.create_mark(None, new_end_iter, False) + self.text_view.scroll_to_mark(mark, 0.0, True, 0.0, 1.0) + buffer.delete_mark(mark) + + return False + + def send_completion_notification(self): + # Create the notification object with a title + notification = Gio.Notification.new(_("Your apps have been installed!")) + + # Optional: Set a system icon (or use your app's specific icon name) + icon = Gio.ThemedIcon.new("object-select-symbolic") + notification.set_icon(icon) + + # Get the application instance and send the notification + # The string "install-complete" is just a unique ID for this specific notification + app = self.get_application() + if app: + app.send_notification("install-complete", notification) + + def cache_cleanup(self): + if os.path.exists(f"{CACHE}/workspace"): + shutil.rmtree(f"{CACHE}/workspace") + + GLib.idle_add(self.start_auto_close_timer) + + def start_auto_close_timer(self): + # 300 seconds = 2 minutes + # GLib.timeout_add_seconds will wait in the background and then execute 'self.close_app' + GLib.timeout_add_seconds(120, self.close_app) + + self.set_title(_("Your apps have been installed!")) + self.status_page.set_icon_name("done") + self.status_page.set_description(_("This window will be closed automatically in 2 minutes.")) + + return False # Mandatory: tells idle_add to only run this setup once + + def close_app(self): + # This is triggered when the 2 minutes are up. + # Safely closes the current GTK window. + self.close() + + return False # Mandatory: tells the timeout timer not to repeat itself + +class FlatpaksInstallerApp(Adw.Application): + def __init__(self, **kwargs): + super().__init__(**kwargs, flags=Gio.ApplicationFlags.FLAGS_NONE, + application_id="io.github.vikdevelop.SaveDesktop") + self.connect('activate', self.on_activate) + + # Show the app window + def on_activate(self, app): + self.win = FlatpaksWindow(application=app) + self.win.present() diff --git a/src/gui/meson.build b/src/gui/meson.build index d1ad5d2c..b0f5c647 100644 --- a/src/gui/meson.build +++ b/src/gui/meson.build @@ -5,7 +5,8 @@ gui_sources = [ '__init__.py', 'custom_dirs_dialog.py', 'flatpak_apps_dialog.py', 'more_options_dialog.py', - 'synchronization_dialogs.py', + 'synchronization_dialogs.py', + 'flatpaks_installer_window.py', ] install_data(gui_sources, install_dir: moduledir / 'gui') diff --git a/src/savedesktop.in b/src/savedesktop.in index ad67a011..bd987900 100755 --- a/src/savedesktop.in +++ b/src/savedesktop.in @@ -53,6 +53,7 @@ parser.add_argument("--save-without-archive", type=str, help="Save the configura parser.add_argument("--sync", "--periodic-sync", action="store_true", help="Start periodic synchronization") parser.add_argument("--sync-now", action="store_true", help="Sync the configuration immediately") parser.add_argument("--import-config", help="Import a configuration from a file (*.sd.zip or *.sd.tar.gz) or folder", type=str, dest="CFG_ARCHIVE_PATH") +parser.add_argument("--show-flatpaks-installer", action="store_true", help="Show a status window about Flatpak apps' installation progress") cmd = parser.parse_args() # Import these modules before starting periodic saving or synchronization @@ -85,6 +86,13 @@ elif cmd.CFG_ARCHIVE_PATH: # import a configuration from a file, or folder raise ValueError(f"Encrypted archives are not supported in this mode") Unpack(dir_path) # archive.py +elif cmd.show_flatpaks_installer: + try: + from savedesktop.gui.flatpaks_installer_window import FlatpaksInstallerApp + app = FlatpaksInstallerApp() + app.run([]) + except FileNotFoundError: + print("File CACHE/workspace/flatpak-prefs.json doesn't exist, so it's impossible to open the window.") else: # show the app window resource = Gio.Resource.load(os.path.join(pkgdatadir, 'savedesktop.gresource')) resource._register() From b79f3915bb1d85762425b06c6fbf43709c5d8978 Mon Sep 17 00:00:00 2001 From: vikdevelop Date: Sat, 21 Mar 2026 18:07:40 +0100 Subject: [PATCH 03/13] Add Gtk.ProgressBar() to GUI --- src/core/flatpaks_installer.py | 203 ++++++++++++--------------- src/gui/flatpaks_installer_window.py | 40 +++++- 2 files changed, 127 insertions(+), 116 deletions(-) diff --git a/src/core/flatpaks_installer.py b/src/core/flatpaks_installer.py index d7244d0d..e9b9389b 100644 --- a/src/core/flatpaks_installer.py +++ b/src/core/flatpaks_installer.py @@ -24,8 +24,7 @@ if dest_dir: os.chdir(dest_dir) - # Check Flatpak user preferences - with open(f"flatpak-prefs.json") as fl: + with open("flatpak-prefs.json") as fl: j = json.load(fl) copy_data = j["copy-data"] @@ -33,121 +32,103 @@ disabled_flatpaks = j["disabled-flatpaks"] keep_flatpaks = j["keep-flatpaks"] - # Restore Flatpak user data archive - if copy_data: - print("[COPY] Copying the Flatpak's user data to ~/.var/app", flush=True) - if os.path.exists(f"app"): - os.system(f"cp -au ./app/ ~/.var/") - - archive_file = "Flatpak_Apps/flatpak-apps-data.tgz" if os.path.exists("Flatpak_Apps/flatpak-apps-data.tgz") else "flatpak-apps-data.tgz" - - if os.path.exists(archive_file): - tar_cmd = ["tar", "-xzf", archive_file, "-C", f"{Path.home()}/.var"] - for d_app in disabled_flatpaks: - tar_cmd.extend([f"--exclude={d_app}", f"--exclude=app/{d_app}"]) - subprocess.run(tar_cmd) - print("✔ Copied Flatpak's user data", flush=True) - - # Load Flatpaks from bash scripts + # ========================================== + # PHASE 1: PRE-CALCULATION (Math only, no actions) + # ========================================== + desired_flatpaks = {} if install_flatpaks: - if os.path.exists("Flatpak_Apps"): - installed_flatpaks_files = ['Flatpak_Apps/installed_flatpaks.sh', 'Flatpak_Apps/installed_user_flatpaks.sh'] - else: - installed_flatpaks_files = ['installed_flatpaks.sh', 'installed_user_flatpaks.sh'] - - desired_flatpaks = {} - for file in installed_flatpaks_files: + files_to_check = ['Flatpak_Apps/installed_flatpaks.sh', 'Flatpak_Apps/installed_user_flatpaks.sh'] if os.path.exists("Flatpak_Apps") else ['installed_flatpaks.sh', 'installed_user_flatpaks.sh'] + for file in files_to_check: if os.path.exists(file): with open(file, 'r') as f: - # Read all lines into memory - lines = f.readlines() - - # The file is safely closed here, we process the list in memory - for line in lines: - if 'flatpak' in line and 'install' in line: - parts = line.split() - - app_id = None - for part in parts: - # Look for something that looks like an app ID (e.g. com.obsproject.Studio) - if "." in part and not part.startswith("-"): - app_id = part - break - - if not app_id: - continue - - if app_id in disabled_flatpaks: - print(f"[SKIP] Disabled app: {app_id}", flush=True) - continue - - # Determine if it's a user or system install - method = "--user" if "--user" in parts else "--system" - desired_flatpaks[app_id] = method - - # Count the actual valid apps we found, not just raw lines in the file - apps_count = len(desired_flatpaks) - if not desired_flatpaks: - print("Installation queue is empty.", flush=True) - else: - print(f"Installing {apps_count} apps", flush=True) - - # Get currently installed Flatpaks first, before doing any math - system_flatpak_dir = '/var/lib/flatpak/app' - user_flatpak_dir = os.path.expanduser('~/.local/share/flatpak/app') - installed_apps = {} - - for directory, method in [(system_flatpak_dir, '--system'), (user_flatpak_dir, '--user')]: - if os.path.exists(directory): - for app in os.listdir(directory): - if os.path.isdir(os.path.join(directory, app)): - installed_apps[app] = method - - # Filter out the apps that are already on the system - apps_to_install = {app: method for app, method in desired_flatpaks.items() if app not in installed_apps} - apps_count = len(apps_to_install) - - # Print the ones we are skipping just so the user knows - for app in desired_flatpaks: - if app in installed_apps: - print(f"[INFO] {app} is already available in the system.", flush=True) - - # Now handle the actual installation queue - if apps_count == 0: - print("Installation queue is empty. Nothing new to install.", flush=True) - else: - print(f"Installing {apps_count} new apps", flush=True) - - # Start the installation loop only for the missing apps - for i, (app, method) in enumerate(apps_to_install.items(), start=1): - print(f"↓ Installing {app} ({i}/{apps_count})", flush=True) - - # Ensure the user repo exists if we are doing a user install - if method == '--user': - os.system("flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo") - - # Run the actual Flatpak install command - subprocess.run(['flatpak', 'install', method, 'flathub', app, '-y']) - - print("✔ All apps have been installed", flush=True) - - # Remove Flatpaks, which are not in the list (only if it's allowed by the user) + for line in f.readlines(): + if 'flatpak' in line and 'install' in line: + parts = line.split() + app_id = next((p for p in parts if "." in p and not p.startswith("-")), None) + if app_id and app_id not in disabled_flatpaks: + desired_flatpaks[app_id] = "--user" if "--user" in parts else "--system" + + installed_apps = {} + for directory, method in [('/var/lib/flatpak/app', '--system'), (os.path.expanduser('~/.local/share/flatpak/app'), '--user')]: + if os.path.exists(directory): + for app in os.listdir(directory): + if os.path.isdir(os.path.join(directory, app)): + installed_apps[app] = method + + apps_to_install = {app: method for app, method in desired_flatpaks.items() if app not in installed_apps} + apps_to_remove = {app: method for app, method in installed_apps.items() if app not in desired_flatpaks} if not keep_flatpaks else {} + + # Calculate exact total steps + total_steps = 0 + if copy_data: total_steps += 1 + if install_flatpaks: total_steps += len(apps_to_install) if not keep_flatpaks: - for app, method in installed_apps.items(): - if app not in desired_flatpaks: + total_steps += len(apps_to_remove) + total_steps += 1 # 1 extra step for orphaned dir cleanup + + current_step = 0 + + def report_progress(): + """Helper to print the hidden progress tag for the GTK UI""" + global current_step + current_step += 1 + print(f"[PROGRESS] {current_step}/{total_steps}", flush=True) + + if total_steps == 0: + print("Nothing to do.", flush=True) + else: + # ========================================== + # PHASE 2: EXECUTION + # ========================================== + + # 1. Restore Data + if copy_data: + print("[COPY] Copying the Flatpak's user data to ~/.var/app", flush=True) + if os.path.exists("app"): + os.system("cp -au ./app/ ~/.var/") + archive_file = "Flatpak_Apps/flatpak-apps-data.tgz" if os.path.exists("Flatpak_Apps/flatpak-apps-data.tgz") else "flatpak-apps-data.tgz" + if os.path.exists(archive_file): + tar_cmd = ["tar", "-xzf", archive_file, "-C", f"{Path.home()}/.var"] + for d_app in disabled_flatpaks: + tar_cmd.extend([f"--exclude={d_app}", f"--exclude=app/{d_app}"]) + subprocess.run(tar_cmd) + print("✔ Copied Flatpak's user data", flush=True) + report_progress() # <--- SEND UPDATE TO UI + + # 2. Install Apps + if install_flatpaks: + for app in desired_flatpaks: + if app in installed_apps: + print(f"[INFO] {app} is already available in the system.", flush=True) + + if apps_to_install: + print(f"Installing {len(apps_to_install)} new apps", flush=True) + for i, (app, method) in enumerate(apps_to_install.items(), start=1): + print(f"↓ Installing {app} ({i}/{len(apps_to_install)})", flush=True) + if method == '--user': + os.system("flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo") + subprocess.run(['flatpak', 'install', method, 'flathub', app, '-y']) + print(f"✔ Finished installing {app}", flush=True) + report_progress() # <--- SEND UPDATE TO UI + + # 3. Remove Apps & Cleanup + if not keep_flatpaks: + for app, method in apps_to_remove.items(): print(f"[REMOVE] {method.title()} Flatpak: {app}", flush=True) subprocess.run(['flatpak', 'uninstall', method, app, '--delete-data', '-y']) - - # Remove orphaned ~/.var/app directories - user_var_app = Path.home() / ".var/app" - if user_var_app.exists(): - for app_dir in user_var_app.iterdir(): - if app_dir.is_dir() and app_dir.name not in desired_flatpaks: - print(f"[REMOVE] Orphaned Flatpak user data: {app_dir.name}", flush=True) - shutil.rmtree(app_dir) - print("[OK] All useless apps have been removed", flush=True) - - print("✔ All operations have been completed successfully.") + report_progress() # <--- SEND UPDATE TO UI + + print("[REMOVE] Cleaning up orphaned user data...", flush=True) + user_var_app = Path.home() / ".var/app" + if user_var_app.exists(): + for app_dir in user_var_app.iterdir(): + if app_dir.is_dir() and app_dir.name not in desired_flatpaks: + print(f" -> Deleted: {app_dir.name}", flush=True) + shutil.rmtree(app_dir) + print("[OK] All useless apps and orphaned data have been removed", flush=True) + report_progress() # <--- SEND UPDATE TO UI + + print("✔ All operations have been completed successfully.", flush=True) else: print("Nothing to do.", flush=True) diff --git a/src/gui/flatpaks_installer_window.py b/src/gui/flatpaks_installer_window.py index ea095488..6fd5980e 100644 --- a/src/gui/flatpaks_installer_window.py +++ b/src/gui/flatpaks_installer_window.py @@ -1,4 +1,4 @@ -import sys, gi, threading, time, shutil, os +import sys, gi, threading, time, shutil, os, re gi.require_version('Gtk', '4.0') gi.require_version('Adw', '1') from gi.repository import Gtk, Adw, Gio, GLib @@ -33,10 +33,18 @@ def __init__(self, *args, **kwargs): # Status page self.status_page = Adw.StatusPage.new() - self.status_page.set_icon_name("preferences-desktop-apps-symbolic") self.status_page.set_description(_("You can continue using your computer; everything runs in the background. Once the installation of your Flatpak apps and, if applicable, your user data is complete, a message will appear here and you’ll also see a system notification. You can also show the progress below.")) self.winBox.append(self.status_page) + self.progress_bar = Gtk.ProgressBar() + # Add some margins so it doesn't touch the edges of the window + self.progress_bar.set_margin_start(20) + self.progress_bar.set_margin_end(20) + self.progress_bar.set_margin_bottom(10) + # Set initial state to 0% + self.progress_bar.set_fraction(0.0) + self.winBox.append(self.progress_bar) + # Create TextView for logging self.text_view = Gtk.TextView() self.text_view.set_editable(False) @@ -98,19 +106,41 @@ def read_pipe_background(self): print(f"[THREAD] FATAL ERROR: {e}") def add_text_to_ui(self, text): + print(f"DEBUG INCOMING: {text.strip()}") buffer = self.text_view.get_buffer() end_iter = buffer.get_end_iter() - # 1. Check for completion and trigger events + # 1. Full completion if "✔ All operations have been completed successfully." in text: + self.progress_bar.set_fraction(1.0) self.send_completion_notification() cleanup_thread = threading.Thread(target=self.cache_cleanup) cleanup_thread.start() - # Insert the text into the view + # 2. Pulse effect for ANY active operation + if any(keyword in text for keyword in ["↓ Installing", "[COPY]", "[REMOVE]"]): + self.progress_bar.pulse() + + # 3. Unified hidden progress tracker + match = re.search(r"\[PROGRESS\] (\d+)/(\d+)", text) + if match: + try: + current_step = int(match.group(1)) + total_steps = int(match.group(2)) + if total_steps > 0: + fraction = current_step / total_steps + self.progress_bar.set_fraction(fraction) + except ValueError: + pass + + # CRITICAL: We return False here immediately! + # We DO NOT want to show the "[PROGRESS] 1/5" text in the UI log. + return False + + # Insert normal text into the view buffer.insert(end_iter, text) - # 2. Autoscroll to the bottom + # Autoscroll new_end_iter = buffer.get_end_iter() mark = buffer.create_mark(None, new_end_iter, False) self.text_view.scroll_to_mark(mark, 0.0, True, 0.0, 1.0) From b682ba76bbc1870ffdac4ffc3ddbb9b4d0420d57 Mon Sep 17 00:00:00 2001 From: vikdevelop Date: Sun, 22 Mar 2026 20:13:41 +0100 Subject: [PATCH 04/13] Fix an error with launching the window after start up (#521) --- src/core/de_config.py | 2 +- src/core/flatpaks_installer.py | 4 ++-- src/gui/flatpaks_installer_window.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/de_config.py b/src/core/de_config.py index 0291dd5a..01cdec01 100644 --- a/src/core/de_config.py +++ b/src/core/de_config.py @@ -105,7 +105,7 @@ def create_flatpak_autostart(): "[Desktop Entry]\n" "Name=SaveDesktop (Flatpak Apps installer)\n" "Type=Application\n" - f"Exec=flatpak run io.github.vikdevelop.SaveDesktop --show-flatpaks-installer & python3 -u {CACHE}/workspace/flatpaks_installer.py > {CACHE}/workspace/log.pipe 2>&1\n" + f"Exec=sh -c \"flatpak run io.github.vikdevelop.SaveDesktop --show-flatpaks-installer & python3 -u /var/home/vikdevelop/.var/app/io.github.vikdevelop.SaveDesktop/cache/tmp/workspace/flatpaks_installer.py > /var/home/vikdevelop/.var/app/io.github.vikdevelop.SaveDesktop/cache/tmp/workspace/log.pipe 2>&1\"" ) print("[OK] Created Flatpak autostart") diff --git a/src/core/flatpaks_installer.py b/src/core/flatpaks_installer.py index e9b9389b..5ccb0983 100644 --- a/src/core/flatpaks_installer.py +++ b/src/core/flatpaks_installer.py @@ -75,7 +75,7 @@ def report_progress(): print(f"[PROGRESS] {current_step}/{total_steps}", flush=True) if total_steps == 0: - print("Nothing to do.", flush=True) + print("There's no need to install any new apps, since they're all available on your system.", flush=True) else: # ========================================== # PHASE 2: EXECUTION @@ -131,7 +131,7 @@ def report_progress(): print("✔ All operations have been completed successfully.", flush=True) else: - print("Nothing to do.", flush=True) + print("There's no need to install any new apps, since they're all available on your system.", flush=True) # Remove the autostart file after finishing the operations autostart_file = f"{Path.home()}/.config/autostart/io.github.vikdevelop.SaveDesktop.Flatpak.desktop" diff --git a/src/gui/flatpaks_installer_window.py b/src/gui/flatpaks_installer_window.py index 6fd5980e..4bdfc12e 100644 --- a/src/gui/flatpaks_installer_window.py +++ b/src/gui/flatpaks_installer_window.py @@ -111,7 +111,7 @@ def add_text_to_ui(self, text): end_iter = buffer.get_end_iter() # 1. Full completion - if "✔ All operations have been completed successfully." in text: + if "✔ All operations have been completed successfully." in text or "There's no need to install any new apps, since they're all available on your system." in text: self.progress_bar.set_fraction(1.0) self.send_completion_notification() cleanup_thread = threading.Thread(target=self.cache_cleanup) From 29915f2c35a2fba3cd29e3e4cb5b19abe27b09f0 Mon Sep 17 00:00:00 2001 From: vikdevelop Date: Sun, 22 Mar 2026 20:23:56 +0100 Subject: [PATCH 05/13] Fix the path name --- src/core/de_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/de_config.py b/src/core/de_config.py index 01cdec01..6843fdd7 100644 --- a/src/core/de_config.py +++ b/src/core/de_config.py @@ -105,7 +105,7 @@ def create_flatpak_autostart(): "[Desktop Entry]\n" "Name=SaveDesktop (Flatpak Apps installer)\n" "Type=Application\n" - f"Exec=sh -c \"flatpak run io.github.vikdevelop.SaveDesktop --show-flatpaks-installer & python3 -u /var/home/vikdevelop/.var/app/io.github.vikdevelop.SaveDesktop/cache/tmp/workspace/flatpaks_installer.py > /var/home/vikdevelop/.var/app/io.github.vikdevelop.SaveDesktop/cache/tmp/workspace/log.pipe 2>&1\"" + f"Exec=sh -c \"flatpak run io.github.vikdevelop.SaveDesktop --show-flatpaks-installer & python3 -u {CACHE}/workspace/flatpaks_installer.py > {CACHE}/workspace/log.pipe 2>&1\"" ) print("[OK] Created Flatpak autostart") From 4a1a944151c68c557d8cc2bc46a9a6c665f2a6c2 Mon Sep 17 00:00:00 2001 From: vikdevelop Date: Thu, 28 May 2026 17:25:02 +0200 Subject: [PATCH 06/13] update GNOME runtime, 7z and app itself to the newer versions --- io.github.vikdevelop.SaveDesktop.json | 6 +++--- meson.build | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/io.github.vikdevelop.SaveDesktop.json b/io.github.vikdevelop.SaveDesktop.json index 1e4501be..b8fd8c19 100644 --- a/io.github.vikdevelop.SaveDesktop.json +++ b/io.github.vikdevelop.SaveDesktop.json @@ -2,7 +2,7 @@ "app-id" : "io.github.vikdevelop.SaveDesktop", "desktop-file-name-suffix" : " (Development)", "runtime" : "org.gnome.Platform", - "runtime-version" : "49", + "runtime-version" : "50", "sdk" : "org.gnome.Sdk", "command" : "savedesktop", "finish-args" : [ @@ -47,8 +47,8 @@ { "type" : "git", "url" : "https://github.com/ip7z/7zip.git", - "tag" : "26.00", - "commit" : "839151eaaad24771892afaae6bac690e31e58384" + "tag" : "26.01", + "commit" : "8c63d71ff886bda90c86db28466287f977374237" } ], "x-checker-data" : { diff --git a/meson.build b/meson.build index 09256d99..9a649043 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('savedesktop', - version: '4.0', + version: '4.1-beta', meson_version: '>= 1.0.0', default_options: [ 'warning_level=2', 'werror=false', ], ) From 43d49b3a5361b9c97d60e5e8b8047cf0982ba161 Mon Sep 17 00:00:00 2001 From: vikdevelop Date: Thu, 28 May 2026 19:02:56 +0200 Subject: [PATCH 07/13] Exclude kdeconnect dir from configurations (#533) --- src/core/de_config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/de_config.py b/src/core/de_config.py index 6843fdd7..197e9116 100644 --- a/src/core/de_config.py +++ b/src/core/de_config.py @@ -296,8 +296,8 @@ def __init__(self): print("Saving KDE Plasma configuration...") os.makedirs("DE/xdg-config", exist_ok=True) os.makedirs("DE/xdg-data", exist_ok=True) - os.system(f"cp -R {home}/.config/[k]* ./DE/xdg-config/") - os.system(f"cp -R {home}/.local/share/[k]* ./DE/xdg-data/") + os.system(f"find {home}/.config/ -maxdepth 1 -name '[k]*' ! -name 'kdeconnect' -exec cp -R {} ./DE/xdg-config/ \;") + os.system(f"find {home}/.local/share -maxdepth 1 -name '[k]*' ! -name 'kdeconnect' -exec cp -R {} ./DE/xdg-data/ \;") for src, dst in KDE_DIRS_SAVE: if os.path.isfile(src): safe_copy(src, os.path.join("DE", dst)) From 36876fd1dde3e9f99a7e4b125d732eff49d5b6b6 Mon Sep 17 00:00:00 2001 From: vikdevelop Date: Fri, 29 May 2026 12:28:05 +0200 Subject: [PATCH 08/13] Fix xdg-paths logic in de_config.py, improve UI of flatpaks_installer_window.py and add a new strings to savedesktop.pot --- po/savedesktop.pot | 40 +++++++++++++++++ src/core/de_config.py | 33 +++++++++++++- src/gui/flatpaks_installer_window.py | 67 +++++++++++++++++++++++++++- 3 files changed, 136 insertions(+), 4 deletions(-) diff --git a/po/savedesktop.pot b/po/savedesktop.pot index 0a50f639..0caa6e7d 100644 --- a/po/savedesktop.pot +++ b/po/savedesktop.pot @@ -116,6 +116,46 @@ msgstr "" msgid "You can continue using your computer; everything runs in the background. Once the installation of your Flatpak apps and, if applicable, your user data is complete, a message will appear here and you’ll also see a system notification. You can also show the progress below." msgstr "" +#: src/gui/flatpaks_installer_window.py +msgid "Installation queue is empty. Nothing new to install." +msgstr "" + +#: src/gui/flatpaks_installer_window.py +msgid "Copying the Flatpak's user data to ~/.var/app" +msgstr "" + +#: src/gui/flatpaks_installer_window.py +msgid "✔ Copied Flatpak's user data" +msgstr "" + +#: src/gui/flatpaks_installer_window.py +msgid "Cleaning up orphaned user data..." +msgstr "" + +#: src/gui/flatpaks_installer_window.py +msgid "All useless apps and orphaned data have been removed" +msgstr "" + +#: src/gui/flatpaks_installer_window.py +msgid "Installing {count} new apps" +msgstr "" + +#: src/gui/flatpaks_installer_window.py +msgid "{app} is already available in the system." +msgstr "" + +#: src/gui/flatpaks_installer_window.py +msgid "↓ Installing {app} ({current}/{total})" +msgstr "" + +#: src/gui/flatpaks_installer_window.py +msgid "✔ Finished installing {app}" +msgstr "" + +#: src/gui/flatpaks_installer_window.py +msgid "✔ All operations have been completed successfully." +msgstr "" + #: src/gui/flatpaks_installer_window.py msgid "Your apps have been installed!" msgstr "" diff --git a/src/core/de_config.py b/src/core/de_config.py index 197e9116..4b86b6dd 100644 --- a/src/core/de_config.py +++ b/src/core/de_config.py @@ -296,8 +296,7 @@ def __init__(self): print("Saving KDE Plasma configuration...") os.makedirs("DE/xdg-config", exist_ok=True) os.makedirs("DE/xdg-data", exist_ok=True) - os.system(f"find {home}/.config/ -maxdepth 1 -name '[k]*' ! -name 'kdeconnect' -exec cp -R {} ./DE/xdg-config/ \;") - os.system(f"find {home}/.local/share -maxdepth 1 -name '[k]*' ! -name 'kdeconnect' -exec cp -R {} ./DE/xdg-data/ \;") + self.save_kde_cfg_data_dirs() for src, dst in KDE_DIRS_SAVE: if os.path.isfile(src): safe_copy(src, os.path.join("DE", dst)) @@ -316,6 +315,36 @@ def save_dconf(self): os.system("dconf dump / > ./General/dconf-settings.ini") print("[OK] Saved dconf") + def save_kde_config_data_dirs(self): + # Target dirs + target_base_dir = "./DE" + mapping = [ + (f"{self.home}/.config", Path(target_base_dir) / "xdg-config"), + (f"{self.home}/.local/share", Path(target_base_dir) / "xdg-data"), + ] + + for src_dir, dst_path in mapping: + # Creating destination dir + dst_path.mkdir(parents=True, exist_ok=True) + + # Find folders begining with 'k' or 'K' + for path_str in glob.glob(f"{src_dir}/[kK]*"): + src_path = Path(path_str) + + # Ignore kdeconnect + if src_path.name.lower() == "kdeconnect": + continue + + target = dst_path / src_path.name + + # Copying folders and files + if src_path.is_dir(): + if target.exists(): + shutil.rmtree(target) + shutil.copytree(src_path, target) + else: + shutil.copy2(src_path, target) + # ------------------------ # Import pipeline # ------------------------ diff --git a/src/gui/flatpaks_installer_window.py b/src/gui/flatpaks_installer_window.py index 4bdfc12e..981c90fd 100644 --- a/src/gui/flatpaks_installer_window.py +++ b/src/gui/flatpaks_installer_window.py @@ -7,7 +7,9 @@ class FlatpaksWindow(Adw.ApplicationWindow): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.set_title(_("Installing your Flatpak apps...")) + + self.win_title = f'{_("Installing your Flatpak apps...")} | Save Desktop' + self.set_title(self.win_title) # header bar and toolbarview self.headerbar = Adw.HeaderBar.new() @@ -15,6 +17,8 @@ def __init__(self, *args, **kwargs): self.toolbarview.add_top_bar(self.headerbar) self.set_content(self.toolbarview) + self.headerbar.pack_start(Gtk.Image.new_from_icon_name("io.github.vikdevelop.SaveDesktop-symbolic")) + self.set_size_request(360, 600) # primary layout @@ -137,8 +141,11 @@ def add_text_to_ui(self, text): # We DO NOT want to show the "[PROGRESS] 1/5" text in the UI log. return False + # 2. TRANSLATION (Translate the text right before showing it) + translated_text = self.translate_log_line(text) + # Insert normal text into the view - buffer.insert(end_iter, text) + buffer.insert(end_iter, translated_text) # Autoscroll new_end_iter = buffer.get_end_iter() @@ -148,6 +155,62 @@ def add_text_to_ui(self, text): return False + def translate_log_line(self, original_text): + # Remove whitespace/newlines for easier exact matching + text = original_text.strip() + + # --- STATIC EXACT MATCHES --- + if text == "✔ All operations have been completed successfully.": + return _("✔ All operations have been completed successfully.") + "\n" + + elif text == "Installation queue is empty. Nothing new to install.": + return _("Installation queue is empty. Nothing new to install.") + "\n" + + elif text == "[COPY] Copying the Flatpak's user data to ~/.var/app": + return _("Copying the Flatpak's user data to ~/.var/app") + "[COPY]\n" + + elif text == "✔ Copied Flatpak's user data": + return _("✔ Copied Flatpak's user data") + "\n" + + elif text == "[REMOVE] Cleaning up orphaned user data...": + return _("Cleaning up orphaned user data...") + "[REMOVE]\n" + + elif text == "[OK] All useless apps and orphaned data have been removed": + return _("All useless apps and orphaned data have been removed") + "[OK]\n" + + # --- DYNAMIC MATCHES (REGEX) --- + # 1. Matches: "↓ Installing com.example.App (1/5)" + match_installing = re.search(r"↓ Installing (.*) \((\d+)/(\d+)\)", text) + if match_installing: + app = match_installing.group(1) + current = match_installing.group(2) + total = match_installing.group(3) + # We translate the string with placeholders, then format it with variables + return _("↓ Installing {app} ({current}/{total})").format( + app=app, current=current, total=total + ) + "\n" + + # 2. Matches: "✔ Finished installing com.example.App" + match_finished = re.search(r"✔ Finished installing (.*)", text) + if match_finished: + app = match_finished.group(1) + return _("✔ Finished installing {app}").format(app=app) + "\n" + + # 3. Matches: "Installing 5 new apps" + match_new_apps = re.search(r"Installing (\d+) new apps", text) + if match_new_apps: + count = match_new_apps.group(1) + return _("Installing {count} new apps").format(count=count) + "\n" + + # 4. Matches: "[INFO] com.example.App is already available in the system." + match_info = re.search(r"\[INFO\] (.*) is already available in the system\.", text) + if match_info: + app = match_info.group(1) + return _("{app} is already available in the system.").format(app=app) + "[INFO]\n" + + # If no translation matches, just return the original English text + return original_text + def send_completion_notification(self): # Create the notification object with a title notification = Gio.Notification.new(_("Your apps have been installed!")) From e16d9146eeb45dfa647d414eb8dbb539e068ccac Mon Sep 17 00:00:00 2001 From: vikdevelop Date: Tue, 2 Jun 2026 08:56:24 +0200 Subject: [PATCH 09/13] improve code style --- src/core/flatpaks_installer.py | 20 +++++++---------- src/gui/flatpaks_installer_window.py | 32 ++++++++-------------------- 2 files changed, 17 insertions(+), 35 deletions(-) diff --git a/src/core/flatpaks_installer.py b/src/core/flatpaks_installer.py index 5ccb0983..4919d963 100644 --- a/src/core/flatpaks_installer.py +++ b/src/core/flatpaks_installer.py @@ -32,9 +32,7 @@ disabled_flatpaks = j["disabled-flatpaks"] keep_flatpaks = j["keep-flatpaks"] - # ========================================== - # PHASE 1: PRE-CALCULATION (Math only, no actions) - # ========================================== + # pre-calculation (math only, no actions) desired_flatpaks = {} if install_flatpaks: files_to_check = ['Flatpak_Apps/installed_flatpaks.sh', 'Flatpak_Apps/installed_user_flatpaks.sh'] if os.path.exists("Flatpak_Apps") else ['installed_flatpaks.sh', 'installed_user_flatpaks.sh'] @@ -64,7 +62,7 @@ if install_flatpaks: total_steps += len(apps_to_install) if not keep_flatpaks: total_steps += len(apps_to_remove) - total_steps += 1 # 1 extra step for orphaned dir cleanup + total_steps += 1 # one extra step for orphaned dir cleanup current_step = 0 @@ -77,11 +75,9 @@ def report_progress(): if total_steps == 0: print("There's no need to install any new apps, since they're all available on your system.", flush=True) else: - # ========================================== - # PHASE 2: EXECUTION - # ========================================== + # phase 2: execution - # 1. Restore Data + # Restore Data if copy_data: print("[COPY] Copying the Flatpak's user data to ~/.var/app", flush=True) if os.path.exists("app"): @@ -93,9 +89,9 @@ def report_progress(): tar_cmd.extend([f"--exclude={d_app}", f"--exclude=app/{d_app}"]) subprocess.run(tar_cmd) print("✔ Copied Flatpak's user data", flush=True) - report_progress() # <--- SEND UPDATE TO UI + report_progress() # sends update to UI - # 2. Install Apps + # Install Apps if install_flatpaks: for app in desired_flatpaks: if app in installed_apps: @@ -109,9 +105,9 @@ def report_progress(): os.system("flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo") subprocess.run(['flatpak', 'install', method, 'flathub', app, '-y']) print(f"✔ Finished installing {app}", flush=True) - report_progress() # <--- SEND UPDATE TO UI + report_progress() # sends update to UI - # 3. Remove Apps & Cleanup + # Remove Apps and Cleanup if not keep_flatpaks: for app, method in apps_to_remove.items(): print(f"[REMOVE] {method.title()} Flatpak: {app}", flush=True) diff --git a/src/gui/flatpaks_installer_window.py b/src/gui/flatpaks_installer_window.py index 981c90fd..60493494 100644 --- a/src/gui/flatpaks_installer_window.py +++ b/src/gui/flatpaks_installer_window.py @@ -75,33 +75,29 @@ def read_pipe_background(self): log_path = os.path.expanduser(f"{CACHE}/workspace/log.pipe") print(f"[THREAD] Looking for log file at: {log_path}") - # 1. Wait patiently until the logic script actually creates the file + # Wait patiently until the logic script actually creates the file while not os.path.exists(log_path): print(f"[THREAD] Still waiting for {log_path} to exist...") time.sleep(0.5) print("[THREAD] File found! Opening it now...") - # 2. Open the regular file ONCE and read it continuously + # Open the regular file ONCE and read it continuously try: with open(log_path, 'r') as log_file: while True: line = log_file.readline() if not line: - # Check if the host script finished and deleted the file if not os.path.exists(log_path): print("[THREAD] File was deleted. Killing thread cleanly.") - break # Kill the thread + break - # If file exists but no new data, chill for 100ms time.sleep(0.1) continue - # Send the new line to the main UI thread GLib.idle_add(self.add_text_to_ui, line) - # 3. Explicit kill switch: FIXED TO MATCH YOUR REAL STRING if "✔ All operations have been completed successfully." in line: print("[THREAD] Success string found. Stopping background read.") break @@ -114,18 +110,18 @@ def add_text_to_ui(self, text): buffer = self.text_view.get_buffer() end_iter = buffer.get_end_iter() - # 1. Full completion + # Full completion if "✔ All operations have been completed successfully." in text or "There's no need to install any new apps, since they're all available on your system." in text: self.progress_bar.set_fraction(1.0) self.send_completion_notification() cleanup_thread = threading.Thread(target=self.cache_cleanup) cleanup_thread.start() - # 2. Pulse effect for ANY active operation + # Pulse effect for ANY active operation if any(keyword in text for keyword in ["↓ Installing", "[COPY]", "[REMOVE]"]): self.progress_bar.pulse() - # 3. Unified hidden progress tracker + # Unified hidden progress tracker match = re.search(r"\[PROGRESS\] (\d+)/(\d+)", text) if match: try: @@ -137,17 +133,13 @@ def add_text_to_ui(self, text): except ValueError: pass - # CRITICAL: We return False here immediately! - # We DO NOT want to show the "[PROGRESS] 1/5" text in the UI log. return False - # 2. TRANSLATION (Translate the text right before showing it) translated_text = self.translate_log_line(text) - # Insert normal text into the view + buffer.insert(end_iter, translated_text) - # Autoscroll new_end_iter = buffer.get_end_iter() mark = buffer.create_mark(None, new_end_iter, False) self.text_view.scroll_to_mark(mark, 0.0, True, 0.0, 1.0) @@ -159,7 +151,6 @@ def translate_log_line(self, original_text): # Remove whitespace/newlines for easier exact matching text = original_text.strip() - # --- STATIC EXACT MATCHES --- if text == "✔ All operations have been completed successfully.": return _("✔ All operations have been completed successfully.") + "\n" @@ -178,25 +169,20 @@ def translate_log_line(self, original_text): elif text == "[OK] All useless apps and orphaned data have been removed": return _("All useless apps and orphaned data have been removed") + "[OK]\n" - # --- DYNAMIC MATCHES (REGEX) --- - # 1. Matches: "↓ Installing com.example.App (1/5)" match_installing = re.search(r"↓ Installing (.*) \((\d+)/(\d+)\)", text) if match_installing: app = match_installing.group(1) current = match_installing.group(2) total = match_installing.group(3) - # We translate the string with placeholders, then format it with variables return _("↓ Installing {app} ({current}/{total})").format( app=app, current=current, total=total ) + "\n" - # 2. Matches: "✔ Finished installing com.example.App" match_finished = re.search(r"✔ Finished installing (.*)", text) if match_finished: app = match_finished.group(1) return _("✔ Finished installing {app}").format(app=app) + "\n" - # 3. Matches: "Installing 5 new apps" match_new_apps = re.search(r"Installing (\d+) new apps", text) if match_new_apps: count = match_new_apps.group(1) @@ -240,14 +226,14 @@ def start_auto_close_timer(self): self.status_page.set_icon_name("done") self.status_page.set_description(_("This window will be closed automatically in 2 minutes.")) - return False # Mandatory: tells idle_add to only run this setup once + return False # tells idle_add to only run this setup once def close_app(self): # This is triggered when the 2 minutes are up. # Safely closes the current GTK window. self.close() - return False # Mandatory: tells the timeout timer not to repeat itself + return False # tells the timeout timer not to repeat itself class FlatpaksInstallerApp(Adw.Application): def __init__(self, **kwargs): From 3ef7b82ef6eeb63dea90c3dbb94d50a99ac8d5d7 Mon Sep 17 00:00:00 2001 From: vikdevelop Date: Tue, 2 Jun 2026 09:45:36 +0200 Subject: [PATCH 10/13] fix typo --- src/core/de_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/de_config.py b/src/core/de_config.py index 4b86b6dd..b7db6e32 100644 --- a/src/core/de_config.py +++ b/src/core/de_config.py @@ -296,7 +296,7 @@ def __init__(self): print("Saving KDE Plasma configuration...") os.makedirs("DE/xdg-config", exist_ok=True) os.makedirs("DE/xdg-data", exist_ok=True) - self.save_kde_cfg_data_dirs() + self.save_kde_config_data_dirs() for src, dst in KDE_DIRS_SAVE: if os.path.isfile(src): safe_copy(src, os.path.join("DE", dst)) From 5112cb68b22a353ce8519785d3d32a5236b9d4a4 Mon Sep 17 00:00:00 2001 From: vikdevelop Date: Sun, 28 Jun 2026 12:05:23 +0200 Subject: [PATCH 11/13] Bump to 4.1 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 9a649043..f1a9e076 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('savedesktop', - version: '4.1-beta', + version: '4.1', meson_version: '>= 1.0.0', default_options: [ 'warning_level=2', 'werror=false', ], ) From bd3a865dc3ef32e3b282c6d76736c61f910dfbc7 Mon Sep 17 00:00:00 2001 From: vikdevelop Date: Sun, 28 Jun 2026 12:15:45 +0200 Subject: [PATCH 12/13] Add release notes and update 7z to the latest version --- data/io.github.vikdevelop.SaveDesktop.metainfo.xml.in | 10 ++++++++++ io.github.vikdevelop.SaveDesktop.json | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/data/io.github.vikdevelop.SaveDesktop.metainfo.xml.in b/data/io.github.vikdevelop.SaveDesktop.metainfo.xml.in index e289d0f0..e47bd08a 100644 --- a/data/io.github.vikdevelop.SaveDesktop.metainfo.xml.in +++ b/data/io.github.vikdevelop.SaveDesktop.metainfo.xml.in @@ -66,6 +66,16 @@ + + https://github.com/vikdevelop/SaveDesktop/releases/tag/4.0 + +
    +
  • Added support for Niri WM and LXQt DE
  • +
  • Added a status window for showing the progress of Flatpak apps installation after configuration import
  • +
  • Updated the GNOME runtime and 7-Zip to the latest versions
  • +
+
+
https://github.com/vikdevelop/SaveDesktop/releases/tag/4.0 diff --git a/io.github.vikdevelop.SaveDesktop.json b/io.github.vikdevelop.SaveDesktop.json index b8fd8c19..14b2e014 100644 --- a/io.github.vikdevelop.SaveDesktop.json +++ b/io.github.vikdevelop.SaveDesktop.json @@ -47,8 +47,8 @@ { "type" : "git", "url" : "https://github.com/ip7z/7zip.git", - "tag" : "26.01", - "commit" : "8c63d71ff886bda90c86db28466287f977374237" + "tag" : "26.02", + "commit" : "f9d78aff31a5f2521ae7ddbdc97c4a8855808959" } ], "x-checker-data" : { From 4e0b1c0ff89d964dda916117584cd3bdb4a223bd Mon Sep 17 00:00:00 2001 From: vikdevelop Date: Sun, 28 Jun 2026 12:24:19 +0200 Subject: [PATCH 13/13] Update ci.yml --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ddba87f6..db2624e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,8 +25,8 @@ jobs: - name: Install GNOME Platform and SDK run: | flatpak remote-add --if-not-exists --user flathub https://flathub.org/repo/flathub.flatpakrepo - flatpak install --user -y flathub org.gnome.Platform//49 - flatpak install --user -y flathub org.gnome.Sdk//49 + flatpak install --user -y flathub org.gnome.Platform//50 + flatpak install --user -y flathub org.gnome.Sdk//50 - name: Build Flatpak run: |