Repository: ultrabug/py3status

Issue:
group: do not cycle when urgent
Since we merged PR to support `frame: pls expose urgent modules`, there have been several "false" positive for urgent in frame. The reason is that the group can cycle modules including urgent ones... so you expands the frame, see nothing, and scroll the group to find the hidden urgent module.

Too many times when I get an urgent, I unfold and see nothing. I think group should stop cycling when urgent... so group and frame could work together much better than before.


Discussion:
Things can be improved.  I think that maybe with the group if one or more module(s) is urgent then we just cycle between urgent modules.  The problem is what if as a user say github is urgent but I still want the group to cycle?

The frame should probably learn to only show as urgent if one of it's content it is showing is urgent. It would cause the urgent to come and go though which would be annoying.


>The problem is what if as a user say github is urgent but I still want the group to cycle?

We have `urgent` for github, scratchpad_async, group, imap, pomodoro, and frame.

The user should not ignore urgent then... or use `allow_urgent` from yet-to-be-merged `Universal allow_urgent` PR to disable github urgent so it can keep cycling.... since that user seems to not want to be bothered with urgents. 

Resolving patch:
diff --git a/py3status/module.py b/py3status/module.py
--- a/py3status/module.py
+++ b/py3status/module.py
@@ -443,7 +443,10 @@ def process_composite(self, response):
             raise Exception(err)
 
         # set markup
-        if "markup" in self.py3status_module_options and self.module_name != "frame":
+        if "markup" in self.py3status_module_options and self.module_name not in [
+            "frame",
+            "group",
+        ]:
             markup = self.py3status_module_options["markup"]
             line = ""
             for item in composite:
diff --git a/py3status/modules/group.py b/py3status/modules/group.py
--- a/py3status/modules/group.py
+++ b/py3status/modules/group.py
@@ -91,6 +91,7 @@
 
 # maximum wait for initial content at startup
 MAX_NO_CONTENT_WAIT = 5
+STRING_ERROR = "missing modules"
 
 
 class Py3status:
@@ -115,17 +116,16 @@ class Meta:
         container = True
 
     def post_config_hook(self):
-        # if no items don't cycle
         if not self.items:
-            self.cycle = 0
+            raise Exception(STRING_ERROR)
 
-        self._first_run = True
         self.active = 0
+        self.cycle_time = time() + self.cycle
+        self.cycle_timeout = self.cycle
         self.last_active = 0
         self.urgent = False
-        self._cycle_time = time() + self.cycle
+        self.urgent_history = {}
 
-        self.open = bool(self.open)
         # set default format etc based on click_mode
         if self.format is None:
             if self.click_mode == "button":
@@ -140,8 +140,8 @@ def post_config_hook(self):
 
     def _content_function(self):
         """
-        This returns a set containing the actively shown module.  This is so we
-        only get update events triggered for these modules.
+        This returns a set containing the actively shown module.
+        This is so we only get update events triggered for these modules.
         """
         # ensure that active is valid
         self.active = self.active % len(self.items)
@@ -150,7 +150,8 @@ def _content_function(self):
 
     def _urgent_function(self, module_list):
         """
-        A contained module has become urgent.  We want to display it to the user
+        A contained module has become urgent.
+        We want to display it to the user.
         """
         for module in module_list:
             if module in self.items:
@@ -186,11 +187,6 @@ def _get_output(self):
                 current[-1]["full_text"] += padding
         return current
 
-    def _get_current_module_name(self):
-        if not self.items:
-            return
-        return self.items[self.active]
-
     def _change_active(self, delta):
         # we want to ignore any empty outputs
         # to prevent endless cycling we limit ourselves to only going through
@@ -201,87 +197,88 @@ def _change_active(self, delta):
         self.last_active = self.active
 
     def group(self):
-        """
-        Display a output of current module
-        """
+        update_time = None
+        self.cycle = 0
+
+        # get an output. again if empty (twice).
+        for x in range(3):
+            output = self._get_output()
+            if output:
+                break
+            self._change_active(1)
+        else:
+            update_time = MAX_NO_CONTENT_WAIT
 
