From eb3ccf7669393457e17a79edb1b34591854dfade Mon Sep 17 00:00:00 2001
From: Marco Ceppi <marco@ceppi.net>
Date: Sun, 6 Aug 2023 22:44:34 -0400
Subject: [PATCH 1/3] fix: support for multiple package screens in one config

---
 tests/example-multi-package.yml        | 69 ++++++++++++++++++++++++++
 yafti/screen/package/screen/install.py |  6 ++-
 yafti/screen/package/screen/package.py | 13 +++--
 yafti/screen/package/screen/picker.py  | 14 +++---
 yafti/screen/package/state.py          | 16 +++---
 yafti/screen/package/utils.py          |  6 +++
 6 files changed, 106 insertions(+), 18 deletions(-)
 create mode 100644 tests/example-multi-package.yml

diff --git a/tests/example-multi-package.yml b/tests/example-multi-package.yml
new file mode 100644
index 0000000..aa31341
--- /dev/null
+++ b/tests/example-multi-package.yml
@@ -0,0 +1,69 @@
+title: uBlue First Boot
+properties:
+  mode: "run-on-change"
+actions:
+  pre:
+    - run: /full/path/to/bin --with --params
+    - run: /another/command run
+    - yafti.plugin.flatpak:
+        install: org.gnome.Calculator
+  post:
+    - run: /run/these/commands --after --all --screens
+screens:
+  first-screen:
+    source: yafti.screen.title
+    values:
+      title: "That was pretty cool"
+      icon: "/path/to/icon"
+      description: |
+        Time to play overwatch
+  can-we-modify-your-flatpaks:
+    source: yafti.screen.consent
+    values:
+      title: Welcome traveler
+      condition:
+        run: flatpak remotes --system | grep fedora
+      description: |
+        This tool modifies your flatpaks and flatpak sources. If you do not want to do this exit the installer.
+        For new users just do it (tm)
+      actions:
+        - run: flatpak remote-delete fedora --force
+        - run: flatpak remove --system --noninteractive --all
+  applications:
+    source: yafti.screen.package
+    values:
+      title: Install flatpaks
+      show_terminal: true
+      package_manager: yafti.plugin.flatpak
+      groups:
+        Core:
+          description: All the good stuff
+          packages:
+            - Calculator: org.gnome.Calculator
+            - Firefox: org.mozilla.firefox
+        Gaming:
+          description: GAMES GAMES GAMES
+          default: false
+          packages:
+            - Steam: com.valvesoftware.Steam
+            - Games: org.gnome.Games
+  applications-two:
+    source: yafti.screen.package
+    values:
+      title: Install more flatpaks
+      show_terminal: true
+      package_manager: yafti.plugin.flatpak
+      groups:
+        Office:
+          description: All the work stuff
+          default: false
+          packages:
+            - LibreOffice: org.libreoffice.LibreOffice
+            - Calendar: org.gnome.Calendar
+  final-screen:
+    source: yafti.screen.title
+    values:
+      title: "All done"
+      icon: "/atph/to/icon"
+      description: |
+        Thanks for installing, join the community, next steps
diff --git a/yafti/screen/package/screen/install.py b/yafti/screen/package/screen/install.py
index 02f4ee9..94e12b6 100644
--- a/yafti/screen/package/screen/install.py
+++ b/yafti/screen/package/screen/install.py
@@ -9,7 +9,7 @@
 from yafti import log
 from yafti.abc import YaftiScreen
 from yafti.screen.console import ConsoleScreen
-from yafti.screen.package.state import STATE
+from yafti.screen.package.state import PackageScreenState
 
 _xml = """\
 <?xml version="1.0" encoding="UTF-8"?>
@@ -77,6 +77,7 @@ class PackageInstallScreen(YaftiScreen, Gtk.Box):
 
     def __init__(
         self,
+        id: str,
         title: str = "Package Installation",
         package_manager: str = "yafti.plugin.flatpak",
         package_manager_defaults: Optional[dict] = None,
@@ -89,6 +90,7 @@ def __init__(
         self.package_manager = PLUGINS.get(package_manager)
         self.package_manager_defaults = package_manager_defaults or {}
         self.btn_console.connect("clicked", self.toggle_console)
+        self.state = PackageScreenState(id)
 
     async def on_activate(self):
         if self.started or self.already_run:
@@ -114,7 +116,7 @@ async def do_pulse(self):
     def draw(self):
         self.console.hide()
         self.append(self.console)
-        packages = [item.replace("pkg:", "") for item in STATE.get_on("pkg:")]
+        packages = [item.replace("pkg:", "") for item in self.state.get_on("pkg:")]
         asyncio.create_task(self.do_pulse())
         return self.install(packages)
 
diff --git a/yafti/screen/package/screen/package.py b/yafti/screen/package/screen/package.py
index 62f68ab..930f71f 100644
--- a/yafti/screen/package/screen/package.py
+++ b/yafti/screen/package/screen/package.py
@@ -7,8 +7,8 @@
 from yafti.abc import YaftiScreen, YaftiScreenConfig
 from yafti.screen.package.models import PackageConfig, PackageGroupConfig
 from yafti.screen.package.screen import PackageInstallScreen, PackagePickerScreen
-from yafti.screen.package.state import STATE
-from yafti.screen.package.utils import parse_packages
+from yafti.screen.package.state import PackageScreenState
+from yafti.screen.package.utils import parse_packages, generate_fingerprint
 
 _xml = """\
 <?xml version="1.0" encoding="UTF-8"?>
