Repository: qtile/qtile

Issue:
Missing file for backlight should not crash whole setup
# Issue description

After upgrade qtile 1.5.1 -> 1.6.1, the configuration crashed. The problem is I have same setup for two PC. Both have different GPU: intel and Nvidia. The older version just printed an error message inside the plugin. Not cool, but at least the environment worked. 

I am not sure if it is a bug or a feature, but I prefer to have a more resilient config. I do not see a point of crashing for such situations. 

# Qtile version

1.6.1

# Stack traces
```
Traceback (most recent call last):
  File "/usr/lib/python3.8/site-packages/libqtile/confreader.py", line 92, in load
    config = __import__(name)  # noqa: F811
  File "/home/neworld/.config/qtile/config.py", line 363, in <module>
    widget.Backlight(
  File "/usr/lib/python3.8/site-packages/libqtile/utils.py", line 213, in class_proxy
    return cls(*args, **kwargs)
  File "/usr/lib/python3.8/site-packages/libqtile/widget/backlight.py", line 96, in __init__
    self.max_value = self._load_file(self.max_brightness_file)
  File "/usr/lib/python3.8/site-packages/libqtile/widget/backlight.py", line 105, in _load_file
    raise RuntimeError(
RuntimeError: Unable to read status for max_brightness
```

# Configuration

```
                widget.Backlight(
                    background=color_red,
                    backlight_name = 'intel_backlight',
                ),
```

Missing file for backlight should not crash whole setup
# Issue description

After upgrade qtile 1.5.1 -> 1.6.1, the configuration crashed. The problem is I have same setup for two PC. Both have different GPU: intel and Nvidia. The older version just printed an error message inside the plugin. Not cool, but at least the environment worked. 

I am not sure if it is a bug or a feature, but I prefer to have a more resilient config. I do not see a point of crashing for such situations. 

# Qtile version

1.6.1

# Stack traces
```
Traceback (most recent call last):
  File "/usr/lib/python3.8/site-packages/libqtile/confreader.py", line 92, in load
    config = __import__(name)  # noqa: F811
  File "/home/neworld/.config/qtile/config.py", line 363, in <module>
    widget.Backlight(
  File "/usr/lib/python3.8/site-packages/libqtile/utils.py", line 213, in class_proxy
    return cls(*args, **kwargs)
  File "/usr/lib/python3.8/site-packages/libqtile/widget/backlight.py", line 96, in __init__
    self.max_value = self._load_file(self.max_brightness_file)
  File "/usr/lib/python3.8/site-packages/libqtile/widget/backlight.py", line 105, in _load_file
    raise RuntimeError(
RuntimeError: Unable to read status for max_brightness
```

# Configuration

```
                widget.Backlight(
                    background=color_red,
                    backlight_name = 'intel_backlight',
                ),
```



Discussion:
I don't know, seems reasonable to me to fail fast(er) in this case.
In most cases, I prefer to fail fast as well. But not for WM::

- Crashed Qtile does not work. I had to fix the problem in different TTY.
- Having the same config for a few different machines is a real problem. 
- WM potentially could crash in a very wrong moment due to edge cases I was not aware of. 

Because the backlight widget is not essential to function, I prefer to be defensive here. Old behavior just writes error down into widget. That makes me be aware of the problem, but not forced to fix it. Now, I was forced quickly to remove it. That of course leaves a laptop without indicator until I will have time to investigate proper solutions.
Would it not be better to just log the error message and display 'ERROR' or similar as the widget's text? This is what happens when the widget cannot get brightness info during poll().
I think this behavior change is a result of b524e4427ca43f77d022aa05d85dc16a1aef7215, so we could just go back to doing stuff in _load_file().
I'm giving the widget an edit with regards to #1868 so figure I can address this at the same time.
I don't know, seems reasonable to me to fail fast(er) in this case.
In most cases, I prefer to fail fast as well. But not for WM::

- Crashed Qtile does not work. I had to fix the problem in different TTY.
- Having the same config for a few different machines is a real problem. 
- WM potentially could crash in a very wrong moment due to edge cases I was not aware of. 