-        # hide if no contents
-        if not self.items:
-            return {"cached_until": self.py3.CACHE_FOREVER, "full_text": ""}
+        # check for urgents
+        urgent = output and output[0].get("urgent")
+        self.urgent_history[self.active] = urgent
+        mod_urgent = any(self.urgent_history.values())
 
-        if self.open:
-            urgent = False
-            if self.cycle and time() >= self._cycle_time:
+        # keep cycling if no urgent
+        if not urgent:
+            self.cycle = self.cycle_timeout
+            if time() >= self.cycle_time:
                 self._change_active(1)
-                self._cycle_time = time() + self.cycle
-            update_time = self.cycle or None
-            current_output = self._get_output()
-            # if the output is empty try and find some output
-            if not current_output:
-                if self._first_run:
-                    self._first_run = False
-                    return {
-                        "cached_until": self.py3.time_in(MAX_NO_CONTENT_WAIT),
-                        "full_text": "",
-                    }
+                self.cycle_time = time() + self.cycle
 
-                self._change_active(1)
-                current_output = self._get_output()
-                # there is no output for any module retry later
-                self._first_run = False
+        # time
+        update_time = update_time or self.cycle or None
+        if update_time is not None:
+            cached_until = self.py3.time_in(self.cycle)
         else:
-            urgent = self.urgent
-            current_output = []
-            update_time = None
+            cached_until = self.py3.CACHE_FOREVER
 
         if self.open:
             format_control = self.format_button_open
-            format = self.format
+            current_format = self.format
+            urgent = False
         else:
             format_control = self.format_button_closed
-            format = self.format_closed
+            current_format = self.format_closed
+            urgent = mod_urgent or self.urgent
 
         button = {"full_text": format_control, "index": "button"}
-        composites = {
-            "output": self.py3.composite_create(current_output),
-            "button": self.py3.composite_create(button),
+        response = {
+            "cached_until": cached_until,
+            "full_text": self.py3.safe_format(
+                current_format,
+                {
+                    "output": self.py3.composite_create(output),
+                    "button": self.py3.composite_create(button),
+                },
+            ),
         }
-        output = self.py3.safe_format(format, composites)
-
-        if update_time is not None:
-            cached_until = self.py3.time_in(update_time)
-        else:
-            cached_until = self.py3.CACHE_FOREVER
-
-        response = {"cached_until": cached_until, "full_text": output}
-
         if urgent:
             response["urgent"] = urgent
+            self.urgent = False
         return response
 
     def on_click(self, event):
         """
         Switch the displayed module or pass the event on to the active module
         """
-        if not self.items:
-            return
+        button = event["button"]
+        index = event["index"]
 
-        # if click_mode is button then we only action clicks that are
-        # directly on the group not its contents.
+        # if click_mode is button, prevent the contents from changing when
+        # if the events are not registered on the button index. this allow
+        # the users to interact with the modules via scrolling or clicking
+        # without group getting in the way of the contents.
         if self.click_mode == "button":
-            if not self.py3.is_my_event(event) or event.get("index") != "button":
+            if not self.py3.is_my_event(event) or index != "button":
                 return
 
-        # reset cycle time
-        self._cycle_time = time() + self.cycle
-        if self.button_next and event["button"] == self.button_next:
-            self._change_active(1)
-        if self.button_prev and event["button"] == self.button_prev:
-            self._change_active(-1)
-        if self.button_toggle and event["button"] == self.button_toggle:
-            # we only toggle if button was used
-            if event.get("index") == "button":
-                self.urgent = False
+        self.cycle = self.cycle_timeout
+        self.cycle_time = time() + self.cycle
+
+        if button == self.button_next:
+            if self.open:
+                self._change_active(+1)
+        elif button == self.button_prev:
+            if self.open:
+                self._change_active(-1)
+        elif button == self.button_toggle:
+            if index == "button":
                 self.open = not self.open
 
 


Test patch: