Repository: qtile/qtile

Issue:
Qtile shell should better handle exceptions
### The issue:

I was playing around with the qtile shell, specifically I was exploring the folder structure.
When I tried to ls a folder defined as None, the shell broke down and dropped me back to the terminal.

I think the exception should be catched, the error message shown but to keep being in the the shell.

Here's the example:
`bar[bottom] > ls widget/
Traceback (most recent call last):
  File "/usr/lib/python-exec/python3.8/qtile", line 33, in <module>
    sys.exit(load_entry_point('qtile==0.21.0', 'console_scripts', 'qtile')())
  File "/usr/lib/python3.8/site-packages/libqtile/scripts/main.py", line 66, in main
    options.func(options)
  File "/usr/lib/python3.8/site-packages/libqtile/scripts/shell.py", line 36, in qshell
    qsh.loop()
  File "/usr/lib/python3.8/site-packages/libqtile/sh.py", line 360, in loop
    val = self.process_line(line)
  File "/usr/lib/python3.8/site-packages/libqtile/sh.py", line 317, in process_line
    val = builtin(args)
  File "/usr/lib/python3.8/site-packages/libqtile/sh.py", line 236, in do_ls
    objects, items = self._ls(node, rest_path)
  File "/usr/lib/python3.8/site-packages/libqtile/sh.py", line 127, in _ls
    str_items = [str(i) for i in items]
TypeError: 'NoneType' object is not iterable
`
I dont have a bottom bar (it's on the top)

Qtile version is 0.21.0

### Required:

- [X] I have searched past issues to see if this bug has already been reported.


Discussion:
Thanks. Agreed that that looks like something we should fix.

I'll take a look.
How did you navigate to `bar[bottom]` if you don't have a bottom bar?

At the moment, I can't recreate this.
I just checked my configuration, I have a something on the bottom: a
bar.Gap(45)

I guess that's the reason, although it's kinda weird, Bar inherits Gap, not
viceversa

Il mer 29 giu 2022, 22:32 elParaguayo ***@***.***> ha scritto:

> How did you navigate to bar[bottom] if you don't have a bottom bar?
>
> At the moment, I can't recreate this.
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/qtile/qtile/issues/3668#issuecomment-1170466140>, or
> unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAAZEDEQUK5CCJQQDHIT373VRSXFRANCNFSM52FZLOSA>
> .
> You are receiving this because you authored the thread.Message ID:
> ***@***.***>
>

Ah. That makes sense. That's definitely something I can work with.

As for the inheritance, it's the right way round. Gap reserves space in the screen, Bar goes further and draws a bar in that gap.

Will work on a fix for this.
> Ah. That makes sense. That's definitely something I can work with.
> 
> As for the inheritance, it's the right way round. Gap reserves space in the screen, Bar goes further and draws a bar in that gap.
> 
> Will work on a fix for this.

No, I understand Bar inherits Gap, the weird thing to me is that qtile shell "see" the Gap as a Bar when, in fact, it's not, being a superclass.
My poor english was not good enough to communicate what I meant 🤣 
Ah, yes, I understand exactly what you mean now. Agreed, that is a bit confusing.
Interesting, turns out `Gap` is a command object, and it even has an `info()` command. This seems kinda useless, so maybe we can move `CommandObject` inheritance to `Bar` and hide `Gap` from all of the command logic. What do you think @elParaguayo?
Agreed - I think that's the right approach.

Resolving patch:
diff --git a/libqtile/bar.py b/libqtile/bar.py
--- a/libqtile/bar.py
+++ b/libqtile/bar.py
@@ -33,7 +33,7 @@
     from libqtile.widget.base import _Widget
 
 
-class Gap(CommandObject):
+class Gap:
     """A gap placed along one of the edges of the screen
 
     If a gap has been defined, Qtile will avoid covering it with windows. The
@@ -108,30 +108,12 @@ def finalize(self):
     def geometry(self):
         return (self.x, self.y, self.width, self.height)
 
-    def _items(self, name: str) -> ItemT:
-        if name == "screen" and self.screen is not None:
-            return True, []
-        return None
-
-    def _select(self, name, sel):
-        if name == "screen":
-            return self.screen
-
     @property
     def position(self):
         for i in ["top", "bottom", "left", "right"]:
             if getattr(self.screen, i) is self:
                 return i
 
-    def info(self):
-        return dict(position=self.position)
-
-    def cmd_info(self):
-        """
-        Info for this object.
-        """
-        return self.info()
-
 
 class Obj:
     def __init__(self, name):
@@ -149,7 +131,7 @@ def __repr__(self):
 STATIC = Obj("STATIC")
 
 
-class Bar(Gap, configurable.Configurable):
+class Bar(Gap, configurable.Configurable, CommandObject):
     """A bar, which can contain widgets
 
     Parameters
@@ -681,5 +663,11 @@ def cmd_fake_button_press(self, screen, position, x, y, button=1):
         """
         self.process_button_click(x, y, button)
 
+    def cmd_info(self):
+        """
+        Info for this object.
+        """
+        return self.info()
+
 
 BarType = typing.Union[Bar, Gap]
diff --git a/libqtile/config.py b/libqtile/config.py
--- a/libqtile/config.py
+++ b/libqtile/config.py
@@ -496,7 +496,7 @@ def _items(self, name: str) -> ItemT:
         elif name == "window" and self.group is not None:
             return True, [i.wid for i in self.group.windows]
         elif name == "bar":
-            return False, [x.position for x in self.gaps]
+            return False, [x.position for x in self.gaps if isinstance(x, Bar)]
         elif name == "widget":
             bars = (g for g in self.gaps if isinstance(g, Bar))
             return False, [w.name for b in bars for w in b.widgets]
@@ -520,7 +520,10 @@ def _select(self, name: str, sel: str | int | None) -> CommandObject | None:
                         return i
         elif name == "bar":
             assert isinstance(sel, str)
-            return getattr(self, sel)
+            bar = getattr(self, sel)
+            if isinstance(bar, Bar):
+                return bar
+
         elif name == "widget":
             for gap in self.gaps:
                 if not isinstance(gap, Bar):
diff --git a/libqtile/core/manager.py b/libqtile/core/manager.py
--- a/libqtile/core/manager.py
+++ b/libqtile/core/manager.py
@@ -790,7 +790,7 @@ def _items(self, name: str) -> ItemT:
         elif name == "widget":
             return False, list(self.widgets_map.keys())
         elif name == "bar":
-            return False, [x.position for x in self.current_screen.gaps]
+            return False, [x.position for x in self.current_screen.gaps if isinstance(x, bar.Bar)]
         elif name == "window":
             windows: list[str | int]
             windows = [
@@ -819,7 +819,9 @@ def _select(self, name: str, sel: str | int | None) -> CommandObject | None:
         elif name == "widget":
             return self.widgets_map.get(sel)  # type: ignore
         elif name == "bar":
-            return getattr(self.current_screen, sel)  # type: ignore
+            gap = getattr(self.current_screen, sel)  # type: ignore
+            if isinstance(gap, bar.Bar):
+                return gap
         elif name == "window":
             if sel is None:
                 return self.current_window


Test patch: