From 93d870b5bf0a58aae4dab2a2a4fb3a57e914eb7c Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 20 Aug 2026 10:13:37 +0900 Subject: [PATCH] feat: uninstall wizard WIP --- dzgui/api/shortcuts.py | 7 + dzgui/main.py | 24 ++-- dzgui/strings/uninstall.py | 38 ++++++ dzgui/views/dialogs/uninstall.py | 226 +++++++++++++++++++++++++++++++ dzgui/views/dialogs/wizard.py | 17 ++- 5 files changed, 293 insertions(+), 19 deletions(-) create mode 100644 dzgui/strings/uninstall.py create mode 100644 dzgui/views/dialogs/uninstall.py diff --git a/dzgui/api/shortcuts.py b/dzgui/api/shortcuts.py index 154645e..50c1d33 100644 --- a/dzgui/api/shortcuts.py +++ b/dzgui/api/shortcuts.py @@ -160,6 +160,13 @@ class Shortcuts: NEW_ENTRY["tags"] = {} return NEW_ENTRY + def delete_shortcut(self, start_path: Path) -> None: + for s in self.shortcuts["shortcuts"]: + if self.shortcuts[s]["StartDir"] == str(start_path): + del self.shortcuts[s] + break + self.save_shortcuts() + def save_shortcuts(self) -> None: try: backup = self.shortcuts_path.with_suffix(".vdf.bak") diff --git a/dzgui/main.py b/dzgui/main.py index 731f58e..ff59ba8 100644 --- a/dzgui/main.py +++ b/dzgui/main.py @@ -8,30 +8,26 @@ from dzgui.init.prefix import get_version from dzgui.util.map_count import set_map_count from dzgui.util.strings import flags +from dzgui.views.dialogs.uninstall import UninstallWizard +from dzgui.config.xdg import get_xdg_paths + parser = argparse.ArgumentParser(description=flags.description) -parser.add_argument("-d", "--debug", action="store_true", help=flags.debug) -parser.add_argument("-m", "--map", action="store_true", help=flags.map_count) -parser.add_argument("-u", "--uninstall", action="store_true", help=flags.uninstall) -parser.add_argument("-v", "--version", action="store_true", help=flags.version) +group = parser.add_mutually_exclusive_group() +group.add_argument("-d", "--debug", action="store_true", help=flags.debug) +group.add_argument("-m", "--map", action="store_true", help=flags.map_count) +group.add_argument("-u", "--uninstall", action="store_true", help=flags.uninstall) +group.add_argument("-v", "--version", action="store_true", help=flags.version) args = parser.parse_args() -def uninstall() -> None: - # TODO: uninstall data files (-u) - # -u removes state, log, freedesktop - # XDG_SHARE_HOME/dzgui - # XDG_STATE_HOME/dzgui - # XDG_DATA_HOME/dzgui - pass - - def main() -> None: # TODO: isolate single flags if args.version is True: print(get_version()) sys.exit(0) if args.uninstall is True: - uninstall() + paths = get_xdg_paths() + UninstallWizard(False, paths) sys.exit(0) if args.debug is True: warnings.filterwarnings("default", category=DeprecationWarning) diff --git a/dzgui/strings/uninstall.py b/dzgui/strings/uninstall.py new file mode 100644 index 0000000..676392f --- /dev/null +++ b/dzgui/strings/uninstall.py @@ -0,0 +1,38 @@ +pg1_title = "Choose uninstall targets" +pg1_heading = "Choose uninstall targets" +pg1_blurb = "Here you can choose which DZGUI configuration/state files to remove. " + +pg2_title = "Complete" +pg2_heading = "Uninstall complete" +pg2_blurb = "Finished removing the selected files. See the system log for errors." + +config_label = "Configuration files" +config_details = ( + "User preferences and API key. " + "Keep this file if you want to import it again later." +) +desktop_label = "Desktop shortcut" +desktop_details = "Shortcut set on the desktop, if applicable." + +start_menu_label = "Start menu shortcut" +start_menu_details = ( + "Shortcut set under the Games category of the start menu, if applicable." +) +state_label = "Long-term state files" +state_details = ( + "UI size, filter preferences, column size, server notes, and mod signatures." +) +steam_shortcut = "Steam shortcut" +steam_details = "Shortcut to DZGUI installed within Steam, if applicable. Requires Steam to be restarted to reflect the change." + + +path_remove_prefix = "The following path will be removed:" +not_installed = "It looks like DZGUI was not previously set up; nothing to uninstall." +standalone_uninstall = ( + "It looks like you are running the standalone version of DZGUI. " + "To finalize the uninstall process, you can remove the 'dzgui' file." +) +system_uninstall = ( + "It looks like DZGUI was installed via your system package manager or directly from source code.\n" + "To finalize the uninstall process, follow the standard removal procedure for that context." +) diff --git a/dzgui/views/dialogs/uninstall.py b/dzgui/views/dialogs/uninstall.py new file mode 100644 index 0000000..afda8eb --- /dev/null +++ b/dzgui/views/dialogs/uninstall.py @@ -0,0 +1,226 @@ +import os +import subprocess + +from pathlib import Path +from typing import Self, TYPE_CHECKING + +from dzgui.api.shortcuts import Shortcuts +from dzgui.const.constants import APP_NAME +from dzgui.strings import uninstall +from dzgui.util._json import read_json +from dzgui.util.css import load_css +from dzgui.views.components.box import HBox +from dzgui.views.dialogs.wizard import ScrolledWizardPage, CheckboxWithLabel + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, GLib, Pango # noqa E402 + + +class Assistant(Gtk.Assistant): + def __init__(self, is_deck: bool, paths: dict[str, str]): + super().__init__() + if is_deck: + # NOTE: deemed to be "safe" dimensions that exclude taskbar size + self.set_default_size(1085, 670) + else: + self.set_default_size(1500, 900) + + self.set_forward_page_func(self._advance_page) + + pyapp = os.getenv("PYAPP") + + self.page1 = UninstallPage(paths, pyapp) + self.page2 = CompletionPage(pyapp) + + self.append_page(self.page1) + self.set_page_type(self.page1, Gtk.AssistantPageType.INTRO) + self.set_page_title(self.page1, uninstall.pg1_title) + self.set_page_complete(self.page1, True) + + self.append_page(self.page2) + self.set_page_title(self.page2, uninstall.pg2_title) + self.set_page_type(self.page2, Gtk.AssistantPageType.SUMMARY) + + self.connect("cancel", self.destroy_and_quit) + self.connect("close", self.destroy_and_quit) + self.show_all() + load_css() + + def _advance_page(self, index: int) -> int: + cur_page = self.get_nth_page(index) + if cur_page == self.page1: + self.page1.uninstall() + return index + 1 + + def destroy_and_quit(self, widget: Self) -> None: + self.destroy() + Gtk.main_quit() + + +class Window(Gtk.Window): + def __init__(self, is_deck: bool, paths: dict[str, str]) -> None: + super().__init__(title=APP_NAME, icon_name=APP_NAME) + self.assistant = Assistant(is_deck, paths) + + +class CompletionPage(ScrolledWizardPage): + def __init__(self, pyapp: str | None): + super().__init__(heading=uninstall.pg2_heading, description=uninstall.pg2_blurb) + + text = ( + uninstall.standalone_uninstall + if pyapp is not None + else uninstall.system_uninstall + ) + + label = Gtk.Label( + label=text, + wrap_mode=Pango.WrapMode.WORD, + margin=10, + max_width_chars=80, + justify=Gtk.Justification.CENTER, + ) + frame = Gtk.Frame() + frame.add(label) + self.add_start(frame) + + +class FileLabel(HBox): + def __init__(self, path: Path) -> None: + super().__init__(spacing=10) + + label = Gtk.Label(label=uninstall.path_remove_prefix, halign=Gtk.Align.START) + textview = Gtk.TextView( + editable=False, + halign=Gtk.Align.START, + left_margin=10, + right_margin=10, + ) + textview.set_buffer(Gtk.TextBuffer(text=str(path))) + self.extend([label, textview]) + + +class CheckboxWithPath(CheckboxWithLabel): + def __init__( + self, label: str, details: str, path: Path, active: bool = True + ) -> None: + super().__init__(text=label, blurb_text=details) + + self.conf_path = path + self.set_active(active) + fl = FileLabel(path) + self.indent_below(fl) + + def get_conf_path(self) -> Path: + return self.conf_path + + +class UninstallPage(ScrolledWizardPage): + def __init__(self, paths: dict[str, str], pyapp: str | None): + super().__init__(heading=uninstall.pg1_heading, description=uninstall.pg1_blurb) + + self.pyapp = pyapp + + config = Path(paths["XDG_CONFIG_HOME"]) + state = Path(paths["XDG_STATE_HOME"]) + self.share = Path(paths["XDG_DATA_HOME"]) + shortcut = self.share.parent.joinpath("applications/dzgui.desktop") + desktop = config.parent.parent.joinpath("Desktop/dzgui.desktop") + self.steam_path = self.parse_steam_path(config, self.share) + + self.boxes: list[CheckboxWithPath] = [] + self.checks_area = Gtk.Box( + orientation=Gtk.Orientation.VERTICAL, spacing=20, margin_top=20 + ) + self.config_box = CheckboxWithPath( + uninstall.config_label, uninstall.config_details, config, active=False + ) + self.desktop_box = CheckboxWithPath( + uninstall.desktop_label, uninstall.desktop_details, desktop + ) + self.start_menu_box = CheckboxWithPath( + uninstall.start_menu_label, uninstall.start_menu_details, shortcut + ) + self.share_box = CheckboxWithPath( + uninstall.desktop_label, uninstall.desktop_details, desktop + ) + self.state_box = CheckboxWithPath( + uninstall.state_label, uninstall.state_details, state + ) + self.steam_box = CheckboxWithLabel( + uninstall.steam_shortcut, uninstall.steam_details + ) + + for box in ( + self.config_box, + self.state_box, + self.desktop_box, + self.start_menu_box, + ): + self.checks_area.add(box) + self.boxes.append(box) + + self.checks_area.add(self.steam_box) + + self.add_start(self.checks_area) + self.show_all() + + def parse_steam_path(self, config: Path, steam: Path) -> Path | None: + try: + file = config.joinpath("config.json") + conf = read_json(file) + steam = conf["default_steam_path"] + self.steam_path = Path(steam) + return Path(steam) + except Exception as e: + print(e) + return None + + def wipe_conf_file(self, box: CheckboxWithPath) -> None: + if box.get_active(): + path = box.get_conf_path() + try: + # TODO: + # path.unlink() + print(f"Deleted '{path}'") + except Exception as e: + print(e) + + def wipe_steam_shortcut(self) -> None: + # TODO: + pass + # try: + # shortcuts = Shortcuts(self.steam_path) + # shortcuts.delete_shortcut(self.share) + # except Exception as e: + # print(e) + + def wipe_pyapp(self) -> None: + if self.pyapp is None: + return + res = subprocess.run([self.pyapp, "self", "remove"]) + print(res) + + def uninstall(self) -> None: + for box in self.boxes: + self.wipe_conf_file(box) + if self.steam_box.get_active(): + self.wipe_steam_shortcut() + # TODO: + # self.wipe_pyapp() + pass + + +class UninstallWizard(Gtk.Application): + def __init__(self, is_deck: bool, paths: dict[str, str]) -> None: + super().__init__() + config = Path(paths["XDG_CONFIG_HOME"]) + # TODO: tests + if not config.is_dir() or not config.joinpath("config.json").is_file(): + print(uninstall.not_installed) + return + GLib.set_prgname(APP_NAME) + self.win = Window(is_deck, paths) + Gtk.main() diff --git a/dzgui/views/dialogs/wizard.py b/dzgui/views/dialogs/wizard.py index 3a57570..d0f0aa0 100644 --- a/dzgui/views/dialogs/wizard.py +++ b/dzgui/views/dialogs/wizard.py @@ -99,6 +99,8 @@ class ScrolledWizardPage(Gtk.ScrolledWindow): self.box.pack_start(image, expand=False, fill=True, padding=0) self.add(self.box) + self.box.pack_start(self.heading, expand=False, fill=True, padding=0) + self.box.pack_start(self.description, expand=False, fill=True, padding=0) def get_page_type(self) -> Gtk.AssistantPageType: return self.page_type @@ -123,12 +125,9 @@ class EnumeratedWizardPage(ScrolledWizardPage): def __init__(self, enum: PageNum, heading: str, description: str) -> None: super().__init__(heading=heading, description=description) - self.enum = enum self.prog = Progress() self.box.pack_end(self.prog, expand=False, fill=False, padding=0) - self.box.pack_start(self.heading, expand=False, fill=True, padding=0) - self.box.pack_start(self.description, expand=False, fill=True, padding=0) def get_enum(self) -> PageNum: return self.enum @@ -541,7 +540,9 @@ class Assistant(Gtk.Assistant): return self.set_page_complete(page, True) - def _add_page(self, page: EnumeratedWizardPage, ptype: Gtk.AssistantPageType) -> None: + def _add_page( + self, page: EnumeratedWizardPage, ptype: Gtk.AssistantPageType + ) -> None: self.append_page(page) self.set_page_type(page, ptype) self.set_page_title(page, page.get_title()) @@ -567,15 +568,21 @@ class CheckboxWithLabel(Gtk.Box): def __init__(self, text: str, blurb_text: str) -> None: super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=5) + self.indent = 20 self.button = Gtk.CheckButton(label=text) self.button.set_active(True) - label = Gtk.Label(label="", halign=Gtk.Align.START, margin_start=20) + label = Gtk.Label(label="", halign=Gtk.Align.START, margin_start=self.indent) + wrapped = textwrap.fill(blurb_text, width=100) label.set_markup(f"- {wrapped}") for el in self.button, label: self.add(el) + def indent_below(self, widget: Gtk.Widget) -> None: + widget.set_margin_start(self.indent) + self.add(widget) + def get_checkbox(self) -> Gtk.CheckButton: return self.button