@@ -62,17 +62,22 @@ def __init__(
         self.show_terminal = show_terminal
         self.package_manager = package_manager
         self.package_manager_defaults = package_manager_defaults
-        STATE.load(parse_packages(self.packages))
+        self.fingerprint = generate_fingerprint(self.packages)
+        self.state = PackageScreenState(self.fingerprint)
+        self.state.load(parse_packages(self.packages))
         self.pkg_carousel.connect("page-changed", self.changed)
         self.draw()
 
     def draw(self):
         self.pkg_carousel.append(
-            PackagePickerScreen(title=self.title, packages=self.packages)
+            PackagePickerScreen(
+                id=self.fingerprint, title=self.title, packages=self.packages
+            )
         )
         self.pkg_carousel.append(
             PackageInstallScreen(
                 title=self.title,
+                id=self.fingerprint,
                 package_manager=self.package_manager,
                 package_manager_defaults=self.package_manager_defaults,
             )
diff --git a/yafti/screen/package/screen/picker.py b/yafti/screen/package/screen/picker.py
index a522923..929b715 100644
--- a/yafti/screen/package/screen/picker.py
+++ b/yafti/screen/package/screen/picker.py
@@ -7,7 +7,7 @@
 from yafti import log
 from yafti.abc import YaftiScreen
 from yafti.screen.dialog import DialogBox
-from yafti.screen.package.state import STATE
+from yafti.screen.package.state import PackageScreenState
 from yafti.screen.utils import find_parent
 
 _xml = """\
@@ -58,6 +58,7 @@ class Config(BaseModel):
 
     def __init__(
         self,
+        id: str,
         title: str,
         packages: list | dict,
         **kwargs,
@@ -65,6 +66,7 @@ def __init__(
         super().__init__(**kwargs)
         self.status_page.set_title(title)
         self.packages = packages
+        self.state = PackageScreenState(id)
         self.draw()
 
     def draw(self):
@@ -76,17 +78,17 @@ def draw(self):
             action_row = Adw.ActionRow(title=name, subtitle=details.get("description"))
 
             def state_set(group, _, value):
-                STATE.set(f"group:{group}", value)
+                self.state.set(f"group:{group}", value)
                 d = self.packages.get(group)
                 for pkg in d.get("packages", []):
                     for pkg_name in pkg.values():
                         if isinstance(pkg_name, dict):
                             pkg_name = json.dumps(pkg_name)
-                        STATE.set(f"pkg:{pkg_name}", value)
+                        self.state.set(f"pkg:{pkg_name}", value)
 
             state_set(name, None, details.get("default", True))
             _switcher = Gtk.Switch()
-            _switcher.set_active(STATE.get(f"group:{name}"))
+            _switcher.set_active(self.state.get(f"group:{name}"))
             _switcher.set_valign(Gtk.Align.CENTER)
 
             state_set_fn = partial(state_set, name)
@@ -148,12 +150,12 @@ def _build_apps(self, packages: list):
                 _app_switcher = Gtk.Switch()
                 if isinstance(pkg, dict):
                     pkg = json.dumps(pkg)
-                _app_switcher.set_active(STATE.get(f"pkg:{pkg}"))
+                _app_switcher.set_active(self.state.get(f"pkg:{pkg}"))
                 _app_switcher.set_valign(Gtk.Align.CENTER)
 
                 def set_state(pkg, btn, value):
                     log.debug("state-set", pkg=pkg, value=value)
-                    STATE.set(f"pkg:{pkg}", value)
+                    self.state.set(f"pkg:{pkg}", value)
 
                 set_state_func = partial(set_state, pkg)
                 _app_switcher.connect("state-set", set_state_func)
diff --git a/yafti/screen/package/state.py b/yafti/screen/package/state.py
index 44f61b7..0e66de0 100644
--- a/yafti/screen/package/state.py
+++ b/yafti/screen/package/state.py
@@ -4,6 +4,16 @@
 class PackageScreenState:
     __slots__ = ["state"]
 
+    def __new__(cls, id: str):
+        if not hasattr(cls, "instances"):
+            cls.instances = {}
+        if id not in cls.instances:
+            cls.instances[id] = super(PackageScreenState, cls).__new__(cls)
+        return cls.instances[id]
+
+    def __init__(self, id: str):
+        self.state = {}
+
     @classmethod
     def from_dict(cls, data: dict) -> "PackageScreenState":
         self = cls()
@@ -15,9 +25,6 @@ def load(self, data: dict):
         for k, v in data.items():
             self.set(k, v)
 
-    def __init__(self):
-        self.state = {}
-
     @validate_arguments
     def remove(self, item: str) -> None:
         del self.state[item]
@@ -53,6 +60,3 @@ def keys(self) -> list[str]:
     @validate_arguments
     def get(self, item: str) -> bool:
         return self.state.get(item)
-
-
-STATE = PackageScreenState()
diff --git a/yafti/screen/package/utils.py b/yafti/screen/package/utils.py
index 7e6a0f8..0d6a6df 100644
--- a/yafti/screen/package/utils.py
+++ b/yafti/screen/package/utils.py
@@ -1,4 +1,10 @@
 import json
+from typing import Any
+from hashlib import sha256
+
+
+def generate_fingerprint(obj: Any):
+    return sha256(json.dumps(obj).encode()).hexdigest()
 
 
 def parse_packages(packages: dict | list) -> dict:

From 90245d8bc6564bea88f84d32403a2e32b6a3fb31 Mon Sep 17 00:00:00 2001
From: Marco Ceppi <marco@ceppi.net>
Date: Sun, 6 Aug 2023 22:45:33 -0400
Subject: [PATCH 2/3] fix: add --force flag for running from cli

---
 yafti/__main__.py | 11 +++++++++--
 yafti/app.py      | 17 +++++++++--------
 2 files changed, 18 insertions(+), 10 deletions(-)

diff --git a/yafti/__main__.py b/yafti/__main__.py
index d249021..e5ba677 100644
--- a/yafti/__main__.py
+++ b/yafti/__main__.py
@@ -15,6 +15,7 @@
 """
 
 import logging
+from typing import Annotated
 
 import typer
 import yaml
@@ -25,12 +26,18 @@
 from yafti.parser import Config
 
 
-def run(config: typer.FileText = typer.Argument("/etc/yafti.yml"), debug: bool = False):
+def run(
+    config: typer.FileText = typer.Argument("/etc/yafti.yml"),
+    debug: bool = False,
+    force_run: Annotated[
+        bool, typer.Option("-f", "--force", help="Ignore run mode and force run")
+    ] = False,
+):
     log.set_level(logging.DEBUG if debug else logging.INFO)
     log.debug("starting up", config=config, debug=debug)
     config = Config.parse_obj(yaml.safe_load(config))
     app = Yafti(config)
-    app.run(None)
+    app.run(None, force_run=force_run)
 
 
 def app():
diff --git a/yafti/app.py b/yafti/app.py
index f7fbec7..e99c11e 100644
--- a/yafti/app.py
+++ b/yafti/app.py
@@ -31,7 +31,7 @@ def __init__(self, cfg: Config = None, loop=None):
         self.config = cfg
         self.loop = loop or gbulb.get_event_loop()
 
-    def run(self, *args, **kwargs):
+    def run(self, *args, force_run: bool = False, **kwargs):
         configured_mode = self.config.properties.mode
         _p: Path = self.config.properties.path.expanduser()
         # TODO(GH-#103): Remove this prior to 1.0 release. Start.
@@ -43,15 +43,16 @@ def run(self, *args, **kwargs):
                 _p.unlink()
             _old_p.rename(_p)
         # TODO(GH-#103): End.
-        if configured_mode == YaftiRunModes.disable:
-            return
-
-        if configured_mode == YaftiRunModes.changed:
-            if _p.exists() and _p.read_text() == self.config_sha:
+        if not force_run:
+            if configured_mode == YaftiRunModes.disable:
                 return
 
-        if configured_mode == YaftiRunModes.ignore and _p.exists():
-            return
+            if configured_mode == YaftiRunModes.changed:
+                if _p.exists() and _p.read_text() == self.config_sha:
+                    return
+
+            if configured_mode == YaftiRunModes.ignore and _p.exists():
+                return
 
         super().run(*args, **kwargs)
 

From 02557c0b7bae3fa0235e4891c0b8fbfae4f9d28f Mon Sep 17 00:00:00 2001
From: Marco Ceppi <marco@ceppi.net>
Date: Sun, 6 Aug 2023 22:52:26 -0400
Subject: [PATCH 3/3] chore(ci): fix action, update unit tests

---
 .github/workflows/ci.yml           |  4 +++-
 tests/test_screen_package_state.py | 25 +++++++++----------------
 yafti/screen/package/state.py      |  6 ------
 3 files changed, 12 insertions(+), 23 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0a9c679..f7e992c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -22,7 +22,9 @@ jobs:
           virtualenvs-create: true
           virtualenvs-in-project: true
       - name: System Deps
-        run: sudo apt install libgirepository1.0-dev libgtk-3-dev libadwaita-1-dev
+        run: |
+          sudo apt update
+          sudo apt install libgirepository1.0-dev libgtk-3-dev libadwaita-1-dev
       - name: Cache Dependencies
         id: cache-deps
         uses: actions/cache@v3
diff --git a/tests/test_screen_package_state.py b/tests/test_screen_package_state.py
index 864a086..5b7edbd 100644
--- a/tests/test_screen_package_state.py
+++ b/tests/test_screen_package_state.py
@@ -4,34 +4,27 @@
 
 
 def test_state_set():
-    state = PackageScreenState()
+    state = PackageScreenState("test_state_set")
     state.set("hello", True)
     assert state.get("hello") is True
 
 
 def test_state_set_fail():
-    state = PackageScreenState()
+    state = PackageScreenState("test_state_set_fail")
     with pytest.raises(ValidationError):
         state.set("hello", "world")
 
 
 def test_state_load():
     input = {"hello": True, "world": False}
-    state = PackageScreenState()
+    state = PackageScreenState("test_state_load")
     state.load(input)
     assert state.get("hello") is True
     assert state.get("world") is False
 
 
-def test_state_from_dict():
-    input = {"hello": True, "world": False}
-    state = PackageScreenState.from_dict(input)
-    assert state.get("hello") is True
-    assert state.get("world") is False
-
-
 def test_state_remove():
-    state = PackageScreenState()
+    state = PackageScreenState("test_state_remove")
     state.set("kenobi", False)
     state.set("general", True)
     assert state.get("kenobi") is False
@@ -42,7 +35,7 @@ def test_state_remove():
 
 
 def test_state_on_off():
-    state = PackageScreenState()
+    state = PackageScreenState("test_state_on_off")
     state.on("grievous")
     assert state.get("grievous") is True
     state.off("grievous")
@@ -55,7 +48,7 @@ def test_state_on_off():
 
 
 def test_state_toggle():
-    state = PackageScreenState()
+    state = PackageScreenState("test_state_toggle")
     state.on("chewy")
     assert state.get("chewy") is True
     state.toggle("chewy")
@@ -65,13 +58,13 @@ def test_state_toggle():
 
 
 def test_state_toggle_error():
-    state = PackageScreenState()
+    state = PackageScreenState("test_state_toggle_error")
     with pytest.raises(KeyError):
         state.toggle("barf")
 
 
 def test_state_get_on():
-    state = PackageScreenState()
+    state = PackageScreenState("test_state_get_on")
     state.on("chewy")
     state.on("han")
     state.off("greedo")
@@ -81,7 +74,7 @@ def test_state_get_on():
 
 
 def test_state_keys():
-    state = PackageScreenState()
+    state = PackageScreenState("test_state_keys")
     state.on("AA")
     state.on("BB")
     state.off("CC")
diff --git a/yafti/screen/package/state.py b/yafti/screen/package/state.py
index 0e66de0..4b465a1 100644
--- a/yafti/screen/package/state.py
+++ b/yafti/screen/package/state.py
@@ -14,12 +14,6 @@ def __new__(cls, id: str):
     def __init__(self, id: str):
         self.state = {}
 
-    @classmethod
-    def from_dict(cls, data: dict) -> "PackageScreenState":
-        self = cls()
-        self.load(data)
-        return self
-
     @validate_arguments
     def load(self, data: dict):
         for k, v in data.items():