Because the backlight widget is not essential to function, I prefer to be defensive here. Old behavior just writes error down into widget. That makes me be aware of the problem, but not forced to fix it. Now, I was forced quickly to remove it. That of course leaves a laptop without indicator until I will have time to investigate proper solutions.
Would it not be better to just log the error message and display 'ERROR' or similar as the widget's text? This is what happens when the widget cannot get brightness info during poll().
I think this behavior change is a result of b524e4427ca43f77d022aa05d85dc16a1aef7215, so we could just go back to doing stuff in _load_file().
I'm giving the widget an edit with regards to #1868 so figure I can address this at the same time.

Resolving patch:
diff --git a/libqtile/widget/backlight.py b/libqtile/widget/backlight.py
--- a/libqtile/widget/backlight.py
+++ b/libqtile/widget/backlight.py
@@ -85,7 +85,7 @@ class Backlight(base.InLoopPollText):
     def __init__(self, **config):
         base.InLoopPollText.__init__(self, **config)
         self.add_defaults(Backlight.defaults)
-        self.future = None
+        self._future = None
 
         self.brightness_file = os.path.join(
             BACKLIGHT_DIR, self.backlight_name, self.brightness_file,
@@ -93,8 +93,13 @@ def __init__(self, **config):
         self.max_brightness_file = os.path.join(
             BACKLIGHT_DIR, self.backlight_name, self.max_brightness_file,
         )
-        self.max_value = self._load_file(self.max_brightness_file)
-        self.step = self.max_value * self.step / 100
+
+        mouse_callbacks = {
+            'Button4': lambda q: self.cmd_change_backlight(ChangeDirection.UP),
+            'Button5': lambda q: self.cmd_change_backlight(ChangeDirection.DOWN),
+        }
+        mouse_callbacks.update(self.mouse_callbacks)
+        self.mouse_callbacks = mouse_callbacks
 
     def _load_file(self, path):
         try:
@@ -108,24 +113,20 @@ def _load_file(self, path):
 
     def _get_info(self):
         brightness = self._load_file(self.brightness_file)
-
-        info = {
-            'brightness': brightness,
-            'max': self.max_value,
-        }
-        return info
+        max_value = self._load_file(self.max_brightness_file)
+        return brightness / max_value
 
     def poll(self):
         try:
-            info = self._get_info()
+            percent = self._get_info()
         except RuntimeError as e:
             return 'Error: {}'.format(e)
 
-        percent = info['brightness'] / info['max']
         return self.format.format(percent=percent)
 
-    def change_backlight(self, value):
+    def _change_backlight(self, value):
         if self.change_command is None:
+            value = self._load_file(self.max_brightness_file) * value / 100
             try:
                 with open(self.brightness_file, 'w') as f:
                     f.write(str(round(value)))
@@ -136,23 +137,12 @@ def change_backlight(self, value):
             self.call_process(shlex.split(self.change_command.format(value)))
 
     def cmd_change_backlight(self, direction):
-        if self.future and not self.future.done():
+        if self._future and not self._future.done():
             return
-        info = self._get_info()
-        if not info:
-            new = now = self.max_value
-        else:
-            new = now = info["brightness"]
-        if direction is ChangeDirection.DOWN:  # down
+        new = now = self._get_info() * 100
+        if direction is ChangeDirection.DOWN:
             new = max(now - self.step, 0)
-        elif direction is ChangeDirection.UP:  # up
-            new = min(now + self.step, self.max_value)
+        elif direction is ChangeDirection.UP:
+            new = min(now + self.step, 100)
         if new != now:
-            self.future = self.qtile.run_in_executor(self.change_backlight,
-                                                     new)
-
-    def button_press(self, x, y, button):
-        if button == 5:
-            self.cmd_change_backlight(ChangeDirection.DOWN)
-        elif button == 4:
-            self.cmd_change_backlight(ChangeDirection.UP)
+            self._future = self.qtile.run_in_executor(self._change_backlight, new)


Test patch: