From e17ef8614fe8a25ffb913748dfa090463cb4b969 Mon Sep 17 00:00:00 2001 From: es80 <49912978+es80@users.noreply.github.com> Date: Sun, 21 Jun 2020 17:56:52 +0100 Subject: [PATCH 001/506] fix a regression for shell subprocess --- bumblebee_status/modules/contrib/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bumblebee_status/modules/contrib/shell.py b/bumblebee_status/modules/contrib/shell.py index a451692..6fd9aa2 100644 --- a/bumblebee_status/modules/contrib/shell.py +++ b/bumblebee_status/modules/contrib/shell.py @@ -60,7 +60,7 @@ class Module(core.module.Module): def update(self): # if requested then run not async version and just execute command in this thread if not self.__async: - self.__output = util.cli.execute(self.__command, ignore_errors=True).strip() + self.__output = util.cli.execute(self.__command, shell=True, ignore_errors=True).strip() return # if previous thread didn't end yet then don't do anything From f67ef9d64a5d71c4cbad5cb5a28471e943814bc4 Mon Sep 17 00:00:00 2001 From: LtPeriwinkle Date: Sun, 21 Jun 2020 10:28:37 -0700 Subject: [PATCH 002/506] add spotify-buttons --- .../modules/contrib/spotify-buttons.py | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 bumblebee_status/modules/contrib/spotify-buttons.py diff --git a/bumblebee_status/modules/contrib/spotify-buttons.py b/bumblebee_status/modules/contrib/spotify-buttons.py new file mode 100644 index 0000000..46c5e7a --- /dev/null +++ b/bumblebee_status/modules/contrib/spotify-buttons.py @@ -0,0 +1,93 @@ +import sys +import dbus + +import core.module +import core.widget +import core.input +import core.decorators +"""Displays the current song being played + +Requires the following library: + * python-dbus + +Parameters: + * spotify-buttons.format: Format string (defaults to '{artist} - {title}') + Available values are: {album}, {title}, {artist}, {trackNumber}, {playbackStatus} + * spotify-buttons.layout: +""" + +class Module(core.module.Module): + def __init__(self, config, theme): + super().__init__(config, theme, core.widget.Widget(self.spotify)) + + self.__layout = self.parameter("layout", "spotify-buttons.prev spotify-buttons.pause spotify-buttons.next") + + self.__song = "" + self.__format = self.parameter("format", "{artist} - {title}") + prev_button = self.parameter("previous", "LEFT_CLICK") + next_button = self.parameter("next", "RIGHT_CLICK") + pause_button = self.parameter("pause", "MIDDLE_CLICK") + + cmd = "dbus-send --session --type=method_call --dest=org.mpris.MediaPlayer2.spotify \ + /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player." + #core.input.register(self, button=buttons[prev_button], cmd=cmd + "Previous") + #core.input.register(self, button=buttons[next_button], cmd=cmd + "Next") + #core.input.register(self, button=buttons[pause_button], cmd=cmd + "PlayPause") + + widget_map = {} + for widget in self.__layout.split(): + widget = self.add_widget(name = widget_name) + if widget_name = "spotify-buttons.prev": + widget_map[widget] = { + "button": core.input.LEFT_MOUSE, + "cmd": cmd + "Previous", + } + elif widget_name = "spotify-buttons.pause": + widget_map[widget] = { + "button": core.input.LEFT_MOUSE, + "cmd": cmd + "PlayPause", + } + elif widget_name = "spotify-buttons.next": + widget_map[widget] = { + "button": core.input.LEFT_MOUSE, + "cmd": cmd + "Next", + } + + @core.decorators.scrollable + def spotify(self, widget): + return self.string_song + + def hidden(self): + return self.string_song == "" + + def update(self): + try: + bus = dbus.SessionBus() + spotify = bus.get_object( + "org.mpris.MediaPlayer2.spotify", "/org/mpris/MediaPlayer2" + ) + spotify_iface = dbus.Interface(spotify, "org.freedesktop.DBus.Properties") + props = spotify_iface.Get("org.mpris.MediaPlayer2.Player", "Metadata") + playback_status = str( + spotify_iface.Get("org.mpris.MediaPlayer2.Player", "PlaybackStatus") + ) + self.__song = self.__format.format( + album=str(props.get("xesam:album")), + title=str(props.get("xesam:title")), + artist=",".join(props.get("xesam:artist")), + trackNumber=str(props.get("xesam:trackNumber")), + playbackStatus="\u25B6" + if playback_status == "Playing" + else "\u258D\u258D" + if playback_status == "Paused" + else "", + ) + + except Exception: + self.__song = "" + + @property + def string_song(self): + if sys.version_info.major < 3: + return unicode(self.__song) + return str(self.__song) From ca62f689067248b99afd93f03d80b71480aed6d5 Mon Sep 17 00:00:00 2001 From: LtPeriwinkle Date: Sun, 21 Jun 2020 12:31:11 -0700 Subject: [PATCH 003/506] create + map widgets for buttons --- bumblebee_status/modules/contrib/spotify-buttons.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/bumblebee_status/modules/contrib/spotify-buttons.py b/bumblebee_status/modules/contrib/spotify-buttons.py index 46c5e7a..36f126c 100644 --- a/bumblebee_status/modules/contrib/spotify-buttons.py +++ b/bumblebee_status/modules/contrib/spotify-buttons.py @@ -30,28 +30,27 @@ class Module(core.module.Module): cmd = "dbus-send --session --type=method_call --dest=org.mpris.MediaPlayer2.spotify \ /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player." - #core.input.register(self, button=buttons[prev_button], cmd=cmd + "Previous") - #core.input.register(self, button=buttons[next_button], cmd=cmd + "Next") - #core.input.register(self, button=buttons[pause_button], cmd=cmd + "PlayPause") widget_map = {} - for widget in self.__layout.split(): + for widget_name in self.__layout.split(): widget = self.add_widget(name = widget_name) - if widget_name = "spotify-buttons.prev": + if widget_name == "spotify-buttons.prev": widget_map[widget] = { "button": core.input.LEFT_MOUSE, "cmd": cmd + "Previous", } - elif widget_name = "spotify-buttons.pause": + elif widget_name == "spotify-buttons.pause": widget_map[widget] = { "button": core.input.LEFT_MOUSE, "cmd": cmd + "PlayPause", } - elif widget_name = "spotify-buttons.next": + elif widget_name == "spotify-buttons.next": widget_map[widget] = { "button": core.input.LEFT_MOUSE, "cmd": cmd + "Next", } + for widget, callback_options in widget_map.items(): + core.input.register(widget, **callback_options) @core.decorators.scrollable def spotify(self, widget): From 3558176044c2b48ed70c94d23436071ff4748193 Mon Sep 17 00:00:00 2001 From: "tobias.hannaske" Date: Mon, 22 Jun 2020 10:33:49 +0200 Subject: [PATCH 004/506] Do not execute iwgetid if the interface is recognized as tunnel --- bumblebee_status/modules/core/nic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bumblebee_status/modules/core/nic.py b/bumblebee_status/modules/core/nic.py index a5f26b0..585c65f 100644 --- a/bumblebee_status/modules/core/nic.py +++ b/bumblebee_status/modules/core/nic.py @@ -125,7 +125,7 @@ class Module(core.module.Module): widget.set("state", state) def get_ssid(self, intf): - if self._iswlan(intf) and self.iwgetid: + if self._iswlan(intf) and not self._istunnel(intf) and self.iwgetid: return util.cli.execute( "{} -r {}".format(self.iwgetid, intf), ignore_errors=True ) From 34dadadf90705e50099af8b66aa0afd38cd73560 Mon Sep 17 00:00:00 2001 From: Tobias Witek Date: Tue, 23 Jun 2020 15:51:14 +0200 Subject: [PATCH 005/506] [core] re-enable minimize of widgets by default, allow toggling the minimized state of a widget via the middle mouse and draw a single unicode char instead of the actual widget, maintaining all states. fixes #661 --- bumblebee-status | 5 +++++ bumblebee_status/core/module.py | 8 +++++--- bumblebee_status/core/output.py | 8 +++++++- bumblebee_status/core/widget.py | 1 + tests/core/test_module.py | 7 +++++++ tests/core/test_output.py | 33 +++++++++++++++++++++++---------- 6 files changed, 48 insertions(+), 14 deletions(-) diff --git a/bumblebee-status b/bumblebee-status index f995850..21f8c32 100755 --- a/bumblebee-status +++ b/bumblebee-status @@ -19,6 +19,7 @@ import core.module import core.input import core.event +import util.format started = False @@ -110,6 +111,10 @@ def main(): modules.reverse() output.modules(modules) + + if util.format.asbool(config.get("engine.collapsible", True)) == True: + core.input.register(None, core.input.MIDDLE_MOUSE, output.toggle_minimize) + core.event.trigger("start") started = True while True: diff --git a/bumblebee_status/core/module.py b/bumblebee_status/core/module.py index cba01ac..af708db 100644 --- a/bumblebee_status/core/module.py +++ b/bumblebee_status/core/module.py @@ -184,12 +184,14 @@ class Module(core.input.Object): :rtype: bumblebee_status.widget.Widget """ - def widget(self, name=None): - if not name: + def widget(self, name=None, widget_id=None): + if not name and not widget_id: return self.widgets()[0] for w in self.widgets(): - if w.name == name: + if name and w.name == name: + return w + if w.id == widget_id: return w return None diff --git a/bumblebee_status/core/output.py b/bumblebee_status/core/output.py index ab68c6a..4aaf1e9 100644 --- a/bumblebee_status/core/output.py +++ b/bumblebee_status/core/output.py @@ -152,6 +152,12 @@ class i3(object): return self.__modules self.__modules = modules if isinstance(modules, list) else [modules] + def toggle_minimize(self, event): + for module in self.__modules: + widget = module.widget(widget_id=event["instance"]) + if widget: + widget.minimized = not widget.minimized + def draw(self, what, args=None): cb = getattr(self, what) data = cb(args) if args else cb() @@ -187,7 +193,7 @@ class i3(object): except: blk.set("min-width", minwidth) blk.set("align", widget.theme("align")) - blk.set("full_text", self.__content[widget]) + blk.set("full_text", "\u2026" if widget.minimized else self.__content[widget]) if widget.get("pango", False): blk.set("markup", "pango") if self.__config.debug(): diff --git a/bumblebee_status/core/widget.py b/bumblebee_status/core/widget.py index dd9fe1f..928aedd 100644 --- a/bumblebee_status/core/widget.py +++ b/bumblebee_status/core/widget.py @@ -15,6 +15,7 @@ class Widget(util.store.Store, core.input.Object): self.__full_text = full_text self.module = None self.name = name + self.minimized = False @property def module(self): diff --git a/tests/core/test_module.py b/tests/core/test_module.py index ef65c88..68415b7 100644 --- a/tests/core/test_module.py +++ b/tests/core/test_module.py @@ -129,6 +129,13 @@ def test_get_widget_by_name(empty_config, widget_a, widget_b): assert module.widget("i-do-not-exist") == None assert module.widget() == widget_a +def test_get_widget_by_id(empty_config, widget_a, widget_b): + module = SampleModule(config=empty_config, widgets=[widget_a, widget_b]) + + assert module.widget(widget_id=widget_a.id) == widget_a + assert module.widget(widget_id=widget_b.id) == widget_b + assert module.widget(widget_id="i-do-not-exist") == None + def test_default_thresholds(empty_config, widget_a, widget_b): module = SampleModule(config=empty_config, widgets=[widget_a, widget_b]) diff --git a/tests/core/test_output.py b/tests/core/test_output.py index fcd9e22..828078c 100644 --- a/tests/core/test_output.py +++ b/tests/core/test_output.py @@ -25,6 +25,16 @@ def i3(): def module_a(mocker): widget = mocker.MagicMock() widget.full_text.return_value = "test" + widget.id = "a" + widget.minimized = False + return SampleModule(config=core.config.Config([]), widgets=[widget, widget, widget]) + +@pytest.fixture +def module_b(mocker): + widget = mocker.MagicMock() + widget.full_text.return_value = "test" + widget.id = "b" + widget.minimized = False return SampleModule(config=core.config.Config([]), widgets=[widget, widget, widget]) @@ -46,16 +56,6 @@ def block_a(separatorTheme, module_a): theme=separatorTheme, module=module_a, widget=module_a.widget(), ) - -# def setUp(self): -# self.i3 = core.output.i3() -# widget = unittest.mock.MagicMock() -# widget.full_text.return_value = "test" -# self.someModule = SampleModule( -# config=core.config.Config([]), widgets=[widget, widget, widget] -# ) -# self.separator = "***" -# def test_start(i3): all_data = i3.start() data = all_data["blocks"] @@ -83,6 +83,19 @@ def test_register_multiple_modules(i3, module_a): i3.modules([module_a, module_a, module_a]) assert i3.modules() == [module_a, module_a, module_a] +def test_toggle_module(i3, module_a, module_b): + i3.modules([module_a, module_b]) + + i3.toggle_minimize({ "instance": module_a.widget().id }) + + assert module_a.widget().minimized == True + assert module_b.widget().minimized == False + + i3.toggle_minimize({ "instance": module_a.widget().id }) + i3.toggle_minimize({ "instance": module_b.widget().id }) + + assert module_a.widget().minimized == False + assert module_b.widget().minimized == True def test_draw_existing_module(mocker, i3): i3.test_draw = mocker.MagicMock( From 441e7d5041b9f91ddb516eaead83f8975cc74e01 Mon Sep 17 00:00:00 2001 From: Tobias Witek Date: Tue, 23 Jun 2020 20:03:17 +0200 Subject: [PATCH 006/506] [core] fix minimize for all modules (nic, traffic, etc.) make it possible to toggle the display state of a widget between "displayed" and "minimized" also for modules that re-create their widgets during each iteration. see #661 --- bumblebee_status/core/module.py | 3 ++- bumblebee_status/core/output.py | 17 +++++++++++------ bumblebee_status/core/widget.py | 4 ++-- tests/core/test_output.py | 20 +++++++++++--------- 4 files changed, 26 insertions(+), 18 deletions(-) diff --git a/bumblebee_status/core/module.py b/bumblebee_status/core/module.py index af708db..23ea4b5 100644 --- a/bumblebee_status/core/module.py +++ b/bumblebee_status/core/module.py @@ -171,7 +171,8 @@ class Module(core.input.Object): """ def add_widget(self, full_text="", name=None): - widget = core.widget.Widget(full_text=full_text, name=name) + widget_id = "{}::{}".format(self.name, len(self.widgets())) + widget = core.widget.Widget(full_text=full_text, name=name, widget_id=widget_id) self.widgets().append(widget) widget.module = self return widget diff --git a/bumblebee_status/core/output.py b/bumblebee_status/core/output.py index 4aaf1e9..929f0f4 100644 --- a/bumblebee_status/core/output.py +++ b/bumblebee_status/core/output.py @@ -142,6 +142,9 @@ class i3(object): core.event.register("draw", self.draw, "statusline") core.event.register("stop", self.draw, "stop") + def content(self): + return self.__content + def theme(self, new_theme=None): if new_theme: self.__theme = new_theme @@ -153,10 +156,10 @@ class i3(object): self.__modules = modules if isinstance(modules, list) else [modules] def toggle_minimize(self, event): - for module in self.__modules: - widget = module.widget(widget_id=event["instance"]) - if widget: - widget.minimized = not widget.minimized + widget_id = event["instance"] + + if widget_id in self.__content: + self.__content[widget_id]["minimized"] = not self.__content[widget_id]["minimized"] def draw(self, what, args=None): cb = getattr(self, what) @@ -193,7 +196,7 @@ class i3(object): except: blk.set("min-width", minwidth) blk.set("align", widget.theme("align")) - blk.set("full_text", "\u2026" if widget.minimized else self.__content[widget]) + blk.set("full_text", "\u2026" if self.__content[widget.id]["minimized"] else self.__content[widget.id]["text"]) if widget.get("pango", False): blk.set("markup", "pango") if self.__config.debug(): @@ -236,7 +239,9 @@ class i3(object): module.parameter("interval", self.__config.interval()) ) for widget in module.widgets(): - self.__content[widget] = widget.full_text() + if not widget.id in self.__content: + self.__content[widget.id] = { "minimized": False } + self.__content[widget.id]["text"] = widget.full_text() def statusline(self): blocks = [] diff --git a/bumblebee_status/core/widget.py b/bumblebee_status/core/widget.py index 928aedd..b1c3b4e 100644 --- a/bumblebee_status/core/widget.py +++ b/bumblebee_status/core/widget.py @@ -10,12 +10,12 @@ log = logging.getLogger(__name__) class Widget(util.store.Store, core.input.Object): - def __init__(self, full_text="", name=None): + def __init__(self, full_text="", name=None, widget_id=None): super(Widget, self).__init__() self.__full_text = full_text self.module = None self.name = name - self.minimized = False + self.id = widget_id or self.id @property def module(self): diff --git a/tests/core/test_output.py b/tests/core/test_output.py index 828078c..4e0d905 100644 --- a/tests/core/test_output.py +++ b/tests/core/test_output.py @@ -26,7 +26,6 @@ def module_a(mocker): widget = mocker.MagicMock() widget.full_text.return_value = "test" widget.id = "a" - widget.minimized = False return SampleModule(config=core.config.Config([]), widgets=[widget, widget, widget]) @pytest.fixture @@ -34,7 +33,6 @@ def module_b(mocker): widget = mocker.MagicMock() widget.full_text.return_value = "test" widget.id = "b" - widget.minimized = False return SampleModule(config=core.config.Config([]), widgets=[widget, widget, widget]) @@ -86,16 +84,20 @@ def test_register_multiple_modules(i3, module_a): def test_toggle_module(i3, module_a, module_b): i3.modules([module_a, module_b]) + i3.update() i3.toggle_minimize({ "instance": module_a.widget().id }) + i3.update() - assert module_a.widget().minimized == True - assert module_b.widget().minimized == False + assert i3.content()[module_a.widget().id]["minimized"] == True - i3.toggle_minimize({ "instance": module_a.widget().id }) - i3.toggle_minimize({ "instance": module_b.widget().id }) - - assert module_a.widget().minimized == False - assert module_b.widget().minimized == True +# assert module_a.widget().minimized == True +# assert module_b.widget().minimized == False +# +# i3.toggle_minimize({ "instance": module_a.widget().id }) +# i3.toggle_minimize({ "instance": module_b.widget().id }) +# +# assert module_a.widget().minimized == False +# assert module_b.widget().minimized == True def test_draw_existing_module(mocker, i3): i3.test_draw = mocker.MagicMock( From 582c828deb36e98099ff16ed5fda4dd0c226a826 Mon Sep 17 00:00:00 2001 From: Tobias Witek Date: Tue, 23 Jun 2020 20:20:36 +0200 Subject: [PATCH 007/506] [core] guard against concurrent updates when a "regular" update (once per interval) and a input-triggered update (e.g. mouse click on a widget) collide, this can cause the theme colors to be interleaved wrongly. fixes #661 --- bumblebee-status | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/bumblebee-status b/bumblebee-status index 21f8c32..e4f5982 100755 --- a/bumblebee-status +++ b/bumblebee-status @@ -39,7 +39,7 @@ class CommandSocket(object): os.unlink(self.__name) -def handle_input(output): +def handle_input(output, update_lock): with CommandSocket() as cmdsocket: poll = select.poll() poll.register(sys.stdin.fileno(), select.POLLIN) @@ -67,8 +67,10 @@ def handle_input(output): modules[event["name"]] = True except ValueError: pass + update_lock.acquire() core.event.trigger("update", modules.keys()) core.event.trigger("draw") + update_lock.release() poll.unregister(sys.stdin.fileno()) @@ -96,7 +98,8 @@ def main(): core.input.register(None, core.input.WHEEL_UP, "i3-msg workspace prev_on_output") core.input.register(None, core.input.WHEEL_DOWN, "i3-msg workspace next_on_output") - input_thread = threading.Thread(target=handle_input, args=(output,)) + update_lock = threading.Lock() + input_thread = threading.Thread(target=handle_input, args=(output, update_lock, )) input_thread.daemon = True input_thread.start() @@ -118,8 +121,10 @@ def main(): core.event.trigger("start") started = True while True: - core.event.trigger("update") - core.event.trigger("draw") + if update_lock.acquire(blocking=False) == True: + core.event.trigger("update") + core.event.trigger("draw") + update_lock.release() output.wait(config.interval()) core.event.trigger("stop") From 92ab1a3e008f581737e25934115e70e5e8bc6b19 Mon Sep 17 00:00:00 2001 From: Tobias Witek Date: Wed, 24 Jun 2020 07:47:31 +0200 Subject: [PATCH 008/506] [util/cli] make sure language is set to "C" to make parsing of CLI output reliable (i.e. always in english), set LC_ALL accordingly. see #662 --- bumblebee_status/util/cli.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bumblebee_status/util/cli.py b/bumblebee_status/util/cli.py index 3e4face..8b39aab 100644 --- a/bumblebee_status/util/cli.py +++ b/bumblebee_status/util/cli.py @@ -30,6 +30,11 @@ def execute( """ args = cmd if shell else shlex.split(cmd) logging.debug(cmd) + + if not env: + env = os.environ.copy() + env["LC_ALL"] = "C" + try: proc = subprocess.Popen( args, From 2eee4c390cf2e411543acba013c8f438b5f9dae9 Mon Sep 17 00:00:00 2001 From: LtPeriwinkle Date: Wed, 24 Jun 2020 12:14:26 -0700 Subject: [PATCH 009/506] put text on buttons --- .../modules/contrib/spotify-buttons.py | 80 ++++++++++--------- 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/bumblebee_status/modules/contrib/spotify-buttons.py b/bumblebee_status/modules/contrib/spotify-buttons.py index 36f126c..95be303 100644 --- a/bumblebee_status/modules/contrib/spotify-buttons.py +++ b/bumblebee_status/modules/contrib/spotify-buttons.py @@ -5,62 +5,35 @@ import core.module import core.widget import core.input import core.decorators -"""Displays the current song being played +"""Displays the current song being played and allows pausing, skipping ahead, and skipping back. Requires the following library: * python-dbus Parameters: * spotify-buttons.format: Format string (defaults to '{artist} - {title}') - Available values are: {album}, {title}, {artist}, {trackNumber}, {playbackStatus} - * spotify-buttons.layout: + Available values are: {album}, {title}, {artist}, {trackNumber} """ class Module(core.module.Module): def __init__(self, config, theme): - super().__init__(config, theme, core.widget.Widget(self.spotify)) + super().__init__(config, theme, []) - self.__layout = self.parameter("layout", "spotify-buttons.prev spotify-buttons.pause spotify-buttons.next") + self.__layout = self.parameter("layout", "spotify-buttons.song spotify-buttons.prev spotify-buttons.pause spotify-buttons.next") self.__song = "" + self.__pause = "" self.__format = self.parameter("format", "{artist} - {title}") - prev_button = self.parameter("previous", "LEFT_CLICK") - next_button = self.parameter("next", "RIGHT_CLICK") - pause_button = self.parameter("pause", "MIDDLE_CLICK") - cmd = "dbus-send --session --type=method_call --dest=org.mpris.MediaPlayer2.spotify \ + self.__cmd = "dbus-send --session --type=method_call --dest=org.mpris.MediaPlayer2.spotify \ /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player." - widget_map = {} - for widget_name in self.__layout.split(): - widget = self.add_widget(name = widget_name) - if widget_name == "spotify-buttons.prev": - widget_map[widget] = { - "button": core.input.LEFT_MOUSE, - "cmd": cmd + "Previous", - } - elif widget_name == "spotify-buttons.pause": - widget_map[widget] = { - "button": core.input.LEFT_MOUSE, - "cmd": cmd + "PlayPause", - } - elif widget_name == "spotify-buttons.next": - widget_map[widget] = { - "button": core.input.LEFT_MOUSE, - "cmd": cmd + "Next", - } - for widget, callback_options in widget_map.items(): - core.input.register(widget, **callback_options) - - @core.decorators.scrollable - def spotify(self, widget): - return self.string_song - def hidden(self): return self.string_song == "" def update(self): try: + self.clear_widgets() bus = dbus.SessionBus() spotify = bus.get_object( "org.mpris.MediaPlayer2.spotify", "/org/mpris/MediaPlayer2" @@ -70,17 +43,46 @@ class Module(core.module.Module): playback_status = str( spotify_iface.Get("org.mpris.MediaPlayer2.Player", "PlaybackStatus") ) + if playback_status == "Playing": + self.__pause = "\u25B6" + else: + self.__pause = "\u258D\u258D" self.__song = self.__format.format( album=str(props.get("xesam:album")), title=str(props.get("xesam:title")), artist=",".join(props.get("xesam:artist")), trackNumber=str(props.get("xesam:trackNumber")), - playbackStatus="\u25B6" - if playback_status == "Playing" - else "\u258D\u258D" - if playback_status == "Paused" - else "", ) + #this feels like a stupid way to do this but its all i can think of + widget_map = {} + for widget_name in self.__layout.split(): + widget = self.add_widget(name = widget_name) + if widget_name == "spotify-buttons.prev": + widget_map[widget] = { + "button": core.input.LEFT_MOUSE, + "cmd": self.__cmd + "Previous", + } + widget.full_text("\u258F\u25C0") + elif widget_name == "spotify-buttons.pause": + widget_map[widget] = { + "button": core.input.LEFT_MOUSE, + "cmd": self.__cmd + "PlayPause", + } + widget.full_text(self.__pause) + elif widget_name == "spotify-buttons.next": + widget_map[widget] = { + "button": core.input.LEFT_MOUSE, + "cmd": self.__cmd + "Next", + } + widget.full_text("\u25B6\u2595") + elif widget_name == "spotify-buttons.song": + widget.full_text(self.__song) + else: + raise KeyError( + "The spotify-buttons module does not have a {widget_name!r} widget".format(widget_name=widget_name) + ) + for widget, callback_options in widget_map.items(): + core.input.register(widget, **callback_options) except Exception: self.__song = "" From 0c32d13e6f6a3df98d100d05ce3f3465e83c5d5b Mon Sep 17 00:00:00 2001 From: LtPeriwinkle Date: Wed, 24 Jun 2020 12:15:32 -0700 Subject: [PATCH 010/506] fix playback status --- bumblebee_status/modules/contrib/spotify-buttons.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bumblebee_status/modules/contrib/spotify-buttons.py b/bumblebee_status/modules/contrib/spotify-buttons.py index 95be303..54b2437 100644 --- a/bumblebee_status/modules/contrib/spotify-buttons.py +++ b/bumblebee_status/modules/contrib/spotify-buttons.py @@ -44,9 +44,9 @@ class Module(core.module.Module): spotify_iface.Get("org.mpris.MediaPlayer2.Player", "PlaybackStatus") ) if playback_status == "Playing": - self.__pause = "\u25B6" - else: self.__pause = "\u258D\u258D" + else: + self.__pause = "\u25B6" self.__song = self.__format.format( album=str(props.get("xesam:album")), title=str(props.get("xesam:title")), From e1a97824586bc9713aa5edecb81d19a22e2faff4 Mon Sep 17 00:00:00 2001 From: LtPeriwinkle Date: Wed, 24 Jun 2020 12:18:28 -0700 Subject: [PATCH 011/506] modify comments --- bumblebee_status/modules/contrib/spotify-buttons.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bumblebee_status/modules/contrib/spotify-buttons.py b/bumblebee_status/modules/contrib/spotify-buttons.py index 54b2437..88c8ae9 100644 --- a/bumblebee_status/modules/contrib/spotify-buttons.py +++ b/bumblebee_status/modules/contrib/spotify-buttons.py @@ -13,6 +13,8 @@ Requires the following library: Parameters: * spotify-buttons.format: Format string (defaults to '{artist} - {title}') Available values are: {album}, {title}, {artist}, {trackNumber} + * spotify-buttons.layout: Order in which widgets appear (defaults to song, previous, pause, next) + Widget names are: spotify-buttons.song, spotify-buttons.prev, spotify-buttons.pause, spotify-buttons.next """ class Module(core.module.Module): From 7215a11ffe9c1787aef6b5032d6a897956183fb2 Mon Sep 17 00:00:00 2001 From: LtPeriwinkle Date: Wed, 24 Jun 2020 12:22:03 -0700 Subject: [PATCH 012/506] black -t py34 --- .../modules/contrib/spotify-buttons.py | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/bumblebee_status/modules/contrib/spotify-buttons.py b/bumblebee_status/modules/contrib/spotify-buttons.py index 88c8ae9..26ec14c 100644 --- a/bumblebee_status/modules/contrib/spotify-buttons.py +++ b/bumblebee_status/modules/contrib/spotify-buttons.py @@ -5,6 +5,7 @@ import core.module import core.widget import core.input import core.decorators + """Displays the current song being played and allows pausing, skipping ahead, and skipping back. Requires the following library: @@ -17,11 +18,15 @@ Parameters: Widget names are: spotify-buttons.song, spotify-buttons.prev, spotify-buttons.pause, spotify-buttons.next """ + class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, []) - self.__layout = self.parameter("layout", "spotify-buttons.song spotify-buttons.prev spotify-buttons.pause spotify-buttons.next") + self.__layout = self.parameter( + "layout", + "spotify-buttons.song spotify-buttons.prev spotify-buttons.pause spotify-buttons.next", + ) self.__song = "" self.__pause = "" @@ -55,34 +60,36 @@ class Module(core.module.Module): artist=",".join(props.get("xesam:artist")), trackNumber=str(props.get("xesam:trackNumber")), ) - #this feels like a stupid way to do this but its all i can think of + # this feels like a stupid way to do this but its all i can think of widget_map = {} for widget_name in self.__layout.split(): - widget = self.add_widget(name = widget_name) + widget = self.add_widget(name=widget_name) if widget_name == "spotify-buttons.prev": widget_map[widget] = { "button": core.input.LEFT_MOUSE, "cmd": self.__cmd + "Previous", - } + } widget.full_text("\u258F\u25C0") elif widget_name == "spotify-buttons.pause": widget_map[widget] = { "button": core.input.LEFT_MOUSE, "cmd": self.__cmd + "PlayPause", - } + } widget.full_text(self.__pause) elif widget_name == "spotify-buttons.next": widget_map[widget] = { "button": core.input.LEFT_MOUSE, "cmd": self.__cmd + "Next", - } + } widget.full_text("\u25B6\u2595") elif widget_name == "spotify-buttons.song": widget.full_text(self.__song) else: raise KeyError( - "The spotify-buttons module does not have a {widget_name!r} widget".format(widget_name=widget_name) + "The spotify-buttons module does not have a {widget_name!r} widget".format( + widget_name=widget_name ) + ) for widget, callback_options in widget_map.items(): core.input.register(widget, **callback_options) From 4b54d1981c6f415bf4e2ced30f1a6c0ab3cb817c Mon Sep 17 00:00:00 2001 From: LtPeriwinkle Date: Wed, 24 Jun 2020 13:53:02 -0700 Subject: [PATCH 013/506] fix grammar --- bumblebee_status/modules/contrib/spotify-buttons.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bumblebee_status/modules/contrib/spotify-buttons.py b/bumblebee_status/modules/contrib/spotify-buttons.py index 26ec14c..8adb6d2 100644 --- a/bumblebee_status/modules/contrib/spotify-buttons.py +++ b/bumblebee_status/modules/contrib/spotify-buttons.py @@ -60,7 +60,8 @@ class Module(core.module.Module): artist=",".join(props.get("xesam:artist")), trackNumber=str(props.get("xesam:trackNumber")), ) - # this feels like a stupid way to do this but its all i can think of + + #add widgets widget_map = {} for widget_name in self.__layout.split(): widget = self.add_widget(name=widget_name) From bdc38e7934ab8de5ad931d04a7909b6867a84239 Mon Sep 17 00:00:00 2001 From: Tobias Witek Date: Thu, 25 Jun 2020 15:43:25 +0200 Subject: [PATCH 014/506] [doc] add shield for bumblebee-status-git AUR package --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6663f4d..9be6247 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,8 @@ [![Build Status](https://travis-ci.org/tobi-wan-kenobi/bumblebee-status.svg?branch=main)](https://travis-ci.org/tobi-wan-kenobi/bumblebee-status) [![Documentation Status](https://readthedocs.org/projects/bumblebee-status/badge/?version=main)](https://bumblebee-status.readthedocs.io/en/main/?badge=main) -![AUR version](https://img.shields.io/aur/version/bumblebee-status) +![AUR version (release)](https://img.shields.io/aur/version/bumblebee-status) +![AUR version (git)](https://img.shields.io/aur/version/bumblebee-status-git) [![PyPI version](https://badge.fury.io/py/bumblebee-status.svg)](https://badge.fury.io/py/bumblebee-status) [![Code Climate](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/gpa.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status) [![Test Coverage](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/coverage.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/coverage) From 6a8d8302818a716597e7e8d36f943e9912fae6f7 Mon Sep 17 00:00:00 2001 From: LtPeriwinkle Date: Thu, 25 Jun 2020 10:20:02 -0700 Subject: [PATCH 015/506] util.format.aslist() for layout --- bumblebee_status/modules/contrib/spotify-buttons.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bumblebee_status/modules/contrib/spotify-buttons.py b/bumblebee_status/modules/contrib/spotify-buttons.py index 8adb6d2..be198b8 100644 --- a/bumblebee_status/modules/contrib/spotify-buttons.py +++ b/bumblebee_status/modules/contrib/spotify-buttons.py @@ -5,6 +5,7 @@ import core.module import core.widget import core.input import core.decorators +import util.format """Displays the current song being played and allows pausing, skipping ahead, and skipping back. @@ -25,7 +26,7 @@ class Module(core.module.Module): self.__layout = self.parameter( "layout", - "spotify-buttons.song spotify-buttons.prev spotify-buttons.pause spotify-buttons.next", + util.format.aslist("spotify-buttons.song,spotify-buttons.prev,spotify-buttons.pause,spotify-buttons.next"), ) self.__song = "" @@ -63,7 +64,7 @@ class Module(core.module.Module): #add widgets widget_map = {} - for widget_name in self.__layout.split(): + for widget_name in self.__layout: widget = self.add_widget(name=widget_name) if widget_name == "spotify-buttons.prev": widget_map[widget] = { From 057faa55777547ea0ac24394f900a8c1922b4af2 Mon Sep 17 00:00:00 2001 From: LtPeriwinkle Date: Thu, 25 Jun 2020 10:23:45 -0700 Subject: [PATCH 016/506] replace original spotify with buttons version --- .../modules/contrib/spotify-buttons.py | 105 ----------------- bumblebee_status/modules/contrib/spotify.py | 106 ++++++++++-------- 2 files changed, 58 insertions(+), 153 deletions(-) delete mode 100644 bumblebee_status/modules/contrib/spotify-buttons.py diff --git a/bumblebee_status/modules/contrib/spotify-buttons.py b/bumblebee_status/modules/contrib/spotify-buttons.py deleted file mode 100644 index be198b8..0000000 --- a/bumblebee_status/modules/contrib/spotify-buttons.py +++ /dev/null @@ -1,105 +0,0 @@ -import sys -import dbus - -import core.module -import core.widget -import core.input -import core.decorators -import util.format - -"""Displays the current song being played and allows pausing, skipping ahead, and skipping back. - -Requires the following library: - * python-dbus - -Parameters: - * spotify-buttons.format: Format string (defaults to '{artist} - {title}') - Available values are: {album}, {title}, {artist}, {trackNumber} - * spotify-buttons.layout: Order in which widgets appear (defaults to song, previous, pause, next) - Widget names are: spotify-buttons.song, spotify-buttons.prev, spotify-buttons.pause, spotify-buttons.next -""" - - -class Module(core.module.Module): - def __init__(self, config, theme): - super().__init__(config, theme, []) - - self.__layout = self.parameter( - "layout", - util.format.aslist("spotify-buttons.song,spotify-buttons.prev,spotify-buttons.pause,spotify-buttons.next"), - ) - - self.__song = "" - self.__pause = "" - self.__format = self.parameter("format", "{artist} - {title}") - - self.__cmd = "dbus-send --session --type=method_call --dest=org.mpris.MediaPlayer2.spotify \ - /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player." - - def hidden(self): - return self.string_song == "" - - def update(self): - try: - self.clear_widgets() - bus = dbus.SessionBus() - spotify = bus.get_object( - "org.mpris.MediaPlayer2.spotify", "/org/mpris/MediaPlayer2" - ) - spotify_iface = dbus.Interface(spotify, "org.freedesktop.DBus.Properties") - props = spotify_iface.Get("org.mpris.MediaPlayer2.Player", "Metadata") - playback_status = str( - spotify_iface.Get("org.mpris.MediaPlayer2.Player", "PlaybackStatus") - ) - if playback_status == "Playing": - self.__pause = "\u258D\u258D" - else: - self.__pause = "\u25B6" - self.__song = self.__format.format( - album=str(props.get("xesam:album")), - title=str(props.get("xesam:title")), - artist=",".join(props.get("xesam:artist")), - trackNumber=str(props.get("xesam:trackNumber")), - ) - - #add widgets - widget_map = {} - for widget_name in self.__layout: - widget = self.add_widget(name=widget_name) - if widget_name == "spotify-buttons.prev": - widget_map[widget] = { - "button": core.input.LEFT_MOUSE, - "cmd": self.__cmd + "Previous", - } - widget.full_text("\u258F\u25C0") - elif widget_name == "spotify-buttons.pause": - widget_map[widget] = { - "button": core.input.LEFT_MOUSE, - "cmd": self.__cmd + "PlayPause", - } - widget.full_text(self.__pause) - elif widget_name == "spotify-buttons.next": - widget_map[widget] = { - "button": core.input.LEFT_MOUSE, - "cmd": self.__cmd + "Next", - } - widget.full_text("\u25B6\u2595") - elif widget_name == "spotify-buttons.song": - widget.full_text(self.__song) - else: - raise KeyError( - "The spotify-buttons module does not have a {widget_name!r} widget".format( - widget_name=widget_name - ) - ) - for widget, callback_options in widget_map.items(): - core.input.register(widget, **callback_options) - - except Exception: - self.__song = "" - - @property - def string_song(self): - if sys.version_info.major < 3: - return unicode(self.__song) - return str(self.__song) diff --git a/bumblebee_status/modules/contrib/spotify.py b/bumblebee_status/modules/contrib/spotify.py index dc371db..159f2ce 100644 --- a/bumblebee_status/modules/contrib/spotify.py +++ b/bumblebee_status/modules/contrib/spotify.py @@ -1,24 +1,3 @@ -# pylint: disable=C0111,R0903 - -"""Displays the current song being played - -Requires the following library: - * python-dbus - -Parameters: - * spotify.format: Format string (defaults to '{artist} - {title}') - Available values are: {album}, {title}, {artist}, {trackNumber}, {playbackStatus} - * spotify.previous: Change binding for previous song (default is left click) - * spotify.next: Change binding for next song (default is right click) - * spotify.pause: Change binding for toggling pause (default is middle click) - - Available options for spotify.previous, spotify.next and spotify.pause are: - LEFT_CLICK, RIGHT_CLICK, MIDDLE_CLICK, SCROLL_UP, SCROLL_DOWN - - -contributed by `yvesh `_ - many thanks! -""" - import sys import dbus @@ -26,41 +5,43 @@ import core.module import core.widget import core.input import core.decorators +import util.format + +"""Displays the current song being played and allows pausing, skipping ahead, and skipping back. + +Requires the following library: + * python-dbus + +Parameters: + * spotify-buttons.format: Format string (defaults to '{artist} - {title}') + Available values are: {album}, {title}, {artist}, {trackNumber} + * spotify-buttons.layout: Order in which widgets appear (defaults to song, previous, pause, next) + Widget names are: spotify-buttons.song, spotify-buttons.prev, spotify-buttons.pause, spotify-buttons.next +""" class Module(core.module.Module): def __init__(self, config, theme): - super().__init__(config, theme, core.widget.Widget(self.spotify)) + super().__init__(config, theme, []) - buttons = { - "LEFT_CLICK": core.input.LEFT_MOUSE, - "RIGHT_CLICK": core.input.RIGHT_MOUSE, - "MIDDLE_CLICK": core.input.MIDDLE_MOUSE, - "SCROLL_UP": core.input.WHEEL_UP, - "SCROLL_DOWN": core.input.WHEEL_DOWN, - } + self.__layout = self.parameter( + "layout", + util.format.aslist("spotify-buttons.song,spotify-buttons.prev,spotify-buttons.pause,spotify-buttons.next"), + ) self.__song = "" + self.__pause = "" self.__format = self.parameter("format", "{artist} - {title}") - prev_button = self.parameter("previous", "LEFT_CLICK") - next_button = self.parameter("next", "RIGHT_CLICK") - pause_button = self.parameter("pause", "MIDDLE_CLICK") - cmd = "dbus-send --session --type=method_call --dest=org.mpris.MediaPlayer2.spotify \ + self.__cmd = "dbus-send --session --type=method_call --dest=org.mpris.MediaPlayer2.spotify \ /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player." - core.input.register(self, button=buttons[prev_button], cmd=cmd + "Previous") - core.input.register(self, button=buttons[next_button], cmd=cmd + "Next") - core.input.register(self, button=buttons[pause_button], cmd=cmd + "PlayPause") - - @core.decorators.scrollable - def spotify(self, widget): - return self.string_song def hidden(self): return self.string_song == "" def update(self): try: + self.clear_widgets() bus = dbus.SessionBus() spotify = bus.get_object( "org.mpris.MediaPlayer2.spotify", "/org/mpris/MediaPlayer2" @@ -70,18 +51,50 @@ class Module(core.module.Module): playback_status = str( spotify_iface.Get("org.mpris.MediaPlayer2.Player", "PlaybackStatus") ) + if playback_status == "Playing": + self.__pause = "\u258D\u258D" + else: + self.__pause = "\u25B6" self.__song = self.__format.format( album=str(props.get("xesam:album")), title=str(props.get("xesam:title")), artist=",".join(props.get("xesam:artist")), trackNumber=str(props.get("xesam:trackNumber")), - playbackStatus="\u25B6" - if playback_status == "Playing" - else "\u258D\u258D" - if playback_status == "Paused" - else "", ) + #add widgets + widget_map = {} + for widget_name in self.__layout: + widget = self.add_widget(name=widget_name) + if widget_name == "spotify-buttons.prev": + widget_map[widget] = { + "button": core.input.LEFT_MOUSE, + "cmd": self.__cmd + "Previous", + } + widget.full_text("\u258F\u25C0") + elif widget_name == "spotify-buttons.pause": + widget_map[widget] = { + "button": core.input.LEFT_MOUSE, + "cmd": self.__cmd + "PlayPause", + } + widget.full_text(self.__pause) + elif widget_name == "spotify-buttons.next": + widget_map[widget] = { + "button": core.input.LEFT_MOUSE, + "cmd": self.__cmd + "Next", + } + widget.full_text("\u25B6\u2595") + elif widget_name == "spotify-buttons.song": + widget.full_text(self.__song) + else: + raise KeyError( + "The spotify module does not have a {widget_name!r} widget".format( + widget_name=widget_name + ) + ) + for widget, callback_options in widget_map.items(): + core.input.register(widget, **callback_options) + except Exception: self.__song = "" @@ -90,6 +103,3 @@ class Module(core.module.Module): if sys.version_info.major < 3: return unicode(self.__song) return str(self.__song) - - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From 4cbe04f0b0c39921bfd57e873a99f4f364e08bb2 Mon Sep 17 00:00:00 2001 From: LtPeriwinkle Date: Thu, 25 Jun 2020 10:53:53 -0700 Subject: [PATCH 017/506] remove -buttons, move getting song out of update() --- bumblebee_status/modules/contrib/spotify.py | 56 +++++++++++---------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/bumblebee_status/modules/contrib/spotify.py b/bumblebee_status/modules/contrib/spotify.py index 159f2ce..66b67c9 100644 --- a/bumblebee_status/modules/contrib/spotify.py +++ b/bumblebee_status/modules/contrib/spotify.py @@ -13,10 +13,10 @@ Requires the following library: * python-dbus Parameters: - * spotify-buttons.format: Format string (defaults to '{artist} - {title}') + * spotify.format: Format string (defaults to '{artist} - {title}') Available values are: {album}, {title}, {artist}, {trackNumber} - * spotify-buttons.layout: Order in which widgets appear (defaults to song, previous, pause, next) - Widget names are: spotify-buttons.song, spotify-buttons.prev, spotify-buttons.pause, spotify-buttons.next + * spotify.layout: Comma-separated list to change order of widgets (defaults to song, previous, pause, next) + Widget names are: spotify.song, spotify.prev, spotify.pause, spotify.next """ @@ -26,7 +26,7 @@ class Module(core.module.Module): self.__layout = self.parameter( "layout", - util.format.aslist("spotify-buttons.song,spotify-buttons.prev,spotify-buttons.pause,spotify-buttons.next"), + util.format.aslist("spotify.song,spotify.prev,spotify.pause,spotify.next"), ) self.__song = "" @@ -39,52 +39,54 @@ class Module(core.module.Module): def hidden(self): return self.string_song == "" - def update(self): - try: - self.clear_widgets() - bus = dbus.SessionBus() - spotify = bus.get_object( - "org.mpris.MediaPlayer2.spotify", "/org/mpris/MediaPlayer2" - ) - spotify_iface = dbus.Interface(spotify, "org.freedesktop.DBus.Properties") - props = spotify_iface.Get("org.mpris.MediaPlayer2.Player", "Metadata") - playback_status = str( - spotify_iface.Get("org.mpris.MediaPlayer2.Player", "PlaybackStatus") - ) - if playback_status == "Playing": - self.__pause = "\u258D\u258D" - else: - self.__pause = "\u25B6" + def __get_song(self): + bus = dbus.SessionBus() + spotify = bus.get_object( + "org.mpris.MediaPlayer2.spotify", "/org/mpris/MediaPlayer2" + ) + spotify_iface = dbus.Interface(spotify, "org.freedesktop.DBus.Properties") + props = spotify_iface.Get("org.mpris.MediaPlayer2.Player", "Metadata") + playback_status = str( + spotify_iface.Get("org.mpris.MediaPlayer2.Player", "PlaybackStatus") + ) + if playback_status == "Playing": + self.__pause = "\u258D\u258D" + else: + self.__pause = "\u25B6" self.__song = self.__format.format( - album=str(props.get("xesam:album")), - title=str(props.get("xesam:title")), + album=str(props.get("xesam:album")), + title=str(props.get("xesam:title")), artist=",".join(props.get("xesam:artist")), trackNumber=str(props.get("xesam:trackNumber")), ) - #add widgets + def update(self): + try: + self.clear_widgets() + self.__get_song() + widget_map = {} for widget_name in self.__layout: widget = self.add_widget(name=widget_name) - if widget_name == "spotify-buttons.prev": + if widget_name == "spotify.prev": widget_map[widget] = { "button": core.input.LEFT_MOUSE, "cmd": self.__cmd + "Previous", } widget.full_text("\u258F\u25C0") - elif widget_name == "spotify-buttons.pause": + elif widget_name == "spotify.pause": widget_map[widget] = { "button": core.input.LEFT_MOUSE, "cmd": self.__cmd + "PlayPause", } widget.full_text(self.__pause) - elif widget_name == "spotify-buttons.next": + elif widget_name == "spotify.next": widget_map[widget] = { "button": core.input.LEFT_MOUSE, "cmd": self.__cmd + "Next", } widget.full_text("\u25B6\u2595") - elif widget_name == "spotify-buttons.song": + elif widget_name == "spotify.song": widget.full_text(self.__song) else: raise KeyError( From 542d841622b4aee1339d8f1f274c6d5bb7bf2db9 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Thu, 25 Jun 2020 20:00:05 +0200 Subject: [PATCH 018/506] [modules/spotify] add contribution --- bumblebee_status/modules/contrib/spotify.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bumblebee_status/modules/contrib/spotify.py b/bumblebee_status/modules/contrib/spotify.py index 66b67c9..3f26ef7 100644 --- a/bumblebee_status/modules/contrib/spotify.py +++ b/bumblebee_status/modules/contrib/spotify.py @@ -17,6 +17,10 @@ Parameters: Available values are: {album}, {title}, {artist}, {trackNumber} * spotify.layout: Comma-separated list to change order of widgets (defaults to song, previous, pause, next) Widget names are: spotify.song, spotify.prev, spotify.pause, spotify.next + +contributed by `yvesh `_ - many thanks! + +added controls by `LtPeriwinkle `_ - many thanks! """ From e9b917c214991ee19d1e5777c0b895d23751578e Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Thu, 25 Jun 2020 20:00:57 +0200 Subject: [PATCH 019/506] [doc] update spotify --- bumblebee_status/modules/contrib/spotify.py | 18 +++++++++--------- docs/modules.rst | 15 ++++++--------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/bumblebee_status/modules/contrib/spotify.py b/bumblebee_status/modules/contrib/spotify.py index 3f26ef7..eab5573 100644 --- a/bumblebee_status/modules/contrib/spotify.py +++ b/bumblebee_status/modules/contrib/spotify.py @@ -1,12 +1,3 @@ -import sys -import dbus - -import core.module -import core.widget -import core.input -import core.decorators -import util.format - """Displays the current song being played and allows pausing, skipping ahead, and skipping back. Requires the following library: @@ -23,6 +14,15 @@ contributed by `yvesh `_ - many thanks! added controls by `LtPeriwinkle `_ - many thanks! """ +import sys +import dbus + +import core.module +import core.widget +import core.input +import core.decorators +import util.format + class Module(core.module.Module): def __init__(self, config, theme): diff --git a/docs/modules.rst b/docs/modules.rst index ed99936..41366aa 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -1123,24 +1123,21 @@ contributed by `rad4day `_ - many thanks! spotify ~~~~~~~ -Displays the current song being played +Displays the current song being played and allows pausing, skipping ahead, and skipping back. Requires the following library: * python-dbus Parameters: * spotify.format: Format string (defaults to '{artist} - {title}') - Available values are: {album}, {title}, {artist}, {trackNumber}, {playbackStatus} - * spotify.previous: Change binding for previous song (default is left click) - * spotify.next: Change binding for next song (default is right click) - * spotify.pause: Change binding for toggling pause (default is middle click) - - Available options for spotify.previous, spotify.next and spotify.pause are: - LEFT_CLICK, RIGHT_CLICK, MIDDLE_CLICK, SCROLL_UP, SCROLL_DOWN - + Available values are: {album}, {title}, {artist}, {trackNumber} + * spotify.layout: Comma-separated list to change order of widgets (defaults to song, previous, pause, next) + Widget names are: spotify.song, spotify.prev, spotify.pause, spotify.next contributed by `yvesh `_ - many thanks! +added controls by `LtPeriwinkle `_ - many thanks! + .. image:: ../screenshots/spotify.png stock From 72deb7eaf80308d06773183c59f6cc3e6e95cf18 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Thu, 25 Jun 2020 20:34:13 +0200 Subject: [PATCH 020/506] [modules] add speed test module --- bumblebee_status/core/output.py | 5 ++- bumblebee_status/modules/core/ping.py | 2 - bumblebee_status/modules/core/speedtest.py | 52 ++++++++++++++++++++++ themes/icons/awesome-fonts.json | 4 ++ 4 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 bumblebee_status/modules/core/speedtest.py diff --git a/bumblebee_status/core/output.py b/bumblebee_status/core/output.py index 929f0f4..088678d 100644 --- a/bumblebee_status/core/output.py +++ b/bumblebee_status/core/output.py @@ -228,16 +228,17 @@ class i3(object): if affected_modules and not module.id in affected_modules: continue if not affected_modules and module.next_update: - if module.parameter("interval", "") == "never": - continue if now < module.next_update: continue + if not redraw_only: module.update_wrapper() if module.parameter("interval", "") != "never": module.next_update = now + util.format.seconds( module.parameter("interval", self.__config.interval()) ) + else: + module.next_update = sys.maxsize for widget in module.widgets(): if not widget.id in self.__content: self.__content[widget.id] = { "minimized": False } diff --git a/bumblebee_status/modules/core/ping.py b/bumblebee_status/modules/core/ping.py index d43c027..7d650cd 100644 --- a/bumblebee_status/modules/core/ping.py +++ b/bumblebee_status/modules/core/ping.py @@ -24,8 +24,6 @@ import core.decorators import util.cli - - class Module(core.module.Module): @core.decorators.every(seconds=60) def __init__(self, config, theme): diff --git a/bumblebee_status/modules/core/speedtest.py b/bumblebee_status/modules/core/speedtest.py new file mode 100644 index 0000000..8f08bf9 --- /dev/null +++ b/bumblebee_status/modules/core/speedtest.py @@ -0,0 +1,52 @@ +# pylint: disable=C0111,R0903 + +"""Performs a speedtest - only updates when the "play" button is clicked + +Requires the following python module: + * speedtest-cli + +""" + +import core.module +import core.widget +import core.decorators + +import speedtest + + +class Module(core.module.Module): + @core.decorators.never + def __init__(self, config, theme): + super().__init__(config, theme, []) + + self.background = True + self.__result = "waiting" + self.__running = False + + start = self.add_widget(name="start") + main = self.add_widget(name="main", full_text=self.result) + + def result(self, _): + return self.__result + + def update(self): + self.__running = True + s = speedtest.Speedtest() + s.get_best_server() + s.download(threads=None) + s.upload(threads=None) + + self.__result = "ping: {:.2f}ms down: {:.2f}Mbps up: {:.2f}Mbps".format( + s.results.ping, + s.results.download / 1024 / 1024, + s.results.upload / 1024 / 1024, + ) + self.__running = False + + def state(self, widget): + if widget.name == "start": + return "running" if self.__running else "not-running" + return None + + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/themes/icons/awesome-fonts.json b/themes/icons/awesome-fonts.json index 5a5269c..f524de3 100644 --- a/themes/icons/awesome-fonts.json +++ b/themes/icons/awesome-fonts.json @@ -281,5 +281,9 @@ }, "arandr": { "prefix": "" + }, + "speedtest": { + "running": { "prefix": "\uf110" }, + "not-running": { "prefix": "\uf144" } } } From 7a6788dc1c32ef42c11706ad13cdc1224562f981 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Thu, 25 Jun 2020 20:35:00 +0200 Subject: [PATCH 021/506] [doc] add speedtest module --- docs/modules.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/modules.rst b/docs/modules.rst index 41366aa..7e28095 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -221,6 +221,14 @@ Parameters: .. image:: ../screenshots/spacer.png +speedtest +~~~~~~~~~ + +Performs a speedtest - only updates when the "play" button is clicked + +Requires the following python module: + * speedtest-cli + test ~~~~ From 44b3df58271f5af3c001bcc2ccaf734a48c650d0 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Thu, 25 Jun 2020 20:44:55 +0200 Subject: [PATCH 022/506] [modules/speedtest] make speedtest re-triggerable --- bumblebee_status/modules/core/speedtest.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bumblebee_status/modules/core/speedtest.py b/bumblebee_status/modules/core/speedtest.py index 8f08bf9..2b39cff 100644 --- a/bumblebee_status/modules/core/speedtest.py +++ b/bumblebee_status/modules/core/speedtest.py @@ -9,6 +9,7 @@ Requires the following python module: import core.module import core.widget +import core.input import core.decorators import speedtest @@ -26,9 +27,14 @@ class Module(core.module.Module): start = self.add_widget(name="start") main = self.add_widget(name="main", full_text=self.result) + core.input.register(start, button=core.input.LEFT_MOUSE, cmd=self.update_event) + def result(self, _): return self.__result + def update_event(self, _): + self.update() + def update(self): self.__running = True s = speedtest.Speedtest() From 4ac8c2ef7a5166326978e61210c6e128c44c4e8f Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Thu, 25 Jun 2020 20:46:13 +0200 Subject: [PATCH 023/506] [modules/speedtest] small fix for retrigger --- bumblebee_status/modules/core/speedtest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bumblebee_status/modules/core/speedtest.py b/bumblebee_status/modules/core/speedtest.py index 2b39cff..aa07a16 100644 --- a/bumblebee_status/modules/core/speedtest.py +++ b/bumblebee_status/modules/core/speedtest.py @@ -10,6 +10,7 @@ Requires the following python module: import core.module import core.widget import core.input +import core.event import core.decorators import speedtest @@ -37,6 +38,7 @@ class Module(core.module.Module): def update(self): self.__running = True + core.event.trigger("update", [self.id], redraw_only=True) s = speedtest.Speedtest() s.get_best_server() s.download(threads=None) From 4925e09995b6b65e603caf0c2c7e45ef24925d04 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Fri, 26 Jun 2020 09:40:35 +0200 Subject: [PATCH 024/506] [modules/speedtest] no autostart, improve icons do not start a speedtest automatically during startup, and improve the icons a bit. --- bumblebee_status/modules/core/speedtest.py | 9 +++++++-- themes/icons/ascii.json | 4 ++++ themes/icons/awesome-fonts.json | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/bumblebee_status/modules/core/speedtest.py b/bumblebee_status/modules/core/speedtest.py index aa07a16..947666a 100644 --- a/bumblebee_status/modules/core/speedtest.py +++ b/bumblebee_status/modules/core/speedtest.py @@ -7,6 +7,8 @@ Requires the following python module: """ +import sys + import core.module import core.widget import core.input @@ -22,7 +24,7 @@ class Module(core.module.Module): super().__init__(config, theme, []) self.background = True - self.__result = "waiting" + self.__result = "" self.__running = False start = self.add_widget(name="start") @@ -34,10 +36,12 @@ class Module(core.module.Module): return self.__result def update_event(self, _): + self.__running = True self.update() def update(self): - self.__running = True + if not self.__running: + return core.event.trigger("update", [self.id], redraw_only=True) s = speedtest.Speedtest() s.get_best_server() @@ -50,6 +54,7 @@ class Module(core.module.Module): s.results.upload / 1024 / 1024, ) self.__running = False + core.event.trigger("update", [self.id], redraw_only=True) def state(self, widget): if widget.name == "start": diff --git a/themes/icons/ascii.json b/themes/icons/ascii.json index 1913752..ba2f414 100644 --- a/themes/icons/ascii.json +++ b/themes/icons/ascii.json @@ -362,5 +362,9 @@ }, "arandr": { "prefix": " displays " + }, + "speedtest": { + "running": { "prefix": [".", "..", "...", ".."] }, + "not-running": { "prefix": "[start]" } } } diff --git a/themes/icons/awesome-fonts.json b/themes/icons/awesome-fonts.json index f524de3..d6b2eb5 100644 --- a/themes/icons/awesome-fonts.json +++ b/themes/icons/awesome-fonts.json @@ -283,7 +283,7 @@ "prefix": "" }, "speedtest": { - "running": { "prefix": "\uf110" }, + "running": { "prefix": ["\uf251", "\uf252", "\uf253"] }, "not-running": { "prefix": "\uf144" } } } From 1484109fe07b78f605278bdac22921749e36e604 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Fri, 26 Jun 2020 09:43:04 +0200 Subject: [PATCH 025/506] [doc] removing user contributions as somospocos decided to delete his github account, remove the broken links. again, somospocos, many thanks for the ways in which you improved bumblebee-status! --- README.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/README.md b/README.md index 9be6247..7c26ddd 100644 --- a/README.md +++ b/README.md @@ -133,14 +133,6 @@ bar { Restart i3wm and - that's it! -# User contributions - -[@somospocos:bumblebee-status-contrib](https://github.com/somospocos/bumblebee-status-contrib): Collected resources and useful tricks by @somospocos - -[@somospocos:bumblebee-bridge-dwm](https://github.com/somospocos/bumblebee-bridge-dwm): Bridge bumblebee-status output into dwm status bar - -[@somospocos:bumblebee-bridge-dzen2](https://github.com/somospocos/bumblebee-bridge-dzen2): Bridge bumblebee-status output into dzen2 - # Examples [List of themes](https://bumblebee-status.readthedocs.io/en/main/themes.html) From 954d7545e38394088a363d04cc98f234c0610087 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sat, 27 Jun 2020 15:02:55 +0200 Subject: [PATCH 026/506] [util/format] make temperature metric case insensitive see #664 --- bumblebee_status/util/format.py | 2 +- tests/util/test_format.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/bumblebee_status/util/format.py b/bumblebee_status/util/format.py index 198e995..65242a3 100644 --- a/bumblebee_status/util/format.py +++ b/bumblebee_status/util/format.py @@ -68,7 +68,7 @@ def astemperature(val, unit="metric"): :return: temperature representation of the input value :rtype: string """ - return "{}°{}".format(int(val), __UNITS.get(unit, __UNITS["default"])) + return "{}°{}".format(int(val), __UNITS.get(unit.lower(), __UNITS["default"])) def byte(val, fmt="{:.2f}"): diff --git a/tests/util/test_format.py b/tests/util/test_format.py index 4dda6e9..1e34dec 100644 --- a/tests/util/test_format.py +++ b/tests/util/test_format.py @@ -110,4 +110,7 @@ def test_temperature(): assert astemperature(-100, "kelvin") == "-100°K" +def test_temperature_case(): + assert astemperature(100, "ImPeRiAl") == "100°F" + # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From 350648b0cd9e3c3b980c6e72946ddfc8d14cb11f Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sat, 27 Jun 2020 15:05:57 +0200 Subject: [PATCH 027/506] [util/location] reverse location providers seems that ipapi gives better results wrt. location --- bumblebee_status/util/location.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/bumblebee_status/util/location.py b/bumblebee_status/util/location.py index f8cb441..12242ea 100644 --- a/bumblebee_status/util/location.py +++ b/bumblebee_status/util/location.py @@ -16,15 +16,6 @@ __document = None __data = {} __next = 0 __sources = [ - { - "url": "http://free.ipwhois.io/json/", - "mapping": { - "latitude": "latitude", - "longitude": "longitude", - "country": "country", - "ip": "public_ip", - }, - }, { "url": "http://ipapi.co/json", "mapping": { @@ -34,6 +25,15 @@ __sources = [ "ip": "public_ip", }, }, + { + "url": "http://free.ipwhois.io/json/", + "mapping": { + "latitude": "latitude", + "longitude": "longitude", + "country": "country", + "ip": "public_ip", + }, + } ] From fc8783ee133dffafe7c8dfbf520dda2ce0ba5ae2 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sun, 28 Jun 2020 10:44:30 +0200 Subject: [PATCH 028/506] [util/cli] fix CLI invokation for sway in sway/wayland, make sure that the wayland socket is cleared, which seems to cause issue for some unspecified reason. also, while at it, improve code so that the environment dict that is passed in is *not* modified. fixes #628 --- bumblebee_status/util/cli.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/bumblebee_status/util/cli.py b/bumblebee_status/util/cli.py index 8b39aab..d14dc6a 100644 --- a/bumblebee_status/util/cli.py +++ b/bumblebee_status/util/cli.py @@ -33,14 +33,19 @@ def execute( if not env: env = os.environ.copy() - env["LC_ALL"] = "C" + + myenv = env.copy() + + myenv["LC_ALL"] = "C" + if "WAYLAND_SOCKET" in myenv: + del myenv["WAYLAND_SOCKET"] try: proc = subprocess.Popen( args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT if include_stderr else subprocess.PIPE, - env=env, + env=myenv, shell=shell, ) except FileNotFoundError as e: From 81c5e75624d3d960f60f40ccb76d777827d43be2 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sun, 28 Jun 2020 10:45:22 +0200 Subject: [PATCH 029/506] [tests] fix location tests a previous commit that switched the primary and secondary location provider broke the unit tests - fix that. --- tests/util/test_location.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/util/test_location.py b/tests/util/test_location.py index 21dd2e2..e04e600 100644 --- a/tests/util/test_location.py +++ b/tests/util/test_location.py @@ -11,7 +11,7 @@ def urllib_req(mocker): @pytest.fixture -def primaryLocation(): +def secondaryLocation(): return { "country": "Middle Earth", "longitude": "10.0", @@ -21,7 +21,7 @@ def primaryLocation(): @pytest.fixture -def secondaryLocation(): +def primaryLocation(): return { "country_name": "Rivia", "longitude": "-10.0", @@ -33,7 +33,7 @@ def secondaryLocation(): def test_primary_provider(urllib_req, primaryLocation): urllib_req.urlopen.return_value.read.return_value = json.dumps(primaryLocation) - assert util.location.country() == primaryLocation["country"] + assert util.location.country() == primaryLocation["country_name"] assert util.location.coordinates() == ( primaryLocation["latitude"], primaryLocation["longitude"], @@ -46,7 +46,7 @@ def test_secondary_provider(mocker, urllib_req, secondaryLocation): urlopen.read.return_value = json.dumps(secondaryLocation) urllib_req.urlopen.side_effect = [RuntimeError(), urlopen] - assert util.location.country() == secondaryLocation["country_name"] + assert util.location.country() == secondaryLocation["country"] assert util.location.coordinates() == ( secondaryLocation["latitude"], secondaryLocation["longitude"], From 8f3d48c0e68773719cfa017dcb3e3f3384f12bcc Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sun, 28 Jun 2020 10:59:43 +0200 Subject: [PATCH 030/506] [modules/brightness] re-enable reading brightness from ACPI to enable reading the brightness from ACPF, set the device path and - other than previously - explicitly enable this by setting the parameter "brightness.use_acpi" to "true". fixes #665 --- .../modules/contrib/brightness.py | 32 +++++++++++++++++-- docs/modules.rst | 2 ++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/bumblebee_status/modules/contrib/brightness.py b/bumblebee_status/modules/contrib/brightness.py index eb68a4f..28b94a8 100644 --- a/bumblebee_status/modules/contrib/brightness.py +++ b/bumblebee_status/modules/contrib/brightness.py @@ -4,6 +4,8 @@ Parameters: * brightness.step: The amount of increase/decrease on scroll in % (defaults to 2) + * brightness.device_path: The device path (defaults to /sys/class/backlight/intel_backlight), can contain wildcards (in this case, the first matching path will be used); This is only used when brightness.use_acpi is set to true + * brightness.use_acpi: If set to true, read brightness directly from the sys ACPI interface, using the device specified in brightness.device_path (defaults to false) contributed by `TheEdgeOfRage `_ - many thanks! """ @@ -27,8 +29,12 @@ class Module(core.module.Module): self.__brightness = "n/a" self.__readcmd = None step = self.parameter("step", 2) + self.__device_path = self.find_device(self.parameter("device_path", "/sys/class/backlight/intel_backlight")) - if shutil.which("light"): + if util.format.asbool(self.parameter("use_acpi", False)): + self.__readcmd = self.__acpi + # TODO: add setting + elif shutil.which("light"): self.__readcmd = self.__light self.register_cmd("light -A {}%".format(step), "light -U {}%".format(step)) elif shutil.which("brightnessctl"): @@ -42,6 +48,12 @@ class Module(core.module.Module): "xbacklight +{}%".format(step), "xbacklight -{}%".format(step) ) + def find_device(self, device_path): + res = glob.glob(device_path) + if len(res) == 0: + return device_path + return res[0] + def register_cmd(self, up_cmd, down_cmd): core.input.register(self, button=core.input.WHEEL_UP, cmd=up_cmd) core.input.register(self, button=core.input.WHEEL_DOWN, cmd=down_cmd) @@ -49,6 +61,18 @@ class Module(core.module.Module): def brightness(self, widget): return self.__brightness + def __acpi(self): + try: + backlight = 1 + max_brightness = 1 + with open("{}/brightness".format(self.__device_path)) as f: + backlight = int(f.readline()) + with open("{}/max_brightness".format(self.__device_path)) as f: + max_brightness = int(f.readline()) + return float(backlight*100)/max_brightness + except: + return "unable to read brightness from {}".format(self.__device_path) + def __light(self): return util.cli.execute("light").strip() @@ -62,7 +86,11 @@ class Module(core.module.Module): def update(self): try: - self.__brightness = "{:3.0f}%".format(float(self.__readcmd())) + tmp = self.__readcmd() + if isinstance(tmp, str): + self.__brightness = tmp + else: + self.__brightness = "{:3.0f}%".format(float(tmp)) except: self.__brightness = "n/a" diff --git a/docs/modules.rst b/docs/modules.rst index 7e28095..984abb9 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -404,6 +404,8 @@ Displays the brightness of a display Parameters: * brightness.step: The amount of increase/decrease on scroll in % (defaults to 2) + * brightness.device_path: The device path (defaults to /sys/class/backlight/intel_backlight), can contain wildcards (in this case, the first matching path will be used); This is only used when brightness.use_acpi is set to true + * brightness.use_acpi: If set to true, read brightness directly from the sys ACPI interface, using the device specified in brightness.device_path (defaults to false) contributed by `TheEdgeOfRage `_ - many thanks! From 320bba97d059a59e253ca40aff92378492896027 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sun, 28 Jun 2020 20:01:08 +0200 Subject: [PATCH 031/506] [core/theme] Make theme iconsets *not* override settings Make sure that iconsets used as part of a theme do *not* override anything already existing inside the theme. Only iconsets that are manually specified can override settings in the theme now (because those, you typically specify on the CLI). TODO: Write unit test for this fixes #666 --- bumblebee_status/core/theme.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bumblebee_status/core/theme.py b/bumblebee_status/core/theme.py index f50286a..8ed689d 100644 --- a/bumblebee_status/core/theme.py +++ b/bumblebee_status/core/theme.py @@ -53,7 +53,7 @@ class Theme(object): self.__data = raw_data if raw_data else self.load(name) for icons in self.__data.get("icons", []): - self.__data = util.algorithm.merge(self.load(icons, "icons"), self.__data) + util.algorithm.merge(self.load(icons, "icons"), self.__data) if iconset != "auto": self.__data = util.algorithm.merge(self.load(iconset, "icons"), self.__data) for colors in self.__data.get("colors", []): From 6b09be1993751c56c24c5ffe55573d45f6bc88ec Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sun, 28 Jun 2020 20:09:55 +0200 Subject: [PATCH 032/506] Revert "[core/theme] Make theme iconsets *not* override settings" This reverts commit 320bba97d059a59e253ca40aff92378492896027. --- bumblebee_status/core/theme.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bumblebee_status/core/theme.py b/bumblebee_status/core/theme.py index 8ed689d..f50286a 100644 --- a/bumblebee_status/core/theme.py +++ b/bumblebee_status/core/theme.py @@ -53,7 +53,7 @@ class Theme(object): self.__data = raw_data if raw_data else self.load(name) for icons in self.__data.get("icons", []): - util.algorithm.merge(self.load(icons, "icons"), self.__data) + self.__data = util.algorithm.merge(self.load(icons, "icons"), self.__data) if iconset != "auto": self.__data = util.algorithm.merge(self.load(iconset, "icons"), self.__data) for colors in self.__data.get("colors", []): From 810daac16b5142353a8dabd96326ec29e2766bc4 Mon Sep 17 00:00:00 2001 From: gkeep Date: Mon, 29 Jun 2020 00:48:09 +0300 Subject: [PATCH 033/506] Add icons for different states --- themes/icons/ascii.json | 6 +++++- themes/icons/awesome-fonts.json | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/themes/icons/ascii.json b/themes/icons/ascii.json index ba2f414..0482a3b 100644 --- a/themes/icons/ascii.json +++ b/themes/icons/ascii.json @@ -297,7 +297,11 @@ "prefix": "" }, "spotify": { - "prefix": "" + "song": { "prefix": "spotify" }, + "prev": { "prefix": "|<" }, + "playing": { "prefix": "|>" }, + "paused": { "prefix": "||" }, + "next": { "prefix": ">|" } }, "uptime": { "prefix": "uptime" diff --git a/themes/icons/awesome-fonts.json b/themes/icons/awesome-fonts.json index d6b2eb5..07a11a8 100644 --- a/themes/icons/awesome-fonts.json +++ b/themes/icons/awesome-fonts.json @@ -219,7 +219,11 @@ "prefix": "  " }, "spotify": { - "prefix": "  " + "song": { "prefix": "" }, + "prev": { "prefix": "" }, + "playing": { "prefix": "" }, + "paused": { "prefix": "" }, + "next": { "prefix": "" } }, "publicip": { "prefix": "  " From 134bbbd74363929982e897f77cb9b8c7d470883b Mon Sep 17 00:00:00 2001 From: gkeep Date: Mon, 29 Jun 2020 00:57:01 +0300 Subject: [PATCH 034/506] Use icons depending on widget state and icon set --- bumblebee_status/modules/contrib/spotify.py | 32 +++++++++++---------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/bumblebee_status/modules/contrib/spotify.py b/bumblebee_status/modules/contrib/spotify.py index eab5573..f1e925c 100644 --- a/bumblebee_status/modules/contrib/spotify.py +++ b/bumblebee_status/modules/contrib/spotify.py @@ -50,25 +50,18 @@ class Module(core.module.Module): ) spotify_iface = dbus.Interface(spotify, "org.freedesktop.DBus.Properties") props = spotify_iface.Get("org.mpris.MediaPlayer2.Player", "Metadata") - playback_status = str( - spotify_iface.Get("org.mpris.MediaPlayer2.Player", "PlaybackStatus") - ) - if playback_status == "Playing": - self.__pause = "\u258D\u258D" - else: - self.__pause = "\u25B6" - self.__song = self.__format.format( + self.__song = self.__format.format( album=str(props.get("xesam:album")), title=str(props.get("xesam:title")), - artist=",".join(props.get("xesam:artist")), - trackNumber=str(props.get("xesam:trackNumber")), - ) + artist=",".join(props.get("xesam:artist")), + trackNumber=str(props.get("xesam:trackNumber")), + ) def update(self): try: self.clear_widgets() self.__get_song() - + widget_map = {} for widget_name in self.__layout: widget = self.add_widget(name=widget_name) @@ -77,20 +70,29 @@ class Module(core.module.Module): "button": core.input.LEFT_MOUSE, "cmd": self.__cmd + "Previous", } - widget.full_text("\u258F\u25C0") + widget.set("state", "prev") elif widget_name == "spotify.pause": widget_map[widget] = { "button": core.input.LEFT_MOUSE, "cmd": self.__cmd + "PlayPause", } - widget.full_text(self.__pause) + playback_status = str( + dbus.Interface(dbus.SessionBus().get_object( + "org.mpris.MediaPlayer2.spotify", "/org/mpris/MediaPlayer2"), "org.freedesktop.DBus.Properties") + .Get("org.mpris.MediaPlayer2.Player", "PlaybackStatus") + ) + if playback_status == "Playing": + widget.set("state", "playing") + else: + widget.set("state", "paused") elif widget_name == "spotify.next": widget_map[widget] = { "button": core.input.LEFT_MOUSE, "cmd": self.__cmd + "Next", } - widget.full_text("\u25B6\u2595") + widget.set("state", "next") elif widget_name == "spotify.song": + widget.set("state", "song") widget.full_text(self.__song) else: raise KeyError( From 9136ebd3211d7b68ce5d4052e616ef4d1d165eb5 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Mon, 29 Jun 2020 07:44:22 +0200 Subject: [PATCH 035/506] [core/input] clear previous input registrations make sure that for a given event (widget/object/module, whatever), only a *single* input event per button can be registered at one time. the problem otherwise is with modules that re-register their widgets with the same IDs (cmus, spotify, etc.): Each time the widget is re-created (each intervall, typically), it re-registers an input event, creating an always longer list of callbacks being executed when the button is clicked (not speaking of the memory leak this introduces). fixes #668 --- bumblebee_status/core/event.py | 3 +++ bumblebee_status/core/input.py | 1 + tests/core/test_input.py | 4 ++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/bumblebee_status/core/event.py b/bumblebee_status/core/event.py index 88096a5..8e969f0 100644 --- a/bumblebee_status/core/event.py +++ b/bumblebee_status/core/event.py @@ -8,6 +8,9 @@ def register(event, callback, *args, **kwargs): __callbacks.setdefault(event, []).append(cb) +def unregister(event): + if event in __callbacks: + del __callbacks[event] def clear(): __callbacks.clear() diff --git a/bumblebee_status/core/input.py b/bumblebee_status/core/input.py index a2ebaa8..9dbc2a6 100644 --- a/bumblebee_status/core/input.py +++ b/bumblebee_status/core/input.py @@ -50,6 +50,7 @@ def __execute(event, cmd, wait=False): def register(obj, button=None, cmd=None, wait=False): event_id = __event_id(obj.id if obj is not None else "", button) logging.debug("registering callback {}".format(event_id)) + core.event.unregister(event_id) # make sure there's always only one input event if callable(cmd): core.event.register(event_id, cmd) else: diff --git a/tests/core/test_input.py b/tests/core/test_input.py index 962b5b2..a12deb2 100644 --- a/tests/core/test_input.py +++ b/tests/core/test_input.py @@ -59,14 +59,14 @@ def test_different_events(obj, obj2, cb, cb2): cb2.assert_not_called() -def test_multiple_registrations(obj, cb, cb2): +def test_multiple_registrations_on_same_button(obj, cb, cb2): core.input.register(obj, event(obj)["button"], cb) core.input.register(obj, event(obj)["button"], cb2) core.input.trigger(event(obj)) - cb.assert_called_once_with(event(obj)) cb2.assert_called_once_with(event(obj)) + cb.assert_not_called() def test_event_names(): From e006344dccc3becbedf96ef53d4d8bddb15e5000 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Mon, 29 Jun 2020 07:51:35 +0200 Subject: [PATCH 036/506] [doc] add how to create icon-only widgets fixes #669 --- docs/development/theme.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/development/theme.rst b/docs/development/theme.rst index 7ea8af6..24e7cbe 100644 --- a/docs/development/theme.rst +++ b/docs/development/theme.rst @@ -52,6 +52,21 @@ JSON file located in ``$(THEME_DIRECTORY)/icons/``. The format of the icon file is identical to the theme itself (as the two are essentially just merged into a single JSON. +To create an "icon-only" widget (e.g. the play/pause/forward/rewind buttons +of a media player), you need to do the following: + +1. In the module, create a widget, and set its state to a descriptive value + (for example `widget.set("state", "next")` +2. In the theme's icon definition JSON, define a `prefix` for that state: + +.. code:: json + + { + "spotify": { + "next": { "prefix": "" } + }, + } + Color definitions and pyWAL support ----------------------------------- From 227a23fdb539ff4170e747add2d594e860c0c801 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Mon, 29 Jun 2020 20:07:24 +0200 Subject: [PATCH 037/506] [core/theme] fix mergeing of iconsets iconsets should only overwrite parameters that are *not* set in the main theme file. fixes #666 --- bumblebee_status/core/theme.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bumblebee_status/core/theme.py b/bumblebee_status/core/theme.py index f50286a..b52c465 100644 --- a/bumblebee_status/core/theme.py +++ b/bumblebee_status/core/theme.py @@ -53,7 +53,7 @@ class Theme(object): self.__data = raw_data if raw_data else self.load(name) for icons in self.__data.get("icons", []): - self.__data = util.algorithm.merge(self.load(icons, "icons"), self.__data) + self.__data = util.algorithm.merge(self.__data, self.load(icons, "icons")) if iconset != "auto": self.__data = util.algorithm.merge(self.load(iconset, "icons"), self.__data) for colors in self.__data.get("colors", []): From f75f1a9f26c6a4b96251f076cfd369e7fc276cab Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Tue, 30 Jun 2020 07:20:54 +0200 Subject: [PATCH 038/506] [modules/spotify] fix layout parameter --- bumblebee_status/modules/contrib/spotify.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/bumblebee_status/modules/contrib/spotify.py b/bumblebee_status/modules/contrib/spotify.py index f1e925c..85b8c31 100644 --- a/bumblebee_status/modules/contrib/spotify.py +++ b/bumblebee_status/modules/contrib/spotify.py @@ -12,6 +12,8 @@ Parameters: contributed by `yvesh `_ - many thanks! added controls by `LtPeriwinkle `_ - many thanks! + +fixed icons and layout parameter by `gkeep `_ - many thanks! """ import sys @@ -28,9 +30,10 @@ class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, []) - self.__layout = self.parameter( - "layout", - util.format.aslist("spotify.song,spotify.prev,spotify.pause,spotify.next"), + self.__layout = util.format.aslist( + self.parameter( + "layout", "spotify.song,spotify.prev,spotify.pause,spotify.next", + ) ) self.__song = "" @@ -77,9 +80,13 @@ class Module(core.module.Module): "cmd": self.__cmd + "PlayPause", } playback_status = str( - dbus.Interface(dbus.SessionBus().get_object( - "org.mpris.MediaPlayer2.spotify", "/org/mpris/MediaPlayer2"), "org.freedesktop.DBus.Properties") - .Get("org.mpris.MediaPlayer2.Player", "PlaybackStatus") + dbus.Interface( + dbus.SessionBus().get_object( + "org.mpris.MediaPlayer2.spotify", + "/org/mpris/MediaPlayer2", + ), + "org.freedesktop.DBus.Properties", + ).Get("org.mpris.MediaPlayer2.Player", "PlaybackStatus") ) if playback_status == "Playing": widget.set("state", "playing") From 7579a086153947fc99e2b61100da924baff25704 Mon Sep 17 00:00:00 2001 From: gkeep Date: Tue, 30 Jun 2020 13:58:10 +0300 Subject: [PATCH 039/506] Use icon actions instead of states --- themes/icons/ascii.json | 28 +++++++++++++++++++--------- themes/icons/awesome-fonts.json | 22 +++++++++++----------- themes/icons/ionicons.json | 24 ++++++++++++++---------- 3 files changed, 44 insertions(+), 30 deletions(-) diff --git a/themes/icons/ascii.json b/themes/icons/ascii.json index 0482a3b..2580d32 100644 --- a/themes/icons/ascii.json +++ b/themes/icons/ascii.json @@ -29,10 +29,10 @@ }, "cmus": { "playing": { - "prefix": ">" + "prefix": "||" }, "paused": { - "prefix": "||" + "prefix": "|>" }, "stopped": { "prefix": "[]" @@ -263,10 +263,10 @@ }, "mpd": { "playing": { - "prefix": ">" + "prefix": "||" }, "paused": { - "prefix": "||" + "prefix": "|>" }, "stopped": { "prefix": "[]" @@ -297,11 +297,21 @@ "prefix": "" }, "spotify": { - "song": { "prefix": "spotify" }, - "prev": { "prefix": "|<" }, - "playing": { "prefix": "|>" }, - "paused": { "prefix": "||" }, - "next": { "prefix": ">|" } + "song": { + "prefix": "spotify" + }, + "playing": { + "prefix": "||" + }, + "paused": { + "prefix": "|>" + }, + "prev": { + "prefix": "|<" + }, + "next": { + "prefix": ">|" + } }, "uptime": { "prefix": "uptime" diff --git a/themes/icons/awesome-fonts.json b/themes/icons/awesome-fonts.json index 07a11a8..d0ac2bc 100644 --- a/themes/icons/awesome-fonts.json +++ b/themes/icons/awesome-fonts.json @@ -22,7 +22,7 @@ "fan": { "prefix": "" } }, "disk": { "prefix": "" }, - "smartstatus": { "prefix": "" }, + "smartstatus": { "prefix": "" }, "dnf": { "prefix": "" }, "apt": { "prefix": "" }, "pacman": { "prefix": "" }, @@ -48,8 +48,8 @@ "DEGRADED": { "prefix": "!" } }, "cmus": { - "playing": { "prefix": "" }, - "paused": { "prefix": "" }, + "playing": { "prefix": "" }, + "paused": { "prefix": "" }, "stopped": { "prefix": "" }, "prev": { "prefix": "" }, "next": { "prefix": "" }, @@ -59,15 +59,15 @@ "repeat-off": { "prefix": "" } }, "gpmdp": { - "playing": { "prefix": "" }, - "paused": { "prefix": "" }, + "playing": { "prefix": "" }, + "paused": { "prefix": "" }, "stopped": { "prefix": "" }, "prev": { "prefix": "" }, "next": { "prefix": "" } }, "playerctl": { - "playing": { "prefix": "" }, - "paused": { "prefix": "" }, + "playing": { "prefix": "" }, + "paused": { "prefix": "" }, "stopped": { "prefix": "" }, "prev": { "prefix": "" }, "next": { "prefix": "" } @@ -199,8 +199,8 @@ "tx": { "prefix": "" } }, "mpd": { - "playing": { "prefix": "" }, - "paused": { "prefix": "" }, + "playing": { "prefix": "" }, + "paused": { "prefix": "" }, "stopped": { "prefix": "" }, "prev": { "prefix": "" }, "next": { "prefix": "" }, @@ -220,9 +220,9 @@ }, "spotify": { "song": { "prefix": "" }, + "playing": { "prefix": "" }, + "paused": { "prefix": "" }, "prev": { "prefix": "" }, - "playing": { "prefix": "" }, - "paused": { "prefix": "" }, "next": { "prefix": "" } }, "publicip": { diff --git a/themes/icons/ionicons.json b/themes/icons/ionicons.json index 3e577bf..b510cc1 100644 --- a/themes/icons/ionicons.json +++ b/themes/icons/ionicons.json @@ -30,8 +30,8 @@ "DEGRADED": { "prefix": "\u26c1\uf3bc" } }, "cmus": { - "playing": { "prefix": "\uf488" }, - "paused": { "prefix": "\uf210" }, + "playing": { "prefix": "\uf210" }, + "paused": { "prefix": "\uf488" }, "stopped": { "prefix": "\uf24f" }, "prev": { "prefix": "\uf4ab" }, "next": { "prefix": "\uf4ad" }, @@ -41,15 +41,15 @@ "repeat-off": { "prefix": "\uf30f" } }, "gpmdp": { - "playing": { "prefix": "\uf488" }, - "paused": { "prefix": "\uf210" }, + "playing": { "prefix": "\uf210" }, + "paused": { "prefix": "\uf488" }, "stopped": { "prefix": "\uf24f" }, "prev": { "prefix": "\uf4ab" }, "next": { "prefix": "\uf4ad" } }, "playerctl": { - "playing": { "prefix": "\uf488" }, - "paused": { "prefix": "\uf210" }, + "playing": { "prefix": "\uf210" }, + "paused": { "prefix": "\uf488" }, "stopped": { "prefix": "\uf24f" }, "prev": { "prefix": "\uf4ab" }, "next": { "prefix": "\uf4ad" } @@ -147,8 +147,8 @@ "tx": { "prefix": "\uf35f" } }, "mpd": { - "playing": { "prefix": "\uf488" }, - "paused": { "prefix": "\uf210" }, + "playing": { "prefix": "\uf210" }, + "paused": { "prefix": "\uf488" }, "stopped": { "prefix": "\uf24f" }, "prev": { "prefix": "\uf4ab" }, "next": { "prefix": "\uf4ad" }, @@ -164,7 +164,11 @@ "prefix": "\uf305" }, "spotify": { - "prefix": "\uf305" + "song": { "prefix": "\uf305" }, + "playing": { "prefix": "\uf210" }, + "paused": { "prefix": "\uf488" }, + "prev": { "prefix": "\uf4ab" }, + "next": { "prefix": "\uf4ad" } }, "publicip": { "prefix": "\uf268" @@ -202,6 +206,6 @@ "on": { "prefix": "\uf488" } }, "arandr": { - "prefix": "\uf465" + "prefix": "\uf465" } } From 6d9d325eca0c04c4ba2931f76a51bd34ed623686 Mon Sep 17 00:00:00 2001 From: Marvin Steadfast Date: Tue, 30 Jun 2020 14:49:43 +0200 Subject: [PATCH 040/506] [modules/nic] Using `iw` to find out whats the SSID name --- bumblebee_status/modules/core/nic.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/bumblebee_status/modules/core/nic.py b/bumblebee_status/modules/core/nic.py index 585c65f..75bcd77 100644 --- a/bumblebee_status/modules/core/nic.py +++ b/bumblebee_status/modules/core/nic.py @@ -12,6 +12,7 @@ Parameters: * nic.format: Format string (defaults to '{intf} {state} {ip} {ssid}') """ +import re import shutil import netifaces import subprocess @@ -44,7 +45,7 @@ class Module(core.module.Module): else: self._states["include"].append(state) self._format = self.parameter("format", "{intf} {state} {ip} {ssid}") - self.iwgetid = shutil.which("iwgetid") + self.iw = shutil.which("iw") self._update_widgets(widgets) def update(self): @@ -125,10 +126,13 @@ class Module(core.module.Module): widget.set("state", state) def get_ssid(self, intf): - if self._iswlan(intf) and not self._istunnel(intf) and self.iwgetid: - return util.cli.execute( - "{} -r {}".format(self.iwgetid, intf), ignore_errors=True - ) + if self._iswlan(intf) and not self._istunnel(intf) and self.iw: + ssid = util.cli.execute("{} dev {} link".format(self.iw, intf)) + found_ssid = re.findall("SSID:\s(.+)", ssid) + if len(found_ssid) > 0: + return found_ssid[0] + else: + return "" return "" From edb8eaf410b52ab2593a0c439bb316d6c85343f9 Mon Sep 17 00:00:00 2001 From: Andrew Reisner Date: Sun, 5 Jul 2020 12:35:44 -0600 Subject: [PATCH 041/506] Small fix to xrandr module. --- bumblebee_status/modules/core/xrandr.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bumblebee_status/modules/core/xrandr.py b/bumblebee_status/modules/core/xrandr.py index 04278ba..e1717e8 100644 --- a/bumblebee_status/modules/core/xrandr.py +++ b/bumblebee_status/modules/core/xrandr.py @@ -85,14 +85,14 @@ class Module(core.module.Module): self._needs_update = True def _toggle(self, event): - self._refresh(self, event) + self._refresh(event) if util.format.asbool(self.parameter("overwrite_i3config", False)) == True: toggle_cmd = utility("toggle-display.sh") else: toggle_cmd = "xrandr" - widget = self.widget_by_id(event["instance"]) + widget = self.widget(widget_id=event["instance"]) if widget.get("state") == "on": util.cli.execute("{} --output {} --off".format(toggle_cmd, widget.name)) From aac2316d746be42a1b34d67b002bee4d2796714a Mon Sep 17 00:00:00 2001 From: ColdFire Date: Mon, 6 Jul 2020 14:20:41 +0530 Subject: [PATCH 042/506] Updating config parser --- bumblebee_status/core/config.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bumblebee_status/core/config.py b/bumblebee_status/core/config.py index 67aba5f..bde212e 100644 --- a/bumblebee_status/core/config.py +++ b/bumblebee_status/core/config.py @@ -233,7 +233,10 @@ class Config(util.store.Store): """ def modules(self): - return [item for sub in self.__args.modules for item in sub] + list_of_modules = self.get('modules', None) + if (list_of_modules is None) or (type(list_of_modules) != list): + list_of_modules = [item for sub in self.__args.modules for item in sub] + return list_of_modules """Returns the global update interval @@ -278,7 +281,7 @@ class Config(util.store.Store): """ def theme(self): - return self.__args.theme + return self.get('theme', self.__args.theme) """Returns the configured iconset name From 057f894d527c06e1b0ab521912f64b75f466a582 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Thu, 9 Jul 2020 06:56:43 +0200 Subject: [PATCH 043/506] [core/config] change preferred theme source now, it works like this: - if present, use what's on the CLI - if not, use what's present in the config - fallback is "default" see #679 --- bumblebee_status/core/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bumblebee_status/core/config.py b/bumblebee_status/core/config.py index bde212e..353d42d 100644 --- a/bumblebee_status/core/config.py +++ b/bumblebee_status/core/config.py @@ -153,7 +153,7 @@ class Config(util.store.Store): default=[], help=PARAMETER_HELP, ) - parser.add_argument("-t", "--theme", default="default", help=THEME_HELP) + parser.add_argument("-t", "--theme", default=None, help=THEME_HELP) parser.add_argument( "-i", "--iconset", @@ -281,7 +281,7 @@ class Config(util.store.Store): """ def theme(self): - return self.get('theme', self.__args.theme) + return self.__args.theme or self.get("theme") or "default" """Returns the configured iconset name From a9f50f1b513b2aed54ea87f13a8e5673a4449b87 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Thu, 9 Jul 2020 07:04:22 +0200 Subject: [PATCH 044/506] [core/config] add "core" config section move theme and modules into a "core" config section --- bumblebee_status/core/config.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bumblebee_status/core/config.py b/bumblebee_status/core/config.py index 353d42d..0a47f93 100644 --- a/bumblebee_status/core/config.py +++ b/bumblebee_status/core/config.py @@ -225,6 +225,10 @@ class Config(util.store.Store): if tmp.has_section("module-parameters"): for key, value in tmp.items("module-parameters"): self.set(key, value) + if tmp.has_section("core"): + for key, value in tmp.items("core"): + self.set(key, value) + """Returns a list of configured modules From 72045b2318280410e5191692866268bf4c823c33 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Thu, 9 Jul 2020 07:04:45 +0200 Subject: [PATCH 045/506] [core/config] make configurable module list work configparser doesn't seem to have direct array support, so use format.aslist() to get a list of modules fixes #678 --- bumblebee_status/core/config.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bumblebee_status/core/config.py b/bumblebee_status/core/config.py index 0a47f93..94a92e5 100644 --- a/bumblebee_status/core/config.py +++ b/bumblebee_status/core/config.py @@ -237,9 +237,10 @@ class Config(util.store.Store): """ def modules(self): - list_of_modules = self.get('modules', None) - if (list_of_modules is None) or (type(list_of_modules) != list): - list_of_modules = [item for sub in self.__args.modules for item in sub] + list_of_modules = [item for sub in self.__args.modules for item in sub] + + if list_of_modules == []: + list_of_modules = util.format.aslist(self.get('modules', [])) return list_of_modules """Returns the global update interval From 0067ce83f07f4463b0c391c5bf658f815910b733 Mon Sep 17 00:00:00 2001 From: Bernhard B Date: Fri, 10 Jul 2020 17:11:50 +0200 Subject: [PATCH 046/506] added new module messagereceiver * binds to unix sockets and listens for incoming messages. The message will then be displayed in the status bar. --- .../modules/contrib/messagereceiver.py | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 bumblebee_status/modules/contrib/messagereceiver.py diff --git a/bumblebee_status/modules/contrib/messagereceiver.py b/bumblebee_status/modules/contrib/messagereceiver.py new file mode 100644 index 0000000..1f643d6 --- /dev/null +++ b/bumblebee_status/modules/contrib/messagereceiver.py @@ -0,0 +1,85 @@ +# pylint: disable=C0111,R0903 + +""" +Displays the message that's received via unix socket. + +Parameteres: + * messagereceiver : Unix socket address (e.g: /tmp/bumblebee_messagereceiver.sock) +""" + +import core.module +import core.widget +import core.input + +import socket +import threading +import logging +import os +import queue +import json + + +class Worker(threading.Thread): + def __init__(self, unix_socket_address, queue): + threading.Thread.__init__(self) + self.__unix_socket_address = unix_socket_address + self.__queue = queue + + def run(self): + while True: + try: + os.unlink(self.__unix_socket_address) + except OSError as e: + if os.path.exists(self.__unix_socket_address): + logging.exception( + "Couldn't bind to unix socket %s" % self.__unix_socket_address + ) + raise + + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as s: + s.bind(self.__unix_socket_address) + s.listen() + + conn, _ = s.accept() + with conn: + while True: + data = conn.recv(1024) + if not data: + break + self.__queue.put(data.decode("utf-8")) + + +class Module(core.module.Module): + @core.decorators.every(seconds=1) + def __init__(self, config, theme): + super().__init__(config, theme, core.widget.Widget(self.message)) + + self.__unix_socket_address = self.parameter("address", "") + + self.__message = "" + self.__state = [] + + self.__queue = queue.Queue() + self.__worker = Worker(self.__unix_socket_address, self.__queue) + self.__worker.daemon = True + self.__worker.start() + + def message(self, widget): + return self.__message + + def update(self): + try: + received_data = self.__queue.get(block=False) + parsed_data = json.loads(received_data) + self.__message = parsed_data["message"] + self.__state = parsed_data["state"] + except json.JSONDecodeError as e: + logging.exception("Couldn't parse message") + except queue.Empty as e: + pass + + def state(self, widget): + return self.__state + + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From adbba9bf9a06461c9182e77f383a41a5d036203c Mon Sep 17 00:00:00 2001 From: Bernhard B Date: Fri, 10 Jul 2020 17:17:16 +0200 Subject: [PATCH 047/506] fixed small bug in messagereceiver * wrong logging syntax --- bumblebee_status/modules/contrib/messagereceiver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bumblebee_status/modules/contrib/messagereceiver.py b/bumblebee_status/modules/contrib/messagereceiver.py index 1f643d6..a202b0b 100644 --- a/bumblebee_status/modules/contrib/messagereceiver.py +++ b/bumblebee_status/modules/contrib/messagereceiver.py @@ -32,7 +32,7 @@ class Worker(threading.Thread): except OSError as e: if os.path.exists(self.__unix_socket_address): logging.exception( - "Couldn't bind to unix socket %s" % self.__unix_socket_address + "Couldn't bind to unix socket %s", self.__unix_socket_address ) raise From 01cde70e14b9a873a2a02f44242328843e598075 Mon Sep 17 00:00:00 2001 From: Bernhard B Date: Fri, 10 Jul 2020 17:35:40 +0200 Subject: [PATCH 048/506] improved documentation of messagereceiver module --- bumblebee_status/modules/contrib/messagereceiver.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/bumblebee_status/modules/contrib/messagereceiver.py b/bumblebee_status/modules/contrib/messagereceiver.py index a202b0b..1fb8ca2 100644 --- a/bumblebee_status/modules/contrib/messagereceiver.py +++ b/bumblebee_status/modules/contrib/messagereceiver.py @@ -5,6 +5,17 @@ Displays the message that's received via unix socket. Parameteres: * messagereceiver : Unix socket address (e.g: /tmp/bumblebee_messagereceiver.sock) + +Example: + The following examples assume that /tmp/bumblebee_messagereceiver.sock is used as unix socket address. + + In order to send the string "I  bumblebee-status" to your status bar, use the following command: + echo -e '{"message":"I  bumblebee-status", "state": ""}' | socat unix-connect:/tmp/bumblebee_messagereceiver.sock STDIO + + In order to highlight the text, the state variable can be used: + echo -e '{"message":"I  bumblebee-status", "state": "warning"}' | socat unix-connect:/tmp/bumblebee_messagereceiver.sock STDIO + + """ import core.module From 2b888325a637bbc49bc941a3e8561ac351b25b4a Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Fri, 10 Jul 2020 20:19:43 +0200 Subject: [PATCH 049/506] [doc] add messagereceiver (+ attribution) --- .../modules/contrib/messagereceiver.py | 2 +- docs/modules.rst | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/bumblebee_status/modules/contrib/messagereceiver.py b/bumblebee_status/modules/contrib/messagereceiver.py index 1fb8ca2..9f00c8c 100644 --- a/bumblebee_status/modules/contrib/messagereceiver.py +++ b/bumblebee_status/modules/contrib/messagereceiver.py @@ -15,7 +15,7 @@ Example: In order to highlight the text, the state variable can be used: echo -e '{"message":"I  bumblebee-status", "state": "warning"}' | socat unix-connect:/tmp/bumblebee_messagereceiver.sock STDIO - +contributed by `bbernhard `_ - many thanks! """ import core.module diff --git a/docs/modules.rst b/docs/modules.rst index 984abb9..1337cd2 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -760,6 +760,25 @@ Required the following python packages: contributed by `maxpivo `_ - many thanks! +messagereceiver +~~~~~~~~~~~~~~~ + +Displays the message that's received via unix socket. + +Parameteres: + * messagereceiver : Unix socket address (e.g: /tmp/bumblebee_messagereceiver.sock) + +Example: + The following examples assume that /tmp/bumblebee_messagereceiver.sock is used as unix socket address. + + In order to send the string "I  bumblebee-status" to your status bar, use the following command: + echo -e '{"message":"I  bumblebee-status", "state": ""}' | socat unix-connect:/tmp/bumblebee_messagereceiver.sock STDIO + + In order to highlight the text, the state variable can be used: + echo -e '{"message":"I  bumblebee-status", "state": "warning"}' | socat unix-connect:/tmp/bumblebee_messagereceiver.sock STDIO + +contributed by `bbernhard `_ - many thanks! + mocp ~~~~ @@ -1148,6 +1167,8 @@ contributed by `yvesh `_ - many thanks! added controls by `LtPeriwinkle `_ - many thanks! +fixed icons and layout parameter by `gkeep `_ - many thanks! + .. image:: ../screenshots/spotify.png stock From a4a622252b08565265e8f0715d6a46ddbcc85bc0 Mon Sep 17 00:00:00 2001 From: Bernhard B Date: Sat, 11 Jul 2020 18:07:57 +0200 Subject: [PATCH 050/506] reworked messagereceiver module * use bumblebee's internal threading capabilities * various small code improvements (pylint) --- .../modules/contrib/messagereceiver.py | 69 ++++++++----------- 1 file changed, 29 insertions(+), 40 deletions(-) diff --git a/bumblebee_status/modules/contrib/messagereceiver.py b/bumblebee_status/modules/contrib/messagereceiver.py index 1fb8ca2..869ec82 100644 --- a/bumblebee_status/modules/contrib/messagereceiver.py +++ b/bumblebee_status/modules/contrib/messagereceiver.py @@ -18,29 +18,36 @@ Example: """ +import socket +import logging +import os +import json + import core.module import core.widget import core.input -import socket -import threading -import logging -import os -import queue -import json +class Module(core.module.Module): + @core.decorators.never + def __init__(self, config, theme): + super().__init__(config, theme, core.widget.Widget(self.message)) -class Worker(threading.Thread): - def __init__(self, unix_socket_address, queue): - threading.Thread.__init__(self) - self.__unix_socket_address = unix_socket_address - self.__queue = queue + self.background = True - def run(self): + self.__unix_socket_address = self.parameter("address", "") + + self.__message = "" + self.__state = [] + + def message(self, widget): + return self.__message + + def __read_data_from_socket(self): while True: try: os.unlink(self.__unix_socket_address) - except OSError as e: + except OSError: if os.path.exists(self.__unix_socket_address): logging.exception( "Couldn't bind to unix socket %s", self.__unix_socket_address @@ -57,37 +64,19 @@ class Worker(threading.Thread): data = conn.recv(1024) if not data: break - self.__queue.put(data.decode("utf-8")) - - -class Module(core.module.Module): - @core.decorators.every(seconds=1) - def __init__(self, config, theme): - super().__init__(config, theme, core.widget.Widget(self.message)) - - self.__unix_socket_address = self.parameter("address", "") - - self.__message = "" - self.__state = [] - - self.__queue = queue.Queue() - self.__worker = Worker(self.__unix_socket_address, self.__queue) - self.__worker.daemon = True - self.__worker.start() - - def message(self, widget): - return self.__message + yield data.decode("utf-8") def update(self): try: - received_data = self.__queue.get(block=False) - parsed_data = json.loads(received_data) - self.__message = parsed_data["message"] - self.__state = parsed_data["state"] - except json.JSONDecodeError as e: + for received_data in self.__read_data_from_socket(): + parsed_data = json.loads(received_data) + self.__message = parsed_data["message"] + self.__state = parsed_data["state"] + core.event.trigger("update", [self.id], redraw_only=True) + except json.JSONDecodeError: logging.exception("Couldn't parse message") - except queue.Empty as e: - pass + except Exception: + logging.exception("Unexpected exception while reading from socket") def state(self, widget): return self.__state From 0785202860630dfc121bcd0fc24b86dd77ac841c Mon Sep 17 00:00:00 2001 From: Andrew Reisner Date: Sun, 12 Jul 2020 10:52:05 -0600 Subject: [PATCH 051/506] Add simple portage status module. This adds a simple module to display the status of Gentoo portage operations by reading its logfile. --- .../modules/contrib/portage_status.py | 64 +++++++++++++++++++ themes/icons/awesome-fonts.json | 4 ++ 2 files changed, 68 insertions(+) create mode 100644 bumblebee_status/modules/contrib/portage_status.py diff --git a/bumblebee_status/modules/contrib/portage_status.py b/bumblebee_status/modules/contrib/portage_status.py new file mode 100644 index 0000000..507eda3 --- /dev/null +++ b/bumblebee_status/modules/contrib/portage_status.py @@ -0,0 +1,64 @@ +"""Displays the status of Gentoo portage operations. + +Parameters: + * portage_status.logfile: logfile for portage (default is /var/log/emerge.log) +""" + +import os + +import core.module +import core.widget + + +class Module(core.module.Module): + def __init__(self, config, theme): + super().__init__(config, theme, core.widget.Widget(self.output)) + self.__logfile = self.parameter( + "logfile", "/var/log/emerge.log") + self.clear() + + + def clear(self): + self.__action = "" + self.__package = "" + self.__status = "" + + + def output(self, widget): + return " ".join([atom for atom in + (self.__action, self.__package, self.__status) if atom != ""]) + + + def state(self, widgets): + if self.__action == "": + return "idle" + return "active" + + + def update(self): + try: + with open(self.__logfile, "rb") as f: + f.seek(-2, os.SEEK_END) + while f.read(1) != b"\n": + f.seek(-2, os.SEEK_CUR) + last_line = f.readline().decode() + if "===" in last_line: + if "Unmerging..." in last_line: + self.__action = "Unmerging" + package_beg = last_line.find("(") + 1 + package_end = last_line.find("-", last_line.find("/")) - 1 + self.__package = last_line[package_beg:package_end+1] + else: # merging + status_beg = last_line.find("(") + status_end = last_line.find(")") + self.__status = last_line[status_beg:status_end+1] + package_beg = last_line.find('(', status_end) + 1 + package_end = package_beg + last_line[package_beg:].find("-", last_line[package_beg:].find("/")) - 1 + self.__package = last_line[package_beg:package_end+1] + action_beg = status_end + 2 + action_end = package_beg - 3 + self.__action = last_line[action_beg:action_end+1] + else: + self.clear() + except Exception: + self.clear() diff --git a/themes/icons/awesome-fonts.json b/themes/icons/awesome-fonts.json index d0ac2bc..ca8d38f 100644 --- a/themes/icons/awesome-fonts.json +++ b/themes/icons/awesome-fonts.json @@ -35,6 +35,10 @@ "empty": { "prefix": "\uf0e0" }, "items": { "prefix": "\uf0e0" } }, + "portage_status": { + "idle": { "prefix": "" }, + "active": { "prefix": "" } + }, "todo": { "empty": { "prefix": "" }, "items": { "prefix": "" }, From b1bb0fe69064bb9ed741975120bc16d46edba11a Mon Sep 17 00:00:00 2001 From: Andrew Reisner Date: Sun, 12 Jul 2020 11:01:00 -0600 Subject: [PATCH 052/506] Run formatter. --- .../modules/contrib/portage_status.py | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/bumblebee_status/modules/contrib/portage_status.py b/bumblebee_status/modules/contrib/portage_status.py index 507eda3..7b8c897 100644 --- a/bumblebee_status/modules/contrib/portage_status.py +++ b/bumblebee_status/modules/contrib/portage_status.py @@ -13,28 +13,28 @@ import core.widget class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.output)) - self.__logfile = self.parameter( - "logfile", "/var/log/emerge.log") + self.__logfile = self.parameter("logfile", "/var/log/emerge.log") self.clear() - def clear(self): self.__action = "" self.__package = "" self.__status = "" - def output(self, widget): - return " ".join([atom for atom in - (self.__action, self.__package, self.__status) if atom != ""]) - + return " ".join( + [ + atom + for atom in (self.__action, self.__package, self.__status) + if atom != "" + ] + ) def state(self, widgets): if self.__action == "": return "idle" return "active" - def update(self): try: with open(self.__logfile, "rb") as f: @@ -47,17 +47,23 @@ class Module(core.module.Module): self.__action = "Unmerging" package_beg = last_line.find("(") + 1 package_end = last_line.find("-", last_line.find("/")) - 1 - self.__package = last_line[package_beg:package_end+1] - else: # merging + self.__package = last_line[package_beg : package_end + 1] + else: # merging status_beg = last_line.find("(") status_end = last_line.find(")") - self.__status = last_line[status_beg:status_end+1] - package_beg = last_line.find('(', status_end) + 1 - package_end = package_beg + last_line[package_beg:].find("-", last_line[package_beg:].find("/")) - 1 - self.__package = last_line[package_beg:package_end+1] + self.__status = last_line[status_beg : status_end + 1] + package_beg = last_line.find("(", status_end) + 1 + package_end = ( + package_beg + + last_line[package_beg:].find( + "-", last_line[package_beg:].find("/") + ) + - 1 + ) + self.__package = last_line[package_beg : package_end + 1] action_beg = status_end + 2 action_end = package_beg - 3 - self.__action = last_line[action_beg:action_end+1] + self.__action = last_line[action_beg : action_end + 1] else: self.clear() except Exception: From 83a4be3bc0ce269a8560200aa76cc963763831ed Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sun, 12 Jul 2020 19:31:28 +0200 Subject: [PATCH 053/506] [doc] add module portage_status (plus attribution) --- bumblebee_status/modules/contrib/portage_status.py | 2 ++ docs/modules.rst | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/bumblebee_status/modules/contrib/portage_status.py b/bumblebee_status/modules/contrib/portage_status.py index 7b8c897..ab2a32e 100644 --- a/bumblebee_status/modules/contrib/portage_status.py +++ b/bumblebee_status/modules/contrib/portage_status.py @@ -2,6 +2,8 @@ Parameters: * portage_status.logfile: logfile for portage (default is /var/log/emerge.log) + +contributed by `andrewreisner `_ - many thanks! """ import os diff --git a/docs/modules.rst b/docs/modules.rst index 1337cd2..a258899 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -967,6 +967,16 @@ Parameters: contributed by `martindoublem `_, inspired by `karthink `_ - many thanks! +portage_status +~~~~~~~~~~~~~~ + +Displays the status of Gentoo portage operations. + +Parameters: + * portage_status.logfile: logfile for portage (default is /var/log/emerge.log) + +contributed by `andrewreisner `_ - many thanks! + prime ~~~~~ From df29627983fc6c8ce90c42afd59d3dbf4ef3c867 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sat, 18 Jul 2020 08:14:23 +0200 Subject: [PATCH 054/506] [core/doc] add autogenerated warning to modules.rst --- bumblebee_status/core/config.py | 5 +++++ docs/modules.rst | 2 ++ 2 files changed, 7 insertions(+) diff --git a/bumblebee_status/core/config.py b/bumblebee_status/core/config.py index 94a92e5..c84d10c 100644 --- a/bumblebee_status/core/config.py +++ b/bumblebee_status/core/config.py @@ -71,6 +71,11 @@ class print_usage(argparse.Action): ) rst = {} + + if self._format == "rst": + print(".. THIS DOCUMENT IS AUTO-GENERATED, DO NOT MODIFY") + print(".. To change this document, please update the docstrings in the individual modules") + for m in all_modules(): try: module_type = "core" diff --git a/docs/modules.rst b/docs/modules.rst index a258899..048eeeb 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -1,3 +1,5 @@ +.. THIS DOCUMENT IS AUTO-GENERATED, DO NOT MODIFY +.. To change this document, please update the docstrings in the individual modules List of modules =============== From 72966ee37dae3ca5f2884de054e76f79e79d6106 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sat, 18 Jul 2020 08:16:57 +0200 Subject: [PATCH 055/506] [modules/{cpu,load,memory}] Add gnome-system-monitor dependency to doc --- bumblebee_status/modules/contrib/stock.py | 2 +- bumblebee_status/modules/core/cpu.py | 3 +++ bumblebee_status/modules/core/load.py | 5 +++++ bumblebee_status/modules/core/memory.py | 5 +++++ 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/bumblebee_status/modules/contrib/stock.py b/bumblebee_status/modules/contrib/stock.py index 7c13488..c8bfb5e 100644 --- a/bumblebee_status/modules/contrib/stock.py +++ b/bumblebee_status/modules/contrib/stock.py @@ -1,7 +1,7 @@ # -*- coding: UTF-8 -*- # pylint: disable=C0111,R0903 -"""Display a stock quote from worldtradingdata.com +"""Display a stock quote from finance.yahoo.com Requires the following python packages: * requests diff --git a/bumblebee_status/modules/core/cpu.py b/bumblebee_status/modules/core/cpu.py index 1e8c9db..59c6e71 100644 --- a/bumblebee_status/modules/core/cpu.py +++ b/bumblebee_status/modules/core/cpu.py @@ -2,8 +2,11 @@ """Displays CPU utilization across all CPUs. +By default, opens `gnome-system-monitor` on left mouse click. + Requirements: * the psutil Python module for the first three items from the list above + * gnome-system-monitor for default mouse click action Parameters: * cpu.warning : Warning threshold in % of CPU usage (defaults to 70%) diff --git a/bumblebee_status/modules/core/load.py b/bumblebee_status/modules/core/load.py index 7ca839b..7800c88 100644 --- a/bumblebee_status/modules/core/load.py +++ b/bumblebee_status/modules/core/load.py @@ -2,6 +2,11 @@ """Displays system load. +By default, opens `gnome-system-monitor` on left mouse click. + +Requirements: + * gnome-system-monitor for default mouse click action + Parameters: * load.warning : Warning threshold for the one-minute load average (defaults to 70% of the number of CPUs) * load.critical: Critical threshold for the one-minute load average (defaults to 80% of the number of CPUs) diff --git a/bumblebee_status/modules/core/memory.py b/bumblebee_status/modules/core/memory.py index 7ce7545..82de769 100644 --- a/bumblebee_status/modules/core/memory.py +++ b/bumblebee_status/modules/core/memory.py @@ -2,6 +2,11 @@ """Displays available RAM, total amount of RAM and percentage available. +By default, opens `gnome-system-monitor` on left mouse click. + +Requirements: + * gnome-system-monitor for default mouse click action + Parameters: * memory.warning : Warning threshold in % of memory used (defaults to 80%) * memory.critical: Critical threshold in % of memory used (defaults to 90%) From 7c8ddc9c87d837c3de2238b7021bc9f5bbca0c66 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sat, 18 Jul 2020 08:23:28 +0200 Subject: [PATCH 056/506] [doc] update docstrings as per PR #683 --- bumblebee_status/modules/contrib/amixer.py | 3 +++ bumblebee_status/modules/contrib/bluetooth.py | 2 +- bumblebee_status/modules/contrib/bluetooth2.py | 2 +- bumblebee_status/modules/contrib/brightness.py | 5 +++++ bumblebee_status/modules/contrib/datetimetz.py | 4 ++++ bumblebee_status/modules/contrib/github.py | 2 ++ bumblebee_status/modules/contrib/indicator.py | 3 +++ bumblebee_status/modules/contrib/octoprint.py | 5 ++++- bumblebee_status/modules/contrib/prime.py | 3 ++- bumblebee_status/modules/contrib/smartstatus.py | 4 ++++ bumblebee_status/modules/contrib/sun.py | 1 + bumblebee_status/modules/contrib/twmn.py | 3 +++ bumblebee_status/modules/contrib/zpool.py | 3 +++ bumblebee_status/modules/core/nic.py | 3 +++ bumblebee_status/modules/core/vault.py | 3 +++ 15 files changed, 42 insertions(+), 4 deletions(-) diff --git a/bumblebee_status/modules/contrib/amixer.py b/bumblebee_status/modules/contrib/amixer.py index 4ab30f9..b0fda92 100644 --- a/bumblebee_status/modules/contrib/amixer.py +++ b/bumblebee_status/modules/contrib/amixer.py @@ -1,5 +1,8 @@ """get volume level or control it +Requires the following executable: + * amixer + Parameters: * amixer.device: Device to use (default is Master,0) * amixer.percent_change: How much to change volume by when scrolling on the module (default is 4%) diff --git a/bumblebee_status/modules/contrib/bluetooth.py b/bumblebee_status/modules/contrib/bluetooth.py index 64fcc0b..b565494 100644 --- a/bumblebee_status/modules/contrib/bluetooth.py +++ b/bumblebee_status/modules/contrib/bluetooth.py @@ -1,4 +1,4 @@ -"""Displays bluetooth status (Bluez). Left mouse click launches manager app, +"""Displays bluetooth status (Bluez). Left mouse click launches manager app `blueman-manager`, right click toggles bluetooth. Needs dbus-send to toggle bluetooth state. Parameters: diff --git a/bumblebee_status/modules/contrib/bluetooth2.py b/bumblebee_status/modules/contrib/bluetooth2.py index 2bcdc32..b8fac09 100644 --- a/bumblebee_status/modules/contrib/bluetooth2.py +++ b/bumblebee_status/modules/contrib/bluetooth2.py @@ -1,4 +1,4 @@ -"""Displays bluetooth status. Left mouse click launches manager app, +"""Displays bluetooth status. Left mouse click launches manager app `blueman-manager`, right click toggles bluetooth. Needs dbus-send to toggle bluetooth state and python-dbus to count the number of connections diff --git a/bumblebee_status/modules/contrib/brightness.py b/bumblebee_status/modules/contrib/brightness.py index 28b94a8..dc1691a 100644 --- a/bumblebee_status/modules/contrib/brightness.py +++ b/bumblebee_status/modules/contrib/brightness.py @@ -2,6 +2,11 @@ """Displays the brightness of a display +The following executables can be used if `use_acpi` is not enabled: + * brightnessctl + * light + * xbacklight + Parameters: * brightness.step: The amount of increase/decrease on scroll in % (defaults to 2) * brightness.device_path: The device path (defaults to /sys/class/backlight/intel_backlight), can contain wildcards (in this case, the first matching path will be used); This is only used when brightness.use_acpi is set to true diff --git a/bumblebee_status/modules/contrib/datetimetz.py b/bumblebee_status/modules/contrib/datetimetz.py index e4baa4c..df4507f 100644 --- a/bumblebee_status/modules/contrib/datetimetz.py +++ b/bumblebee_status/modules/contrib/datetimetz.py @@ -2,6 +2,10 @@ """Displays the current date and time with timezone options. +Requires the following python packages: + * tzlocal + * pytz + Parameters: * datetimetz.format : strftime()-compatible formatting string * datetimetz.timezone : IANA timezone name diff --git a/bumblebee_status/modules/contrib/github.py b/bumblebee_status/modules/contrib/github.py index fc16df0..abcdfa5 100644 --- a/bumblebee_status/modules/contrib/github.py +++ b/bumblebee_status/modules/contrib/github.py @@ -5,6 +5,8 @@ Displays the unread GitHub notifications count for a GitHub user using the follo * https://developer.github.com/v3/activity/notifications/#notification-reasons +Uses `xdg-open` or `x-www-browser` to open web-pages. + Requires the following library: * requests diff --git a/bumblebee_status/modules/contrib/indicator.py b/bumblebee_status/modules/contrib/indicator.py index 6b01c41..63445f2 100644 --- a/bumblebee_status/modules/contrib/indicator.py +++ b/bumblebee_status/modules/contrib/indicator.py @@ -2,6 +2,9 @@ """Displays the indicator status, for numlock, scrolllock and capslock +Requires the following executable: + * xset + Parameters: * indicator.include: Comma-separated list of interface prefixes to include (defaults to 'numlock,capslock') * indicator.signalstype: If you want the signali type color to be 'critical' or 'warning' (defaults to 'warning') diff --git a/bumblebee_status/modules/contrib/octoprint.py b/bumblebee_status/modules/contrib/octoprint.py index ce1aa89..a324af2 100644 --- a/bumblebee_status/modules/contrib/octoprint.py +++ b/bumblebee_status/modules/contrib/octoprint.py @@ -1,9 +1,12 @@ # pylint: disable=C0111,R0903 -"""Displays the Octorpint status and the printer's bed/tools temperature in the status bar. +"""Displays the Octorrint status and the printer's bed/tools temperature in the status bar. Left click opens a popup which shows the bed & tools temperatures and additionally a livestream of the webcam (if enabled). +Prerequisites: + * tk python library (usually python-tk or python3-tk, depending on your distribution) + Parameters: * octoprint.address : Octoprint address (e.q: http://192.168.1.3) * octoprint.apitoken : Octorpint API Token (can be obtained from the Octoprint Webinterface) diff --git a/bumblebee_status/modules/contrib/prime.py b/bumblebee_status/modules/contrib/prime.py index 8a096cb..3fbd049 100644 --- a/bumblebee_status/modules/contrib/prime.py +++ b/bumblebee_status/modules/contrib/prime.py @@ -20,7 +20,8 @@ Parameters: * prime.nvidiastring: String to use when nvidia is selected (defaults to 'intel') * prime.intelstring: String to use when intel is selected (defaults to 'intel') -Requires the following executable: +Requires the following executables: + * sudo * prime-select contributed by `jeffeb3 `_ - many thanks! diff --git a/bumblebee_status/modules/contrib/smartstatus.py b/bumblebee_status/modules/contrib/smartstatus.py index 2b886e3..b5f3037 100644 --- a/bumblebee_status/modules/contrib/smartstatus.py +++ b/bumblebee_status/modules/contrib/smartstatus.py @@ -5,6 +5,10 @@ """Displays HDD smart status of different drives or all drives +Requires the following executables: + * sudo + * smartctl + Parameters: * smartstatus.display: how to display (defaults to 'combined', other choices: 'seperate' or 'singles') * smartstatus.drives: in the case of singles which drives to display, separated comma list value, multiple accepted (defaults to 'sda', example:'sda,sdc') diff --git a/bumblebee_status/modules/contrib/sun.py b/bumblebee_status/modules/contrib/sun.py index dddc10d..6b0734d 100644 --- a/bumblebee_status/modules/contrib/sun.py +++ b/bumblebee_status/modules/contrib/sun.py @@ -5,6 +5,7 @@ Requires the following python packages: * requests * suntime + * python-dateutil Parameters: * cpu.lat : Latitude of your location diff --git a/bumblebee_status/modules/contrib/twmn.py b/bumblebee_status/modules/contrib/twmn.py index 58b2ba1..0fc46b3 100644 --- a/bumblebee_status/modules/contrib/twmn.py +++ b/bumblebee_status/modules/contrib/twmn.py @@ -2,6 +2,9 @@ """Toggle twmn notifications. +Requires the following executable: + * systemctl + contributed by `Pseudonick47 `_ - many thanks! """ diff --git a/bumblebee_status/modules/contrib/zpool.py b/bumblebee_status/modules/contrib/zpool.py index 78cc26a..9863793 100644 --- a/bumblebee_status/modules/contrib/zpool.py +++ b/bumblebee_status/modules/contrib/zpool.py @@ -1,5 +1,8 @@ """Displays info about zpools present on the system +Requires the following executable: + * sudo (if `zpool.sudo` is explicitly set to `true`) + Parameters: * zpool.list: Comma-separated list of zpools to display info for. If empty, info for all zpools is displayed. (Default: '') diff --git a/bumblebee_status/modules/core/nic.py b/bumblebee_status/modules/core/nic.py index 75bcd77..d64a14f 100644 --- a/bumblebee_status/modules/core/nic.py +++ b/bumblebee_status/modules/core/nic.py @@ -5,6 +5,9 @@ Requires the following python module: * netifaces +Requires the following executable: + * iw + Parameters: * nic.exclude: Comma-separated list of interface prefixes to exclude (defaults to 'lo,virbr,docker,vboxnet,veth,br') * nic.include: Comma-separated list of interfaces to include diff --git a/bumblebee_status/modules/core/vault.py b/bumblebee_status/modules/core/vault.py index 0ec4257..28f37f4 100644 --- a/bumblebee_status/modules/core/vault.py +++ b/bumblebee_status/modules/core/vault.py @@ -4,6 +4,9 @@ Many thanks to [@bbernhard](https://github.com/bbernhard) for the idea! +Requires the following executable: + * pass (aka password-store) + Parameters: * vault.duration: Duration until password is cleared from clipboard (defaults to 30) * vault.location: Location of the password store (defaults to ~/.password-store) From 6025fcd2dafe63cf694ddc5da42f853d2e868b04 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sat, 18 Jul 2020 08:23:44 +0200 Subject: [PATCH 057/506] [doc] update as per PR #683 --- docs/modules.rst | 61 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 5 deletions(-) diff --git a/docs/modules.rst b/docs/modules.rst index 048eeeb..09879f7 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -12,8 +12,11 @@ cpu Displays CPU utilization across all CPUs. +By default, opens `gnome-system-monitor` on left mouse click. + Requirements: * the psutil Python module for the first three items from the list above + * gnome-system-monitor for default mouse click action Parameters: * cpu.warning : Warning threshold in % of CPU usage (defaults to 70%) @@ -99,6 +102,11 @@ load Displays system load. +By default, opens `gnome-system-monitor` on left mouse click. + +Requirements: + * gnome-system-monitor for default mouse click action + Parameters: * load.warning : Warning threshold for the one-minute load average (defaults to 70% of the number of CPUs) * load.critical: Critical threshold for the one-minute load average (defaults to 80% of the number of CPUs) @@ -110,6 +118,11 @@ memory Displays available RAM, total amount of RAM and percentage available. +By default, opens `gnome-system-monitor` on left mouse click. + +Requirements: + * gnome-system-monitor for default mouse click action + Parameters: * memory.warning : Warning threshold in % of memory used (defaults to 80%) * memory.critical: Critical threshold in % of memory used (defaults to 90%) @@ -126,6 +139,9 @@ Displays the name, IP address(es) and status of each available network interface Requires the following python module: * netifaces +Requires the following executable: + * iw + Parameters: * nic.exclude: Comma-separated list of interface prefixes to exclude (defaults to 'lo,virbr,docker,vboxnet,veth,br') * nic.include: Comma-separated list of interfaces to include @@ -254,6 +270,9 @@ Copy passwords from a password store into the clipboard (currently supports only Many thanks to [@bbernhard](https://github.com/bbernhard) for the idea! +Requires the following executable: + * pass (aka password-store) + Parameters: * vault.duration: Duration until password is cleared from clipboard (defaults to 30) * vault.location: Location of the password store (defaults to ~/.password-store) @@ -293,6 +312,9 @@ amixer get volume level or control it +Requires the following executable: + * amixer + Parameters: * amixer.device: Device to use (default is Master,0) * amixer.percent_change: How much to change volume by when scrolling on the module (default is 4%) @@ -373,7 +395,7 @@ contributed by `martindoublem `_ - many thanks bluetooth ~~~~~~~~~ -Displays bluetooth status (Bluez). Left mouse click launches manager app, +Displays bluetooth status (Bluez). Left mouse click launches manager app `blueman-manager`, right click toggles bluetooth. Needs dbus-send to toggle bluetooth state. Parameters: @@ -390,7 +412,7 @@ contributed by `brunosmmm `_ - many thanks! bluetooth2 ~~~~~~~~~~ -Displays bluetooth status. Left mouse click launches manager app, +Displays bluetooth status. Left mouse click launches manager app `blueman-manager`, right click toggles bluetooth. Needs dbus-send to toggle bluetooth state and python-dbus to count the number of connections @@ -404,6 +426,11 @@ brightness Displays the brightness of a display +The following executables can be used if `use_acpi` is not enabled: + * brightnessctl + * light + * xbacklight + Parameters: * brightness.step: The amount of increase/decrease on scroll in % (defaults to 2) * brightness.device_path: The device path (defaults to /sys/class/backlight/intel_backlight), can contain wildcards (in this case, the first matching path will be used); This is only used when brightness.use_acpi is set to true @@ -518,6 +545,10 @@ datetimetz Displays the current date and time with timezone options. +Requires the following python packages: + * tzlocal + * pytz + Parameters: * datetimetz.format : strftime()-compatible formatting string * datetimetz.timezone : IANA timezone name @@ -655,6 +686,8 @@ Displays the unread GitHub notifications count for a GitHub user using the follo * https://developer.github.com/v3/activity/notifications/#notification-reasons +Uses `xdg-open` or `x-www-browser` to open web-pages. + Requires the following library: * requests @@ -713,6 +746,9 @@ indicator Displays the indicator status, for numlock, scrolllock and capslock +Requires the following executable: + * xset + Parameters: * indicator.include: Comma-separated list of interface prefixes to include (defaults to 'numlock,capslock') * indicator.signalstype: If you want the signali type color to be 'critical' or 'warning' (defaults to 'warning') @@ -899,10 +935,13 @@ contributed by `RileyRedpath `_ - many thanks! octoprint ~~~~~~~~~ -Displays the Octorpint status and the printer's bed/tools temperature in the status bar. +Displays the Octorrint status and the printer's bed/tools temperature in the status bar. Left click opens a popup which shows the bed & tools temperatures and additionally a livestream of the webcam (if enabled). +Prerequisites: + * tk python library (usually python-tk or python3-tk, depending on your distribution) + Parameters: * octoprint.address : Octoprint address (e.q: http://192.168.1.3) * octoprint.apitoken : Octorpint API Token (can be obtained from the Octoprint Webinterface) @@ -1002,7 +1041,8 @@ Parameters: * prime.nvidiastring: String to use when nvidia is selected (defaults to 'intel') * prime.intelstring: String to use when intel is selected (defaults to 'intel') -Requires the following executable: +Requires the following executables: + * sudo * prime-select contributed by `jeffeb3 `_ - many thanks! @@ -1129,6 +1169,10 @@ smartstatus Displays HDD smart status of different drives or all drives +Requires the following executables: + * sudo + * smartctl + Parameters: * smartstatus.display: how to display (defaults to 'combined', other choices: 'seperate' or 'singles') * smartstatus.drives: in the case of singles which drives to display, separated comma list value, multiple accepted (defaults to 'sda', example:'sda,sdc') @@ -1186,7 +1230,7 @@ fixed icons and layout parameter by `gkeep `_ - many t stock ~~~~~ -Display a stock quote from worldtradingdata.com +Display a stock quote from finance.yahoo.com Requires the following python packages: * requests @@ -1208,6 +1252,7 @@ Displays sunrise and sunset times Requires the following python packages: * requests * suntime + * python-dateutil Parameters: * cpu.lat : Latitude of your location @@ -1322,6 +1367,9 @@ twmn Toggle twmn notifications. +Requires the following executable: + * systemctl + contributed by `Pseudonick47 `_ - many thanks! uptime @@ -1409,6 +1457,9 @@ zpool Displays info about zpools present on the system +Requires the following executable: + * sudo (if `zpool.sudo` is explicitly set to `true`) + Parameters: * zpool.list: Comma-separated list of zpools to display info for. If empty, info for all zpools is displayed. (Default: '') From dc70527797a84c568559c5ef4759a19f975fe9bc Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Mon, 20 Jul 2020 13:16:37 +0200 Subject: [PATCH 058/506] [util] add a script to generate basic autotests Add a small utility that can generate a basic "import this unless you are missing dependencies" test for all modules. --- generate-base-tests.py | 82 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100755 generate-base-tests.py diff --git a/generate-base-tests.py b/generate-base-tests.py new file mode 100755 index 0000000..2c08165 --- /dev/null +++ b/generate-base-tests.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python + +import os +import re +import sys +import glob + +def is_psl(module): + lib_path = os.path.dirname(os.__file__) + old_sys = sys.path + sys.path = [lib_path] + is_psl = True + try: + __import__(module) + except Exception as e: + is_psl = False + sys.path = old_sys + + return is_psl + + +def is_internal(module): + if module.startswith("core.") or module == "core": return True + if module.startswith("util.") or module == "util": return True + if module.startswith("."): return True + + return is_psl(module) + +def dependencies(filename): + deps = [] + with open(filename) as f: + for line in f: + if "import" in line: + match = re.match("\s*(from (\S+) )?import (\S+)", line) + if not match: continue + dep = match.group(2) or match.group(3) + if "util.popup" in dep or ("util" in line and "popup" in line): + deps.append("tkinter") + elif not is_internal(dep): + deps.append(dep) + return deps + +def write_test(testname, modname, deps): + if not os.path.exists(testname): + with open(testname, "w") as f: + f.writelines([ + "import pytest\n\n", + "import core.module\n\n", + ]) + for dep in deps: + f.write("pytest.importorskip(\"{}\")\n\n".format(dep)) + + with open(testname) as f: + for line in f: + if "def test_load_module(" in line: + print("skipping {}, already contains test".format(modname)) + return + + print("writing base test for {}".format(modname)) + with open(testname, "a+") as f: + f.writelines([ + "def test_load_module():\n", + " core.module.load(\"{}\")\n\n".format(modname), + ]) + +def main(): + for f in glob.glob("bumblebee_status/modules/*/*.py"): + if os.path.basename(f) == "__init__.py": continue + + modname = os.path.splitext(os.path.basename(f))[0] + + modpath = os.path.dirname(f) + deps = dependencies(f) + testname = os.path.join("tests", "modules", modpath.split(os.sep)[2], "test_{}.py".format(modname)) + + write_test(testname, modname, deps) + + +if __name__ == "__main__": + main() + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From 9be1331e1bdd570be822de282447dd1349edf72e Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Mon, 20 Jul 2020 13:17:35 +0200 Subject: [PATCH 059/506] [tests] add placeholder for core tests --- tests/modules/core/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/modules/core/__init__.py diff --git a/tests/modules/core/__init__.py b/tests/modules/core/__init__.py new file mode 100644 index 0000000..e69de29 From 548ccc5e94f94b404cfddba2febb482096a3d7d1 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Mon, 20 Jul 2020 13:56:18 +0200 Subject: [PATCH 060/506] [tests] add somewhat experimental import-time tests add auto-generated tests that check that a given module can be imported, if all prerequisites are followed. see #641 --- generate-base-tests.py | 6 ++++-- tests/modules/contrib/test_amixer.py | 5 +++++ tests/modules/contrib/test_apt.py | 5 +++++ tests/modules/contrib/test_arandr.py | 7 +++++++ tests/modules/contrib/test_arch-update.py | 5 +++++ tests/modules/contrib/test_battery-upower.py | 7 +++++++ tests/modules/contrib/test_battery.py | 7 +++++++ tests/modules/contrib/test_bluetooth.py | 7 +++++++ tests/modules/contrib/test_bluetooth2.py | 11 +++++++++++ tests/modules/contrib/test_brightness.py | 5 +++++ tests/modules/contrib/test_caffeine.py | 7 +++++++ tests/modules/contrib/test_cmus.py | 5 +++++ tests/modules/contrib/test_cpu2.py | 7 +++++++ tests/modules/contrib/test_currency.py | 9 +++++++++ tests/modules/contrib/test_datetimetz.py | 11 +++++++++++ tests/modules/contrib/test_datetz.py | 11 +++++++++++ tests/modules/contrib/test_deadbeef.py | 7 +++++++ tests/modules/contrib/test_deezer.py | 7 +++++++ tests/modules/contrib/test_dnf.py | 5 +++++ tests/modules/contrib/test_docker_ps.py | 9 +++++++++ tests/modules/contrib/test_dunst.py | 5 +++++ tests/modules/contrib/test_getcrypto.py | 9 +++++++++ tests/modules/contrib/test_github.py | 7 +++++++ tests/modules/contrib/test_gpmdp.py | 5 +++++ tests/modules/contrib/test_hddtemp.py | 7 +++++++ tests/modules/contrib/test_hostname.py | 5 +++++ tests/modules/contrib/test_http_status.py | 9 +++++++++ tests/modules/contrib/test_indicator.py | 5 +++++ tests/modules/contrib/test_kernel.py | 4 ++++ tests/modules/contrib/test_layout-xkbswitch.py | 5 +++++ tests/modules/contrib/test_layout.py | 5 +++++ tests/modules/contrib/test_libvirtvms.py | 7 +++++++ tests/modules/contrib/test_messagereceiver.py | 7 +++++++ tests/modules/contrib/test_mocp.py | 5 +++++ tests/modules/contrib/test_mpd.py | 5 +++++ tests/modules/contrib/test_network_traffic.py | 9 +++++++++ tests/modules/contrib/test_notmuch_count.py | 5 +++++ tests/modules/contrib/test_nvidiagpu.py | 5 +++++ tests/modules/contrib/test_octoprint.py | 13 +++++++++++++ tests/modules/contrib/test_pacman.py | 7 +++++++ tests/modules/contrib/test_pihole.py | 7 +++++++ tests/modules/contrib/test_playerctl.py | 5 +++++ tests/modules/contrib/test_pomodoro.py | 9 +++++++++ tests/modules/contrib/test_portage_status.py | 5 +++++ tests/modules/contrib/test_prime.py | 5 +++++ tests/modules/contrib/test_progress.py | 5 +++++ tests/modules/contrib/test_publicip.py | 5 +++++ tests/modules/contrib/test_rotation.py | 5 +++++ tests/modules/contrib/test_rss.py | 13 +++++++++++++ tests/modules/contrib/test_sensors.py | 5 +++++ tests/modules/contrib/test_shell.py | 7 +++++++ tests/modules/contrib/test_shortcut.py | 5 +++++ tests/modules/contrib/test_smartstatus.py | 5 +++++ tests/modules/contrib/test_spaceapi.py | 7 +++++++ tests/modules/contrib/test_spotify.py | 7 +++++++ tests/modules/contrib/test_stock.py | 7 +++++++ tests/modules/contrib/test_sun.py | 13 +++++++++++++ tests/modules/contrib/test_system.py | 11 +++++++++++ tests/modules/contrib/test_taskwarrior.py | 7 +++++++ tests/modules/contrib/test_timetz.py | 11 +++++++++++ tests/modules/contrib/test_title.py | 7 +++++++ tests/modules/contrib/test_todo.py | 5 +++++ tests/modules/contrib/test_traffic.py | 9 +++++++++ tests/modules/contrib/test_twmn.py | 5 +++++ tests/modules/contrib/test_uptime.py | 7 +++++++ tests/modules/contrib/test_vpn.py | 7 +++++++ tests/modules/contrib/test_watson.py | 5 +++++ tests/modules/contrib/test_weather.py | 9 +++++++++ tests/modules/contrib/test_xkcd.py | 5 +++++ tests/modules/contrib/test_yubikey.py | 7 +++++++ tests/modules/contrib/test_zpool.py | 7 +++++++ tests/modules/core/test_cpu.py | 7 +++++++ tests/modules/core/test_date.py | 5 +++++ tests/modules/core/test_datetime.py | 7 +++++++ tests/modules/core/test_debug.py | 5 +++++ tests/modules/core/test_disk.py | 5 +++++ tests/modules/core/test_error.py | 5 +++++ tests/modules/core/test_git.py | 7 +++++++ tests/modules/core/test_layout-xkb.py | 7 +++++++ tests/modules/core/test_load.py | 7 +++++++ tests/modules/core/test_memory.py | 5 +++++ tests/modules/core/test_nic.py | 9 +++++++++ tests/modules/core/test_pasink.py | 5 +++++ tests/modules/core/test_pasource.py | 5 +++++ tests/modules/core/test_ping.py | 5 +++++ tests/modules/core/test_pulseaudio.py | 5 +++++ tests/modules/core/test_redshift.py | 5 +++++ tests/modules/core/test_sensors2.py | 5 +++++ tests/modules/core/test_spacer.py | 5 +++++ tests/modules/core/test_speedtest.py | 7 +++++++ tests/modules/core/test_test.py | 5 +++++ tests/modules/core/test_time.py | 5 +++++ tests/modules/core/test_vault.py | 7 +++++++ tests/modules/core/test_xrandr.py | 9 +++++++++ 94 files changed, 624 insertions(+), 2 deletions(-) create mode 100644 tests/modules/contrib/test_amixer.py create mode 100644 tests/modules/contrib/test_apt.py create mode 100644 tests/modules/contrib/test_arandr.py create mode 100644 tests/modules/contrib/test_arch-update.py create mode 100644 tests/modules/contrib/test_battery-upower.py create mode 100644 tests/modules/contrib/test_battery.py create mode 100644 tests/modules/contrib/test_bluetooth.py create mode 100644 tests/modules/contrib/test_bluetooth2.py create mode 100644 tests/modules/contrib/test_brightness.py create mode 100644 tests/modules/contrib/test_caffeine.py create mode 100644 tests/modules/contrib/test_cmus.py create mode 100644 tests/modules/contrib/test_cpu2.py create mode 100644 tests/modules/contrib/test_currency.py create mode 100644 tests/modules/contrib/test_datetimetz.py create mode 100644 tests/modules/contrib/test_datetz.py create mode 100644 tests/modules/contrib/test_deadbeef.py create mode 100644 tests/modules/contrib/test_deezer.py create mode 100644 tests/modules/contrib/test_dnf.py create mode 100644 tests/modules/contrib/test_docker_ps.py create mode 100644 tests/modules/contrib/test_dunst.py create mode 100644 tests/modules/contrib/test_getcrypto.py create mode 100644 tests/modules/contrib/test_github.py create mode 100644 tests/modules/contrib/test_gpmdp.py create mode 100644 tests/modules/contrib/test_hddtemp.py create mode 100644 tests/modules/contrib/test_hostname.py create mode 100644 tests/modules/contrib/test_http_status.py create mode 100644 tests/modules/contrib/test_indicator.py create mode 100644 tests/modules/contrib/test_layout-xkbswitch.py create mode 100644 tests/modules/contrib/test_layout.py create mode 100644 tests/modules/contrib/test_libvirtvms.py create mode 100644 tests/modules/contrib/test_messagereceiver.py create mode 100644 tests/modules/contrib/test_mocp.py create mode 100644 tests/modules/contrib/test_network_traffic.py create mode 100644 tests/modules/contrib/test_notmuch_count.py create mode 100644 tests/modules/contrib/test_nvidiagpu.py create mode 100644 tests/modules/contrib/test_octoprint.py create mode 100644 tests/modules/contrib/test_pacman.py create mode 100644 tests/modules/contrib/test_pihole.py create mode 100644 tests/modules/contrib/test_playerctl.py create mode 100644 tests/modules/contrib/test_pomodoro.py create mode 100644 tests/modules/contrib/test_portage_status.py create mode 100644 tests/modules/contrib/test_prime.py create mode 100644 tests/modules/contrib/test_progress.py create mode 100644 tests/modules/contrib/test_publicip.py create mode 100644 tests/modules/contrib/test_rotation.py create mode 100644 tests/modules/contrib/test_rss.py create mode 100644 tests/modules/contrib/test_sensors.py create mode 100644 tests/modules/contrib/test_shell.py create mode 100644 tests/modules/contrib/test_shortcut.py create mode 100644 tests/modules/contrib/test_smartstatus.py create mode 100644 tests/modules/contrib/test_spaceapi.py create mode 100644 tests/modules/contrib/test_spotify.py create mode 100644 tests/modules/contrib/test_stock.py create mode 100644 tests/modules/contrib/test_sun.py create mode 100644 tests/modules/contrib/test_system.py create mode 100644 tests/modules/contrib/test_taskwarrior.py create mode 100644 tests/modules/contrib/test_timetz.py create mode 100644 tests/modules/contrib/test_title.py create mode 100644 tests/modules/contrib/test_todo.py create mode 100644 tests/modules/contrib/test_traffic.py create mode 100644 tests/modules/contrib/test_twmn.py create mode 100644 tests/modules/contrib/test_uptime.py create mode 100644 tests/modules/contrib/test_vpn.py create mode 100644 tests/modules/contrib/test_watson.py create mode 100644 tests/modules/contrib/test_weather.py create mode 100644 tests/modules/contrib/test_xkcd.py create mode 100644 tests/modules/contrib/test_yubikey.py create mode 100644 tests/modules/contrib/test_zpool.py create mode 100644 tests/modules/core/test_cpu.py create mode 100644 tests/modules/core/test_date.py create mode 100644 tests/modules/core/test_datetime.py create mode 100644 tests/modules/core/test_debug.py create mode 100644 tests/modules/core/test_disk.py create mode 100644 tests/modules/core/test_error.py create mode 100644 tests/modules/core/test_git.py create mode 100644 tests/modules/core/test_layout-xkb.py create mode 100644 tests/modules/core/test_load.py create mode 100644 tests/modules/core/test_memory.py create mode 100644 tests/modules/core/test_nic.py create mode 100644 tests/modules/core/test_pasink.py create mode 100644 tests/modules/core/test_pasource.py create mode 100644 tests/modules/core/test_ping.py create mode 100644 tests/modules/core/test_pulseaudio.py create mode 100644 tests/modules/core/test_redshift.py create mode 100644 tests/modules/core/test_sensors2.py create mode 100644 tests/modules/core/test_spacer.py create mode 100644 tests/modules/core/test_speedtest.py create mode 100644 tests/modules/core/test_test.py create mode 100644 tests/modules/core/test_time.py create mode 100644 tests/modules/core/test_vault.py create mode 100644 tests/modules/core/test_xrandr.py diff --git a/generate-base-tests.py b/generate-base-tests.py index 2c08165..03efc37 100755 --- a/generate-base-tests.py +++ b/generate-base-tests.py @@ -36,16 +36,18 @@ def dependencies(filename): dep = match.group(2) or match.group(3) if "util.popup" in dep or ("util" in line and "popup" in line): deps.append("tkinter") + if ".datetimetz" in line: + deps.extend(dependencies("bumblebee_status/modules/contrib/datetimetz.py")) elif not is_internal(dep): deps.append(dep) return deps def write_test(testname, modname, deps): + fqmn = ".".join(["modules", testname.split(os.sep)[2], modname]) if not os.path.exists(testname): with open(testname, "w") as f: f.writelines([ "import pytest\n\n", - "import core.module\n\n", ]) for dep in deps: f.write("pytest.importorskip(\"{}\")\n\n".format(dep)) @@ -60,7 +62,7 @@ def write_test(testname, modname, deps): with open(testname, "a+") as f: f.writelines([ "def test_load_module():\n", - " core.module.load(\"{}\")\n\n".format(modname), + " __import__(\"{}\")\n\n".format(fqmn), ]) def main(): diff --git a/tests/modules/contrib/test_amixer.py b/tests/modules/contrib/test_amixer.py new file mode 100644 index 0000000..b1cfbf0 --- /dev/null +++ b/tests/modules/contrib/test_amixer.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.contrib.amixer") + diff --git a/tests/modules/contrib/test_apt.py b/tests/modules/contrib/test_apt.py new file mode 100644 index 0000000..7b151bb --- /dev/null +++ b/tests/modules/contrib/test_apt.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.contrib.apt") + diff --git a/tests/modules/contrib/test_arandr.py b/tests/modules/contrib/test_arandr.py new file mode 100644 index 0000000..130d858 --- /dev/null +++ b/tests/modules/contrib/test_arandr.py @@ -0,0 +1,7 @@ +import pytest + +pytest.importorskip("tkinter") + +def test_load_module(): + __import__("modules.contrib.arandr") + diff --git a/tests/modules/contrib/test_arch-update.py b/tests/modules/contrib/test_arch-update.py new file mode 100644 index 0000000..6a1c172 --- /dev/null +++ b/tests/modules/contrib/test_arch-update.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.contrib.arch-update") + diff --git a/tests/modules/contrib/test_battery-upower.py b/tests/modules/contrib/test_battery-upower.py new file mode 100644 index 0000000..cb62a16 --- /dev/null +++ b/tests/modules/contrib/test_battery-upower.py @@ -0,0 +1,7 @@ +import pytest + +pytest.importorskip("dbus") + +def test_load_module(): + __import__("modules.contrib.battery-upower") + diff --git a/tests/modules/contrib/test_battery.py b/tests/modules/contrib/test_battery.py new file mode 100644 index 0000000..41c865c --- /dev/null +++ b/tests/modules/contrib/test_battery.py @@ -0,0 +1,7 @@ +import pytest + +pytest.importorskip("power") + +def test_load_module(): + __import__("modules.contrib.battery") + diff --git a/tests/modules/contrib/test_bluetooth.py b/tests/modules/contrib/test_bluetooth.py new file mode 100644 index 0000000..b069bb1 --- /dev/null +++ b/tests/modules/contrib/test_bluetooth.py @@ -0,0 +1,7 @@ +import pytest + +pytest.importorskip("tkinter") + +def test_load_module(): + __import__("modules.contrib.bluetooth") + diff --git a/tests/modules/contrib/test_bluetooth2.py b/tests/modules/contrib/test_bluetooth2.py new file mode 100644 index 0000000..0ebcdf8 --- /dev/null +++ b/tests/modules/contrib/test_bluetooth2.py @@ -0,0 +1,11 @@ +import pytest + +pytest.importorskip("subprocess") + +pytest.importorskip("dbus") + +pytest.importorskip("dbus.mainloop.glib") + +def test_load_module(): + __import__("modules.contrib.bluetooth2") + diff --git a/tests/modules/contrib/test_brightness.py b/tests/modules/contrib/test_brightness.py new file mode 100644 index 0000000..023682e --- /dev/null +++ b/tests/modules/contrib/test_brightness.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.contrib.brightness") + diff --git a/tests/modules/contrib/test_caffeine.py b/tests/modules/contrib/test_caffeine.py new file mode 100644 index 0000000..cc13ab4 --- /dev/null +++ b/tests/modules/contrib/test_caffeine.py @@ -0,0 +1,7 @@ +import pytest + +pytest.importorskip("psutil") + +def test_load_module(): + __import__("modules.contrib.caffeine") + diff --git a/tests/modules/contrib/test_cmus.py b/tests/modules/contrib/test_cmus.py new file mode 100644 index 0000000..03458b0 --- /dev/null +++ b/tests/modules/contrib/test_cmus.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.contrib.cmus") + diff --git a/tests/modules/contrib/test_cpu2.py b/tests/modules/contrib/test_cpu2.py new file mode 100644 index 0000000..bbb2756 --- /dev/null +++ b/tests/modules/contrib/test_cpu2.py @@ -0,0 +1,7 @@ +import pytest + +pytest.importorskip("psutil") + +def test_load_module(): + __import__("modules.contrib.cpu2") + diff --git a/tests/modules/contrib/test_currency.py b/tests/modules/contrib/test_currency.py new file mode 100644 index 0000000..0762ea3 --- /dev/null +++ b/tests/modules/contrib/test_currency.py @@ -0,0 +1,9 @@ +import pytest + +pytest.importorskip("requests") + +pytest.importorskip("babel.numbers") + +def test_load_module(): + __import__("modules.contrib.currency") + diff --git a/tests/modules/contrib/test_datetimetz.py b/tests/modules/contrib/test_datetimetz.py new file mode 100644 index 0000000..315d35b --- /dev/null +++ b/tests/modules/contrib/test_datetimetz.py @@ -0,0 +1,11 @@ +import pytest + +pytest.importorskip("datetime") + +pytest.importorskip("pytz") + +pytest.importorskip("tzlocal") + +def test_load_module(): + __import__("modules.contrib.datetimetz") + diff --git a/tests/modules/contrib/test_datetz.py b/tests/modules/contrib/test_datetz.py new file mode 100644 index 0000000..ae15f67 --- /dev/null +++ b/tests/modules/contrib/test_datetz.py @@ -0,0 +1,11 @@ +import pytest + +pytest.importorskip("datetime") + +pytest.importorskip("pytz") + +pytest.importorskip("tzlocal") + +def test_load_module(): + __import__("modules.contrib.datetz") + diff --git a/tests/modules/contrib/test_deadbeef.py b/tests/modules/contrib/test_deadbeef.py new file mode 100644 index 0000000..5d98401 --- /dev/null +++ b/tests/modules/contrib/test_deadbeef.py @@ -0,0 +1,7 @@ +import pytest + +pytest.importorskip("subprocess") + +def test_load_module(): + __import__("modules.contrib.deadbeef") + diff --git a/tests/modules/contrib/test_deezer.py b/tests/modules/contrib/test_deezer.py new file mode 100644 index 0000000..6bb507e --- /dev/null +++ b/tests/modules/contrib/test_deezer.py @@ -0,0 +1,7 @@ +import pytest + +pytest.importorskip("dbus") + +def test_load_module(): + __import__("modules.contrib.deezer") + diff --git a/tests/modules/contrib/test_dnf.py b/tests/modules/contrib/test_dnf.py new file mode 100644 index 0000000..858fb5b --- /dev/null +++ b/tests/modules/contrib/test_dnf.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.contrib.dnf") + diff --git a/tests/modules/contrib/test_docker_ps.py b/tests/modules/contrib/test_docker_ps.py new file mode 100644 index 0000000..fcae290 --- /dev/null +++ b/tests/modules/contrib/test_docker_ps.py @@ -0,0 +1,9 @@ +import pytest + +pytest.importorskip("docker") + +pytest.importorskip("requests.exceptions") + +def test_load_module(): + __import__("modules.contrib.docker_ps") + diff --git a/tests/modules/contrib/test_dunst.py b/tests/modules/contrib/test_dunst.py new file mode 100644 index 0000000..2ca2d40 --- /dev/null +++ b/tests/modules/contrib/test_dunst.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.contrib.dunst") + diff --git a/tests/modules/contrib/test_getcrypto.py b/tests/modules/contrib/test_getcrypto.py new file mode 100644 index 0000000..07a7ada --- /dev/null +++ b/tests/modules/contrib/test_getcrypto.py @@ -0,0 +1,9 @@ +import pytest + +pytest.importorskip("requests") + +pytest.importorskip("requests.exceptions") + +def test_load_module(): + __import__("modules.contrib.getcrypto") + diff --git a/tests/modules/contrib/test_github.py b/tests/modules/contrib/test_github.py new file mode 100644 index 0000000..57e19d8 --- /dev/null +++ b/tests/modules/contrib/test_github.py @@ -0,0 +1,7 @@ +import pytest + +pytest.importorskip("requests") + +def test_load_module(): + __import__("modules.contrib.github") + diff --git a/tests/modules/contrib/test_gpmdp.py b/tests/modules/contrib/test_gpmdp.py new file mode 100644 index 0000000..3ae9aa9 --- /dev/null +++ b/tests/modules/contrib/test_gpmdp.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.contrib.gpmdp") + diff --git a/tests/modules/contrib/test_hddtemp.py b/tests/modules/contrib/test_hddtemp.py new file mode 100644 index 0000000..20f2343 --- /dev/null +++ b/tests/modules/contrib/test_hddtemp.py @@ -0,0 +1,7 @@ +import pytest + +pytest.importorskip("socket") + +def test_load_module(): + __import__("modules.contrib.hddtemp") + diff --git a/tests/modules/contrib/test_hostname.py b/tests/modules/contrib/test_hostname.py new file mode 100644 index 0000000..e76adb7 --- /dev/null +++ b/tests/modules/contrib/test_hostname.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.contrib.hostname") + diff --git a/tests/modules/contrib/test_http_status.py b/tests/modules/contrib/test_http_status.py new file mode 100644 index 0000000..9d9338b --- /dev/null +++ b/tests/modules/contrib/test_http_status.py @@ -0,0 +1,9 @@ +import pytest + +pytest.importorskip("requests") + +pytest.importorskip("psutil") + +def test_load_module(): + __import__("modules.contrib.http_status") + diff --git a/tests/modules/contrib/test_indicator.py b/tests/modules/contrib/test_indicator.py new file mode 100644 index 0000000..f94fc53 --- /dev/null +++ b/tests/modules/contrib/test_indicator.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.contrib.indicator") + diff --git a/tests/modules/contrib/test_kernel.py b/tests/modules/contrib/test_kernel.py index 9953132..f74021d 100644 --- a/tests/modules/contrib/test_kernel.py +++ b/tests/modules/contrib/test_kernel.py @@ -21,4 +21,8 @@ def test_full_text(mocker, kernel_module): assert some_kernel == kernel_module.widget().full_text() +def test_load_module(): + __import__("modules.contrib.kernel") + + # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/modules/contrib/test_layout-xkbswitch.py b/tests/modules/contrib/test_layout-xkbswitch.py new file mode 100644 index 0000000..08cfd96 --- /dev/null +++ b/tests/modules/contrib/test_layout-xkbswitch.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.contrib.layout-xkbswitch") + diff --git a/tests/modules/contrib/test_layout.py b/tests/modules/contrib/test_layout.py new file mode 100644 index 0000000..61895ab --- /dev/null +++ b/tests/modules/contrib/test_layout.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.contrib.layout") + diff --git a/tests/modules/contrib/test_libvirtvms.py b/tests/modules/contrib/test_libvirtvms.py new file mode 100644 index 0000000..efa5880 --- /dev/null +++ b/tests/modules/contrib/test_libvirtvms.py @@ -0,0 +1,7 @@ +import pytest + +pytest.importorskip("libvirt") + +def test_load_module(): + __import__("modules.contrib.libvirtvms") + diff --git a/tests/modules/contrib/test_messagereceiver.py b/tests/modules/contrib/test_messagereceiver.py new file mode 100644 index 0000000..a4fbcca --- /dev/null +++ b/tests/modules/contrib/test_messagereceiver.py @@ -0,0 +1,7 @@ +import pytest + +pytest.importorskip("socket") + +def test_load_module(): + __import__("modules.contrib.messagereceiver") + diff --git a/tests/modules/contrib/test_mocp.py b/tests/modules/contrib/test_mocp.py new file mode 100644 index 0000000..8f2ffcc --- /dev/null +++ b/tests/modules/contrib/test_mocp.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.contrib.mocp") + diff --git a/tests/modules/contrib/test_mpd.py b/tests/modules/contrib/test_mpd.py index 1fb48da..8664b43 100644 --- a/tests/modules/contrib/test_mpd.py +++ b/tests/modules/contrib/test_mpd.py @@ -65,3 +65,8 @@ def test_update_calls_load_song(mocker, mpd_module): def test_default_layout(mpd_module): assert mpd_module._layout == "mpd.prev mpd.main mpd.next mpd.shuffle mpd.repeat" + + +def test_load_module(): + __import__("modules.contrib.mpd") + diff --git a/tests/modules/contrib/test_network_traffic.py b/tests/modules/contrib/test_network_traffic.py new file mode 100644 index 0000000..0dd52a9 --- /dev/null +++ b/tests/modules/contrib/test_network_traffic.py @@ -0,0 +1,9 @@ +import pytest + +pytest.importorskip("psutil") + +pytest.importorskip("netifaces") + +def test_load_module(): + __import__("modules.contrib.network_traffic") + diff --git a/tests/modules/contrib/test_notmuch_count.py b/tests/modules/contrib/test_notmuch_count.py new file mode 100644 index 0000000..37ca1cc --- /dev/null +++ b/tests/modules/contrib/test_notmuch_count.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.contrib.notmuch_count") + diff --git a/tests/modules/contrib/test_nvidiagpu.py b/tests/modules/contrib/test_nvidiagpu.py new file mode 100644 index 0000000..69cdc09 --- /dev/null +++ b/tests/modules/contrib/test_nvidiagpu.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.contrib.nvidiagpu") + diff --git a/tests/modules/contrib/test_octoprint.py b/tests/modules/contrib/test_octoprint.py new file mode 100644 index 0000000..8b8264b --- /dev/null +++ b/tests/modules/contrib/test_octoprint.py @@ -0,0 +1,13 @@ +import pytest + +pytest.importorskip("tkinter") + +pytest.importorskip("PIL") + +pytest.importorskip("requests") + +pytest.importorskip("simplejson") + +def test_load_module(): + __import__("modules.contrib.octoprint") + diff --git a/tests/modules/contrib/test_pacman.py b/tests/modules/contrib/test_pacman.py new file mode 100644 index 0000000..1bd656e --- /dev/null +++ b/tests/modules/contrib/test_pacman.py @@ -0,0 +1,7 @@ +import pytest + +pytest.importorskip("bumblebee_status.discover") + +def test_load_module(): + __import__("modules.contrib.pacman") + diff --git a/tests/modules/contrib/test_pihole.py b/tests/modules/contrib/test_pihole.py new file mode 100644 index 0000000..6d1849d --- /dev/null +++ b/tests/modules/contrib/test_pihole.py @@ -0,0 +1,7 @@ +import pytest + +pytest.importorskip("requests") + +def test_load_module(): + __import__("modules.contrib.pihole") + diff --git a/tests/modules/contrib/test_playerctl.py b/tests/modules/contrib/test_playerctl.py new file mode 100644 index 0000000..f5e15a6 --- /dev/null +++ b/tests/modules/contrib/test_playerctl.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.contrib.playerctl") + diff --git a/tests/modules/contrib/test_pomodoro.py b/tests/modules/contrib/test_pomodoro.py new file mode 100644 index 0000000..d8557ca --- /dev/null +++ b/tests/modules/contrib/test_pomodoro.py @@ -0,0 +1,9 @@ +import pytest + +pytest.importorskip("datetime") + +pytest.importorskip("math") + +def test_load_module(): + __import__("modules.contrib.pomodoro") + diff --git a/tests/modules/contrib/test_portage_status.py b/tests/modules/contrib/test_portage_status.py new file mode 100644 index 0000000..05fdd0f --- /dev/null +++ b/tests/modules/contrib/test_portage_status.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.contrib.portage_status") + diff --git a/tests/modules/contrib/test_prime.py b/tests/modules/contrib/test_prime.py new file mode 100644 index 0000000..8ccb0e3 --- /dev/null +++ b/tests/modules/contrib/test_prime.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.contrib.prime") + diff --git a/tests/modules/contrib/test_progress.py b/tests/modules/contrib/test_progress.py new file mode 100644 index 0000000..4d99931 --- /dev/null +++ b/tests/modules/contrib/test_progress.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.contrib.progress") + diff --git a/tests/modules/contrib/test_publicip.py b/tests/modules/contrib/test_publicip.py new file mode 100644 index 0000000..6c5a31a --- /dev/null +++ b/tests/modules/contrib/test_publicip.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.contrib.publicip") + diff --git a/tests/modules/contrib/test_rotation.py b/tests/modules/contrib/test_rotation.py new file mode 100644 index 0000000..7495828 --- /dev/null +++ b/tests/modules/contrib/test_rotation.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.contrib.rotation") + diff --git a/tests/modules/contrib/test_rss.py b/tests/modules/contrib/test_rss.py new file mode 100644 index 0000000..3a99569 --- /dev/null +++ b/tests/modules/contrib/test_rss.py @@ -0,0 +1,13 @@ +import pytest + +pytest.importorskip("feedparser") + +pytest.importorskip("webbrowser") + +pytest.importorskip("tempfile") + +pytest.importorskip("random") + +def test_load_module(): + __import__("modules.contrib.rss") + diff --git a/tests/modules/contrib/test_sensors.py b/tests/modules/contrib/test_sensors.py new file mode 100644 index 0000000..d32a30f --- /dev/null +++ b/tests/modules/contrib/test_sensors.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.contrib.sensors") + diff --git a/tests/modules/contrib/test_shell.py b/tests/modules/contrib/test_shell.py new file mode 100644 index 0000000..56ba0e8 --- /dev/null +++ b/tests/modules/contrib/test_shell.py @@ -0,0 +1,7 @@ +import pytest + +pytest.importorskip("subprocess") + +def test_load_module(): + __import__("modules.contrib.shell") + diff --git a/tests/modules/contrib/test_shortcut.py b/tests/modules/contrib/test_shortcut.py new file mode 100644 index 0000000..52cc474 --- /dev/null +++ b/tests/modules/contrib/test_shortcut.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.contrib.shortcut") + diff --git a/tests/modules/contrib/test_smartstatus.py b/tests/modules/contrib/test_smartstatus.py new file mode 100644 index 0000000..04a84f9 --- /dev/null +++ b/tests/modules/contrib/test_smartstatus.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.contrib.smartstatus") + diff --git a/tests/modules/contrib/test_spaceapi.py b/tests/modules/contrib/test_spaceapi.py new file mode 100644 index 0000000..bedcb1c --- /dev/null +++ b/tests/modules/contrib/test_spaceapi.py @@ -0,0 +1,7 @@ +import pytest + +pytest.importorskip("requests") + +def test_load_module(): + __import__("modules.contrib.spaceapi") + diff --git a/tests/modules/contrib/test_spotify.py b/tests/modules/contrib/test_spotify.py new file mode 100644 index 0000000..cf6a66b --- /dev/null +++ b/tests/modules/contrib/test_spotify.py @@ -0,0 +1,7 @@ +import pytest + +pytest.importorskip("dbus") + +def test_load_module(): + __import__("modules.contrib.spotify") + diff --git a/tests/modules/contrib/test_stock.py b/tests/modules/contrib/test_stock.py new file mode 100644 index 0000000..3c4adb1 --- /dev/null +++ b/tests/modules/contrib/test_stock.py @@ -0,0 +1,7 @@ +import pytest + +pytest.importorskip("urllib.request") + +def test_load_module(): + __import__("modules.contrib.stock") + diff --git a/tests/modules/contrib/test_sun.py b/tests/modules/contrib/test_sun.py new file mode 100644 index 0000000..3830ea6 --- /dev/null +++ b/tests/modules/contrib/test_sun.py @@ -0,0 +1,13 @@ +import pytest + +pytest.importorskip("suntime") + +pytest.importorskip("requests") + +pytest.importorskip("dateutil.tz") + +pytest.importorskip("datetime") + +def test_load_module(): + __import__("modules.contrib.sun") + diff --git a/tests/modules/contrib/test_system.py b/tests/modules/contrib/test_system.py new file mode 100644 index 0000000..6f107a0 --- /dev/null +++ b/tests/modules/contrib/test_system.py @@ -0,0 +1,11 @@ +import pytest + +pytest.importorskip("tkinter") + +pytest.importorskip("tkinter") + +pytest.importorskip("tkinter") + +def test_load_module(): + __import__("modules.contrib.system") + diff --git a/tests/modules/contrib/test_taskwarrior.py b/tests/modules/contrib/test_taskwarrior.py new file mode 100644 index 0000000..ab6f08f --- /dev/null +++ b/tests/modules/contrib/test_taskwarrior.py @@ -0,0 +1,7 @@ +import pytest + +pytest.importorskip("taskw") + +def test_load_module(): + __import__("modules.contrib.taskwarrior") + diff --git a/tests/modules/contrib/test_timetz.py b/tests/modules/contrib/test_timetz.py new file mode 100644 index 0000000..01e5879 --- /dev/null +++ b/tests/modules/contrib/test_timetz.py @@ -0,0 +1,11 @@ +import pytest + +pytest.importorskip("datetime") + +pytest.importorskip("pytz") + +pytest.importorskip("tzlocal") + +def test_load_module(): + __import__("modules.contrib.timetz") + diff --git a/tests/modules/contrib/test_title.py b/tests/modules/contrib/test_title.py new file mode 100644 index 0000000..089102b --- /dev/null +++ b/tests/modules/contrib/test_title.py @@ -0,0 +1,7 @@ +import pytest + +pytest.importorskip("i3ipc") + +def test_load_module(): + __import__("modules.contrib.title") + diff --git a/tests/modules/contrib/test_todo.py b/tests/modules/contrib/test_todo.py new file mode 100644 index 0000000..203b30a --- /dev/null +++ b/tests/modules/contrib/test_todo.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.contrib.todo") + diff --git a/tests/modules/contrib/test_traffic.py b/tests/modules/contrib/test_traffic.py new file mode 100644 index 0000000..8711414 --- /dev/null +++ b/tests/modules/contrib/test_traffic.py @@ -0,0 +1,9 @@ +import pytest + +pytest.importorskip("psutil") + +pytest.importorskip("netifaces") + +def test_load_module(): + __import__("modules.contrib.traffic") + diff --git a/tests/modules/contrib/test_twmn.py b/tests/modules/contrib/test_twmn.py new file mode 100644 index 0000000..11e3421 --- /dev/null +++ b/tests/modules/contrib/test_twmn.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.contrib.twmn") + diff --git a/tests/modules/contrib/test_uptime.py b/tests/modules/contrib/test_uptime.py new file mode 100644 index 0000000..67c791f --- /dev/null +++ b/tests/modules/contrib/test_uptime.py @@ -0,0 +1,7 @@ +import pytest + +pytest.importorskip("datetime") + +def test_load_module(): + __import__("modules.contrib.uptime") + diff --git a/tests/modules/contrib/test_vpn.py b/tests/modules/contrib/test_vpn.py new file mode 100644 index 0000000..ef46640 --- /dev/null +++ b/tests/modules/contrib/test_vpn.py @@ -0,0 +1,7 @@ +import pytest + +pytest.importorskip("tkinter") + +def test_load_module(): + __import__("modules.contrib.vpn") + diff --git a/tests/modules/contrib/test_watson.py b/tests/modules/contrib/test_watson.py new file mode 100644 index 0000000..6786f38 --- /dev/null +++ b/tests/modules/contrib/test_watson.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.contrib.watson") + diff --git a/tests/modules/contrib/test_weather.py b/tests/modules/contrib/test_weather.py new file mode 100644 index 0000000..ef4dc70 --- /dev/null +++ b/tests/modules/contrib/test_weather.py @@ -0,0 +1,9 @@ +import pytest + +pytest.importorskip("requests") + +pytest.importorskip("requests.exceptions") + +def test_load_module(): + __import__("modules.contrib.weather") + diff --git a/tests/modules/contrib/test_xkcd.py b/tests/modules/contrib/test_xkcd.py new file mode 100644 index 0000000..35e00be --- /dev/null +++ b/tests/modules/contrib/test_xkcd.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.contrib.xkcd") + diff --git a/tests/modules/contrib/test_yubikey.py b/tests/modules/contrib/test_yubikey.py new file mode 100644 index 0000000..6b9d36a --- /dev/null +++ b/tests/modules/contrib/test_yubikey.py @@ -0,0 +1,7 @@ +import pytest + +pytest.importorskip("yubico") + +def test_load_module(): + __import__("modules.contrib.yubikey") + diff --git a/tests/modules/contrib/test_zpool.py b/tests/modules/contrib/test_zpool.py new file mode 100644 index 0000000..5577e00 --- /dev/null +++ b/tests/modules/contrib/test_zpool.py @@ -0,0 +1,7 @@ +import pytest + +pytest.importorskip("pkg_resources") + +def test_load_module(): + __import__("modules.contrib.zpool") + diff --git a/tests/modules/core/test_cpu.py b/tests/modules/core/test_cpu.py new file mode 100644 index 0000000..9ea85ed --- /dev/null +++ b/tests/modules/core/test_cpu.py @@ -0,0 +1,7 @@ +import pytest + +pytest.importorskip("psutil") + +def test_load_module(): + __import__("modules.core.cpu") + diff --git a/tests/modules/core/test_date.py b/tests/modules/core/test_date.py new file mode 100644 index 0000000..b87e64b --- /dev/null +++ b/tests/modules/core/test_date.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.core.date") + diff --git a/tests/modules/core/test_datetime.py b/tests/modules/core/test_datetime.py new file mode 100644 index 0000000..b8384d9 --- /dev/null +++ b/tests/modules/core/test_datetime.py @@ -0,0 +1,7 @@ +import pytest + +pytest.importorskip("datetime") + +def test_load_module(): + __import__("modules.core.datetime") + diff --git a/tests/modules/core/test_debug.py b/tests/modules/core/test_debug.py new file mode 100644 index 0000000..04305b2 --- /dev/null +++ b/tests/modules/core/test_debug.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.core.debug") + diff --git a/tests/modules/core/test_disk.py b/tests/modules/core/test_disk.py new file mode 100644 index 0000000..791ba48 --- /dev/null +++ b/tests/modules/core/test_disk.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.core.disk") + diff --git a/tests/modules/core/test_error.py b/tests/modules/core/test_error.py new file mode 100644 index 0000000..296fbd7 --- /dev/null +++ b/tests/modules/core/test_error.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.core.error") + diff --git a/tests/modules/core/test_git.py b/tests/modules/core/test_git.py new file mode 100644 index 0000000..62f67ad --- /dev/null +++ b/tests/modules/core/test_git.py @@ -0,0 +1,7 @@ +import pytest + +pytest.importorskip("pygit2") + +def test_load_module(): + __import__("modules.core.git") + diff --git a/tests/modules/core/test_layout-xkb.py b/tests/modules/core/test_layout-xkb.py new file mode 100644 index 0000000..8eacfad --- /dev/null +++ b/tests/modules/core/test_layout-xkb.py @@ -0,0 +1,7 @@ +import pytest + +pytest.importorskip("xkbgroup") + +def test_load_module(): + __import__("modules.core.layout-xkb") + diff --git a/tests/modules/core/test_load.py b/tests/modules/core/test_load.py new file mode 100644 index 0000000..2210f9e --- /dev/null +++ b/tests/modules/core/test_load.py @@ -0,0 +1,7 @@ +import pytest + +pytest.importorskip("multiprocessing") + +def test_load_module(): + __import__("modules.core.load") + diff --git a/tests/modules/core/test_memory.py b/tests/modules/core/test_memory.py new file mode 100644 index 0000000..2b87b8a --- /dev/null +++ b/tests/modules/core/test_memory.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.core.memory") + diff --git a/tests/modules/core/test_nic.py b/tests/modules/core/test_nic.py new file mode 100644 index 0000000..e827eb3 --- /dev/null +++ b/tests/modules/core/test_nic.py @@ -0,0 +1,9 @@ +import pytest + +pytest.importorskip("netifaces") + +pytest.importorskip("subprocess") + +def test_load_module(): + __import__("modules.core.nic") + diff --git a/tests/modules/core/test_pasink.py b/tests/modules/core/test_pasink.py new file mode 100644 index 0000000..2319e63 --- /dev/null +++ b/tests/modules/core/test_pasink.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.core.pasink") + diff --git a/tests/modules/core/test_pasource.py b/tests/modules/core/test_pasource.py new file mode 100644 index 0000000..88ef465 --- /dev/null +++ b/tests/modules/core/test_pasource.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.core.pasource") + diff --git a/tests/modules/core/test_ping.py b/tests/modules/core/test_ping.py new file mode 100644 index 0000000..269321a --- /dev/null +++ b/tests/modules/core/test_ping.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.core.ping") + diff --git a/tests/modules/core/test_pulseaudio.py b/tests/modules/core/test_pulseaudio.py new file mode 100644 index 0000000..66b69e3 --- /dev/null +++ b/tests/modules/core/test_pulseaudio.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.core.pulseaudio") + diff --git a/tests/modules/core/test_redshift.py b/tests/modules/core/test_redshift.py new file mode 100644 index 0000000..3f6f72f --- /dev/null +++ b/tests/modules/core/test_redshift.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.core.redshift") + diff --git a/tests/modules/core/test_sensors2.py b/tests/modules/core/test_sensors2.py new file mode 100644 index 0000000..59bd6e4 --- /dev/null +++ b/tests/modules/core/test_sensors2.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.core.sensors2") + diff --git a/tests/modules/core/test_spacer.py b/tests/modules/core/test_spacer.py new file mode 100644 index 0000000..b592e22 --- /dev/null +++ b/tests/modules/core/test_spacer.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.core.spacer") + diff --git a/tests/modules/core/test_speedtest.py b/tests/modules/core/test_speedtest.py new file mode 100644 index 0000000..51cc775 --- /dev/null +++ b/tests/modules/core/test_speedtest.py @@ -0,0 +1,7 @@ +import pytest + +pytest.importorskip("speedtest") + +def test_load_module(): + __import__("modules.core.speedtest") + diff --git a/tests/modules/core/test_test.py b/tests/modules/core/test_test.py new file mode 100644 index 0000000..ede1244 --- /dev/null +++ b/tests/modules/core/test_test.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.core.test") + diff --git a/tests/modules/core/test_time.py b/tests/modules/core/test_time.py new file mode 100644 index 0000000..603a50d --- /dev/null +++ b/tests/modules/core/test_time.py @@ -0,0 +1,5 @@ +import pytest + +def test_load_module(): + __import__("modules.core.time") + diff --git a/tests/modules/core/test_vault.py b/tests/modules/core/test_vault.py new file mode 100644 index 0000000..eed6cc0 --- /dev/null +++ b/tests/modules/core/test_vault.py @@ -0,0 +1,7 @@ +import pytest + +pytest.importorskip("tkinter") + +def test_load_module(): + __import__("modules.core.vault") + diff --git a/tests/modules/core/test_xrandr.py b/tests/modules/core/test_xrandr.py new file mode 100644 index 0000000..b2d7120 --- /dev/null +++ b/tests/modules/core/test_xrandr.py @@ -0,0 +1,9 @@ +import pytest + +pytest.importorskip("bumblebee_status.discover") + +pytest.importorskip("i3") + +def test_load_module(): + __import__("modules.core.xrandr") + From 54a2fc3a419ac73700fc2bcd85c777508d52363d Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Mon, 20 Jul 2020 13:58:46 +0200 Subject: [PATCH 061/506] [generate-tests] black --- generate-base-tests.py | 44 +++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/generate-base-tests.py b/generate-base-tests.py index 03efc37..281dc45 100755 --- a/generate-base-tests.py +++ b/generate-base-tests.py @@ -5,6 +5,7 @@ import re import sys import glob + def is_psl(module): lib_path = os.path.dirname(os.__file__) old_sys = sys.path @@ -20,37 +21,45 @@ def is_psl(module): def is_internal(module): - if module.startswith("core.") or module == "core": return True - if module.startswith("util.") or module == "util": return True - if module.startswith("."): return True + if module.startswith("core.") or module == "core": + return True + if module.startswith("util.") or module == "util": + return True + if module.startswith("."): + return True return is_psl(module) + def dependencies(filename): deps = [] with open(filename) as f: for line in f: if "import" in line: match = re.match("\s*(from (\S+) )?import (\S+)", line) - if not match: continue + if not match: + continue dep = match.group(2) or match.group(3) if "util.popup" in dep or ("util" in line and "popup" in line): deps.append("tkinter") if ".datetimetz" in line: - deps.extend(dependencies("bumblebee_status/modules/contrib/datetimetz.py")) + deps.extend( + dependencies("bumblebee_status/modules/contrib/datetimetz.py") + ) elif not is_internal(dep): deps.append(dep) return deps + def write_test(testname, modname, deps): fqmn = ".".join(["modules", testname.split(os.sep)[2], modname]) if not os.path.exists(testname): with open(testname, "w") as f: - f.writelines([ - "import pytest\n\n", - ]) + f.writelines( + ["import pytest\n\n",] + ) for dep in deps: - f.write("pytest.importorskip(\"{}\")\n\n".format(dep)) + f.write('pytest.importorskip("{}")\n\n'.format(dep)) with open(testname) as f: for line in f: @@ -60,23 +69,26 @@ def write_test(testname, modname, deps): print("writing base test for {}".format(modname)) with open(testname, "a+") as f: - f.writelines([ - "def test_load_module():\n", - " __import__(\"{}\")\n\n".format(fqmn), - ]) + f.writelines( + ["def test_load_module():\n", ' __import__("{}")\n\n'.format(fqmn),] + ) + def main(): for f in glob.glob("bumblebee_status/modules/*/*.py"): - if os.path.basename(f) == "__init__.py": continue + if os.path.basename(f) == "__init__.py": + continue modname = os.path.splitext(os.path.basename(f))[0] modpath = os.path.dirname(f) deps = dependencies(f) - testname = os.path.join("tests", "modules", modpath.split(os.sep)[2], "test_{}.py".format(modname)) + testname = os.path.join( + "tests", "modules", modpath.split(os.sep)[2], "test_{}.py".format(modname) + ) write_test(testname, modname, deps) - + if __name__ == "__main__": main() From a008ce3e58c19096be9c93b43481b617cb9c7470 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Tue, 21 Jul 2020 08:08:22 +0200 Subject: [PATCH 062/506] [travis] add all dependencies to make all unit tests run, and none skipped (hopefully), add a list of dependencies --- .travis.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.travis.yml b/.travis.yml index b730e3e..d7d46d2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,15 @@ before_install: install: - pip install -U coverage==4.3 pytest pytest-mock - pip install codeclimate-test-reporter + - pip install i3-py Pillow Babel DateTime python-dateutil + - pip install dbus-python docker feedparser i3ipc + - pip install libvirt-python math-utils + - pip install multiprocessing netifaces power + - pip install psutil pygit2 pytz random + - pip install requests simplejson + - pip install socket speedtest suntime + - pip install tempfile tkinter tzlocal + - pip install taskw webbrowser xkbgroup yubico script: - coverage run --source=. -m pytest tests -v - CODECLIMATE_REPO_TOKEN=40cb00907f7a10e04868e856570bb997ab9c42fd3b63d980f2b2269433195fdf codeclimate-test-reporter From 463bc9665bd8e5a26d291a654c25daa0d13141ca Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Tue, 21 Jul 2020 08:14:21 +0200 Subject: [PATCH 063/506] [travis] fix python-dbus --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d7d46d2..c996ea8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,10 +8,11 @@ python: before_install: - sudo apt-get -qq update install: + - apt-get install dbus-python - pip install -U coverage==4.3 pytest pytest-mock - pip install codeclimate-test-reporter - pip install i3-py Pillow Babel DateTime python-dateutil - - pip install dbus-python docker feedparser i3ipc + - pip install docker feedparser i3ipc - pip install libvirt-python math-utils - pip install multiprocessing netifaces power - pip install psutil pygit2 pytz random From bf1cae23992fd282d60a40c5624e9dbc0d2e5715 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Tue, 21 Jul 2020 08:15:47 +0200 Subject: [PATCH 064/506] [travis] forgot sudo --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c996ea8..3b6a516 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ python: before_install: - sudo apt-get -qq update install: - - apt-get install dbus-python + - sudo apt-get install dbus-python - pip install -U coverage==4.3 pytest pytest-mock - pip install codeclimate-test-reporter - pip install i3-py Pillow Babel DateTime python-dateutil From 6c9e89627a35a1de85159334f92ddd6e7bb51d0f Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Tue, 21 Jul 2020 08:17:25 +0200 Subject: [PATCH 065/506] [travis] fix package name --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3b6a516..1346cd3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ python: before_install: - sudo apt-get -qq update install: - - sudo apt-get install dbus-python + - sudo apt-get install python-dbus - pip install -U coverage==4.3 pytest pytest-mock - pip install codeclimate-test-reporter - pip install i3-py Pillow Babel DateTime python-dateutil From 4fe56fc00d6ff0bebdc1443d76c1a6046ff5ed92 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Tue, 21 Jul 2020 08:19:51 +0200 Subject: [PATCH 066/506] [travis] remove math module --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 1346cd3..b6d8f42 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,7 @@ install: - pip install codeclimate-test-reporter - pip install i3-py Pillow Babel DateTime python-dateutil - pip install docker feedparser i3ipc - - pip install libvirt-python math-utils + - pip install libvirt-python - pip install multiprocessing netifaces power - pip install psutil pygit2 pytz random - pip install requests simplejson From 550b594c864e76cf0f26aab09a4dff971bd3e4a1 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Tue, 21 Jul 2020 08:20:55 +0200 Subject: [PATCH 067/506] [tests] fix some test prerequisites --- generate-base-tests.py | 2 ++ tests/modules/contrib/test_pacman.py | 2 -- tests/modules/core/test_xrandr.py | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/generate-base-tests.py b/generate-base-tests.py index 281dc45..3f94905 100755 --- a/generate-base-tests.py +++ b/generate-base-tests.py @@ -25,6 +25,8 @@ def is_internal(module): return True if module.startswith("util.") or module == "util": return True + if module.startswith("bumblebee_status."): + return True if module.startswith("."): return True diff --git a/tests/modules/contrib/test_pacman.py b/tests/modules/contrib/test_pacman.py index 1bd656e..01db8c6 100644 --- a/tests/modules/contrib/test_pacman.py +++ b/tests/modules/contrib/test_pacman.py @@ -1,7 +1,5 @@ import pytest -pytest.importorskip("bumblebee_status.discover") - def test_load_module(): __import__("modules.contrib.pacman") diff --git a/tests/modules/core/test_xrandr.py b/tests/modules/core/test_xrandr.py index b2d7120..720d96a 100644 --- a/tests/modules/core/test_xrandr.py +++ b/tests/modules/core/test_xrandr.py @@ -1,7 +1,5 @@ import pytest -pytest.importorskip("bumblebee_status.discover") - pytest.importorskip("i3") def test_load_module(): From 6c7737cdd51c564516761f5e33399b729763ca08 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Tue, 21 Jul 2020 08:23:17 +0200 Subject: [PATCH 068/506] [travis] remove libvirt-python requires at least python3.5 --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b6d8f42..4018eb3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,6 @@ install: - pip install codeclimate-test-reporter - pip install i3-py Pillow Babel DateTime python-dateutil - pip install docker feedparser i3ipc - - pip install libvirt-python - pip install multiprocessing netifaces power - pip install psutil pygit2 pytz random - pip install requests simplejson From b7083aacce3eb8a6ed4fa5d97bd8b83446cf0f71 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Tue, 21 Jul 2020 08:25:18 +0200 Subject: [PATCH 069/506] [travis] remove multiprocessing doesn't install :( --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 4018eb3..e763746 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,7 @@ install: - pip install codeclimate-test-reporter - pip install i3-py Pillow Babel DateTime python-dateutil - pip install docker feedparser i3ipc - - pip install multiprocessing netifaces power + - pip install netifaces power - pip install psutil pygit2 pytz random - pip install requests simplejson - pip install socket speedtest suntime From 84f2fdd419a0eb985fc9be46d7e4e254cfb1227e Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Tue, 21 Jul 2020 08:27:23 +0200 Subject: [PATCH 070/506] [travis] removed random --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e763746..8d731c9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,7 @@ install: - pip install i3-py Pillow Babel DateTime python-dateutil - pip install docker feedparser i3ipc - pip install netifaces power - - pip install psutil pygit2 pytz random + - pip install psutil pygit2 pytz - pip install requests simplejson - pip install socket speedtest suntime - pip install tempfile tkinter tzlocal From 993be61eec3669a3d4b49723a1386b7d747da153 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Tue, 21 Jul 2020 08:43:08 +0200 Subject: [PATCH 071/506] [travis] remove pygit2 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 8d731c9..27da83f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,7 @@ install: - pip install i3-py Pillow Babel DateTime python-dateutil - pip install docker feedparser i3ipc - pip install netifaces power - - pip install psutil pygit2 pytz + - pip install psutil pytz - pip install requests simplejson - pip install socket speedtest suntime - pip install tempfile tkinter tzlocal From 6870c3ba84be1f438f544375ab9dec71da7c66e6 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Tue, 21 Jul 2020 08:53:41 +0200 Subject: [PATCH 072/506] [travis] removed a couple more modules --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 27da83f..7f79b21 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,9 +16,9 @@ install: - pip install netifaces power - pip install psutil pytz - pip install requests simplejson - - pip install socket speedtest suntime + - pip install speedtest suntime - pip install tempfile tkinter tzlocal - - pip install taskw webbrowser xkbgroup yubico + - pip install taskw webbrowser script: - coverage run --source=. -m pytest tests -v - CODECLIMATE_REPO_TOKEN=40cb00907f7a10e04868e856570bb997ab9c42fd3b63d980f2b2269433195fdf codeclimate-test-reporter From 632eb0c4509b5624fb362c656d41c7c0dc515492 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Tue, 21 Jul 2020 08:55:54 +0200 Subject: [PATCH 073/506] [travis] remove speedtest --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7f79b21..f78af0a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ install: - pip install netifaces power - pip install psutil pytz - pip install requests simplejson - - pip install speedtest suntime + - pip install suntime - pip install tempfile tkinter tzlocal - pip install taskw webbrowser script: From bee2586ed4b59f96a83cadc3fc312c0791a09dea Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Tue, 21 Jul 2020 08:58:36 +0200 Subject: [PATCH 074/506] [travis] remove tempfile --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f78af0a..b42cb34 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ install: - pip install psutil pytz - pip install requests simplejson - pip install suntime - - pip install tempfile tkinter tzlocal + - pip install tkinter tzlocal - pip install taskw webbrowser script: - coverage run --source=. -m pytest tests -v From c692a776b66077835d44b101c6f1b4e4bb47d4e0 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Tue, 21 Jul 2020 09:00:59 +0200 Subject: [PATCH 075/506] [travis] remove tkinter --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b42cb34..799dc03 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ install: - pip install psutil pytz - pip install requests simplejson - pip install suntime - - pip install tkinter tzlocal + - pip install tzlocal - pip install taskw webbrowser script: - coverage run --source=. -m pytest tests -v From b39dba8867312405b3d1dcadef57be99354d9b06 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Tue, 21 Jul 2020 09:02:51 +0200 Subject: [PATCH 076/506] [travis] remove webbrowser --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 799dc03..59061c6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,7 @@ install: - pip install requests simplejson - pip install suntime - pip install tzlocal - - pip install taskw webbrowser + - pip install taskw script: - coverage run --source=. -m pytest tests -v - CODECLIMATE_REPO_TOKEN=40cb00907f7a10e04868e856570bb997ab9c42fd3b63d980f2b2269433195fdf codeclimate-test-reporter From d358790c6a20e571e10d2ab359836059fff43446 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Tue, 21 Jul 2020 09:04:50 +0200 Subject: [PATCH 077/506] [travis] make unit tests work again --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 59061c6..400bd81 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,6 @@ install: - pip install requests simplejson - pip install suntime - pip install tzlocal - - pip install taskw script: - coverage run --source=. -m pytest tests -v - CODECLIMATE_REPO_TOKEN=40cb00907f7a10e04868e856570bb997ab9c42fd3b63d980f2b2269433195fdf codeclimate-test-reporter From 4aff0499f02c3d2deb5ae595f8ac456094da0eea Mon Sep 17 00:00:00 2001 From: Naya Verdier Date: Sat, 25 Jul 2020 08:43:26 -0700 Subject: [PATCH 078/506] [util.popup] Deduplicate code, "close" button only if leave=False --- bumblebee_status/util/popup.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/bumblebee_status/util/popup.py b/bumblebee_status/util/popup.py index f162846..bbabe66 100644 --- a/bumblebee_status/util/popup.py +++ b/bumblebee_status/util/popup.py @@ -16,23 +16,19 @@ class menu(object): def __init__(self, parent=None, leave=True): self.running = True - self.parent = None - if not parent: - self._root = tk.Tk() - self._root.withdraw() - self._menu = tk.Menu(self._root, tearoff=0) - self._menu.bind("", self.__on_focus_out) - self.add_menuitem("close", self.__on_focus_out) - self.add_separator() - else: - self._root = parent.root() - self._root.withdraw() - self._menu = tk.Menu(self._root, tearoff=0) - self._menu.bind("", self.__on_focus_out) - self.parent = parent + self.parent = parent + + self._root = parent.root() if parent else tk.Tk() + self._root.withdraw() + self._menu = tk.Menu(self._root, tearoff=0) + self._menu.bind("", self.__on_focus_out) + if leave: self._menu.bind("", self.__on_focus_out) + elif not parent: + self.add_menuitem("close", self.__on_focus_out) + self.add_separator() self._menu.bind("", self.release) From 5f2857ad9aa3d090b6894be0b3d9a1cde96dee60 Mon Sep 17 00:00:00 2001 From: Naya Verdier Date: Sat, 25 Jul 2020 08:46:57 -0700 Subject: [PATCH 079/506] [vault] add leave_menu parameter to auto-close the menu --- bumblebee_status/modules/core/vault.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bumblebee_status/modules/core/vault.py b/bumblebee_status/modules/core/vault.py index 28f37f4..ee4fc7f 100644 --- a/bumblebee_status/modules/core/vault.py +++ b/bumblebee_status/modules/core/vault.py @@ -12,6 +12,8 @@ Parameters: * vault.location: Location of the password store (defaults to ~/.password-store) * vault.offx: x-axis offset of popup menu (defaults to 0) * vault.offy: y-axis offset of popup menu (defaults to 0) + * vault.leave_menu: Boolean flag to close menu when the mouse leaves (defaults to False) + * vault.text: Text to display on the widget (defaults to ) Many thanks to `bbernhard `_ for the idea! """ @@ -72,7 +74,7 @@ class Module(core.module.Module): core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.popup) def popup(self, widget): - menu = util.popup.menu(leave=False) + menu = util.popup.menu(leave=util.format.asbool(self.parameter("leave_menu", False))) build_menu(menu, self.__path, self.__callback) menu.show(widget, offset_x=self.__offx, offset_y=self.__offy) From 8f57bb952de03cb6e297abcbb3ba15471268d473 Mon Sep 17 00:00:00 2001 From: Naya Verdier Date: Sat, 25 Jul 2020 08:47:31 -0700 Subject: [PATCH 080/506] [xrandr] add exclude parameter to ignore certain display prefixes --- bumblebee_status/modules/core/xrandr.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/bumblebee_status/modules/core/xrandr.py b/bumblebee_status/modules/core/xrandr.py index e1717e8..f088fb5 100644 --- a/bumblebee_status/modules/core/xrandr.py +++ b/bumblebee_status/modules/core/xrandr.py @@ -8,6 +8,7 @@ Parameters: and appending a file '~/.config/i3/config.' for every screen. * xrandr.autoupdate: If set to 'false', does *not* invoke xrandr automatically. Instead, the module will only refresh when displays are enabled or disabled (defaults to true) + * xrandr.exclude: Comma-separated list of display name prefixes to exclude Requires the following python module: * (optional) i3 - if present, the need for updating the widget list is auto-detected @@ -40,6 +41,7 @@ class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, []) + self._exclude = tuple(filter(len, self.parameter("exclude", "").split(","))) self._autoupdate = util.format.asbool(self.parameter("autoupdate", True)) self._needs_update = True @@ -62,16 +64,20 @@ class Module(core.module.Module): for line in util.cli.execute("xrandr -q").split("\n"): if not " connected" in line: continue + display = line.split(" ", 2)[0] - m = re.search(r"\d+x\d+\+(\d+)\+\d+", line) + if display.startswith(self._exclude): + continue + + resolution = re.search(r"\d+x\d+\+(\d+)\+\d+", line) widget = self.widget(display) if not widget: widget = self.add_widget(full_text=display, name=display) core.input.register(widget, button=1, cmd=self._toggle) core.input.register(widget, button=3, cmd=self._toggle) - widget.set("state", "on" if m else "off") - widget.set("pos", int(m.group(1)) if m else sys.maxsize) + widget.set("state", "on" if resolution else "off") + widget.set("pos", int(resolution.group(1)) if resolution else sys.maxsize) if self._autoupdate == False: widget = self.add_widget(full_text="") From 5874850bd5360ceb1039615cf65af02b8fa400cc Mon Sep 17 00:00:00 2001 From: Naya Verdier Date: Sat, 25 Jul 2020 10:52:10 -0700 Subject: [PATCH 081/506] [xrandr] Fix neighbor functionality when some displays are excluded --- bumblebee_status/modules/core/xrandr.py | 73 +++++++++---------------- 1 file changed, 27 insertions(+), 46 deletions(-) diff --git a/bumblebee_status/modules/core/xrandr.py b/bumblebee_status/modules/core/xrandr.py index f088fb5..eee6be4 100644 --- a/bumblebee_status/modules/core/xrandr.py +++ b/bumblebee_status/modules/core/xrandr.py @@ -17,7 +17,6 @@ Requires the following executable: * xrandr """ -import os import re import sys @@ -32,7 +31,7 @@ import util.format try: import i3 -except: +except ModuleNotFoundError: pass @@ -42,35 +41,40 @@ class Module(core.module.Module): super().__init__(config, theme, []) self._exclude = tuple(filter(len, self.parameter("exclude", "").split(","))) + self._active_displays = [] self._autoupdate = util.format.asbool(self.parameter("autoupdate", True)) self._needs_update = True try: i3.Subscription(self._output_update, "output") - except: + except NameError: pass def _output_update(self, event, data, _): self._needs_update = True def update(self): - self.clear_widgets() - - if self._autoupdate == False and self._needs_update == False: + if not self._autoupdate and not self._needs_update: return + self.clear_widgets() + self._active_displays.clear() + self._needs_update = False for line in util.cli.execute("xrandr -q").split("\n"): - if not " connected" in line: + if " connected" not in line: continue display = line.split(" ", 2)[0] + resolution = re.search(r"\d+x\d+\+(\d+)\+\d+", line) + + if resolution: + self._active_displays.append(display) + if display.startswith(self._exclude): continue - resolution = re.search(r"\d+x\d+\+(\d+)\+\d+", line) - widget = self.widget(display) if not widget: widget = self.add_widget(full_text=display, name=display) @@ -79,7 +83,7 @@ class Module(core.module.Module): widget.set("state", "on" if resolution else "off") widget.set("pos", int(resolution.group(1)) if resolution else sys.maxsize) - if self._autoupdate == False: + if not self._autoupdate: widget = self.add_widget(full_text="") widget.set("state", "refresh") core.input.register(widget, button=1, cmd=self._refresh) @@ -91,9 +95,7 @@ class Module(core.module.Module): self._needs_update = True def _toggle(self, event): - self._refresh(event) - - if util.format.asbool(self.parameter("overwrite_i3config", False)) == True: + if util.format.asbool(self.parameter("overwrite_i3config", False)): toggle_cmd = utility("toggle-display.sh") else: toggle_cmd = "xrandr" @@ -102,41 +104,20 @@ class Module(core.module.Module): if widget.get("state") == "on": util.cli.execute("{} --output {} --off".format(toggle_cmd, widget.name)) + elif not self._active_displays: + util.cli.execute("{} --output {} --auto".format(toggle_cmd, widget.name)) else: - first_neighbor = next( - (widget for widget in self.widgets() if widget.get("state") == "on"), - None, - ) - last_neighbor = next( - ( - widget - for widget in reversed(self.widgets()) - if widget.get("state") == "on" - ), - None, - ) - - neighbor = ( - first_neighbor - if event["button"] == core.input.LEFT_MOUSE - else last_neighbor - ) - - if neighbor is None: - util.cli.execute( - "{} --output {} --auto".format(toggle_cmd, widget.name) - ) + if event["button"] == core.input.LEFT_MOUSE: + side, neighbor = "left", self._active_displays[0] else: - util.cli.execute( - "{} --output {} --auto --{}-of {}".format( - toggle_cmd, - widget.name, - "left" - if event.get("button") == core.input.LEFT_MOUSE - else "right", - neighbor.name, - ) - ) + side, neighbor = "right", self._active_displays[-1] + util.cli.execute( + "{} --output {} --auto --{}-of {}".format( + toggle_cmd, widget.name, side, neighbor, + ) + ) + + self._refresh(event) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From b14eae4d6c3344d2d4c5cd2c243fdf315e419032 Mon Sep 17 00:00:00 2001 From: Naya Verdier Date: Sat, 25 Jul 2020 10:52:57 -0700 Subject: [PATCH 082/506] [xrandr] add safeguard to prevent turning off the only display --- bumblebee_status/modules/core/xrandr.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bumblebee_status/modules/core/xrandr.py b/bumblebee_status/modules/core/xrandr.py index eee6be4..a3fe1b7 100644 --- a/bumblebee_status/modules/core/xrandr.py +++ b/bumblebee_status/modules/core/xrandr.py @@ -103,7 +103,8 @@ class Module(core.module.Module): widget = self.widget(widget_id=event["instance"]) if widget.get("state") == "on": - util.cli.execute("{} --output {} --off".format(toggle_cmd, widget.name)) + if len(self._active_displays) > 1: + util.cli.execute("{} --output {} --off".format(toggle_cmd, widget.name)) elif not self._active_displays: util.cli.execute("{} --output {} --auto".format(toggle_cmd, widget.name)) else: From d05ff39f027a8156f6d23d821064c89f4eb64b31 Mon Sep 17 00:00:00 2001 From: Naya Verdier Date: Sat, 25 Jul 2020 10:53:44 -0700 Subject: [PATCH 083/506] [xrandr] Add unit tests --- tests/modules/core/test_xrandr.py | 158 +++++++++++++++++++++++++++++- 1 file changed, 154 insertions(+), 4 deletions(-) diff --git a/tests/modules/core/test_xrandr.py b/tests/modules/core/test_xrandr.py index 720d96a..b4e6156 100644 --- a/tests/modules/core/test_xrandr.py +++ b/tests/modules/core/test_xrandr.py @@ -1,7 +1,157 @@ -import pytest +import sys -pytest.importorskip("i3") +from core.input import trigger, LEFT_MOUSE, RIGHT_MOUSE +from core.config import Config +from modules.core.xrandr import Module -def test_load_module(): - __import__("modules.core.xrandr") +MOCK_EXECUTE = "modules.core.xrandr.util.cli.execute" + + +def mock_xrandr(mocker, xrandr_output): + return mocker.patch(MOCK_EXECUTE, return_value=xrandr_output) + + +def assert_widgets(module, *expected_widgets): + assert len(module.widgets()) == len(expected_widgets) + + for widget, (name, state, pos) in zip(module.widgets(), expected_widgets): + assert widget.name == name + assert widget.get("state") == state + assert widget.get("pos") == pos + + +def assert_trigger(mocker, module, widget_index, button, expected_command): + xrandr_cli = mock_xrandr(mocker, "") + widget = module.widgets()[widget_index] + trigger({"button": button, "instance": widget.id, "name": module.id}) + + if expected_command is None: + xrandr_cli.assert_not_called() + else: + xrandr_cli.assert_called_once_with(expected_command) + + +def test_autoupdate(mocker): + xrandr_cli = mock_xrandr(mocker, FULL_OUTPUT_TWO_DISPLAYS) + module = Module(Config([]), theme=None) + module.update() + xrandr_cli.assert_called_once_with("xrandr -q") + + assert_widgets(module, ("eDP-1-1", "on", 0), ("HDMI-1-1", "on", 1920)) + + assert_trigger(mocker, module, 0, LEFT_MOUSE, "xrandr --output eDP-1-1 --off") + assert_trigger(mocker, module, 0, RIGHT_MOUSE, "xrandr --output eDP-1-1 --off") + assert_trigger(mocker, module, 1, LEFT_MOUSE, "xrandr --output HDMI-1-1 --off") + assert_trigger(mocker, module, 1, RIGHT_MOUSE, "xrandr --output HDMI-1-1 --off") + + +def test_display_off(mocker): + xrandr_cli = mock_xrandr(mocker, TRUNCATED_OUTPUT_DISPLAY_OFF) + module = Module(Config([]), theme=None) + module.update() + xrandr_cli.assert_called_once_with("xrandr -q") + + assert_widgets(module, ("eDP-1-1", "on", 0), ("HDMI-1-1", "off", sys.maxsize)) + + assert_trigger(mocker, module, 0, LEFT_MOUSE, None) + assert_trigger(mocker, module, 0, RIGHT_MOUSE, None) + assert_trigger(mocker, module, 1, LEFT_MOUSE, "xrandr --output HDMI-1-1 --auto --left-of eDP-1-1") + assert_trigger(mocker, module, 1, RIGHT_MOUSE, "xrandr --output HDMI-1-1 --auto --right-of eDP-1-1") + + +def test_no_autoupdate(mocker): + xrandr_cli = mock_xrandr(mocker, FULL_OUTPUT_TWO_DISPLAYS) + module = Module(Config(["-p", "xrandr.autoupdate=false"]), theme=None) + module.update() + xrandr_cli.assert_called_once_with("xrandr -q") + + assert_widgets( + module, ("eDP-1-1", "on", 0), ("HDMI-1-1", "on", 1920), (None, "refresh", None) + ) + + assert_trigger(mocker, module, 0, LEFT_MOUSE, "xrandr --output eDP-1-1 --off") + assert_trigger(mocker, module, 0, RIGHT_MOUSE, "xrandr --output eDP-1-1 --off") + assert_trigger(mocker, module, 1, LEFT_MOUSE, "xrandr --output HDMI-1-1 --off") + assert_trigger(mocker, module, 1, RIGHT_MOUSE, "xrandr --output HDMI-1-1 --off") + + +def test_exclude(mocker): + xrandr_cli = mock_xrandr(mocker, FULL_OUTPUT_TWO_DISPLAYS) + module = Module(Config(["-p", "xrandr.exclude=eDP"]), theme=None) + module.update() + xrandr_cli.assert_called_once_with("xrandr -q") + + assert_widgets(module, ("HDMI-1-1", "on", 1920)) + + assert_trigger(mocker, module, 0, LEFT_MOUSE, "xrandr --output HDMI-1-1 --off") + assert_trigger(mocker, module, 0, RIGHT_MOUSE, "xrandr --output HDMI-1-1 --off") + + +def test_exclude_off(mocker): + xrandr_cli = mock_xrandr(mocker, TRUNCATED_OUTPUT_DISPLAY_OFF) + module = Module(Config(["-p", "xrandr.exclude=eDP"]), theme=None) + module.update() + xrandr_cli.assert_called_once_with("xrandr -q") + + assert_widgets(module, ("HDMI-1-1", "off", sys.maxsize)) + + assert_trigger(mocker, module, 0, LEFT_MOUSE, "xrandr --output HDMI-1-1 --auto --left-of eDP-1-1") + assert_trigger(mocker, module, 0, RIGHT_MOUSE, "xrandr --output HDMI-1-1 --auto --right-of eDP-1-1") + + +# xrandr sample data + +FULL_OUTPUT_TWO_DISPLAYS = """Screen 0: minimum 8 x 8, current 4480 x 1440, maximum 32767 x 32767 +eDP-1-1 connected primary 1920x1080+0+0 344mm x 193mm + 1920x1080 60.00*+ 59.93 48.00 + 1680x1050 59.95 59.88 + 1600x1024 60.17 + 1400x1050 59.98 + 1280x1024 60.02 + 1440x900 59.89 + 1280x960 60.00 + 1360x768 59.80 59.96 + 1152x864 60.00 + 1024x768 60.04 60.00 + 960x720 60.00 + 928x696 60.05 + 896x672 60.01 + 960x600 60.00 + 960x540 59.99 + 800x600 60.00 60.32 56.25 + 840x525 60.01 59.88 + 800x512 60.17 + 700x525 59.98 + 640x512 60.02 + 720x450 59.89 + 640x480 60.00 59.94 + 680x384 59.80 59.96 + 576x432 60.06 + 512x384 60.00 + 400x300 60.32 56.34 + 320x240 60.05 +HDMI-1-1 connected 2560x1440+1920+0 596mm x 335mm + 2560x1440 59.95*+ + 1920x1080 60.00 50.00 59.94 + 1920x1080i 60.00 50.00 59.94 + 1680x1050 59.88 + 1600x900 60.00 + 1280x1024 60.02 + 1280x800 59.91 + 1280x720 60.00 50.00 59.94 + 1024x768 60.00 + 800x600 60.32 + 720x576 50.00 + 720x576i 50.00 + 720x480 60.00 59.94 + 720x480i 60.00 59.94 + 640x480 60.00 59.94 +""" + + +TRUNCATED_OUTPUT_DISPLAY_OFF = """eDP-1-1 connected primary 1920x1080+0+0 344mm x 193mm + 1920x1080 60.00*+ 59.93 48.00 +HDMI-1-1 connected + 2560x1440 59.95 + +""" From 22d85d6d1d1a4942bbb1001eba76c1ccaeb9fadc Mon Sep 17 00:00:00 2001 From: Naya Verdier Date: Sat, 25 Jul 2020 11:33:42 -0700 Subject: [PATCH 084/506] [xrandr-test] Catch all exceptions if i3 Subscription fails --- bumblebee_status/modules/core/xrandr.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bumblebee_status/modules/core/xrandr.py b/bumblebee_status/modules/core/xrandr.py index a3fe1b7..7060cf1 100644 --- a/bumblebee_status/modules/core/xrandr.py +++ b/bumblebee_status/modules/core/xrandr.py @@ -47,7 +47,7 @@ class Module(core.module.Module): try: i3.Subscription(self._output_update, "output") - except NameError: + except Exception: pass def _output_update(self, event, data, _): @@ -121,4 +121,5 @@ class Module(core.module.Module): self._refresh(event) + # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From 647ad44b31111391ea60ca997dfa174b484f4b67 Mon Sep 17 00:00:00 2001 From: Naya Verdier Date: Sat, 25 Jul 2020 11:50:13 -0700 Subject: [PATCH 085/506] ModuleNotFoundError incompatible with python 3.4, 3.5 --- bumblebee_status/modules/core/xrandr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bumblebee_status/modules/core/xrandr.py b/bumblebee_status/modules/core/xrandr.py index 7060cf1..59d37f0 100644 --- a/bumblebee_status/modules/core/xrandr.py +++ b/bumblebee_status/modules/core/xrandr.py @@ -31,7 +31,7 @@ import util.format try: import i3 -except ModuleNotFoundError: +except Exception: pass From b1adc382aa6e4465cd8341cf1457d07380c2a01d Mon Sep 17 00:00:00 2001 From: Naya Verdier Date: Sat, 25 Jul 2020 12:38:28 -0700 Subject: [PATCH 086/506] [vault] Turns out, is triggered when going into a submenu --- bumblebee_status/modules/core/vault.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bumblebee_status/modules/core/vault.py b/bumblebee_status/modules/core/vault.py index ee4fc7f..7f3fb75 100644 --- a/bumblebee_status/modules/core/vault.py +++ b/bumblebee_status/modules/core/vault.py @@ -12,7 +12,6 @@ Parameters: * vault.location: Location of the password store (defaults to ~/.password-store) * vault.offx: x-axis offset of popup menu (defaults to 0) * vault.offy: y-axis offset of popup menu (defaults to 0) - * vault.leave_menu: Boolean flag to close menu when the mouse leaves (defaults to False) * vault.text: Text to display on the widget (defaults to ) Many thanks to `bbernhard `_ for the idea! @@ -74,7 +73,7 @@ class Module(core.module.Module): core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.popup) def popup(self, widget): - menu = util.popup.menu(leave=util.format.asbool(self.parameter("leave_menu", False))) + menu = util.popup.menu(leave=False) build_menu(menu, self.__path, self.__callback) menu.show(widget, offset_x=self.__offx, offset_y=self.__offy) From 06d6739da42c964f17f25e54c4986cabd03a554c Mon Sep 17 00:00:00 2001 From: Naya Verdier Date: Sun, 26 Jul 2020 13:21:49 -0700 Subject: [PATCH 087/506] [xrandr] Add autotoggle behavior to xrandr Also makes i3 subscription events and widget click live-update --- bumblebee_status/modules/core/xrandr.py | 154 ++++++++++++++------ tests/modules/core/test_xrandr.py | 185 ++++++++++++++++++++---- 2 files changed, 264 insertions(+), 75 deletions(-) diff --git a/bumblebee_status/modules/core/xrandr.py b/bumblebee_status/modules/core/xrandr.py index 59d37f0..699599f 100644 --- a/bumblebee_status/modules/core/xrandr.py +++ b/bumblebee_status/modules/core/xrandr.py @@ -9,6 +9,8 @@ Parameters: * xrandr.autoupdate: If set to 'false', does *not* invoke xrandr automatically. Instead, the module will only refresh when displays are enabled or disabled (defaults to true) * xrandr.exclude: Comma-separated list of display name prefixes to exclude + * xrandr.autotoggle: Boolean flag to automatically enable new displays (defaults to false) + * xrandr.autotoggle_side: Which side to put autotoggled displays on ('right' or 'left', defaults to 'right') Requires the following python module: * (optional) i3 - if present, the need for updating the widget list is auto-detected @@ -35,91 +37,147 @@ except Exception: pass +RESOLUTION_REGEX = re.compile(r"\d+x\d+\+(\d+)\+\d+") + + +class DisplayInfo: + def __init__(self, name, resolution, connected, added, removed): + self.name = name + self.active = resolution is not None + self.connected = connected + self.added = added + self.removed = removed + + self.position = int(resolution.group(1)) if self.active else sys.maxsize + self.state = "on" if self.active else "off" + + def __str__(self): + return "DisplayInfo(name={}, active={}, connected={}, added={}, removed={}, position={}, state={})".format( + self.name, + self.active, + self.connected, + self.added, + self.removed, + self.position, + self.state, + ) + + def __repr__(self): + return str(self) + + class Module(core.module.Module): @core.decorators.every(seconds=5) # takes up to 5s to detect a new screen def __init__(self, config, theme): super().__init__(config, theme, []) - self._exclude = tuple(filter(len, self.parameter("exclude", "").split(","))) - self._active_displays = [] + self._exclude = tuple(util.format.aslist(self.parameter("exclude"))) self._autoupdate = util.format.asbool(self.parameter("autoupdate", True)) - self._needs_update = True + self._autotoggle = util.format.asbool(self.parameter("autotoggle", False)) + self._autotoggle_side = self.parameter("autotoggle_side", "right") + + self._connected_displays = [] + self._active_displays = [] + self._initialized = False try: i3.Subscription(self._output_update, "output") except Exception: pass - def _output_update(self, event, data, _): - self._needs_update = True + def _output_update(self, *_): + self.update(force=True) - def update(self): - if not self._autoupdate and not self._needs_update: + def _query_displays(self): + displays = [] + + for line in util.cli.execute("xrandr -q").split("\n"): + # disconnected or connected + if "connected" not in line: + continue + + name = line.split(" ", 2)[0] + resolution = RESOLUTION_REGEX.search(line) + active = resolution is not None + + connected = "disconnected" not in line + added = connected and not active and name not in self._connected_displays + removed = not connected and active and name in self._active_displays + + displays.append(DisplayInfo(name, resolution, connected, added, removed)) + + self._connected_displays = [ + display.name for display in displays if display.connected + ] + self._active_displays = [display.name for display in displays if display.active] + + return displays + + def update(self, force=False): + if not (self._autoupdate or force or not self._initialized): return self.clear_widgets() - self._active_displays.clear() - self._needs_update = False - - for line in util.cli.execute("xrandr -q").split("\n"): - if " connected" not in line: + for display in self._query_displays(): + if display.name.startswith(self._exclude): continue - display = line.split(" ", 2)[0] - resolution = re.search(r"\d+x\d+\+(\d+)\+\d+", line) + if self._initialized and self._autotoggle: + if display.added: + self._enable_display(display.name, self._autotoggle_side) + elif display.removed: + self._disable_display(display.name) - if resolution: - self._active_displays.append(display) - - if display.startswith(self._exclude): + if not display.connected: continue - widget = self.widget(display) - if not widget: - widget = self.add_widget(full_text=display, name=display) - core.input.register(widget, button=1, cmd=self._toggle) - core.input.register(widget, button=3, cmd=self._toggle) - widget.set("state", "on" if resolution else "off") - widget.set("pos", int(resolution.group(1)) if resolution else sys.maxsize) + widget = self.add_widget(full_text=display.name, name=display.name) + core.input.register(widget, button=1, cmd=self._toggle) + core.input.register(widget, button=3, cmd=self._toggle) + + widget.set("state", display.state) + widget.set("pos", display.position) if not self._autoupdate: widget = self.add_widget(full_text="") widget.set("state", "refresh") - core.input.register(widget, button=1, cmd=self._refresh) + core.input.register(widget, button=1, cmd=self.update) + + self._initialized = True def state(self, widget): return widget.get("state", "off") - def _refresh(self, event): - self._needs_update = True + def _toggle_cmd(self): + if util.format.asbool(self.parameter("overwrite_i3config", False)): + return utility("toggle-display.sh") + else: + return "xrandr" + + def _disable_display(self, name): + if len(self._active_displays) > 1: + util.cli.execute("{} --output {} --off".format(self._toggle_cmd(), name)) + + def _enable_display(self, name, side=None): + # TODO: is there ever a case when there isn't a neighbor? + command = "{} --output {} --auto".format(self._toggle_cmd(), name) + if side and self._active_displays: + neighbor_index = 0 if side == "left" else -1 + command += " --{}-of {}".format(side, self._active_displays[neighbor_index]) + + util.cli.execute(command) def _toggle(self, event): - if util.format.asbool(self.parameter("overwrite_i3config", False)): - toggle_cmd = utility("toggle-display.sh") - else: - toggle_cmd = "xrandr" - widget = self.widget(widget_id=event["instance"]) if widget.get("state") == "on": - if len(self._active_displays) > 1: - util.cli.execute("{} --output {} --off".format(toggle_cmd, widget.name)) - elif not self._active_displays: - util.cli.execute("{} --output {} --auto".format(toggle_cmd, widget.name)) + self._disable_display(widget.name) else: - if event["button"] == core.input.LEFT_MOUSE: - side, neighbor = "left", self._active_displays[0] - else: - side, neighbor = "right", self._active_displays[-1] + side = "left" if event["button"] == core.input.LEFT_MOUSE else "right" + self._enable_display(widget.name, side) - util.cli.execute( - "{} --output {} --auto --{}-of {}".format( - toggle_cmd, widget.name, side, neighbor, - ) - ) - - self._refresh(event) + self.update(force=True) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/modules/core/test_xrandr.py b/tests/modules/core/test_xrandr.py index b4e6156..c2d5dca 100644 --- a/tests/modules/core/test_xrandr.py +++ b/tests/modules/core/test_xrandr.py @@ -21,47 +21,50 @@ def assert_widgets(module, *expected_widgets): assert widget.get("pos") == pos -def assert_trigger(mocker, module, widget_index, button, expected_command): - xrandr_cli = mock_xrandr(mocker, "") +def assert_trigger(xrandr_cli, module, widget_index, button, expected_command): + xrandr_cli.reset_mock() + widget = module.widgets()[widget_index] trigger({"button": button, "instance": widget.id, "name": module.id}) if expected_command is None: - xrandr_cli.assert_not_called() + xrandr_cli.assert_called_once_with("xrandr -q") else: - xrandr_cli.assert_called_once_with(expected_command) + assert xrandr_cli.call_count == 2 + xrandr_cli.assert_any_call(expected_command) + xrandr_cli.assert_called_with("xrandr -q") def test_autoupdate(mocker): - xrandr_cli = mock_xrandr(mocker, FULL_OUTPUT_TWO_DISPLAYS) + xrandr_cli = mock_xrandr(mocker, HDMI_CONNECTED_ACTIVE) module = Module(Config([]), theme=None) module.update() xrandr_cli.assert_called_once_with("xrandr -q") assert_widgets(module, ("eDP-1-1", "on", 0), ("HDMI-1-1", "on", 1920)) - assert_trigger(mocker, module, 0, LEFT_MOUSE, "xrandr --output eDP-1-1 --off") - assert_trigger(mocker, module, 0, RIGHT_MOUSE, "xrandr --output eDP-1-1 --off") - assert_trigger(mocker, module, 1, LEFT_MOUSE, "xrandr --output HDMI-1-1 --off") - assert_trigger(mocker, module, 1, RIGHT_MOUSE, "xrandr --output HDMI-1-1 --off") + assert_trigger(xrandr_cli, module, 0, LEFT_MOUSE, "xrandr --output eDP-1-1 --off") + assert_trigger(xrandr_cli, module, 0, RIGHT_MOUSE, "xrandr --output eDP-1-1 --off") + assert_trigger(xrandr_cli, module, 1, LEFT_MOUSE, "xrandr --output HDMI-1-1 --off") + assert_trigger(xrandr_cli, module, 1, RIGHT_MOUSE, "xrandr --output HDMI-1-1 --off") def test_display_off(mocker): - xrandr_cli = mock_xrandr(mocker, TRUNCATED_OUTPUT_DISPLAY_OFF) + xrandr_cli = mock_xrandr(mocker, HDMI_CONNECTED_INACTIVE) module = Module(Config([]), theme=None) module.update() xrandr_cli.assert_called_once_with("xrandr -q") assert_widgets(module, ("eDP-1-1", "on", 0), ("HDMI-1-1", "off", sys.maxsize)) - assert_trigger(mocker, module, 0, LEFT_MOUSE, None) - assert_trigger(mocker, module, 0, RIGHT_MOUSE, None) - assert_trigger(mocker, module, 1, LEFT_MOUSE, "xrandr --output HDMI-1-1 --auto --left-of eDP-1-1") - assert_trigger(mocker, module, 1, RIGHT_MOUSE, "xrandr --output HDMI-1-1 --auto --right-of eDP-1-1") + assert_trigger(xrandr_cli, module, 0, LEFT_MOUSE, None) + assert_trigger(xrandr_cli, module, 0, RIGHT_MOUSE, None) + assert_trigger(xrandr_cli, module, 1, LEFT_MOUSE, "xrandr --output HDMI-1-1 --auto --left-of eDP-1-1") + assert_trigger(xrandr_cli, module, 1, RIGHT_MOUSE, "xrandr --output HDMI-1-1 --auto --right-of eDP-1-1") def test_no_autoupdate(mocker): - xrandr_cli = mock_xrandr(mocker, FULL_OUTPUT_TWO_DISPLAYS) + xrandr_cli = mock_xrandr(mocker, HDMI_CONNECTED_ACTIVE) module = Module(Config(["-p", "xrandr.autoupdate=false"]), theme=None) module.update() xrandr_cli.assert_called_once_with("xrandr -q") @@ -70,39 +73,151 @@ def test_no_autoupdate(mocker): module, ("eDP-1-1", "on", 0), ("HDMI-1-1", "on", 1920), (None, "refresh", None) ) - assert_trigger(mocker, module, 0, LEFT_MOUSE, "xrandr --output eDP-1-1 --off") - assert_trigger(mocker, module, 0, RIGHT_MOUSE, "xrandr --output eDP-1-1 --off") - assert_trigger(mocker, module, 1, LEFT_MOUSE, "xrandr --output HDMI-1-1 --off") - assert_trigger(mocker, module, 1, RIGHT_MOUSE, "xrandr --output HDMI-1-1 --off") + assert_trigger(xrandr_cli, module, 0, LEFT_MOUSE, "xrandr --output eDP-1-1 --off") + assert_trigger(xrandr_cli, module, 0, RIGHT_MOUSE, "xrandr --output eDP-1-1 --off") + assert_trigger(xrandr_cli, module, 1, LEFT_MOUSE, "xrandr --output HDMI-1-1 --off") + assert_trigger(xrandr_cli, module, 1, RIGHT_MOUSE, "xrandr --output HDMI-1-1 --off") def test_exclude(mocker): - xrandr_cli = mock_xrandr(mocker, FULL_OUTPUT_TWO_DISPLAYS) + xrandr_cli = mock_xrandr(mocker, HDMI_CONNECTED_ACTIVE) module = Module(Config(["-p", "xrandr.exclude=eDP"]), theme=None) module.update() xrandr_cli.assert_called_once_with("xrandr -q") assert_widgets(module, ("HDMI-1-1", "on", 1920)) - assert_trigger(mocker, module, 0, LEFT_MOUSE, "xrandr --output HDMI-1-1 --off") - assert_trigger(mocker, module, 0, RIGHT_MOUSE, "xrandr --output HDMI-1-1 --off") + assert_trigger(xrandr_cli, module, 0, LEFT_MOUSE, "xrandr --output HDMI-1-1 --off") + assert_trigger(xrandr_cli, module, 0, RIGHT_MOUSE, "xrandr --output HDMI-1-1 --off") def test_exclude_off(mocker): - xrandr_cli = mock_xrandr(mocker, TRUNCATED_OUTPUT_DISPLAY_OFF) + xrandr_cli = mock_xrandr(mocker, HDMI_CONNECTED_INACTIVE) module = Module(Config(["-p", "xrandr.exclude=eDP"]), theme=None) module.update() xrandr_cli.assert_called_once_with("xrandr -q") assert_widgets(module, ("HDMI-1-1", "off", sys.maxsize)) - assert_trigger(mocker, module, 0, LEFT_MOUSE, "xrandr --output HDMI-1-1 --auto --left-of eDP-1-1") - assert_trigger(mocker, module, 0, RIGHT_MOUSE, "xrandr --output HDMI-1-1 --auto --right-of eDP-1-1") + assert_trigger(xrandr_cli, module, 0, LEFT_MOUSE, "xrandr --output HDMI-1-1 --auto --left-of eDP-1-1") + assert_trigger(xrandr_cli, module, 0, RIGHT_MOUSE, "xrandr --output HDMI-1-1 --auto --right-of eDP-1-1") + + +def test_autotoggle_excluded_active_disconnected(mocker): + xrandr_cli = mock_xrandr(mocker, HDMI_CONNECTED_ACTIVE) + module = Module(Config(["-p", "xrandr.autotoggle=true", "xrandr.exclude=HDMI"]), theme=None) + module.update() + xrandr_cli.assert_called_once_with("xrandr -q") + + assert_widgets(module, ("eDP-1-1", "on", 0)) + + xrandr_cli.return_value = HDMI_DISCONNECTED_ACTIVE + xrandr_cli.reset_mock() + + module.update() + xrandr_cli.assert_called_once_with("xrandr -q") + + +def test_autotoggle_excluded_inactive_connected(mocker): + xrandr_cli = mock_xrandr(mocker, HDMI_DISCONNECTED_INACTIVE) + module = Module(Config(["-p", "xrandr.autotoggle=true", "xrandr.exclude=HDMI"]), theme=None) + module.update() + xrandr_cli.assert_called_once_with("xrandr -q") + + assert_widgets(module, ("eDP-1-1", "on", 0)) + + xrandr_cli.return_value = HDMI_CONNECTED_INACTIVE + xrandr_cli.reset_mock() + + module.update() + xrandr_cli.assert_called_once_with("xrandr -q") + + +def test_autotoggle_active_disconnected(mocker): + xrandr_cli = mock_xrandr(mocker, HDMI_CONNECTED_ACTIVE) + module = Module(Config(["-p", "xrandr.autotoggle=true"]), theme=None) + module.update() + xrandr_cli.assert_called_once_with("xrandr -q") + + assert_widgets(module, ("eDP-1-1", "on", 0), ("HDMI-1-1", "on", 1920)) + + xrandr_cli.return_value = HDMI_DISCONNECTED_ACTIVE + xrandr_cli.reset_mock() + + module.update() + assert xrandr_cli.call_count == 2 + xrandr_cli.assert_any_call("xrandr -q") + xrandr_cli.assert_called_with("xrandr --output HDMI-1-1 --off") + + +def test_autotoggle_inactive_disconnected(mocker): + xrandr_cli = mock_xrandr(mocker, HDMI_CONNECTED_INACTIVE) + module = Module(Config(["-p", "xrandr.autotoggle=true"]), theme=None) + module.update() + xrandr_cli.assert_called_once_with("xrandr -q") + + assert_widgets(module, ("eDP-1-1", "on", 0), ("HDMI-1-1", "off", sys.maxsize)) + + xrandr_cli.return_value = HDMI_DISCONNECTED_INACTIVE + xrandr_cli.reset_mock() + + module.update() + xrandr_cli.assert_called_once_with("xrandr -q") + + +def test_autotoggle_active_connected(mocker): + xrandr_cli = mock_xrandr(mocker, HDMI_DISCONNECTED_ACTIVE) + module = Module(Config(["-p", "xrandr.autotoggle=true"]), theme=None) + module.update() + xrandr_cli.assert_called_once_with("xrandr -q") + + assert_widgets(module, ("eDP-1-1", "on", 0)) + + xrandr_cli.return_value = HDMI_CONNECTED_ACTIVE + xrandr_cli.reset_mock() + + module.update() + xrandr_cli.assert_called_once_with("xrandr -q") + + +def test_autotoggle_inactive_connected(mocker): + xrandr_cli = mock_xrandr(mocker, HDMI_DISCONNECTED_INACTIVE) + module = Module(Config(["-p", "xrandr.autotoggle=true"]), theme=None) + module.update() + xrandr_cli.assert_called_once_with("xrandr -q") + + assert_widgets(module, ("eDP-1-1", "on", 0)) + + xrandr_cli.return_value = HDMI_CONNECTED_INACTIVE + xrandr_cli.reset_mock() + + module.update() + assert xrandr_cli.call_count == 2 + xrandr_cli.assert_any_call("xrandr -q") + xrandr_cli.assert_called_with("xrandr --output HDMI-1-1 --auto --right-of eDP-1-1") + + +def test_autotoggle_left(mocker): + xrandr_cli = mock_xrandr(mocker, HDMI_DISCONNECTED_INACTIVE) + module = Module(Config(["-p", "xrandr.autotoggle=true", "xrandr.autotoggle_side=left"]), theme=None) + module.update() + xrandr_cli.assert_called_once_with("xrandr -q") + + assert_widgets(module, ("eDP-1-1", "on", 0)) + + xrandr_cli.return_value = HDMI_CONNECTED_INACTIVE + xrandr_cli.reset_mock() + + module.update() + assert xrandr_cli.call_count == 2 + xrandr_cli.assert_any_call("xrandr -q") + xrandr_cli.assert_called_with("xrandr --output HDMI-1-1 --auto --left-of eDP-1-1") # xrandr sample data -FULL_OUTPUT_TWO_DISPLAYS = """Screen 0: minimum 8 x 8, current 4480 x 1440, maximum 32767 x 32767 +HDMI_CONNECTED_ACTIVE = """ +Screen 0: minimum 8 x 8, current 4480 x 1440, maximum 32767 x 32767 eDP-1-1 connected primary 1920x1080+0+0 344mm x 193mm 1920x1080 60.00*+ 59.93 48.00 1680x1050 59.95 59.88 @@ -150,8 +265,24 @@ HDMI-1-1 connected 2560x1440+1920+0 596mm x 335mm """ -TRUNCATED_OUTPUT_DISPLAY_OFF = """eDP-1-1 connected primary 1920x1080+0+0 344mm x 193mm +HDMI_CONNECTED_INACTIVE = """ +eDP-1-1 connected primary 1920x1080+0+0 344mm x 193mm 1920x1080 60.00*+ 59.93 48.00 HDMI-1-1 connected 2560x1440 59.95 + """ + +HDMI_DISCONNECTED_ACTIVE = """ +eDP-1-1 connected primary 1920x1080+0+0 344mm x 193mm + 1920x1080 60.00*+ 59.93 48.00 +HDMI-1-1 disconnected 2560x1440+1920+0 0mm x 0mm + 2560x1440 (0x6b) 241.500MHz +HSync +VSync + h: width 2560 start 2608 end 2640 total 2720 skew 0 clock 88.79KHz + v: height 1440 start 1442 end 1447 total 1481 clock 59.95Hz +""" + +HDMI_DISCONNECTED_INACTIVE = """ +eDP-1-1 connected primary 1920x1080+0+0 344mm x 193mm + 1920x1080 60.00*+ 59.93 48.00 +HDMI-1-1 disconnected +""" From 06afb03807b1b3d376cb182b9b9ae01ee834010d Mon Sep 17 00:00:00 2001 From: Naya Verdier Date: Sun, 26 Jul 2020 13:29:14 -0700 Subject: [PATCH 088/506] [xrandr] add tests for dis/connecting without autotoggle --- tests/modules/core/test_xrandr.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/tests/modules/core/test_xrandr.py b/tests/modules/core/test_xrandr.py index c2d5dca..fb1acb4 100644 --- a/tests/modules/core/test_xrandr.py +++ b/tests/modules/core/test_xrandr.py @@ -103,14 +103,29 @@ def test_exclude_off(mocker): assert_trigger(xrandr_cli, module, 0, RIGHT_MOUSE, "xrandr --output HDMI-1-1 --auto --right-of eDP-1-1") -def test_autotoggle_excluded_active_disconnected(mocker): - xrandr_cli = mock_xrandr(mocker, HDMI_CONNECTED_ACTIVE) - module = Module(Config(["-p", "xrandr.autotoggle=true", "xrandr.exclude=HDMI"]), theme=None) +def test_no_autotoggle_inactive_connected(mocker): + xrandr_cli = mock_xrandr(mocker, HDMI_DISCONNECTED_INACTIVE) + module = Module(Config([]), theme=None) module.update() xrandr_cli.assert_called_once_with("xrandr -q") assert_widgets(module, ("eDP-1-1", "on", 0)) + xrandr_cli.return_value = HDMI_CONNECTED_INACTIVE + xrandr_cli.reset_mock() + + module.update() + xrandr_cli.assert_called_once_with("xrandr -q") + + +def test_no_autotoggle_active_disconnected(mocker): + xrandr_cli = mock_xrandr(mocker, HDMI_CONNECTED_ACTIVE) + module = Module(Config([]), theme=None) + module.update() + xrandr_cli.assert_called_once_with("xrandr -q") + + assert_widgets(module, ("eDP-1-1", "on", 0), ("HDMI-1-1", "on", 1920)) + xrandr_cli.return_value = HDMI_DISCONNECTED_ACTIVE xrandr_cli.reset_mock() From 13a851a6360b45049867f97fc517238d35d94515 Mon Sep 17 00:00:00 2001 From: Naya Verdier Date: Sun, 26 Jul 2020 15:52:47 -0700 Subject: [PATCH 089/506] [xrandr-tests] Test disconnecting excluded active display --- tests/modules/core/test_xrandr.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/modules/core/test_xrandr.py b/tests/modules/core/test_xrandr.py index fb1acb4..d005baa 100644 --- a/tests/modules/core/test_xrandr.py +++ b/tests/modules/core/test_xrandr.py @@ -148,6 +148,21 @@ def test_autotoggle_excluded_inactive_connected(mocker): xrandr_cli.assert_called_once_with("xrandr -q") +def test_autotoggle_excluded_active_disconnected(mocker): + xrandr_cli = mock_xrandr(mocker, HDMI_CONNECTED_ACTIVE) + module = Module(Config(["-p", "xrandr.autotoggle=true", "xrandr.exclude=HDMI"]), theme=None) + module.update() + xrandr_cli.assert_called_once_with("xrandr -q") + + assert_widgets(module, ("eDP-1-1", "on", 0)) + + xrandr_cli.return_value = HDMI_DISCONNECTED_ACTIVE + xrandr_cli.reset_mock() + + module.update() + xrandr_cli.assert_called_once_with("xrandr -q") + + def test_autotoggle_active_disconnected(mocker): xrandr_cli = mock_xrandr(mocker, HDMI_CONNECTED_ACTIVE) module = Module(Config(["-p", "xrandr.autotoggle=true"]), theme=None) From 72d255f0ae4ad50da15a6d07687f4ffc19617dad Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Mon, 27 Jul 2020 07:04:47 +0200 Subject: [PATCH 090/506] [doc] document additional config file parameters see #678 --- docs/features.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/features.rst b/docs/features.rst index 162f3c3..f050a2b 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -110,6 +110,10 @@ An example: Configuration files ------------------- +Using a configuration file, it is possible to define a list of modules +that will be loaded if no modules are specified on the CLI, as well as +defining a default theme to use. + Any parameter that can be specified using ``-p =`` on the commandline, can alternatively be specified in one of the following configuration files: - ~/.bumblebee-status.conf - @@ -122,6 +126,10 @@ Configuration files have the following format: :: + [core] + modules = + theme = + [module-parameters] = @@ -131,3 +139,5 @@ For example: [module-parameters] github.token=abcdefabcdef12345 + + From 5de616ff893b92823d207723d3154a176eaa5c09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Lindahl=20Fl=C3=A5ten?= Date: Fri, 31 Jul 2020 15:26:49 +0200 Subject: [PATCH 091/506] [modules/contrib/stock] handle urllib request exception Handle exception that is raised when e.g. your network connection is down. --- bumblebee_status/modules/contrib/stock.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bumblebee_status/modules/contrib/stock.py b/bumblebee_status/modules/contrib/stock.py index c8bfb5e..36afe17 100644 --- a/bumblebee_status/modules/contrib/stock.py +++ b/bumblebee_status/modules/contrib/stock.py @@ -56,7 +56,11 @@ class Module(core.module.Module): self.__symbols + "&fields=regularMarketPrice,currency,regularMarketChange" ) - return urllib.request.urlopen(url).read().strip() + try: + return urllib.request.urlopen(url).read().strip() + except urllib.request.URLError: + logging.error("unable to open stock exchange url") + return None else: logging.error("unable to retrieve stock exchange rate") return None From 5959d73cde8f8533577c7d4ecd5a4910e109f57a Mon Sep 17 00:00:00 2001 From: Alexander Scheid-Rehder Date: Thu, 6 Aug 2020 14:49:41 +0200 Subject: [PATCH 092/506] Fix wrong usage of asbool in layout-xkb --- bumblebee_status/modules/core/layout-xkb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bumblebee_status/modules/core/layout-xkb.py b/bumblebee_status/modules/core/layout-xkb.py index 4d1dc7a..909017d 100644 --- a/bumblebee_status/modules/core/layout-xkb.py +++ b/bumblebee_status/modules/core/layout-xkb.py @@ -53,7 +53,7 @@ class Module(core.module.Module): log.debug("group num: {}".format(xkb.group_num)) name = ( xkb.group_name - if util.format.asbool(self.parameter("showname"), False) + if util.format.asbool(self.parameter("showname", False)) else xkb.group_symbol ) if self.__show_variant: From c77f3aa3bc806828c2aeaabfeffa72943cf44af7 Mon Sep 17 00:00:00 2001 From: Joshua Barrass Date: Tue, 11 Aug 2020 00:45:13 +0100 Subject: [PATCH 093/506] Force update using USR1 signal --- bumblebee-status | 8 ++++++++ bumblebee_status/core/output.py | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/bumblebee-status b/bumblebee-status index e4f5982..e02dfee 100755 --- a/bumblebee-status +++ b/bumblebee-status @@ -4,6 +4,7 @@ import os import sys import json import time +import signal import socket import select import logging @@ -103,6 +104,12 @@ def main(): input_thread.daemon = True input_thread.start() + def sig_USR1_handler(signum,stack): + if update_lock.acquire(blocking=False) == True: + core.event.trigger("update", redraw_only=False) + core.event.trigger("draw") + update_lock.release() + if config.debug(): modules.append(core.module.load("debug", config, theme)) @@ -120,6 +127,7 @@ def main(): core.event.trigger("start") started = True + signal.signal(10, sig_USR1_handler) while True: if update_lock.acquire(blocking=False) == True: core.event.trigger("update") diff --git a/bumblebee_status/core/output.py b/bumblebee_status/core/output.py index 088678d..ed36fcd 100644 --- a/bumblebee_status/core/output.py +++ b/bumblebee_status/core/output.py @@ -222,13 +222,14 @@ class i3(object): return blocks # TODO: only updates full text, not the state!? + # can this TODO be removed now? Will update the state if not redraw_only def update(self, affected_modules=None, redraw_only=False): now = time.time() for module in self.__modules: if affected_modules and not module.id in affected_modules: continue if not affected_modules and module.next_update: - if now < module.next_update: + if now < module.next_update and redraw_only: continue if not redraw_only: From 9e04e0a27b60a35f62d751d0c8c1a2b6f16c26c6 Mon Sep 17 00:00:00 2001 From: Joshua Barrass Date: Tue, 11 Aug 2020 00:54:35 +0100 Subject: [PATCH 094/506] Quotes around the deadbeef string to prevent parsing errors by deadbeef --- bumblebee_status/modules/contrib/deadbeef.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bumblebee_status/modules/contrib/deadbeef.py b/bumblebee_status/modules/contrib/deadbeef.py index 327972d..948fdc5 100644 --- a/bumblebee_status/modules/contrib/deadbeef.py +++ b/bumblebee_status/modules/contrib/deadbeef.py @@ -114,7 +114,7 @@ class Module(core.module.Module): self._song = "" return ## perform the actual query -- these can be much more sophisticated - data = util.cli.execute(self.now_playing_tf + self._tf_format) + data = util.cli.execute(self.now_playing_tf + '"'+self._tf_format+'"') self._song = data def update_standard(self, widgets): From 9b5477675cf1e344881f01d6c45490e566d359a4 Mon Sep 17 00:00:00 2001 From: Cristian Miranda Date: Thu, 20 Aug 2020 10:47:56 -0300 Subject: [PATCH 095/506] [github]: Remove error printing if exception caught I experienced that when an exception is caught and we print it I get an ugly error on the whole bar making it unusable. This fixes that problem. --- bumblebee_status/modules/contrib/github.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bumblebee_status/modules/contrib/github.py b/bumblebee_status/modules/contrib/github.py index abcdfa5..07b8ea0 100644 --- a/bumblebee_status/modules/contrib/github.py +++ b/bumblebee_status/modules/contrib/github.py @@ -83,7 +83,6 @@ class Module(core.module.Module): self.__label += "/".join(counts) except Exception as err: - print(err) self.__label = "n/a" def __getUnreadNotificationsCountByReason(self, notifications, reason): From db41792afb43583ef129871f361b0ba5c4b7fa87 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Thu, 27 Aug 2020 20:06:33 +0200 Subject: [PATCH 096/506] [dnf] simplify threading use framework threading to simplify the dnf module see #692 --- bumblebee_status/modules/contrib/dnf.py | 75 +++++++++++-------------- 1 file changed, 33 insertions(+), 42 deletions(-) diff --git a/bumblebee_status/modules/contrib/dnf.py b/bumblebee_status/modules/contrib/dnf.py index 3f5e484..21c9245 100644 --- a/bumblebee_status/modules/contrib/dnf.py +++ b/bumblebee_status/modules/contrib/dnf.py @@ -5,13 +5,8 @@ Requires the following executable: * dnf -Parameters: - * dnf.interval: Time in minutes between two consecutive update checks (defaults to 30 minutes) - """ -import threading - import core.event import core.module import core.widget @@ -20,46 +15,13 @@ import core.decorators import util.cli -def get_dnf_info(widget): - res = util.cli.execute("dnf updateinfo", ignore_errors=True) - - security = 0 - bugfixes = 0 - enhancements = 0 - other = 0 - for line in res.split("\n"): - if not line.startswith(" "): - continue - elif "ecurity" in line: - for s in line.split(): - if s.isdigit(): - security += int(s) - elif "ugfix" in line: - for s in line.split(): - if s.isdigit(): - bugfixes += int(s) - elif "hancement" in line: - for s in line.split(): - if s.isdigit(): - enhancements += int(s) - else: - for s in line.split(): - if s.isdigit(): - other += int(s) - - widget.set("security", security) - widget.set("bugfixes", bugfixes) - widget.set("enhancements", enhancements) - widget.set("other", other) - - core.event.trigger("update", [widget.module.id], redraw_only=True) - - class Module(core.module.Module): @core.decorators.every(minutes=30) def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.updates)) + self.background = True + def updates(self, widget): result = [] for t in ["security", "bugfixes", "enhancements", "other"]: @@ -67,8 +29,37 @@ class Module(core.module.Module): return "/".join(result) def update(self): - thread = threading.Thread(target=get_dnf_info, args=(self.widget(),)) - thread.start() + res = util.cli.execute("dnf updateinfo", ignore_errors=True) + + security = 0 + bugfixes = 0 + enhancements = 0 + other = 0 + for line in res.split("\n"): + if not line.startswith(" "): + continue + elif "ecurity" in line: + for s in line.split(): + if s.isdigit(): + security += int(s) + elif "ugfix" in line: + for s in line.split(): + if s.isdigit(): + bugfixes += int(s) + elif "hancement" in line: + for s in line.split(): + if s.isdigit(): + enhancements += int(s) + else: + for s in line.split(): + if s.isdigit(): + other += int(s) + + widget.set("security", security) + widget.set("bugfixes", bugfixes) + widget.set("enhancements", enhancements) + widget.set("other", other) + def state(self, widget): cnt = 0 From 362d1a5f6f8229e3cd5a849cc474c48bbf52551f Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Fri, 28 Aug 2020 14:03:34 +0200 Subject: [PATCH 097/506] [modules/contrib/dnf] fix undefined "widget" error while refactoring, i overlooked that the variable "widget" doesn't exist anymore. see #692 --- bumblebee_status/modules/contrib/dnf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bumblebee_status/modules/contrib/dnf.py b/bumblebee_status/modules/contrib/dnf.py index 21c9245..f90ee84 100644 --- a/bumblebee_status/modules/contrib/dnf.py +++ b/bumblebee_status/modules/contrib/dnf.py @@ -29,6 +29,7 @@ class Module(core.module.Module): return "/".join(result) def update(self): + widget = self.widget() res = util.cli.execute("dnf updateinfo", ignore_errors=True) security = 0 From 905f71fa525c3e80b443c95f416adb874b95624c Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Fri, 28 Aug 2020 17:14:05 +0200 Subject: [PATCH 098/506] [core] fix broken "sparse" updates c77f3aa accidentially broke "sparse" updates (i.e. updates that do not trigger during each update interval). Introduce a new update parameter, "force", to model the use case "update everything on SIGUSR1". fixes #692 --- bumblebee-status | 2 +- bumblebee_status/core/output.py | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/bumblebee-status b/bumblebee-status index e02dfee..dda14e4 100755 --- a/bumblebee-status +++ b/bumblebee-status @@ -106,7 +106,7 @@ def main(): def sig_USR1_handler(signum,stack): if update_lock.acquire(blocking=False) == True: - core.event.trigger("update", redraw_only=False) + core.event.trigger("update", force=True) core.event.trigger("draw") update_lock.release() diff --git a/bumblebee_status/core/output.py b/bumblebee_status/core/output.py index ed36fcd..687cafd 100644 --- a/bumblebee_status/core/output.py +++ b/bumblebee_status/core/output.py @@ -221,15 +221,13 @@ class i3(object): core.event.trigger("next-widget") return blocks - # TODO: only updates full text, not the state!? - # can this TODO be removed now? Will update the state if not redraw_only - def update(self, affected_modules=None, redraw_only=False): + def update(self, affected_modules=None, redraw_only=False, force=False): now = time.time() for module in self.__modules: if affected_modules and not module.id in affected_modules: continue if not affected_modules and module.next_update: - if now < module.next_update and redraw_only: + if now < module.next_update and not force: continue if not redraw_only: From 2c8dafec701d80ddd9a3d1855a14a8eef0c44790 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thayn=C3=A3=20Moretti?= Date: Sat, 29 Aug 2020 20:17:39 -0300 Subject: [PATCH 099/506] Add Network Traffic module tests --- tests/modules/contrib/test_network_traffic.py | 54 +++++++++++++++++-- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/tests/modules/contrib/test_network_traffic.py b/tests/modules/contrib/test_network_traffic.py index 0dd52a9..256aac1 100644 --- a/tests/modules/contrib/test_network_traffic.py +++ b/tests/modules/contrib/test_network_traffic.py @@ -1,9 +1,57 @@ import pytest +from unittest import TestCase, mock + +import core.config +import core.widget +import modules.contrib.network_traffic + +from types import SimpleNamespace pytest.importorskip("psutil") - pytest.importorskip("netifaces") -def test_load_module(): - __import__("modules.contrib.network_traffic") +def io_counters_mock(recv, sent): + return { + 'lo': SimpleNamespace( + bytes_sent = sent, + bytes_recv = recv + ) + } + +def gateways_response(): + return { + 'default': { + 1: ('10.0.0.10', 'lo') + } + } + +def build_module(): + return modules.contrib.network_traffic.Module(config=core.config.Config([]), theme=None) + +class TestNetworkTrafficUnit(TestCase): + def test_load_module(self): + __import__("modules.contrib.network_traffic") + + @mock.patch('psutil.net_io_counters') + @mock.patch('netifaces.gateways') + @mock.patch('netifaces.AF_INET', 1) + def test_update_rates(self, gateways_mock, net_io_counters_mock): + net_io_counters_mock.return_value = io_counters_mock(0, 0) + gateways_mock.return_value = gateways_response() + + module = build_module() + + net_io_counters_mock.return_value = io_counters_mock(2842135, 1932215) + module.update() + + assert module.widgets()[1].full_text() == '1.84MiB/s' + assert module.widgets()[0].full_text() == '2.71MiB/s' + + def test_initial_download_rate(self): + module = build_module() + assert module.widgets()[0].full_text() == '0.00B/s' + + def test_initial_upload_rate(self): + module = build_module() + assert module.widgets()[1].full_text() == '0.00B/s' From dff187252a44a6d44cbae4a26e3aec26ef51f800 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thayn=C3=A3=20Moretti?= Date: Sat, 29 Aug 2020 21:09:59 -0300 Subject: [PATCH 100/506] Fix RECV/SENT start values --- bumblebee_status/modules/contrib/network_traffic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bumblebee_status/modules/contrib/network_traffic.py b/bumblebee_status/modules/contrib/network_traffic.py index bd1a9b6..f9c8c98 100644 --- a/bumblebee_status/modules/contrib/network_traffic.py +++ b/bumblebee_status/modules/contrib/network_traffic.py @@ -38,8 +38,8 @@ class Module(core.module.Module): try: self._bandwidth = BandwidthInfo() - self._rate_recv = "?" - self._rate_sent = "?" + self._rate_recv = 0 + self._rate_sent = 0 self._bytes_recv = self._bandwidth.bytes_recv() self._bytes_sent = self._bandwidth.bytes_sent() except Exception: From 22ddcf42bddca59f2028ed61665594eafadad746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thayn=C3=A3=20Moretti?= Date: Sat, 29 Aug 2020 21:49:41 -0300 Subject: [PATCH 101/506] Improve tests --- tests/modules/contrib/test_network_traffic.py | 60 +++++++++++++++---- 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/tests/modules/contrib/test_network_traffic.py b/tests/modules/contrib/test_network_traffic.py index 256aac1..1abae72 100644 --- a/tests/modules/contrib/test_network_traffic.py +++ b/tests/modules/contrib/test_network_traffic.py @@ -26,12 +26,48 @@ def gateways_response(): } def build_module(): - return modules.contrib.network_traffic.Module(config=core.config.Config([]), theme=None) + config = core.config.Config([]) + return modules.contrib.network_traffic.Module(config=config, theme=None) + +def download_widget(module): + return module.widgets()[0] + +def upload_widget(module): + return module.widgets()[1] + +def mb_to_bytes(value): + return value*1024**2 class TestNetworkTrafficUnit(TestCase): def test_load_module(self): __import__("modules.contrib.network_traffic") + def test_initial_download_rate(self): + module = build_module() + assert download_widget(module).full_text() == '0.00B/s' + + def test_initial_upload_rate(self): + module = build_module() + assert upload_widget(module).full_text() == '0.00B/s' + + @mock.patch('netifaces.gateways') + def test_invalid_gateways(self, gateways_mock): + gateways_mock.return_value = { 'invalid': 'gateways' } + + module = build_module() + + assert download_widget(module).full_text() == '0.00B/s' + assert upload_widget(module).full_text() == '0.00B/s' + + @mock.patch('psutil.net_io_counters') + def test_invalid_io_counters(self, net_io_counters_mock): + net_io_counters_mock.return_value = { 'invalid': 'io_counters' } + + module = build_module() + + assert download_widget(module).full_text() == '0.00B/s' + assert upload_widget(module).full_text() == '0.00B/s' + @mock.patch('psutil.net_io_counters') @mock.patch('netifaces.gateways') @mock.patch('netifaces.AF_INET', 1) @@ -41,17 +77,15 @@ class TestNetworkTrafficUnit(TestCase): module = build_module() - net_io_counters_mock.return_value = io_counters_mock(2842135, 1932215) + assert download_widget(module).full_text() == '0.00B/s' + assert upload_widget(module).full_text() == '0.00B/s' + + net_io_counters_mock.return_value = io_counters_mock( + mb_to_bytes(30), + mb_to_bytes(0.5) + ) + module.update() - assert module.widgets()[1].full_text() == '1.84MiB/s' - assert module.widgets()[0].full_text() == '2.71MiB/s' - - def test_initial_download_rate(self): - module = build_module() - assert module.widgets()[0].full_text() == '0.00B/s' - - def test_initial_upload_rate(self): - module = build_module() - assert module.widgets()[1].full_text() == '0.00B/s' - + assert download_widget(module).full_text() == '30.00MiB/s' + assert upload_widget(module).full_text() == '512.00KiB/s' From c339a163655cd35316f7b44982237c53ca603608 Mon Sep 17 00:00:00 2001 From: Bernhard B Date: Sun, 30 Aug 2020 09:03:34 +0200 Subject: [PATCH 102/506] small improvements in octoprint plugin * octoprint sometimes returns additional information a the 3d printer is offline. so, it's better to check if the octoprint job state starts with "Offline". * in case the returned job state is really long, truncate it. --- bumblebee_status/modules/contrib/octoprint.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/bumblebee_status/modules/contrib/octoprint.py b/bumblebee_status/modules/contrib/octoprint.py index a324af2..a7ed8ac 100644 --- a/bumblebee_status/modules/contrib/octoprint.py +++ b/bumblebee_status/modules/contrib/octoprint.py @@ -85,8 +85,15 @@ class Module(core.module.Module): core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.__show_popup) def octoprint_status(self, widget): - if self.__octoprint_state == "Offline" or self.__octoprint_state == "Unknown": - return self.__octoprint_state + if ( + self.__octoprint_state.startswith("Offline") + or self.__octoprint_state == "Unknown" + ): + return ( + (self.__octoprint_state[:25] + "...") + if len(self.__octoprint_state) > 25 + else self.__octoprint_state + ) return ( self.__octoprint_state + " | B: " From 945838dc748a5bd9f499812a369a0d3a95f5f413 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thayn=C3=A3=20Moretti?= Date: Sun, 30 Aug 2020 11:15:23 -0300 Subject: [PATCH 103/506] Create memory module tests --- tests/modules/core/test_memory.py | 120 +++++++++++++++++++++++++++++- 1 file changed, 118 insertions(+), 2 deletions(-) diff --git a/tests/modules/core/test_memory.py b/tests/modules/core/test_memory.py index 2b87b8a..b133c52 100644 --- a/tests/modules/core/test_memory.py +++ b/tests/modules/core/test_memory.py @@ -1,5 +1,121 @@ import pytest +from unittest import TestCase, mock -def test_load_module(): - __import__("modules.core.memory") +import core.config +import core.widget +import modules.core.memory +def build_module(args = []): + config = core.config.Config(args) + return modules.core.memory.Module(config=config, theme=None) + +def memory_widget(module): + return module.widgets()[0] + +def meminfo_mock( + total, + available, + free = 0, + buffers = 0, + cached = 0, + slab = 0 +): + data = [] + states = [ + ('MemTotal', total), + ('MemAvailable', available), + ('MemFree', free), + ('Buffers', buffers), + ('Cached', cached), + ('Slab', slab) + ] + + + for i, (key, value) in enumerate(states): + data.append('{}: {} kB'.format(key, value)) + + return '\n'.join(data) + +class TestMemory(TestCase): + def test_load_module(self): + __import__("modules.core.memory") + + @mock.patch('builtins.open', mock.mock_open(read_data=meminfo_mock(2048, 1024))) + def test_default_healthy_state(self): + module = build_module() + module.update() + + widget = memory_widget(module) + + assert widget.full_text() == '1.00MiB/2.00MiB (50.00%)' + assert module.state(widget) == None + + @mock.patch('builtins.open', mock.mock_open(read_data=meminfo_mock(8196, 1024))) + def test_default_warning_state(self): + module = build_module() + module.update() + + widget = memory_widget(module) + + assert widget.full_text() == '7.00MiB/8.00MiB (87.51%)' + assert module.state(widget) == 'warning' + + @mock.patch('builtins.open', mock.mock_open(read_data=meminfo_mock(2048, 0))) + def test_default_critical_state(self): + module = build_module() + module.update() + + widget = memory_widget(module) + + assert widget.full_text() == '2.00MiB/2.00MiB (100.00%)' + assert module.state(widget) == 'critical' + + @mock.patch('builtins.open', mock.mock_open(read_data=meminfo_mock(4096, 3068))) + def test_custom_warning_parameter(self): + module = build_module(['-p', 'memory.warning=20']) + module.update() + + widget = memory_widget(module) + + assert widget.full_text() == '1.00MiB/4.00MiB (25.10%)' + assert module.state(widget) == 'warning' + + @mock.patch('builtins.open', mock.mock_open(read_data=meminfo_mock(8196, 4096))) + def test_custom_critical_parameter(self): + module = build_module(['-p', 'memory.critical=50']) + module.update() + + widget = memory_widget(module) + + assert widget.full_text() == '4.00MiB/8.00MiB (50.02%)' + assert module.state(widget) == 'critical' + + @mock.patch('builtins.open', mock.mock_open(read_data=meminfo_mock(2048, 1024))) + def test_usedonly_parameter(self): + module = build_module(['-p', 'memory.usedonly=true']) + module.update() + + widget = memory_widget(module) + + assert widget.full_text() == '1.00MiB' + assert module.state(widget) == None + + @mock.patch('builtins.open', mock.mock_open(read_data=meminfo_mock(2048, 1024))) + def test_format_parameter(self): + module = build_module(['-p', 'memory.format={used}.{total}']) + module.update() + + widget = memory_widget(module) + + assert widget.full_text() == '1.00MiB.2.00MiB' + assert module.state(widget) == None + + @mock.patch('builtins.open', mock.mock_open(read_data=meminfo_mock(2048, 1024))) + def test_format_parameter_with_percent(self): + module = build_module(['-p', 'memory.format={percent}%']) + module.update() + + widget = memory_widget(module) + + assert widget.full_text() == '50.0%' + assert module.state(widget) == None From 820598b1b8c7433332584616074ff87e506c67e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thayn=C3=A3=20Moretti?= Date: Sun, 30 Aug 2020 12:11:48 -0300 Subject: [PATCH 104/506] Fix memory module tests --- bumblebee_status/modules/core/memory.py | 35 ++++++++++++++++--------- tests/modules/core/test_memory.py | 1 - 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/bumblebee_status/modules/core/memory.py b/bumblebee_status/modules/core/memory.py index 82de769..9b4a3ab 100644 --- a/bumblebee_status/modules/core/memory.py +++ b/bumblebee_status/modules/core/memory.py @@ -41,18 +41,8 @@ class Module(core.module.Module): return self._format.format(**self._mem) def update(self): - data = {} - with open("/proc/meminfo", "r") as f: - for line in f: - tmp = re.split(r"[:\s]+", line) - value = int(tmp[1]) - if tmp[2] == "kB": - value = value * 1024 - if tmp[2] == "mB": - value = value * 1024 * 1024 - if tmp[2] == "gB": - value = value * 1024 * 1024 * 1024 - data[tmp[0]] = value + data = self.__parse_meminfo() + if "MemAvailable" in data: used = data["MemTotal"] - data["MemAvailable"] else: @@ -78,5 +68,26 @@ class Module(core.module.Module): return "warning" return None + def __parse_meminfo(self): + data = {} + with open("/proc/meminfo", "r") as f: + # https://bugs.python.org/issue32933 + while True: + line = f.readline() + + if line == '': + break + + tmp = re.split(r"[:\s]+", line) + value = int(tmp[1]) + if tmp[2] == "kB": + value = value * 1024 + if tmp[2] == "mB": + value = value * 1024 * 1024 + if tmp[2] == "gB": + value = value * 1024 * 1024 * 1024 + data[tmp[0]] = value + + return data # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/modules/core/test_memory.py b/tests/modules/core/test_memory.py index b133c52..9f52032 100644 --- a/tests/modules/core/test_memory.py +++ b/tests/modules/core/test_memory.py @@ -30,7 +30,6 @@ def meminfo_mock( ('Slab', slab) ] - for i, (key, value) in enumerate(states): data.append('{}: {} kB'.format(key, value)) From 6f6f3cedd930103f9f4bfd8b740dce43ce80422d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thayn=C3=A3=20Moretti?= Date: Sun, 30 Aug 2020 12:28:48 -0300 Subject: [PATCH 105/506] Improve meminfo parse logic --- bumblebee_status/modules/core/memory.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/bumblebee_status/modules/core/memory.py b/bumblebee_status/modules/core/memory.py index 9b4a3ab..a4015ff 100644 --- a/bumblebee_status/modules/core/memory.py +++ b/bumblebee_status/modules/core/memory.py @@ -72,12 +72,7 @@ class Module(core.module.Module): data = {} with open("/proc/meminfo", "r") as f: # https://bugs.python.org/issue32933 - while True: - line = f.readline() - - if line == '': - break - + for line in f.readlines(): tmp = re.split(r"[:\s]+", line) value = int(tmp[1]) if tmp[2] == "kB": From 49de0e520b8c879111096c00d1c7abba77ea1fee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thayn=C3=A3=20Moretti?= Date: Sun, 30 Aug 2020 12:37:58 -0300 Subject: [PATCH 106/506] Reduce code cognitive complexity --- bumblebee_status/modules/core/memory.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/bumblebee_status/modules/core/memory.py b/bumblebee_status/modules/core/memory.py index a4015ff..c9ecf44 100644 --- a/bumblebee_status/modules/core/memory.py +++ b/bumblebee_status/modules/core/memory.py @@ -74,15 +74,22 @@ class Module(core.module.Module): # https://bugs.python.org/issue32933 for line in f.readlines(): tmp = re.split(r"[:\s]+", line) - value = int(tmp[1]) - if tmp[2] == "kB": - value = value * 1024 - if tmp[2] == "mB": - value = value * 1024 * 1024 - if tmp[2] == "gB": - value = value * 1024 * 1024 * 1024 + value = self.__parse_value(tmp) + data[tmp[0]] = value return data + def __parse_value(self, data): + value = int(data[1]) + + if data[2] == "kB": + value = value * 1024 + if data[2] == "mB": + value = value * 1024 * 1024 + if data[2] == "gB": + value = value * 1024 * 1024 * 1024 + + return value + # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From 0c8d682d62b1305df5643e12cabfc50ea5ed8a94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thayn=C3=A3=20Moretti?= Date: Sun, 30 Aug 2020 12:49:33 -0300 Subject: [PATCH 107/506] Add unit tests --- tests/modules/core/test_memory.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/tests/modules/core/test_memory.py b/tests/modules/core/test_memory.py index 9f52032..4787395 100644 --- a/tests/modules/core/test_memory.py +++ b/tests/modules/core/test_memory.py @@ -15,6 +15,7 @@ def memory_widget(module): def meminfo_mock( total, available, + unit = 'kB', free = 0, buffers = 0, cached = 0, @@ -31,7 +32,7 @@ def meminfo_mock( ] for i, (key, value) in enumerate(states): - data.append('{}: {} kB'.format(key, value)) + data.append('{}: {} {}'.format(key, value, unit)) return '\n'.join(data) @@ -118,3 +119,25 @@ class TestMemory(TestCase): assert widget.full_text() == '50.0%' assert module.state(widget) == None + + + @mock.patch('builtins.open', mock.mock_open(read_data=meminfo_mock(8196, 4096, 'mB'))) + def test_mb_unit(self): + module = build_module() + module.update() + + widget = memory_widget(module) + + assert widget.full_text() == '4.00GiB/8.00GiB (50.02%)' + assert module.state(widget) == None + + @mock.patch('builtins.open', mock.mock_open(read_data=meminfo_mock(2, 1, 'gB'))) + def test_gb_unit(self): + module = build_module() + module.update() + + widget = memory_widget(module) + + assert widget.full_text() == '1.00GiB/2.00GiB (50.00%)' + assert module.state(widget) == None + From f22095e978f7147baf9199b7809fe4ef814a4fe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thayn=C3=A3=20Moretti?= Date: Mon, 31 Aug 2020 17:48:19 -0300 Subject: [PATCH 108/506] Add datetime module tests --- tests/modules/core/test_datetime.py | 56 +++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/tests/modules/core/test_datetime.py b/tests/modules/core/test_datetime.py index b8384d9..f99d552 100644 --- a/tests/modules/core/test_datetime.py +++ b/tests/modules/core/test_datetime.py @@ -1,7 +1,59 @@ import pytest +from unittest import mock, TestCase + +import datetime +from freezegun import freeze_time + +import core.config +import core.input +import core.widget +import modules.core.datetime pytest.importorskip("datetime") -def test_load_module(): - __import__("modules.core.datetime") +def build_module(args = []): + config = core.config.Config(args) + return modules.core.datetime.Module(config=config, theme=None) + +def build_widget(): + return core.widget.Widget() + +class DatetimeTest(TestCase): + def setup_class(self): + locale_patcher = mock.patch('locale.getdefaultlocale') + locale_mock = locale_patcher.start() + locale_mock.return_value = ('en-US', 'UTF-8') + + self.widget = build_widget() + + def test_load_module(self): + __import__("modules.core.datetime") + + @freeze_time('2020-10-15') + def test_default_format(self): + module = build_module() + assert module.full_text(self.widget) == '10/15/2020 12:00:00 AM' + + @freeze_time('2020-10-20') + def test_custom_format(self): + module = build_module(['-p', 'datetime.format=%Y.%m.%d']) + assert module.full_text(self.widget) == '2020.10.20' + + @freeze_time('2020-01-10 10:20:30') + @mock.patch('locale.getdefaultlocale') + def test_invalid_locale(self, locale_mock): + locale_mock.return_value = ('in-IN', 'UTF-0') + + module = build_module() + assert module.full_text(self.widget) == '01/10/2020 10:20:30 AM' + + @mock.patch('core.input.register') + def test_register_left_mouse_action(self, input_register_mock): + module = build_module() + + input_register_mock.assert_called_with( + module, + button=core.input.LEFT_MOUSE, + cmd='calendar' + ) From 0776592da66da7067237bab0cfb60bf234e0d3f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thayn=C3=A3=20Moretti?= Date: Tue, 1 Sep 2020 20:12:31 -0300 Subject: [PATCH 109/506] Add freezegun to Travis dependencies --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 400bd81..78aa402 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ before_install: - sudo apt-get -qq update install: - sudo apt-get install python-dbus - - pip install -U coverage==4.3 pytest pytest-mock + - pip install -U coverage==4.3 pytest pytest-mock freezegun - pip install codeclimate-test-reporter - pip install i3-py Pillow Babel DateTime python-dateutil - pip install docker feedparser i3ipc From b1501a815940b7bfa7129594a7aa6aa606cd445d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thayn=C3=A3=20Moretti?= Date: Tue, 1 Sep 2020 20:29:04 -0300 Subject: [PATCH 110/506] Remove useless import --- tests/modules/core/test_datetime.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/modules/core/test_datetime.py b/tests/modules/core/test_datetime.py index f99d552..33b7d58 100644 --- a/tests/modules/core/test_datetime.py +++ b/tests/modules/core/test_datetime.py @@ -1,7 +1,5 @@ import pytest from unittest import mock, TestCase - -import datetime from freezegun import freeze_time import core.config From 07f0b7e34ab1c2b1f800acb7014851d0c6f5ba3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thayn=C3=A3=20Moretti?= Date: Tue, 1 Sep 2020 20:54:10 -0300 Subject: [PATCH 111/506] Add CPU module tests --- tests/modules/core/test_cpu.py | 68 +++++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/tests/modules/core/test_cpu.py b/tests/modules/core/test_cpu.py index 9ea85ed..eca2362 100644 --- a/tests/modules/core/test_cpu.py +++ b/tests/modules/core/test_cpu.py @@ -1,7 +1,71 @@ import pytest +from unittest import TestCase, mock + +import core.config +import core.widget +import modules.core.cpu pytest.importorskip("psutil") -def test_load_module(): - __import__("modules.core.cpu") +def build_module(): + config = core.config.Config([]) + return modules.core.cpu.Module(config=config, theme=None) + +def cpu_widget(module): + return module.widgets()[0] + +class TestCPU(TestCase): + def test_load_module(self): + __import__("modules.core.cpu") + + @mock.patch('psutil.cpu_percent') + def test_cpu_percent(self, cpu_percent_mock): + cpu_percent_mock.return_value = 5 + module = build_module() + + assert cpu_widget(module).full_text() == '5.0%' + + @mock.patch('psutil.cpu_percent') + def test_cpu_percent_update(self, cpu_percent_mock): + cpu_percent_mock.return_value = 10 + module = build_module() + + assert cpu_widget(module).full_text() == '10.0%' + + cpu_percent_mock.return_value = 20 + module.update() + + assert cpu_widget(module).full_text() == '20.0%' + + @mock.patch('psutil.cpu_percent') + def test_healthy_state(self, cpu_percent_mock): + cpu_percent_mock.return_value = 50 + module = build_module() + + assert module.state(None) == None + + @mock.patch('psutil.cpu_percent') + def test_warning_state(self, cpu_percent_mock): + cpu_percent_mock.return_value = 75 + module = build_module() + + assert module.state(None) == 'warning' + + @mock.patch('psutil.cpu_percent') + def test_critical_state(self, cpu_percent_mock): + cpu_percent_mock.return_value = 82 + module = build_module() + + assert module.state(None) == 'critical' + + @mock.patch('core.input.register') + def test_register_left_mouse_action(self, input_register_mock): + module = build_module() + + input_register_mock.assert_called_with( + module, + button=core.input.LEFT_MOUSE, + cmd='gnome-system-monitor' + ) + From 032a651efaa5374d8a3d6f6af8c04806f2022106 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thayn=C3=A3=20Moretti?= Date: Tue, 1 Sep 2020 22:12:54 -0300 Subject: [PATCH 112/506] Improve network traffic module tests --- bumblebee_status/modules/contrib/network_traffic.py | 3 --- tests/modules/contrib/test_network_traffic.py | 12 ++++++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/bumblebee_status/modules/contrib/network_traffic.py b/bumblebee_status/modules/contrib/network_traffic.py index f9c8c98..ad5781f 100644 --- a/bumblebee_status/modules/contrib/network_traffic.py +++ b/bumblebee_status/modules/contrib/network_traffic.py @@ -97,9 +97,6 @@ class BandwidthInfo(object): """Return default active network adapter""" gateway = netifaces.gateways()["default"] - if not gateway: - raise "No default gateway found" - return gateway[netifaces.AF_INET][1] @classmethod diff --git a/tests/modules/contrib/test_network_traffic.py b/tests/modules/contrib/test_network_traffic.py index 1abae72..c18692e 100644 --- a/tests/modules/contrib/test_network_traffic.py +++ b/tests/modules/contrib/test_network_traffic.py @@ -89,3 +89,15 @@ class TestNetworkTrafficUnit(TestCase): assert download_widget(module).full_text() == '30.00MiB/s' assert upload_widget(module).full_text() == '512.00KiB/s' + + def test_widget_states(self): + module = build_module() + + assert module.state(download_widget(module)) == 'rx' + assert module.state(upload_widget(module)) == 'tx' + + def test_invalid_widget_state(self): + module = build_module() + invalid_widget = core.widget.Widget(name='invalid') + + assert module.state(invalid_widget) == None From 213c28992cebe03b3b0143b831c005d620503510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thayn=C3=A3=20Moretti?= Date: Tue, 1 Sep 2020 20:34:18 -0300 Subject: [PATCH 113/506] Add date tests --- tests/modules/core/test_date.py | 55 +++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/tests/modules/core/test_date.py b/tests/modules/core/test_date.py index b87e64b..4c4afc3 100644 --- a/tests/modules/core/test_date.py +++ b/tests/modules/core/test_date.py @@ -1,5 +1,56 @@ import pytest +from unittest import TestCase, mock +from freezegun import freeze_time + +import core.config +import core.input +import core.widget +import modules.core.date + +def build_module(args = []): + config = core.config.Config(args) + return modules.core.date.Module(config=config, theme=None) + +def build_widget(): + return core.widget.Widget() + +class DateTest(TestCase): + def setup_class(self): + locale_patcher = mock.patch('locale.getdefaultlocale') + locale_mock = locale_patcher.start() + locale_mock.return_value = ('en-US', 'UTF-8') + + self.widget = build_widget() + + def test_load_module(self): + __import__("modules.core.date") + + @freeze_time('2020-10-15 03:25:59') + def test_default_format(self): + module = build_module() + assert module.full_text(self.widget) == '10/15/2020' + + @freeze_time('2020-10-20 12:30:00') + def test_custom_format(self): + module = build_module(['-p', 'date.format=%d.%m.%y']) + assert module.full_text(self.widget) == '20.10.20' + + @freeze_time('2020-01-10 10:20:30') + @mock.patch('locale.getdefaultlocale') + def test_invalid_locale(self, locale_mock): + locale_mock.return_value = ('in-IN', 'UTF-0') + + module = build_module() + assert module.full_text(self.widget) == '01/10/2020' + + @mock.patch('core.input.register') + def test_register_left_mouse_action(self, input_register_mock): + module = build_module() + + input_register_mock.assert_called_with( + module, + button=core.input.LEFT_MOUSE, + cmd='calendar' + ) -def test_load_module(): - __import__("modules.core.date") From 04f6a6a08fa8e97018b12e827c951fae974d048b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thayn=C3=A3=20Moretti?= Date: Tue, 1 Sep 2020 21:49:51 -0300 Subject: [PATCH 114/506] Add time module tests --- tests/modules/core/test_date.py | 2 ++ tests/modules/core/test_time.py | 57 +++++++++++++++++++++++++++++++-- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/tests/modules/core/test_date.py b/tests/modules/core/test_date.py index 4c4afc3..9f5e888 100644 --- a/tests/modules/core/test_date.py +++ b/tests/modules/core/test_date.py @@ -7,6 +7,8 @@ import core.input import core.widget import modules.core.date +pytest.importorskip("datetime") + def build_module(args = []): config = core.config.Config(args) return modules.core.date.Module(config=config, theme=None) diff --git a/tests/modules/core/test_time.py b/tests/modules/core/test_time.py index 603a50d..0395e82 100644 --- a/tests/modules/core/test_time.py +++ b/tests/modules/core/test_time.py @@ -1,5 +1,58 @@ import pytest +from unittest import TestCase, mock +from freezegun import freeze_time + +import core.config +import core.input +import core.widget +import modules.core.time + +pytest.importorskip("datetime") + +def build_module(args = []): + config = core.config.Config(args) + return modules.core.time.Module(config=config, theme=None) + +def build_widget(): + return core.widget.Widget() + +class TimeTest(TestCase): + def setup_class(self): + locale_patcher = mock.patch('locale.getdefaultlocale') + locale_mock = locale_patcher.start() + locale_mock.return_value = ('en-US', 'UTF-8') + + self.widget = build_widget() + + def test_load_module(self): + __import__("modules.core.time") + + @freeze_time('2020-10-15 03:25:59') + def test_default_format(self): + module = build_module() + assert module.full_text(self.widget) == '03:25:59 AM' + + @freeze_time('2020-10-20 12:30:12') + def test_custom_format(self): + module = build_module(['-p', 'time.format=%H.%M.%S']) + assert module.full_text(self.widget) == '12.30.12' + + @freeze_time('2020-01-10 10:20:30') + @mock.patch('locale.getdefaultlocale') + def test_invalid_locale(self, locale_mock): + locale_mock.return_value = ('in-IN', 'UTF-0') + + module = build_module() + assert module.full_text(self.widget) == '10:20:30 AM' + + @mock.patch('core.input.register') + def test_register_left_mouse_action(self, input_register_mock): + module = build_module() + + input_register_mock.assert_called_with( + module, + button=core.input.LEFT_MOUSE, + cmd='calendar' + ) -def test_load_module(): - __import__("modules.core.time") From 6c0930cfae2885ab06ed3382cd7df82d8f677508 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thayn=C3=A3=20Moretti?= Date: Wed, 2 Sep 2020 14:33:07 -0300 Subject: [PATCH 115/506] Add load module tests --- bumblebee_status/modules/core/load.py | 1 + tests/modules/core/test_load.py | 128 +++++++++++++++++++++++++- 2 files changed, 127 insertions(+), 2 deletions(-) diff --git a/bumblebee_status/modules/core/load.py b/bumblebee_status/modules/core/load.py index 7800c88..ca7d26a 100644 --- a/bumblebee_status/modules/core/load.py +++ b/bumblebee_status/modules/core/load.py @@ -27,6 +27,7 @@ class Module(core.module.Module): self._cpus = multiprocessing.cpu_count() except NotImplementedError as e: self._cpus = 1 + core.input.register( self, button=core.input.LEFT_MOUSE, cmd="gnome-system-monitor" ) diff --git a/tests/modules/core/test_load.py b/tests/modules/core/test_load.py index 2210f9e..a80e6f6 100644 --- a/tests/modules/core/test_load.py +++ b/tests/modules/core/test_load.py @@ -1,7 +1,131 @@ import pytest +from unittest import TestCase, mock +import core.config +import core.widget +import modules.core.load + +pytest.importorskip("os") pytest.importorskip("multiprocessing") -def test_load_module(): - __import__("modules.core.load") +def build_module(): + config = core.config.Config([]) + return modules.core.load.Module(config=config, theme=None) + +def widget(module): + return module.widgets()[0] + +class TestLoad(TestCase): + def test_load_module(self): + __import__("modules.core.load") + + @mock.patch('multiprocessing.cpu_count') + @mock.patch('os.getloadavg') + def test_initial_values(self, load_avg_mock, cpu_count_mock): + cpu_count_mock.return_value = 1 + load_avg_mock.return_value = (0.10, 0.20, 0.30) + + module = build_module() + + assert widget(module).full_text() == '0.00/0.00/0.00' + + @mock.patch('multiprocessing.cpu_count') + @mock.patch('os.getloadavg') + def test_update_values(self, load_avg_mock, cpu_count_mock): + cpu_count_mock.return_value = 1 + load_avg_mock.return_value = (0.85, 0.95, 0.25) + + module = build_module() + module.update() + + assert widget(module).full_text() == '0.85/0.95/0.25' + + @mock.patch('multiprocessing.cpu_count') + @mock.patch('os.getloadavg') + def test_cpu_count_exception(self, load_avg_mock, cpu_count_mock): + cpu_count_mock.side_effect = NotImplementedError + load_avg_mock.return_value = (0.1, 0.2, 0.3) + + module = build_module() + module.update() + + assert widget(module).full_text() == '0.10/0.20/0.30' + + @mock.patch('multiprocessing.cpu_count') + @mock.patch('os.getloadavg') + def test_healthy_state(self, load_avg_mock, cpu_count_mock): + cpu_count_mock.return_value = 1 + load_avg_mock.return_value = (0.55, 0.95, 0.25) + + module = build_module() + module.update() + + assert module.state(widget(module)) == None + + @mock.patch('multiprocessing.cpu_count') + @mock.patch('os.getloadavg') + def test_warning_state(self, load_avg_mock, cpu_count_mock): + cpu_count_mock.return_value = 1 + load_avg_mock.return_value = (0.8, 0.85, 0.9) + + module = build_module() + module.update() + + assert module.state(widget(module)) == 'warning' + + @mock.patch('multiprocessing.cpu_count') + @mock.patch('os.getloadavg') + def test_critical_state(self, load_avg_mock, cpu_count_mock): + cpu_count_mock.return_value = 1 + load_avg_mock.return_value = (0.95, 0.85, 0.9) + + module = build_module() + module.update() + + assert module.state(widget(module)) == 'critical' + + @mock.patch('multiprocessing.cpu_count') + @mock.patch('os.getloadavg') + def test_healthy_state_with_8_cpus(self, load_avg_mock, cpu_count_mock): + cpu_count_mock.return_value = 8 + load_avg_mock.return_value = (4.42, 0.85, 0.9) + + module = build_module() + module.update() + + assert module.state(widget(module)) == None + + @mock.patch('multiprocessing.cpu_count') + @mock.patch('os.getloadavg') + def test_warning_state_with_8_cpus(self, load_avg_mock, cpu_count_mock): + cpu_count_mock.return_value = 8 + load_avg_mock.return_value = (5.65, 0.85, 0.9) + + module = build_module() + module.update() + + assert module.state(widget(module)) == 'warning' + + @mock.patch('multiprocessing.cpu_count') + @mock.patch('os.getloadavg') + def test_critical_state_with_8_cpus(self, load_avg_mock, cpu_count_mock): + cpu_count_mock.return_value = 8 + load_avg_mock.return_value = (6.45, 0.85, 0.9) + + module = build_module() + module.update() + + assert module.state(widget(module)) == 'critical' + + @mock.patch('multiprocessing.cpu_count') + @mock.patch('os.getloadavg') + @mock.patch('core.input.register') + def test_register_left_mouse_action(self, input_register_mock, load_avg_mock, cpu_count_mock): + module = build_module() + + input_register_mock.assert_called_with( + module, + button=core.input.LEFT_MOUSE, + cmd='gnome-system-monitor' + ) From 710a2cef98689500149dabfde62524247b4a76cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thayn=C3=A3=20Moretti?= Date: Wed, 2 Sep 2020 22:10:09 -0300 Subject: [PATCH 116/506] Create uptime and public ip tests --- tests/modules/contrib/test_publicip.py | 43 ++++++++++++++++++++++++-- tests/modules/contrib/test_uptime.py | 25 +++++++++++++-- 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/tests/modules/contrib/test_publicip.py b/tests/modules/contrib/test_publicip.py index 6c5a31a..891cfc8 100644 --- a/tests/modules/contrib/test_publicip.py +++ b/tests/modules/contrib/test_publicip.py @@ -1,5 +1,44 @@ import pytest +from unittest import TestCase, mock -def test_load_module(): - __import__("modules.contrib.publicip") +import core.config +import core.widget +import modules.contrib.publicip + +def build_module(): + config = core.config.Config([]) + return modules.contrib.publicip.Module(config=config, theme=None) + +def widget(module): + return module.widgets()[0] + +class PublicIPTest(TestCase): + def test_load_module(self): + __import__("modules.contrib.publicip") + + @mock.patch('util.location.public_ip') + def test_public_ip(self, public_ip_mock): + public_ip_mock.return_value = '5.12.220.2' + + module = build_module() + module.update() + + assert widget(module).full_text() == '5.12.220.2' + + @mock.patch('util.location.public_ip') + def test_public_ip_with_exception(self, public_ip_mock): + public_ip_mock.side_effect = Exception + + module = build_module() + module.update() + + assert widget(module).full_text() == 'n/a' + + @mock.patch('util.location.public_ip') + def test_interval_seconds(self, public_ip_mock): + public_ip_mock.side_effect = Exception + + module = build_module() + + assert module.parameter('interval') == 3600 diff --git a/tests/modules/contrib/test_uptime.py b/tests/modules/contrib/test_uptime.py index 67c791f..18a59d4 100644 --- a/tests/modules/contrib/test_uptime.py +++ b/tests/modules/contrib/test_uptime.py @@ -1,7 +1,26 @@ import pytest +from unittest import TestCase, mock -pytest.importorskip("datetime") +import core.config +import core.widget +import modules.contrib.uptime -def test_load_module(): - __import__("modules.contrib.uptime") +def build_module(): + config = core.config.Config([]) + return modules.contrib.uptime.Module(config=config, theme=None) + +def widget(module): + return module.widgets()[0] + +class UptimeTest(TestCase): + def test_load_module(self): + __import__("modules.contrib.uptime") + + @mock.patch('builtins.open', new_callable=mock.mock_open, read_data='300000 10.45') + def test_uptime(self, uptime_mock): + module = build_module() + module.update() + + uptime_mock.assert_called_with('/proc/uptime', 'r') + assert widget(module).full_text() == '3 days, 11:20:00' From e7b853c667b5785355214380954c83b843c46f05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thayn=C3=A3=20Moretti?= Date: Wed, 2 Sep 2020 22:16:58 -0300 Subject: [PATCH 117/506] Remove useless mock side effect --- tests/modules/contrib/test_publicip.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/modules/contrib/test_publicip.py b/tests/modules/contrib/test_publicip.py index 891cfc8..9584bd7 100644 --- a/tests/modules/contrib/test_publicip.py +++ b/tests/modules/contrib/test_publicip.py @@ -35,10 +35,7 @@ class PublicIPTest(TestCase): assert widget(module).full_text() == 'n/a' - @mock.patch('util.location.public_ip') - def test_interval_seconds(self, public_ip_mock): - public_ip_mock.side_effect = Exception - + def test_interval_seconds(self): module = build_module() assert module.parameter('interval') == 3600 From d568ef36228e074b75f277b0aab197b6c588877a Mon Sep 17 00:00:00 2001 From: Cristian Miranda Date: Fri, 4 Sep 2020 17:30:46 -0300 Subject: [PATCH 118/506] [modules/dunstctl]: Toggle dunst v1.5.0+ notifications using dunstctl --- bumblebee_status/modules/contrib/dunstctl.py | 42 ++++++++++++++++++++ themes/icons/awesome-fonts.json | 4 ++ 2 files changed, 46 insertions(+) create mode 100644 bumblebee_status/modules/contrib/dunstctl.py diff --git a/bumblebee_status/modules/contrib/dunstctl.py b/bumblebee_status/modules/contrib/dunstctl.py new file mode 100644 index 0000000..1b7649c --- /dev/null +++ b/bumblebee_status/modules/contrib/dunstctl.py @@ -0,0 +1,42 @@ +# pylint: disable=C0111,R0903 + +""" +Toggle dunst notifications using dunstctl. + +When notifications are paused using this module dunst doesn't get killed and you'll keep getting notifications on the background that will be displayed when unpausing. +This is specially useful if you're using dunst's scripting (https://wiki.archlinux.org/index.php/Dunst#Scripting), which requires dunst to be running. Scripts will be executed when dunst gets unpaused. + +Requires: + * dunst v1.5.0+ + +contributed by `cristianmiranda `_ - many thanks! +""" + +import core.module +import core.widget +import core.input + +import util.cli + + +class Module(core.module.Module): + def __init__(self, config, theme): + super().__init__(config, theme, core.widget.Widget("")) + self._paused = self.__isPaused() + core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.toggle_status) + + def toggle_status(self, event): + self._paused = self.__isPaused() + if self._paused: + util.cli.execute("dunstctl set-paused false") + else: + util.cli.execute("dunstctl set-paused true") + self._paused = not self._paused + + def __isPaused(self): + return util.cli.execute("dunstctl is-paused").strip() == "true" + + def state(self, widget): + if self._paused: + return ["muted", "warning"] + return ["unmuted"] diff --git a/themes/icons/awesome-fonts.json b/themes/icons/awesome-fonts.json index ca8d38f..963bcea 100644 --- a/themes/icons/awesome-fonts.json +++ b/themes/icons/awesome-fonts.json @@ -257,6 +257,10 @@ "muted": { "prefix": "" }, "unmuted": { "prefix": "" } }, + "dunstctl": { + "muted": { "prefix": "" }, + "unmuted": { "prefix": "" } + }, "twmn": { "muted": { "prefix": "" }, "unmuted": { "prefix": "" } From 100568206ad9a138fc9ef447a3341549d63d17a9 Mon Sep 17 00:00:00 2001 From: Cristian Miranda Date: Sat, 5 Sep 2020 18:05:16 -0300 Subject: [PATCH 119/506] [modules/thunderbird]: Thunderbird's unread email counts by inbox --- .../modules/contrib/thunderbird.py | 89 +++++++++++++++++++ themes/icons/awesome-fonts.json | 3 + 2 files changed, 92 insertions(+) create mode 100644 bumblebee_status/modules/contrib/thunderbird.py diff --git a/bumblebee_status/modules/contrib/thunderbird.py b/bumblebee_status/modules/contrib/thunderbird.py new file mode 100644 index 0000000..5240e6b --- /dev/null +++ b/bumblebee_status/modules/contrib/thunderbird.py @@ -0,0 +1,89 @@ +# pylint: disable=C0111,R0903 + +""" +Displays the unread emails count for one or more Thunderbird inboxes + +Parameters: + * thunderbird.home: Absolute path of your .thunderbird directory (e.g.: /home/pi/.thunderbird) + * thunderbird.inboxes: Comma separated values for all MSF inboxes and their parent directory (account) (e.g.: imap.gmail.com/INBOX.msf,outlook.office365.com/Work.msf) + +Tips: + * You can run the following command in order to list all your Thunderbird inboxes + + find ~/.thunderbird -name '*.msf' | awk -F '/' '{print $(NF-1)"/"$(NF)}' + +contributed by `cristianmiranda `_ - many thanks! +""" + +import core.module +import core.widget +import core.decorators +import core.input + +import util.cli + + +class Module(core.module.Module): + @core.decorators.every(minutes=1) + def __init__(self, config, theme): + super().__init__(config, theme, core.widget.Widget(self.thunderbird)) + + self.__total = 0 + self.__label = "" + self.__inboxes = [] + + self.__home = self.parameter("home", "") + inboxes = self.parameter("inboxes", "") + if inboxes: + self.__inboxes = util.format.aslist(inboxes) + + def thunderbird(self, _): + return str(self.__label) + + def update(self): + try: + self.__total = 0 + self.__label = "" + + stream = self.__getThunderbirdStream() + unread = self.__getUnreadMessagesByInbox(stream) + + counts = [] + for inbox in self.__inboxes: + count = unread[inbox] + self.__total += int(count) + counts.append(count) + + self.__label = "/".join(counts) + + except Exception as err: + self.__label = err + + def __getThunderbirdStream(self): + cmd = ( + "find " + + self.__home + + " -name '*.msf' -exec grep -REo 'A2=[0-9]' {} + | grep" + ) + for inbox in self.__inboxes: + cmd += " -e {}".format(inbox) + cmd += "| awk -F / '{print $(NF-1)\"/\"$(NF)}'" + + return util.cli.execute(cmd, shell=True).strip().split("\n") + + def __getUnreadMessagesByInbox(self, stream): + unread = {} + for line in stream: + entry = line.split(":A2=") + inbox = entry[0] + count = entry[1] + unread[inbox] = count + + return unread + + def state(self, widget): + if self.__total > 0: + return ["warning"] + + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/themes/icons/awesome-fonts.json b/themes/icons/awesome-fonts.json index ca8d38f..8a6f1f2 100644 --- a/themes/icons/awesome-fonts.json +++ b/themes/icons/awesome-fonts.json @@ -293,5 +293,8 @@ "speedtest": { "running": { "prefix": ["\uf251", "\uf252", "\uf253"] }, "not-running": { "prefix": "\uf144" } + }, + "thunderbird": { + "prefix": "" } } From 965e7b2453e7e3dcb460d1e328a25214ef8c254d Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sun, 6 Sep 2020 14:00:32 +0200 Subject: [PATCH 120/506] [modules/spotify] improve update mechanism instead of updating the widget list during each update, create the list of widgets during initialization, and later only update the widget states. see #702 --- bumblebee_status/modules/contrib/spotify.py | 70 +++++++++++---------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/bumblebee_status/modules/contrib/spotify.py b/bumblebee_status/modules/contrib/spotify.py index 85b8c31..3192088 100644 --- a/bumblebee_status/modules/contrib/spotify.py +++ b/bumblebee_status/modules/contrib/spotify.py @@ -25,7 +25,6 @@ import core.input import core.decorators import util.format - class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, []) @@ -43,6 +42,39 @@ class Module(core.module.Module): self.__cmd = "dbus-send --session --type=method_call --dest=org.mpris.MediaPlayer2.spotify \ /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player." + widget_map = {} + for widget_name in self.__layout: + widget = self.add_widget(name=widget_name) + if widget_name == "spotify.prev": + widget_map[widget] = { + "button": core.input.LEFT_MOUSE, + "cmd": self.__cmd + "Previous", + } + widget.set("state", "prev") + elif widget_name == "spotify.pause": + widget_map[widget] = { + "button": core.input.LEFT_MOUSE, + "cmd": self.__cmd + "PlayPause", + } + elif widget_name == "spotify.next": + widget_map[widget] = { + "button": core.input.LEFT_MOUSE, + "cmd": self.__cmd + "Next", + } + widget.set("state", "next") + elif widget_name == "spotify.song": + widget.set("state", "song") + widget.full_text(self.__song) + else: + raise KeyError( + "The spotify module does not have a {widget_name!r} widget".format( + widget_name=widget_name + ) + ) + for widget, callback_options in widget_map.items(): + core.input.register(widget, **callback_options) + + def hidden(self): return self.string_song == "" @@ -62,23 +94,10 @@ class Module(core.module.Module): def update(self): try: - self.clear_widgets() self.__get_song() - widget_map = {} - for widget_name in self.__layout: - widget = self.add_widget(name=widget_name) - if widget_name == "spotify.prev": - widget_map[widget] = { - "button": core.input.LEFT_MOUSE, - "cmd": self.__cmd + "Previous", - } - widget.set("state", "prev") - elif widget_name == "spotify.pause": - widget_map[widget] = { - "button": core.input.LEFT_MOUSE, - "cmd": self.__cmd + "PlayPause", - } + for widget in self.widgets(): + if widget.name == "spotify.pause": playback_status = str( dbus.Interface( dbus.SessionBus().get_object( @@ -92,25 +111,8 @@ class Module(core.module.Module): widget.set("state", "playing") else: widget.set("state", "paused") - elif widget_name == "spotify.next": - widget_map[widget] = { - "button": core.input.LEFT_MOUSE, - "cmd": self.__cmd + "Next", - } - widget.set("state", "next") - elif widget_name == "spotify.song": - widget.set("state", "song") - widget.full_text(self.__song) - else: - raise KeyError( - "The spotify module does not have a {widget_name!r} widget".format( - widget_name=widget_name - ) - ) - for widget, callback_options in widget_map.items(): - core.input.register(widget, **callback_options) - except Exception: + except Exception as e: self.__song = "" @property From 3a5365a2c3bde494c79595e19674c1760debaba6 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sun, 6 Sep 2020 14:09:18 +0200 Subject: [PATCH 121/506] [doc] add testing guidelines, general improvements --- docs/development/general.rst | 37 +++++++++--------------------------- docs/development/index.rst | 1 + docs/development/testing.rst | 33 ++++++++++++++++++++++++++++++++ docs/modules.rst | 36 ++++++++++++++++++++++++++++++++--- 4 files changed, 76 insertions(+), 31 deletions(-) create mode 100644 docs/development/testing.rst diff --git a/docs/development/general.rst b/docs/development/general.rst index 824d1a7..0ec89a0 100644 --- a/docs/development/general.rst +++ b/docs/development/general.rst @@ -1,34 +1,15 @@ General guidelines ================== -Writing unit tests ------------------- +Not much, right now. If you have an idea and some code, just +create a PR, I will gladly review and comment (and, likely, merge) -Some general hints: +Just one minor note: ``bumblebee-status`` is mostly a one-person, +spare-time project, so please be patient when answering an issue, +question or PR takes a while. -- Tests should run with just Python Standard Library modules installed - (i.e. if there are additional requirements, the test should be skipped - if those are missing) -- Tests should run even if there is no network connectivity (please mock - urllib calls, for example) -- Tests should be stable and not require modifications every time the - tested code's implementation changes slightly (been there / done that) +Also, the (small) community that has gathered around ``bumblebee-status`` +is extremely friendly and helpful, so don't hesitate to create issues +with questions, somebody will always come up with a useful answer. -Right now, ``bumblebee-status`` is moving away from Python's -built-in ``unittest`` framework (tests located inside ``tests/``) -and towards ``pytest`` (tests located inside ``pytests/``). - -First implication: To run the new tests, you need to have ``pytest`` -installed, it is not part of the Python Standard Library. Most -distributions call the package ``python-pytest`` or ``python3-pytest`` -or something similar (or you just use ``pip install --use pytest``) - -Aside from that, you just write your tests using ``pytest`` as usual, -with one big caveat: - -**If** you create a new directory inside ``pytests/``, you need to -also create a file called ``__init__.py`` inside that, otherwise, -modules won't load correctly. - -For examples, just browse the existing code. A good, minimal sample -for unit testing ``bumblebee-status`` is ``pytests/core/test_event.py``. +:) diff --git a/docs/development/index.rst b/docs/development/index.rst index 179dee0..6881fdb 100644 --- a/docs/development/index.rst +++ b/docs/development/index.rst @@ -8,4 +8,5 @@ Developer's Guide general module theme + testing diff --git a/docs/development/testing.rst b/docs/development/testing.rst new file mode 100644 index 0000000..c5c9375 --- /dev/null +++ b/docs/development/testing.rst @@ -0,0 +1,33 @@ +Testing guidelines +================== + +Writing unit tests +------------------ + +Some general hints: + +- Tests should run with just Python Standard Library modules installed + (i.e. if there are additional requirements, the test should be skipped + if those are missing) +- Tests should run even if there is no network connectivity (please mock + urllib calls, for example) +- Tests should be stable and not require modifications every time the + tested code's implementation changes slightly (been there / done that) + +Right now, ``bumblebee-status`` uses the ``pytest`` framework, and its +unit tests are located inside the ``tests/`` subdirectory. + +First implication: To run the new tests, you need to have ``pytest`` +installed, it is not part of the Python Standard Library. Most +distributions call the package ``python-pytest`` or ``python3-pytest`` +or something similar (or you just use ``pip install --use pytest``) + +Aside from that, you just write your tests using ``pytest`` as usual, +with one big caveat: + +**If** you create a new directory inside ``tests/``, you need to +also create a file called ``__init__.py`` inside that, otherwise, +modules won't load correctly. + +For examples, just browse the existing code. A good, minimal sample +for unit testing ``bumblebee-status`` is ``tests/core/test_event.py``. diff --git a/docs/modules.rst b/docs/modules.rst index 09879f7..18b3806 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -278,6 +278,7 @@ Parameters: * vault.location: Location of the password store (defaults to ~/.password-store) * vault.offx: x-axis offset of popup menu (defaults to 0) * vault.offy: y-axis offset of popup menu (defaults to 0) + * vault.text: Text to display on the widget (defaults to ) Many thanks to `bbernhard `_ for the idea! @@ -294,6 +295,9 @@ Parameters: and appending a file '~/.config/i3/config.' for every screen. * xrandr.autoupdate: If set to 'false', does *not* invoke xrandr automatically. Instead, the module will only refresh when displays are enabled or disabled (defaults to true) + * xrandr.exclude: Comma-separated list of display name prefixes to exclude + * xrandr.autotoggle: Boolean flag to automatically enable new displays (defaults to false) + * xrandr.autotoggle_side: Which side to put autotoggled displays on ('right' or 'left', defaults to 'right') Requires the following python module: * (optional) i3 - if present, the need for updating the widget list is auto-detected @@ -636,9 +640,6 @@ Displays DNF package update information (///`_ - many thanks! .. image:: ../screenshots/dunst.png +dunstctl +~~~~~~~~ + +Toggle dunst notifications using dunstctl. + +When notifications are paused using this module dunst doesn't get killed and you'll keep getting notifications on the background that will be displayed when unpausing. +This is specially useful if you're using dunst's scripting (https://wiki.archlinux.org/index.php/Dunst#Scripting), which requires dunst to be running. Scripts will be executed when dunst gets unpaused. + +Requires: + * dunst v1.5.0+ + +contributed by `cristianmiranda `_ - many thanks! + getcrypto ~~~~~~~~~ @@ -1303,6 +1317,22 @@ contributed by `chdorb `_ - many thanks! .. image:: ../screenshots/taskwarrior.png +thunderbird +~~~~~~~~~~~ + +Displays the unread emails count for one or more Thunderbird inboxes + +Parameters: + * thunderbird.home: Absolute path of your .thunderbird directory (e.g.: /home/pi/.thunderbird) + * thunderbird.inboxes: Comma separated values for all MSF inboxes and their parent directory (account) (e.g.: imap.gmail.com/INBOX.msf,outlook.office365.com/Work.msf) + +Tips: + * You can run the following command in order to list all your Thunderbird inboxes + + find ~/.thunderbird -name '*.msf' | awk -F '/' '{print $(NF-1)"/"$(NF)}' + +contributed by `cristianmiranda `_ - many thanks! + timetz ~~~~~~ From b79c19b616a0f42216ed69c57dd4097341166c4c Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sun, 6 Sep 2020 14:27:17 +0200 Subject: [PATCH 122/506] [modules/spotify] fix song not shown issue the previous commit accidentially removed the song display. re-add that and also add a bit of logging for troubleshooting. --- bumblebee_status/modules/contrib/spotify.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/bumblebee_status/modules/contrib/spotify.py b/bumblebee_status/modules/contrib/spotify.py index 3192088..c3f4d1f 100644 --- a/bumblebee_status/modules/contrib/spotify.py +++ b/bumblebee_status/modules/contrib/spotify.py @@ -25,6 +25,8 @@ import core.input import core.decorators import util.format +import logging + class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, []) @@ -63,8 +65,7 @@ class Module(core.module.Module): } widget.set("state", "next") elif widget_name == "spotify.song": - widget.set("state", "song") - widget.full_text(self.__song) + pass else: raise KeyError( "The spotify module does not have a {widget_name!r} widget".format( @@ -111,8 +112,12 @@ class Module(core.module.Module): widget.set("state", "playing") else: widget.set("state", "paused") + elif widget.name == "spotify.song": + widget.set("state", "song") + widget.full_text(self.__song) except Exception as e: + logging.exception(e) self.__song = "" @property From 070fe865ddd47607316afd0e0f38e5d4a0480a2c Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Wed, 9 Sep 2020 14:15:16 +0200 Subject: [PATCH 123/506] [modules/spotify] update in the background to rule out issues in the dbus communication, update the spotify module in the background. see #702 --- bumblebee_status/modules/contrib/spotify.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bumblebee_status/modules/contrib/spotify.py b/bumblebee_status/modules/contrib/spotify.py index c3f4d1f..078ccd2 100644 --- a/bumblebee_status/modules/contrib/spotify.py +++ b/bumblebee_status/modules/contrib/spotify.py @@ -31,6 +31,8 @@ class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, []) + self.background = True + self.__layout = util.format.aslist( self.parameter( "layout", "spotify.song,spotify.prev,spotify.pause,spotify.next", From 2390dfa946fa7b05ca44ecd678f0aeb737390533 Mon Sep 17 00:00:00 2001 From: Cristian Miranda Date: Thu, 10 Sep 2020 17:42:19 -0300 Subject: [PATCH 124/506] docs[dunstctl]: Added missing screenshot --- docs/modules.rst | 2 ++ screenshots/dunstctl.png | Bin 0 -> 37370 bytes 2 files changed, 2 insertions(+) create mode 100644 screenshots/dunstctl.png diff --git a/docs/modules.rst b/docs/modules.rst index 18b3806..72f59a5 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -674,6 +674,8 @@ Requires: contributed by `cristianmiranda `_ - many thanks! +.. image:: ../screenshots/dunstctl.png + getcrypto ~~~~~~~~~ diff --git a/screenshots/dunstctl.png b/screenshots/dunstctl.png new file mode 100644 index 0000000000000000000000000000000000000000..1aef636e15119df2b37c5a8270faad681268eb0c GIT binary patch literal 37370 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 literal 0 HcmV?d00001 From 4621de95b5507b99e6fa33057c2a5d30a433e32f Mon Sep 17 00:00:00 2001 From: Cristian Miranda Date: Thu, 10 Sep 2020 17:42:41 -0300 Subject: [PATCH 125/506] docs[thunderbird]: Added missing screenshot --- docs/modules.rst | 2 ++ screenshots/thunderbird.png | Bin 0 -> 12067 bytes 2 files changed, 2 insertions(+) create mode 100644 screenshots/thunderbird.png diff --git a/docs/modules.rst b/docs/modules.rst index 72f59a5..9319671 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -1335,6 +1335,8 @@ Tips: contributed by `cristianmiranda `_ - many thanks! +.. image:: ../screenshots/thunderbird.png + timetz ~~~~~~ diff --git a/screenshots/thunderbird.png b/screenshots/thunderbird.png new file mode 100644 index 0000000000000000000000000000000000000000..7baffd83af1aaef750953a36b7689fc9341caf00 GIT binary patch literal 12067 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 Date: Sun, 13 Sep 2020 11:16:57 +0200 Subject: [PATCH 126/506] [modules/spotify] make global dbus object instead of creating a new dbus instance during each update interval, reuse one dbus instance. see #702 --- bumblebee_status/modules/contrib/spotify.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bumblebee_status/modules/contrib/spotify.py b/bumblebee_status/modules/contrib/spotify.py index 078ccd2..25bff6f 100644 --- a/bumblebee_status/modules/contrib/spotify.py +++ b/bumblebee_status/modules/contrib/spotify.py @@ -39,6 +39,7 @@ class Module(core.module.Module): ) ) + self.__bus = dbus.SessionBus() self.__song = "" self.__pause = "" self.__format = self.parameter("format", "{artist} - {title}") @@ -82,7 +83,7 @@ class Module(core.module.Module): return self.string_song == "" def __get_song(self): - bus = dbus.SessionBus() + bus = self.__bus spotify = bus.get_object( "org.mpris.MediaPlayer2.spotify", "/org/mpris/MediaPlayer2" ) @@ -103,7 +104,7 @@ class Module(core.module.Module): if widget.name == "spotify.pause": playback_status = str( dbus.Interface( - dbus.SessionBus().get_object( + self.__bus.get_object( "org.mpris.MediaPlayer2.spotify", "/org/mpris/MediaPlayer2", ), From aa6238a5c6f5781ebd136810e115ba297ee15a23 Mon Sep 17 00:00:00 2001 From: Tobias Witek Date: Sun, 13 Sep 2020 11:19:05 +0200 Subject: [PATCH 127/506] [modules/spotify] properly initialize widgets see #702 --- bumblebee_status/modules/contrib/spotify.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/bumblebee_status/modules/contrib/spotify.py b/bumblebee_status/modules/contrib/spotify.py index 25bff6f..77960b7 100644 --- a/bumblebee_status/modules/contrib/spotify.py +++ b/bumblebee_status/modules/contrib/spotify.py @@ -68,7 +68,8 @@ class Module(core.module.Module): } widget.set("state", "next") elif widget_name == "spotify.song": - pass + widget.set("state", "song") + widget.full_text(self.__song) else: raise KeyError( "The spotify module does not have a {widget_name!r} widget".format( @@ -115,9 +116,6 @@ class Module(core.module.Module): widget.set("state", "playing") else: widget.set("state", "paused") - elif widget.name == "spotify.song": - widget.set("state", "song") - widget.full_text(self.__song) except Exception as e: logging.exception(e) From e42f09b27e86bd9b8c4104ac7122d7c6b9cc1b9a Mon Sep 17 00:00:00 2001 From: Tobias Witek Date: Sun, 13 Sep 2020 11:21:39 +0200 Subject: [PATCH 128/506] [bumblebee] Make update lock on input non-blocking for consistent lock behaviour, make the input loop lock non-blocking. see #702 --- bumblebee-status | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bumblebee-status b/bumblebee-status index dda14e4..f6bcb94 100755 --- a/bumblebee-status +++ b/bumblebee-status @@ -68,10 +68,10 @@ def handle_input(output, update_lock): modules[event["name"]] = True except ValueError: pass - update_lock.acquire() - core.event.trigger("update", modules.keys()) - core.event.trigger("draw") - update_lock.release() + if update_lock.acquire(blocking=False) == True: + core.event.trigger("update", modules.keys(), force=True) + core.event.trigger("draw") + update_lock.release() poll.unregister(sys.stdin.fileno()) From 8a4fc409478294a721c82167dfc15091bfaad662 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Tue, 15 Sep 2020 20:27:50 +0200 Subject: [PATCH 129/506] Revert "[modules/spotify] properly initialize widgets" This reverts commit aa6238a5c6f5781ebd136810e115ba297ee15a23. --- bumblebee_status/modules/contrib/spotify.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bumblebee_status/modules/contrib/spotify.py b/bumblebee_status/modules/contrib/spotify.py index 77960b7..25bff6f 100644 --- a/bumblebee_status/modules/contrib/spotify.py +++ b/bumblebee_status/modules/contrib/spotify.py @@ -68,8 +68,7 @@ class Module(core.module.Module): } widget.set("state", "next") elif widget_name == "spotify.song": - widget.set("state", "song") - widget.full_text(self.__song) + pass else: raise KeyError( "The spotify module does not have a {widget_name!r} widget".format( @@ -116,6 +115,9 @@ class Module(core.module.Module): widget.set("state", "playing") else: widget.set("state", "paused") + elif widget.name == "spotify.song": + widget.set("state", "song") + widget.full_text(self.__song) except Exception as e: logging.exception(e) From 806b97895e29d151eea8db1779df267792a560e0 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Wed, 16 Sep 2020 09:09:49 +0200 Subject: [PATCH 130/506] [bumblebee] add small sleep before update in some situations/modules (spotify, playerctl, etc.), it is possible that a state change (e.g. play/pause) takes a small time to actually propagate into the whole system (e.g. until the next "update" retrieves the correct value). To alleviate that, add a very small delay to the update. --- bumblebee-status | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bumblebee-status b/bumblebee-status index f6bcb94..d7e8dab 100755 --- a/bumblebee-status +++ b/bumblebee-status @@ -68,6 +68,8 @@ def handle_input(output, update_lock): modules[event["name"]] = True except ValueError: pass + + time.sleep(0.2) if update_lock.acquire(blocking=False) == True: core.event.trigger("update", modules.keys(), force=True) core.event.trigger("draw") From 1759c33cf30dc1ee2bab71bf79380c626da99d6e Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Wed, 16 Sep 2020 09:13:48 +0200 Subject: [PATCH 131/506] [bumblebee] make input delay configurable make the delay before updating the bar after an input event configurable as engine.input_delay. see #702 --- bumblebee-status | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/bumblebee-status b/bumblebee-status index d7e8dab..0c1623e 100755 --- a/bumblebee-status +++ b/bumblebee-status @@ -40,7 +40,7 @@ class CommandSocket(object): os.unlink(self.__name) -def handle_input(output, update_lock): +def handle_input(output, config, update_lock): with CommandSocket() as cmdsocket: poll = select.poll() poll.register(sys.stdin.fileno(), select.POLLIN) @@ -69,7 +69,9 @@ def handle_input(output, update_lock): except ValueError: pass - time.sleep(0.2) + delay = float(config.get("engine.input_delay", 0.2)) + if delay > 0: + time.sleep(delay) if update_lock.acquire(blocking=False) == True: core.event.trigger("update", modules.keys(), force=True) core.event.trigger("draw") @@ -102,7 +104,7 @@ def main(): core.input.register(None, core.input.WHEEL_DOWN, "i3-msg workspace next_on_output") update_lock = threading.Lock() - input_thread = threading.Thread(target=handle_input, args=(output, update_lock, )) + input_thread = threading.Thread(target=handle_input, args=(output, config, update_lock, )) input_thread.daemon = True input_thread.start() From 9e80d4d9076d5bbe812e7ce6754a1c427e0a8ea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thayn=C3=A3=20Moretti?= Date: Sat, 26 Sep 2020 10:05:19 -0300 Subject: [PATCH 132/506] Add amixer tests --- tests/modules/contrib/test_amixer.py | 154 +++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) diff --git a/tests/modules/contrib/test_amixer.py b/tests/modules/contrib/test_amixer.py index b1cfbf0..4ff37a3 100644 --- a/tests/modules/contrib/test_amixer.py +++ b/tests/modules/contrib/test_amixer.py @@ -1,5 +1,159 @@ import pytest +import util.cli +import core.config +import modules.contrib.amixer + +@pytest.fixture +def module_mock(request): + def _module_mock(config = []): + return modules.contrib.amixer.Module( + config=core.config.Config(config), + theme=None + ) + + yield _module_mock + +@pytest.fixture +def amixer_mock(): + def _mock(device='Master', volume='10%', state='on'): + return """ + Simple mixer control '{device}',0 + Capabilities: pvolume pswitch pswitch-joined + Playback channels: Front Left - Front Right + Limits: Playback 0 - 65536 + Mono: + Front Left: Playback 55705 [{volume}%] [{state}] + Front Right: Playback 55705 [{volume}%] [{state}] + """.format( + device=device, + volume=volume, + state=state + ) + + return _mock + def test_load_module(): __import__("modules.contrib.amixer") +def test_initial_full_text(module_mock, amixer_mock, mocker): + module = module_mock() + assert module.widget().full_text() == 'n/a' + +def test_input_registration(mocker): + input_register = mocker.patch('core.input.register') + + module = modules.contrib.amixer.Module( + config=core.config.Config([]), + theme=None + ) + + input_register.assert_any_call( + module, + button=core.input.WHEEL_DOWN, + cmd=module.decrease_volume + ) + + input_register.assert_any_call( + module, + button=core.input.WHEEL_UP, + cmd=module.increase_volume + ) + + input_register.assert_any_call( + module, + button=core.input.LEFT_MOUSE, + cmd=module.toggle + ) + +def test_volume_update(module_mock, amixer_mock, mocker): + mocker.patch( + 'util.cli.execute', + return_value=amixer_mock(volume='25%', state='on') + ) + + module = module_mock() + widget = module.widget() + + module.update() + assert widget.full_text() == '25%' + assert module.state(widget) == ['unmuted'] + +def test_muted_update(module_mock, amixer_mock, mocker): + mocker.patch( + 'util.cli.execute', + return_value=amixer_mock(volume='50%', state='off') + ) + + module = module_mock() + widget = module.widget() + + module.update() + assert widget.full_text() == '50%' + assert module.state(widget) == ['warning', 'muted'] + +def test_exception_update(module_mock, mocker): + mocker.patch( + 'util.cli.execute', + side_effect=Exception + ) + + module = module_mock() + widget = module.widget() + + module.update() + assert widget.full_text() == 'n/a' + +def test_unavailable_amixer(module_mock, mocker): + mocker.patch('util.cli.execute', return_value='Invalid') + + module = module_mock() + widget = module.widget() + + module.update() + assert widget.full_text() == '0%' + +def test_toggle(module_mock, mocker): + command = mocker.patch('util.cli.execute') + module = module_mock() + module.toggle(False) + command.assert_called_once_with('amixer -q set Master,0 toggle') + +def test_default_volume(module_mock, mocker): + module = module_mock() + + command = mocker.patch('util.cli.execute') + module.increase_volume(False) + command.assert_called_once_with('amixer -q set Master,0 4%+') + + command = mocker.patch('util.cli.execute') + module.decrease_volume(False) + command.assert_called_once_with('amixer -q set Master,0 4%-') + +def test_custom_volume(module_mock, mocker): + module = module_mock(['-p', 'amixer.percent_change=25']) + + command = mocker.patch('util.cli.execute') + module.increase_volume(False) + command.assert_called_once_with('amixer -q set Master,0 25%+') + + command = mocker.patch('util.cli.execute') + module.decrease_volume(False) + command.assert_called_once_with('amixer -q set Master,0 25%-') + +def test_custom_device(module_mock, mocker): + mocker.patch('util.cli.execute') + module = module_mock(['-p', 'amixer.device=CustomMaster']) + + command = mocker.patch('util.cli.execute') + module.toggle(False) + command.assert_called_once_with('amixer -q set CustomMaster toggle') + + command = mocker.patch('util.cli.execute') + module.increase_volume(False) + command.assert_called_once_with('amixer -q set CustomMaster 4%+') + + command = mocker.patch('util.cli.execute') + module.decrease_volume(False) + command.assert_called_once_with('amixer -q set CustomMaster 4%-') + From 7d85ba87d543148544e74a2693dc280dbc80fa2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thayn=C3=A3=20Moretti?= Date: Mon, 28 Sep 2020 19:38:18 -0300 Subject: [PATCH 133/506] Force feedparser to 6.0.0b1 --- .travis.yml | 2 +- requirements/modules/rss.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 78aa402..dbc9da8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ install: - pip install -U coverage==4.3 pytest pytest-mock freezegun - pip install codeclimate-test-reporter - pip install i3-py Pillow Babel DateTime python-dateutil - - pip install docker feedparser i3ipc + - pip install docker feedparser==6.0.0b1 i3ipc - pip install netifaces power - pip install psutil pytz - pip install requests simplejson diff --git a/requirements/modules/rss.txt b/requirements/modules/rss.txt index 1b25361..4373b0d 100644 --- a/requirements/modules/rss.txt +++ b/requirements/modules/rss.txt @@ -1 +1 @@ -feedparser +feedparser==6.0.0b1 From 4df495601a8c8605b29a06eebf58cf95a9cca679 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thayn=C3=A3=20Moretti?= Date: Thu, 1 Oct 2020 19:10:46 -0300 Subject: [PATCH 134/506] Create symbolic links --- bumblebee_status/modules/contrib/arch_update.py | 1 + bumblebee_status/modules/contrib/battery_upower.py | 1 + bumblebee_status/modules/contrib/layout_xkbswitch.py | 1 + bumblebee_status/modules/core/layout_xkb.py | 1 + tests/modules/contrib/test_arch-update.py | 2 ++ tests/modules/contrib/test_battery-upower.py | 3 +++ tests/modules/contrib/test_layout-xkbswitch.py | 2 ++ tests/modules/core/test_layout-xkb.py | 2 ++ 8 files changed, 13 insertions(+) create mode 120000 bumblebee_status/modules/contrib/arch_update.py create mode 120000 bumblebee_status/modules/contrib/battery_upower.py create mode 120000 bumblebee_status/modules/contrib/layout_xkbswitch.py create mode 120000 bumblebee_status/modules/core/layout_xkb.py diff --git a/bumblebee_status/modules/contrib/arch_update.py b/bumblebee_status/modules/contrib/arch_update.py new file mode 120000 index 0000000..57fd99f --- /dev/null +++ b/bumblebee_status/modules/contrib/arch_update.py @@ -0,0 +1 @@ +arch-update.py \ No newline at end of file diff --git a/bumblebee_status/modules/contrib/battery_upower.py b/bumblebee_status/modules/contrib/battery_upower.py new file mode 120000 index 0000000..4a7bb68 --- /dev/null +++ b/bumblebee_status/modules/contrib/battery_upower.py @@ -0,0 +1 @@ +battery-upower.py \ No newline at end of file diff --git a/bumblebee_status/modules/contrib/layout_xkbswitch.py b/bumblebee_status/modules/contrib/layout_xkbswitch.py new file mode 120000 index 0000000..e7d6b94 --- /dev/null +++ b/bumblebee_status/modules/contrib/layout_xkbswitch.py @@ -0,0 +1 @@ +layout-xkbswitch.py \ No newline at end of file diff --git a/bumblebee_status/modules/core/layout_xkb.py b/bumblebee_status/modules/core/layout_xkb.py new file mode 120000 index 0000000..f2e8037 --- /dev/null +++ b/bumblebee_status/modules/core/layout_xkb.py @@ -0,0 +1 @@ +layout-xkb.py \ No newline at end of file diff --git a/tests/modules/contrib/test_arch-update.py b/tests/modules/contrib/test_arch-update.py index 6a1c172..b11187b 100644 --- a/tests/modules/contrib/test_arch-update.py +++ b/tests/modules/contrib/test_arch-update.py @@ -3,3 +3,5 @@ import pytest def test_load_module(): __import__("modules.contrib.arch-update") +def test_load_symbolic_link_module(): + __import__("modules.contrib.arch_update") diff --git a/tests/modules/contrib/test_battery-upower.py b/tests/modules/contrib/test_battery-upower.py index cb62a16..d129679 100644 --- a/tests/modules/contrib/test_battery-upower.py +++ b/tests/modules/contrib/test_battery-upower.py @@ -5,3 +5,6 @@ pytest.importorskip("dbus") def test_load_module(): __import__("modules.contrib.battery-upower") +def test_load_symbolic_link_module(): + __import__("modules.contrib.battery_upower") + diff --git a/tests/modules/contrib/test_layout-xkbswitch.py b/tests/modules/contrib/test_layout-xkbswitch.py index 08cfd96..b709254 100644 --- a/tests/modules/contrib/test_layout-xkbswitch.py +++ b/tests/modules/contrib/test_layout-xkbswitch.py @@ -3,3 +3,5 @@ import pytest def test_load_module(): __import__("modules.contrib.layout-xkbswitch") +def test_load_symbolic_link_module(): + __import__("modules.contrib.layout_xkbswitch") diff --git a/tests/modules/core/test_layout-xkb.py b/tests/modules/core/test_layout-xkb.py index 8eacfad..852b9da 100644 --- a/tests/modules/core/test_layout-xkb.py +++ b/tests/modules/core/test_layout-xkb.py @@ -5,3 +5,5 @@ pytest.importorskip("xkbgroup") def test_load_module(): __import__("modules.core.layout-xkb") +def test_load_symbolic_link_module(): + __import__("modules.core.layout_xkb") From a253e703280d836f57997980ba0ef80f57b5e56b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thayn=C3=A3=20Moretti?= Date: Thu, 1 Oct 2020 19:16:51 -0300 Subject: [PATCH 135/506] Update documentation --- docs/development/module.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/development/module.rst b/docs/development/module.rst index 1d6e716..113a6f7 100644 --- a/docs/development/module.rst +++ b/docs/development/module.rst @@ -11,6 +11,7 @@ Adding a new module to ``bumblebee-status`` is straight-forward: ``bumblebee-status`` (i.e. a module called ``bumblebee_status/modules/contrib/test.py`` will be loaded using ``bumblebee-status -m test``) +- The module name must follow the `Python Naming Conventions `_ - See below for how to actually write the module - Test (run ``bumblebee-status`` in the CLI) - Make sure your changes don’t break anything: ``./coverage.sh`` From f2153b95a535445f520d94c29b9eca3ae6906633 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Fri, 2 Oct 2020 09:34:53 +0200 Subject: [PATCH 136/506] [doc] add scrolling parameters see #710 --- docs/features.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/features.rst b/docs/features.rst index f050a2b..f48ebf0 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -87,6 +87,19 @@ attention, it will remain hidden. Note that this parameter is specified *in addition* to ``-m`` (i.e. to autohide the CPU module, you would use ``bumblebee-status -m cpu memory traffic -a cpu``). +Scrolling widget text +----------------------- +Some widgets support scrolling for long text (e.g. most music player +widgets, rss, etc.). Those have some additional settings for customizing +the scrolling behaviour, in particular: + + - ``scrolling.width``: Desired width of the scrolling panel + - ``scrolling.makewide``: If set to true, extends texts shorter than + ``scrolling.width`` to that width + - ``scrolling.bounce``: If set to true, bounces the text when it reaches + the end, otherwise, it behaves like marquee (scroll-through) text + - ``scrolling.speed``: Defines the scroll speed, in characters per update + Additional widget theme settings -------------------------------- From 130bfc0f0b76ccae20ef1d71016ac29203a5acdf Mon Sep 17 00:00:00 2001 From: Niladri Bhattacharjee <47671104+Niladri29@users.noreply.github.com> Date: Sat, 3 Oct 2020 00:53:04 +0530 Subject: [PATCH 137/506] Some minor --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7c26ddd..18e76a5 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Supported Python versions: 3.4, 3.5, 3.6, 3.7, 3.8 Supported FontAwesome version: 4 (free version of 5 doesn't include some of the icons) --- -**NOTE** +***NOTE*** The default branch for this project is `main` - I'm keeping `master` around for backwards compatibility (I do not want to break anybody's setup), but the default branch is now `main`! From 823a57d2616c5251c44d7039b10fc2c8759a7154 Mon Sep 17 00:00:00 2001 From: Joshua Barrass Date: Sun, 4 Oct 2020 14:44:27 +0100 Subject: [PATCH 138/506] Add org-mode TODO module --- bumblebee_status/modules/contrib/todo_org.py | 57 ++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 bumblebee_status/modules/contrib/todo_org.py diff --git a/bumblebee_status/modules/contrib/todo_org.py b/bumblebee_status/modules/contrib/todo_org.py new file mode 100644 index 0000000..05fa90b --- /dev/null +++ b/bumblebee_status/modules/contrib/todo_org.py @@ -0,0 +1,57 @@ +# pylint: disable=C0111,R0903 +"""Displays the number of todo items from an org-mode file +Parameters: + * todo_org.file: File to read TODOs from (defaults to ~/org/todo.org) + * todo_org.remaining: False by default. When true, will output the number of remaining todos instead of the number completed (i.e. 1/4 means 1 of 4 todos remaining, rather than 1 of 4 todos completed) +Based on the todo module by `codingo ` +""" + +import re +import os.path + +import core.module +import core.widget +import core.input +from util.format import asbool + +class Module(core.module.Module): + def __init__(self, config, theme): + super().__init__(config, theme, core.widget.Widget(self.output)) + + self.__todo_regex = re.compile("^\\s*\\*+\\s*TODO") + self.__done_regex = re.compile("^\\s*\\*+\\s*DONE") + + self.__doc = os.path.expanduser( + self.parameter("file", "~/org/todo.org") + ) + self.__remaining = asbool(self.parameter("remaining", "False")) + self.__todo, self.__total = self.count_items() + core.input.register( + self, + button=core.input.LEFT_MOUSE, + cmd="emacs {}".format(self.__doc) + ) + + def output(self, widget): + if self.__remaining: + return "TODO: {}/{}".format(self.__todo, self.__total) + return "TODO: {}/{}".format(self.__total-self.__todo, self.__total) + + def update(self): + self.__todo, self.__total = self.count_items() + + def count_items(self): + todo, total = 0, 0 + try: + with open(self.__doc, "r") as f: + for line in f: + if self.__todo_regex.match(line.upper()) is not None: + todo += 1 + total += 1 + elif self.__done_regex.match(line.upper()) is not None: + total += 1 + return todo, total + except OSError: + return -1, -1 + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From fbe57643131c483970dfe2269f893beb928aef00 Mon Sep 17 00:00:00 2001 From: Joshua Barrass Date: Sun, 4 Oct 2020 15:36:24 +0100 Subject: [PATCH 139/506] Add "concise controls" to spotify module --- bumblebee_status/modules/contrib/spotify.py | 24 +++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/bumblebee_status/modules/contrib/spotify.py b/bumblebee_status/modules/contrib/spotify.py index 25bff6f..2295609 100644 --- a/bumblebee_status/modules/contrib/spotify.py +++ b/bumblebee_status/modules/contrib/spotify.py @@ -8,6 +8,8 @@ Parameters: Available values are: {album}, {title}, {artist}, {trackNumber} * spotify.layout: Comma-separated list to change order of widgets (defaults to song, previous, pause, next) Widget names are: spotify.song, spotify.prev, spotify.pause, spotify.next + * spotify.concise_controls: When enabled, allows spotify to be controlled from just the spotify.song widget. + Concise controls are: Left Click: Toggle Pause; Wheel Up: Next; Wheel Down; Previous. contributed by `yvesh `_ - many thanks! @@ -68,15 +70,33 @@ class Module(core.module.Module): } widget.set("state", "next") elif widget_name == "spotify.song": - pass + if util.format.asbool(self.parameter("concise_controls", "false")): + widget_map[widget] = [ + { + "button": core.input.LEFT_MOUSE, + "cmd": self.__cmd + "PlayPause", + }, { + "button": core.input.WHEEL_UP, + "cmd": self.__cmd + "Next", + }, { + "button": core.input.WHEEL_DOWN, + "cmd": self.__cmd + "Previous", + } + ] else: raise KeyError( "The spotify module does not have a {widget_name!r} widget".format( widget_name=widget_name ) ) + # is there any reason the inputs can't be directly registered above? for widget, callback_options in widget_map.items(): - core.input.register(widget, **callback_options) + if isinstance(callback_options, dict): + core.input.register(widget, **callback_options) + + elif isinstance(callback_options, list): # used by concise_controls + for opts in callback_options: + core.input.register(widget, **opts) def hidden(self): From 0a7a4150e090204365be5ae85328a4966ea1ef75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thayn=C3=A3=20Moretti?= Date: Sun, 4 Oct 2020 15:35:13 -0300 Subject: [PATCH 140/506] Create arch-update tests --- tests/modules/contrib/test_arch-update.py | 69 +++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/tests/modules/contrib/test_arch-update.py b/tests/modules/contrib/test_arch-update.py index b11187b..53c2941 100644 --- a/tests/modules/contrib/test_arch-update.py +++ b/tests/modules/contrib/test_arch-update.py @@ -1,7 +1,76 @@ import pytest +import util.cli +import core.config +import modules.contrib.arch_update + +@pytest.fixture +def module(): + module = modules.contrib.arch_update.Module( + config=core.config.Config([]), + theme=None + ) + + yield module + def test_load_module(): __import__("modules.contrib.arch-update") def test_load_symbolic_link_module(): __import__("modules.contrib.arch_update") + +def test_with_one_package(module, mocker): + command = mocker.patch( + 'util.cli.execute', + return_value=(0, 'bumblebee 1.0.0') + ) + + module.update() + + command.assert_called_with( + 'checkupdates', + ignore_errors=True, + return_exitcode=True + ) + + widget = module.widget() + assert widget.full_text() == 'Update Arch: 1' + assert module.state(widget) == None + assert module.hidden() == False + +def test_with_two_packages(module, mocker): + command = mocker.patch( + 'util.cli.execute', + return_value=(0, 'bumblebee 1.0.0\ni3wm 3.5.7') + ) + + module.update() + + widget = module.widget() + assert widget.full_text() == 'Update Arch: 2' + assert module.state(widget) == 'warning' + assert module.hidden() == False + +def test_with_no_packages(module, mocker): + mocker.patch('util.cli.execute', return_value=(2, '')) + + module.update() + + widget = module.widget() + assert widget.full_text() == 'Update Arch: 0' + assert module.state(widget) == None + assert module.hidden() == True + +def test_with_unknown_code(module, mocker): + mocker.patch('util.cli.execute', return_value=(99, 'error')) + logger = mocker.patch('logging.error') + + module.update() + + logger.assert_called_with('checkupdates exited with {}: {}'.format(99, 'error')) + + widget = module.widget() + assert widget.full_text() == 'Update Arch: 0' + assert module.state(widget) == 'warning' + assert module.hidden() == False + From 180a87e0c3165da8e34298bc336c3184786cb825 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thayn=C3=A3=20Moretti?= Date: Sun, 4 Oct 2020 15:57:48 -0300 Subject: [PATCH 141/506] Create dunstctl tests --- bumblebee_status/modules/contrib/dunstctl.py | 1 + tests/modules/contrib/test_dunstctl.py | 67 ++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 tests/modules/contrib/test_dunstctl.py diff --git a/bumblebee_status/modules/contrib/dunstctl.py b/bumblebee_status/modules/contrib/dunstctl.py index 1b7649c..24b93f0 100644 --- a/bumblebee_status/modules/contrib/dunstctl.py +++ b/bumblebee_status/modules/contrib/dunstctl.py @@ -27,6 +27,7 @@ class Module(core.module.Module): def toggle_status(self, event): self._paused = self.__isPaused() + if self._paused: util.cli.execute("dunstctl set-paused false") else: diff --git a/tests/modules/contrib/test_dunstctl.py b/tests/modules/contrib/test_dunstctl.py new file mode 100644 index 0000000..e6a7ce3 --- /dev/null +++ b/tests/modules/contrib/test_dunstctl.py @@ -0,0 +1,67 @@ +import pytest + +import util.cli +import core.config +import modules.contrib.dunstctl + +def build_module(): + return modules.contrib.dunstctl.Module( + config=core.config.Config([]), + theme=None + ) + +def test_load_module(): + __import__("modules.contrib.dunstctl") + +def test_dunst_running(mocker): + command = mocker.patch('util.cli.execute', return_value='false') + + module = build_module() + module.update() + + command.assert_called_with('dunstctl is-paused') + + widget = module.widget() + assert module.state(widget) == ['unmuted'] + +def test_dunst_paused(mocker): + command = mocker.patch('util.cli.execute', return_value='true') + + module = build_module() + module.update() + + command.assert_called_with('dunstctl is-paused') + + widget = module.widget() + assert module.state(widget) == ['muted', 'warning'] + +def test_toggle_status_pause(mocker): + command = mocker.patch('util.cli.execute') + command.side_effect = ['true', 'true', None] + + module = build_module() + module.toggle_status(False) + + command.assert_any_call('dunstctl set-paused false') + +def test_toggle_status_unpause(mocker): + command = mocker.patch('util.cli.execute') + command.side_effect = ['false', 'false', None] + + module = build_module() + module.toggle_status(False) + + command.assert_called_with('dunstctl set-paused true') + +def test_input_register(mocker): + command = mocker.patch('util.cli.execute') + input_register = mocker.patch('core.input.register') + + module = build_module() + + input_register.assert_called_with( + module, + button=core.input.LEFT_MOUSE, + cmd=module.toggle_status + ) + From 04a2ea438bb9147df2e95d45235b55254b2cfbfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thayn=C3=A3=20Moretti?= Date: Wed, 7 Oct 2020 17:36:58 -0300 Subject: [PATCH 142/506] Create layout-xkbswitch tests --- .../modules/contrib/layout-xkbswitch.py | 4 +- .../modules/contrib/test_layout-xkbswitch.py | 51 +++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/bumblebee_status/modules/contrib/layout-xkbswitch.py b/bumblebee_status/modules/contrib/layout-xkbswitch.py index 767deb9..a749522 100644 --- a/bumblebee_status/modules/contrib/layout-xkbswitch.py +++ b/bumblebee_status/modules/contrib/layout-xkbswitch.py @@ -19,13 +19,13 @@ class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.current_layout)) - core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.__next_keymap) + core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.next_keymap) self.__current_layout = self.__get_current_layout() def current_layout(self, _): return self.__current_layout - def __next_keymap(self, event): + def next_keymap(self, event): util.cli.execute("xkb-switch -n", ignore_errors=True) def __get_current_layout(self): diff --git a/tests/modules/contrib/test_layout-xkbswitch.py b/tests/modules/contrib/test_layout-xkbswitch.py index b709254..404aa73 100644 --- a/tests/modules/contrib/test_layout-xkbswitch.py +++ b/tests/modules/contrib/test_layout-xkbswitch.py @@ -1,7 +1,58 @@ import pytest +import util.cli +import core.config +import modules.contrib.layout_xkbswitch + +def build_module(): + return modules.contrib.layout_xkbswitch.Module( + config=core.config.Config([]), + theme=None + ) + def test_load_module(): __import__("modules.contrib.layout-xkbswitch") def test_load_symbolic_link_module(): __import__("modules.contrib.layout_xkbswitch") + +def test_current_layout(mocker): + command = mocker.patch('util.cli.execute') + command.side_effect = ['en', 'en'] + + module = build_module() + widget = module.widget() + + module.update() + + assert widget.full_text() == 'en' + +def test_current_layout_exception(mocker): + command = mocker.patch('util.cli.execute') + command.side_effect = RuntimeError + + module = build_module() + widget = module.widget() + + module.update() + + assert widget.full_text() == ['n/a'] + +def test_input_register(mocker): + input_register = mocker.patch('core.input.register') + + module = build_module() + + input_register.assert_called_with( + module, + button=core.input.LEFT_MOUSE, + cmd=module.next_keymap + ) + +def test_next_keymap(mocker): + command = mocker.patch('util.cli.execute') + + module = build_module() + module.next_keymap(False) + + command.assert_called_with('xkb-switch -n', ignore_errors=True) From 1912f3053d3d6e7bb815e465697a7ec4e52c23fe Mon Sep 17 00:00:00 2001 From: Martin Morlot Date: Fri, 9 Oct 2020 10:59:45 +0200 Subject: [PATCH 143/506] [Bluetooth2] fixed the execution of the toggle state --- bumblebee_status/modules/contrib/bluetooth2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bumblebee_status/modules/contrib/bluetooth2.py b/bumblebee_status/modules/contrib/bluetooth2.py index b8fac09..52474b9 100644 --- a/bumblebee_status/modules/contrib/bluetooth2.py +++ b/bumblebee_status/modules/contrib/bluetooth2.py @@ -69,7 +69,7 @@ class Module(core.module.Module): ) logging.debug("bt: toggling bluetooth") - core.util.execute(cmd) + util.cli.execute(cmd) def state(self, widget): """Get current state.""" From 1a7ae9ecc69bb43732713ae6f4c9901e459cd99b Mon Sep 17 00:00:00 2001 From: w1kl4s Date: Wed, 14 Oct 2020 18:07:29 +0200 Subject: [PATCH 144/506] Fix Python 3.9 compatibility Replaced threading.Thread.isAlive() with threading.Thread.is_alive() --- bumblebee_status/core/module.py | 2 +- bumblebee_status/modules/contrib/apt.py | 2 +- bumblebee_status/modules/core/redshift.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bumblebee_status/core/module.py b/bumblebee_status/core/module.py index 23ea4b5..f4bac7e 100644 --- a/bumblebee_status/core/module.py +++ b/bumblebee_status/core/module.py @@ -123,7 +123,7 @@ class Module(core.input.Object): def update_wrapper(self): if self.background == True: - if self.__thread and self.__thread.isAlive(): + if self.__thread and self.__thread.is_alive(): return # skip this update interval self.__thread = threading.Thread(target=self.internal_update, args=(True,)) self.__thread.start() diff --git a/bumblebee_status/modules/contrib/apt.py b/bumblebee_status/modules/contrib/apt.py index 2a41aea..b7200bb 100644 --- a/bumblebee_status/modules/contrib/apt.py +++ b/bumblebee_status/modules/contrib/apt.py @@ -65,7 +65,7 @@ class Module(core.module.Module): ) def update(self): - if self.__thread and self.__thread.isAlive(): + if self.__thread and self.__thread.is_alive(): return self.__thread = threading.Thread(target=get_apt_check_info, args=(self,)) diff --git a/bumblebee_status/modules/core/redshift.py b/bumblebee_status/modules/core/redshift.py index c60ed9e..c6735b1 100644 --- a/bumblebee_status/modules/core/redshift.py +++ b/bumblebee_status/modules/core/redshift.py @@ -101,7 +101,7 @@ class Module(core.module.Module): return val def update(self): - if self.__thread is not None and self.__thread.isAlive(): + if self.__thread is not None and self.__thread.is_alive(): return self.__thread = threading.Thread(target=get_redshift_value, args=(self,)) self.__thread.start() From 3c0499ba56759e40732ded9cf607f83b595c6aab Mon Sep 17 00:00:00 2001 From: Joachim Mathes Date: Sun, 18 Oct 2020 15:34:52 +0200 Subject: [PATCH 145/506] Provide alternative dunstctl implementation --- bumblebee_status/modules/contrib/dunstctl.py | 41 ++++++++------- tests/modules/contrib/test_dunstctl.py | 54 ++++++-------------- themes/icons/ascii.json | 11 ++++ themes/icons/awesome-fonts.json | 3 +- themes/icons/ionicons.json | 5 ++ 5 files changed, 55 insertions(+), 59 deletions(-) diff --git a/bumblebee_status/modules/contrib/dunstctl.py b/bumblebee_status/modules/contrib/dunstctl.py index 24b93f0..3c803a4 100644 --- a/bumblebee_status/modules/contrib/dunstctl.py +++ b/bumblebee_status/modules/contrib/dunstctl.py @@ -1,43 +1,42 @@ # pylint: disable=C0111,R0903 -""" -Toggle dunst notifications using dunstctl. +"""Toggle dunst notifications using dunstctl. -When notifications are paused using this module dunst doesn't get killed and you'll keep getting notifications on the background that will be displayed when unpausing. -This is specially useful if you're using dunst's scripting (https://wiki.archlinux.org/index.php/Dunst#Scripting), which requires dunst to be running. Scripts will be executed when dunst gets unpaused. +When notifications are paused using this module dunst doesn't get killed and +you'll keep getting notifications on the background that will be displayed when +unpausing. This is specially useful if you're using dunst's scripting +(https://wiki.archlinux.org/index.php/Dunst#Scripting), which requires dunst to +be running. Scripts will be executed when dunst gets unpaused. Requires: * dunst v1.5.0+ contributed by `cristianmiranda `_ - many thanks! +contributed by `joachimmathes `_ - many thanks! """ import core.module import core.widget import core.input - import util.cli class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget("")) - self._paused = self.__isPaused() - core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.toggle_status) + core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.__toggle_state) + self.__states = {"unknown": ["unknown", "critical"], + "true": ["muted", "warning"], + "false": ["unmuted"]} - def toggle_status(self, event): - self._paused = self.__isPaused() - - if self._paused: - util.cli.execute("dunstctl set-paused false") - else: - util.cli.execute("dunstctl set-paused true") - self._paused = not self._paused - - def __isPaused(self): - return util.cli.execute("dunstctl is-paused").strip() == "true" + def __toggle_state(self, event): + util.cli.execute("dunstctl set-paused toggle", ignore_errors=True) def state(self, widget): - if self._paused: - return ["muted", "warning"] - return ["unmuted"] + return self.__states[self.__is_dunst_paused()] + + def __is_dunst_paused(self): + result = util.cli.execute("dunstctl is-paused", + return_exitcode=True, + ignore_errors=True) + return result[1].rstrip() if result[0] == 0 else "unknown" diff --git a/tests/modules/contrib/test_dunstctl.py b/tests/modules/contrib/test_dunstctl.py index e6a7ce3..db77fe3 100644 --- a/tests/modules/contrib/test_dunstctl.py +++ b/tests/modules/contrib/test_dunstctl.py @@ -14,54 +14,34 @@ def test_load_module(): __import__("modules.contrib.dunstctl") def test_dunst_running(mocker): - command = mocker.patch('util.cli.execute', return_value='false') + command = mocker.patch('util.cli.execute', return_value=(0, "false")) module = build_module() module.update() - - command.assert_called_with('dunstctl is-paused') - widget = module.widget() - assert module.state(widget) == ['unmuted'] + + actual = module.state(widget) + command.assert_called_with('dunstctl is-paused', return_exitcode=True, ignore_errors=True) + assert actual == ['unmuted'] def test_dunst_paused(mocker): - command = mocker.patch('util.cli.execute', return_value='true') + command = mocker.patch('util.cli.execute', return_value=(0, "true")) module = build_module() module.update() - - command.assert_called_with('dunstctl is-paused') - widget = module.widget() - assert module.state(widget) == ['muted', 'warning'] -def test_toggle_status_pause(mocker): - command = mocker.patch('util.cli.execute') - command.side_effect = ['true', 'true', None] + actual = module.state(widget) + command.assert_called_with('dunstctl is-paused', return_exitcode=True, ignore_errors=True) + assert actual == ['muted', 'warning'] + +def test_dunst_off(mocker): + command = mocker.patch('util.cli.execute', return_value=(1, "dontcare")) module = build_module() - module.toggle_status(False) - - command.assert_any_call('dunstctl set-paused false') - -def test_toggle_status_unpause(mocker): - command = mocker.patch('util.cli.execute') - command.side_effect = ['false', 'false', None] - - module = build_module() - module.toggle_status(False) - - command.assert_called_with('dunstctl set-paused true') - -def test_input_register(mocker): - command = mocker.patch('util.cli.execute') - input_register = mocker.patch('core.input.register') - - module = build_module() - - input_register.assert_called_with( - module, - button=core.input.LEFT_MOUSE, - cmd=module.toggle_status - ) + module.update() + widget = module.widget() + actual = module.state(widget) + command.assert_called_with('dunstctl is-paused', return_exitcode=True, ignore_errors=True) + assert actual == ['unknown', 'critical'] diff --git a/themes/icons/ascii.json b/themes/icons/ascii.json index 2580d32..77ef931 100644 --- a/themes/icons/ascii.json +++ b/themes/icons/ascii.json @@ -355,6 +355,17 @@ "prefix": "dunst" } }, + "dunstctl": { + "muted": { + "prefix": "dunst(muted)" + }, + "unmuted": { + "prefix": "dunst" + }, + "unknown": { + "prefix": "dunst(unknown)" + } + }, "twmn": { "muted": { "prefix": "twmn" diff --git a/themes/icons/awesome-fonts.json b/themes/icons/awesome-fonts.json index 2ac9061..2d05dc6 100644 --- a/themes/icons/awesome-fonts.json +++ b/themes/icons/awesome-fonts.json @@ -259,7 +259,8 @@ }, "dunstctl": { "muted": { "prefix": "" }, - "unmuted": { "prefix": "" } + "unmuted": { "prefix": "" }, + "unknown": { "prefix": "" } }, "twmn": { "muted": { "prefix": "" }, diff --git a/themes/icons/ionicons.json b/themes/icons/ionicons.json index b510cc1..c9931f1 100644 --- a/themes/icons/ionicons.json +++ b/themes/icons/ionicons.json @@ -187,6 +187,11 @@ "muted": { "prefix": "\uf39a" }, "unmuted": { "prefix": "\uf39b" } }, + "dunstctl": { + "muted": { "prefix": "\uf39a" }, + "unmuted": { "prefix": "\uf39b" }, + "unknown": { "prefix": "\uf142" } + }, "twmn": { "muted": { "prefix": "\uf1f6" }, "unmuted": { "prefix": "\uf0f3" } From 9b82e736a00b4743c1149da67c0fe7eddd78f484 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Sun, 25 Oct 2020 17:29:00 -0700 Subject: [PATCH 146/506] Fix arch-updates off-by-one. There's a newline in the output so this overcounts by one. --- bumblebee_status/modules/contrib/arch-update.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bumblebee_status/modules/contrib/arch-update.py b/bumblebee_status/modules/contrib/arch-update.py index c93545b..ed9ae58 100644 --- a/bumblebee_status/modules/contrib/arch-update.py +++ b/bumblebee_status/modules/contrib/arch-update.py @@ -40,7 +40,7 @@ class Module(core.module.Module): ) if code == 0: - self.__packages = len(result.split("\n")) + self.__packages = len(result.strip().split("\n")) elif code == 2: self.__packages = 0 else: From 5e96b63c603e01a1a1dcac60be8e769b6c5993fb Mon Sep 17 00:00:00 2001 From: Frank Scherrer Date: Mon, 26 Oct 2020 08:28:55 +0100 Subject: [PATCH 147/506] update python name in shebang to respect PEP-0394 --- bumblebee-status | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bumblebee-status b/bumblebee-status index dda14e4..bd4ab1f 100755 --- a/bumblebee-status +++ b/bumblebee-status @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import os import sys From a811c9c886003aef252ab555c1d3e75512607e30 Mon Sep 17 00:00:00 2001 From: Martin Morlot Date: Fri, 6 Nov 2020 12:13:35 +0100 Subject: [PATCH 148/506] [module] Improved smartstatus with combined_singles Added combined_singles as way to detect the drives that are permanently inside your machine and not plugged via USB. As USB flash drives without smartstatus sometime caused the module to crash. --- bumblebee_status/modules/contrib/smartstatus.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bumblebee_status/modules/contrib/smartstatus.py b/bumblebee_status/modules/contrib/smartstatus.py index b5f3037..81060da 100644 --- a/bumblebee_status/modules/contrib/smartstatus.py +++ b/bumblebee_status/modules/contrib/smartstatus.py @@ -10,7 +10,7 @@ Requires the following executables: * smartctl Parameters: - * smartstatus.display: how to display (defaults to 'combined', other choices: 'seperate' or 'singles') + * smartstatus.display: how to display (defaults to 'combined', other choices: 'combined_singles', 'seperate' or 'singles') * smartstatus.drives: in the case of singles which drives to display, separated comma list value, multiple accepted (defaults to 'sda', example:'sda,sdc') * smartstatus.show_names: boolean in the form of "True" or "False" to show the name of the drives in the form of sda, sbd, combined or none at all. """ @@ -38,7 +38,7 @@ class Module(core.module.Module): self.create_widgets() def create_widgets(self): - if self.display == "combined": + if self.display == "combined" or self.display == "combined_singles": widget = self.add_widget() widget.set("device", "combined") widget.set("assessment", self.combined()) @@ -81,6 +81,8 @@ class Module(core.module.Module): def combined(self): for device in self.devices: + if self.display == "combined_singles" and device not in self.drives: + continue result = self.smart(device) if result == "Fail": return "Fail" From 08ef42834eb2299ce629218c5fee4d01c76d33a2 Mon Sep 17 00:00:00 2001 From: Tobias Witek Date: Fri, 13 Nov 2020 14:56:31 +0100 Subject: [PATCH 149/506] [modules/nic] update documentation to include iwgetid fixes #734 --- bumblebee_status/modules/core/nic.py | 1 + docs/modules.rst | 68 ++++++++++++++++++++++++++-- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/bumblebee_status/modules/core/nic.py b/bumblebee_status/modules/core/nic.py index d64a14f..41607d9 100644 --- a/bumblebee_status/modules/core/nic.py +++ b/bumblebee_status/modules/core/nic.py @@ -7,6 +7,7 @@ Requires the following python module: Requires the following executable: * iw + * (until and including 2.0.5: iwgetid) Parameters: * nic.exclude: Comma-separated list of interface prefixes to exclude (defaults to 'lo,virbr,docker,vboxnet,veth,br') diff --git a/docs/modules.rst b/docs/modules.rst index 9319671..9ebca7f 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -88,6 +88,20 @@ layout-xkb Displays the current keyboard layout using libX11 +Requires the following library: + * libX11.so.6 +and python module: + * xkbgroup + +Parameters: + * layout-xkb.showname: Boolean that indicate whether the full name should be displayed. Defaults to false (only the symbol will be displayed) + * layout-xkb.show_variant: Boolean that indecates whether the variant name should be displayed. Defaults to true. + +layout_xkb +~~~~~~~~~~ + +Displays the current keyboard layout using libX11 + Requires the following library: * libX11.so.6 and python module: @@ -141,6 +155,7 @@ Requires the following python module: Requires the following executable: * iw + * (until and including 2.0.5: iwgetid) Parameters: * nic.exclude: Comma-separated list of interface prefixes to exclude (defaults to 'lo,virbr,docker,vboxnet,veth,br') @@ -366,6 +381,16 @@ Requires the following executable: contributed by `lucassouto `_ - many thanks! +arch_update +~~~~~~~~~~~ + +Check updates to Arch Linux. + +Requires the following executable: + * checkupdates (from pacman-contrib) + +contributed by `lucassouto `_ - many thanks! + battery ~~~~~~~ @@ -396,6 +421,18 @@ Parameters: contributed by `martindoublem `_ - many thanks! +battery_upower +~~~~~~~~~~~~~~ + +Displays battery status, remaining percentage and charging information. + +Parameters: + * battery-upower.warning : Warning threshold in % of remaining charge (defaults to 20) + * battery-upower.critical : Critical threshold in % of remaining charge (defaults to 10) + * battery-upower.showremaining : If set to true (default) shows the remaining time until the batteries are completely discharged + +contributed by `martindoublem `_ - many thanks! + bluetooth ~~~~~~~~~ @@ -666,13 +703,17 @@ dunstctl Toggle dunst notifications using dunstctl. -When notifications are paused using this module dunst doesn't get killed and you'll keep getting notifications on the background that will be displayed when unpausing. -This is specially useful if you're using dunst's scripting (https://wiki.archlinux.org/index.php/Dunst#Scripting), which requires dunst to be running. Scripts will be executed when dunst gets unpaused. +When notifications are paused using this module dunst doesn't get killed and +you'll keep getting notifications on the background that will be displayed when +unpausing. This is specially useful if you're using dunst's scripting +(https://wiki.archlinux.org/index.php/Dunst#Scripting), which requires dunst to +be running. Scripts will be executed when dunst gets unpaused. Requires: * dunst v1.5.0+ contributed by `cristianmiranda `_ - many thanks! +contributed by `joachimmathes `_ - many thanks! .. image:: ../screenshots/dunstctl.png @@ -804,6 +845,16 @@ Requires the following executable: contributed by `somospocos `_ - many thanks! +layout_xkbswitch +~~~~~~~~~~~~~~~~ + +Displays and changes the current keyboard layout + +Requires the following executable: + * xkb-switch + +contributed by `somospocos `_ - many thanks! + libvirtvms ~~~~~~~~~~ @@ -1190,7 +1241,7 @@ Requires the following executables: * smartctl Parameters: - * smartstatus.display: how to display (defaults to 'combined', other choices: 'seperate' or 'singles') + * smartstatus.display: how to display (defaults to 'combined', other choices: 'combined_singles', 'seperate' or 'singles') * smartstatus.drives: in the case of singles which drives to display, separated comma list value, multiple accepted (defaults to 'sda', example:'sda,sdc') * smartstatus.show_names: boolean in the form of "True" or "False" to show the name of the drives in the form of sda, sbd, combined or none at all. @@ -1234,6 +1285,8 @@ Parameters: Available values are: {album}, {title}, {artist}, {trackNumber} * spotify.layout: Comma-separated list to change order of widgets (defaults to song, previous, pause, next) Widget names are: spotify.song, spotify.prev, spotify.pause, spotify.next + * spotify.concise_controls: When enabled, allows spotify to be controlled from just the spotify.song widget. + Concise controls are: Left Click: Toggle Pause; Wheel Up: Next; Wheel Down; Previous. contributed by `yvesh `_ - many thanks! @@ -1377,6 +1430,15 @@ contributed by `codingo `_ - many thanks! .. image:: ../screenshots/todo.png +todo_org +~~~~~~~~ + +Displays the number of todo items from an org-mode file +Parameters: + * todo_org.file: File to read TODOs from (defaults to ~/org/todo.org) + * todo_org.remaining: False by default. When true, will output the number of remaining todos instead of the number completed (i.e. 1/4 means 1 of 4 todos remaining, rather than 1 of 4 todos completed) +Based on the todo module by `codingo ` + traffic ~~~~~~~ From 02465ea0c2e54a68654ad4fd26981286581a3e75 Mon Sep 17 00:00:00 2001 From: James Baumgarten Date: Mon, 23 Nov 2020 17:31:12 -0800 Subject: [PATCH 150/506] add rofication module --- .../modules/contrib/rofication.py | 48 +++++++++++++++++++ themes/icons/awesome-fonts.json | 3 ++ 2 files changed, 51 insertions(+) create mode 100644 bumblebee_status/modules/contrib/rofication.py diff --git a/bumblebee_status/modules/contrib/rofication.py b/bumblebee_status/modules/contrib/rofication.py new file mode 100644 index 0000000..79e6afd --- /dev/null +++ b/bumblebee_status/modules/contrib/rofication.py @@ -0,0 +1,48 @@ +"""Rofication indicator + + https://github.com/DaveDavenport/Rofication + simple module to show an icon + the number of notifications stored in rofication + module will have normal highlighting if there are zero notifications, + "warning" highlighting if there are nonzero notifications, + "critical" highlighting if there are any critical notifications +""" + +import core.module +import core.widget +import core.decorators + +import sys +import socket + +class Module(core.module.Module): + @core.decorators.every(seconds=5) + def __init__(self, config, theme): + super().__init__(config, theme, core.widget.Widget(self.full_text)) + self.__critical = False + self.__numnotifications = 0 + + + def full_text(self, widgets): + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client: + client.connect("/tmp/rofi_notification_daemon") + # below code will fetch two numbers in a list, e.g. ['22', '1'] + # first is total number of notifications, second is number of critical notifications + client.sendall(bytes("num", "utf-8")) + val = client.recv(512) + val = val.decode("utf-8") + l = val.split('\n',2) + self.__numnotifications = int(l[0]) + self.__critical = bool(int(l[1])) + return self.__numnotifications + + def state(self, widget): + # rofication doesn't really support the idea of seen vs unseen notifications + # marking a message as "seen" actually just sets its urgency to normal + # so, doing highlighting if any notifications are present + if self.__critical: + return ["critical"] + elif self.__numnotifications: + return ["warning"] + return [] + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/themes/icons/awesome-fonts.json b/themes/icons/awesome-fonts.json index 2d05dc6..9f14da9 100644 --- a/themes/icons/awesome-fonts.json +++ b/themes/icons/awesome-fonts.json @@ -262,6 +262,9 @@ "unmuted": { "prefix": "" }, "unknown": { "prefix": "" } }, + "rofication": { + "prefix": "" + }, "twmn": { "muted": { "prefix": "" }, "unmuted": { "prefix": "" } From 681bba4f125ec1a3f4c4e9a084144206bd819223 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= <6774676+eumiro@users.noreply.github.com> Date: Sun, 29 Nov 2020 22:14:33 +0100 Subject: [PATCH 151/506] add python 3.9 support --- .travis.yml | 2 ++ README.md | 2 +- docs/index.rst | 18 +++++++++--------- setup.cfg | 1 + 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index dbc9da8..4ae69a7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,8 @@ python: - "3.5" - "3.6" - "3.7" + - "3.8" + - "3.9" before_install: - sudo apt-get -qq update install: diff --git a/README.md b/README.md index 18e76a5..75ace2d 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Thanks a lot! Required i3wm version: 4.12+ (in earlier versions, blocks won't have background colors) -Supported Python versions: 3.4, 3.5, 3.6, 3.7, 3.8 +Supported Python versions: 3.4, 3.5, 3.6, 3.7, 3.8, 3.9 Supported FontAwesome version: 4 (free version of 5 doesn't include some of the icons) diff --git a/docs/index.rst b/docs/index.rst index 084a8fd..f405d91 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -20,15 +20,15 @@ feature requests, etc. :) Thanks a lot! -+------------------------------------+-------------------------+ -| **Required i3wm version** | 4.12+ | -+------------------------------------+-------------------------+ -| **Supported Python versions** | 3.4, 3.5, 3.6, 3.7, 3.8 | -+------------------------------------+-------------------------+ -| **Supported FontAwesome versions** | 4 only | -+------------------------------------+-------------------------+ -| **Per-module requirements** | see :doc:`modules` | -+------------------------------------+-------------------------+ ++------------------------------------+------------------------------+ +| **Required i3wm version** | 4.12+ | ++------------------------------------+------------------------------+ +| **Supported Python versions** | 3.4, 3.5, 3.6, 3.7, 3.8, 3.9 | ++------------------------------------+------------------------------+ +| **Supported FontAwesome versions** | 4 only | ++------------------------------------+------------------------------+ +| **Per-module requirements** | see :doc:`modules` | ++------------------------------------+------------------------------+ see :doc:`FAQ` for details on this diff --git a/setup.cfg b/setup.cfg index 9352f13..081fab6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,6 +23,7 @@ classifiers = Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 Topic :: Software Development :: Libraries Topic :: Software Development :: Internationalization Topic :: Utilities From d0ee1b06e4394e1f139226ee2b76b286bd7df1ec Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Mon, 30 Nov 2020 17:21:17 +0100 Subject: [PATCH 152/506] [modules/nic] make exclude list regular expression capable Allow the usage of regexps in the exclude list, but keep the "this is a prefix" logic for backwards compatibility. should address (see #738) --- bumblebee_status/modules/core/nic.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/bumblebee_status/modules/core/nic.py b/bumblebee_status/modules/core/nic.py index 41607d9..667cffa 100644 --- a/bumblebee_status/modules/core/nic.py +++ b/bumblebee_status/modules/core/nic.py @@ -10,7 +10,7 @@ Requires the following executable: * (until and including 2.0.5: iwgetid) Parameters: - * nic.exclude: Comma-separated list of interface prefixes to exclude (defaults to 'lo,virbr,docker,vboxnet,veth,br') + * nic.exclude: Comma-separated list of interface prefixes (supporting regular expressions) to exclude (defaults to 'lo,virbr,docker,vboxnet,veth,br,.*:avahi') * nic.include: Comma-separated list of interfaces to include * nic.states: Comma-separated list of states to show (prefix with '^' to invert - i.e. ^down -> show all devices that are not in state down) * nic.format: Format string (defaults to '{intf} {state} {ip} {ssid}') @@ -32,13 +32,10 @@ class Module(core.module.Module): def __init__(self, config, theme): widgets = [] super().__init__(config, theme, widgets) - self._exclude = tuple( - filter( - len, - self.parameter("exclude", "lo,virbr,docker,vboxnet,veth,br").split(","), - ) + self._exclude = util.format.aslist( + self.parameter("exclude", "lo,virbr,docker,vboxnet,veth,br,.*:avahi") ) - self._include = self.parameter("include", "").split(",") + self._include = util.format.aslist(self.parameter("include", "")) self._states = {"include": [], "exclude": []} for state in tuple( @@ -90,11 +87,18 @@ class Module(core.module.Module): return [] return retval + def _excluded(self, intf): + for e in self._exclude: + if re.match(e, intf): + return True + return False + def _update_widgets(self, widgets): self.clear_widgets() - interfaces = [ - i for i in netifaces.interfaces() if not i.startswith(self._exclude) - ] + interfaces = [] + for i in netifaces.interfaces(): + if not self._excluded(i): + interfaces.append(i) interfaces.extend([i for i in netifaces.interfaces() if i in self._include]) for intf in interfaces: From f5e6bc12db8da5ab3f2948a02f714e8c14232e5b Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Mon, 30 Nov 2020 17:22:42 +0100 Subject: [PATCH 153/506] [doc] update doc --- docs/modules.rst | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/modules.rst b/docs/modules.rst index 9ebca7f..8ec9ecf 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -158,7 +158,7 @@ Requires the following executable: * (until and including 2.0.5: iwgetid) Parameters: - * nic.exclude: Comma-separated list of interface prefixes to exclude (defaults to 'lo,virbr,docker,vboxnet,veth,br') + * nic.exclude: Comma-separated list of interface prefixes (supporting regular expressions) to exclude (defaults to 'lo,virbr,docker,vboxnet,veth,br,.*:avahi') * nic.include: Comma-separated list of interfaces to include * nic.states: Comma-separated list of states to show (prefix with '^' to invert - i.e. ^down -> show all devices that are not in state down) * nic.format: Format string (defaults to '{intf} {state} {ip} {ssid}') @@ -1137,6 +1137,17 @@ publicip Displays public IP address +rofication +~~~~~~~~~~ + +Rofication indicator + +https://github.com/DaveDavenport/Rofication +simple module to show an icon + the number of notifications stored in rofication +module will have normal highlighting if there are zero notifications, + "warning" highlighting if there are nonzero notifications, + "critical" highlighting if there are any critical notifications + rotation ~~~~~~~~ From de01d96b915b8a7698f082970dd0ca51ae81afef Mon Sep 17 00:00:00 2001 From: Florian Eich Date: Mon, 30 Nov 2020 23:21:21 +0100 Subject: [PATCH 154/506] Change iw call in module nic from link to info --- bumblebee_status/modules/core/nic.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/bumblebee_status/modules/core/nic.py b/bumblebee_status/modules/core/nic.py index 667cffa..70ea1cf 100644 --- a/bumblebee_status/modules/core/nic.py +++ b/bumblebee_status/modules/core/nic.py @@ -134,13 +134,15 @@ class Module(core.module.Module): widget.set("state", state) def get_ssid(self, intf): - if self._iswlan(intf) and not self._istunnel(intf) and self.iw: - ssid = util.cli.execute("{} dev {} link".format(self.iw, intf)) - found_ssid = re.findall("SSID:\s(.+)", ssid) - if len(found_ssid) > 0: - return found_ssid[0] - else: - return "" + if not self._iswlan(intf) or self._istunnel(intf) or not self.iw: + return "" + + iw_info = util.cli.execute("{} dev {} info".format(self.iw, intf)) + for line in iw_info.split("\n"): + match = re.match("^\s+ssid\s(.+)$", line) + if match: + return match.group(1) + return "" From a8d1254e067a60d61df8a5c704d3ced57388441c Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Tue, 1 Dec 2020 15:58:55 +0100 Subject: [PATCH 155/506] [modules/nic] Make regex for SSID a raw string --- bumblebee_status/modules/core/nic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bumblebee_status/modules/core/nic.py b/bumblebee_status/modules/core/nic.py index 70ea1cf..7dde2f0 100644 --- a/bumblebee_status/modules/core/nic.py +++ b/bumblebee_status/modules/core/nic.py @@ -139,7 +139,7 @@ class Module(core.module.Module): iw_info = util.cli.execute("{} dev {} info".format(self.iw, intf)) for line in iw_info.split("\n"): - match = re.match("^\s+ssid\s(.+)$", line) + match = re.match(r"^\s+ssid\s(.+)$", line) if match: return match.group(1) From 2de39be731a540dc98b5879851ff29c80bf3f530 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Wed, 2 Dec 2020 11:07:19 +0100 Subject: [PATCH 156/506] [travis/code climate] update to new reporter + small fixes --- .travis.yml | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4ae69a7..00a9074 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,9 @@ -sudo: false +dist: xenial +os: linux language: python +env: + global: + - CC_TEST_REPORTER_ID=40cb00907f7a10e04868e856570bb997ab9c42fd3b63d980f2b2269433195fdf python: - "3.4" - "3.5" @@ -7,12 +11,15 @@ python: - "3.7" - "3.8" - "3.9" +before_script: + - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter + - chmod +x ./cc-test-reporter + - ./cc-test-reporter before-build before_install: - - sudo apt-get -qq update + - apt-get -qq update install: - - sudo apt-get install python-dbus - - pip install -U coverage==4.3 pytest pytest-mock freezegun - - pip install codeclimate-test-reporter + - apt-get install python-dbus + - pip install -U coverage pytest pytest-mock freezegun - pip install i3-py Pillow Babel DateTime python-dateutil - pip install docker feedparser==6.0.0b1 i3ipc - pip install netifaces power @@ -22,7 +29,5 @@ install: - pip install tzlocal script: - coverage run --source=. -m pytest tests -v - - CODECLIMATE_REPO_TOKEN=40cb00907f7a10e04868e856570bb997ab9c42fd3b63d980f2b2269433195fdf codeclimate-test-reporter -addons: - code_climate: - repo_token: 40cb00907f7a10e04868e856570bb997ab9c42fd3b63d980f2b2269433195fdf +after_script: + - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT From 3c08eafa4a401b641c48d069e846fbb301e7fe2c Mon Sep 17 00:00:00 2001 From: Florian Eich Date: Wed, 2 Dec 2020 19:08:45 +0100 Subject: [PATCH 157/506] Add TiB to disk units, add SI unit option for disk space --- bumblebee_status/modules/core/disk.py | 8 +++++--- bumblebee_status/util/format.py | 21 ++++++++++++++++----- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/bumblebee_status/modules/core/disk.py b/bumblebee_status/modules/core/disk.py index 12c7a16..ff0d4f9 100644 --- a/bumblebee_status/modules/core/disk.py +++ b/bumblebee_status/modules/core/disk.py @@ -8,6 +8,7 @@ Parameters: * disk.path: Path to calculate disk usage from (defaults to /) * disk.open: Which application / file manager to launch (default xdg-open) * disk.format: Format string, tags {path}, {used}, {left}, {size} and {percent} (defaults to '{path} {used}/{size} ({percent:05.02f}%)') + * disk.system: Unit system to use - SI (KB, MB, ...) or IEC (KiB, MiB, ...) (defaults to 'IEC') """ import os @@ -25,6 +26,7 @@ class Module(core.module.Module): self._path = self.parameter("path", "/") self._format = self.parameter("format", "{used}/{size} ({percent:05.02f}%)") + self._system = self.parameter("system", "IEC") self._used = 0 self._left = 0 @@ -38,9 +40,9 @@ class Module(core.module.Module): ) def diskspace(self, widget): - used_str = util.format.byte(self._used) - size_str = util.format.byte(self._size) - left_str = util.format.byte(self._left) + used_str = util.format.byte(self._used, sys=self._system) + size_str = util.format.byte(self._size, sys=self._system) + left_str = util.format.byte(self._left, sys=self._system) percent_str = self._percent return self._format.format( diff --git a/bumblebee_status/util/format.py b/bumblebee_status/util/format.py index 65242a3..3acf440 100644 --- a/bumblebee_status/util/format.py +++ b/bumblebee_status/util/format.py @@ -71,22 +71,33 @@ def astemperature(val, unit="metric"): return "{}°{}".format(int(val), __UNITS.get(unit.lower(), __UNITS["default"])) -def byte(val, fmt="{:.2f}"): +def byte(val, fmt="{:.2f}", sys="IEC"): """Returns a byte representation of the input value :param val: value to format, must be convertible into a float :param fmt: optional output format string, defaults to {:.2f} + :param sys: optional unit system specifier - SI (kilo, Mega, Giga, ...) or + IEC (kibi, Mebi, Gibi, ...) - defaults to IEC :return: byte representation (e.g. KiB, GiB, etc.) of the input value :rtype: string """ + if sys == "IEC": + div = 1024.0 + units = ["", "Ki", "Mi", "Gi", "Ti"] + final = "TiB" + elif sys == "SI": + div = 1000.0 + units = ["", "K", "M", "G", "T"] + final = "TB" + val = float(val) - for unit in ["", "Ki", "Mi", "Gi"]: - if val < 1024.0: + for unit in units: + if val < div: return "{}{}B".format(fmt, unit).format(val) - val /= 1024.0 - return "{}GiB".format(fmt).format(val * 1024.0) + val /= div + return "{}{}".format(fmt).format(val * div, final) __seconds_pattern = re.compile(r"(([\d\.?]+)h)?(([\d\.]+)m)?([\d\.]+)?s?") From efebc8d0490f9d217e39b2b374b89dd86eb704d2 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Wed, 2 Dec 2020 20:53:43 +0100 Subject: [PATCH 158/506] [travis] add sudo (accidentially removed) --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 00a9074..4566db0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,9 +16,9 @@ before_script: - chmod +x ./cc-test-reporter - ./cc-test-reporter before-build before_install: - - apt-get -qq update + - sudo apt-get -qq update install: - - apt-get install python-dbus + - sudo apt-get install python-dbus - pip install -U coverage pytest pytest-mock freezegun - pip install i3-py Pillow Babel DateTime python-dateutil - pip install docker feedparser==6.0.0b1 i3ipc From cd851340e2a72342fc10b7ae975c2150bf98db4f Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Wed, 2 Dec 2020 21:20:40 +0100 Subject: [PATCH 159/506] [pip] updated/fixed dependencies many thanks to @jayvdb for pointing those out! fixes #741 --- bumblebee_status/modules/contrib/deadbeef.py | 2 -- bumblebee_status/modules/contrib/spaceapi.py | 1 - requirements/modules/battery.txt | 1 + requirements/modules/battery_upower_reqs.txt | 1 + requirements/modules/currency.txt | 1 + requirements/modules/dunst.txt | 1 - requirements/modules/hddtemp.txt | 1 - requirements/modules/libvirtvms.txt | 1 + requirements/modules/octoprint.txt | 2 ++ requirements/modules/spaceapi.txt | 2 -- requirements/modules/speedtest.txt | 1 + requirements/modules/yubikey.txt | 2 +- requirements/modules/zpool.txt | 1 + 13 files changed, 9 insertions(+), 8 deletions(-) create mode 100644 requirements/modules/battery.txt delete mode 100644 requirements/modules/dunst.txt delete mode 100644 requirements/modules/hddtemp.txt create mode 100644 requirements/modules/libvirtvms.txt create mode 100644 requirements/modules/octoprint.txt create mode 100644 requirements/modules/speedtest.txt create mode 100644 requirements/modules/zpool.txt diff --git a/bumblebee_status/modules/contrib/deadbeef.py b/bumblebee_status/modules/contrib/deadbeef.py index 948fdc5..0bbba41 100644 --- a/bumblebee_status/modules/contrib/deadbeef.py +++ b/bumblebee_status/modules/contrib/deadbeef.py @@ -5,8 +5,6 @@ some media control bindings. Left click toggles pause, scroll up skips the current song, scroll down returns to the previous song. -Requires the following library: - * subprocess Parameters: * deadbeef.format: Format string (defaults to '{artist} - {title}') Available values are: {artist}, {title}, {album}, {length}, diff --git a/bumblebee_status/modules/contrib/spaceapi.py b/bumblebee_status/modules/contrib/spaceapi.py index d29c081..a6c2772 100644 --- a/bumblebee_status/modules/contrib/spaceapi.py +++ b/bumblebee_status/modules/contrib/spaceapi.py @@ -9,7 +9,6 @@ an example. Requires the following libraries: * requests - * regex Parameters: * spaceapi.url: String representation of the api endpoint diff --git a/requirements/modules/battery.txt b/requirements/modules/battery.txt new file mode 100644 index 0000000..d028b9b --- /dev/null +++ b/requirements/modules/battery.txt @@ -0,0 +1 @@ +power diff --git a/requirements/modules/battery_upower_reqs.txt b/requirements/modules/battery_upower_reqs.txt index e2182f1..a7fc9f1 100644 --- a/requirements/modules/battery_upower_reqs.txt +++ b/requirements/modules/battery_upower_reqs.txt @@ -1 +1,2 @@ dbus +power diff --git a/requirements/modules/currency.txt b/requirements/modules/currency.txt index f229360..126f5e8 100644 --- a/requirements/modules/currency.txt +++ b/requirements/modules/currency.txt @@ -1 +1,2 @@ requests +Babel diff --git a/requirements/modules/dunst.txt b/requirements/modules/dunst.txt deleted file mode 100644 index 053749f..0000000 --- a/requirements/modules/dunst.txt +++ /dev/null @@ -1 +0,0 @@ -dunst diff --git a/requirements/modules/hddtemp.txt b/requirements/modules/hddtemp.txt deleted file mode 100644 index b4e9f6f..0000000 --- a/requirements/modules/hddtemp.txt +++ /dev/null @@ -1 +0,0 @@ -hddtemp diff --git a/requirements/modules/libvirtvms.txt b/requirements/modules/libvirtvms.txt new file mode 100644 index 0000000..76aa22c --- /dev/null +++ b/requirements/modules/libvirtvms.txt @@ -0,0 +1 @@ +libvirt diff --git a/requirements/modules/octoprint.txt b/requirements/modules/octoprint.txt new file mode 100644 index 0000000..2e1fb7c --- /dev/null +++ b/requirements/modules/octoprint.txt @@ -0,0 +1,2 @@ +Pillow +simplejson diff --git a/requirements/modules/spaceapi.txt b/requirements/modules/spaceapi.txt index 3a2dc45..f229360 100644 --- a/requirements/modules/spaceapi.txt +++ b/requirements/modules/spaceapi.txt @@ -1,3 +1 @@ requests -json -time diff --git a/requirements/modules/speedtest.txt b/requirements/modules/speedtest.txt new file mode 100644 index 0000000..4b024b7 --- /dev/null +++ b/requirements/modules/speedtest.txt @@ -0,0 +1 @@ +speedtest-cli diff --git a/requirements/modules/yubikey.txt b/requirements/modules/yubikey.txt index 0ad4a6b..3466c66 100644 --- a/requirements/modules/yubikey.txt +++ b/requirements/modules/yubikey.txt @@ -1 +1 @@ -yubico +python-yubico diff --git a/requirements/modules/zpool.txt b/requirements/modules/zpool.txt new file mode 100644 index 0000000..49fe098 --- /dev/null +++ b/requirements/modules/zpool.txt @@ -0,0 +1 @@ +setuptools From 38c4b46351496aa2cb8bf40e639e58246bc7efbb Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Thu, 3 Dec 2020 07:56:36 +0700 Subject: [PATCH 160/506] libvirtvms.txt: Fix PyPI dependency name Related to https://github.com/tobi-wan-kenobi/bumblebee-status/issues/741 --- requirements/modules/libvirtvms.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/modules/libvirtvms.txt b/requirements/modules/libvirtvms.txt index 76aa22c..ff1a357 100644 --- a/requirements/modules/libvirtvms.txt +++ b/requirements/modules/libvirtvms.txt @@ -1 +1 @@ -libvirt +libvirt-python From 659a0147da7fda458ed3d1c8751eef2483a733d9 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Thu, 3 Dec 2020 08:02:12 +0700 Subject: [PATCH 161/506] system.txt: Replace tk with Pillow Related to https://github.com/tobi-wan-kenobi/bumblebee-status/issues/741 --- requirements/modules/system.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/modules/system.txt b/requirements/modules/system.txt index 5d6fce4..aa8980c 100644 --- a/requirements/modules/system.txt +++ b/requirements/modules/system.txt @@ -1 +1 @@ -tkinter +Pillow # placeholder for tk From 071a69b2d87776da26a78d0669f754f7081b578d Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Thu, 3 Dec 2020 08:02:41 +0700 Subject: [PATCH 162/506] .travis.yml: Install declared PyPI dependencies --- .travis.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4566db0..47c124c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,13 +20,7 @@ before_install: install: - sudo apt-get install python-dbus - pip install -U coverage pytest pytest-mock freezegun - - pip install i3-py Pillow Babel DateTime python-dateutil - - pip install docker feedparser==6.0.0b1 i3ipc - - pip install netifaces power - - pip install psutil pytz - - pip install requests simplejson - - pip install suntime - - pip install tzlocal + - pip install $(cat requirements/modules/*.txt | cut -d ' ' -f 1 | sort -u) script: - coverage run --source=. -m pytest tests -v after_script: From 21268d7d861774e7e1a9677b6e77a8076627c1f8 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Thu, 3 Dec 2020 10:55:23 +0700 Subject: [PATCH 163/506] Use dbus-python PyPI package --- requirements/modules/battery_upower_reqs.txt | 2 +- requirements/modules/spotify.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/modules/battery_upower_reqs.txt b/requirements/modules/battery_upower_reqs.txt index a7fc9f1..a8ec395 100644 --- a/requirements/modules/battery_upower_reqs.txt +++ b/requirements/modules/battery_upower_reqs.txt @@ -1,2 +1,2 @@ -dbus +dbus-python power diff --git a/requirements/modules/spotify.txt b/requirements/modules/spotify.txt index e2182f1..555438c 100644 --- a/requirements/modules/spotify.txt +++ b/requirements/modules/spotify.txt @@ -1 +1 @@ -dbus +dbus-python From a58454de97d3003fc558284e92392a07bf708910 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Thu, 3 Dec 2020 12:54:44 +0700 Subject: [PATCH 164/506] .travis.yml: Allow building dbus-python --- .travis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 47c124c..72e3881 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,3 @@ -dist: xenial os: linux language: python env: @@ -15,10 +14,11 @@ before_script: - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter - chmod +x ./cc-test-reporter - ./cc-test-reporter before-build -before_install: - - sudo apt-get -qq update +addons: + apt: + packages: + libdbus-1-dev install: - - sudo apt-get install python-dbus - pip install -U coverage pytest pytest-mock freezegun - pip install $(cat requirements/modules/*.txt | cut -d ' ' -f 1 | sort -u) script: From e0fc98bfb0e813b2bee8db9a040837caf9b8e157 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Thu, 3 Dec 2020 20:48:06 +0100 Subject: [PATCH 165/506] [travis] temporarily exclude dbus and libvirt from requirements since they cause issues in the automated tests, do not install the dbus and libvirt python libraries. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 72e3881..e9708a5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,7 @@ addons: libdbus-1-dev install: - pip install -U coverage pytest pytest-mock freezegun - - pip install $(cat requirements/modules/*.txt | cut -d ' ' -f 1 | sort -u) + - pip install $(cat requirements/modules/*.txt | cut -d ' ' -f 1 | sort -u|grep -v dbus-python|grep -v libvirt-python) script: - coverage run --source=. -m pytest tests -v after_script: From 3937e73e7df61ade2fc6f45e7e5baa111e10fb7d Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Fri, 4 Dec 2020 18:53:52 +0100 Subject: [PATCH 166/506] [travis] hopefully fixed test build locally tested with a xenial image, and this should work. Unfortunately, cannot get the pygit2 to build against the provided libgit2-dev, so using the packaged version instead. --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e9708a5..a9bb5fb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,9 +18,10 @@ addons: apt: packages: libdbus-1-dev + libffi-dev install: - pip install -U coverage pytest pytest-mock freezegun - - pip install $(cat requirements/modules/*.txt | cut -d ' ' -f 1 | sort -u|grep -v dbus-python|grep -v libvirt-python) + - pip install $(cat requirements/modules/*.txt | cut -d ' ' -f 1 | sort -u|grep -v dbus-python|grep -v libvirt-python|grep -v Pillow|grep -v pygit2) script: - coverage run --source=. -m pytest tests -v after_script: From 5ff626398673f2c71044ec488230ef6ac55834bc Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Fri, 4 Dec 2020 18:55:21 +0100 Subject: [PATCH 167/506] [travis] forgotten in previous commit --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index a9bb5fb..ee38e1e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,6 +19,7 @@ addons: packages: libdbus-1-dev libffi-dev + python3-pygit2 install: - pip install -U coverage pytest pytest-mock freezegun - pip install $(cat requirements/modules/*.txt | cut -d ' ' -f 1 | sort -u|grep -v dbus-python|grep -v libvirt-python|grep -v Pillow|grep -v pygit2) From b5988694508e374594b5ffef488851f56676d835 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Fri, 4 Dec 2020 18:48:49 +0700 Subject: [PATCH 168/506] .travis.yml: Add taskwarrior Taskwarrior package requires task to be installed. --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index ee38e1e..079ecc2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,8 +18,7 @@ addons: apt: packages: libdbus-1-dev - libffi-dev - python3-pygit2 + taskwarrior install: - pip install -U coverage pytest pytest-mock freezegun - pip install $(cat requirements/modules/*.txt | cut -d ' ' -f 1 | sort -u|grep -v dbus-python|grep -v libvirt-python|grep -v Pillow|grep -v pygit2) From d0e309ad0f72350e2833d9c84ff523c7f5f7ebb0 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Sat, 5 Dec 2020 05:48:59 +0700 Subject: [PATCH 169/506] test_format: Allow 2.00TiB --- tests/util/test_format.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/util/test_format.py b/tests/util/test_format.py index 1e34dec..dc69ced 100644 --- a/tests/util/test_format.py +++ b/tests/util/test_format.py @@ -71,7 +71,7 @@ def test_byteformat(): assert byte(1024 + 512) == "1.50KiB" assert byte(1024 * 1024 * 2 + 1024 * 512) == "2.50MiB" assert byte(1024 * 1024 * 1024 * 4 + 1024 * 1024 * 512) == "4.50GiB" - assert byte(1024 * 1024 * 1024 * 1024 * 2) == "2048.00GiB" + assert byte(1024 * 1024 * 1024 * 1024 * 2) in ["2048.00GiB", "2.00TiB"] def test_duration(): From a018343af68e4e599c712548ff2193ae4d191fe8 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Sat, 5 Dec 2020 07:37:06 +0700 Subject: [PATCH 170/506] .travis.yml: Add libgit2-dev And pre-install pygit2<1 for Python 3.4 and 3.5 support. --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 079ecc2..7d1bbc0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,10 +18,12 @@ addons: apt: packages: libdbus-1-dev + libgit2-dev taskwarrior install: - pip install -U coverage pytest pytest-mock freezegun - - pip install $(cat requirements/modules/*.txt | cut -d ' ' -f 1 | sort -u|grep -v dbus-python|grep -v libvirt-python|grep -v Pillow|grep -v pygit2) + - pip install 'pygit2<1' || true + - pip install $(cat requirements/modules/*.txt | cut -d ' ' -f 1 | sort -u) script: - coverage run --source=. -m pytest tests -v after_script: From e8728973461f6a45ab96b678e8babc4064f13253 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Fri, 4 Dec 2020 17:08:44 +0700 Subject: [PATCH 171/506] .travis.yml: Pre-install libvirt-dev Also install libvirt-python<6.3 for Python 3.4 support. --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7d1bbc0..ca3b78d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,10 +19,11 @@ addons: packages: libdbus-1-dev libgit2-dev + libvirt-dev taskwarrior install: - pip install -U coverage pytest pytest-mock freezegun - - pip install 'pygit2<1' || true + - pip install 'pygit2<1' 'libvirt-python<6.3' || true - pip install $(cat requirements/modules/*.txt | cut -d ' ' -f 1 | sort -u) script: - coverage run --source=. -m pytest tests -v From 9e9dff7658d8417151b5c4f82f27749457180ad5 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Sat, 5 Dec 2020 16:54:59 +0700 Subject: [PATCH 172/506] rss.txt: Unpin feedparser And install feedparser<6 on Travis for Python 3.4-5. --- .travis.yml | 2 +- requirements/modules/rss.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index ca3b78d..369aeb7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,7 +23,7 @@ addons: taskwarrior install: - pip install -U coverage pytest pytest-mock freezegun - - pip install 'pygit2<1' 'libvirt-python<6.3' || true + - pip install 'pygit2<1' 'libvirt-python<6.3' 'feedparser<6' || true - pip install $(cat requirements/modules/*.txt | cut -d ' ' -f 1 | sort -u) script: - coverage run --source=. -m pytest tests -v diff --git a/requirements/modules/rss.txt b/requirements/modules/rss.txt index 4373b0d..1b25361 100644 --- a/requirements/modules/rss.txt +++ b/requirements/modules/rss.txt @@ -1 +1 @@ -feedparser==6.0.0b1 +feedparser From fa668735822f9e2b3054a38b661c905243016d77 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Mon, 7 Dec 2020 08:59:56 +0100 Subject: [PATCH 173/506] [core/theme] add xresources support add support for reading foreground and background, and colors, from xresources.py many thanks to @Aliuakbar for the addition! fixes #731 --- bumblebee_status/core/theme.py | 13 +++++++++++-- bumblebee_status/util/xresources.py | 10 ++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 bumblebee_status/util/xresources.py diff --git a/bumblebee_status/core/theme.py b/bumblebee_status/core/theme.py index b52c465..800bd66 100644 --- a/bumblebee_status/core/theme.py +++ b/bumblebee_status/core/theme.py @@ -7,6 +7,7 @@ import glob import core.event import util.algorithm +import util.xresources log = logging.getLogger(__name__) @@ -89,13 +90,21 @@ class Theme(object): try: if isinstance(name, dict): return name + + result = {} if name.lower() == "wal": wal = self.__load_json("~/.cache/wal/colors.json") - result = {} for field in ["special", "colors"]: for key in wal.get(field, {}): result[key] = wal[field][key] - return result + if name.lower() == "xresources": + for key in ("background", "foreground"): + result[key] = xresources.query(key) + for i in range(16): + key = color + str(i) + result[key] = xresources.query(key) + + return result except Exception as e: log.error("failed to load colors: {}", e) diff --git a/bumblebee_status/util/xresources.py b/bumblebee_status/util/xresources.py new file mode 100644 index 0000000..70665a9 --- /dev/null +++ b/bumblebee_status/util/xresources.py @@ -0,0 +1,10 @@ +import subprocess +import shutil + +def query(key): + if shutil.which("xgetres"): + return subprocess.run(["xgetres", key], + capture_output=True).stdout.decode("utf-8").strip() + else: + raise Exception("xgetres must be installed for this theme") + From 3644acce76c198c01b971a8a68b3a7bdf72b74d9 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Mon, 7 Dec 2020 10:00:20 +0100 Subject: [PATCH 174/506] [pip] adjust removed dependencies --- setup.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/setup.py b/setup.py index f9f2ee4..2e5bafb 100755 --- a/setup.py +++ b/setup.py @@ -20,11 +20,9 @@ EXTRAS_REQUIREMENTS_MAP = { "cpu2": read_module("cpu2"), "currency": read_module("currency"), "docker_ps": read_module("docker_ps"), - "dunst": read_module("dunst"), "getcrypto": read_module("getcrypto"), "git": read_module("git"), "github": read_module("github"), - "hddtemp": read_module("hddtemp"), "layout-xkb": read_module("layout_xkb"), "memory": read_module("memory"), "network_traffic": read_module("network_traffic"), From 601b2115ce9a45229f044c8ab0687428c7ef4a8b Mon Sep 17 00:00:00 2001 From: gkeep Date: Fri, 18 Dec 2020 13:56:26 +0300 Subject: [PATCH 175/506] Add initial spotifyd compatibility --- bumblebee_status/modules/contrib/spotify.py | 35 ++++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/bumblebee_status/modules/contrib/spotify.py b/bumblebee_status/modules/contrib/spotify.py index 2295609..7544b48 100644 --- a/bumblebee_status/modules/contrib/spotify.py +++ b/bumblebee_status/modules/contrib/spotify.py @@ -10,6 +10,8 @@ Parameters: Widget names are: spotify.song, spotify.prev, spotify.pause, spotify.next * spotify.concise_controls: When enabled, allows spotify to be controlled from just the spotify.song widget. Concise controls are: Left Click: Toggle Pause; Wheel Up: Next; Wheel Down; Previous. + * spotify.bus_name: String (defaults to `spotify`) + Available values: spotify, spotifyd contributed by `yvesh `_ - many thanks! @@ -35,6 +37,8 @@ class Module(core.module.Module): self.background = True + self.__bus_name = self.parameter("bus_name", "spotify") + self.__layout = util.format.aslist( self.parameter( "layout", "spotify.song,spotify.prev,spotify.pause,spotify.next", @@ -46,7 +50,11 @@ class Module(core.module.Module): self.__pause = "" self.__format = self.parameter("format", "{artist} - {title}") - self.__cmd = "dbus-send --session --type=method_call --dest=org.mpris.MediaPlayer2.spotify \ + if self.__bus_name == "spotifyd": + self.__cmd = "dbus-send --session --type=method_call --dest=org.mpris.MediaPlayer2.spotifyd \ + /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player." + else: + self.__cmd = "dbus-send --session --type=method_call --dest=org.mpris.MediaPlayer2.spotify \ /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player." widget_map = {} @@ -104,9 +112,14 @@ class Module(core.module.Module): def __get_song(self): bus = self.__bus - spotify = bus.get_object( - "org.mpris.MediaPlayer2.spotify", "/org/mpris/MediaPlayer2" - ) + if self.__bus_name == "spotifyd": + spotify = bus.get_object( + "org.mpris.MediaPlayer2.spotifyd", "/org/mpris/MediaPlayer2" + ) + else: + spotify = bus.get_object( + "org.mpris.MediaPlayer2.spotify", "/org/mpris/MediaPlayer2" + ) spotify_iface = dbus.Interface(spotify, "org.freedesktop.DBus.Properties") props = spotify_iface.Get("org.mpris.MediaPlayer2.Player", "Metadata") self.__song = self.__format.format( @@ -120,14 +133,20 @@ class Module(core.module.Module): try: self.__get_song() + if self.__bus_name == "spotifyd": + bus = self.__bus.get_object( + "org.mpris.MediaPlayer2.spotifyd", "/org/mpris/MediaPlayer2" + ) + else: + bus = self.__bus.get_object( + "org.mpris.MediaPlayer2.spotify", "/org/mpris/MediaPlayer2" + ) + for widget in self.widgets(): if widget.name == "spotify.pause": playback_status = str( dbus.Interface( - self.__bus.get_object( - "org.mpris.MediaPlayer2.spotify", - "/org/mpris/MediaPlayer2", - ), + bus, "org.freedesktop.DBus.Properties", ).Get("org.mpris.MediaPlayer2.Player", "PlaybackStatus") ) From a94114dd9496d801ce262bea721fa89436f7981c Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sat, 19 Dec 2020 13:07:29 +0100 Subject: [PATCH 176/506] [core/module] better error reporting for failed module loads if a module fails to load, explicitly log errors for core and contrib in the error log, but be a bit less verbose (and less confusing) in the module error message itself. fixes #747 --- bumblebee_status/core/module.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/bumblebee_status/core/module.py b/bumblebee_status/core/module.py index f4bac7e..dad06e2 100644 --- a/bumblebee_status/core/module.py +++ b/bumblebee_status/core/module.py @@ -33,20 +33,20 @@ def load(module_name, config=core.config.Config([]), theme=None): error = None module_short, alias = (module_name.split(":") + [module_name])[0:2] config.set("__alias__", alias) - for namespace in ["core", "contrib"]: + + try: + mod = importlib.import_module("modules.core.{}".format(module_short)) + log.debug("importing {} from core".format(module_short)) + return getattr(mod, "Module")(config, theme) + except ImportError as e: try: - mod = importlib.import_module( - "modules.{}.{}".format(namespace, module_short) - ) - log.debug( - "importing {} from {}.{}".format(module_short, namespace, module_short) - ) + log.warning("failed to import {} from core: {}".format(module_short, e)) + mod = importlib.import_module("modules.contrib.{}".format(module_short)) + log.debug("importing {} from contrib".format(module_short)) return getattr(mod, "Module")(config, theme) except ImportError as e: - log.debug("failed to import {}: {}".format(module_name, e)) - error = e - log.fatal("failed to import {}: {}".format(module_name, error)) - return Error(config=config, module=module_name, error=error) + log.fatal("failed to import {} from contrib: {}".format(module_short, e)) + return Error(config=config, module=module_name, error="unable to load module") class Module(core.input.Object): From 7fd4f710a12c213b19b02cbbde4c7cb9247bf09d Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sat, 19 Dec 2020 13:17:05 +0100 Subject: [PATCH 177/506] [doc] improve interval documentation Add information about global vs. per-module intervals. see #751 --- docs/introduction.rst | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/introduction.rst b/docs/introduction.rst index 927a384..a27b8c7 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -56,8 +56,16 @@ To change the update interval, use: $ ./bumblebee-status -m -p interval= +Note that some modules define their own intervals (e.g. most modules that query +an online service), such as to not cause a storm of "once every second" queries. + +For such modules, the "global" interval defined here effectively defines the +highest possible "resolution". If you have a global interval of 10s, for example, +any other module can update at 10s, 20s, 30s, etc., but not every 25s. The status +bar will internally always align to the next future time slot. + The update interval can also be changed on a per-module basis, like -this: +this (overriding the default module interval indicated above): .. code-block:: bash From 7b1659a1b594cc49f8207a2858b499086cda27a8 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sun, 20 Dec 2020 10:23:06 +0100 Subject: [PATCH 178/506] [core/theme] add /usr/share as theme directory add a theme directory /usr/share/bumblebee-status/themes for system-wide theme installation. fixes #753 --- bumblebee_status/core/theme.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bumblebee_status/core/theme.py b/bumblebee_status/core/theme.py index 800bd66..4de58d3 100644 --- a/bumblebee_status/core/theme.py +++ b/bumblebee_status/core/theme.py @@ -17,6 +17,7 @@ PATHS = [ os.path.join(THEME_BASE_DIR, "../../themes"), os.path.expanduser("~/.config/bumblebee-status/themes"), os.path.expanduser("~/.local/share/bumblebee-status/themes"), # PIP + "/usr/share/bumblebee-status/themes", ] From 73b071edb0c8c0c63a22758a12704be8b1d9a521 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sun, 20 Dec 2020 15:11:35 +0100 Subject: [PATCH 179/506] [doc] clarify interval handling further fixes #751 --- docs/features.rst | 19 +++++++++++++++++++ docs/introduction.rst | 17 ++++++----------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/docs/features.rst b/docs/features.rst index f48ebf0..f167033 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -1,6 +1,25 @@ Advanced usage =========================== +Intervals +--------- + +Some modules define their own update intervals (e.g. most modules that query +an online service), such as to not cause a storm of "once every second" queries. + +For such modules, the "global" interval defined via the ``interval`` parameter effectively defines the +highest possible "resolution". If you have a global interval of 10s, for example, +any other module can update at 10s, 20s, 30s, etc., but not every 25s. The status +bar will internally always align to the next future time slot. + +The update interval can also be changed on a per-module basis, like +this (overriding the default module interval indicated above): + +.. code-block:: bash + + $ ./bumblebee-status -m cpu memory -p cpu.interval=5s memory.interval=1m + + Events ------ diff --git a/docs/introduction.rst b/docs/introduction.rst index a27b8c7..7c3de02 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -56,20 +56,15 @@ To change the update interval, use: $ ./bumblebee-status -m -p interval= +The update interval is the global "refresh" interval of the modules (i.e. how often +the bar will be updated with new data). The default interval is one second. It is +possible to use suffixes such as "m" (for minutes), or "h" for hours (e.g. +``-p interval=5m`` to update once every 5 minutes. + Note that some modules define their own intervals (e.g. most modules that query an online service), such as to not cause a storm of "once every second" queries. -For such modules, the "global" interval defined here effectively defines the -highest possible "resolution". If you have a global interval of 10s, for example, -any other module can update at 10s, 20s, 30s, etc., but not every 25s. The status -bar will internally always align to the next future time slot. - -The update interval can also be changed on a per-module basis, like -this (overriding the default module interval indicated above): - -.. code-block:: bash - - $ ./bumblebee-status -m cpu memory -p cpu.interval=5s memory.interval=1m +For more details on that, please refer to :doc:`features`. All modules can be given “aliases” using ``:``, by which they can be parametrized, for example: From 960792b2e5ffe999d1559fa27720ff482e165a9b Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sun, 27 Dec 2020 14:49:12 +0100 Subject: [PATCH 180/506] [tests] fix module load test for python 3.6 and further --- tests/core/test_module.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/core/test_module.py b/tests/core/test_module.py index 68415b7..5383c8b 100644 --- a/tests/core/test_module.py +++ b/tests/core/test_module.py @@ -55,7 +55,8 @@ def test_importerror(mocker): module = core.module.load(module_name="test", config=config) assert module.__class__.__name__ == "Error" - assert module.widget().full_text() == "test: some-error" + assert module.widget().full_text() == "test: some-error" or + module.widget().full_text() == "test: unable to load module" def test_loadvalid_module(): From af95b2e27615891ed16b4e1b87c53525b35d6607 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sun, 27 Dec 2020 14:52:45 +0100 Subject: [PATCH 181/506] [tests] fix syntax error --- tests/core/test_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/core/test_module.py b/tests/core/test_module.py index 5383c8b..4bca263 100644 --- a/tests/core/test_module.py +++ b/tests/core/test_module.py @@ -55,7 +55,7 @@ def test_importerror(mocker): module = core.module.load(module_name="test", config=config) assert module.__class__.__name__ == "Error" - assert module.widget().full_text() == "test: some-error" or + assert module.widget().full_text() == "test: some-error" or \ module.widget().full_text() == "test: unable to load module" From 436cea8f374577f5d5df5e7a9e4ade07eb9ccce0 Mon Sep 17 00:00:00 2001 From: gkeep Date: Sun, 27 Dec 2020 19:02:45 +0300 Subject: [PATCH 182/506] [modules/playerctl] Add format and layout parameters --- bumblebee_status/modules/contrib/playerctl.py | 132 +++++++++++++----- 1 file changed, 94 insertions(+), 38 deletions(-) diff --git a/bumblebee_status/modules/contrib/playerctl.py b/bumblebee_status/modules/contrib/playerctl.py index b145500..3a61c00 100755 --- a/bumblebee_status/modules/contrib/playerctl.py +++ b/bumblebee_status/modules/contrib/playerctl.py @@ -5,57 +5,113 @@ Requires the following executable: * playerctl -contributed by `smitajit `_ - many thanks! +Parameters: + * playerctl.format: Format string (defaults to '{artist} - {title}') + Available values are: {album}, {title}, {artist}, {trackNumber} + * playerctl.layout: Comma-separated list to change order of widgets (defaults to song, previous, pause, next) + Widget names are: playerctl.song, playerctl.prev, playerctl.pause, playerctl.next +Parameters are inherited from `spotify` module, many thanks to its developers! + +contributed by `smitajit `_ - many thanks! """ import core.module import core.widget import core.input import util.cli +import util.format + +import logging class Module(core.module.Module): - def __init__(self,config , theme): - widgets = [ - core.widget.Widget(name="playerctl.prev"), - core.widget.Widget(name="playerctl.main", full_text=self.description), - core.widget.Widget(name="playerctl.next"), - ] - super(Module, self).__init__(config, theme , widgets) + def __init__(self, config, theme): + super(Module, self).__init__(config, theme, []) - core.input.register(widgets[0], button=core.input.LEFT_MOUSE, - cmd="playerctl previous") - core.input.register(widgets[1], button=core.input.LEFT_MOUSE, - cmd="playerctl play-pause") - core.input.register(widgets[2], button=core.input.LEFT_MOUSE, - cmd="playerctl next") + self.background = True - self._status = None - self._tags = None + self.__layout = util.format.aslist( + self.parameter( + "layout", "playerctl.prev, playerctl.song, playerctl.pause, playerctl.next" + ) + ) - def description(self, widget): - return self._tags if self._tags else "..." + self.__song = "" + self.__cmd = "playerctl " + self.__format = self.parameter("format", "{artist} - {title}") + + widget_map = {} + for widget_name in self.__layout: + widget = self.add_widget(name=widget_name) + if widget_name == "playerctl.prev": + widget_map[widget] = { + "button": core.input.LEFT_MOUSE, + "cmd": self.__cmd + "previous", + } + widget.set("state", "prev") + elif widget_name == "playerctl.pause": + widget_map[widget] = { + "button": core.input.LEFT_MOUSE, + "cmd": self.__cmd + "play-pause", + } + elif widget_name == "playerctl.next": + widget_map[widget] = { + "button": core.input.LEFT_MOUSE, + "cmd": self.__cmd + "next", + } + widget.set("state", "next") + elif widget_name == "playerctl.song": + widget_map[widget] = [ + { + "button": core.input.LEFT_MOUSE, + "cmd": self.__cmd + "play-pause", + }, { + "button": core.input.WHEEL_UP, + "cmd": self.__cmd + "next", + }, { + "button": core.input.WHEEL_DOWN, + "cmd": self.__cmd + "previous", + } + ] + else: + raise KeyError( + "The playerctl module does not have a {widget_name!r} widget".format( + widget_name=widget_name + ) + ) + + for widget, callback_options in widget_map.items(): + if isinstance(callback_options, dict): + core.input.register(widget, **callback_options) def update(self): - self._load_song() - - def state(self, widget): - if widget.name == "playerctl.prev": - return "prev" - if widget.name == "playerctl.next": - return "next" - return self._status - - def _load_song(self): - info = "" try: - status = util.cli.execute("playerctl status").lower() - info = util.cli.execute("playerctl metadata xesam:title") - except : - self._status = None - self._tags = None - return - self._status = status.split("\n")[0].lower() - self._tags = info.split("\n")[0][:20] + self.__get_song() -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 + for widget in self.widgets(): + if widget.name == "playerctl.pause": + playback_status = str(util.cli.execute(self.__cmd + "status")).strip() + if playback_status != "": + if playback_status == "Playing": + widget.set("state", "playing") + else: + widget.set("state", "paused") + elif widget.name == "playerctl.song": + widget.set("state", "song") + widget.full_text(self.__song) + except Exception as e: + logging.exception(e) + self.__song = "" + + def __get_song(self): + album = str(util.cli.execute(self.__cmd + "metadata xesam:album")).strip() + title = str(util.cli.execute(self.__cmd + "metadata xesam:title")).strip() + artist = str(util.cli.execute(self.__cmd + "metadata xesam:albumArtist")).strip() + track_number = str(util.cli.execute(self.__cmd + "metadata xesam:trackNumber")).strip() + + self.__song = self.__format.format( + album = album, + title = title, + artist = artist, + track_number = track_number + ) From b74ebce702c5f272c71a26a68e8f7d688043aebb Mon Sep 17 00:00:00 2001 From: gkeep Date: Sun, 27 Dec 2020 19:12:36 +0300 Subject: [PATCH 183/506] [modules/playerctl] Small fix --- bumblebee_status/modules/contrib/playerctl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bumblebee_status/modules/contrib/playerctl.py b/bumblebee_status/modules/contrib/playerctl.py index 3a61c00..3a0d7e5 100755 --- a/bumblebee_status/modules/contrib/playerctl.py +++ b/bumblebee_status/modules/contrib/playerctl.py @@ -113,5 +113,5 @@ class Module(core.module.Module): album = album, title = title, artist = artist, - track_number = track_number + trackNumber = track_number ) From 5e790b7496b8128c67656c3c5218250a345e30f5 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Thu, 31 Dec 2020 13:21:35 +0100 Subject: [PATCH 184/506] [doc] Add reference to slackbuild fixes #755 --- README.md | 2 ++ docs/introduction.rst | 3 +++ 2 files changed, 5 insertions(+) diff --git a/README.md b/README.md index 75ace2d..7268a16 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,8 @@ makepkg -sicr pip install --user bumblebee-status ``` +There is also a SlackBuild available here: [slackbuilds:bumblebee-status](http://slackbuilds.org/repository/14.2/desktop/bumblebee-status/) - many thanks to [@Tonus1](https://github.com/Tonus1)! + # Dependencies [Available modules](https://bumblebee-status.readthedocs.io/en/main/modules.html) lists the dependencies (Python modules and external executables) for each module. If you are not using a module, you don't need the dependencies. diff --git a/docs/introduction.rst b/docs/introduction.rst index 7c3de02..3831d4b 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -19,6 +19,9 @@ Installation # will install bumblebee-status into ~/.local/bin/bumblebee-status pip install --user bumblebee-status + +There is also a SlackBuild available here: [slackbuilds:bumblebee-status](http://slackbuilds.org/repository/14.2/desktop/bumblebee-status/) - many thanks to [@Tonus1](https://github.com/Tonus1)! + Dependencies ------------ From 413abdcae79f17b638c26c51dacf54aaf4956d4d Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Thu, 31 Dec 2020 13:22:40 +0100 Subject: [PATCH 185/506] [doc] add license badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 7268a16..25d32b4 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ [![Code Climate](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/gpa.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status) [![Test Coverage](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/coverage.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/coverage) [![Issue Count](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/issue_count.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status) +![License](https://img.shields.io/github/license/tobi-wan-kenobi/bumblebee-status) **Many, many thanks to all contributors! I am still amazed by and deeply grateful for how many PRs this project gets.** From 21ded8f640359829159248aa5b7dfcbb71e56661 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sun, 17 Jan 2021 14:18:58 +0100 Subject: [PATCH 186/506] [core] Allow module loading from user directory If a module fails to load from both core and contrib, fall back to loading by file name from "~/.config/bumblebee-status/modules/.py" fixes #757 --- bumblebee_status/core/module.py | 10 +++++++++- docs/development/module.rst | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/bumblebee_status/core/module.py b/bumblebee_status/core/module.py index dad06e2..145d16d 100644 --- a/bumblebee_status/core/module.py +++ b/bumblebee_status/core/module.py @@ -17,6 +17,7 @@ except Exception as e: log = logging.getLogger(__name__) +import sys """Loads a module by name @@ -45,7 +46,14 @@ def load(module_name, config=core.config.Config([]), theme=None): log.debug("importing {} from contrib".format(module_short)) return getattr(mod, "Module")(config, theme) except ImportError as e: - log.fatal("failed to import {} from contrib: {}".format(module_short, e)) + try: + log.warning("failed to import {} from system: {}".format(module_short, e)) + mod = importlib.machinery.SourceFileLoader("modules.{}".format(module_short), + os.path.expanduser("~/.config/bumblebee-status/modules/{}.py".format(module_short))).load_module() + log.debug("importing {} from user".format(module_short)) + return getattr(mod, "Module")(config, theme) + except ImportError as e: + log.fatal("failed to import {}: {}".format(module_short, e)) return Error(config=config, module=module_name, error="unable to load module") diff --git a/docs/development/module.rst b/docs/development/module.rst index 113a6f7..cc71900 100644 --- a/docs/development/module.rst +++ b/docs/development/module.rst @@ -11,6 +11,7 @@ Adding a new module to ``bumblebee-status`` is straight-forward: ``bumblebee-status`` (i.e. a module called ``bumblebee_status/modules/contrib/test.py`` will be loaded using ``bumblebee-status -m test``) +- Alternatively, you can put your module in ``~/.config/bumblebee-status/modules/`` - The module name must follow the `Python Naming Conventions `_ - See below for how to actually write the module - Test (run ``bumblebee-status`` in the CLI) From 45c0a382c99c6f58977b0342972b2e914c3b3026 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sun, 17 Jan 2021 15:17:14 +0100 Subject: [PATCH 187/506] [core/module] fix load error when no user module exists --- bumblebee_status/core/module.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bumblebee_status/core/module.py b/bumblebee_status/core/module.py index 145d16d..9239a49 100644 --- a/bumblebee_status/core/module.py +++ b/bumblebee_status/core/module.py @@ -46,10 +46,13 @@ def load(module_name, config=core.config.Config([]), theme=None): log.debug("importing {} from contrib".format(module_short)) return getattr(mod, "Module")(config, theme) except ImportError as e: + usermod = os.path.expanduser("~/.config/bumblebee-status/modules/{}.py".format(module_short)) + if not os.path.exists(usermod): + raise e try: log.warning("failed to import {} from system: {}".format(module_short, e)) mod = importlib.machinery.SourceFileLoader("modules.{}".format(module_short), - os.path.expanduser("~/.config/bumblebee-status/modules/{}.py".format(module_short))).load_module() + os.path.expanduser(usermod)).load_module() log.debug("importing {} from user".format(module_short)) return getattr(mod, "Module")(config, theme) except ImportError as e: From beca26c2bf4676c6095abdb3551aa75abdbd6ad2 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sun, 17 Jan 2021 15:21:40 +0100 Subject: [PATCH 188/506] [core/config] Allow modules to be hidden when in critical/error state When a module is in critical state, the user can now hide the module (e.g. if pulseaudio fails to load). fixes #746 --- bumblebee_status/core/config.py | 20 +++++++++++++++++--- bumblebee_status/core/output.py | 2 ++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/bumblebee_status/core/config.py b/bumblebee_status/core/config.py index c84d10c..b777e6d 100644 --- a/bumblebee_status/core/config.py +++ b/bumblebee_status/core/config.py @@ -172,6 +172,13 @@ class Config(util.store.Store): default=[], help="Specify a list of modules to hide when not in warning/error state", ) + parser.add_argument( + "-e", + "--errorhide", + nargs="+", + default=[], + help="Specify a list of modules that are hidden when in state error" + ) parser.add_argument( "-d", "--debug", action="store_true", help="Add debug fields to i3 output" ) @@ -302,14 +309,21 @@ class Config(util.store.Store): def iconset(self): return self.__args.iconset - """Returns which modules should be hidden if their state is not warning/critical + """Returns whether a module should be hidden if their state is not warning/critical - :return: list of modules to hide automatically - :rtype: list of strings + :return: True if module should be hidden automatically, False otherwise + :rtype: bool """ def autohide(self, name): return name in self.__args.autohide + """Returns which modules should be hidden if they are in state error + + :return: returns True if name should be hidden, False otherwise + :rtype: bool + """ + def errorhide(self, name): + return name in self.__args.errorhide # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/core/output.py b/bumblebee_status/core/output.py index 687cafd..e3ec476 100644 --- a/bumblebee_status/core/output.py +++ b/bumblebee_status/core/output.py @@ -216,6 +216,8 @@ class i3(object): continue if module.hidden(): continue + if "critical" in widget.state() and self.__config.errorhide(widget.module.name): + continue blocks.extend(self.separator_block(module, widget)) blocks.append(self.__content_block(module, widget)) core.event.trigger("next-widget") From a27c284869eef0cd4262d7a92aa499a9906b6ead Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sun, 17 Jan 2021 15:29:44 +0100 Subject: [PATCH 189/506] [core/module] fix failing unit test wrong error handling again --- bumblebee_status/core/module.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/bumblebee_status/core/module.py b/bumblebee_status/core/module.py index 9239a49..6987129 100644 --- a/bumblebee_status/core/module.py +++ b/bumblebee_status/core/module.py @@ -17,8 +17,6 @@ except Exception as e: log = logging.getLogger(__name__) -import sys - """Loads a module by name :param module_name: Name of the module to load @@ -47,16 +45,16 @@ def load(module_name, config=core.config.Config([]), theme=None): return getattr(mod, "Module")(config, theme) except ImportError as e: usermod = os.path.expanduser("~/.config/bumblebee-status/modules/{}.py".format(module_short)) - if not os.path.exists(usermod): - raise e - try: - log.warning("failed to import {} from system: {}".format(module_short, e)) - mod = importlib.machinery.SourceFileLoader("modules.{}".format(module_short), - os.path.expanduser(usermod)).load_module() - log.debug("importing {} from user".format(module_short)) - return getattr(mod, "Module")(config, theme) - except ImportError as e: - log.fatal("failed to import {}: {}".format(module_short, e)) + if os.path.exists(usermod): + try: + log.warning("failed to import {} from system: {}".format(module_short, e)) + mod = importlib.machinery.SourceFileLoader("modules.{}".format(module_short), + os.path.expanduser(usermod)).load_module() + log.debug("importing {} from user".format(module_short)) + return getattr(mod, "Module")(config, theme) + except ImportError as e: + log.fatal("import failed: {}".format(e)) + log.fatal("failed to import {}".format(module_short)) return Error(config=config, module=module_name, error="unable to load module") From 406eadeac749307d5f7f63d7d038a0a02b9487aa Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sun, 17 Jan 2021 15:35:39 +0100 Subject: [PATCH 190/506] [modules/time] update once per second fixes #676 --- bumblebee_status/modules/core/time.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bumblebee_status/modules/core/time.py b/bumblebee_status/modules/core/time.py index 1c2c4b1..020bef1 100644 --- a/bumblebee_status/modules/core/time.py +++ b/bumblebee_status/modules/core/time.py @@ -12,7 +12,6 @@ from .datetime import Module class Module(Module): - @core.decorators.every(seconds=59) # ensures one update per minute def __init__(self, config, theme): super().__init__(config, theme) From 0734c970b0cc4846e4517ddd5ed6455119e09c98 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Fri, 12 Feb 2021 09:30:24 +0100 Subject: [PATCH 191/506] [modules/hddtemp] fix typo fixes #761 --- bumblebee_status/modules/contrib/hddtemp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bumblebee_status/modules/contrib/hddtemp.py b/bumblebee_status/modules/contrib/hddtemp.py index a039166..6e269cb 100644 --- a/bumblebee_status/modules/contrib/hddtemp.py +++ b/bumblebee_status/modules/contrib/hddtemp.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -"""Fetch hard drive temeperature data from a hddtemp daemon +"""Fetch hard drive temperature data from a hddtemp daemon that runs on localhost and default port (7634) contributed by `somospocos `_ - many thanks! From 6e3caa6f14a8baf0bcd8fd9e6d0744d34f6f88ba Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Fri, 12 Feb 2021 09:31:09 +0100 Subject: [PATCH 192/506] [modules/shortcut] fix typo fixes #760 --- bumblebee_status/modules/contrib/shortcut.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bumblebee_status/modules/contrib/shortcut.py b/bumblebee_status/modules/contrib/shortcut.py index 4ea0a7c..129bfdc 100644 --- a/bumblebee_status/modules/contrib/shortcut.py +++ b/bumblebee_status/modules/contrib/shortcut.py @@ -4,7 +4,7 @@ when clicking on it. For more than one shortcut, the commands and labels are strings separated by -a demiliter (; semicolon by default). +a delimiter (; semicolon by default). For example in order to create two shortcuts labeled A and B with commands cmdA and cmdB you could do: From 31f1f991022f94ba826784435b867270c2358471 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Fri, 12 Feb 2021 09:31:32 +0100 Subject: [PATCH 193/506] [doc] regenerate to fix typos --- docs/modules.rst | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/docs/modules.rst b/docs/modules.rst index 8ec9ecf..87a80ad 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -63,6 +63,7 @@ Parameters: * disk.path: Path to calculate disk usage from (defaults to /) * disk.open: Which application / file manager to launch (default xdg-open) * disk.format: Format string, tags {path}, {used}, {left}, {size} and {percent} (defaults to '{path} {used}/{size} ({percent:05.02f}%)') + * disk.system: Unit system to use - SI (KB, MB, ...) or IEC (KiB, MiB, ...) (defaults to 'IEC') .. image:: ../screenshots/disk.png @@ -620,8 +621,6 @@ some media control bindings. Left click toggles pause, scroll up skips the current song, scroll down returns to the previous song. -Requires the following library: - * subprocess Parameters: * deadbeef.format: Format string (defaults to '{artist} - {title}') Available values are: {artist}, {title}, {album}, {length}, @@ -772,7 +771,7 @@ contributed by `TheEdgeOfRage `_ - many thanks hddtemp ~~~~~~~ -Fetch hard drive temeperature data from a hddtemp daemon +Fetch hard drive temperature data from a hddtemp daemon that runs on localhost and default port (7634) contributed by `somospocos `_ - many thanks! @@ -1051,6 +1050,14 @@ Displays information about the current song in vlc, audacious, bmp, xmms2, spoti Requires the following executable: * playerctl +Parameters: + * playerctl.format: Format string (defaults to '{artist} - {title}') + Available values are: {album}, {title}, {artist}, {trackNumber} + * playerctl.layout: Comma-separated list to change order of widgets (defaults to song, previous, pause, next) + Widget names are: playerctl.song, playerctl.prev, playerctl.pause, playerctl.next + +Parameters are inherited from `spotify` module, many thanks to its developers! + contributed by `smitajit `_ - many thanks! .. image:: ../screenshots/playerctl.png @@ -1225,7 +1232,7 @@ Shows a widget per user-defined shortcut and allows to define the behaviour when clicking on it. For more than one shortcut, the commands and labels are strings separated by -a demiliter (; semicolon by default). +a delimiter (; semicolon by default). For example in order to create two shortcuts labeled A and B with commands cmdA and cmdB you could do: @@ -1265,7 +1272,6 @@ an example. Requires the following libraries: * requests - * regex Parameters: * spaceapi.url: String representation of the api endpoint @@ -1298,6 +1304,8 @@ Parameters: Widget names are: spotify.song, spotify.prev, spotify.pause, spotify.next * spotify.concise_controls: When enabled, allows spotify to be controlled from just the spotify.song widget. Concise controls are: Left Click: Toggle Pause; Wheel Up: Next; Wheel Down; Previous. + * spotify.bus_name: String (defaults to `spotify`) + Available values: spotify, spotifyd contributed by `yvesh `_ - many thanks! From 618ebbecccd03df95dfe62b053adc130387341b9 Mon Sep 17 00:00:00 2001 From: Michal Cieslicki Date: Sat, 20 Feb 2021 13:50:31 +0100 Subject: [PATCH 194/506] Add parameter to specify a configuration file --- bumblebee_status/core/config.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/bumblebee_status/core/config.py b/bumblebee_status/core/config.py index b777e6d..b5fd5ef 100644 --- a/bumblebee_status/core/config.py +++ b/bumblebee_status/core/config.py @@ -147,6 +147,13 @@ class Config(util.store.Store): parser = argparse.ArgumentParser( description="bumblebee-status is a modular, theme-able status line generator for the i3 window manager. https://github.com/tobi-wan-kenobi/bumblebee-status/wiki" ) + parser.add_argument( + "-c", + "--config-file", + action="store", + default=None, + help="Specify a configuration file to use" + ) parser.add_argument( "-m", "--modules", nargs="+", action="append", default=[], help=MODULE_HELP ) @@ -203,13 +210,18 @@ class Config(util.store.Store): self.__args = parser.parse_args(args) - for cfg in [ - "~/.bumblebee-status.conf", - "~/.config/bumblebee-status.conf", - "~/.config/bumblebee-status/config", - ]: + if self.__args.config_file: + cfg = self.__args.config_file cfg = os.path.expanduser(cfg) self.load_config(cfg) + else: + for cfg in [ + "~/.bumblebee-status.conf", + "~/.config/bumblebee-status.conf", + "~/.config/bumblebee-status/config", + ]: + cfg = os.path.expanduser(cfg) + self.load_config(cfg) parameters = [item for sub in self.__args.parameters for item in sub] for param in parameters: From 6d7934f0fe903edcacf651c364dc8b1e6d25241a Mon Sep 17 00:00:00 2001 From: Michal Cieslicki Date: Fri, 26 Feb 2021 18:02:47 +0100 Subject: [PATCH 195/506] Add code to enable scrolling of shell module output --- bumblebee_status/modules/contrib/shell.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bumblebee_status/modules/contrib/shell.py b/bumblebee_status/modules/contrib/shell.py index 6fd9aa2..cb7bc38 100644 --- a/bumblebee_status/modules/contrib/shell.py +++ b/bumblebee_status/modules/contrib/shell.py @@ -54,6 +54,7 @@ class Module(core.module.Module): def set_output(self, value): self.__output = value + @core.decorators.scrollable def get_output(self, _): return self.__output From 32eef6b204221ac75660567b7b8b8c486c425a2d Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sat, 27 Feb 2021 12:53:00 +0100 Subject: [PATCH 196/506] [doc] fix typos/wrong grammar fixes #769 --- docs/development/module.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/development/module.rst b/docs/development/module.rst index cc71900..992ef05 100644 --- a/docs/development/module.rst +++ b/docs/development/module.rst @@ -16,7 +16,7 @@ Adding a new module to ``bumblebee-status`` is straight-forward: - See below for how to actually write the module - Test (run ``bumblebee-status`` in the CLI) - Make sure your changes don’t break anything: ``./coverage.sh`` -- If you want to do me favour, run your module through +- If you want to do me a favour, run your module through ``black -t py34`` before submitting Pull requests @@ -24,7 +24,7 @@ Pull requests The project **gladly** accepts PRs for bugfixes, new functionality, new modules, etc. When you feel comfortable with what you’ve developed, -please just open a PR, somebody will look at it eventually :) Thanks! +please just open a PR. Somebody will look at it eventually :) Thanks! Coding guidelines ----------------- @@ -67,7 +67,7 @@ Of modules and widgets There are two important concepts for module writers: - A module is something that offers a single set of coherent functionality - A module -has 1 to n “widgets”, which translates to individual blocks in the i3bar +has 1 to n “widgets”, which translates to individual blocks in the i3bar. Very often, this is a 1:1 relationship, and a single module has a single widget. If that’s the case for you, you can stop reading now :) From 0ff49ac7d5922bc97725b29e696014ea727ff4e4 Mon Sep 17 00:00:00 2001 From: Frederic Junod Date: Tue, 2 Mar 2021 17:00:12 +0100 Subject: [PATCH 197/506] [doc] Remove requests dependency in stock module The module is using `urllib.request` --- bumblebee_status/modules/contrib/stock.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/bumblebee_status/modules/contrib/stock.py b/bumblebee_status/modules/contrib/stock.py index 36afe17..224a5fb 100644 --- a/bumblebee_status/modules/contrib/stock.py +++ b/bumblebee_status/modules/contrib/stock.py @@ -3,9 +3,6 @@ """Display a stock quote from finance.yahoo.com -Requires the following python packages: - * requests - Parameters: * stock.symbols : Comma-separated list of symbols to fetch * stock.change : Should we fetch change in stock value (defaults to True) From 7d0d1455c8894758ff6b83765194d28bd2a7ac9b Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Tue, 9 Mar 2021 19:12:59 +0100 Subject: [PATCH 198/506] [core/module] Add fallback for user module loading If importlib.machinery is not present, fall back to importlib.util to load the module by its absolute name. hopefully fixes #763 --- bumblebee_status/core/module.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/bumblebee_status/core/module.py b/bumblebee_status/core/module.py index 6987129..5de00ee 100644 --- a/bumblebee_status/core/module.py +++ b/bumblebee_status/core/module.py @@ -17,6 +17,22 @@ except Exception as e: log = logging.getLogger(__name__) +def import_user(module_short, config, theme): + usermod = os.path.expanduser("~/.config/bumblebee-status/modules/{}.py".format(module_short)) + if os.path.exists(usermod): + if hasattr(importlib, "machinery"): + log.debug("importing {} from user via machinery".format(module_short)) + mod = importlib.machinery.SourceFileLoader("modules.{}".format(module_short), + os.path.expanduser(usermod)).load_module() + return getattr(mod, "Module")(config, theme) + else: + log.debug("importing {} from user via importlib.util".format(module_short)) + spec = importlib.util.spec_from_file_location("modules.{}".format(module_short), usermod) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod.Module(config, theme) + raise ImportError("not found") + """Loads a module by name :param module_name: Name of the module to load @@ -44,16 +60,11 @@ def load(module_name, config=core.config.Config([]), theme=None): log.debug("importing {} from contrib".format(module_short)) return getattr(mod, "Module")(config, theme) except ImportError as e: - usermod = os.path.expanduser("~/.config/bumblebee-status/modules/{}.py".format(module_short)) - if os.path.exists(usermod): - try: - log.warning("failed to import {} from system: {}".format(module_short, e)) - mod = importlib.machinery.SourceFileLoader("modules.{}".format(module_short), - os.path.expanduser(usermod)).load_module() - log.debug("importing {} from user".format(module_short)) - return getattr(mod, "Module")(config, theme) - except ImportError as e: - log.fatal("import failed: {}".format(e)) + try: + log.warning("failed to import {} from system: {}".format(module_short, e)) + return import_user(module_short, config, theme) + except ImportError as e: + log.fatal("import failed: {}".format(e)) log.fatal("failed to import {}".format(module_short)) return Error(config=config, module=module_name, error="unable to load module") From 8d88b23947542aca7622fe46a4efc36467e80165 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sat, 13 Mar 2021 13:17:20 +0100 Subject: [PATCH 199/506] [modules] add a module "keys" that shows whether a key is pressed also, add backend functionality to hide individual widgets of a module. --- bumblebee_status/core/module.py | 4 +- bumblebee_status/core/output.py | 2 + bumblebee_status/core/widget.py | 3 +- themes/gruvbox-powerline.json | 65 ++++++++++++++++++++------------- 4 files changed, 46 insertions(+), 28 deletions(-) diff --git a/bumblebee_status/core/module.py b/bumblebee_status/core/module.py index 5de00ee..5be7a4f 100644 --- a/bumblebee_status/core/module.py +++ b/bumblebee_status/core/module.py @@ -190,9 +190,9 @@ class Module(core.input.Object): :rtype: bumblebee_status.widget.Widget """ - def add_widget(self, full_text="", name=None): + def add_widget(self, full_text="", name=None, hidden=False): widget_id = "{}::{}".format(self.name, len(self.widgets())) - widget = core.widget.Widget(full_text=full_text, name=name, widget_id=widget_id) + widget = core.widget.Widget(full_text=full_text, name=name, widget_id=widget_id, hidden=hidden) self.widgets().append(widget) widget.module = self return widget diff --git a/bumblebee_status/core/output.py b/bumblebee_status/core/output.py index e3ec476..1760309 100644 --- a/bumblebee_status/core/output.py +++ b/bumblebee_status/core/output.py @@ -216,6 +216,8 @@ class i3(object): continue if module.hidden(): continue + if widget.hidden: + continue if "critical" in widget.state() and self.__config.errorhide(widget.module.name): continue blocks.extend(self.separator_block(module, widget)) diff --git a/bumblebee_status/core/widget.py b/bumblebee_status/core/widget.py index b1c3b4e..5d823ea 100644 --- a/bumblebee_status/core/widget.py +++ b/bumblebee_status/core/widget.py @@ -10,12 +10,13 @@ log = logging.getLogger(__name__) class Widget(util.store.Store, core.input.Object): - def __init__(self, full_text="", name=None, widget_id=None): + def __init__(self, full_text="", name=None, widget_id=None, hidden=False): super(Widget, self).__init__() self.__full_text = full_text self.module = None self.name = name self.id = widget_id or self.id + self.hidden = hidden @property def module(self): diff --git a/themes/gruvbox-powerline.json b/themes/gruvbox-powerline.json index 3481dbc..d199243 100644 --- a/themes/gruvbox-powerline.json +++ b/themes/gruvbox-powerline.json @@ -44,29 +44,44 @@ "bg": "#b8bb26" } }, - "bluetooth": { - "ON": { - "fg": "#1d2021", - "bg": "#b8bb26" - } - }, - "git": { - "modified": { "bg": "#458588" }, - "deleted": { "bg": "#9d0006" }, - "new": { "bg": "#b16286" } - }, - "pomodoro": { - "paused": { - "fg": "#1d2021", - "bg": "#d79921" - }, - "work": { - "fg": "#1d2021", - "bg": "#b8bb26" - }, - "break": { - "fg": "#002b36", - "bg": "#859900" - } - } + "bluetooth": { + "ON": { + "fg": "#1d2021", + "bg": "#b8bb26" + } + }, + "git": { + "modified": { "bg": "#458588" }, + "deleted": { "bg": "#9d0006" }, + "new": { "bg": "#b16286" } + }, + "pomodoro": { + "paused": { + "fg": "#1d2021", + "bg": "#d79921" + }, + "work": { + "fg": "#1d2021", + "bg": "#b8bb26" + }, + "break": { + "fg": "#002b36", + "bg": "#859900" + } + }, + "keys": { + "Key.cmd": { + "bg": "#8ec07c", + "full_text": "***" + }, + "Key.shift": { + "bg": "#fabd2f" + }, + "Key.ctrl": { + "bg": "#83a598" + }, + "Key.alt": { + "bg": "#f28019" + } + } } From 868502d62e9a7aaa1fd7b9bf75030dfa359436b3 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sat, 13 Mar 2021 14:04:42 +0100 Subject: [PATCH 200/506] [modules/keys] add missing modules forgot to add in the previous commit --- bumblebee_status/modules/core/keys.py | 56 +++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 bumblebee_status/modules/core/keys.py diff --git a/bumblebee_status/modules/core/keys.py b/bumblebee_status/modules/core/keys.py new file mode 100644 index 0000000..a579c7d --- /dev/null +++ b/bumblebee_status/modules/core/keys.py @@ -0,0 +1,56 @@ +# pylint: disable=C0111,R0903 + +"""Shows when a key is pressed + +Parameters: + * keys.keys: Comma-separated list of keys to monitor (defaults to "") +""" + +import core.module +import core.widget +import core.decorators +import core.event + +import util.format + +from pynput.keyboard import Listener + +NAMES = { + "Key.cmd": "cmd", + "Key.ctrl": "ctrl", + "Key.shift": "shift", + "Key.alt": "alt", +} + +class Module(core.module.Module): + @core.decorators.never + def __init__(self, config, theme): + super().__init__(config, theme, []) + + self._listener = Listener(on_press=self._key_press, on_release=self._key_release) + + self._keys = util.format.aslist(self.parameter("keys", "Key.cmd,Key.ctrl,Key.alt,Key.shift")) + + for k in self._keys: + self.add_widget(name=k, full_text=self._display_name(k), hidden=True) + self._listener.start() + + def _display_name(self, key): + return NAMES.get(key, key) + + def _key_press(self, key): + key = str(key) + if not key in self._keys: return + self.widget(key).hidden = False + core.event.trigger("update", [self.id], redraw_only=False) + + def _key_release(self, key): + key = str(key) + if not key in self._keys: return + self.widget(key).hidden = True + core.event.trigger("update", [self.id], redraw_only=False) + + def state(self, widget): + return widget.name + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From 9f89e3a657abfe26d664387723ca112749a2e33e Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sat, 13 Mar 2021 14:09:42 +0100 Subject: [PATCH 201/506] [core] make bumblebee more reactive - set default delay to 0 - split input reading into 2 threads - get rid of polling --- bumblebee-status | 74 +++++++++++++++++----------------- bumblebee_status/core/event.py | 7 ++++ bumblebee_status/core/input.py | 4 +- 3 files changed, 45 insertions(+), 40 deletions(-) diff --git a/bumblebee-status b/bumblebee-status index a0f2fad..698bcf8 100755 --- a/bumblebee-status +++ b/bumblebee-status @@ -6,7 +6,6 @@ import json import time import signal import socket -import select import logging import threading @@ -39,45 +38,40 @@ class CommandSocket(object): self.__socket.close() os.unlink(self.__name) +def process_event(event_line, config, update_lock): + modules = {} + try: + event = json.loads(event_line) + core.input.trigger(event) + if "name" in event: + modules[event["name"]] = True + except ValueError: + pass -def handle_input(output, config, update_lock): + delay = float(config.get("engine.input_delay", 0.0)) + if delay > 0: + time.sleep(delay) + if update_lock.acquire(blocking=False) == True: + core.event.trigger("update", modules.keys(), force=True) + core.event.trigger("draw") + update_lock.release() + +def handle_commands(config, update_lock): with CommandSocket() as cmdsocket: - poll = select.poll() - poll.register(sys.stdin.fileno(), select.POLLIN) - poll.register(cmdsocket, select.POLLIN) - while True: - events = poll.poll() + tmp, _ = cmdsocket.accept() + line = tmp.recv(4096).decode() + tmp.close() + logging.debug("socket event {}".format(line)) + process_event(line, config, update_lock) - modules = {} - for fileno, event in events: - if fileno == cmdsocket.fileno(): - tmp, _ = cmdsocket.accept() - line = tmp.recv(4096).decode() - tmp.close() - logging.debug("socket event {}".format(line)) - else: - line = "[" - while line.startswith("["): - line = sys.stdin.readline().strip(",").strip() - logging.info("input event: {}".format(line)) - try: - event = json.loads(line) - core.input.trigger(event) - if "name" in event: - modules[event["name"]] = True - except ValueError: - pass - delay = float(config.get("engine.input_delay", 0.2)) - if delay > 0: - time.sleep(delay) - if update_lock.acquire(blocking=False) == True: - core.event.trigger("update", modules.keys(), force=True) - core.event.trigger("draw") - update_lock.release() - - poll.unregister(sys.stdin.fileno()) +def handle_events(config, update_lock): + while True: + line = sys.stdin.readline().strip(",").strip() + if line == "[": continue + logging.info("input event: {}".format(line)) + process_event(line, config, update_lock) def main(): @@ -104,9 +98,13 @@ def main(): core.input.register(None, core.input.WHEEL_DOWN, "i3-msg workspace next_on_output") update_lock = threading.Lock() - input_thread = threading.Thread(target=handle_input, args=(output, config, update_lock, )) - input_thread.daemon = True - input_thread.start() + event_thread = threading.Thread(target=handle_events, args=(config, update_lock, )) + event_thread.daemon = True + event_thread.start() + + cmd_thread = threading.Thread(target=handle_commands, args=(config, update_lock, )) + cmd_thread.daemon = True + cmd_thread.start() def sig_USR1_handler(signum,stack): if update_lock.acquire(blocking=False) == True: diff --git a/bumblebee_status/core/event.py b/bumblebee_status/core/event.py index 8e969f0..70b6b0c 100644 --- a/bumblebee_status/core/event.py +++ b/bumblebee_status/core/event.py @@ -8,6 +8,13 @@ def register(event, callback, *args, **kwargs): __callbacks.setdefault(event, []).append(cb) +def register_exclusive(event, callback, *args, **kwargs): + cb = callback + if args or kwargs: + cb = lambda: callback(*args, **kwargs) + + __callbacks[event] = [cb] + def unregister(event): if event in __callbacks: del __callbacks[event] diff --git a/bumblebee_status/core/input.py b/bumblebee_status/core/input.py index 9dbc2a6..b0d9f23 100644 --- a/bumblebee_status/core/input.py +++ b/bumblebee_status/core/input.py @@ -52,9 +52,9 @@ def register(obj, button=None, cmd=None, wait=False): logging.debug("registering callback {}".format(event_id)) core.event.unregister(event_id) # make sure there's always only one input event if callable(cmd): - core.event.register(event_id, cmd) + core.event.register_exclusive(event_id, cmd) else: - core.event.register(event_id, lambda event: __execute(event, cmd, wait)) + core.event.register_exclusive(event_id, lambda event: __execute(event, cmd, wait)) def trigger(event): From 38613495f2c649dae7e8872caede1c4ddf3cbcee Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sat, 13 Mar 2021 20:44:36 +0100 Subject: [PATCH 202/506] [tests] Adjust for widget hiding --- tests/core/test_output.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/core/test_output.py b/tests/core/test_output.py index 4e0d905..870ded8 100644 --- a/tests/core/test_output.py +++ b/tests/core/test_output.py @@ -26,6 +26,7 @@ def module_a(mocker): widget = mocker.MagicMock() widget.full_text.return_value = "test" widget.id = "a" + widget.hidden = False return SampleModule(config=core.config.Config([]), widgets=[widget, widget, widget]) @pytest.fixture From 65da1e2246efe2c157446be85ed0d452131f782d Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sat, 13 Mar 2021 20:45:56 +0100 Subject: [PATCH 203/506] [doc] migrate to travis.com --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 25d32b4..848434e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # bumblebee-status -[![Build Status](https://travis-ci.org/tobi-wan-kenobi/bumblebee-status.svg?branch=main)](https://travis-ci.org/tobi-wan-kenobi/bumblebee-status) +[![Build Status](https://travis-ci.com/tobi-wan-kenobi/bumblebee-status.svg?branch=main)](https://travis-ci.com/tobi-wan-kenobi/bumblebee-status) [![Documentation Status](https://readthedocs.org/projects/bumblebee-status/badge/?version=main)](https://bumblebee-status.readthedocs.io/en/main/?badge=main) ![AUR version (release)](https://img.shields.io/aur/version/bumblebee-status) ![AUR version (git)](https://img.shields.io/aur/version/bumblebee-status-git) From 4187bddad6bc34cb2120f7827a5ed4e0dde27400 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Thu, 18 Mar 2021 15:29:06 +0100 Subject: [PATCH 204/506] [modules/shell] do not default to "makewide" to avoid unnecessarily wide shell modules, set "makewide" to false, if it is not set at all. fixes #775 --- bumblebee_status/modules/contrib/shell.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bumblebee_status/modules/contrib/shell.py b/bumblebee_status/modules/contrib/shell.py index cb7bc38..9d75a60 100644 --- a/bumblebee_status/modules/contrib/shell.py +++ b/bumblebee_status/modules/contrib/shell.py @@ -47,6 +47,9 @@ class Module(core.module.Module): self.__output = "please wait..." self.__current_thread = threading.Thread() + if self.parameter("scrolling.makewide") is None: + self.set("scrolling.makewide", False) + # LMB and RMB will update output regardless of timer core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.update) core.input.register(self, button=core.input.RIGHT_MOUSE, cmd=self.update) From e5606495314d9db78257b13525b8147c44f3bb16 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Thu, 18 Mar 2021 15:30:03 +0100 Subject: [PATCH 205/506] [modules/shell] remove obsolete event handlers modules are now automatically updated when clicked. fixes #776 --- bumblebee_status/modules/contrib/shell.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/bumblebee_status/modules/contrib/shell.py b/bumblebee_status/modules/contrib/shell.py index 9d75a60..566de42 100644 --- a/bumblebee_status/modules/contrib/shell.py +++ b/bumblebee_status/modules/contrib/shell.py @@ -50,10 +50,6 @@ class Module(core.module.Module): if self.parameter("scrolling.makewide") is None: self.set("scrolling.makewide", False) - # LMB and RMB will update output regardless of timer - core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.update) - core.input.register(self, button=core.input.RIGHT_MOUSE, cmd=self.update) - def set_output(self, value): self.__output = value From 7756eaaa31523111df898e4af506c66b4b43c0f4 Mon Sep 17 00:00:00 2001 From: jslabik Date: Sat, 20 Mar 2021 01:18:46 +0100 Subject: [PATCH 206/506] Adding the ability to change the editor to module todo --- bumblebee_status/modules/contrib/todo.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bumblebee_status/modules/contrib/todo.py b/bumblebee_status/modules/contrib/todo.py index 878b63f..df2d788 100644 --- a/bumblebee_status/modules/contrib/todo.py +++ b/bumblebee_status/modules/contrib/todo.py @@ -21,9 +21,10 @@ class Module(core.module.Module): super().__init__(config, theme, core.widget.Widget(self.output)) self.__doc = os.path.expanduser(self.parameter("file", "~/Documents/todo.txt")) + self.__editor = self.parameter("editor", "xdg-open") self.__todos = self.count_items() core.input.register( - self, button=core.input.LEFT_MOUSE, cmd="xdg-open {}".format(self.__doc) + self, button=core.input.LEFT_MOUSE, cmd="{} {}".format(self.__editor, self.__doc) ) def output(self, widget): From 10c9321c24ff0f0ca1056d5c34ed74c57e0fc14b Mon Sep 17 00:00:00 2001 From: Marcos Gabriel Miller Date: Sat, 27 Mar 2021 17:22:10 -0300 Subject: [PATCH 207/506] [themes] add albiceleste-powerline --- themes/albiceleste-powerline.json | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 themes/albiceleste-powerline.json diff --git a/themes/albiceleste-powerline.json b/themes/albiceleste-powerline.json new file mode 100644 index 0000000..e046882 --- /dev/null +++ b/themes/albiceleste-powerline.json @@ -0,0 +1,20 @@ +{ + "icons": [ "awesome-fonts" ], + "defaults": { + "separator-block-width": 0, + "warning": { + "fg": "#000000", + "bg": "#ffff00" + }, + "critical": { + "fg": "#000000", + "bg": "#ffff00" + + } + }, + "cycle": [ + {"fg": "#000000", "bg": "#87ceeb"}, + {"fg": "#000000", "bg": "#FFFFFF"} + ] + +} From abcf861fcbfbaaf1d615f154dfcfa75e938bd377 Mon Sep 17 00:00:00 2001 From: Marcos Gabriel Miller Date: Sat, 27 Mar 2021 17:22:35 -0300 Subject: [PATCH 208/506] [themes] add rastafari-powerline --- themes/rastafari-powerline.json | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 themes/rastafari-powerline.json diff --git a/themes/rastafari-powerline.json b/themes/rastafari-powerline.json new file mode 100644 index 0000000..b7c8c21 --- /dev/null +++ b/themes/rastafari-powerline.json @@ -0,0 +1,21 @@ +{ + "icons": [ "awesome-fonts" ], + "defaults": { + "separator-block-width": 0, + "warning": { + "fg": "#000000", + "bg": "#FFFFFF" + }, + "critical": { + "fg": "#000000", + "bg": "#FFFFFF" + } + }, + "cycle": [ + {"fg": "#FFFFFF", "bg": "#008000"}, + {"fg": "#000000", "bg": "#ffff00"}, + {"fg": "#000000", "bg": "#ff0000"} + + ] + +} \ No newline at end of file From 527d1706c2b842bdebf9a83224dc76619d7f6ffc Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Fri, 2 Apr 2021 03:30:09 +0000 Subject: [PATCH 209/506] [core/module] add fallback for module loading Looks like some Python versions work with find_spec(), others with spec_from_file_location(), so add find_spec() as fallback. fixes #779 --- bumblebee_status/core/module.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/bumblebee_status/core/module.py b/bumblebee_status/core/module.py index 5be7a4f..c4b66a4 100644 --- a/bumblebee_status/core/module.py +++ b/bumblebee_status/core/module.py @@ -27,10 +27,16 @@ def import_user(module_short, config, theme): return getattr(mod, "Module")(config, theme) else: log.debug("importing {} from user via importlib.util".format(module_short)) - spec = importlib.util.spec_from_file_location("modules.{}".format(module_short), usermod) - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - return mod.Module(config, theme) + try: + spec = importlib.util.spec_from_file_location("modules.{}".format(module_short), usermod) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod.Module(config, theme) + except Exception as e: + spec = importlib.util.find_spec("modules.{}".format(module_short), usermod) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod.Module(config, theme) raise ImportError("not found") """Loads a module by name From 0410ac9c6b2860a0bb077d384f8da07ba37d15ce Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sat, 3 Apr 2021 19:24:01 +0000 Subject: [PATCH 210/506] [doc/shortcut] better example for shortcut module --- bumblebee_status/modules/contrib/shortcut.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bumblebee_status/modules/contrib/shortcut.py b/bumblebee_status/modules/contrib/shortcut.py index 129bfdc..ff5b47f 100644 --- a/bumblebee_status/modules/contrib/shortcut.py +++ b/bumblebee_status/modules/contrib/shortcut.py @@ -9,7 +9,7 @@ a delimiter (; semicolon by default). For example in order to create two shortcuts labeled A and B with commands cmdA and cmdB you could do: - ./bumblebee-status -m shortcut -p shortcut.cmd='ls;ps' shortcut.label='A;B' + ./bumblebee-status -m shortcut -p shortcut.cmd='firefox https://www.google.com;google-chrome https://google.com' shortcut.label='Google (Firefox);Google (Chrome)' Parameters: * shortcut.cmds : List of commands to execute From 4a6be622a88668cab02790191c312323a848fbbe Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sat, 3 Apr 2021 19:29:40 +0000 Subject: [PATCH 211/506] [modules/rotation] fix widget creation each iteration of the rotation module created new/duplicate widgets, causing a status bar of infinite length. fixes #782 --- bumblebee_status/modules/contrib/rotation.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bumblebee_status/modules/contrib/rotation.py b/bumblebee_status/modules/contrib/rotation.py index 13f656e..d05c463 100644 --- a/bumblebee_status/modules/contrib/rotation.py +++ b/bumblebee_status/modules/contrib/rotation.py @@ -31,14 +31,13 @@ class Module(core.module.Module): orientation = curr_orient break - widget = self.widget(display) + widget = self.widget(name=display) if not widget: widget = self.add_widget(full_text=display, name=display) core.input.register( widget, button=core.input.LEFT_MOUSE, cmd=self.__toggle ) widget.set("orientation", orientation) - widgets.append(widget) def state(self, widget): return widget.get("orientation", "normal") From 8001ed3ada13eb9799442a258d7c1a5f17633029 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Tue, 27 Apr 2021 17:16:54 +0200 Subject: [PATCH 212/506] [modules/cpu] Add per-cpu mode By explicitly setting "cpu.percpu=True" as parameter, it is now possible to get the CPU utilization on a per-cpu basis. Future extension: One widget per CPU (add ID of CPU and add warning/error state on a per CPU basis) see #785 --- bumblebee_status/modules/core/cpu.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/bumblebee_status/modules/core/cpu.py b/bumblebee_status/modules/core/cpu.py index 59c6e71..51dc695 100644 --- a/bumblebee_status/modules/core/cpu.py +++ b/bumblebee_status/modules/core/cpu.py @@ -12,6 +12,7 @@ Parameters: * cpu.warning : Warning threshold in % of CPU usage (defaults to 70%) * cpu.critical: Critical threshold in % of CPU usage (defaults to 80%) * cpu.format : Format string (defaults to '{:.01f}%') + * cpu.percpu : If set to true, show each individual cpu (defaults to false) """ import psutil @@ -20,28 +21,37 @@ import core.module import core.widget import core.input +import util.format + class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.utilization)) - self.widget().set("theme.minwidth", self._format.format(100.0 - 10e-20)) - self._utilization = psutil.cpu_percent(percpu=False) + self._percpu = util.format.asbool(self.parameter("percpu", False)) + self.update() + self.widget().set("theme.minwidth", self._format.format(*[100.0 - 10e-20]*len(self._utilization))) core.input.register( self, button=core.input.LEFT_MOUSE, cmd="gnome-system-monitor" ) @property def _format(self): - return self.parameter("format", "{:.01f}%") + fmt = self.parameter("format", "{:.01f}%") + if self._percpu: + fmt = [fmt]*len(self._utilization) + fmt = " ".join(fmt) + return fmt def utilization(self, _): - return self._format.format(self._utilization) + return self._format.format(*self._utilization) def update(self): - self._utilization = psutil.cpu_percent(percpu=False) + self._utilization = psutil.cpu_percent(percpu=self._percpu) + if not self._percpu: + self._utilization = [self._utilization] def state(self, _): - return self.threshold_state(self._utilization, 70, 80) + return self.threshold_state(sum(self._utilization)/len(self._utilization), 70, 80) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From 10c169af8a7035d1d7bf450adede183bbfab8f3c Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Tue, 27 Apr 2021 17:17:13 +0200 Subject: [PATCH 213/506] [modules/core/cpu] optionally add per-cpu widget if the parameter "percpu" is set to true, create one widget per cpu, and also handle warning/error state on a per-widget basis. see #785 --- bumblebee_status/modules/core/cpu.py | 33 +++++++++++++++------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/bumblebee_status/modules/core/cpu.py b/bumblebee_status/modules/core/cpu.py index 51dc695..77ac20a 100644 --- a/bumblebee_status/modules/core/cpu.py +++ b/bumblebee_status/modules/core/cpu.py @@ -26,32 +26,35 @@ import util.format class Module(core.module.Module): def __init__(self, config, theme): - super().__init__(config, theme, core.widget.Widget(self.utilization)) + super().__init__(config, theme, []) self._percpu = util.format.asbool(self.parameter("percpu", False)) - self.update() - self.widget().set("theme.minwidth", self._format.format(*[100.0 - 10e-20]*len(self._utilization))) + + for idx, cpu_perc in enumerate(self.cpu_utilization()): + widget = self.add_widget(name="cpu#{}".format(idx), full_text=self.utilization) + widget.set("utilization", cpu_perc) + widget.set("theme.minwidth", self._format.format(100.0 - 10e-20)) + core.input.register( self, button=core.input.LEFT_MOUSE, cmd="gnome-system-monitor" ) @property def _format(self): - fmt = self.parameter("format", "{:.01f}%") - if self._percpu: - fmt = [fmt]*len(self._utilization) - fmt = " ".join(fmt) - return fmt + return self.parameter("format", "{:.01f}%") - def utilization(self, _): - return self._format.format(*self._utilization) + def utilization(self, widget): + return self._format.format(widget.get("utilization", 0.0)) + + def cpu_utilization(self): + tmp = psutil.cpu_percent(percpu=self._percpu) + return tmp if self._percpu else [tmp] def update(self): - self._utilization = psutil.cpu_percent(percpu=self._percpu) - if not self._percpu: - self._utilization = [self._utilization] + for idx, cpu_perc in enumerate(self.cpu_utilization()): + self.widgets()[idx].set("utilization", cpu_perc) - def state(self, _): - return self.threshold_state(sum(self._utilization)/len(self._utilization), 70, 80) + def state(self, widget): + return self.threshold_state(widget.get("utilization", 0.0), 70, 80) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From fb6be007e590a48fbcb8ef623943118323b56ddf Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Tue, 27 Apr 2021 17:17:28 +0200 Subject: [PATCH 214/506] [core/output] fix minimum width with padding when calculating the minimum width of a widget, also take the padding into consideration. see #785 --- bumblebee_status/core/output.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/bumblebee_status/core/output.py b/bumblebee_status/core/output.py index 1760309..9c032d6 100644 --- a/bumblebee_status/core/output.py +++ b/bumblebee_status/core/output.py @@ -57,6 +57,9 @@ class block(object): def set(self, key, value): self.__attributes[key] = value + def get(self, key, default=None): + return self.__attributes.get(key, default) + def is_pango(self, attr): if isinstance(attr, dict) and "pango" in attr: return True @@ -91,9 +94,17 @@ class block(object): assign(self.__attributes, result, "background", "bg") if "full_text" in self.__attributes: + prefix = self.__pad(self.pangoize(self.__attributes.get("prefix"))) + suffix = self.__pad(self.pangoize(self.__attributes.get("suffix"))) + self.set("_prefix", prefix) + self.set("_suffix", suffix) + self.set("_raw", self.get("full_text")) result["full_text"] = self.pangoize(result["full_text"]) result["full_text"] = self.__format(self.__attributes["full_text"]) + if "min-width" in self.__attributes and "padding" in self.__attributes: + self.set("min-width", self.__format(self.get("min-width"))) + for k in [ "name", "instance", @@ -123,11 +134,8 @@ class block(object): def __format(self, text): if text is None: return None - prefix = self.__pad(self.pangoize(self.__attributes.get("prefix"))) - suffix = self.__pad(self.pangoize(self.__attributes.get("suffix"))) - self.set("_prefix", prefix) - self.set("_suffix", suffix) - self.set("_raw", text) + prefix = self.get("_prefix") + suffix = self.get("_suffix") return "{}{}{}".format(prefix, text, suffix) From 028932a560a87f12829324c2565be6664cce1e26 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Tue, 27 Apr 2021 17:17:55 +0200 Subject: [PATCH 215/506] [tests/cpu] adapt tests and add per-cpu tests fixes #785 --- tests/modules/core/test_cpu.py | 37 +++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/tests/modules/core/test_cpu.py b/tests/modules/core/test_cpu.py index eca2362..d1a015e 100644 --- a/tests/modules/core/test_cpu.py +++ b/tests/modules/core/test_cpu.py @@ -7,8 +7,9 @@ import modules.core.cpu pytest.importorskip("psutil") -def build_module(): - config = core.config.Config([]) +def build_module(percpu=False): + config = core.config.Config(["-p", "percpu={}".format(percpu)]) + config.set("cpu.percpu", percpu) return modules.core.cpu.Module(config=config, theme=None) def cpu_widget(module): @@ -42,21 +43,47 @@ class TestCPU(TestCase): cpu_percent_mock.return_value = 50 module = build_module() - assert module.state(None) == None + assert module.state(module.widget()) == None @mock.patch('psutil.cpu_percent') def test_warning_state(self, cpu_percent_mock): cpu_percent_mock.return_value = 75 module = build_module() - assert module.state(None) == 'warning' + assert module.state(module.widget()) == 'warning' @mock.patch('psutil.cpu_percent') def test_critical_state(self, cpu_percent_mock): cpu_percent_mock.return_value = 82 module = build_module() - assert module.state(None) == 'critical' + assert module.state(module.widget()) == 'critical' + + @mock.patch('psutil.cpu_percent') + def test_healthy_state_percpu(self, cpu_percent_mock): + cpu_percent_mock.return_value = [50,42,47] + module = build_module(percpu=True) + + for widget in module.widgets(): + assert module.state(widget) == None + + @mock.patch('psutil.cpu_percent') + def test_warning_state_percpu(self, cpu_percent_mock): + cpu_percent_mock.return_value = [50,72,47] + module = build_module(percpu=True) + + assert module.state(module.widgets()[0]) == None + assert module.state(module.widgets()[1]) == "warning" + assert module.state(module.widgets()[2]) == None + + @mock.patch('psutil.cpu_percent') + def test_warning_state_percpu(self, cpu_percent_mock): + cpu_percent_mock.return_value = [50,72,99] + module = build_module(percpu=True) + + assert module.state(module.widgets()[0]) == None + assert module.state(module.widgets()[1]) == "warning" + assert module.state(module.widgets()[2]) == "critical" @mock.patch('core.input.register') def test_register_left_mouse_action(self, input_register_mock): From 1e13798c958e2ddbd33aba70dfed09204ab5f925 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Wed, 28 Apr 2021 12:41:04 +0200 Subject: [PATCH 216/506] [core/input] add pseudo-event "update" to selectively update modules to trigger an update of a module (without actually triggering a mouse interaction), use the special event "update": bumblebee-ctl -m -b update see #784 --- bumblebee-ctl | 3 ++- bumblebee_status/core/input.py | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/bumblebee-ctl b/bumblebee-ctl index b1c0f37..435162d 100755 --- a/bumblebee-ctl +++ b/bumblebee-ctl @@ -12,6 +12,7 @@ button = { "right-mouse": 3, "wheel-up": 4, "wheel-down": 5, + "update": -1, } @@ -20,7 +21,7 @@ def main(): parser.add_argument( "-b", "--button", - choices=["left-mouse", "right-mouse", "middle-mouse", "wheel-up", "wheel-down"], + choices=["left-mouse", "right-mouse", "middle-mouse", "wheel-up", "wheel-down", "update"], help="button to emulate", default="left-mouse", ) diff --git a/bumblebee_status/core/input.py b/bumblebee_status/core/input.py index b0d9f23..2f9fdfc 100644 --- a/bumblebee_status/core/input.py +++ b/bumblebee_status/core/input.py @@ -10,6 +10,7 @@ MIDDLE_MOUSE = 2 RIGHT_MOUSE = 3 WHEEL_UP = 4 WHEEL_DOWN = 5 +UPDATE = -1 def button_name(button): @@ -23,6 +24,8 @@ def button_name(button): return "wheel-up" if button == WHEEL_DOWN: return "wheel-down" + if button == UPDATE: + return "update" return "n/a" From 9553bec7db7113dd82c443d57a6a722a222e6d07 Mon Sep 17 00:00:00 2001 From: Edward <57063825+solar-core@users.noreply.github.com> Date: Wed, 28 Apr 2021 20:48:49 +0300 Subject: [PATCH 217/506] Update modules.rst --- docs/modules.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/modules.rst b/docs/modules.rst index 87a80ad..440a0a3 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -22,6 +22,7 @@ Parameters: * cpu.warning : Warning threshold in % of CPU usage (defaults to 70%) * cpu.critical: Critical threshold in % of CPU usage (defaults to 80%) * cpu.format : Format string (defaults to '{:.01f}%') + * cpu.percpu : If set to true, show each individual cpu (defaults to false) .. image:: ../screenshots/cpu.png From 046b950b8a0c9c5bd91b40b1d5b1ceeb4f7ef939 Mon Sep 17 00:00:00 2001 From: Frederic Junod Date: Mon, 10 May 2021 14:35:34 +0200 Subject: [PATCH 218/506] Fix parameters name for the sun module --- bumblebee_status/modules/contrib/sun.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bumblebee_status/modules/contrib/sun.py b/bumblebee_status/modules/contrib/sun.py index 6b0734d..e9eefd2 100644 --- a/bumblebee_status/modules/contrib/sun.py +++ b/bumblebee_status/modules/contrib/sun.py @@ -8,8 +8,8 @@ Requires the following python packages: * python-dateutil Parameters: - * cpu.lat : Latitude of your location - * cpu.lon : Longitude of your location + * sun.lat : Latitude of your location + * sun.lon : Longitude of your location (if none of those are set, location is determined automatically via location APIs) From 7f03c9ce2d075ca42385fd8c5a9ed1804ecda211 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Mon, 10 May 2021 17:48:23 +0000 Subject: [PATCH 219/506] [doc] update module documentation --- docs/modules.rst | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/docs/modules.rst b/docs/modules.rst index 440a0a3..0e166e6 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -85,6 +85,14 @@ Requires: .. image:: ../screenshots/git.png +keys +~~~~ + +Shows when a key is pressed + +Parameters: + * keys.keys: Comma-separated list of keys to monitor (defaults to "") + layout-xkb ~~~~~~~~~~ @@ -1238,7 +1246,7 @@ a delimiter (; semicolon by default). For example in order to create two shortcuts labeled A and B with commands cmdA and cmdB you could do: - ./bumblebee-status -m shortcut -p shortcut.cmd='ls;ps' shortcut.label='A;B' + ./bumblebee-status -m shortcut -p shortcut.cmd='firefox https://www.google.com;google-chrome https://google.com' shortcut.label='Google (Firefox);Google (Chrome)' Parameters: * shortcut.cmds : List of commands to execute @@ -1321,9 +1329,6 @@ stock Display a stock quote from finance.yahoo.com -Requires the following python packages: - * requests - Parameters: * stock.symbols : Comma-separated list of symbols to fetch * stock.change : Should we fetch change in stock value (defaults to True) @@ -1344,8 +1349,8 @@ Requires the following python packages: * python-dateutil Parameters: - * cpu.lat : Latitude of your location - * cpu.lon : Longitude of your location + * sun.lat : Latitude of your location + * sun.lon : Longitude of your location (if none of those are set, location is determined automatically via location APIs) From 902288f30d5f5cdcf619950ebc5acc1976486f72 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Tue, 11 May 2021 11:23:06 +0200 Subject: [PATCH 220/506] [modules/sensors] do not truncate temperature use strip() instead of a sub-list to get the value for the temperature. fixes #787 --- bumblebee_status/modules/contrib/sensors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bumblebee_status/modules/contrib/sensors.py b/bumblebee_status/modules/contrib/sensors.py index 68b792a..8164f00 100644 --- a/bumblebee_status/modules/contrib/sensors.py +++ b/bumblebee_status/modules/contrib/sensors.py @@ -98,7 +98,7 @@ class Module(core.module.Module): try: temperature = open( self.parameter("path", "/sys/class/thermal/thermal_zone0/temp") - ).read()[:2] + ).read().strip() log.debug("retrieved temperature from /sys/class/") # TODO: Iterate through all thermal zones to determine the correct one and use its value # https://unix.stackexchange.com/questions/304845/discrepancy-between-number-of-cores-and-thermal-zones-in-sys-class-thermal From dfd23a44de8964dcd4ae9e1971c24c6efed71664 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sun, 16 May 2021 21:09:58 +0200 Subject: [PATCH 221/506] [modules/layout] add a new - generic - layout module Add a new module "layout" that will eventually evolve into the only keyboard layout module. Right now, it uses an external binary (get-kbd-layout) to determine the layout of a keyboard device (because I did not manage to call libX11 with ctypes correctly). see #788 see #790 --- .gitignore | 2 ++ bin/get-kbd-layout | Bin 0 -> 21512 bytes bumblebee_status/modules/core/layout.py | 39 ++++++++++++++++++++++++ util/Makefile | 13 ++++++++ util/layout.c | 39 ++++++++++++++++++++++++ 5 files changed, 93 insertions(+) create mode 100755 bin/get-kbd-layout create mode 100644 bumblebee_status/modules/core/layout.py create mode 100644 util/Makefile create mode 100644 util/layout.c diff --git a/.gitignore b/.gitignore index 2bf1ac8..c21fe43 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +*.o + # Vim swap files *swp *~ diff --git a/bin/get-kbd-layout b/bin/get-kbd-layout new file mode 100755 index 0000000000000000000000000000000000000000..7ce6ccf1f0f105b58ed302f5950b540abfcf4a6f GIT binary patch literal 21512 zcmeHPeUMw#b-z#2uC&(fTY1+QgDI9BLv29X5A4N!tgT&pZ7tYfuuYr>^U$uey9!!q zD?ORD6PhI^l&$07P^O`ol3@tRkaVC9O(~>&81V4bk^x%4O+#4-G;5&LkU&iXY=7t8 zd$do_QYXxG`bS51M)#cayXTyH-+A|5-TS`2+P87N$1nsZuUIWeIvP?)Ocw+$(hNvU ztP(-wOU36zJ!GH6RC!Dhl&Uo1DW?IAdqK(WE?w@Xr)WK<+(M#cS1pBg1x$rL4zjBw zBQ3k>K57sWQ!X#n7na3Bxah5to9Cq?pj}koM=M>rh6CDd|$${HC2Gep#5L<0vjz`8XL<_lSr#$X?Xb7Fc4Hs_{-@R_mo15NVwZ^*r zuKX__`zUt93-|sdwVT>N{z!)s>2vsra=AP%<2q9D5H=UT=Tko$J@@^++#RW`ERo-E z8KQ(+Kw2u`)PhR*pI5;XRq$u4;C2=KzAE_d0T1KnPQw5ywe!bS@SCdOX8{l6=T6rF zsN{cr75rh~OWaN%{|LXi;v_KXS`3Zl(^-26 zN`vDGYbc#fWYW8-y%+_9y1k=`bXGv#-qAtBpo{oH#ckNw-@Df8jII#Obw*dJxKKam zeos7&07VsNKzbV}kJowWbQ4*8pt0h~=?2oAAyXut*ErtK(O^NE((@MM`!zlulf{B@ zmJhE{IE_6+u`-`>;LiDG+JU1%rF7JRqv54=%z^W5ymkhBp#z&)jLPAGT$6i0_g`8 zEA#C!B@o`ISeaiHQv%_pVr4!OQv%`5imm0f2-G4_i$E;`wFuNA@c%Rd@jdSZ;(O~} zX~PoZ>j!Pm%yaQQhXRk6j&^3{>nNHz?G5D3i(=qN9-;ChQ!_}X{hVZ4vK@I&$}f;i zOQs_SrTh%Zv{X8BK+2DiOiQFAw@dk_B-7I9$bKo`OEN9>j!a1TMGt4AW=?n_D#UM^7pQD7I~j@9Dmr z*!bj|c4K^R_cIWtBRHh#5oAx)Jq_8|!FEiKd>Mt?V1pAIn!NVo_+;_uo@+lA_HxvE z6HMZJw;he|TNyCP^ON^e+o!MjbY|ueI)&4>f!qJ|UH0M)ggf z1*^B~?x!{WWZ$8^n-0bIZJUD5Q)<5|{&(@yFkGIA5MQ#VYW#jTsXbesN z^~b38(NDbg!j6AwUL-b7zQ1wu-F1_{>Y17MYJAV*M*Q4Yif_@_`r?kBD|&YHT-jsc zoCz9fatxM@I$3uut3{v|fm#G=5vWC=7J*s>Y7wYKpca8z1pZeeK=*rw5_TeUUih@a z98t{Djj^zu3+HeTE1c9fv9Qxz=sh3XVR?V!*neP?2>$+~Y8Uks=v9#Kes5;xInZ}O zkAfb1e`Y3xjo$WO&&+J6?IjZ3r=?`*PO_R=K;lRdSY+U&faLUmn%-;1cP2UPT!p&LAF zo9e%U20^6qui*C~%4a}J<(~hN3gq)I@SDQ1?x6BsGjx-u-(2ubZ=V_7=j$_qn{dj95s?{OsKItx7&gk<#;i z3$@I9s*|eim-ZC)MOL&&0Lsa=L*_DX1ID&$ca4usA_e zCfZDhm_l$jSEhir=Y1sINTzh6B4U9C7?0_P47uHhwH`lX$o2nrMja1|r7G@G%Hs=Z zd+Zve%yGsxQp)e>1OG_NrFQFj{;vr?SMIL2OZS8InqIEykfu8|y+PAI)AR?L-lypk zn!cnd*E1ed+=jJl&kHZZ6WPnd9qrN1Xh*oSy|cZev;AyUPR|XibE(b=MxZoR zPW{d!aG$%>^PHt>Uyr9((d+mOmzQ54?bqNe>2lp}eny$cgYLHF#*mbKqGaEq?YY0- zsK*n_ZU?xf%*QLc4j=DA$HS>K)+jv2#ItCIPhR5)823TH@l^CDD!zi;7=bSW^LYF> zA%Y&Czk#HH-$%r#-vQ!Xg3LJmM0)z$-6R7);h*6&;UvE(@C%0OFyh{mK&>AEDddg0kw}pr-Jb7N8_=k}BPHQ;; zQrGBv#nnQG@R_B?W~4LL98`b)WYsXyUl&GELy? z$QHw>p%aXkn)n`-`g}C;c>=yT$~>XE^Ca=D1@T^jtnL(fz;m|2WvKq%z&Nnr$EdFU zCw1$;GoXwICkRxEVy&wGWJ}t4TBk2sH_5u7{sSdMBhQK)HDbfmafEw*)CS z|CYczP)Xxs5~SSx2Lc}^@MQ^7ZoYs z@4hqzC^sJ?u!q2BB}lpXGJ$Ur___osH{T&}CxQPdLCQ@72iyDtxW-0sO##Zy`I4nG zX)c!RZN#1>*?WmSN3u^6+bh{$5W7jT6l8O|WGOi2pk(hNHY?eOh}|Vws@S|;h$2Y3;<6&azjPd1Cdosg+S%6<``6G*2`kB>Ru zhog}HaDaM9@MRSGUv2oMY(#)2g$%3uG0Z*ppjoZqK(BcxY16O^tTSoUir`B43G@Z# zf;0v*a1dA@{2{bLqOkxH_y&|7U}s)2Zw0Ar@RB26gPdo`jhsGx;#6{`CbU0<9!+Qz zEYCOu3kQ=-r41(lU6O&K=VVnxUBeqEUt;i|;v38-ROr?>M8V?|kLF4u^4P!2* zfVoDC;WZbUrROFp z5G8fz1`U+dHz`Gk{jIC8p7Co-3D5x>{jH?fuvqlBwn1S|E1Do(z&Vu!wF_AnxoA8c zs_5{|Xbv&vIZ|$JMO7DI$rdVk(2baTqH2?N*y41EbE{jdOXsa{9NLXW&_jzKcO;xT zYfd5+?`#Nss(mm*NVnCO1=NjfhjXuJrz(u!noY>%Yl%2TMl{-1*i}fT#tLnC@5}01 zA@H^g-r0$6QSXX0c=&J9yV~%EqP?G6y=m#v;!bbK9`P9PM3b3UJXyu}Sgw#BZyQMC ztsK{N6R2O;p{B^?lr(BpyyleDeX z5>lb(Ns{nV*JwJkOY~)ix>i`9&*hUMJCe*R#WDDRQ>w2hP-DX3En5(HLv1)LGpc7+ zDrO7m;cO}?t4G7TEb25JUAld%)G1H*T%~*@b47aVt$;VmWZR%#@;Eq> zkZrSer}8KEy z!>LFjlZg!Gvgi?6y8twwD`qk9hT+#9fh^y$i;U$lb_VTS9-i|QU6`Xhf;aB6IS6p- zQUqvc3X#!VGL`A3A(c*KBB}AzpzZ;s$dz@4WJXxMxm*T=P{xSPh#uQkDwjnFX@;1^ z#yeNa&Qj_Sswcvl5VGczHEV;;Qabl%cRFW=61)l;m@zCnF+6LB*9BE)yjb=_-K^R2 z1T}`vFwY=X|0Q_W5W%#Hg;dfx8LAnhO-~rdk53fPAVj8(*2OV^*A-msi3y>D8MpCBq--|&k0g5NhS*T>Q> z=0p_l6$`{zMSS*sW2co@u2`K2`{Ao*p%W8jtAU+uVlD*d#GTNbnDSz5^t zZIZCCKCXZMGf)FpG2Q+1b*<0uRZ9^Wy?;$_D7(iCUEeh5YGzC-l(L1Gr#$pVuxtF% z)~W^Znb7kk$1@5hT(X{JF4X!j>hsm3i)iZQle`w)E@V^K2$~j`6~Fo0S{O3{{is1*bR-x$_ufI##x-5 z53$6TX)bU&FJXONXi-h+pYRKz|L>2HG@Blh?U|j2&(uYWrJq?n=ID9``I(3@qUd?L z-uXI6JRz*9;^)#T_#kjP-U*lg9Pn^?l~S6l;^*5{@E-tQQo+w-z$WW?-s|!U~Y6Qld@CEXnSQ1 z`cOmKO62p2T~;b<=XZ&rJeJW`vN$@r3nmT;>+-Z+riyPxc zywY~b!cIxH`G5RK2A0THdPsh>A{KLgPr^#cb-wds819c+%o?oFAIdoH6Be^7^o{O(h7A%zI zQ<(%AXl^WHi>M4_6l`=j2hmQABd0+CrJU>>(bS0U_O$0HNy=39e`UcuYQTna5Y^{w z@{GpOW-^NNBcd3AqZnAgQUk?dm?W~p7_M5ByLK)b7|5q~atZAmnMb&o)#epumCp{* z$_`U9{&Q90eFf7~G|q%{orx~!&^<`DKcpF^OP$K#`G14qE-+4e-ls4Pv!bSM`*pxS z#63x_pZ7CNX+1}YE{0OV_06(GSNXBpD7ByWJxmYi1Ey^Zv20J56e(SfA8nzy{wdu+ zrhFfmbSb&xcO@|F6-({seH7EB(rUK-To2Q$QBL=8S?2u~({^p|Zoh`bZZOnjwr z)5BWP9X~$)>$QEa*5muqOlh4*iR`i3J1&0T1V$>QZ6dGxG$k8n(%~&Zli-~8VQt6s zU`bOkF8AUVm;IDhV9Mv4-LTB`HkUnL&}I594y<;-^|Qb4x$Jo#%9QWlv%dTIf2i$C z)r7UCrl)I>iQAs`5fneR=XE;Mb!@1q+x`)k{a9EjGNtvfGjZG)dm0SxM#wQl81EnW zJ8N!zmYKc?n`VbSKgTf@A#bz&O-Zvd+rNea>U(?{g!eW49lN8t{oFsR zrb;HN*oRedDgP84)vwh{INQ6g9FQ(0rhYKYs}hypt{onAg5X0gnQkanvftmK6!-AN zrS06hORCtno}&z|aT&No(O$*=PgW`WlX+lOss93P?~dP{bh8lY0X{KGEFCMiV?&kt zuUf4PCbS(Bcl^kYtBF!|O4}dP4;U>0O8?c|?|A-i0Oo3 +#include + +#include + +void err_if(int condition, const char* msg) +{ + if (condition) { + fprintf(stderr, "fatal: %s\n", msg); + exit(1); + } +} + +int main(int argc, char** argv) +{ + Display* display = XOpenDisplay(NULL); + err_if(!display, "unable to open display"); + + int kbd = argc == 1 ? XkbUseCoreKbd : atoi(argv[1]); + + XkbStateRec state; + XkbGetState(display, kbd, &state); + + XkbDescPtr desc = XkbGetKeyboard(display, XkbAllComponentsMask, kbd); + char* symbols = XGetAtomName(display, desc->names->symbols); + printf("%s\n", symbols); + +#if 0 + char *group = XGetAtomName(display, desc->names->groups[state.group]); + XFree(group); +#endif + XFree(symbols); + XFree(desc); + + XCloseDisplay(display); + + return 0; +} + From 4b6b4b905217893a27bb5e336114ea78fa43e799 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Mon, 24 May 2021 12:56:02 +0200 Subject: [PATCH 222/506] [core] add custom minimizer capability Add a new set of parameters to allow modules to be customly minimized. It works like this: If a module has the parameter "minimize" set to a true value, it will *not* use the built-in minimizer, and instead look for "minimized" parameters (e.g. if date has the "format" parameter, it would look for "minimized.format" when in minimized state). This allows the user to have different parametrization for different states. Also, using the "start-minimized" parameter allows for modules to start minimized. Note: This is hinging off the *module*, not the *widget* (the current, hard-coded hiding is per-widget). This means that modules using this method will only show a single widget - the first one - when in minimized state. The module author has to account for that. see #791 --- bumblebee_status/core/module.py | 4 ++++ bumblebee_status/core/output.py | 10 ++++++++++ bumblebee_status/modules/core/datetime.py | 4 ++-- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/bumblebee_status/core/module.py b/bumblebee_status/core/module.py index c4b66a4..312862d 100644 --- a/bumblebee_status/core/module.py +++ b/bumblebee_status/core/module.py @@ -95,6 +95,8 @@ class Module(core.input.Object): self.alias = self.__config.get("__alias__", None) self.id = self.alias if self.alias else self.name self.next_update = None + self.minimized = False + self.minimized = self.parameter("start-minimized", False) self.theme = theme @@ -126,6 +128,8 @@ class Module(core.input.Object): for prefix in [self.name, self.module_name, self.alias]: value = self.__config.get("{}.{}".format(prefix, key), value) + if self.minimized: + value = self.__config.get("{}.minimized.{}".format(prefix, key), value) return value """Set a parameter for this module diff --git a/bumblebee_status/core/output.py b/bumblebee_status/core/output.py index 9c032d6..121df27 100644 --- a/bumblebee_status/core/output.py +++ b/bumblebee_status/core/output.py @@ -166,6 +166,12 @@ class i3(object): def toggle_minimize(self, event): widget_id = event["instance"] + for module in self.__modules: + if module.widget(widget_id=widget_id) and util.format.asbool(module.parameter("minimize", False)) == True: + # this module can customly minimize + module.minimized = not module.minimized + return + if widget_id in self.__content: self.__content[widget_id]["minimized"] = not self.__content[widget_id]["minimized"] @@ -216,6 +222,10 @@ class i3(object): def blocks(self, module): blocks = [] + if module.minimized: + blocks.extend(self.separator_block(module, module.widgets()[0])) + blocks.append(self.__content_block(module, module.widgets()[0])) + return blocks for widget in module.widgets(): if widget.module and self.__config.autohide(widget.module.name): if not any( diff --git a/bumblebee_status/modules/core/datetime.py b/bumblebee_status/modules/core/datetime.py index f421e15..febb5fb 100644 --- a/bumblebee_status/modules/core/datetime.py +++ b/bumblebee_status/modules/core/datetime.py @@ -21,7 +21,6 @@ class Module(core.module.Module): super().__init__(config, theme, core.widget.Widget(self.full_text)) core.input.register(self, button=core.input.LEFT_MOUSE, cmd="calendar") - self._fmt = self.parameter("format", self.default_format()) l = locale.getdefaultlocale() if not l or l == (None, None): l = ("en_US", "UTF-8") @@ -36,7 +35,8 @@ class Module(core.module.Module): def full_text(self, widget): enc = locale.getpreferredencoding() - retval = datetime.datetime.now().strftime(self._fmt) + fmt = self.parameter("format", self.default_format()) + retval = datetime.datetime.now().strftime(fmt) if hasattr(retval, "decode"): return retval.decode(enc) return retval From 51f68addcdb3dbf266a89c1510935ad230eacb5a Mon Sep 17 00:00:00 2001 From: Yufan You Date: Fri, 11 Jun 2021 17:34:46 +0800 Subject: [PATCH 223/506] [modules/playerctl]: BREAKING: use `playerctl -f` and add `playerctl.args` 1. Use `playerctl -f` to format, which is more powerful. This also fixes #767, which is caused by missing a few fields of the metadata. 2. Add `playerctl.args`, so that users can choose a specific player, etc. 3. Display nothing when there's no running player. This is a breaking change. Users need to change `{title}` to `{{title}}`. --- bumblebee_status/modules/contrib/playerctl.py | 65 +++++++++---------- docs/modules.rst | 8 ++- 2 files changed, 37 insertions(+), 36 deletions(-) diff --git a/bumblebee_status/modules/contrib/playerctl.py b/bumblebee_status/modules/contrib/playerctl.py index 3a0d7e5..d370df4 100755 --- a/bumblebee_status/modules/contrib/playerctl.py +++ b/bumblebee_status/modules/contrib/playerctl.py @@ -6,12 +6,14 @@ Requires the following executable: * playerctl Parameters: - * playerctl.format: Format string (defaults to '{artist} - {title}') - Available values are: {album}, {title}, {artist}, {trackNumber} + * playerctl.format: Format string (defaults to '{{artist}} - {{title}} {{duration(position)}}/{{duration(mpris:length)}}') + The format string is passed to 'playerctl -f' as an argument. See the 'Format Strings' section of 'man playerctl' for more information. * playerctl.layout: Comma-separated list to change order of widgets (defaults to song, previous, pause, next) Widget names are: playerctl.song, playerctl.prev, playerctl.pause, playerctl.next + * playerctl.args: The arguments added to playerctl. + You can check 'playerctl --help' or `its README `_. For example, it could be '-p vlc,%any'. -Parameters are inherited from `spotify` module, many thanks to its developers! +Parameters are inspired by the `spotify` module, many thanks to its developers! contributed by `smitajit `_ - many thanks! """ @@ -36,9 +38,8 @@ class Module(core.module.Module): ) ) - self.__song = "" - self.__cmd = "playerctl " - self.__format = self.parameter("format", "{artist} - {title}") + self.__cmd = "playerctl " + self.parameter("args", "") + " " + self.__format = self.parameter("format", "{{artist}} - {{title}} {{duration(position)}}/{{duration(mpris:length)}}") widget_map = {} for widget_name in self.__layout: @@ -48,7 +49,6 @@ class Module(core.module.Module): "button": core.input.LEFT_MOUSE, "cmd": self.__cmd + "previous", } - widget.set("state", "prev") elif widget_name == "playerctl.pause": widget_map[widget] = { "button": core.input.LEFT_MOUSE, @@ -59,7 +59,6 @@ class Module(core.module.Module): "button": core.input.LEFT_MOUSE, "cmd": self.__cmd + "next", } - widget.set("state", "next") elif widget_name == "playerctl.song": widget_map[widget] = [ { @@ -86,32 +85,32 @@ class Module(core.module.Module): def update(self): try: - self.__get_song() - - for widget in self.widgets(): - if widget.name == "playerctl.pause": - playback_status = str(util.cli.execute(self.__cmd + "status")).strip() - if playback_status != "": - if playback_status == "Playing": - widget.set("state", "playing") - else: - widget.set("state", "paused") - elif widget.name == "playerctl.song": - widget.set("state", "song") - widget.full_text(self.__song) + playback_status = str(util.cli.execute(self.__cmd + "status")).strip() except Exception as e: logging.exception(e) - self.__song = "" + playback_status = None + for widget in self.widgets(): + if playback_status: + if widget.name == "playerctl.pause": + if playback_status == "Playing": + widget.set("state", "playing") + elif playback_status == "Paused": + widget.set("state", "paused") + else: + widget.set("state", "") + elif widget.name == "playerctl.next": + widget.set("state", "next") + elif widget.name == "playerctl.prev": + widget.set("state", "prev") + elif widget.name == "playerctl.song": + widget.full_text(self.__get_song()) + else: + widget.set("state", "") + widget.full_text(" ") def __get_song(self): - album = str(util.cli.execute(self.__cmd + "metadata xesam:album")).strip() - title = str(util.cli.execute(self.__cmd + "metadata xesam:title")).strip() - artist = str(util.cli.execute(self.__cmd + "metadata xesam:albumArtist")).strip() - track_number = str(util.cli.execute(self.__cmd + "metadata xesam:trackNumber")).strip() - - self.__song = self.__format.format( - album = album, - title = title, - artist = artist, - trackNumber = track_number - ) + try: + return str(util.cli.execute(self.__cmd + "metadata -f '" + self.__format + "'")).strip() + except Exception as e: + logging.exception(e) + return " " diff --git a/docs/modules.rst b/docs/modules.rst index 0e166e6..25952d8 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -1060,12 +1060,14 @@ Requires the following executable: * playerctl Parameters: - * playerctl.format: Format string (defaults to '{artist} - {title}') - Available values are: {album}, {title}, {artist}, {trackNumber} + * playerctl.format: Format string (defaults to '{{artist}} - {{title}} {{duration(position)}}/{{duration(mpris:length)}}') + The format string is passed to 'playerctl -f' as an argument. See the 'Format Strings' section of 'man playerctl' for more information. * playerctl.layout: Comma-separated list to change order of widgets (defaults to song, previous, pause, next) Widget names are: playerctl.song, playerctl.prev, playerctl.pause, playerctl.next + * playerctl.args: The arguments added to playerctl. + You can check 'playerctl --help' or `its readme `_. For example, it could be '-p vlc,%any'. -Parameters are inherited from `spotify` module, many thanks to its developers! +Parameters are inspired by the `spotify` module, many thanks to its developers! contributed by `smitajit `_ - many thanks! From c4046d0cd22f3d238d63dd751cd7dd625a7d361e Mon Sep 17 00:00:00 2001 From: Yufan You Date: Fri, 11 Jun 2021 18:12:13 +0800 Subject: [PATCH 224/506] [doc]: link to the README instead of manpage --- bumblebee_status/modules/contrib/playerctl.py | 4 ++-- docs/modules.rst | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bumblebee_status/modules/contrib/playerctl.py b/bumblebee_status/modules/contrib/playerctl.py index d370df4..ad3cfc0 100755 --- a/bumblebee_status/modules/contrib/playerctl.py +++ b/bumblebee_status/modules/contrib/playerctl.py @@ -6,8 +6,8 @@ Requires the following executable: * playerctl Parameters: - * playerctl.format: Format string (defaults to '{{artist}} - {{title}} {{duration(position)}}/{{duration(mpris:length)}}') - The format string is passed to 'playerctl -f' as an argument. See the 'Format Strings' section of 'man playerctl' for more information. + * playerctl.format: Format string (defaults to '{{artist}} - {{title}} {{duration(position)}}/{{duration(mpris:length)}}'). + The format string is passed to 'playerctl -f' as an argument. Read `the README `_ for more information. * playerctl.layout: Comma-separated list to change order of widgets (defaults to song, previous, pause, next) Widget names are: playerctl.song, playerctl.prev, playerctl.pause, playerctl.next * playerctl.args: The arguments added to playerctl. diff --git a/docs/modules.rst b/docs/modules.rst index 25952d8..49e509b 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -1060,8 +1060,8 @@ Requires the following executable: * playerctl Parameters: - * playerctl.format: Format string (defaults to '{{artist}} - {{title}} {{duration(position)}}/{{duration(mpris:length)}}') - The format string is passed to 'playerctl -f' as an argument. See the 'Format Strings' section of 'man playerctl' for more information. + * playerctl.format: Format string (defaults to '{{artist}} - {{title}} {{duration(position)}}/{{duration(mpris:length)}}'). + The format string is passed to 'playerctl -f' as an argument. Read `the README `_ for more information. * playerctl.layout: Comma-separated list to change order of widgets (defaults to song, previous, pause, next) Widget names are: playerctl.song, playerctl.prev, playerctl.pause, playerctl.next * playerctl.args: The arguments added to playerctl. From e5007a5729fdf6cdd743b247cb0ab65613f90656 Mon Sep 17 00:00:00 2001 From: Sayan Date: Thu, 24 Jun 2021 23:17:35 +0530 Subject: [PATCH 225/506] Add active gpu module using optimus-manager --- bumblebee_status/modules/contrib/optman.py | 36 ++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 bumblebee_status/modules/contrib/optman.py diff --git a/bumblebee_status/modules/contrib/optman.py b/bumblebee_status/modules/contrib/optman.py new file mode 100644 index 0000000..c928f24 --- /dev/null +++ b/bumblebee_status/modules/contrib/optman.py @@ -0,0 +1,36 @@ +"""Displays currently active gpu by optimus-manager +Requires the following packages: + + * optimus-manager + +""" + +import subprocess + +import core.module +import core.widget + + +class Module(core.module.Module): + def __init__(self, config, theme): + super().__init__(config, theme, core.widget.Widget(self.output)) + self.__gpumode = "" + + def output(self, _): + return "GPU: {}".format(self.__gpumode) + + def update(self): + cmd = ["optimus-manager", "--print-mode"] + output = ( + subprocess.Popen(cmd, stdout=subprocess.PIPE) + .communicate()[0] + .decode("utf-8") + .lower() + ) + + if "intel" in output: + self.__gpumode = "Intel" + elif "nvidia" in output: + self.__gpumode = "Nvidia" + elif "amd" in output: + self.__gpumode = "AMD" From 37ccbd7f4a1e7aa359de4b437eaf58f035673ed9 Mon Sep 17 00:00:00 2001 From: Yufan You Date: Sat, 26 Jun 2021 18:19:12 +0800 Subject: [PATCH 226/506] [modules/playerctl]: support the stopped status --- bumblebee_status/modules/contrib/playerctl.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bumblebee_status/modules/contrib/playerctl.py b/bumblebee_status/modules/contrib/playerctl.py index ad3cfc0..e02fa84 100755 --- a/bumblebee_status/modules/contrib/playerctl.py +++ b/bumblebee_status/modules/contrib/playerctl.py @@ -96,6 +96,8 @@ class Module(core.module.Module): widget.set("state", "playing") elif playback_status == "Paused": widget.set("state", "paused") + elif playback_status == "Stopped": + widget.set("state", "stopped") else: widget.set("state", "") elif widget.name == "playerctl.next": From 4485b65722645d6c9617b5ff4aea6d62ee8a9adf Mon Sep 17 00:00:00 2001 From: Sayan Sil Date: Wed, 30 Jun 2021 11:31:42 +0530 Subject: [PATCH 227/506] Use the existing util.cli module --- bumblebee_status/modules/contrib/optman.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/bumblebee_status/modules/contrib/optman.py b/bumblebee_status/modules/contrib/optman.py index c928f24..337003c 100644 --- a/bumblebee_status/modules/contrib/optman.py +++ b/bumblebee_status/modules/contrib/optman.py @@ -5,11 +5,10 @@ Requires the following packages: """ -import subprocess - import core.module import core.widget +import util.cli class Module(core.module.Module): def __init__(self, config, theme): @@ -20,13 +19,8 @@ class Module(core.module.Module): return "GPU: {}".format(self.__gpumode) def update(self): - cmd = ["optimus-manager", "--print-mode"] - output = ( - subprocess.Popen(cmd, stdout=subprocess.PIPE) - .communicate()[0] - .decode("utf-8") - .lower() - ) + cmd = "optimus-manager --print-mode" + output = util.cli.execute(cmd).strip() if "intel" in output: self.__gpumode = "Intel" From 1232c4d96092be16c76a9baac35a8c4bb4868683 Mon Sep 17 00:00:00 2001 From: nepoz Date: Mon, 5 Jul 2021 07:55:47 -0500 Subject: [PATCH 228/506] Initial commit -- give basic message about interface being used --- bumblebee_status/modules/contrib/network.py | 48 +++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 bumblebee_status/modules/contrib/network.py diff --git a/bumblebee_status/modules/contrib/network.py b/bumblebee_status/modules/contrib/network.py new file mode 100644 index 0000000..37115e1 --- /dev/null +++ b/bumblebee_status/modules/contrib/network.py @@ -0,0 +1,48 @@ +""" +A module to show currently active network connection (ethernet or wifi) +and connection strength. +""" + +import subprocess +import os + +import core.module +import core.widget + + +class Module(core.module.Module): + def __init__(self, config, theme): + super().__init__(config, theme, core.widget.Widget(self.network)) + self._is_wireless = True + self._interface = None + self._message = None + + def network(self, widgets): + # start subprocess to get networked data + std_out = os.popen("ip route get 8.8.8.8") + route_str = " ".join(std_out.read().split()) + route_tokens = route_str.split(" ") + + try: + self._interface = route_tokens[route_tokens.index("dev") + 1] + ":" + except ValueError: + self._interface = None + + with open("/proc/net/wireless", "r") as f: + if self._interface: + self._is_wireless = self._interface in f.read() + + # setup message to send to bar + if self._interface is None: + self._message = "Not connected to a network" + elif self._is_wireless: + self._message = "Connected to WiFi" + else: + # self._message = "Connected to Ethernet" + self._message = self._message + + + return self._message + + + From f141b95d8f7dd53e86b1e9aff3ce9b12a0178be8 Mon Sep 17 00:00:00 2001 From: nepoz Date: Mon, 5 Jul 2021 10:29:37 -0500 Subject: [PATCH 229/506] Basic functionaly for dealingn with signal strength --- bumblebee_status/modules/contrib/network.py | 49 ++++++++++++++++----- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/bumblebee_status/modules/contrib/network.py b/bumblebee_status/modules/contrib/network.py index 37115e1..c40d0d8 100644 --- a/bumblebee_status/modules/contrib/network.py +++ b/bumblebee_status/modules/contrib/network.py @@ -3,8 +3,9 @@ A module to show currently active network connection (ethernet or wifi) and connection strength. """ -import subprocess -import os + +import util.cli +import util.format import core.module import core.widget @@ -18,31 +19,55 @@ class Module(core.module.Module): self._message = None def network(self, widgets): - # start subprocess to get networked data - std_out = os.popen("ip route get 8.8.8.8") - route_str = " ".join(std_out.read().split()) + # run ip route command, tokenize output + cmd = "ip route get 8.8.8.8" + std_out = util.cli.execute(cmd) + route_str = " ".join(std_out.split()) route_tokens = route_str.split(" ") + # Attempt to extract a valid network interface device try: - self._interface = route_tokens[route_tokens.index("dev") + 1] + ":" + self._interface = route_tokens[route_tokens.index("dev") + 1] except ValueError: self._interface = None - with open("/proc/net/wireless", "r") as f: - if self._interface: + # Check to see if the interface (if it exists) is wireless + if self._interface: + with open("/proc/net/wireless", "r") as f: self._is_wireless = self._interface in f.read() + f.close() - # setup message to send to bar + # setup message to send to the user if self._interface is None: self._message = "Not connected to a network" elif self._is_wireless: - self._message = "Connected to WiFi" + cmd = "iwgetid" + iw_dat = util.cli.execute(cmd) + has_ssid = "ESSID" in iw_dat + ssid = iw_dat[iw_dat.index(":") + 2: -2] if has_ssid else "Unknown" + + # Get connection strength + cmd = "iwconfig {}".format(self._interface) + config_dat = " ".join(util.cli.execute(cmd).split()) + config_tokens = config_dat.replace("=", " ").split() + strength = config_tokens[config_tokens.index("level") + 1] + strength = util.format.asint(strength, minimum=-110, maximum=-30) + + self._message = self.__generate_wireless_message(ssid, strength) else: - # self._message = "Connected to Ethernet" self._message = self._message - return self._message + def __generate_wireless_message(self, ssid, strength): + computed_strength = 100 * (strength + 110) / 70.0 + if computed_strength < 25: + return ssid + " poor" + if computed_strength < 50: + return ssid + " fair" + if computed_strength < 75: + return ssid + " good" + + return ssid + " excellent" From 4987c7d3e2fa1152224c3d7d540f56dbfdf315dc Mon Sep 17 00:00:00 2001 From: nepoz Date: Mon, 5 Jul 2021 11:26:46 -0500 Subject: [PATCH 230/506] added stateful behavior --- bumblebee_status/modules/contrib/network.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/bumblebee_status/modules/contrib/network.py b/bumblebee_status/modules/contrib/network.py index c40d0d8..30601b5 100644 --- a/bumblebee_status/modules/contrib/network.py +++ b/bumblebee_status/modules/contrib/network.py @@ -17,6 +17,7 @@ class Module(core.module.Module): self._is_wireless = True self._interface = None self._message = None + self.__signal = -110 def network(self, widgets): # run ip route command, tokenize output @@ -51,19 +52,27 @@ class Module(core.module.Module): config_dat = " ".join(util.cli.execute(cmd).split()) config_tokens = config_dat.replace("=", " ").split() strength = config_tokens[config_tokens.index("level") + 1] - strength = util.format.asint(strength, minimum=-110, maximum=-30) + self.__signal = util.format.asint(strength, minimum=-110, maximum=-30) - self._message = self.__generate_wireless_message(ssid, strength) + self._message = self.__generate_wireless_message(ssid, self.__signal) else: self._message = self._message return self._message + def state(self, widget): + if self.__signal < -65: + return "warning" + if self.__signal < -80: + return "critical" + return None + + def __generate_wireless_message(self, ssid, strength): - computed_strength = 100 * (strength + 110) / 70.0 - if computed_strength < 25: - return ssid + " poor" + computed_strength = 100 * ((strength + 100) / 70.0) + if computed_strength < 30: + return ssid + " poor" if computed_strength < 50: return ssid + " fair" if computed_strength < 75: From 448ab6de836b0eedcc7250c0aa97104f3df0c696 Mon Sep 17 00:00:00 2001 From: nepoz Date: Mon, 5 Jul 2021 12:34:42 -0500 Subject: [PATCH 231/506] Functional display for wireless connection --- bumblebee_status/modules/contrib/network.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/bumblebee_status/modules/contrib/network.py b/bumblebee_status/modules/contrib/network.py index 30601b5..f91073e 100644 --- a/bumblebee_status/modules/contrib/network.py +++ b/bumblebee_status/modules/contrib/network.py @@ -9,9 +9,11 @@ import util.format import core.module import core.widget +import core.input class Module(core.module.Module): + @core.decorators.every(seconds=10) def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.network)) self._is_wireless = True @@ -19,6 +21,9 @@ class Module(core.module.Module): self._message = None self.__signal = -110 + # Set up event handler for left mouse click + core.input.register(self, button=core.input.LEFT_MOUSE, cmd="nm-connection-editor") + def network(self, widgets): # run ip route command, tokenize output cmd = "ip route get 8.8.8.8" @@ -71,12 +76,7 @@ class Module(core.module.Module): def __generate_wireless_message(self, ssid, strength): computed_strength = 100 * ((strength + 100) / 70.0) - if computed_strength < 30: - return ssid + " poor" - if computed_strength < 50: - return ssid + " fair" - if computed_strength < 75: - return ssid + " good" + return " {} {}%".format(ssid, int(computed_strength)) + - return ssid + " excellent" From c7df1926dce8fdf3fb0427d79e802955a60694d6 Mon Sep 17 00:00:00 2001 From: nepoz Date: Mon, 5 Jul 2021 13:09:17 -0500 Subject: [PATCH 232/506] Formatting fixes, fixed state management and added some icons --- bumblebee_status/modules/contrib/network.py | 36 +++++++++++---------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/bumblebee_status/modules/contrib/network.py b/bumblebee_status/modules/contrib/network.py index f91073e..924c7f4 100644 --- a/bumblebee_status/modules/contrib/network.py +++ b/bumblebee_status/modules/contrib/network.py @@ -16,9 +16,9 @@ class Module(core.module.Module): @core.decorators.every(seconds=10) def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.network)) - self._is_wireless = True - self._interface = None - self._message = None + self.__is_wireless = True + self.__interface = None + self.__message = None self.__signal = -110 # Set up event handler for left mouse click @@ -33,48 +33,50 @@ class Module(core.module.Module): # Attempt to extract a valid network interface device try: - self._interface = route_tokens[route_tokens.index("dev") + 1] + self.__interface = route_tokens[route_tokens.index("dev") + 1] except ValueError: - self._interface = None + self.__interface = None # Check to see if the interface (if it exists) is wireless - if self._interface: + if self.__interface: with open("/proc/net/wireless", "r") as f: - self._is_wireless = self._interface in f.read() + self.__is_wireless = self.__interface in f.read() f.close() # setup message to send to the user - if self._interface is None: - self._message = "Not connected to a network" - elif self._is_wireless: + if self.__interface is None: + self.__message = " No connection" + elif self.__is_wireless: cmd = "iwgetid" iw_dat = util.cli.execute(cmd) has_ssid = "ESSID" in iw_dat ssid = iw_dat[iw_dat.index(":") + 2: -2] if has_ssid else "Unknown" # Get connection strength - cmd = "iwconfig {}".format(self._interface) + cmd = "iwconfig {}".format(self.__interface) config_dat = " ".join(util.cli.execute(cmd).split()) config_tokens = config_dat.replace("=", " ").split() strength = config_tokens[config_tokens.index("level") + 1] self.__signal = util.format.asint(strength, minimum=-110, maximum=-30) - self._message = self.__generate_wireless_message(ssid, self.__signal) + self.__message = self.__generate_wireles_message(ssid, self.__signal) else: - self._message = self._message + self.__signal = -30 + self.__message = " Ethernet" - return self._message + return self.__message def state(self, widget): - if self.__signal < -65: - return "warning" if self.__signal < -80: return "critical" + if self.__signal < -65: + return "warning" + return None - def __generate_wireless_message(self, ssid, strength): + def __generate_wireles_message(self, ssid, strength): computed_strength = 100 * ((strength + 100) / 70.0) return " {} {}%".format(ssid, int(computed_strength)) From 911230c65998ac8969eba88507c09b8de463373e Mon Sep 17 00:00:00 2001 From: nepoz Date: Mon, 5 Jul 2021 13:54:28 -0500 Subject: [PATCH 233/506] first complete implementation of the network module --- bumblebee_status/modules/contrib/network.py | 34 ++++++++++++--------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/bumblebee_status/modules/contrib/network.py b/bumblebee_status/modules/contrib/network.py index 924c7f4..0c366d6 100644 --- a/bumblebee_status/modules/contrib/network.py +++ b/bumblebee_status/modules/contrib/network.py @@ -1,6 +1,8 @@ """ -A module to show currently active network connection (ethernet or wifi) -and connection strength. +A module to show currently active network connection (ethernet or wifi) and connection strength if the connection is wireless. + +Dependencies: nm-connection-editor if users would like a graphical +network manager when left-clicking the module """ @@ -16,14 +18,17 @@ class Module(core.module.Module): @core.decorators.every(seconds=10) def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.network)) - self.__is_wireless = True + self.__is_wireless = False self.__interface = None self.__message = None self.__signal = -110 # Set up event handler for left mouse click - core.input.register(self, button=core.input.LEFT_MOUSE, cmd="nm-connection-editor") + core.input.register( + self, button=core.input.LEFT_MOUSE, cmd="nm-connection-editor" + ) + # Get network information to display to the user def network(self, widgets): # run ip route command, tokenize output cmd = "ip route get 8.8.8.8" @@ -33,7 +38,7 @@ class Module(core.module.Module): # Attempt to extract a valid network interface device try: - self.__interface = route_tokens[route_tokens.index("dev") + 1] + self.__interface = route_tokens[route_tokens.index("dev") + 1] except ValueError: self.__interface = None @@ -47,10 +52,13 @@ class Module(core.module.Module): if self.__interface is None: self.__message = " No connection" elif self.__is_wireless: - cmd = "iwgetid" - iw_dat = util.cli.execute(cmd) + iw_dat = util.cli.execute("iwgetid") has_ssid = "ESSID" in iw_dat - ssid = iw_dat[iw_dat.index(":") + 2: -2] if has_ssid else "Unknown" + ssid = ( + iw_dat[iw_dat.index(":") + 1 :].replace('"', "").strip() + if has_ssid + else "Unknown" + ) # Get connection strength cmd = "iwconfig {}".format(self.__interface) @@ -61,12 +69,13 @@ class Module(core.module.Module): self.__message = self.__generate_wireles_message(ssid, self.__signal) else: + # Set signal to -30 as ethernet shouldn't have signal issues self.__signal = -30 - self.__message = " Ethernet" + self.__message = " Ethernet" return self.__message - + # The signal is measured in decibels/milliwatt, hence the weird numbers def state(self, widget): if self.__signal < -80: return "critical" @@ -75,10 +84,7 @@ class Module(core.module.Module): return None - + # manually done for better granularity / ease of parsing strength data def __generate_wireles_message(self, ssid, strength): computed_strength = 100 * ((strength + 100) / 70.0) return " {} {}%".format(ssid, int(computed_strength)) - - - From 3f524ab371a1780619914c12fbb7d87836a57f92 Mon Sep 17 00:00:00 2001 From: nepoz Date: Thu, 8 Jul 2021 09:04:40 -0500 Subject: [PATCH 234/506] Refactoring, making use of netifaces --- bumblebee_status/modules/contrib/network.py | 82 +++++++++++---------- 1 file changed, 44 insertions(+), 38 deletions(-) diff --git a/bumblebee_status/modules/contrib/network.py b/bumblebee_status/modules/contrib/network.py index 0c366d6..d90a21b 100644 --- a/bumblebee_status/modules/contrib/network.py +++ b/bumblebee_status/modules/contrib/network.py @@ -13,78 +13,84 @@ import core.module import core.widget import core.input +import netifaces +import socket + class Module(core.module.Module): @core.decorators.every(seconds=10) def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.network)) self.__is_wireless = False + self.__is_connected = False self.__interface = None self.__message = None self.__signal = -110 - # Set up event handler for left mouse click - core.input.register( - self, button=core.input.LEFT_MOUSE, cmd="nm-connection-editor" - ) - # Get network information to display to the user def network(self, widgets): - # run ip route command, tokenize output - cmd = "ip route get 8.8.8.8" - std_out = util.cli.execute(cmd) - route_str = " ".join(std_out.split()) - route_tokens = route_str.split(" ") + # Determine whether there is an internet connection + try: + socket.create_connection(("1.1.1.1", 53)) + self.__is_connected = True + except OSError: + self.__is_connected = False # Attempt to extract a valid network interface device - try: - self.__interface = route_tokens[route_tokens.index("dev") + 1] - except ValueError: - self.__interface = None + self.__interface = netifaces.gateways()["default"][netifaces.AF_INET][1] - # Check to see if the interface (if it exists) is wireless - if self.__interface: + # Check to see if the interface (if connected to the internet) is wireless + if self.__is_connected: with open("/proc/net/wireless", "r") as f: self.__is_wireless = self.__interface in f.read() - f.close() + f.close() # setup message to send to the user - if self.__interface is None: - self.__message = " No connection" - elif self.__is_wireless: + if not self.__is_connected: + self.__message = "No connection" + elif not self.__is_wireless: + # Assuming that if user is connected via non-wireless means that it will be ethernet + self.__signal = -30 + self.__message = "Ethernet" + else: + # We have a wireless connection iw_dat = util.cli.execute("iwgetid") has_ssid = "ESSID" in iw_dat + signal = self.__compute_signal(self.__interface) + self.__signal = util.format.asint(signal, minimum=-110, maximum=-30) + ssid = ( iw_dat[iw_dat.index(":") + 1 :].replace('"', "").strip() if has_ssid else "Unknown" ) - - # Get connection strength - cmd = "iwconfig {}".format(self.__interface) - config_dat = " ".join(util.cli.execute(cmd).split()) - config_tokens = config_dat.replace("=", " ").split() - strength = config_tokens[config_tokens.index("level") + 1] - self.__signal = util.format.asint(strength, minimum=-110, maximum=-30) - self.__message = self.__generate_wireles_message(ssid, self.__signal) - else: - # Set signal to -30 as ethernet shouldn't have signal issues - self.__signal = -30 - self.__message = " Ethernet" return self.__message - # The signal is measured in decibels/milliwatt, hence the weird numbers + # State determined by signal strength def state(self, widget): - if self.__signal < -80: + if self.__compute_strength(self.__signal) < 50: return "critical" - if self.__signal < -65: + if self.__compute_strength(self.__signal) < 75: return "warning" return None # manually done for better granularity / ease of parsing strength data - def __generate_wireles_message(self, ssid, strength): - computed_strength = 100 * ((strength + 100) / 70.0) - return " {} {}%".format(ssid, int(computed_strength)) + def __generate_wireles_message(self, ssid, signal): + computed_strength = self.__compute_strength(signal) + return "{} {}%".format(ssid, int(computed_strength)) + + def __compute_strength(self, signal): + return 100 * ((signal + 100) / 70.0) + + # get signal strength in decibels/milliwat + def __compute_signal(self, interface): + # Get connection strength + cmd = "iwconfig {}".format(interface) + config_dat = " ".join(util.cli.execute(cmd).split()) + config_tokens = config_dat.replace("=", " ").split() + signal = config_tokens[config_tokens.index("level") + 1] + + return signal From 2100a7cfdbb5b768136c5c5c01a087f8541229db Mon Sep 17 00:00:00 2001 From: nepoz Date: Thu, 8 Jul 2021 12:10:46 -0500 Subject: [PATCH 235/506] Set up initial testing framework for network module --- bumblebee_status/modules/contrib/network.py | 2 +- tests/modules/contrib/test_network.py | 65 +++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 tests/modules/contrib/test_network.py diff --git a/bumblebee_status/modules/contrib/network.py b/bumblebee_status/modules/contrib/network.py index d90a21b..26c7889 100644 --- a/bumblebee_status/modules/contrib/network.py +++ b/bumblebee_status/modules/contrib/network.py @@ -33,7 +33,7 @@ class Module(core.module.Module): try: socket.create_connection(("1.1.1.1", 53)) self.__is_connected = True - except OSError: + except: self.__is_connected = False # Attempt to extract a valid network interface device diff --git a/tests/modules/contrib/test_network.py b/tests/modules/contrib/test_network.py new file mode 100644 index 0000000..e171d69 --- /dev/null +++ b/tests/modules/contrib/test_network.py @@ -0,0 +1,65 @@ +import pytest +from unittest import TestCase, mock + +import core.config +import core.widget +import modules.contrib.network + +def build_module(): + config = core.config.Config([]) + return modules.contrib.network.Module(config=config, theme=None) + +def wireless_default(): + return { + "default": { + 1: ('10.0.1.12', 'wlan0') + } + } + +def wired_default(): + return { + 'default': { + 18: ('10.0.1.12', 'eth0') + } + } + +def exec_side_effect(*args, **kwargs): + if args[0] == "iwgetid": + return "ESSID: bumblefoo" + elif "iwconfig" in args[0]: + return "level=-30" + + return "default" + + +class TestNetworkUnit(TestCase): + def test_load_module(self): + __import__("modules.contrib.network") + + @pytest.mark.allow_hosts(['127.0.0.1']) + def test_no_internet(self): + module = build_module() + assert module.widgets()[0].full_text() == "No connection" + + @mock.patch('util.cli.execute') + @mock.patch('netifaces.gateways') + @mock.patch('netifaces.AF_INET', 1) + def test_wireless_connection(self, gateways_mock, execute_mock): + fake_ssid = "bumblefoo" + gateways_mock.return_value = wireless_default() + execute_mock.side_effect = exec_side_effect + + module = build_module() + + assert fake_ssid in module.widgets()[0].full_text() + + @mock.patch('util.cli.execute') + @mock.patch('netifaces.gateways') + @mock.patch('netifaces.AF_INET', 18) + def test_wired_connection(self, gateways_mock, execute_mock): + gateways_mock.return_value = wired_default() + execute_mock.side_effect = exec_side_effect + + module = build_module() + + assert module.widgets()[0].full_text() == "Ethernet" From f9017c3a38e94d8ab67cc15d71e12bf0a63ca3e0 Mon Sep 17 00:00:00 2001 From: nepoz Date: Thu, 8 Jul 2021 22:55:23 -0500 Subject: [PATCH 236/506] Added more tests and exception handling --- bumblebee_status/modules/contrib/network.py | 43 ++++++---- tests/modules/contrib/test_network.py | 92 +++++++++++++++------ 2 files changed, 96 insertions(+), 39 deletions(-) diff --git a/bumblebee_status/modules/contrib/network.py b/bumblebee_status/modules/contrib/network.py index 26c7889..6c35ee4 100644 --- a/bumblebee_status/modules/contrib/network.py +++ b/bumblebee_status/modules/contrib/network.py @@ -1,8 +1,6 @@ """ A module to show currently active network connection (ethernet or wifi) and connection strength if the connection is wireless. -Dependencies: nm-connection-editor if users would like a graphical -network manager when left-clicking the module """ @@ -18,7 +16,7 @@ import socket class Module(core.module.Module): - @core.decorators.every(seconds=10) + @core.decorators.every(seconds=5) def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.network)) self.__is_wireless = False @@ -33,20 +31,26 @@ class Module(core.module.Module): try: socket.create_connection(("1.1.1.1", 53)) self.__is_connected = True - except: + except Exception: self.__is_connected = False # Attempt to extract a valid network interface device - self.__interface = netifaces.gateways()["default"][netifaces.AF_INET][1] + try: + self.__interface = netifaces.gateways()["default"][netifaces.AF_INET][1] + except Exception: + self.__interface = None # Check to see if the interface (if connected to the internet) is wireless - if self.__is_connected: - with open("/proc/net/wireless", "r") as f: - self.__is_wireless = self.__interface in f.read() - f.close() + if self.__is_connected and self.__interface: + try: + with open("/proc/net/wireless", "r") as f: + self.__is_wireless = self.__interface in f.read() + f.close() + except Exception: + self.__is_wireless = False # setup message to send to the user - if not self.__is_connected: + if not self.__is_connected or not self.__interface: self.__message = "No connection" elif not self.__is_wireless: # Assuming that if user is connected via non-wireless means that it will be ethernet @@ -57,7 +61,11 @@ class Module(core.module.Module): iw_dat = util.cli.execute("iwgetid") has_ssid = "ESSID" in iw_dat signal = self.__compute_signal(self.__interface) - self.__signal = util.format.asint(signal, minimum=-110, maximum=-30) + + # If signal is None, that means that we can't compute the default interface's signal strength + self.__signal = ( + util.format.asint(signal, minimum=-110, maximum=-30) if signal else None + ) ssid = ( iw_dat[iw_dat.index(":") + 1 :].replace('"', "").strip() @@ -80,10 +88,12 @@ class Module(core.module.Module): # manually done for better granularity / ease of parsing strength data def __generate_wireles_message(self, ssid, signal): computed_strength = self.__compute_strength(signal) - return "{} {}%".format(ssid, int(computed_strength)) + strength_str = str(computed_strength) if computed_strength else "?" + + return "{} {}%".format(ssid, strength_str) def __compute_strength(self, signal): - return 100 * ((signal + 100) / 70.0) + return int(100 * ((signal + 100) / 70.0)) if signal else None # get signal strength in decibels/milliwat def __compute_signal(self, interface): @@ -91,6 +101,11 @@ class Module(core.module.Module): cmd = "iwconfig {}".format(interface) config_dat = " ".join(util.cli.execute(cmd).split()) config_tokens = config_dat.replace("=", " ").split() - signal = config_tokens[config_tokens.index("level") + 1] + + # handle weird output + try: + signal = config_tokens[config_tokens.index("level") + 1] + except Exception: + signal = None return signal diff --git a/tests/modules/contrib/test_network.py b/tests/modules/contrib/test_network.py index e171d69..9b270bf 100644 --- a/tests/modules/contrib/test_network.py +++ b/tests/modules/contrib/test_network.py @@ -1,65 +1,107 @@ -import pytest from unittest import TestCase, mock +import pytest import core.config import core.widget import modules.contrib.network +import socket + +pytest.importorskip("netifaces") + + def build_module(): config = core.config.Config([]) return modules.contrib.network.Module(config=config, theme=None) + def wireless_default(): - return { - "default": { - 1: ('10.0.1.12', 'wlan0') - } - } + return {"default": {1: ("10.0.1.12", "wlan3")}} + def wired_default(): - return { - 'default': { - 18: ('10.0.1.12', 'eth0') - } - } + return {"default": {18: ("10.0.1.12", "eth3")}} -def exec_side_effect(*args, **kwargs): + +def exec_side_effect_valid(*args, **kwargs): if args[0] == "iwgetid": return "ESSID: bumblefoo" - elif "iwconfig" in args[0]: + if "iwconfig" in args[0]: return "level=-30" + return mock.DEFAULT - return "default" + +def exec_side_effect_invalid(*args, **kwargs): + return "invalid gibberish, can't parse for info" class TestNetworkUnit(TestCase): def test_load_module(self): __import__("modules.contrib.network") - @pytest.mark.allow_hosts(['127.0.0.1']) + @pytest.mark.allow_hosts(["127.0.0.1"]) def test_no_internet(self): module = build_module() assert module.widgets()[0].full_text() == "No connection" - @mock.patch('util.cli.execute') - @mock.patch('netifaces.gateways') - @mock.patch('netifaces.AF_INET', 1) - def test_wireless_connection(self, gateways_mock, execute_mock): + @mock.patch("util.cli.execute") + @mock.patch("netifaces.gateways") + @mock.patch("socket.create_connection") + @mock.patch("netifaces.AF_INET", 1) + @mock.patch("builtins.open", mock.mock_open(read_data="wlan3")) + def test_valid_wireless_connection(self, socket_mock, gateways_mock, execute_mock): + socket_mock.return_value = mock.MagicMock() fake_ssid = "bumblefoo" gateways_mock.return_value = wireless_default() - execute_mock.side_effect = exec_side_effect + execute_mock.side_effect = exec_side_effect_valid module = build_module() assert fake_ssid in module.widgets()[0].full_text() - @mock.patch('util.cli.execute') - @mock.patch('netifaces.gateways') - @mock.patch('netifaces.AF_INET', 18) - def test_wired_connection(self, gateways_mock, execute_mock): + @mock.patch("netifaces.gateways") + @mock.patch("socket.create_connection") + @mock.patch("netifaces.AF_INET", 18) + @mock.patch("builtins.open", mock.mock_open(read_data="wlan3")) + def test_valid_wired_connection(self, socket_mock, gateways_mock): gateways_mock.return_value = wired_default() - execute_mock.side_effect = exec_side_effect + socket_mock.return_value = mock.MagicMock() module = build_module() assert module.widgets()[0].full_text() == "Ethernet" + + @mock.patch("netifaces.gateways") + @mock.patch("socket.create_connection") + def test_invalid_gateways(self, socket_mock, gateways_mock): + socket_mock.return_value = mock.Mock() + gateways_mock.return_value = {"xyz": "abc"} + + module = build_module() + assert module.widgets()[0].full_text() == "No connection" + + @mock.patch("util.cli.execute") + @mock.patch("socket.create_connection") + @mock.patch("netifaces.gateways") + @mock.patch("netifaces.AF_INET", 1) + @mock.patch("builtins.open", mock.mock_open(read_data="wlan3")) + def test_invalid_execs(self, gateways_mock, socket_mock, execute_mock): + execute_mock.side_effect = exec_side_effect_invalid + socket_mock.return_value = mock.MagicMock() + gateways_mock.return_value = wireless_default() + + module = build_module() + + assert module.widgets()[0].full_text() == "Unknown ?%" + + @mock.patch("builtins.open", **{"return_value.raiseError.side_effect": Exception()}) + @mock.patch("socket.create_connection") + @mock.patch("netifaces.gateways") + @mock.patch("netifaces.AF_INET", 18) + @mock.patch("builtins.open", mock.mock_open(read_data="wlan3")) + def test_no_wireless_file(self, gateways_mock, socket_mock, mock_open): + gateways_mock.return_value = wired_default() + socket_mock.return_value = mock.MagicMock() + module = build_module() + + assert module.widgets()[0].full_text() == "Ethernet" From 48501fa53478075ed0d6ce5d66ddb3499264bb86 Mon Sep 17 00:00:00 2001 From: nepoz Date: Thu, 8 Jul 2021 23:00:57 -0500 Subject: [PATCH 237/506] Updated docstring --- bumblebee_status/modules/contrib/network.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bumblebee_status/modules/contrib/network.py b/bumblebee_status/modules/contrib/network.py index 6c35ee4..b8d16a4 100644 --- a/bumblebee_status/modules/contrib/network.py +++ b/bumblebee_status/modules/contrib/network.py @@ -1,5 +1,9 @@ """ -A module to show currently active network connection (ethernet or wifi) and connection strength if the connection is wireless. +A module to show the currently active network connection (ethernet or wifi) and connection strength if the connection is wireless. + +Requires the Python netifaces package and iw installed on Linux. + +A simpler take on nic and network_traffic. No extra config necessary! """ From 5d80a5a1a0470b72a79608d1952eb126f7d37ab4 Mon Sep 17 00:00:00 2001 From: nepoz Date: Fri, 9 Jul 2021 00:28:00 -0500 Subject: [PATCH 238/506] Slight refactoring to try and break apart networkmethod --- bumblebee_status/modules/contrib/network.py | 35 ++++++++++++++------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/bumblebee_status/modules/contrib/network.py b/bumblebee_status/modules/contrib/network.py index b8d16a4..a91c947 100644 --- a/bumblebee_status/modules/contrib/network.py +++ b/bumblebee_status/modules/contrib/network.py @@ -32,11 +32,7 @@ class Module(core.module.Module): # Get network information to display to the user def network(self, widgets): # Determine whether there is an internet connection - try: - socket.create_connection(("1.1.1.1", 53)) - self.__is_connected = True - except Exception: - self.__is_connected = False + self.__is_connected = self.__attempt_connection() # Attempt to extract a valid network interface device try: @@ -46,12 +42,7 @@ class Module(core.module.Module): # Check to see if the interface (if connected to the internet) is wireless if self.__is_connected and self.__interface: - try: - with open("/proc/net/wireless", "r") as f: - self.__is_wireless = self.__interface in f.read() - f.close() - except Exception: - self.__is_wireless = False + self.__is_wireless = self.__interface_is_wireless(self.__interface) # setup message to send to the user if not self.__is_connected or not self.__interface: @@ -113,3 +104,25 @@ class Module(core.module.Module): signal = None return signal + + def __attempt_connection(self): + can_connect = False + try: + socket.create_connection(("1.1.1.1", 53)) + can_connect = True + except Exception: + can_connect = False + + return can_connect + + def __interface_is_wireless(self, interface): + is_wireless = False + try: + with open("/proc/net/wireless", "r") as f: + is_wireless = interface in f.read() + f.close() + except Exception: + is_wireless = False + + return is_wireless + From 5a2dfc226b5350f70adc790f9650217351540121 Mon Sep 17 00:00:00 2001 From: nepoz Date: Fri, 9 Jul 2021 00:58:09 -0500 Subject: [PATCH 239/506] Removed dependency on pytest-socket for the Network module's unit tests. --- tests/modules/contrib/test_network.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/modules/contrib/test_network.py b/tests/modules/contrib/test_network.py index 9b270bf..05ec3b1 100644 --- a/tests/modules/contrib/test_network.py +++ b/tests/modules/contrib/test_network.py @@ -39,8 +39,9 @@ class TestNetworkUnit(TestCase): def test_load_module(self): __import__("modules.contrib.network") - @pytest.mark.allow_hosts(["127.0.0.1"]) - def test_no_internet(self): + @mock.patch("socket.create_connection") + def test_no_internet(self, socket_mock): + socket_mock.side_effect = Exception() module = build_module() assert module.widgets()[0].full_text() == "No connection" From 98c92bb78facdf0d8456127a437d7fa4b2014839 Mon Sep 17 00:00:00 2001 From: Tom Saleeba Date: Sat, 24 Jul 2021 15:18:04 -0600 Subject: [PATCH 240/506] feat: add GPU usage % and GPU memory usage % to nvidiagpu --- bumblebee_status/modules/contrib/nvidiagpu.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/bumblebee_status/modules/contrib/nvidiagpu.py b/bumblebee_status/modules/contrib/nvidiagpu.py index 4aa9de9..2731314 100644 --- a/bumblebee_status/modules/contrib/nvidiagpu.py +++ b/bumblebee_status/modules/contrib/nvidiagpu.py @@ -4,7 +4,7 @@ Parameters: * nvidiagpu.format: Format string (defaults to '{name}: {temp}°C %{usedmem}/{totalmem} MiB') - Available values are: {name} {temp} {mem_used} {mem_total} {fanspeed} {clock_gpu} {clock_mem} + Available values are: {name} {temp} {mem_used} {mem_total} {fanspeed} {clock_gpu} {clock_mem} {gpu_usage_pct} {mem_usage_pct} Requires nvidia-smi @@ -41,6 +41,8 @@ class Module(core.module.Module): clockMem = "" clockGpu = "" fanspeed = "" + gpuUsagePct = "" + memPct = "" for item in sp.split("\n"): try: key, val = item.split(":") @@ -61,6 +63,11 @@ class Module(core.module.Module): name = val elif key == "Fan Speed": fanspeed = val.split(" ")[0] + elif title == "Utilization": + if key == "Gpu": + gpuUsagePct = val.split(" ")[0] + elif key == "Memory": + memPct = val.split(" ")[0] except: title = item.strip() @@ -76,6 +83,8 @@ class Module(core.module.Module): clock_gpu=clockGpu, clock_mem=clockMem, fanspeed=fanspeed, + gpu_usage_pct=gpuUsagePct, + mem_usage_pct=memPct, ) From f98053371e6fd297346c6c74522a1eb913fd2732 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Soykan=20Ert=C3=BCrk?= Date: Tue, 10 Aug 2021 20:19:30 +0300 Subject: [PATCH 241/506] Dependency for powerline themes --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 848434e..4727ebc 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,8 @@ There is also a SlackBuild available here: [slackbuilds:bumblebee-status](http:/ [Available modules](https://bumblebee-status.readthedocs.io/en/main/modules.html) lists the dependencies (Python modules and external executables) for each module. If you are not using a module, you don't need the dependencies. +Some themes (e.g. all ‘powerline’ themes) require Font Awesome http://fontawesome.io/ and a powerline-compatible font (powerline-fonts) https://github.com/powerline/fonts + # Usage ## Normal usage In your i3wm configuration, modify the *status_command* for your i3bar like this: From 473d2fbd1403bb475587a058ac9827202c7f4d8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Soykan=20Ert=C3=BCrk?= Date: Thu, 12 Aug 2021 22:54:34 +0300 Subject: [PATCH 242/506] Improving docs I added tkinter as dependency in requirements. --- bumblebee_status/modules/contrib/system.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bumblebee_status/modules/contrib/system.py b/bumblebee_status/modules/contrib/system.py index e96ee3f..cc46ef8 100644 --- a/bumblebee_status/modules/contrib/system.py +++ b/bumblebee_status/modules/contrib/system.py @@ -21,6 +21,9 @@ Parameters: * system.suspend: specify a command for suspending (defaults to 'i3exit suspend') * system.hibernate: specify a command for hibernating (defaults to 'i3exit hibernate') +Requirements: + tkinter (python3-tk package on debian based systems either you can install it as python package) + contributed by `bbernhard `_ - many thanks! """ From 8be9f1a05ca411a14e192df9e08313ea9eccaa5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Soykan=20Ert=C3=BCrk?= Date: Thu, 12 Aug 2021 23:15:09 +0300 Subject: [PATCH 243/506] system module requirement added tkinter as requirement --- docs/modules.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/modules.rst b/docs/modules.rst index 49e509b..5b9b075 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -1380,6 +1380,9 @@ Parameters: * system.lock: specify a command for locking the screen (defaults to 'i3exit lock') * system.suspend: specify a command for suspending (defaults to 'i3exit suspend') * system.hibernate: specify a command for hibernating (defaults to 'i3exit hibernate') + +Requirements: + tkinter (python3-tk package on debian based systems either you can install it as python package) contributed by `bbernhard `_ - many thanks! From 5a1addec7f26e0eac294842a91cc4125f9520e97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Soykan=20Ert=C3=BCrk?= Date: Sat, 14 Aug 2021 20:14:54 +0300 Subject: [PATCH 244/506] Fixing a small bug on todo module todo counts new lines (blank lines) as todo and increments todo count. After my fix todo doesn't counts blank lines. --- bumblebee_status/modules/contrib/todo.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/bumblebee_status/modules/contrib/todo.py b/bumblebee_status/modules/contrib/todo.py index df2d788..6c69f10 100644 --- a/bumblebee_status/modules/contrib/todo.py +++ b/bumblebee_status/modules/contrib/todo.py @@ -25,7 +25,7 @@ class Module(core.module.Module): self.__todos = self.count_items() core.input.register( self, button=core.input.LEFT_MOUSE, cmd="{} {}".format(self.__editor, self.__doc) - ) + ) def output(self, widget): return str(self.__todos) @@ -40,11 +40,12 @@ class Module(core.module.Module): def count_items(self): try: - i = -1 + i = 0 with open(self.__doc) as f: - for i, l in enumerate(f): - pass - return i + 1 + for l in f.readlines(): + if l.strip() != '': + i += 1 + return i except Exception: return 0 From 05f76c0d9a236426f6e99a48c6b685f375395280 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Soykan=20Ert=C3=BCrk?= Date: Sat, 14 Aug 2021 20:16:45 +0300 Subject: [PATCH 245/506] Update todo.py --- bumblebee_status/modules/contrib/todo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bumblebee_status/modules/contrib/todo.py b/bumblebee_status/modules/contrib/todo.py index 6c69f10..76e289e 100644 --- a/bumblebee_status/modules/contrib/todo.py +++ b/bumblebee_status/modules/contrib/todo.py @@ -25,7 +25,7 @@ class Module(core.module.Module): self.__todos = self.count_items() core.input.register( self, button=core.input.LEFT_MOUSE, cmd="{} {}".format(self.__editor, self.__doc) - ) + ) def output(self, widget): return str(self.__todos) From d4339f6e43359cf536a897f33353840a39e9312b Mon Sep 17 00:00:00 2001 From: Tom Saleeba Date: Sun, 15 Aug 2021 22:27:44 -0600 Subject: [PATCH 246/506] fix: correct mem usage to be mem *io* usage --- bumblebee_status/modules/contrib/nvidiagpu.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/bumblebee_status/modules/contrib/nvidiagpu.py b/bumblebee_status/modules/contrib/nvidiagpu.py index 2731314..3b929a2 100644 --- a/bumblebee_status/modules/contrib/nvidiagpu.py +++ b/bumblebee_status/modules/contrib/nvidiagpu.py @@ -4,11 +4,15 @@ Parameters: * nvidiagpu.format: Format string (defaults to '{name}: {temp}°C %{usedmem}/{totalmem} MiB') - Available values are: {name} {temp} {mem_used} {mem_total} {fanspeed} {clock_gpu} {clock_mem} {gpu_usage_pct} {mem_usage_pct} + Available values are: {name} {temp} {mem_used} {mem_total} {fanspeed} {clock_gpu} {clock_mem} {gpu_usage_pct} {mem_usage_pct} {mem_io_pct} Requires nvidia-smi contributed by `RileyRedpath `_ - many thanks! + +Note: mem_io_pct is (from `man nvidia-smi`): +> Percent of time over the past sample period during which global (device) +> memory was being read or written. """ import core.module @@ -42,7 +46,8 @@ class Module(core.module.Module): clockGpu = "" fanspeed = "" gpuUsagePct = "" - memPct = "" + memIoPct = "" + memUsage = "not found" for item in sp.split("\n"): try: key, val = item.split(":") @@ -67,11 +72,14 @@ class Module(core.module.Module): if key == "Gpu": gpuUsagePct = val.split(" ")[0] elif key == "Memory": - memPct = val.split(" ")[0] + memIoPct = val.split(" ")[0] except: title = item.strip() + if totalMem and usedMem: + memUsage = int(int(usedMem) / int(totalMem) * 100) + str_format = self.parameter( "format", "{name}: {temp}°C {mem_used}/{mem_total} MiB" ) @@ -84,7 +92,8 @@ class Module(core.module.Module): clock_mem=clockMem, fanspeed=fanspeed, gpu_usage_pct=gpuUsagePct, - mem_usage_pct=memPct, + mem_io_pct=memIoPct, + mem_usage_pct=memUsage, ) From ed5a4e61e482d8e20ea0319cdb6c58f3d78083eb Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Fri, 10 Sep 2021 12:45:11 +0200 Subject: [PATCH 247/506] [modules/bluetooth] Add more error checking Do not kill the bar when the dbus-send command fails. see #818 --- bumblebee_status/modules/contrib/bluetooth.py | 2 +- bumblebee_status/modules/contrib/bluetooth2.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bumblebee_status/modules/contrib/bluetooth.py b/bumblebee_status/modules/contrib/bluetooth.py index b565494..481ae88 100644 --- a/bumblebee_status/modules/contrib/bluetooth.py +++ b/bumblebee_status/modules/contrib/bluetooth.py @@ -106,7 +106,7 @@ class Module(core.module.Module): ) logging.debug("bt: toggling bluetooth") - util.cli.execute(cmd) + util.cli.execute(cmd, ignore_errors=True) def state(self, widget): """Get current state.""" diff --git a/bumblebee_status/modules/contrib/bluetooth2.py b/bumblebee_status/modules/contrib/bluetooth2.py index 52474b9..22eae88 100644 --- a/bumblebee_status/modules/contrib/bluetooth2.py +++ b/bumblebee_status/modules/contrib/bluetooth2.py @@ -69,7 +69,7 @@ class Module(core.module.Module): ) logging.debug("bt: toggling bluetooth") - util.cli.execute(cmd) + util.cli.execute(cmd, ignore_errors=True) def state(self, widget): """Get current state.""" From c96d119b0e3b2550c709bc789f495c660599403c Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Thu, 7 Oct 2021 15:39:29 +0200 Subject: [PATCH 248/506] [core/config] add autohide to configuration file make it possible to configure a list of automatically hidden modules in the configuration file (+ add documentation for it). fixes #822 --- bumblebee_status/core/config.py | 2 +- docs/features.rst | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/bumblebee_status/core/config.py b/bumblebee_status/core/config.py index b5fd5ef..69a4ac6 100644 --- a/bumblebee_status/core/config.py +++ b/bumblebee_status/core/config.py @@ -328,7 +328,7 @@ class Config(util.store.Store): """ def autohide(self, name): - return name in self.__args.autohide + return name in self.__args.autohide or self.get("autohide") """Returns which modules should be hidden if they are in state error diff --git a/docs/features.rst b/docs/features.rst index f167033..3c7fe6e 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -160,6 +160,7 @@ Configuration files have the following format: [core] modules = + autohid = theme = [module-parameters] From b5395fe764c8b580475459e073a480f27525bb19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thayn=C3=A3=20Moretti?= Date: Mon, 11 Oct 2021 19:07:28 -0300 Subject: [PATCH 249/506] chore: public toggle method --- bumblebee_status/modules/contrib/dunstctl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bumblebee_status/modules/contrib/dunstctl.py b/bumblebee_status/modules/contrib/dunstctl.py index 3c803a4..f082f1b 100644 --- a/bumblebee_status/modules/contrib/dunstctl.py +++ b/bumblebee_status/modules/contrib/dunstctl.py @@ -24,12 +24,12 @@ import util.cli class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget("")) - core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.__toggle_state) + core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.toggle_state) self.__states = {"unknown": ["unknown", "critical"], "true": ["muted", "warning"], "false": ["unmuted"]} - def __toggle_state(self, event): + def toggle_state(self, event): util.cli.execute("dunstctl set-paused toggle", ignore_errors=True) def state(self, widget): From 40de07ba2ed659abaa417ab7d76dff54c423b6e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thayn=C3=A3=20Moretti?= Date: Mon, 11 Oct 2021 19:08:46 -0300 Subject: [PATCH 250/506] chore: create dunst tests --- tests/modules/contrib/test_dunst.py | 53 +++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/tests/modules/contrib/test_dunst.py b/tests/modules/contrib/test_dunst.py index 2ca2d40..75e3151 100644 --- a/tests/modules/contrib/test_dunst.py +++ b/tests/modules/contrib/test_dunst.py @@ -1,5 +1,58 @@ import pytest +import core.config +import modules.contrib.dunst + + +def build_module(): + return modules.contrib.dunst.Module( + config=core.config.Config([]), + theme=None + ) + + def test_load_module(): __import__("modules.contrib.dunst") +def test_input_registration(mocker): + input_register = mocker.patch('core.input.register') + + module = build_module() + + input_register.assert_called_with( + module, + button=core.input.LEFT_MOUSE, + cmd=module.toggle_status + ) + +def test_dunst_toggle(mocker): + start_command = mocker.patch('util.cli.execute') + + module = build_module() + start_command.assert_called_with('killall -s SIGUSR2 dunst', ignore_errors=True) + + toggle_command = mocker.patch('util.cli.execute') + module.toggle_status(None) + toggle_command.assert_called_with('killall -s SIGUSR1 dunst') + + widget = module.widget() + actual = module.state(widget) + assert actual == ['muted', 'warning'] + + module.toggle_status(None) + toggle_command.assert_called_with('killall -s SIGUSR2 dunst') + + widget = module.widget() + actual = module.state(widget) + assert actual == ['unmuted'] + +def test_dunst_toggle_exception(mocker): + module = build_module() + + toggle_command = mocker.patch('util.cli.execute', side_effect=Exception) + module.toggle_status(None) + toggle_command.assert_called_with('killall -s SIGUSR1 dunst') + + widget = module.widget() + actual = module.state(widget) + assert actual == ['unmuted'] From 15809514740dcf0848b15e0328ff12f7d02420d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thayn=C3=A3=20Moretti?= Date: Mon, 11 Oct 2021 19:09:15 -0300 Subject: [PATCH 251/506] chore: create missing dunstctl tests --- tests/modules/contrib/test_dunstctl.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/modules/contrib/test_dunstctl.py b/tests/modules/contrib/test_dunstctl.py index db77fe3..2391f7e 100644 --- a/tests/modules/contrib/test_dunstctl.py +++ b/tests/modules/contrib/test_dunstctl.py @@ -1,6 +1,5 @@ import pytest -import util.cli import core.config import modules.contrib.dunstctl @@ -13,6 +12,25 @@ def build_module(): def test_load_module(): __import__("modules.contrib.dunstctl") +def test_input_registration(mocker): + input_register = mocker.patch('core.input.register') + + module = build_module() + + input_register.assert_called_with( + module, + button=core.input.LEFT_MOUSE, + cmd=module.toggle_state + ) + +def test_dunst_toggle_state(mocker): + command = mocker.patch('util.cli.execute') + + module = build_module() + + module.toggle_state(None) + command.assert_called_with('dunstctl set-paused toggle', ignore_errors=True) + def test_dunst_running(mocker): command = mocker.patch('util.cli.execute', return_value=(0, "false")) From 4007517e45a589e10a6111d5066f0a96694e2d6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thayn=C3=A3=20Moretti?= Date: Tue, 12 Oct 2021 11:24:43 -0300 Subject: [PATCH 252/506] chore: create libvirtvms tests --- tests/modules/contrib/test_libvirtvms.py | 43 +++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/tests/modules/contrib/test_libvirtvms.py b/tests/modules/contrib/test_libvirtvms.py index efa5880..48ba72c 100644 --- a/tests/modules/contrib/test_libvirtvms.py +++ b/tests/modules/contrib/test_libvirtvms.py @@ -1,7 +1,48 @@ +import sys import pytest +from unittest.mock import Mock -pytest.importorskip("libvirt") +import core.config + +sys.modules['libvirt'] = Mock() + +import modules.contrib.libvirtvms + +def build_module(): + return modules.contrib.libvirtvms.Module( + config=core.config.Config([]), + theme=None + ) def test_load_module(): __import__("modules.contrib.libvirtvms") +def test_input_registration(mocker): + input_register = mocker.patch('core.input.register') + + module = build_module() + + input_register.assert_called_with( + module, + button=core.input.LEFT_MOUSE, + cmd="virt-manager" + ) + +def test_status_failed(mocker): + mocker.patch('libvirt.openReadOnly', return_value=None) + + module = build_module() + status = module.status(None) + + assert status == "Failed to open connection to the hypervisor" + +def test_status(mocker): + virtMock = mocker.Mock() + virtMock.numOfDomains = mocker.Mock(return_value=10) + + mocker.patch('libvirt.openReadOnly', return_value=virtMock) + + module = build_module() + status = module.status(None) + + assert status == "VMs 10" From 0dc6a95ac284f105b534a546b5e244b161c84694 Mon Sep 17 00:00:00 2001 From: alexcoder04 Date: Thu, 21 Oct 2021 13:45:40 +0200 Subject: [PATCH 253/506] [modules/sensors] auto-determine the correct thermal zone --- bumblebee_status/modules/contrib/sensors.py | 69 +++++++++++++-------- docs/modules.rst | 2 + 2 files changed, 44 insertions(+), 27 deletions(-) diff --git a/bumblebee_status/modules/contrib/sensors.py b/bumblebee_status/modules/contrib/sensors.py index 8164f00..d023d6a 100644 --- a/bumblebee_status/modules/contrib/sensors.py +++ b/bumblebee_status/modules/contrib/sensors.py @@ -18,6 +18,7 @@ contributed by `mijoharas `_ - many thanks! """ import re +import os import json import logging @@ -49,19 +50,25 @@ class Module(core.module.Module): self.determine_method() def determine_method(self): + if self.parameter("use_sensors") == "True": + self.use_sensors = True + return + if self.parameter("use_sensors") == "False": + self.use_sensors = False + return if self.parameter("path") != None and self._json == False: - self.use_sensors = False # use thermal zone - else: - # try to use output of sensors -u - try: - output = util.cli.execute("sensors -u") - self.use_sensors = True - log.debug("Sensors command available") - except FileNotFoundError as e: - log.info( - "Sensors command not available, using /sys/class/thermal/thermal_zone*/" - ) - self.use_sensors = False + self.use_sensors = False # use thermal zone + return + # try to use output of sensors -u + try: + output = util.cli.execute("sensors -u") + self.use_sensors = True + log.debug("Sensors command available") + except FileNotFoundError as e: + log.info( + "Sensors command not available, using /sys/class/thermal/thermal_zone*/" + ) + self.use_sensors = False def _get_temp_from_sensors(self): if self._json == True: @@ -92,22 +99,30 @@ class Module(core.module.Module): def get_temp(self): if self.use_sensors: - temperature = self._get_temp_from_sensors() log.debug("Retrieve temperature from sensors -u") - else: - try: - temperature = open( - self.parameter("path", "/sys/class/thermal/thermal_zone0/temp") - ).read().strip() - log.debug("retrieved temperature from /sys/class/") - # TODO: Iterate through all thermal zones to determine the correct one and use its value - # https://unix.stackexchange.com/questions/304845/discrepancy-between-number-of-cores-and-thermal-zones-in-sys-class-thermal - - except IOError: - temperature = "unknown" - log.info("Can not determine temperature, please install lm-sensors") - - return temperature + return self._get_temp_from_sensors() + try: + path = None + # use path provided by the user + if self.parameter("path") is not None: + path = self.parameter("path") + # find the thermal zone that provides cpu temperature + for zone in os.listdir("/sys/class/thermal"): + if not zone.startswith("thermal_zone"): + continue + if open(f"/sys/class/thermal/{zone}/type").read().strip() != "x86_pkg_temp": + continue + path = f"/sys/class/thermal/{zone}/temp" + # use zone 0 as fallback + if path is None: + log.info("Can not determine temperature path, using thermal_zone0") + path = "/sys/class/thermal/thermal_zone0/temp" + log.debug(f"retrieving temperature from {path}") + # the values are t°C * 1000, so divide by 1000 + return str(int(open(path).read()) / 1000) + except IOError: + log.info("Can not determine temperature, please install lm-sensors") + return "unknown" def get_mhz(self): mhz = None diff --git a/docs/modules.rst b/docs/modules.rst index 5b9b075..ded47dc 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -1195,6 +1195,8 @@ sensors Displays sensor temperature Parameters: + * sensors.use_sensors (True/False): whether to use the 'sensors' command. + If set to 'False', the sysfs-interface at '/sys/class/thermal' is used * sensors.path: path to temperature file (default /sys/class/thermal/thermal_zone0/temp). * sensors.json: if set to 'true', interpret sensors.path as JSON 'path' in the output of 'sensors -j' (i.e. //.../), for example, path could From 6b31cdb698e4086dd07c769d11ebbec8f6c84425 Mon Sep 17 00:00:00 2001 From: alexcoder04 Date: Thu, 21 Oct 2021 14:43:15 +0200 Subject: [PATCH 254/506] [modules/sensors] use util.format.asbool() + auto-check only if no path is specified --- bumblebee_status/modules/contrib/sensors.py | 35 ++++++++++----------- docs/modules.rst | 5 +-- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/bumblebee_status/modules/contrib/sensors.py b/bumblebee_status/modules/contrib/sensors.py index d023d6a..7d42c83 100644 --- a/bumblebee_status/modules/contrib/sensors.py +++ b/bumblebee_status/modules/contrib/sensors.py @@ -4,6 +4,7 @@ """Displays sensor temperature Parameters: + * sensors.use_sensors: whether to use the sensors command * sensors.path: path to temperature file (default /sys/class/thermal/thermal_zone0/temp). * sensors.json: if set to 'true', interpret sensors.path as JSON 'path' in the output of 'sensors -j' (i.e. //.../), for example, path could @@ -47,28 +48,25 @@ class Module(core.module.Module): self._json = util.format.asbool(self.parameter("json", False)) self._freq = util.format.asbool(self.parameter("show_freq", True)) core.input.register(self, button=core.input.LEFT_MOUSE, cmd="xsensors") - self.determine_method() + self.use_sensors = self.determine_method() def determine_method(self): - if self.parameter("use_sensors") == "True": - self.use_sensors = True - return - if self.parameter("use_sensors") == "False": - self.use_sensors = False - return + if util.format.asbool(self.parameter("use_sensors")) == True: + return True + if util.format.asbool(self.parameter("use_sensors")) == False: + return False if self.parameter("path") != None and self._json == False: - self.use_sensors = False # use thermal zone - return + return False # try to use output of sensors -u try: - output = util.cli.execute("sensors -u") - self.use_sensors = True + _ = util.cli.execute("sensors -u") log.debug("Sensors command available") + return True except FileNotFoundError as e: log.info( "Sensors command not available, using /sys/class/thermal/thermal_zone*/" ) - self.use_sensors = False + return False def _get_temp_from_sensors(self): if self._json == True: @@ -107,12 +105,13 @@ class Module(core.module.Module): if self.parameter("path") is not None: path = self.parameter("path") # find the thermal zone that provides cpu temperature - for zone in os.listdir("/sys/class/thermal"): - if not zone.startswith("thermal_zone"): - continue - if open(f"/sys/class/thermal/{zone}/type").read().strip() != "x86_pkg_temp": - continue - path = f"/sys/class/thermal/{zone}/temp" + else: + for zone in os.listdir("/sys/class/thermal"): + if not zone.startswith("thermal_zone"): + continue + if open(f"/sys/class/thermal/{zone}/type").read().strip() != "x86_pkg_temp": + continue + path = f"/sys/class/thermal/{zone}/temp" # use zone 0 as fallback if path is None: log.info("Can not determine temperature path, using thermal_zone0") diff --git a/docs/modules.rst b/docs/modules.rst index ded47dc..442d461 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -1195,8 +1195,9 @@ sensors Displays sensor temperature Parameters: - * sensors.use_sensors (True/False): whether to use the 'sensors' command. - If set to 'False', the sysfs-interface at '/sys/class/thermal' is used + * sensors.use_sensors: whether to use the 'sensors' command. + If set to 'false', the sysfs-interface at '/sys/class/thermal' is used. + If not set, 'sensors' will be used if available. * sensors.path: path to temperature file (default /sys/class/thermal/thermal_zone0/temp). * sensors.json: if set to 'true', interpret sensors.path as JSON 'path' in the output of 'sensors -j' (i.e. //.../), for example, path could From 876774ce406f2fe55a49667c73e2ad8a7628a0e2 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sat, 23 Oct 2021 13:43:08 +0200 Subject: [PATCH 255/506] [travis] removing python 3.4 and 3.5 Since 3.4 and 3.5 are both not supported anymore, do not do unit tests for them. --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 369aeb7..c59ad53 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,8 +4,6 @@ env: global: - CC_TEST_REPORTER_ID=40cb00907f7a10e04868e856570bb997ab9c42fd3b63d980f2b2269433195fdf python: - - "3.4" - - "3.5" - "3.6" - "3.7" - "3.8" From 99bb5e99aa052788f9a382a21d465ffe676445e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thayn=C3=A3=20Moretti?= Date: Sat, 23 Oct 2021 10:12:49 -0300 Subject: [PATCH 256/506] Fix Travis CI build URL --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4727ebc..4511a7d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # bumblebee-status -[![Build Status](https://travis-ci.com/tobi-wan-kenobi/bumblebee-status.svg?branch=main)](https://travis-ci.com/tobi-wan-kenobi/bumblebee-status) +[![Build Status](https://app.travis-ci.com/tobi-wan-kenobi/bumblebee-status.svg?branch=main)](https://app.travis-ci.com/tobi-wan-kenobi/bumblebee-status) [![Documentation Status](https://readthedocs.org/projects/bumblebee-status/badge/?version=main)](https://bumblebee-status.readthedocs.io/en/main/?badge=main) ![AUR version (release)](https://img.shields.io/aur/version/bumblebee-status) ![AUR version (git)](https://img.shields.io/aur/version/bumblebee-status-git) From 7542a47dbc97e92b1c19e12d6bf588ef59fd78b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thayn=C3=A3=20Moretti?= Date: Sat, 23 Oct 2021 11:02:46 -0300 Subject: [PATCH 257/506] fix: generate coverage xml before cc report --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index c59ad53..b05ac86 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,4 +26,5 @@ install: script: - coverage run --source=. -m pytest tests -v after_script: + - coverage xml - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT From d8216a5e2c05d9480faae0334216f22111bd3ae8 Mon Sep 17 00:00:00 2001 From: Kushagra Jain Date: Tue, 26 Oct 2021 13:54:35 +0530 Subject: [PATCH 258/506] added new theme --- themes/nord-colorful.json | 59 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 themes/nord-colorful.json diff --git a/themes/nord-colorful.json b/themes/nord-colorful.json new file mode 100644 index 0000000..acd334b --- /dev/null +++ b/themes/nord-colorful.json @@ -0,0 +1,59 @@ +{ + "icons": [ "awesome-fonts" ], + "colors": [{ + "red": "#BF616A", + "orange": "#D08770", + "yellow": "#EBCB8B", + "green": "#A3BE8C" + }], + "defaults": { + "separator-block-width": 0, + "warning": { + "fg": "#2E3440", + "bg": "#D08770" + }, + "critical": { + "fg": "#2E3440", + "bg": "#BF616A" + } + }, + "cycle": [ + { "fg": "#000000", "bg": "#8FBCBB"}, + { "fg": "#000000", "bg": "#A3BE8C"}, + { "fg": "#000000", "bg": "#EBCB8B"}, + { "fg": "#000000", "bg": "#BF616A"}, + { "fg": "#000000", "bg": "#B48EAD"} + ], + "dnf": { + "good": { + "fg": "#A3BE8C", + "bg": "#2E3440" + } + }, + "apt": { + "good": { + "fg": "#A3BE8C", + "bg": "#2E3440" + } + }, + "pacman": { + "good": { + "fg": "#A3BE8C", + "bg": "#2E3440" + } + }, + "pomodoro": { + "paused": { + "fg": "#2E3440", + "bg": "#D08770" + }, + "work": { + "fg": "#2E3440", + "bg": "#EBCB8B" + }, + "break": { + "fg": "#A3BE8C", + "bg": "#2E3440" + } + } +} From fdc9b7896715d3bfd918f9ec0a264fbd35c52af5 Mon Sep 17 00:00:00 2001 From: Jan Fader Date: Tue, 26 Oct 2021 19:27:02 +0200 Subject: [PATCH 259/506] add new solaar.py for logitech's unifying devices --- bumblebee_status/modules/contrib/solaar.py | 58 ++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 bumblebee_status/modules/contrib/solaar.py diff --git a/bumblebee_status/modules/contrib/solaar.py b/bumblebee_status/modules/contrib/solaar.py new file mode 100644 index 0000000..a718fb2 --- /dev/null +++ b/bumblebee_status/modules/contrib/solaar.py @@ -0,0 +1,58 @@ +"""Shows status and load percentage of logitech's unifying device + +Requires the following executable: + * solaar (from community) + +contributed by `cambid `_ - many thanks! +""" + +import logging + +import core.module +import core.widget +import core.decorators + +import util.cli + + +class Module(core.module.Module): + @core.decorators.every(seconds=30) + def __init__(self, config, theme): + super().__init__(config, theme, core.widget.Widget(self.utilization)) + self.__battery = self.parameter("device", "") + self.background = True + self.__battery_status = "" + self.__error = False + + @property + def __format(self): + return self.parameter("format", "{}") + + def utilization(self, widget): + return self.__format.format(self.__battery_status) + + def update(self): + self.__error = False + if self.__battery != "": + cmd = f"solaar show '{self.__battery}'" + else: + cmd = "solaar show" + code, result = util.cli.execute( + cmd, ignore_errors=True, return_exitcode=True + ) + + if code == 0: + for line in result.split('\n'): + if line.count('Battery') > 0: + self.__battery_status = line.split(':')[1].strip() + else: + self.__error = True + logging.error(f"solaar exited with {code}: {result}") + + def state(self, widget): + if self.__error: + return "warning" + return "okay" + + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From dced20bf892dbe68565ebc59ae0c41b2131de203 Mon Sep 17 00:00:00 2001 From: Jan Fader Date: Tue, 26 Oct 2021 19:52:42 +0200 Subject: [PATCH 260/506] refactor code to decrease cognitive complexity in update --- bumblebee_status/modules/contrib/solaar.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bumblebee_status/modules/contrib/solaar.py b/bumblebee_status/modules/contrib/solaar.py index a718fb2..b7396f3 100644 --- a/bumblebee_status/modules/contrib/solaar.py +++ b/bumblebee_status/modules/contrib/solaar.py @@ -23,6 +23,10 @@ class Module(core.module.Module): self.background = True self.__battery_status = "" self.__error = False + if self.__battery != "": + self.__cmd = f"solaar show '{self.__battery}'" + else: + self.__cmd = "solaar show" @property def __format(self): @@ -33,12 +37,8 @@ class Module(core.module.Module): def update(self): self.__error = False - if self.__battery != "": - cmd = f"solaar show '{self.__battery}'" - else: - cmd = "solaar show" code, result = util.cli.execute( - cmd, ignore_errors=True, return_exitcode=True + self.__cmd, ignore_errors=True, return_exitcode=True ) if code == 0: From 2cb72fcc3027fc74a4d9df2d76b1f0cf1a55be57 Mon Sep 17 00:00:00 2001 From: Jan Fader Date: Tue, 26 Oct 2021 21:52:15 +0200 Subject: [PATCH 261/506] add tests for solaar.py --- tests/modules/contrib/test_solaar.py | 32 ++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 tests/modules/contrib/test_solaar.py diff --git a/tests/modules/contrib/test_solaar.py b/tests/modules/contrib/test_solaar.py new file mode 100644 index 0000000..9c5fe33 --- /dev/null +++ b/tests/modules/contrib/test_solaar.py @@ -0,0 +1,32 @@ +import pytest + +import util.cli +import core.config +import modules.contrib.solaar + + +@pytest.fixture +def module(): + module = modules.contrib.solaar.Module( + config=core.config.Config([]), + theme=None + ) + + yield module + + +def test_load_module(): + __import__("modules.contrib.solaar") + + +def test_with_unknown_code(module, mocker): + mocker.patch('util.cli.execute', return_value=(99, 'error')) + logger = mocker.patch('logging.error') + + module.update() + + logger.assert_called_with('solaar exited with {}: {}'.format(99, 'error')) + + widget = module.widget() + assert module.state(widget) == 'warning' + assert module.hidden() == False From c7f58ae2a4412eb4cd8c017fca7b544ff87a3a11 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Fri, 5 Nov 2021 08:43:33 +0100 Subject: [PATCH 262/506] [doc] fix typo (autohid vs. autohide) see #835 --- docs/features.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features.rst b/docs/features.rst index 3c7fe6e..603c7fe 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -160,7 +160,7 @@ Configuration files have the following format: [core] modules = - autohid = + autohide = theme = [module-parameters] From f0ab3ef03a9f993adb0ff48479b405d9dcfe9832 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Fri, 5 Nov 2021 13:57:05 +0100 Subject: [PATCH 263/506] [core/config] fix autohide parameter from configuration file Need to parse the config parameter "autohide" into a list and actually check whether the current module is in the list. see #835 --- bumblebee_status/core/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bumblebee_status/core/config.py b/bumblebee_status/core/config.py index 69a4ac6..f191673 100644 --- a/bumblebee_status/core/config.py +++ b/bumblebee_status/core/config.py @@ -328,7 +328,7 @@ class Config(util.store.Store): """ def autohide(self, name): - return name in self.__args.autohide or self.get("autohide") + return name in self.__args.autohide or name in util.format.aslist(self.get("autohide", [])) """Returns which modules should be hidden if they are in state error From cbd989309d39db1c35c56b3c7884dc3c42a3f679 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Fri, 5 Nov 2021 14:00:34 +0100 Subject: [PATCH 264/506] [contrib/progress] allow hiding of inactive state Add a new "hide-able" state "mayhide" that can be utilized by modules without warning state. This state indicates that the module *may* be hidden by autohide, if the user configures it like this. see #835 --- bumblebee_status/core/output.py | 2 +- bumblebee_status/modules/contrib/progress.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bumblebee_status/core/output.py b/bumblebee_status/core/output.py index 121df27..cf79f2f 100644 --- a/bumblebee_status/core/output.py +++ b/bumblebee_status/core/output.py @@ -229,7 +229,7 @@ class i3(object): for widget in module.widgets(): if widget.module and self.__config.autohide(widget.module.name): if not any( - state in widget.state() for state in ["warning", "critical"] + state in widget.state() for state in ["warning", "critical", "mayhide"] ): continue if module.hidden(): diff --git a/bumblebee_status/modules/contrib/progress.py b/bumblebee_status/modules/contrib/progress.py index a1938d2..e818197 100644 --- a/bumblebee_status/modules/contrib/progress.py +++ b/bumblebee_status/modules/contrib/progress.py @@ -102,7 +102,7 @@ class Module(core.module.Module): def state(self, widget): if self.__active: return "copying" - return "pending" + return ["pending", "mayhide"] # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From 74ecbb6ca8e3202cafc69bfbf332081a5eec85cb Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Fri, 5 Nov 2021 19:11:45 +0100 Subject: [PATCH 265/506] [core/output] fix logic error when using "autohide" - when state is critical or warning -> *show* the module - when state is mayhide -> *hide* the module see #835 --- bumblebee_status/core/output.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bumblebee_status/core/output.py b/bumblebee_status/core/output.py index cf79f2f..47283eb 100644 --- a/bumblebee_status/core/output.py +++ b/bumblebee_status/core/output.py @@ -229,9 +229,11 @@ class i3(object): for widget in module.widgets(): if widget.module and self.__config.autohide(widget.module.name): if not any( - state in widget.state() for state in ["warning", "critical", "mayhide"] + state in widget.state() for state in ["warning", "critical"] ): continue + if "mayhide" in widget.state(): + continue if module.hidden(): continue if widget.hidden: From 6a3e4761bf7d09e786dd3e0cf43e1f826db47278 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sat, 6 Nov 2021 08:17:11 +0100 Subject: [PATCH 266/506] Revert "[core/output] fix logic error when using "autohide"" This reverts commit 74ecbb6ca8e3202cafc69bfbf332081a5eec85cb. --- bumblebee_status/core/output.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bumblebee_status/core/output.py b/bumblebee_status/core/output.py index 47283eb..cf79f2f 100644 --- a/bumblebee_status/core/output.py +++ b/bumblebee_status/core/output.py @@ -229,11 +229,9 @@ class i3(object): for widget in module.widgets(): if widget.module and self.__config.autohide(widget.module.name): if not any( - state in widget.state() for state in ["warning", "critical"] + state in widget.state() for state in ["warning", "critical", "mayhide"] ): continue - if "mayhide" in widget.state(): - continue if module.hidden(): continue if widget.hidden: From 5ad211f86206181ee8b6284d195185065b55916b Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sat, 6 Nov 2021 08:17:18 +0100 Subject: [PATCH 267/506] Revert "[contrib/progress] allow hiding of inactive state" This reverts commit cbd989309d39db1c35c56b3c7884dc3c42a3f679. --- bumblebee_status/core/output.py | 2 +- bumblebee_status/modules/contrib/progress.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bumblebee_status/core/output.py b/bumblebee_status/core/output.py index cf79f2f..121df27 100644 --- a/bumblebee_status/core/output.py +++ b/bumblebee_status/core/output.py @@ -229,7 +229,7 @@ class i3(object): for widget in module.widgets(): if widget.module and self.__config.autohide(widget.module.name): if not any( - state in widget.state() for state in ["warning", "critical", "mayhide"] + state in widget.state() for state in ["warning", "critical"] ): continue if module.hidden(): diff --git a/bumblebee_status/modules/contrib/progress.py b/bumblebee_status/modules/contrib/progress.py index e818197..a1938d2 100644 --- a/bumblebee_status/modules/contrib/progress.py +++ b/bumblebee_status/modules/contrib/progress.py @@ -102,7 +102,7 @@ class Module(core.module.Module): def state(self, widget): if self.__active: return "copying" - return ["pending", "mayhide"] + return "pending" # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From 26e4bdd7eb9fd8c8509693cd7d061e065c4f348f Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sat, 6 Nov 2021 08:21:08 +0100 Subject: [PATCH 268/506] [modules/progress] improved autohide functionality Simplify the previous autohide functionality by adding a flag that lets a module (e.g. progress) indicate that the current state should be "revealed" (not auto-hidden). This vastly simplifies the implementation. see #835 --- bumblebee_status/core/output.py | 2 +- bumblebee_status/modules/contrib/progress.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bumblebee_status/core/output.py b/bumblebee_status/core/output.py index 121df27..cee579f 100644 --- a/bumblebee_status/core/output.py +++ b/bumblebee_status/core/output.py @@ -229,7 +229,7 @@ class i3(object): for widget in module.widgets(): if widget.module and self.__config.autohide(widget.module.name): if not any( - state in widget.state() for state in ["warning", "critical"] + state in widget.state() for state in ["warning", "critical", "no-autohide"] ): continue if module.hidden(): diff --git a/bumblebee_status/modules/contrib/progress.py b/bumblebee_status/modules/contrib/progress.py index a1938d2..7e148b3 100644 --- a/bumblebee_status/modules/contrib/progress.py +++ b/bumblebee_status/modules/contrib/progress.py @@ -101,7 +101,7 @@ class Module(core.module.Module): def state(self, widget): if self.__active: - return "copying" + return ["copying", "no-autohide"] return "pending" From a84b4f9a65cceb92f274c1a3ba275105abc820a2 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sun, 7 Nov 2021 13:46:52 +0100 Subject: [PATCH 269/506] [docs] fix docs build pin docutils to < 0.18 as per https://github.com/readthedocs/readthedocs.org/issues/8616#issuecomment-952034858 --- .readthedocs.yaml | 6 ++++++ docs/requirements.txt | 1 + 2 files changed, 7 insertions(+) create mode 100644 .readthedocs.yaml create mode 100644 docs/requirements.txt diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..6823857 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,6 @@ +version: 2 + +python: + install: + - requirements: docs/requirements.txt + diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..93120e6 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1 @@ +docutils<0.18 From 6ce761695af9107c5ca3ca3e1679295545bc40ef Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sun, 7 Nov 2021 13:50:44 +0100 Subject: [PATCH 270/506] [doc] remove "master" branch --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 4511a7d..2f7bd86 100644 --- a/README.md +++ b/README.md @@ -36,9 +36,7 @@ Supported FontAwesome version: 4 (free version of 5 doesn't include some of the --- ***NOTE*** -The default branch for this project is `main` - I'm keeping `master` around for backwards compatibility (I do not want to break anybody's setup), but the default branch is now `main`! - -If you are curious why: [ZDNet:github-master-alternative](https://www.zdnet.com/article/github-to-replace-master-with-alternative-term-to-avoid-slavery-references/) +The default branch for this project is `main`. If you are curious why: [ZDNet:github-master-alternative](https://www.zdnet.com/article/github-to-replace-master-with-alternative-term-to-avoid-slavery-references/) --- From 973dd6117e273828c963697a9a56c95b132cb64f Mon Sep 17 00:00:00 2001 From: Yufan You Date: Fri, 17 Dec 2021 18:07:45 +0800 Subject: [PATCH 271/506] [contrib/playerctl]: don't log when no player is found `playerctl status` returns 1 when no player is found, which caused contrib/playerctl to log many times when there's no player. --- bumblebee_status/modules/contrib/playerctl.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bumblebee_status/modules/contrib/playerctl.py b/bumblebee_status/modules/contrib/playerctl.py index e02fa84..56af426 100755 --- a/bumblebee_status/modules/contrib/playerctl.py +++ b/bumblebee_status/modules/contrib/playerctl.py @@ -85,7 +85,9 @@ class Module(core.module.Module): def update(self): try: - playback_status = str(util.cli.execute(self.__cmd + "status")).strip() + playback_status = str(util.cli.execute(self.__cmd + "status 2>&1 || true", shell = True)).strip() + if playback_status == "No players found": + playback_status = None except Exception as e: logging.exception(e) playback_status = None From 8991bba90e6427e3fbc4da328303e7b09178362a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20L=C3=BCftinger?= Date: Tue, 28 Dec 2021 00:34:10 +0100 Subject: [PATCH 272/506] Silence exceptions in the spotify module which may write large amounts of logs to ~/.xsession-errors --- bumblebee_status/modules/contrib/spotify.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bumblebee_status/modules/contrib/spotify.py b/bumblebee_status/modules/contrib/spotify.py index 7544b48..1c7f7e2 100644 --- a/bumblebee_status/modules/contrib/spotify.py +++ b/bumblebee_status/modules/contrib/spotify.py @@ -159,7 +159,6 @@ class Module(core.module.Module): widget.full_text(self.__song) except Exception as e: - logging.exception(e) self.__song = "" @property From d430f904341360222f7ad51080aa7c3fe47044b2 Mon Sep 17 00:00:00 2001 From: Marco Genasci Date: Wed, 5 Jan 2022 09:34:27 +0100 Subject: [PATCH 273/506] Excluding the tests folder from the installation Signed-off-by: Marco Genasci --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2e5bafb..1f6c203 100755 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """Setup file for bumbleestatus bar to allow pip install of full package""" # -*- coding: utf8 - *- -from setuptools import setup +from setuptools import setup, find_packages import versioneer with open("requirements/base.txt") as f: @@ -57,4 +57,5 @@ setup( ("share/bumblebee-status/themes/icons", glob.glob("themes/icons/*.json")), ("share/bumblebee-status/utility", glob.glob("bin/*")), ], + packages=find_packages(exclude=["tests", "tests.*"]) ) From 51c3805f7f8cc1c064c183e1c5b122cae2b568f7 Mon Sep 17 00:00:00 2001 From: Marco Genasci Date: Wed, 5 Jan 2022 09:34:38 +0100 Subject: [PATCH 274/506] Change deprecated dash-separated with underscore in setup.cfg Signed-off-by: Marco Genasci --- setup.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 081fab6..d417aa6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,8 +31,8 @@ keywords = bumblebee-status [options] include_package_data = True -allow-all-external = yes -trusted-host = +allow_all_external = yes +trusted_host = gitlab.* bitbucket.org github.com From 8a50eb6f81ee31764950d70ecd47cb0ae17a784b Mon Sep 17 00:00:00 2001 From: Marco Genasci Date: Thu, 6 Jan 2022 08:06:57 +0100 Subject: [PATCH 275/506] New module emerge_status Display information about the currently running emerge process. Signed-off-by: Marco Genasci --- .../modules/contrib/emerge_status.py | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 bumblebee_status/modules/contrib/emerge_status.py diff --git a/bumblebee_status/modules/contrib/emerge_status.py b/bumblebee_status/modules/contrib/emerge_status.py new file mode 100644 index 0000000..3758585 --- /dev/null +++ b/bumblebee_status/modules/contrib/emerge_status.py @@ -0,0 +1,113 @@ +"""Display information about the currently running emerge process. + +Requires the following executable: + * emerge + +Parameters: + * emerge_status.format: Format string (defaults to '{current}/{total} {action} {category}/{pkg}') + +This code is based on emerge_status module from p3status [1] original created by AnwariasEu. + +[1] https://github.com/ultrabug/py3status/blob/master/py3status/modules/emerge_status.py +""" + +import re +import copy + +import core.module +import core.widget +import core.decorators + +import util.cli +import util.format + + +class Module(core.module.Module): + @core.decorators.every(seconds=10) + def __init__(self, config, theme): + super().__init__(config, theme, []) + self.__format = self.parameter( + "format", "{current}/{total} {action} {category}/{pkg}" + ) + self.__ret_default = { + "action": "", + "category": "", + "current": 0, + "pkg": "", + "total": 0, + } + + def update(self): + response = {} + ret = copy.deepcopy(self.__ret_default) + if self.__emerge_running(): + ret = self.__get_progress() + + widget = self.widget("status") + if not widget: + widget = self.add_widget(name="status") + + if ret["total"] == 0: + widget.full_text("emrg calculating...") + else: + widget.full_text( + " ".join( + self.__format.format( + current=ret["current"], + total=ret["total"], + action=ret["action"], + category=ret["category"], + pkg=ret["pkg"], + ).split() + ) + ) + else: + self.clear_widgets() + + def __emerge_running(self): + """ + Check if emerge is running. + Returns true if at least one instance of emerge is running. + """ + try: + util.cli.execute("pgrep emerge") + return True + except Exception: + return False + + def __get_progress(self): + """ + Get current progress of emerge. + Returns a dict containing current and total value. + """ + input_data = [] + ret = {} + + # traverse emerge.log from bottom up to get latest information + last_lines = util.cli.execute("tail -50 /var/log/emerge.log") + input_data = last_lines.split("\n") + input_data.reverse() + + for line in input_data: + if "*** terminating." in line: + # copy content of ret_default, not only the references + ret = copy.deepcopy(self.__ret_default) + break + else: + status_re = re.compile( + r"\((?P[\d]+) of (?P[\d]+)\) " + r"(?P[a-zA-Z/]+( [a-zA-Z]+)?) " + r"\((?P[\w\-]+)/(?P

[\w.]+)" + ) + res = status_re.search(line) + if res is not None: + ret["action"] = res.group("a").lower() + ret["category"] = res.group("ca") + ret["current"] = res.group("cu") + ret["pkg"] = res.group("p") + ret["total"] = res.group("t") + break + return ret + + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From f4ca5eaa3b177a874da07cc0946fd99b4016f239 Mon Sep 17 00:00:00 2001 From: Marco Genasci Date: Thu, 6 Jan 2022 08:07:49 +0100 Subject: [PATCH 276/506] Added documentation and screenshot for emerge_status module Signed-off-by: Marco Genasci --- docs/modules.rst | 15 +++++++++++++++ screenshots/emerge_status.png | Bin 0 -> 9403 bytes 2 files changed, 15 insertions(+) create mode 100644 screenshots/emerge_status.png diff --git a/docs/modules.rst b/docs/modules.rst index 442d461..413f6cf 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -725,6 +725,21 @@ contributed by `joachimmathes `_ - many thanks .. image:: ../screenshots/dunstctl.png +emerge_status +~~~~~~~~~~~~~ + +Display information about the currently running emerge process. + +Requires the following executable: + * emerge + +Parameters: + * emerge_status.format: Format string (defaults to '{current}/{total} {action} {category}/{pkg}') + +This code is based on `emerge_status module from p3status `_ original created by `AnwariasEu `_. + +.. image:: ../screenshots/emerge_status.png + getcrypto ~~~~~~~~~ diff --git a/screenshots/emerge_status.png b/screenshots/emerge_status.png new file mode 100644 index 0000000000000000000000000000000000000000..1e2a93aed29e07ccee8e56e98310c5b6d73b3217 GIT binary patch literal 9403 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 Date: Thu, 6 Jan 2022 10:13:40 +0100 Subject: [PATCH 277/506] Added ebuild link on README.md Signed-off-by: Marco Genasci --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 2f7bd86..59533bc 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,8 @@ pip install --user bumblebee-status There is also a SlackBuild available here: [slackbuilds:bumblebee-status](http://slackbuilds.org/repository/14.2/desktop/bumblebee-status/) - many thanks to [@Tonus1](https://github.com/Tonus1)! +An ebuild, for Gentoo Linux, is available on [gallifrey overlay](https://github.com/fedeliallalinea/gallifrey/tree/master/x11-misc/bumblebee-status). Instructions for adding the overlay can be found [here](https://github.com/fedeliallalinea/gallifrey/blob/master/README.md). + # Dependencies [Available modules](https://bumblebee-status.readthedocs.io/en/main/modules.html) lists the dependencies (Python modules and external executables) for each module. If you are not using a module, you don't need the dependencies. From 30dd0f2efb6778924f4d9d9fd34a61d32d2fc51c Mon Sep 17 00:00:00 2001 From: Frank Scherrer Date: Thu, 13 Jan 2022 11:05:01 +0100 Subject: [PATCH 278/506] add updating view on apt-cache on click --- bumblebee_status/modules/contrib/apt.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bumblebee_status/modules/contrib/apt.py b/bumblebee_status/modules/contrib/apt.py index b7200bb..575968f 100644 --- a/bumblebee_status/modules/contrib/apt.py +++ b/bumblebee_status/modules/contrib/apt.py @@ -14,6 +14,7 @@ import threading import core.module import core.widget import core.decorators +import core.input import util.cli @@ -56,6 +57,8 @@ class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.updates)) self.__thread = None + core.input.register(self, button=core.input.RIGHT_MOUSE, + cmd=self.updates) def updates(self, widget): if widget.get("error"): From 8bde6378d4db1ae17f749bc3c5c7559184cacb94 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Fri, 14 Jan 2022 13:29:29 +0100 Subject: [PATCH 279/506] [modules/arandr] handle case of "no layouts exist To ensure that arandr works also if no layouts are available, add some (very simplistic) exception handling. see #844 --- bumblebee_status/modules/contrib/arandr.py | 23 ++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/bumblebee_status/modules/contrib/arandr.py b/bumblebee_status/modules/contrib/arandr.py index 7af565d..fd9ad9e 100644 --- a/bumblebee_status/modules/contrib/arandr.py +++ b/bumblebee_status/modules/contrib/arandr.py @@ -136,16 +136,19 @@ class Module(core.module.Module): def _get_layouts(): """Loads and parses the arandr screen layout scripts.""" layouts = {} - for filename in os.listdir(__screenlayout_dir__): - if fnmatch.fnmatch(filename, '*.sh'): - fullpath = os.path.join(__screenlayout_dir__, filename) - with open(fullpath, "r") as file: - for line in file: - s_line = line.strip() - if "xrandr" not in s_line: - continue - displays_in_file = Module._parse_layout(line) - layouts[filename] = displays_in_file + try: + for filename in os.listdir(__screenlayout_dir__): + if fnmatch.fnmatch(filename, '*.sh'): + fullpath = os.path.join(__screenlayout_dir__, filename) + with open(fullpath, "r") as file: + for line in file: + s_line = line.strip() + if "xrandr" not in s_line: + continue + displays_in_file = Module._parse_layout(line) + layouts[filename] = displays_in_file + except Exception as e: + log.error(str(e)) return layouts @staticmethod From 08b5386140fb14ecbccdf9eb7520f597967dee4d Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Fri, 14 Jan 2022 13:39:04 +0100 Subject: [PATCH 280/506] [util/popup] fix endless loop on "close on leave" When closing a popup window when the mouse leave the area (default behaviour, unfortunately), the main "show()" got stuck in an infinite loop. Fix that by setting running to False when exiting. fixes #844 --- bumblebee_status/modules/contrib/arandr.py | 11 ++++++----- bumblebee_status/util/popup.py | 1 + 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/bumblebee_status/modules/contrib/arandr.py b/bumblebee_status/modules/contrib/arandr.py index fd9ad9e..b207524 100644 --- a/bumblebee_status/modules/contrib/arandr.py +++ b/bumblebee_status/modules/contrib/arandr.py @@ -54,7 +54,7 @@ class Module(core.module.Module): def activate_layout(layout_path): log.debug("activating layout") log.debug(layout_path) - execute(layout_path) + execute(layout_path, ignore_errors=True) def popup(self, widget): """Create Popup that allows the user to control their displays in one @@ -64,7 +64,7 @@ class Module(core.module.Module): menu = popup.menu() menu.add_menuitem( "arandr", - callback=partial(execute, self.manager) + callback=partial(execute, self.manager, ignore_errors=True) ) menu.add_separator() @@ -105,11 +105,12 @@ class Module(core.module.Module): if count_on == 1: log.info("attempted to turn off last display") return - execute("{} --output {} --off".format(self.toggle_cmd, display)) + execute("{} --output {} --off".format(self.toggle_cmd, display), ignore_errors=True) else: log.debug("toggling on {}".format(display)) execute( - "{} --output {} --auto".format(self.toggle_cmd, display) + "{} --output {} --auto".format(self.toggle_cmd, display), + ignore_errors=True ) @staticmethod @@ -120,7 +121,7 @@ class Module(core.module.Module): connected). """ displays = {} - for line in execute("xrandr -q").split("\n"): + for line in execute("xrandr -q", ignore_errors=True).split("\n"): if "connected" not in line: continue is_on = bool(re.search(r"\d+x\d+\+(\d+)\+\d+", line)) diff --git a/bumblebee_status/util/popup.py b/bumblebee_status/util/popup.py index bbabe66..784a037 100644 --- a/bumblebee_status/util/popup.py +++ b/bumblebee_status/util/popup.py @@ -49,6 +49,7 @@ class menu(object): return self._menu def __on_focus_out(self, event=None): + self.running = False self._root.destroy() def __on_click(self, callback): From 8867f4f188141a07d3e6d8ee157472cf99a60129 Mon Sep 17 00:00:00 2001 From: Dhananjay Tanpure Date: Thu, 20 Jan 2022 21:06:17 +0000 Subject: [PATCH 281/506] added module for blugon --- bumblebee_status/modules/contrib/blugon.py | 97 ++++++++++++++++++++++ tests/modules/contrib/test_blugon.py | 2 + 2 files changed, 99 insertions(+) create mode 100644 bumblebee_status/modules/contrib/blugon.py create mode 100644 tests/modules/contrib/test_blugon.py diff --git a/bumblebee_status/modules/contrib/blugon.py b/bumblebee_status/modules/contrib/blugon.py new file mode 100644 index 0000000..4fa0079 --- /dev/null +++ b/bumblebee_status/modules/contrib/blugon.py @@ -0,0 +1,97 @@ +"""Displays temperature of blugon and Controls it. + +Use wheel up and down to change temperature, middle click to toggle and right click to reset temprature. + +Default Values: + * Minimum temperature: 1000 (red) + * Maximum temperature: 20000 (blue) + * Default temperature: 6600 + +Requires the following executable: + * blugon + +Parameters: + * blugon.step: The amount of increase/decrease on scroll (default: 200) + +contributed by `DTan13 ` +""" + +import core.module +import core.widget + +import util.cli +import util.format + +import os + + +class Module(core.module.Module): + def __init__(self, config, theme): + super().__init__(config, theme, core.widget.Widget(self.full_text)) + self.__state = True + self.__default = 6600 + self.__step = ( + util.format.asint(self.parameter("step")) if self.parameter("step") else 200 + ) + self.__max, self.__min = 20000, 1000 + + file = open(os.path.expanduser("~/.config/blugon/current")) + self.__current = int(float(file.read())) + + events = [ + { + "type": "toggle", + "action": self.toggle, + "button": core.input.MIDDLE_MOUSE, + }, + { + "type": "blue", + "action": self.blue, + "button": core.input.WHEEL_UP, + }, + { + "type": "red", + "action": self.red, + "button": core.input.WHEEL_DOWN, + }, + { + "type": "reset", + "action": self.reset, + "button": core.input.RIGHT_MOUSE, + }, + ] + + for event in events: + core.input.register(self, button=event["button"], cmd=event["action"]) + + def set_temp(self): + temp = self.__current if self.__state else self.__default + util.cli.execute("blugon --setcurrent={}".format(temp)) + + def full_text(self, widget): + return self.__current if self.__state else self.__default + + def state(self, widget): + if not self.__state: + return ["critical"] + + def toggle(self, event): + self.__state = not self.__state + self.set_temp() + + def reset(self, event): + self.__current = 6600 + self.set_temp() + + def blue(self, event): + if self.__state and (self.__current < self.__max): + self.__current += self.__step + self.set_temp() + + def red(self, event): + if self.__state and (self.__current > self.__min): + self.__current -= self.__step + self.set_temp() + + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/modules/contrib/test_blugon.py b/tests/modules/contrib/test_blugon.py new file mode 100644 index 0000000..6c9dd9b --- /dev/null +++ b/tests/modules/contrib/test_blugon.py @@ -0,0 +1,2 @@ +def test_load_module(): + __import__("modules.contrib.zpool") From c40a1744637b84682731d015420456d35b004182 Mon Sep 17 00:00:00 2001 From: Logan Connolly Date: Tue, 25 Jan 2022 17:50:25 +0100 Subject: [PATCH 282/506] feat(theme): add rose pine theme --- themes/rose-pine.json | 54 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 themes/rose-pine.json diff --git a/themes/rose-pine.json b/themes/rose-pine.json new file mode 100644 index 0000000..e1ce49b --- /dev/null +++ b/themes/rose-pine.json @@ -0,0 +1,54 @@ +{ + "icons": ["awesome-fonts"], + "defaults": { + "separator-block-width": 0, + "warning": { + "fg": "#232136", + "bg": "#f6c177" + }, + "critical": { + "fg": "#232136", + "bg": "#eb6f92" + } + }, + "cycle": [ + { "fg": "#232136", "bg": "#ea9a97" }, + { "fg": "#e0def4", "bg": "#393552" } + ], + "dnf": { + "good": { + "fg": "#232136", + "bg": "#9ccfd8" + } + }, + "pacman": { + "good": { + "fg": "#232136", + "bg": "#9ccfd8" + } + }, + "battery": { + "charged": { + "fg": "#232136", + "bg": "#9ccfd8" + }, + "AC": { + "fg": "#232136", + "bg": "#9ccfd8" + } + }, + "pomodoro": { + "paused": { + "fg": "#232136", + "bg": "#f6c177" + }, + "work": { + "fg": "#232136", + "bg": "#9ccfd8" + }, + "break": { + "fg": "#232136", + "bg": "#c4a7e7" + } + } +} From 4c08cd812e7c436113c7e88245ea929dc35fef44 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Wed, 9 Feb 2022 21:15:08 +0100 Subject: [PATCH 283/506] Create codeql-analysis.yml Trying out CodeQL --- .github/workflows/codeql-analysis.yml | 70 +++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..5e39422 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,70 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ main ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ main ] + schedule: + - cron: '31 0 * * 4' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'python' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://git.io/codeql-language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 From 8458eef1e60fa19d862977b2b08c484e0261d55c Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Wed, 9 Feb 2022 21:24:35 +0100 Subject: [PATCH 284/506] [doc] add codeql badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 59533bc..ba6261f 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ [![Code Climate](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/gpa.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status) [![Test Coverage](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/coverage.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/coverage) [![Issue Count](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/issue_count.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status) +[![CodeQL](https://github.com/tobi-wan-kenobi/bumblebee-status/actions/workflows/codeql-analysis.yml/badge.svg?branch=main)](https://github.com/tobi-wan-kenobi/bumblebee-status/actions/workflows/codeql-analysis.yml) ![License](https://img.shields.io/github/license/tobi-wan-kenobi/bumblebee-status) **Many, many thanks to all contributors! I am still amazed by and deeply grateful for how many PRs this project gets.** From 4f9553f7ea4ca9d9166980384669c451b74cd019 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Fri, 11 Feb 2022 13:44:10 +0100 Subject: [PATCH 285/506] [modules/rss] fix insecure use of tempfile fixes #850 --- bumblebee_status/modules/contrib/rss.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bumblebee_status/modules/contrib/rss.py b/bumblebee_status/modules/contrib/rss.py index 7b8c032..7824e2e 100644 --- a/bumblebee_status/modules/contrib/rss.py +++ b/bumblebee_status/modules/contrib/rss.py @@ -55,7 +55,7 @@ class Module(core.module.Module): self._state = [] - self._newspaper_filename = tempfile.mktemp(".html") + self._newspaper_file = tempfile.NamedTemporaryFile(mode="w", suffix=".html") self._last_refresh = 0 self._last_update = 0 @@ -308,10 +308,11 @@ class Module(core.module.Module): while newspaper_items: content += self._create_news_section(newspaper_items) - open(self._newspaper_filename, "w").write( + self._newspaper_file.write( HTML_TEMPLATE.replace("[[CONTENT]]", content) ) - webbrowser.open("file://" + self._newspaper_filename) + self._newspaper_file.flush() + webbrowser.open("file://" + self._newspaper_file.name) self._update_history("newspaper") self._save_history() From 5c390be25cba3338c858e0fb77e4d446515fd5b1 Mon Sep 17 00:00:00 2001 From: Christopher Kepes Date: Sat, 12 Feb 2022 11:06:10 +0100 Subject: [PATCH 286/506] [modules/nic] Added strength indicator for wifi signals --- bumblebee_status/modules/core/nic.py | 46 ++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/bumblebee_status/modules/core/nic.py b/bumblebee_status/modules/core/nic.py index 7dde2f0..591fe1c 100644 --- a/bumblebee_status/modules/core/nic.py +++ b/bumblebee_status/modules/core/nic.py @@ -13,7 +13,9 @@ Parameters: * nic.exclude: Comma-separated list of interface prefixes (supporting regular expressions) to exclude (defaults to 'lo,virbr,docker,vboxnet,veth,br,.*:avahi') * nic.include: Comma-separated list of interfaces to include * nic.states: Comma-separated list of states to show (prefix with '^' to invert - i.e. ^down -> show all devices that are not in state down) - * nic.format: Format string (defaults to '{intf} {state} {ip} {ssid}') + * nic.format: Format string (defaults to '{intf} {state} {ip} {ssid} {strength}') + * nic.strength_warning: Integer to set the threshold for warning state (defaults to 50) + * nic.strength_critical: Integer to set the threshold for critical state (defaults to 30) """ import re @@ -28,7 +30,7 @@ import util.format class Module(core.module.Module): - @core.decorators.every(seconds=10) + @core.decorators.every(seconds=5) def __init__(self, config, theme): widgets = [] super().__init__(config, theme, widgets) @@ -45,7 +47,15 @@ class Module(core.module.Module): self._states["exclude"].append(state[1:]) else: self._states["include"].append(state) - self._format = self.parameter("format", "{intf} {state} {ip} {ssid}") + self._format = self.parameter("format", "{intf} {state} {ip} {ssid} {strength}") + + self._strength_threshold_critical = self.parameter("strength_critical", 30) + self._strength_threshold_warning = self.parameter("strength_warning", 50) + + # Limits for the accepted dBm values of wifi strength + self.__strength_dbm_lower_bound = -110 + self.__strength_dbm_upper_bound = -30 + self.iw = shutil.which("iw") self._update_widgets(widgets) @@ -64,6 +74,13 @@ class Module(core.module.Module): iftype = "wireless" if self._iswlan(intf) else "wired" iftype = "tunnel" if self._istunnel(intf) else iftype + # "strength" is none if interface type is not wlan + if self._iswlan(intf): + if widget.get("strength") < self._strength_threshold_critical: + states.append("critical") + elif widget.get("strength") < self._strength_threshold_warning: + states.append("warning") + states.append("{}-{}".format(iftype, widget.get("state"))) return states @@ -116,6 +133,9 @@ class Module(core.module.Module): ): continue + strength_dbm = self.get_strength_dbm(intf) + strength_percent = self.convert_strength_dbm_percent(strength_dbm) + widget = self.widget(intf) if not widget: widget = self.add_widget(name=intf) @@ -126,12 +146,14 @@ class Module(core.module.Module): ip=", ".join(addr), intf=intf, state=state, + strength=str(strength_percent) + "%" if strength_percent else "", ssid=self.get_ssid(intf), ).split() ) ) widget.set("intf", intf) widget.set("state", state) + widget.set("strength", strength_percent) def get_ssid(self, intf): if not self._iswlan(intf) or self._istunnel(intf) or not self.iw: @@ -145,5 +167,23 @@ class Module(core.module.Module): return "" + def get_strength_dbm(self, intf): + if not self._iswlan(intf) or self._istunnel(intf) or not self.iw: + return None + + with open("/proc/net/wireless", "r") as file: + for line in file: + if intf in line: + # Remove trailing . by slicing it off ;) + strength_dbm = line.split()[3][:-1] + return util.format.asint(strength_dbm, + minium=self.__strength_dbm_lower_bound, + maximum=self.__strength_dbm_upper_bound) + + return None + + def convert_strength_dbm_percent(self, signal): + return int(100 * ((signal + 100) / 70.0)) if signal else None + # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From 4784be40762601c656ce8964c15c11d556ab8b0d Mon Sep 17 00:00:00 2001 From: Pi-Yueh Chuang Date: Sat, 12 Feb 2022 12:54:31 -0500 Subject: [PATCH 287/506] typo in nic.py: minium -> minimum --- bumblebee_status/modules/core/nic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bumblebee_status/modules/core/nic.py b/bumblebee_status/modules/core/nic.py index 591fe1c..94a9e20 100644 --- a/bumblebee_status/modules/core/nic.py +++ b/bumblebee_status/modules/core/nic.py @@ -177,7 +177,7 @@ class Module(core.module.Module): # Remove trailing . by slicing it off ;) strength_dbm = line.split()[3][:-1] return util.format.asint(strength_dbm, - minium=self.__strength_dbm_lower_bound, + minimum=self.__strength_dbm_lower_bound, maximum=self.__strength_dbm_upper_bound) return None From 2a77e3a85c1f59318a7538028314e0edde9c3b57 Mon Sep 17 00:00:00 2001 From: Mihai Morariu Date: Mon, 14 Feb 2022 15:36:24 +0200 Subject: [PATCH 288/506] Fix exception in location.py. --- bumblebee_status/modules/contrib/sun.py | 10 +++++++++- bumblebee_status/util/location.py | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/bumblebee_status/modules/contrib/sun.py b/bumblebee_status/modules/contrib/sun.py index e9eefd2..34a4b71 100644 --- a/bumblebee_status/modules/contrib/sun.py +++ b/bumblebee_status/modules/contrib/sun.py @@ -39,7 +39,11 @@ class Module(core.module.Module): self.__sun = None if not lat or not lon: - lat, lon = util.location.coordinates() + try: + lat, lon = util.location.coordinates() + except Exception: + pass + if lat and lon: self.__sun = Sun(float(lat), float(lon)) @@ -55,6 +59,10 @@ class Module(core.module.Module): return "n/a" def __calculate_times(self): + if not self.__sun: + self.__sunset = self.__sunrise = None + return + self.__isup = False order_matters = True diff --git a/bumblebee_status/util/location.py b/bumblebee_status/util/location.py index 12242ea..e48b71a 100644 --- a/bumblebee_status/util/location.py +++ b/bumblebee_status/util/location.py @@ -59,11 +59,11 @@ def __load(): __next = time.time() + 60 * 30 # error - try again every 30m -def __get(name, default=None): +def __get(name): global __data if not __data or __expired(): __load() - return __data.get(name, default) + return __data[name] def reset(): From 3aadab5628f0b4f839428231e28f1d7ab9d60d0c Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Mon, 14 Feb 2022 14:58:01 +0100 Subject: [PATCH 289/506] [modules/publicip] handle missing public ip more gracefully If location does not throw, but reports an empty public IP, return "n/a". Since this caused a bug, also add a test for it. fixes #853 --- bumblebee_status/modules/contrib/publicip.py | 4 ++-- tests/modules/contrib/test_publicip.py | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/bumblebee_status/modules/contrib/publicip.py b/bumblebee_status/modules/contrib/publicip.py index 17a23e3..a74d708 100644 --- a/bumblebee_status/modules/contrib/publicip.py +++ b/bumblebee_status/modules/contrib/publicip.py @@ -16,13 +16,13 @@ class Module(core.module.Module): self.__ip = "" def public_ip(self, widget): - return self.__ip + return self.__ip or "n/a" def update(self): try: self.__ip = util.location.public_ip() except Exception: - self.__ip = "n/a" + self.__ip = None # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/modules/contrib/test_publicip.py b/tests/modules/contrib/test_publicip.py index 9584bd7..870ede6 100644 --- a/tests/modules/contrib/test_publicip.py +++ b/tests/modules/contrib/test_publicip.py @@ -26,6 +26,15 @@ class PublicIPTest(TestCase): assert widget(module).full_text() == '5.12.220.2' + @mock.patch('util.location.public_ip') + def test_public_ip(self, public_ip_mock): + public_ip_mock.return_value = None + + module = build_module() + module.update() + + assert widget(module).full_text() == 'n/a' + @mock.patch('util.location.public_ip') def test_public_ip_with_exception(self, public_ip_mock): public_ip_mock.side_effect = Exception From 03731136b6f2147ded54ce2881eddc4b1dba5d83 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Tue, 15 Feb 2022 16:34:02 +0100 Subject: [PATCH 290/506] [modules/nic] fix missing check for None --- bumblebee_status/modules/core/nic.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bumblebee_status/modules/core/nic.py b/bumblebee_status/modules/core/nic.py index 94a9e20..09fe487 100644 --- a/bumblebee_status/modules/core/nic.py +++ b/bumblebee_status/modules/core/nic.py @@ -75,10 +75,11 @@ class Module(core.module.Module): iftype = "tunnel" if self._istunnel(intf) else iftype # "strength" is none if interface type is not wlan - if self._iswlan(intf): - if widget.get("strength") < self._strength_threshold_critical: + strength = widget.get("strength") + if self._iswlan(intf) and strength: + if strength < self._strength_threshold_critical: states.append("critical") - elif widget.get("strength") < self._strength_threshold_warning: + elif strength < self._strength_threshold_warning: states.append("warning") states.append("{}-{}".format(iftype, widget.get("state"))) From 928f8258aabf5cb05b709fa3740099218d5acae2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20L=C3=BCftinger?= Date: Sun, 20 Feb 2022 12:29:49 +0100 Subject: [PATCH 291/506] fix case of Kelvin SI unit in redshift widget --- bumblebee_status/modules/core/redshift.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bumblebee_status/modules/core/redshift.py b/bumblebee_status/modules/core/redshift.py index c6735b1..d50463f 100644 --- a/bumblebee_status/modules/core/redshift.py +++ b/bumblebee_status/modules/core/redshift.py @@ -54,7 +54,7 @@ def get_redshift_value(module): for line in res.split("\n"): line = line.lower() if "temperature" in line: - widget.set("temp", line.split(" ")[2]) + widget.set("temp", line.split(" ")[2].upper()) if "period" in line: state = line.split(" ")[1] if "day" in state: From 950931e1b9e9619d556a57e3a576e2dc1b6bbc51 Mon Sep 17 00:00:00 2001 From: Bernhard B Date: Mon, 28 Feb 2022 19:14:21 +0100 Subject: [PATCH 292/506] added new module 'pactl' * added new module 'pactl' which displays the current default sink and allows to select a different default sink from the popup menu. --- bumblebee_status/modules/contrib/pactl.py | 142 ++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 bumblebee_status/modules/contrib/pactl.py diff --git a/bumblebee_status/modules/contrib/pactl.py b/bumblebee_status/modules/contrib/pactl.py new file mode 100644 index 0000000..0abea68 --- /dev/null +++ b/bumblebee_status/modules/contrib/pactl.py @@ -0,0 +1,142 @@ +# pylint: disable=C0111,R0903 + +""" Displays the current default sink. + + Left click opens a popup menu that lists all available sinks and allows to change the default sink. + + Per default, this module uses the sink names returned by "pactl list sinks short" + + sample output of "pactl list sinks short": + + 2 alsa_output.pci-0000_00_1f.3.analog-stereo module-alsa-card.c s16le 2ch 44100Hz SUSPENDED + 3 alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo module-alsa-card.c s16le 2ch 44100Hz SUSPENDE + + As "alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo" is not a particularly nice name, its possible to map the name to more a + user friendly name. e.g to map "alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo" to the name "Headset", add the following + bumblebee-status config entry: pactl.alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo=Headset + + The module also allows to specify individual (unicode) icons for all the sinks. e.g in order to use the icon 🎧 for the + "alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo" sink, add the following bumblebee-status config entry: + pactl.icon.alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo=🎧 + + Requirements: + * pulseaudio + * pactl +""" + +import logging +import functools + +import core.module +import core.widget +import core.input + +import util.cli +import util.popup + + +class Sink(object): + def __init__(self, id, internal_name, friendly_name, icon): + super().__init__() + self.__id = id + self.__internal_name = internal_name + self.__friendly_name = friendly_name + self.__icon = icon + + @property + def id(self): + return self.__id + + @property + def internal_name(self): + return self.__internal_name + + @property + def friendly_name(self): + return self.__friendly_name + + @property + def icon(self): + return self.__icon + + @property + def display_name(self): + display_name = ( + self.__icon + " " + self.__friendly_name + if self.__icon != "" + else self.__friendly_name + ) + return display_name + + +class Module(core.module.Module): + def __init__(self, config, theme): + super().__init__(config, theme, core.widget.Widget(self.default_sink)) + + self.__default_sink = None + + res = util.cli.execute("pactl list sinks short") + lines = res.splitlines() + + self.__sinks = [] + for line in lines: + info = line.split("\t") + try: + friendly_name = self.parameter(info[1], info[1]) + icon = self.parameter("icon." + info[1], "") + self.__sinks.append(Sink(info[0], info[1], friendly_name, icon)) + except: + logging.exception("Couldn't parse sink") + pass + + core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.popup) + + def __sink(self, internal_sink_name): + for sink in self.__sinks: + logging.info(sink.internal_name) + if internal_sink_name == sink.internal_name: + return sink + return None + + def update(self): + try: + res = util.cli.execute("pactl info") + lines = res.splitlines() + self.__default_sink = None + for line in lines: + if not line.startswith("Default Sink:"): + continue + internal_sink_name = line.replace("Default Sink: ", "") + self.__default_sink = self.__sink(internal_sink_name) + break + except Exception as e: + logging.exception("Could not get pactl info") + self.__default_sink = None + + def default_sink(self, widget): + if self.__default_sink is None: + return "unknown" + return self.__default_sink.display_name + + def __on_sink_selected(self, sink): + try: + util.cli.execute("pactl set-default-sink {}".format(sink.id)) + self.__default_sink = sink + except Exception as e: + logging.exception("Couldn't set default sink") + + def popup(self, widget): + menu = util.popup.menu() + + for sink in self.__sinks: + menu.add_menuitem( + sink.friendly_name, + callback=functools.partial(self.__on_sink_selected, sink), + ) + menu.show(widget) + + def state(self, widget): + return [] + + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From 33d22c2637f0b667182b9b0c3a7b7db2b60578fd Mon Sep 17 00:00:00 2001 From: Bernhard B Date: Mon, 28 Feb 2022 19:20:19 +0100 Subject: [PATCH 293/506] removed debug log from 'pactl' module --- bumblebee_status/modules/contrib/pactl.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bumblebee_status/modules/contrib/pactl.py b/bumblebee_status/modules/contrib/pactl.py index 0abea68..cd62c9a 100644 --- a/bumblebee_status/modules/contrib/pactl.py +++ b/bumblebee_status/modules/contrib/pactl.py @@ -93,7 +93,6 @@ class Module(core.module.Module): def __sink(self, internal_sink_name): for sink in self.__sinks: - logging.info(sink.internal_name) if internal_sink_name == sink.internal_name: return sink return None From 07200c466bd746b4076db0547093f467d8c70254 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Wed, 2 Mar 2022 12:33:02 +0100 Subject: [PATCH 294/506] [themes] add zengarden light (powerline) --- themes/zengarden-powerline-light.json | 64 +++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 themes/zengarden-powerline-light.json diff --git a/themes/zengarden-powerline-light.json b/themes/zengarden-powerline-light.json new file mode 100644 index 0000000..ecf7de5 --- /dev/null +++ b/themes/zengarden-powerline-light.json @@ -0,0 +1,64 @@ +{ + "icons": [ "paxy97", "awesome-fonts" ], + "defaults": { + "warning": { + "fg": "#353839", + "bg": "#967117" + }, + "critical": { + "fg": "#353839", + "bg": "#ba1d58" + }, + "default-separators": false, + "separator-block-width": 0 + }, + "fg": "#353839", + "bg": "#c4b6a3", + "dnf": { + "good": { + "fg": "#353839", + "bg": "#177245" + } + }, + "apt": { + "good": { + "fg": "#353839", + "bg": "#177245" + } + }, + "battery": { + "charged": { + "fg": "#353839", + "bg": "#177245" + }, + "AC": { + "fg": "#353839", + "bg": "#177245" + } + }, + "bluetooth": { + "ON": { + "fg": "#353839", + "bg": "#177245" + } + }, + "git": { + "modified": { "bg": "#174572" }, + "deleted": { "bg": "#ba1d58" }, + "new": { "bg": "#967117" } + }, + "pomodoro": { + "paused": { + "fg": "#353839", + "bg": "#d79921" + }, + "work": { + "fg": "#353839", + "bg": "#177245" + }, + "break": { + "fg": "#353839", + "bg": "#177245" + } + } +} From 9a2e7637c9d5c6d4098b3bcff548dbf4045d31a2 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Wed, 2 Mar 2022 16:16:24 +0100 Subject: [PATCH 295/506] [themes/zengarden] lighter accents --- themes/zengarden-powerline-light.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/themes/zengarden-powerline-light.json b/themes/zengarden-powerline-light.json index ecf7de5..7fe0bc5 100644 --- a/themes/zengarden-powerline-light.json +++ b/themes/zengarden-powerline-light.json @@ -3,11 +3,11 @@ "defaults": { "warning": { "fg": "#353839", - "bg": "#967117" + "bg": "#b38a32" }, "critical": { "fg": "#353839", - "bg": "#ba1d58" + "bg": "#d94070" }, "default-separators": false, "separator-block-width": 0 @@ -17,29 +17,29 @@ "dnf": { "good": { "fg": "#353839", - "bg": "#177245" + "bg": "#378c5d" } }, "apt": { "good": { "fg": "#353839", - "bg": "#177245" + "bg": "#378c5d" } }, "battery": { "charged": { "fg": "#353839", - "bg": "#177245" + "bg": "#378c5d" }, "AC": { "fg": "#353839", - "bg": "#177245" + "bg": "#378c5d" } }, "bluetooth": { "ON": { "fg": "#353839", - "bg": "#177245" + "bg": "#378c5d" } }, "git": { @@ -54,11 +54,11 @@ }, "work": { "fg": "#353839", - "bg": "#177245" + "bg": "#378c5d" }, "break": { "fg": "#353839", - "bg": "#177245" + "bg": "#378c5d" } } } From c57daf65ce6da29d405570e1de09adbc9bad779b Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Thu, 3 Mar 2022 14:31:47 +0100 Subject: [PATCH 296/506] [themes/zengarden] add key colors --- themes/zengarden-powerline-light.json | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/themes/zengarden-powerline-light.json b/themes/zengarden-powerline-light.json index 7fe0bc5..097842a 100644 --- a/themes/zengarden-powerline-light.json +++ b/themes/zengarden-powerline-light.json @@ -60,5 +60,19 @@ "fg": "#353839", "bg": "#378c5d" } - } + }, + "keys": { + "Key.cmd": { + "bg": "#477ab7" + }, + "Key.shift": { + "bg": "#b38a32" + }, + "Key.ctrl": { + "bg": "#377c8b" + }, + "Key.alt": { + "bg": "#e05b1f" + } + } } From 7d33171749b0109ad55bb88b6dafc9b011b12f83 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Fri, 4 Mar 2022 09:35:43 +0100 Subject: [PATCH 297/506] [core/input] methods can be event callbacks When registering an event (especially mouse events), if the parameter is a valid method in the Module, execute that with the event as parameter. Add this in the core.spacer module as an example. fixes #858 see #857 --- bumblebee_status/core/input.py | 3 +++ bumblebee_status/modules/core/spacer.py | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/bumblebee_status/core/input.py b/bumblebee_status/core/input.py index 2f9fdfc..5752dd8 100644 --- a/bumblebee_status/core/input.py +++ b/bumblebee_status/core/input.py @@ -54,8 +54,11 @@ def register(obj, button=None, cmd=None, wait=False): event_id = __event_id(obj.id if obj is not None else "", button) logging.debug("registering callback {}".format(event_id)) core.event.unregister(event_id) # make sure there's always only one input event + if callable(cmd): core.event.register_exclusive(event_id, cmd) + elif obj and hasattr(obj, cmd) and callable(getattr(obj, cmd)): + core.event.register_exclusive(event_id, lambda event: getattr(obj, cmd)(event)) else: core.event.register_exclusive(event_id, lambda event: __execute(event, cmd, wait)) diff --git a/bumblebee_status/modules/core/spacer.py b/bumblebee_status/modules/core/spacer.py index 7e4453a..e5a2d5e 100644 --- a/bumblebee_status/modules/core/spacer.py +++ b/bumblebee_status/modules/core/spacer.py @@ -9,7 +9,7 @@ Parameters: import core.module import core.widget import core.decorators - +import core.input class Module(core.module.Module): @core.decorators.every(minutes=60) @@ -20,5 +20,8 @@ class Module(core.module.Module): def text(self, _): return self.__text + def update_text(self, event): + self.__text = core.input.button_name(event["button"]) + # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From 9cbc39e462df8b2f732d22c1821938fef680bc4f Mon Sep 17 00:00:00 2001 From: Sadegh Hamedani Date: Fri, 4 Mar 2022 18:28:13 +0330 Subject: [PATCH 298/506] [contrib] added module persian_date --- .../modules/contrib/persian_date.py | 45 +++++++++++++++++++ themes/icons/awesome-fonts.json | 1 + 2 files changed, 46 insertions(+) create mode 100644 bumblebee_status/modules/contrib/persian_date.py diff --git a/bumblebee_status/modules/contrib/persian_date.py b/bumblebee_status/modules/contrib/persian_date.py new file mode 100644 index 0000000..6e3eded --- /dev/null +++ b/bumblebee_status/modules/contrib/persian_date.py @@ -0,0 +1,45 @@ +# pylint: disable=C0111,R0903 + +"""Displays the current date and time in Persian(Jalali) Calendar. + +Requires the following python packages: + * jdatetime + +Parameters: + * datetime.format: strftime()-compatible formatting string. default: "%A %d %B" e.g., "جمعه ۱۳ اسفند" + * datetime.locale: locale to use. default: "fa_IR" +""" + +from __future__ import absolute_import +import jdatetime +import locale + +import core.module +import core.widget +import core.input + + +class Module(core.module.Module): + def __init__(self, config, theme): + super().__init__(config, theme, core.widget.Widget(self.full_text)) + + l = ("fa_IR", "UTF-8") + lcl = self.parameter("locale", ".".join(l)) + try: + locale.setlocale(locale.LC_ALL, lcl.split(".")) + except Exception as e: + locale.setlocale(locale.LC_ALL, ("fa_IR", "UTF-8")) + + def default_format(self): + return "%A %d %B" + + def full_text(self, widget): + enc = locale.getpreferredencoding() + fmt = self.parameter("format", self.default_format()) + retval = jdatetime.datetime.now().strftime(fmt) + if hasattr(retval, "decode"): + return retval.decode(enc) + return retval + + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/themes/icons/awesome-fonts.json b/themes/icons/awesome-fonts.json index 9f14da9..f85dfd8 100644 --- a/themes/icons/awesome-fonts.json +++ b/themes/icons/awesome-fonts.json @@ -7,6 +7,7 @@ "default-separators": false }, "date": { "prefix": "" }, + "persian_date": { "prefix": "" }, "time": { "prefix": "" }, "datetime": { "prefix": "" }, "datetz": { "prefix": "" }, From e5cdabcc0f9fb972828cf88739072130bf2f545a Mon Sep 17 00:00:00 2001 From: Dhananjay Tanpure Date: Mon, 7 Mar 2022 08:50:30 +0530 Subject: [PATCH 299/506] mpd and title --- bumblebee_status/modules/contrib/mpd.py | 9 +++++++++ bumblebee_status/modules/contrib/title.py | 6 +++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/bumblebee_status/modules/contrib/mpd.py b/bumblebee_status/modules/contrib/mpd.py index 3efffff..92568d1 100644 --- a/bumblebee_status/modules/contrib/mpd.py +++ b/bumblebee_status/modules/contrib/mpd.py @@ -94,6 +94,12 @@ class Module(core.module.Module): "cmd": "mpc toggle" + self._hostcmd, } widget.full_text(self.description) + elif widget_name == "mpd.toggle": + widget_map[widget] = { + "button": core.input.LEFT_MOUSE, + "cmd": "mpc toggle" + self._hostcmd, + } + widget.full_text(self.toggle) elif widget_name == "mpd.next": widget_map[widget] = { "button": core.input.LEFT_MOUSE, @@ -127,6 +133,9 @@ class Module(core.module.Module): def description(self, widget): return string.Formatter().vformat(self._fmt, (), self._tags) + def toggle(self, widget): + return str(util.cli.execute("mpc status %currenttime%/%totaltime%", ignore_errors=True)).strip() + def update(self): self._load_song() diff --git a/bumblebee_status/modules/contrib/title.py b/bumblebee_status/modules/contrib/title.py index 33af47a..055369e 100644 --- a/bumblebee_status/modules/contrib/title.py +++ b/bumblebee_status/modules/contrib/title.py @@ -9,6 +9,7 @@ Parameters: * title.max : Maximum character length for title before truncating. Defaults to 64. * title.placeholder : Placeholder text to be placed if title was truncated. Defaults to '...'. * title.scroll : Boolean flag for scrolling title. Defaults to False + * title.short : Boolean flag for short title. Defaults to False contributed by `UltimatePancake `_ - many thanks! @@ -35,6 +36,7 @@ class Module(core.module.Module): # parsing of parameters self.__scroll = util.format.asbool(self.parameter("scroll", False)) + self.__short = util.format.asbool(self.parameter("short", False)) self.__max = int(self.parameter("max", 64)) self.__placeholder = self.parameter("placeholder", "...") self.__title = "" @@ -66,7 +68,9 @@ class Module(core.module.Module): def __pollTitle(self): """Updating current title.""" try: - self.__full_title = self.__i3.get_tree().find_focused().name + focused = self.__i3.get_tree().find_focused().name + self.__full_title = focused.split( + "-")[-1].strip() if self.__short else focused except: self.__full_title = no_title if self.__full_title is None: From d52f71306381ac02a0a5a768d74f3d045ab17cf0 Mon Sep 17 00:00:00 2001 From: Bernhard B Date: Mon, 7 Mar 2022 20:53:11 +0100 Subject: [PATCH 300/506] Revert "Merge pull request #857 from bbernhard/pactl" This reverts commit eb51a3c1c7d99829304ba615b9caa4ef8417af42, reversing changes made to c57daf65ce6da29d405570e1de09adbc9bad779b. Instead of creating a separate module, the changes will be integrated into the pulseaudio module. --- bumblebee_status/modules/contrib/pactl.py | 141 ---------------------- 1 file changed, 141 deletions(-) delete mode 100644 bumblebee_status/modules/contrib/pactl.py diff --git a/bumblebee_status/modules/contrib/pactl.py b/bumblebee_status/modules/contrib/pactl.py deleted file mode 100644 index cd62c9a..0000000 --- a/bumblebee_status/modules/contrib/pactl.py +++ /dev/null @@ -1,141 +0,0 @@ -# pylint: disable=C0111,R0903 - -""" Displays the current default sink. - - Left click opens a popup menu that lists all available sinks and allows to change the default sink. - - Per default, this module uses the sink names returned by "pactl list sinks short" - - sample output of "pactl list sinks short": - - 2 alsa_output.pci-0000_00_1f.3.analog-stereo module-alsa-card.c s16le 2ch 44100Hz SUSPENDED - 3 alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo module-alsa-card.c s16le 2ch 44100Hz SUSPENDE - - As "alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo" is not a particularly nice name, its possible to map the name to more a - user friendly name. e.g to map "alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo" to the name "Headset", add the following - bumblebee-status config entry: pactl.alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo=Headset - - The module also allows to specify individual (unicode) icons for all the sinks. e.g in order to use the icon 🎧 for the - "alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo" sink, add the following bumblebee-status config entry: - pactl.icon.alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo=🎧 - - Requirements: - * pulseaudio - * pactl -""" - -import logging -import functools - -import core.module -import core.widget -import core.input - -import util.cli -import util.popup - - -class Sink(object): - def __init__(self, id, internal_name, friendly_name, icon): - super().__init__() - self.__id = id - self.__internal_name = internal_name - self.__friendly_name = friendly_name - self.__icon = icon - - @property - def id(self): - return self.__id - - @property - def internal_name(self): - return self.__internal_name - - @property - def friendly_name(self): - return self.__friendly_name - - @property - def icon(self): - return self.__icon - - @property - def display_name(self): - display_name = ( - self.__icon + " " + self.__friendly_name - if self.__icon != "" - else self.__friendly_name - ) - return display_name - - -class Module(core.module.Module): - def __init__(self, config, theme): - super().__init__(config, theme, core.widget.Widget(self.default_sink)) - - self.__default_sink = None - - res = util.cli.execute("pactl list sinks short") - lines = res.splitlines() - - self.__sinks = [] - for line in lines: - info = line.split("\t") - try: - friendly_name = self.parameter(info[1], info[1]) - icon = self.parameter("icon." + info[1], "") - self.__sinks.append(Sink(info[0], info[1], friendly_name, icon)) - except: - logging.exception("Couldn't parse sink") - pass - - core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.popup) - - def __sink(self, internal_sink_name): - for sink in self.__sinks: - if internal_sink_name == sink.internal_name: - return sink - return None - - def update(self): - try: - res = util.cli.execute("pactl info") - lines = res.splitlines() - self.__default_sink = None - for line in lines: - if not line.startswith("Default Sink:"): - continue - internal_sink_name = line.replace("Default Sink: ", "") - self.__default_sink = self.__sink(internal_sink_name) - break - except Exception as e: - logging.exception("Could not get pactl info") - self.__default_sink = None - - def default_sink(self, widget): - if self.__default_sink is None: - return "unknown" - return self.__default_sink.display_name - - def __on_sink_selected(self, sink): - try: - util.cli.execute("pactl set-default-sink {}".format(sink.id)) - self.__default_sink = sink - except Exception as e: - logging.exception("Couldn't set default sink") - - def popup(self, widget): - menu = util.popup.menu() - - for sink in self.__sinks: - menu.add_menuitem( - sink.friendly_name, - callback=functools.partial(self.__on_sink_selected, sink), - ) - menu.show(widget) - - def state(self, widget): - return [] - - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From 82ca97c65f7ba6a6c65eef2521f1248859fab66b Mon Sep 17 00:00:00 2001 From: Sadegh Hamedani Date: Thu, 10 Mar 2022 11:52:08 +0330 Subject: [PATCH 301/506] [core/datetime] added 'dtlibrary' attribute and 'set_locale' method --- bumblebee_status/modules/core/datetime.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/bumblebee_status/modules/core/datetime.py b/bumblebee_status/modules/core/datetime.py index febb5fb..0a15c70 100644 --- a/bumblebee_status/modules/core/datetime.py +++ b/bumblebee_status/modules/core/datetime.py @@ -17,26 +17,33 @@ import core.input class Module(core.module.Module): - def __init__(self, config, theme): + def __init__(self, config, theme, dtlibrary=None): super().__init__(config, theme, core.widget.Widget(self.full_text)) core.input.register(self, button=core.input.LEFT_MOUSE, cmd="calendar") - l = locale.getdefaultlocale() + self.dtlibrary = dtlibrary or datetime + + def set_locale(self): + l = self.default_locale() if not l or l == (None, None): l = ("en_US", "UTF-8") lcl = self.parameter("locale", ".".join(l)) try: - locale.setlocale(locale.LC_TIME, lcl.split(".")) + locale.setlocale(locale.LC_ALL, lcl.split(".")) except Exception as e: - locale.setlocale(locale.LC_TIME, ("en_US", "UTF-8")) + locale.setlocale(locale.LC_ALL, ("en_US", "UTF-8")) def default_format(self): return "%x %X" + def default_locale(self): + return locale.getdefaultlocale() + def full_text(self, widget): + self.set_locale() enc = locale.getpreferredencoding() fmt = self.parameter("format", self.default_format()) - retval = datetime.datetime.now().strftime(fmt) + retval = self.dtlibrary.datetime.now().strftime(fmt) if hasattr(retval, "decode"): return retval.decode(enc) return retval From c228ca3b128c420d817664b9bc62dd2db7618ead Mon Sep 17 00:00:00 2001 From: Sadegh Hamedani Date: Thu, 10 Mar 2022 11:53:19 +0330 Subject: [PATCH 302/506] [contrib/persian_date] refactor using `core.datetime` module as parent --- .../modules/contrib/persian_date.py | 28 +++++-------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/bumblebee_status/modules/contrib/persian_date.py b/bumblebee_status/modules/contrib/persian_date.py index 6e3eded..9873e1a 100644 --- a/bumblebee_status/modules/contrib/persian_date.py +++ b/bumblebee_status/modules/contrib/persian_date.py @@ -10,36 +10,22 @@ Parameters: * datetime.locale: locale to use. default: "fa_IR" """ -from __future__ import absolute_import import jdatetime -import locale -import core.module -import core.widget -import core.input +import core.decorators +from modules.core.datetime import Module as dtmodule -class Module(core.module.Module): +class Module(dtmodule): + @core.decorators.every(minutes=1) def __init__(self, config, theme): - super().__init__(config, theme, core.widget.Widget(self.full_text)) - - l = ("fa_IR", "UTF-8") - lcl = self.parameter("locale", ".".join(l)) - try: - locale.setlocale(locale.LC_ALL, lcl.split(".")) - except Exception as e: - locale.setlocale(locale.LC_ALL, ("fa_IR", "UTF-8")) + super().__init__(config, theme, dtlibrary=jdatetime) def default_format(self): return "%A %d %B" - def full_text(self, widget): - enc = locale.getpreferredencoding() - fmt = self.parameter("format", self.default_format()) - retval = jdatetime.datetime.now().strftime(fmt) - if hasattr(retval, "decode"): - return retval.decode(enc) - return retval + def default_locale(self): + return ("fa_IR", "UTF-8") # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From 879744e19cab5cc7357912ba670d200adfd58be6 Mon Sep 17 00:00:00 2001 From: ishaan Date: Mon, 14 Mar 2022 22:28:00 +0530 Subject: [PATCH 303/506] add aur-update --- .../modules/contrib/aur-update.py | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 bumblebee_status/modules/contrib/aur-update.py diff --git a/bumblebee_status/modules/contrib/aur-update.py b/bumblebee_status/modules/contrib/aur-update.py new file mode 100644 index 0000000..c175a63 --- /dev/null +++ b/bumblebee_status/modules/contrib/aur-update.py @@ -0,0 +1,65 @@ +"""Check updates for AUR. + +Requires the following packages: + * yay (used as default) + +Note - You can replace yay by changing the "yay -Qum" +command for your preferred AUR helper. Few examples: + +paru -Qum +pikaur -Qua +rua upgrade --printonly +trizen -Su --aur --quiet +yay -Qum + +contributed by `ishaanbhimwal `_ - many thanks! +""" + +import logging + +import core.module +import core.widget +import core.decorators + +import util.cli + + +class Module(core.module.Module): + @core.decorators.every(minutes=60) + def __init__(self, config, theme): + super().__init__(config, theme, core.widget.Widget(self.utilization)) + self.background = True + self.__packages = 0 + self.__error = False + + @property + def __format(self): + return self.parameter("format", "Update AUR: {}") + + def utilization(self, widget): + return self.__format.format(self.__packages) + + def hidden(self): + return self.__packages == 0 and not self.__error + + def update(self): + self.__error = False + code, result = util.cli.execute( + "yay -Qum", ignore_errors=True, return_exitcode=True + ) + + if code == 0: + self.__packages = len(result.strip().split("\n")) + elif code == 2: + self.__packages = 0 + else: + self.__error = True + logging.error("yay -Qum exited with {}: {}".format(code, result)) + + def state(self, widget): + if self.__error: + return "warning" + return self.threshold_state(self.__packages, 1, 100) + + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From f513582d44d33cc32e71dfb16a8968ca055216db Mon Sep 17 00:00:00 2001 From: ishaan Date: Tue, 15 Mar 2022 01:17:57 +0530 Subject: [PATCH 304/506] fix aur-update showing wrong update number --- .../modules/contrib/aur-update.py | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/bumblebee_status/modules/contrib/aur-update.py b/bumblebee_status/modules/contrib/aur-update.py index c175a63..14bee6b 100644 --- a/bumblebee_status/modules/contrib/aur-update.py +++ b/bumblebee_status/modules/contrib/aur-update.py @@ -1,16 +1,7 @@ """Check updates for AUR. Requires the following packages: - * yay (used as default) - -Note - You can replace yay by changing the "yay -Qum" -command for your preferred AUR helper. Few examples: - -paru -Qum -pikaur -Qua -rua upgrade --printonly -trizen -Su --aur --quiet -yay -Qum + * yay (https://github.com/Jguer/yay) contributed by `ishaanbhimwal `_ - many thanks! """ @@ -49,12 +40,13 @@ class Module(core.module.Module): ) if code == 0: - self.__packages = len(result.strip().split("\n")) - elif code == 2: - self.__packages = 0 + if result == "": + self.__packages = 0 + else: + self.__packages = len(result.strip().split("\n")) else: self.__error = True - logging.error("yay -Qum exited with {}: {}".format(code, result)) + logging.error("aur-update exited with {}: {}".format(code, result)) def state(self, widget): if self.__error: From 1fc4139b7ce0376b80b3f3f71a6f8367609bbe01 Mon Sep 17 00:00:00 2001 From: ishaan Date: Tue, 15 Mar 2022 15:56:42 +0530 Subject: [PATCH 305/506] add documentation and screenshot for aur-update --- .../modules/contrib/aur-update.py | 2 +- docs/modules.rst | 14 + screenshots/arch-update.png | Bin 0 -> 4952 bytes screenshots/aur-update.png | Bin 0 -> 4718 bytes themes/icons/awesome-fonts.json | 759 +++++++++++++----- 5 files changed, 595 insertions(+), 180 deletions(-) create mode 100644 screenshots/arch-update.png create mode 100644 screenshots/aur-update.png diff --git a/bumblebee_status/modules/contrib/aur-update.py b/bumblebee_status/modules/contrib/aur-update.py index 14bee6b..f1b85ea 100644 --- a/bumblebee_status/modules/contrib/aur-update.py +++ b/bumblebee_status/modules/contrib/aur-update.py @@ -1,6 +1,6 @@ """Check updates for AUR. -Requires the following packages: +Requires the following executable: * yay (https://github.com/Jguer/yay) contributed by `ishaanbhimwal `_ - many thanks! diff --git a/docs/modules.rst b/docs/modules.rst index 413f6cf..ad4a307 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -401,6 +401,20 @@ Requires the following executable: contributed by `lucassouto `_ - many thanks! +.. image:: ../screenshots/arch-update.png + +aur-update +~~~~~~~~~~~ + +Check updates for AUR. + +Requires the following executable: + * yay (https://github.com/Jguer/yay) + +contributed by `ishaanbhimwal `_ - many thanks! + +.. image:: ../screenshots/aur-update.png + battery ~~~~~~~ diff --git a/screenshots/arch-update.png b/screenshots/arch-update.png new file mode 100644 index 0000000000000000000000000000000000000000..42b268ee2c5a5fd373576bb71cac277a54b345e7 GIT binary patch literal 4952 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%0DL>SW97vr)abRtKs_E|*`paL#Hq z0|3ji=}e|nD)A*rm5LT@`)L&=UR7_S&(E`)Y)-9F@hx9`xnAJaYTHyQx-O#L;rTC> z%m4jfe**vnLB9C>vcaeyJ~ZU>`M3VRJ~(h(X|cM4{-x&suMHDIK9`P!y=IHE-q<`k zZ8hy|#F3B3qASsCHa9u>n7=eD3FkJb7v_OzGR6Yo#AMDiENYeV+70(_|MSg0 z<)u!?HdmCx>3A_S%OCIf{MX<(?!m*!&#zoE8VsGDj{5b*-v6Smd`M)iZwfw5P-EtP}Z;&wo8Dk(H&Tw;qrC z@W-QXm%V$~A%8%i)!GVEiLD6BI--coCyDq%4x~3W}iT&W~@a z@OK8sajUU70Px|l0KSbc7G2#OESbzkfdv3+eJQK4YBeFYc3?FN-ycO#lS%j{N`l}! z4#MGT4X+-fVLip&Or^pC0HNIK>AJDi7_R|$$2Qs4&LEUHzc}I*dvJhv*#`h-ld+cb z@A&)+c=YITrNR&dap=&H(D%EbO>1n^3a=JXL_`k!v*1#3p>;bV5#a)iC^Su9zWgb# za}-6-Ul^}#KRh7#0|5YVIP8L@JH0{YaJB=0*C$fWAqc|L;jXu!C^9hA4*n*6$Z-dhz=MEXyGX;&7~A13Nqa03=H(E8~5;& z1aW5k)S2;9%C*Yq2ZY{eqt)Bw$N+#DOyZGwE}K#kq+H%YO_gC7KCs*tg+k$Sxhj?N z*x?a-d$TJ>g?wRprrOK3uf*et-rjBu!(1IM6h%=KRjX7T9{0)P$7C{Du~@uy^A1zj zt2Z#vhvSM$g|R!@OQkX|EuB_-;^Z-_#R35L?>|f=)|;-1#iGmYBuUa}Hl&j2QmKUF zcz1W#(IX!-41-}KG?*_GZ)iNxm+&w_4ObKVm6ynsZ^<4#xP8+R%x|unaujqadai>bUO(`+1cq)C=^Vk!m%8R zp(>hoI2}ihj&*uE=EeGLn_+ZxSffz~0)hE?k=&SJ7>m`cqGEzL)$?yQn`|~K z!?J??UMnlH?ye36j;mDa#ZA5XJR_rrdV9NdS}n)1!JxTt#+FVbMhJ-$AyA#Z1!%v>7P>Y`TW6PSTxGo3`LPAPaYRqx8&U@8~_T1 z!Y{wvx^UsFLM|U2-SE?nrE>ZDzkex-<#3oowdv^>Z{EDEw=6Eb z(dl(VLjyLe)%NB3Lj_XF^whIw<7ZCQjRgRhn3(+Z(~A^EUc7j|-v0dgbfv-wK4Cra zc;dy(?7(2ZUaz}+`O}&`99gNqpiAZQ)t~=;?)F?i9~|tjxp*RCnL;zjWSGrn z0RXR0e3uRYc$fWFi>1BYB9mc!O>t-Ee=`hInVftwe)hE8ZnN9lePW+MuJIo`7uSADVKWV9asc2sE}Ko!w5ItV zKl29yZntwc(On}#;mG}a6WpeSlYJtc&MYmx0YDUTxlAUjFia|yT3A@Tefzh~jnMo| z9tZ^Q-Jf7tmLv$dTvn-6!r{n+2a|JRyE_7b;L6Ghj^j9vBM6$yW#?Zn+`4_YQmORy z_14ei0FcRKys(J`qnK2qKruF$`rGCX>!YBGK8|myf5OGIb^W=CRxD{Ep*$ zzddMF{VEg+0|R|1ibkWWwMV$Lbp9L7jt2?&0+q%0tDUzf}QlXFo0E(jgpA<=An5_Q$-6aT$P_!nKNj7={6teA47t0lR zyQ3RxTIaYDKYi+COKn@*^5)IkJ9qAF-GKcb;kTMjO+A~MdM5NqNW2rurJ~>an&12* zVEYiITq=hAi&jThlkE`2Lh<_bUyLSWhudv1>PdpwdiA>@9G5Q?W6{{c;*w}#Swcdh zhFpPL+C4$<{O0EN?L`g%g+eY8d}FqCG%wbb$jUzL&Xtgm5JjmZO;&d}uq05q_!8P? zvYAw?yI3V8B;GL;t={}ph-O6&tv`=KLPFx*v-$Z>Er=WdR%4-bvby(BLPFwTqSISc zn)O!5)M87CCYO+q w_^^;fE+HWyAtCW0gA5Fh32hP*62Bw<52K8BP;%z9+5i9m07*qoM6N<$g0ikSiU0rr literal 0 HcmV?d00001 diff --git a/themes/icons/awesome-fonts.json b/themes/icons/awesome-fonts.json index f85dfd8..3f971d7 100644 --- a/themes/icons/awesome-fonts.json +++ b/themes/icons/awesome-fonts.json @@ -2,187 +2,489 @@ "defaults": { "separator": "", "padding": " ", - "unknown": { "prefix": "" }, + "unknown": { + "prefix": "" + }, "separator-block-width": 0, "default-separators": false }, - "date": { "prefix": "" }, - "persian_date": { "prefix": "" }, - "time": { "prefix": "" }, - "datetime": { "prefix": "" }, - "datetz": { "prefix": "" }, - "timetz": { "prefix": "" }, - "datetimetz": { "prefix": "" }, - "memory": { "prefix": "" }, - "cpu": { "prefix": "" }, - "cpu2": { - "freq": { "prefix": "" }, - "load": { "prefix": "" }, - "loads": { "prefix": "" }, - "temp": { "prefix": "" }, - "fan": { "prefix": "" } + "date": { + "prefix": "" + }, + "persian_date": { + "prefix": "" + }, + "time": { + "prefix": "" + }, + "datetime": { + "prefix": "" + }, + "datetz": { + "prefix": "" + }, + "timetz": { + "prefix": "" + }, + "datetimetz": { + "prefix": "" + }, + "memory": { + "prefix": "" + }, + "cpu": { + "prefix": "" + }, + "cpu2": { + "freq": { + "prefix": "" + }, + "load": { + "prefix": "" + }, + "loads": { + "prefix": "" + }, + "temp": { + "prefix": "" + }, + "fan": { + "prefix": "" + } + }, + "disk": { + "prefix": "" + }, + "smartstatus": { + "prefix": "" + }, + "dnf": { + "prefix": "" + }, + "apt": { + "prefix": "" + }, + "pacman": { + "prefix": "" + }, + "brightness": { + "prefix": "" + }, + "load": { + "prefix": "" + }, + "layout": { + "prefix": "" + }, + "layout-xkb": { + "prefix": "" + }, + "layout-xkbswitch": { + "prefix": "" }, - "disk": { "prefix": "" }, - "smartstatus": { "prefix": "" }, - "dnf": { "prefix": "" }, - "apt": { "prefix": "" }, - "pacman": { "prefix": "" }, - "brightness": { "prefix": "" }, - "load": { "prefix": "" }, - "layout": { "prefix": "" }, - "layout-xkb": { "prefix": "" }, - "layout-xkbswitch": { "prefix": "" }, "notmuch_count": { - "empty": { "prefix": "\uf0e0" }, - "items": { "prefix": "\uf0e0" } + "empty": { + "prefix": "\uf0e0" + }, + "items": { + "prefix": "\uf0e0" + } }, "portage_status": { - "idle": { "prefix": "" }, - "active": { "prefix": "" } + "idle": { + "prefix": "" + }, + "active": { + "prefix": "" + } }, "todo": { - "empty": { "prefix": "" }, - "items": { "prefix": "" }, - "uptime": { "prefix": "" } + "empty": { + "prefix": "" + }, + "items": { + "prefix": "" + }, + "uptime": { + "prefix": "" + } }, "zpool": { - "poolread": { "prefix": "→ " }, - "poolwrite": { "prefix": "← " }, - "ONLINE": { "prefix": "" }, - "FAULTED": { "prefix": "!" }, - "DEGRADED": { "prefix": "!" } + "poolread": { + "prefix": "→ " + }, + "poolwrite": { + "prefix": "← " + }, + "ONLINE": { + "prefix": "" + }, + "FAULTED": { + "prefix": "!" + }, + "DEGRADED": { + "prefix": "!" + } }, "cmus": { - "playing": { "prefix": "" }, - "paused": { "prefix": "" }, - "stopped": { "prefix": "" }, - "prev": { "prefix": "" }, - "next": { "prefix": "" }, - "shuffle-on": { "prefix": "" }, - "shuffle-off": { "prefix": "" }, - "repeat-on": { "prefix": "" }, - "repeat-off": { "prefix": "" } + "playing": { + "prefix": "" + }, + "paused": { + "prefix": "" + }, + "stopped": { + "prefix": "" + }, + "prev": { + "prefix": "" + }, + "next": { + "prefix": "" + }, + "shuffle-on": { + "prefix": "" + }, + "shuffle-off": { + "prefix": "" + }, + "repeat-on": { + "prefix": "" + }, + "repeat-off": { + "prefix": "" + } }, "gpmdp": { - "playing": { "prefix": "" }, - "paused": { "prefix": "" }, - "stopped": { "prefix": "" }, - "prev": { "prefix": "" }, - "next": { "prefix": "" } + "playing": { + "prefix": "" + }, + "paused": { + "prefix": "" + }, + "stopped": { + "prefix": "" + }, + "prev": { + "prefix": "" + }, + "next": { + "prefix": "" + } }, "playerctl": { - "playing": { "prefix": "" }, - "paused": { "prefix": "" }, - "stopped": { "prefix": "" }, - "prev": { "prefix": "" }, - "next": { "prefix": "" } + "playing": { + "prefix": "" + }, + "paused": { + "prefix": "" + }, + "stopped": { + "prefix": "" + }, + "prev": { + "prefix": "" + }, + "next": { + "prefix": "" + } }, "pasink": { - "muted": { "prefix": "" }, - "unmuted": { "prefix": "" } + "muted": { + "prefix": "" + }, + "unmuted": { + "prefix": "" + } }, "amixer": { - "muted": { "prefix": "" }, - "unmuted": { "prefix": "" } + "muted": { + "prefix": "" + }, + "unmuted": { + "prefix": "" + } }, "pasource": { - "muted": { "prefix": "" }, - "unmuted": { "prefix": "" } + "muted": { + "prefix": "" + }, + "unmuted": { + "prefix": "" + } }, "kernel": { "prefix": "\uf17c" }, "nic": { - "wireless-up": { "prefix": "" }, - "wireless-down": { "prefix": "" }, - "wired-up": { "prefix": "" }, - "wired-down": { "prefix": "" }, - "tunnel-up": { "prefix": "" }, - "tunnel-down": { "prefix": "" } + "wireless-up": { + "prefix": "" + }, + "wireless-down": { + "prefix": "" + }, + "wired-up": { + "prefix": "" + }, + "wired-down": { + "prefix": "" + }, + "tunnel-up": { + "prefix": "" + }, + "tunnel-down": { + "prefix": "" + } }, "bluetooth": { - "ON": { "prefix": "" }, - "OFF": { "prefix": "" }, - "?": { "prefix": "" } + "ON": { + "prefix": "" + }, + "OFF": { + "prefix": "" + }, + "?": { + "prefix": "" + } }, "bluetooth2": { - "ON": { "prefix": "" }, - "warning": { "prefix": "" }, - "critical": { "prefix": "" } + "ON": { + "prefix": "" + }, + "warning": { + "prefix": "" + }, + "critical": { + "prefix": "" + } }, "battery-upower": { - "charged": { "prefix": "", "suffix": "" }, - "AC": { "suffix": "" }, - "charging": { - "prefix": ["", "", "", "", ""], + "charged": { + "prefix": "", "suffix": "" }, - "discharging-10": { "prefix": "", "suffix": "" }, - "discharging-25": { "prefix": "", "suffix": "" }, - "discharging-50": { "prefix": "", "suffix": "" }, - "discharging-80": { "prefix": "", "suffix": "" }, - "discharging-100": { "prefix": "", "suffix": "" }, - "unlimited": { "prefix": "", "suffix": "" }, - "estimate": { "prefix": "" }, - "unknown-10": { "prefix": "", "suffix": "" }, - "unknown-25": { "prefix": "", "suffix": "" }, - "unknown-50": { "prefix": "", "suffix": "" }, - "unknown-80": { "prefix": "", "suffix": "" }, - "unknown-100": { "prefix": "", "suffix": "" } + "AC": { + "suffix": "" + }, + "charging": { + "prefix": [ + "", + "", + "", + "", + "" + ], + "suffix": "" + }, + "discharging-10": { + "prefix": "", + "suffix": "" + }, + "discharging-25": { + "prefix": "", + "suffix": "" + }, + "discharging-50": { + "prefix": "", + "suffix": "" + }, + "discharging-80": { + "prefix": "", + "suffix": "" + }, + "discharging-100": { + "prefix": "", + "suffix": "" + }, + "unlimited": { + "prefix": "", + "suffix": "" + }, + "estimate": { + "prefix": "" + }, + "unknown-10": { + "prefix": "", + "suffix": "" + }, + "unknown-25": { + "prefix": "", + "suffix": "" + }, + "unknown-50": { + "prefix": "", + "suffix": "" + }, + "unknown-80": { + "prefix": "", + "suffix": "" + }, + "unknown-100": { + "prefix": "", + "suffix": "" + } }, "battery": { - "charged": { "prefix": "", "suffix": "" }, - "AC": { "suffix": "" }, - "charging": { - "prefix": ["", "", "", "", ""], + "charged": { + "prefix": "", "suffix": "" }, - "discharging-10": { "prefix": "", "suffix": "" }, - "discharging-25": { "prefix": "", "suffix": "" }, - "discharging-50": { "prefix": "", "suffix": "" }, - "discharging-80": { "prefix": "", "suffix": "" }, - "discharging-100": { "prefix": "", "suffix": "" }, - "unlimited": { "prefix": "", "suffix": "" }, - "estimate": { "prefix": "" }, - "unknown-10": { "prefix": "", "suffix": "" }, - "unknown-25": { "prefix": "", "suffix": "" }, - "unknown-50": { "prefix": "", "suffix": "" }, - "unknown-80": { "prefix": "", "suffix": "" }, - "unknown-100": { "prefix": "", "suffix": "" } + "AC": { + "suffix": "" + }, + "charging": { + "prefix": [ + "", + "", + "", + "", + "" + ], + "suffix": "" + }, + "discharging-10": { + "prefix": "", + "suffix": "" + }, + "discharging-25": { + "prefix": "", + "suffix": "" + }, + "discharging-50": { + "prefix": "", + "suffix": "" + }, + "discharging-80": { + "prefix": "", + "suffix": "" + }, + "discharging-100": { + "prefix": "", + "suffix": "" + }, + "unlimited": { + "prefix": "", + "suffix": "" + }, + "estimate": { + "prefix": "" + }, + "unknown-10": { + "prefix": "", + "suffix": "" + }, + "unknown-25": { + "prefix": "", + "suffix": "" + }, + "unknown-50": { + "prefix": "", + "suffix": "" + }, + "unknown-80": { + "prefix": "", + "suffix": "" + }, + "unknown-100": { + "prefix": "", + "suffix": "" + } }, "battery_all": { - "charged": { "prefix": "", "suffix": "" }, - "AC": { "suffix": "" }, - "charging": { - "prefix": ["", "", "", "", ""], + "charged": { + "prefix": "", "suffix": "" }, - "discharging-10": { "prefix": "", "suffix": "" }, - "discharging-25": { "prefix": "", "suffix": "" }, - "discharging-50": { "prefix": "", "suffix": "" }, - "discharging-80": { "prefix": "", "suffix": "" }, - "discharging-100": { "prefix": "", "suffix": "" }, - "unlimited": { "prefix": "", "suffix": "" }, - "estimate": { "prefix": "" }, - "unknown-10": { "prefix": "", "suffix": "" }, - "unknown-25": { "prefix": "", "suffix": "" }, - "unknown-50": { "prefix": "", "suffix": "" }, - "unknown-80": { "prefix": "", "suffix": "" }, - "unknown-100": { "prefix": "", "suffix": "" } + "AC": { + "suffix": "" + }, + "charging": { + "prefix": [ + "", + "", + "", + "", + "" + ], + "suffix": "" + }, + "discharging-10": { + "prefix": "", + "suffix": "" + }, + "discharging-25": { + "prefix": "", + "suffix": "" + }, + "discharging-50": { + "prefix": "", + "suffix": "" + }, + "discharging-80": { + "prefix": "", + "suffix": "" + }, + "discharging-100": { + "prefix": "", + "suffix": "" + }, + "unlimited": { + "prefix": "", + "suffix": "" + }, + "estimate": { + "prefix": "" + }, + "unknown-10": { + "prefix": "", + "suffix": "" + }, + "unknown-25": { + "prefix": "", + "suffix": "" + }, + "unknown-50": { + "prefix": "", + "suffix": "" + }, + "unknown-80": { + "prefix": "", + "suffix": "" + }, + "unknown-100": { + "prefix": "", + "suffix": "" + } }, "caffeine": { - "activated": { "prefix": " " }, - "deactivated": { "prefix": " " } + "activated": { + "prefix": " " + }, + "deactivated": { + "prefix": " " + } }, "xrandr": { - "on": { "prefix": " " }, - "off": { "prefix": " " }, - "refresh": { "prefix": "" } + "on": { + "prefix": " " + }, + "off": { + "prefix": " " + }, + "refresh": { + "prefix": "" + } }, "redshift": { - "day": { "prefix": "" }, - "night": { "prefix": "" }, - "transition": { "prefix": "" } + "day": { + "prefix": "" + }, + "night": { + "prefix": "" + }, + "transition": { + "prefix": "" + } }, "docker_ps": { "prefix": "" @@ -191,32 +493,67 @@ "prefix": "" }, "sensors2": { - "temp": { "prefix": "" }, - "fan": { "prefix": "" }, - "cpu": { "prefix": "" } + "temp": { + "prefix": "" + }, + "fan": { + "prefix": "" + }, + "cpu": { + "prefix": "" + } }, "traffic": { - "rx": { "prefix": "" }, - "tx": { "prefix": "" } + "rx": { + "prefix": "" + }, + "tx": { + "prefix": "" + } }, "network_traffic": { - "rx": { "prefix": "" }, - "tx": { "prefix": "" } + "rx": { + "prefix": "" + }, + "tx": { + "prefix": "" + } }, "mpd": { - "playing": { "prefix": "" }, - "paused": { "prefix": "" }, - "stopped": { "prefix": "" }, - "prev": { "prefix": "" }, - "next": { "prefix": "" }, - "shuffle-on": { "prefix": "" }, - "shuffle-off": { "prefix": "" }, - "repeat-on": { "prefix": "" }, - "repeat-off": { "prefix": "" } + "playing": { + "prefix": "" + }, + "paused": { + "prefix": "" + }, + "stopped": { + "prefix": "" + }, + "prev": { + "prefix": "" + }, + "next": { + "prefix": "" + }, + "shuffle-on": { + "prefix": "" + }, + "shuffle-off": { + "prefix": "" + }, + "repeat-on": { + "prefix": "" + }, + "repeat-off": { + "prefix": "" + } }, "arch-update": { "prefix": " " }, + "aur-update": { + "prefix": " " + }, "github": { "prefix": "  " }, @@ -224,21 +561,41 @@ "prefix": "  " }, "spotify": { - "song": { "prefix": "" }, - "playing": { "prefix": "" }, - "paused": { "prefix": "" }, - "prev": { "prefix": "" }, - "next": { "prefix": "" } + "song": { + "prefix": "" + }, + "playing": { + "prefix": "" + }, + "paused": { + "prefix": "" + }, + "prev": { + "prefix": "" + }, + "next": { + "prefix": "" + } }, "publicip": { "prefix": "  " }, "weather": { - "clouds": { "prefix": "" }, - "rain": { "prefix": "" }, - "snow": { "prefix": "" }, - "clear": { "prefix": "" }, - "thunder": { "prefix": "" } + "clouds": { + "prefix": "" + }, + "rain": { + "prefix": "" + }, + "snow": { + "prefix": "" + }, + "clear": { + "prefix": "" + }, + "thunder": { + "prefix": "" + } }, "taskwarrior": { "prefix": "  " @@ -249,30 +606,56 @@ } }, "git": { - "main": { "prefix": "" }, - "new": { "prefix": "" }, - "modified": { "prefix": "" }, - "deleted": { "prefix": "" } + "main": { + "prefix": "" + }, + "new": { + "prefix": "" + }, + "modified": { + "prefix": "" + }, + "deleted": { + "prefix": "" + } }, "dunst": { - "muted": { "prefix": "" }, - "unmuted": { "prefix": "" } + "muted": { + "prefix": "" + }, + "unmuted": { + "prefix": "" + } }, "dunstctl": { - "muted": { "prefix": "" }, - "unmuted": { "prefix": "" }, - "unknown": { "prefix": "" } + "muted": { + "prefix": "" + }, + "unmuted": { + "prefix": "" + }, + "unknown": { + "prefix": "" + } }, "rofication": { - "prefix": "" + "prefix": "" }, "twmn": { - "muted": { "prefix": "" }, - "unmuted": { "prefix": "" } + "muted": { + "prefix": "" + }, + "unmuted": { + "prefix": "" + } }, "pihole": { - "enabled": { "prefix": "" }, - "disabled": { "prefix": "" } + "enabled": { + "prefix": "" + }, + "disabled": { + "prefix": "" + } }, "vpn": { "prefix": "" @@ -287,12 +670,22 @@ "prefix": "" }, "pomodoro": { - "off": { "prefix": "" }, - "paused": { "prefix": "" }, - "work": { "prefix": "" }, - "break": { "prefix": "" } + "off": { + "prefix": "" + }, + "paused": { + "prefix": "" + }, + "work": { + "prefix": "" + }, + "break": { + "prefix": "" + } + }, + "hddtemp": { + "prefix": "" }, - "hddtemp": { "prefix": "" }, "octoprint": { "prefix": " " }, @@ -300,10 +693,18 @@ "prefix": "" }, "speedtest": { - "running": { "prefix": ["\uf251", "\uf252", "\uf253"] }, - "not-running": { "prefix": "\uf144" } + "running": { + "prefix": [ + "\uf251", + "\uf252", + "\uf253" + ] + }, + "not-running": { + "prefix": "\uf144" + } }, "thunderbird": { "prefix": "" } -} +} \ No newline at end of file From 9b61eee725c794ad4dd1e7319ae1b3450afb7c08 Mon Sep 17 00:00:00 2001 From: ishaan Date: Tue, 15 Mar 2022 17:32:45 +0530 Subject: [PATCH 306/506] add better screenshots --- screenshots/arch-update.png | Bin 4952 -> 2409 bytes screenshots/aur-update.png | Bin 4718 -> 2214 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/screenshots/arch-update.png b/screenshots/arch-update.png index 42b268ee2c5a5fd373576bb71cac277a54b345e7..45613fcb330904d27e1c209f2473362a4247faa2 100644 GIT binary patch literal 2409 zcmV-v36}PWP)t#{L00009a7bBm000ie z000ie0hKEb8vpZ*+1Z`eot<{pY4_3DS?qR3t9807wJK;8Q2|BX@iqWi@N0JuG#s-{+xb&8^s z;jmd6bxLHI%x1T@bU$k9?!$4M>a@g99+XDYtVkAcIaC7xnw*{I8{jy8x3ztEe1haa zMJg7(zh_5kY_v=)TqX@kQ^tmch#R|lQ5x;#21jg!{Db`^6*cu#!%vu#M-S+R4UZ>H zRKtHC1VL7&B!&fxsRjU`Y$Ekty#t0ZvND^uHx&l){NhU$AM>kyB_AI<`1`~A)8Z9m z(^G-5Sf@@L5(n{#p+ppP>ck<8#q#%hUV?)JzkPQvRedHt*jIA=?HyG0Wq=?^YHalT zd*1l)m+w$*bC4Dn6D|p%ssI2W;BjWcqOD)oJupbJ20$tnMutksN=LsQ0765=2!ilA z*jvSgd=5r-_^A;h6i}sSLM9e**(`AoUn255+C4QdBE=SFr>)P;XzA&v8s;D|YI$t< zGOGFnjXKU-TlD=f+}zveZw-JVTuvTNP1`>`ck!bmKj*N$-)RONm5PPtDI3}C`8j#? zKv`u?ZfXLPL7y~R&tADPW;6kSR4m+CSP&5^owhq_THAg8iU|28O^z&Bhyc>7A39rp z(>!I1kVy_~+bH02P!#?6*g*hj>UvaKRt^A624kx>SFKba2=wTN&Q(+{_z39Wmfns^s7xzQty*sCiIX z-%MO06^pj6S}7F?N5?16T)m+mG5Ds~yLlZ-qy7EgSA5D`e1ET@qUJ%AT(&x61=Ti- z=&;b#m}sg>A|)=u8x{kj3+x>Tg6t^NXw)fWktil8C51X(F*;#1&s(%U14%KFfBfgy zZjWbuZpQxCHvHj>^8oP9Ya0hf41fKsl*ynU-nqr+Y_wP}Rn_zk4%1O|-{zuC1vzJ~ z+|Z90e)E?vQsZ9PQCRTn&q{sH0I++*YLS5VVd+=aDcg=B?OVl#e?DJEc9>0ig5qp> zB}$`hUz6_(i<76%0l=vfhjtZd|8(|WMvIk!q5zPSlCWWAW@%Y@uYQQn<ocb|N9 z^5+*Wjhihc>$JN!tp4cqIiE>4VxS?2Z#WNbj|Tu~NPu(YY{I31_YNPDAinoTxycD) zzIRg(h&o+1!R^xWN?>+4F1&200_4uAQ_<`!%=b@&#jhL>gp(R2l2^R1} zgaUbp_+sTB(aU}LFCgJE3C3de=!d#=gFFr<51|%rH|m-u%@!QT9WECDWG2SlZftGu(>YwO z$41i#si?22Zy7V1JRVQ&!_F{?-#Wj)*Kn=&DJtk67eY|o+CFYJQ&oSw*@BLu1?rS* z_Zuv>z{yjs^s6PEE>~G)^}W^(04V*oVr}A{_=TCOZ*JZtU$QVb4gfUp zJ{&?k<&$W$Ih@`Pb_b5*0v;Ct+#Zk7;!R7BjWdrb4-v1=%an_58bLJI}D9ICJzN%&e$Bsjj<Kg6N0F5jn8Tw!{7)RdR z_Q{!V0KhOl`QGkrzL+3bwos;;&9u|$_IQK>K2bLaxLmi}J?->HgNV;1J_xuNf*?kV z6#+nLG!C0Z{Qr2QQuh}-w_Lt+|MbOj500;hi&>wW?lS?7Q!?|o(S+mp$-?%bE zz~%6{oHd!Mwm!XYZ_=n!cpQw5q8nCb>V`+hP3FfYvwql6oS%)NC=bI{rKb{}G#ZVE zvB!-j96%xpTAQVsnFClnO4!CW~AQ%Swu;hydVnyTAPIy2Un4 z(HEpgKa})Jyu)lDr)L+95+wdUSFI4YF<`Un5?F?BQi{Sr|BUvg~>97qO{EgIUDn{ z5~CxkTiOAjqfaLb7HwOdSFFj-OHBlTwnw^|W%;uq--!@J%R_Ar2c?r?v)jMERz+6+mmn`^7he4I4Gm*VCZ&@x zXX%?5wOp~xuX(k^Vx_HL*E2XoR^|)~0LW0rQl*!~iBCT7*88n{i5E*lymUsx>h bf6IRWu5)CYO)!6P00000NkvXXu0mjfSYUuV literal 4952 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 literal 4718 zcmV-!5|QnRP)y0*A)h4t`NJtzcVk?16I#F+FR^)9uv!=}; zv`R=w93&E};bbBzFg7J}TCLR^EJCYkb{{G&9!G4k; z>g}ts_|(&9%gf@Q?EfbtBf}#j!?|=VR=#!9WK(Uw)2j2q62^|2iwaDdih1Kps z(OtE^&@^@N;(4uBq++MjYcE_lJ3Bk~aN;q`u|oHILZwoD{q=tWz}26x2ZBL?*Jia| z`s^YAeDlqB>2zA)mDrYnf&QaM4+Frxdk9nsMI7CIhdoTRcrKPuzA5RqulG=9#P0>Qr zdj-c8CX0K|7^fE`R_1zW_ic!)TgvIh|eIoit5<^2zyc|M9=? zGE^i<^2m`fx7&pv2*+`$R9d4^3kU559LI+IOXX5Y=-7y8bse$TTuLP=G`EP^Y_i*J zLTdwth6ZdlD~cijAeYN~db+;+@(Mw2$l)bgg{BXd$nADJiXzciESX56C~9wS_VQ>3 z4j`RQr_wllhhvEcse^hLQ^ZyGVaV+1GqRZZ?^PzWt<-FHAgqboJ`>YuB#FVyl>JS5>V>tb~2JY*T57FkIK^EozNU zVB8sIvq@;GBbi7(oP6wZJG;9&g}ycHb{j>J+3fcBs`ty+U;ja+qQCp@hfF4W=+H+_ zr=6mxN`;BVVzaYzUaw#H0ssJTINE!9x{U^d0#_6Y1)nc4_2enbibs_gChKrJU2d0N zuf=g30LkZbp>Sk&W-hk6T0he5cJ=l5(iBNjB#NK_U^E#1`q#hIkGXy8?!w|yy#)X; z40U(7yShAWY7MT$^Z7zJ9GRJZA$b2CgJCG2paz2>6vcXbd+c@_ilF-!NV6Q9PG_{* zHiE!~zBTj)-G%e#Pz1U0>#dc@iqO4NUe%QwFmzXoQ=+WKLaAi4-^&J(t2KJ9e$VcC z6tz`0{_^#_&*zV=u5KZ60MO~Q2VdkoI-Ty^*)y6p4gV*GVK$r9X0y)B%szbhsD31Z zAjgj%>Fw>QSxA!X>FILV+h5Nw)D5hHBIuv~^c6*INT6t%?xN{VPsiOm_vRNCYxcG_ zjb+Q*t0*K%UbuMHpx4(d6h(SGZjZ-(@BYNgxmR^#cZk($mC0lbQwfD42!b#St57H` zR&yXAETQiqxIzv9m2$(nRCiY=e`32kJ$oqfdJse;UfvC2QyYjSdbD)P~hyId*y3XEf^PUcL$i!?|2e zp->nM`jOFLgI<5)g)Oe9007OpB$oDMqx1Oj1(VE`Z;4%=*2 zhr_XdB3IxFf+PVTkxUAGYm`dW%3t}tuCz5zHk*uweM0M&R7I{ND3i^#CsBpbU^sVv zTrlnIn9AN&U-uiiTz>7^4ZlCY|GByvzj^Zxjw`KJ^T_CMP2>oI3=a8ERa z(wNUxW@cttmN|CpX!Y$j@^E6ZKCCSi3O=7d9FF|?>p$R1+~IQ6CR>(eYqw60WdVQ# zZtJz;;o*xFJ=Ki ztyb%FTHdJBYx$!0^XD%F+I#tOzE~{QTWW|VuH|w$5DEc+M!oX_8-gHRU7Y|hH#b*P zc#WserUAfcGVby_H5d&9K>$FlLct$U6sb4tO$`VE&}h^ejasMIx?GO)=g;=_^#H*0 z=hNwQMmTznVxf5V&b>RgfBPW1Gh)#o+n}v&3#v1DIJ9hcbYU|2MsI6i7X(&+gp5TOaWrKq6upT_h6W4|F=+>S`PSblU1Q9SCf=BLD!)vXMyC;b<4w zx1d-k0)T86Uz}Fg#tZoE8wP5=;%#ctiaYcS}>$4}SWYoI7{di-QEncAyL?|vZK zx>GEc7^Z?@GJ>c+PKQ5WSyogtnT&X=EP^0Tr`_G*Ht6*Pp%mOncN2tBWVJqe<@3)r zdNzP78y2XWfnPj}#}oCny?8vX)~K9L$CD>d*N5&Mzy5m5>uoK4J>baX3TwM3;Cs!N zw3=l7S}7I_q2L>9d&iDb2)ElMG;PG|_viEZ2M;FdyYQm1*wvq}mrA8vF30ym)xBaJ z6hRMvJbLx&b)jSLSqq0mKghaK7&UPX;wXk;F%0DL>SW97vr)abRtKs_E|*`paL#Hq z0|3ji=}e|nD)A*rm5LT@`)L&=UR7_S&(E`)Y)-9F@hx9`xnAJaYTHyQx-O#L;rTC> z%m4jfe**vnLB9C>vcaeyJ~ZU>`M3VRJ~(h(X|cM4{-x&suMHDIK9`P!y=IHE-q<`k zZ8hy|#F3B3qASsCHa9u>n7=eD3FkJb7v_OzGR6Yo#AMDiENYeV+70(_|MSg0 z<)u!?HdmCx>3A_S%OCIf{MX<(?!m*!&#zoE8VsGDj{5b*-v6Smd`M)iZwfw5P-EtP}Z;&wo8Dk(H&Tw;qrC z@W-QXm%V$~A%8%i)!GVEiLD6BI--coCyDq%4x~3W}iT&W~@a z@OK8sajUU70Px|l0KSbc7G2#OESbzkfdv3+eJQK4YBeFYc3?FN-ycO#lS%j{N`l}! z4#MGT4X+-fVLip&Or^pC0HNIK>AJDi7_R|$$2Qs4&LEUHzc}I*dvJhv*#`h-ld+cb z@A&)+c=YITrNR&dap=&H(D%EbO>1n^3a=JXL_`k!v*1#3p>;bV5#a)iC^Su9zWgb# za}-6-Ul^}#KRh7#0|5YVIP8L@JH0{YaJB=0*C$fWAqc|L;jXu!C^9hA4*n*6$Z-dhz=MEXyGX;&7~A13Nqa03=H(E8~5;& z1aW5k)S2;9%C*Yq2ZY{eqt)Bw$N+#DOyZGwE}K#kq+H%YO_gC7KCs*tg+k$Sxhj?N z*x?a-d$TJ>g?wRprrOK3uf*et-rjBu!(1IM6h%=KRjX7T9{0)P$7C{Du~@uy^A1zj zt2Z#vhvSM$g|R!@OQkX|EuB_-;^Z-_#R35L?>|f=)|;-1#iGmYBuUa}Hl&j2QmKUF zcz1W#(IX!-41-}KG?*_GZ)iNxm+&w_4ObKVm6ynsZ^<4#xP8+R%x|unaujqadai>bUO(`+1cq)C=^Vk!m%8R zp(>hoI2}ihj&*uE=EeGLn_+ZxSffz~0)hE?k=&SJ7>m`cqGEzL)$?yQn`|~K z!?J??UMnlH?ye36j;mDa#ZA5XJR_rrdV9NdS}n)1!JxTt#+FVbMhJ-$AyA#Z1!%v>7P>Y`TW6PSTxGo3`LPAPaYRqx8&U@8~_T1 z!Y{wvx^UsFLM|U2-SE?nrE>ZDzkex-<#3oowdv^>Z{EDEw=6Eb z(dl(VLjyLe)%NB3Lj_XF^whIw<7ZCQjRgRhn3(+Z(~A^EUc7j|-v0dgbfv-wK4Cra zc;dy(?7(2ZUaz}+`O}&`99gNqpiAZQ)t~=;?)F?i9~|tjxp*RCnL;zjWSGrn z0RXR0e3uRYc$fWFi>1BYB9mc!O>t-Ee=`hInVftwe)hE8ZnN9lePW+MuJIo`7uSADVKWV9asc2sE}Ko!w5ItV zKl29yZntwc(On}#;mG}a6WpeSlYJtc&MYmx0YDUTxlAUjFia|yT3A@Tefzh~jnMo| z9tZ^Q-Jf7tmLv$dTvn-6!r{n+2a|JRyE_7b;L6Ghj^j9vBM6$yW#?Zn+`4_YQmORy z_14ei0FcRKys(J`qnK2qKruF$`rGCX>!YBGK8|myf5OGIb^W=CRxD{Ep*$ zzddMF{VEg+0|R|1ibkWWwMV$Lbp9L7jt2?&0+q%0tDUzf}QlXFo0E(jgpA<=An5_Q$-6aT$P_!nKNj7={6teA47t0lR zyQ3RxTIaYDKYi+COKn@*^5)IkJ9qAF-GKcb;kTMjO+A~MdM5NqNW2rurJ~>an&12* zVEYiITq=hAi&jThlkE`2Lh<_bUyLSWhudv1>PdpwdiA>@9G5Q?W6{{c;*w}#Swcdh zhFpPL+C4$<{O0EN?L`g%g+eY8d}FqCG%wbb$jUzL&Xtgm5JjmZO;&d}uq05q_!8P? zvYAw?yI3V8B;GL;t={}ph-O6&tv`=KLPFx*v-$Z>Er=WdR%4-bvby(BLPFwTqSISc zn)O!5)M87CCYO+q w_^^;fE+HWyAtCW0gA5Fh32hP*62Bw<52K8BP;%z9+5i9m07*qoM6N<$g0ikSiU0rr From 82fa347f2c734fee654459c4eb75c604a9060e48 Mon Sep 17 00:00:00 2001 From: Bernhard B Date: Thu, 24 Mar 2022 21:19:52 +0100 Subject: [PATCH 307/506] extended pulseaudio module * added possibility to show currently selected default device in the statusbar (default: off) * allows to override the left mouse button click with a different action (e.g open popup menu to change the current default device) --- bumblebee_status/modules/core/pulseaudio.py | 69 ++++++++++++++++++--- 1 file changed, 62 insertions(+), 7 deletions(-) diff --git a/bumblebee_status/modules/core/pulseaudio.py b/bumblebee_status/modules/core/pulseaudio.py index 120470f..1213fe9 100644 --- a/bumblebee_status/modules/core/pulseaudio.py +++ b/bumblebee_status/modules/core/pulseaudio.py @@ -11,6 +11,20 @@ Parameters: Note: If the left and right channels have different volumes, the limit might not be reached exactly. * pulseaudio.showbars: 1 for showing volume bars, requires --markup=pango; 0 for not showing volume bars (default) + * pulseaudio.showdevicename: If set to 'true' (default is 'false'), the currently selected default device is shown. + Per default, the sink/source name returned by "pactl list sinks short" is used as display name. + + As this name is usually not particularly nice (e.g "alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo"), + its possible to map the name to more a user friendly name. + + e.g to map "alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo" to the name "Headset", add the following + bumblebee-status config entry: pulseaudio.alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo=Headset + + Furthermore its possible to specify individual (unicode) icons for all sinks/sources. e.g in order to use the icon 🎧 for the + "alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo" sink, add the following bumblebee-status config entry: + pulseaudio.icon.alsa_output.usb-Logitech_Logitech_USB_Headset-00.analog-stereo=🎧 + * Per default a left mouse button click mutes/unmutes the device. In case you want to open a dropdown menu to change the current + default device add the following config entry to your bumblebee-status config: pulseaudio.left-click=select_default_device_popup Requires the following executable: * pulseaudio @@ -20,6 +34,7 @@ Requires the following executable: import re import logging +import functools import core.module import core.widget @@ -28,11 +43,12 @@ import core.input import util.cli import util.graph import util.format +import util.popup class Module(core.module.Module): def __init__(self, config, theme, channel): - super().__init__(config, theme, core.widget.Widget(self.volume)) + super().__init__(config, theme, core.widget.Widget(self.display)) if util.format.asbool(self.parameter("autostart", False)): util.cli.execute("pulseaudio --start", ignore_errors=True) @@ -48,7 +64,11 @@ class Module(core.module.Module): self._mute = False self._failed = False self._channel = channel + self.__selected_default_device = None self._showbars = util.format.asbool(self.parameter("showbars", 0)) + self.__show_device_name = util.format.asbool( + self.parameter("showdevicename", False) + ) self._patterns = [ {"expr": "Name:", "callback": (lambda line: False)}, @@ -138,19 +158,19 @@ class Module(core.module.Module): logging.error("no pulseaudio device found") return "n/a" - def volume(self, widget): + def display(self, widget): if self._failed == True: return "n/a" + + vol = None if int(self._mono) > 0: vol = "{}%".format(self._mono) if self._showbars: vol = "{} {}".format(vol, util.graph.hbar(float(self._mono))) - return vol elif self._left == self._right: vol = "{}%".format(self._left) if self._showbars: vol = "{} {}".format(vol, util.graph.hbar(float(self._left))) - return vol else: vol = "{}%/{}%".format(self._left, self._right) if self._showbars: @@ -159,19 +179,31 @@ class Module(core.module.Module): util.graph.hbar(float(self._left)), util.graph.hbar(float(self._right)), ) - return vol + + output = vol + if self.__show_device_name: + friendly_name = self.parameter( + self.__selected_default_device, self.__selected_default_device + ) + icon = self.parameter("icon." + self.__selected_default_device, "") + output = ( + icon + " " + friendly_name + " | " + vol + if icon != "" + else friendly_name + " | " + vol + ) + return output def update(self): try: self._failed = False channel = "sinks" if self._channel == "sink" else "sources" - device = self._default_device() + self.__selected_default_device = self._default_device() result = util.cli.execute("pactl list {}".format(channel)) found = False for line in result.split("\n"): - if "Name: {}".format(device) in line: + if "Name: {}".format(self.__selected_default_device) in line: found = True continue if found is False: @@ -189,6 +221,29 @@ class Module(core.module.Module): else: raise e + def __on_sink_selected(self, sink_name): + util.cli.execute("pactl set-default-{} {}".format(self._channel, sink_name)) + + def select_default_device_popup(self, widget): + channel = "sinks" if self._channel == "sink" else "sources" + result = util.cli.execute("pactl list {} short".format(channel)) + + menu = util.popup.menu() + lines = result.splitlines() + for line in lines: + info = line.split("\t") + try: + friendly_name = self.parameter(info[1], info[1]) + menu.add_menuitem( + friendly_name, + callback=functools.partial(self.__on_sink_selected, info[1]), + ) + except: + logging.exception("Couldn't parse {}".format(channel)) + pass + + menu.show(widget) + def state(self, widget): if self._mute: return ["warning", "muted"] From 4d3de3be0446877caa436bbc571b449fbb7fba71 Mon Sep 17 00:00:00 2001 From: Bernhard B Date: Fri, 25 Mar 2022 19:29:37 +0100 Subject: [PATCH 308/506] handle util.popup ImportError gracefully * util.popup requires tkinter to be installed (in order to display popups). As most people will probably use the default configuration of the pulseaudio module (where the popup is disabled) and in order to avoid breaking existing setups, we catch import errors and just log them. --- bumblebee_status/modules/core/pulseaudio.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bumblebee_status/modules/core/pulseaudio.py b/bumblebee_status/modules/core/pulseaudio.py index 1213fe9..906e1dd 100644 --- a/bumblebee_status/modules/core/pulseaudio.py +++ b/bumblebee_status/modules/core/pulseaudio.py @@ -43,8 +43,11 @@ import core.input import util.cli import util.graph import util.format -import util.popup +try: + import util.popup +except ImportError as e: + logging.warning("Couldn't import util.popup: %s. Popups won't work!", e) class Module(core.module.Module): def __init__(self, config, theme, channel): From 8501c406aff6a8f6a87f4c504e7a7f3bfab5a02e Mon Sep 17 00:00:00 2001 From: arivarton Date: Fri, 8 Apr 2022 14:15:28 +0200 Subject: [PATCH 309/506] Google calendar module. --- bumblebee_status/modules/contrib/gcalendar.py | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 bumblebee_status/modules/contrib/gcalendar.py diff --git a/bumblebee_status/modules/contrib/gcalendar.py b/bumblebee_status/modules/contrib/gcalendar.py new file mode 100644 index 0000000..e1ce219 --- /dev/null +++ b/bumblebee_status/modules/contrib/gcalendar.py @@ -0,0 +1,113 @@ +"""Displays first upcoming event in google calendar. + +Events that are set as 'all-day' will not be shown. + +Requires credentials.json from a google api application where the google calendar api is installed. +On first time run the browser will open and google will ask for permission for this app to access the google calendar and then save a .gcalendar_token.json file to the credentials_path directory which stores this permission. + +A refresh is done every 15 minutes. + +Parameters: + * gcalendar.time_format: Format time output. Defaults to "%H:%M". + * gcalendar.date_format: Format date output. Defaults to "%d.%m.%y". + * gcalendar.credentials_path: Path to credentials.json. Defaults to "~/". + +Requires these pip packages: + * google-api-python-client + * google-auth-httplib2 + * google-auth-oauthlib +""" + +# This import belongs to the google code +from __future__ import print_function + +from dateutil.parser import parse as dtparse + +import core.module +import core.widget +import core.decorators + +import datetime +import os.path + +from google.auth.transport.requests import Request +from google.oauth2.credentials import Credentials +from google_auth_oauthlib.flow import InstalledAppFlow +from googleapiclient.discovery import build +from googleapiclient.errors import HttpError + +class Module(core.module.Module): + @core.decorators.every(minutes=15) + def __init__(self, config, theme): + super().__init__(config, theme, core.widget.Widget(self.first_event)) + self.__time_format = self.parameter("time_format", "%H:%M") + self.__date_format = self.parameter("date_format", "%d.%m.%y") + self.__credentials_path = os.path.expanduser(self.parameter("credentials_path", "~/")) + self.__credentials = os.path.join(self.__credentials_path, 'credentials.json') + self.__token = os.path.join(self.__credentials_path, '.gcalendar_token.json') + + def first_event(self, widget): + SCOPES = ['https://www.googleapis.com/auth/calendar.readonly'] + + """Shows basic usage of the Google Calendar API. + Prints the start and name of the next 10 events on the user's calendar. + """ + creds = None + # The file token.json stores the user's access and refresh tokens, and is + # created automatically when the authorization flow completes for the first + # time. + if os.path.exists(self.__token): + creds = Credentials.from_authorized_user_file(self.__token, SCOPES) + # If there are no (valid) credentials available, let the user log in. + if not creds or not creds.valid: + if creds and creds.expired and creds.refresh_token: + creds.refresh(Request()) + else: + flow = InstalledAppFlow.from_client_secrets_file( + self.__credentials, SCOPES) + creds = flow.run_local_server(port=0) + # Save the credentials for the next run + with open(self.__token, 'w') as token: + token.write(creds.to_json()) + + try: + service = build('calendar', 'v3', credentials=creds) + + # Call the Calendar API + now = datetime.datetime.utcnow().isoformat() + 'Z' # 'Z' indicates UTC time + end = (datetime.datetime.utcnow() + datetime.timedelta(days=7)).isoformat() + 'Z' # 'Z' indicates UTC time + # Get all calendars + calendar_list = service.calendarList().list().execute() + event_list = [] + for calendar_list_entry in calendar_list['items']: + calendar_id = calendar_list_entry['id'] + events_result = service.events().list(calendarId=calendar_id, timeMin=now, + timeMax=end, + singleEvents=True, + orderBy='startTime').execute() + events = events_result.get('items', []) + + if not events: + return 'No upcoming events found.' + + for event in events: + start = dtparse(event['start'].get('dateTime', event['start'].get('date'))) + # Only add to list if not an whole day event + if start.tzinfo: + event_list.append({'date': start, + 'summary': event['summary'], + 'type': event['eventType']}) + sorted_list = sorted(event_list, key=lambda t: t['date']) + + for gevent in sorted_list: + if gevent['date'] >= datetime.datetime.now(datetime.timezone.utc): + if gevent['date'].date() == datetime.datetime.utcnow().date(): + return str('%s %s' % (gevent['date'].astimezone().strftime(f'{self.__time_format}'), gevent['summary'])) + else: + return str('%s %s' % (gevent['date'].astimezone().strftime(f'{self.__date_format} {__time_format}'), gevent['summary'])) + return 'No upcoming events found.' + + except: + return None + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From a48ddbb2c80a42e79ecd78c91519239c48c84645 Mon Sep 17 00:00:00 2001 From: arivarton Date: Fri, 8 Apr 2022 14:23:22 +0200 Subject: [PATCH 310/506] Ran black -t py34 --- bumblebee_status/modules/contrib/gcalendar.py | 90 +++++++++++++------ 1 file changed, 63 insertions(+), 27 deletions(-) diff --git a/bumblebee_status/modules/contrib/gcalendar.py b/bumblebee_status/modules/contrib/gcalendar.py index e1ce219..418f059 100644 --- a/bumblebee_status/modules/contrib/gcalendar.py +++ b/bumblebee_status/modules/contrib/gcalendar.py @@ -36,18 +36,21 @@ from google_auth_oauthlib.flow import InstalledAppFlow from googleapiclient.discovery import build from googleapiclient.errors import HttpError + class Module(core.module.Module): @core.decorators.every(minutes=15) def __init__(self, config, theme): super().__init__(config, theme, core.widget.Widget(self.first_event)) self.__time_format = self.parameter("time_format", "%H:%M") self.__date_format = self.parameter("date_format", "%d.%m.%y") - self.__credentials_path = os.path.expanduser(self.parameter("credentials_path", "~/")) - self.__credentials = os.path.join(self.__credentials_path, 'credentials.json') - self.__token = os.path.join(self.__credentials_path, '.gcalendar_token.json') + self.__credentials_path = os.path.expanduser( + self.parameter("credentials_path", "~/") + ) + self.__credentials = os.path.join(self.__credentials_path, "credentials.json") + self.__token = os.path.join(self.__credentials_path, ".gcalendar_token.json") def first_event(self, widget): - SCOPES = ['https://www.googleapis.com/auth/calendar.readonly'] + SCOPES = ["https://www.googleapis.com/auth/calendar.readonly"] """Shows basic usage of the Google Calendar API. Prints the start and name of the next 10 events on the user's calendar. @@ -64,50 +67,83 @@ class Module(core.module.Module): creds.refresh(Request()) else: flow = InstalledAppFlow.from_client_secrets_file( - self.__credentials, SCOPES) + self.__credentials, SCOPES + ) creds = flow.run_local_server(port=0) # Save the credentials for the next run - with open(self.__token, 'w') as token: + with open(self.__token, "w") as token: token.write(creds.to_json()) try: - service = build('calendar', 'v3', credentials=creds) + service = build("calendar", "v3", credentials=creds) # Call the Calendar API - now = datetime.datetime.utcnow().isoformat() + 'Z' # 'Z' indicates UTC time - end = (datetime.datetime.utcnow() + datetime.timedelta(days=7)).isoformat() + 'Z' # 'Z' indicates UTC time + now = datetime.datetime.utcnow().isoformat() + "Z" # 'Z' indicates UTC time + end = ( + datetime.datetime.utcnow() + datetime.timedelta(days=7) + ).isoformat() + "Z" # 'Z' indicates UTC time # Get all calendars calendar_list = service.calendarList().list().execute() event_list = [] - for calendar_list_entry in calendar_list['items']: - calendar_id = calendar_list_entry['id'] - events_result = service.events().list(calendarId=calendar_id, timeMin=now, - timeMax=end, - singleEvents=True, - orderBy='startTime').execute() - events = events_result.get('items', []) + for calendar_list_entry in calendar_list["items"]: + calendar_id = calendar_list_entry["id"] + events_result = ( + service.events() + .list( + calendarId=calendar_id, + timeMin=now, + timeMax=end, + singleEvents=True, + orderBy="startTime", + ) + .execute() + ) + events = events_result.get("items", []) if not events: - return 'No upcoming events found.' + return "No upcoming events found." for event in events: - start = dtparse(event['start'].get('dateTime', event['start'].get('date'))) + start = dtparse( + event["start"].get("dateTime", event["start"].get("date")) + ) # Only add to list if not an whole day event if start.tzinfo: - event_list.append({'date': start, - 'summary': event['summary'], - 'type': event['eventType']}) - sorted_list = sorted(event_list, key=lambda t: t['date']) + event_list.append( + { + "date": start, + "summary": event["summary"], + "type": event["eventType"], + } + ) + sorted_list = sorted(event_list, key=lambda t: t["date"]) for gevent in sorted_list: - if gevent['date'] >= datetime.datetime.now(datetime.timezone.utc): - if gevent['date'].date() == datetime.datetime.utcnow().date(): - return str('%s %s' % (gevent['date'].astimezone().strftime(f'{self.__time_format}'), gevent['summary'])) + if gevent["date"] >= datetime.datetime.now(datetime.timezone.utc): + if gevent["date"].date() == datetime.datetime.utcnow().date(): + return str( + "%s %s" + % ( + gevent["date"] + .astimezone() + .strftime(f"{self.__time_format}"), + gevent["summary"], + ) + ) else: - return str('%s %s' % (gevent['date'].astimezone().strftime(f'{self.__date_format} {__time_format}'), gevent['summary'])) - return 'No upcoming events found.' + return str( + "%s %s" + % ( + gevent["date"] + .astimezone() + .strftime(f"{self.__date_format} {__time_format}"), + gevent["summary"], + ) + ) + return "No upcoming events found." except: return None + # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From 83d910a7ef097a7cdb8cac2cc19c50188b42769b Mon Sep 17 00:00:00 2001 From: arivarton Date: Fri, 8 Apr 2022 17:09:28 +0200 Subject: [PATCH 311/506] Changed __time_format to self.__time_format. --- bumblebee_status/modules/contrib/gcalendar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bumblebee_status/modules/contrib/gcalendar.py b/bumblebee_status/modules/contrib/gcalendar.py index 418f059..e438bdd 100644 --- a/bumblebee_status/modules/contrib/gcalendar.py +++ b/bumblebee_status/modules/contrib/gcalendar.py @@ -136,7 +136,7 @@ class Module(core.module.Module): % ( gevent["date"] .astimezone() - .strftime(f"{self.__date_format} {__time_format}"), + .strftime(f"{self.__date_format} {self.__time_format}"), gevent["summary"], ) ) From 771e7482d719994e72a0381d449ecc54cf342342 Mon Sep 17 00:00:00 2001 From: Samuel Tebbs Date: Sat, 23 Apr 2022 14:44:35 +0100 Subject: [PATCH 312/506] [modules/playerctl] add 'hide' parameter --- bumblebee_status/modules/contrib/playerctl.py | 15 ++++++++++++--- docs/modules.rst | 1 + 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/bumblebee_status/modules/contrib/playerctl.py b/bumblebee_status/modules/contrib/playerctl.py index 56af426..7fef412 100755 --- a/bumblebee_status/modules/contrib/playerctl.py +++ b/bumblebee_status/modules/contrib/playerctl.py @@ -12,6 +12,7 @@ Parameters: Widget names are: playerctl.song, playerctl.prev, playerctl.pause, playerctl.next * playerctl.args: The arguments added to playerctl. You can check 'playerctl --help' or `its README `_. For example, it could be '-p vlc,%any'. + * playerctl.hide: Hide the widgets when no players are found. Defaults to "false". Parameters are inspired by the `spotify` module, many thanks to its developers! @@ -32,6 +33,8 @@ class Module(core.module.Module): self.background = True + self.__hide = util.format.asbool(self.parameter("hide", "false")); + self.__layout = util.format.aslist( self.parameter( "layout", "playerctl.prev, playerctl.song, playerctl.pause, playerctl.next" @@ -83,14 +86,20 @@ class Module(core.module.Module): if isinstance(callback_options, dict): core.input.register(widget, **callback_options) - def update(self): + def hidden(self): + return self.__hide and self.status() == None + + def status(self): try: playback_status = str(util.cli.execute(self.__cmd + "status 2>&1 || true", shell = True)).strip() if playback_status == "No players found": - playback_status = None + return None + return playback_status except Exception as e: logging.exception(e) - playback_status = None + return None + + def update(self): for widget in self.widgets(): if playback_status: if widget.name == "playerctl.pause": diff --git a/docs/modules.rst b/docs/modules.rst index ad4a307..24046a0 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -1095,6 +1095,7 @@ Parameters: Widget names are: playerctl.song, playerctl.prev, playerctl.pause, playerctl.next * playerctl.args: The arguments added to playerctl. You can check 'playerctl --help' or `its readme `_. For example, it could be '-p vlc,%any'. + * playerctl.hide: Hide the widgets when no players are found. Defaults to "false". Parameters are inspired by the `spotify` module, many thanks to its developers! From d20dacb2dc9bca6f1d2fb8a79665f2175af7d7f5 Mon Sep 17 00:00:00 2001 From: Samuel Tebbs Date: Sun, 24 Apr 2022 16:09:54 +0100 Subject: [PATCH 313/506] Fix missing playback_status --- bumblebee_status/modules/contrib/playerctl.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bumblebee_status/modules/contrib/playerctl.py b/bumblebee_status/modules/contrib/playerctl.py index 7fef412..bd8876c 100755 --- a/bumblebee_status/modules/contrib/playerctl.py +++ b/bumblebee_status/modules/contrib/playerctl.py @@ -100,6 +100,7 @@ class Module(core.module.Module): return None def update(self): + playback_status = self.status() for widget in self.widgets(): if playback_status: if widget.name == "playerctl.pause": From 3da0f08fcb370d298cf0313540d76344c308ff56 Mon Sep 17 00:00:00 2001 From: alexcoder04 Date: Sun, 1 May 2022 12:15:29 +0200 Subject: [PATCH 314/506] arch-update: sleep 1 sec before checking When waking up from suspend, there is sometimes a delay connecting to the network, so arch-update gives an error --- bumblebee_status/modules/contrib/arch-update.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bumblebee_status/modules/contrib/arch-update.py b/bumblebee_status/modules/contrib/arch-update.py index ed9ae58..d8da8fb 100644 --- a/bumblebee_status/modules/contrib/arch-update.py +++ b/bumblebee_status/modules/contrib/arch-update.py @@ -7,6 +7,7 @@ contributed by `lucassouto `_ - many thanks! """ import logging +from time import sleep import core.module import core.widget @@ -35,6 +36,7 @@ class Module(core.module.Module): def update(self): self.__error = False + sleep(1) code, result = util.cli.execute( "checkupdates", ignore_errors=True, return_exitcode=True ) From a6d2ccc666ce84acad96dd8116cbb84ea1e2c1ca Mon Sep 17 00:00:00 2001 From: Timoses Date: Wed, 11 May 2022 19:58:34 +0200 Subject: [PATCH 315/506] Fix logout item using hardcoded command --- bumblebee_status/modules/contrib/system.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bumblebee_status/modules/contrib/system.py b/bumblebee_status/modules/contrib/system.py index cc46ef8..00e80e0 100644 --- a/bumblebee_status/modules/contrib/system.py +++ b/bumblebee_status/modules/contrib/system.py @@ -96,7 +96,7 @@ class Module(core.module.Module): menu.add_menuitem( "log out", callback=functools.partial( - self.__on_command, "Log out", "Log out?", "i3exit logout" + self.__on_command, "Log out", "Log out?", logout_cmd ), ) # don't ask for these From f01179290b7a62548c3439c103616e2c5c744253 Mon Sep 17 00:00:00 2001 From: Sam Tebbs Date: Fri, 13 May 2022 10:21:31 +0100 Subject: [PATCH 316/506] Hide progress module if it's inactive --- bumblebee_status/modules/contrib/progress.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bumblebee_status/modules/contrib/progress.py b/bumblebee_status/modules/contrib/progress.py index 7e148b3..9148acb 100644 --- a/bumblebee_status/modules/contrib/progress.py +++ b/bumblebee_status/modules/contrib/progress.py @@ -29,6 +29,9 @@ class Module(core.module.Module): super().__init__(config, theme, core.widget.Widget(self.get_progress_text)) self.__active = False + def hidden(self): + return not self.__active + def get_progress_text(self, widget): if self.update_progress_info(widget): width = util.format.asint(self.parameter("barwidth", 8)) From 91b1b5e037b761453d55de0383e90efbe6e1505f Mon Sep 17 00:00:00 2001 From: Sam Tebbs Date: Fri, 20 May 2022 09:21:20 +0100 Subject: [PATCH 317/506] Add popup command parameter to the system module --- bumblebee_status/modules/contrib/system.py | 5 +++++ docs/modules.rst | 1 + 2 files changed, 6 insertions(+) diff --git a/bumblebee_status/modules/contrib/system.py b/bumblebee_status/modules/contrib/system.py index 00e80e0..79e8846 100644 --- a/bumblebee_status/modules/contrib/system.py +++ b/bumblebee_status/modules/contrib/system.py @@ -72,6 +72,11 @@ class Module(core.module.Module): util.cli.execute(command) def popup(self, widget): + popupcmd = self.parameter("popupcmd", ""); + if (popupcmd != ""): + util.cli.execute(popupcmd) + return + menu = util.popup.menu() reboot_cmd = self.parameter("reboot", "reboot") shutdown_cmd = self.parameter("shutdown", "shutdown -h now") diff --git a/docs/modules.rst b/docs/modules.rst index 24046a0..69e56fd 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -1413,6 +1413,7 @@ Parameters: * system.lock: specify a command for locking the screen (defaults to 'i3exit lock') * system.suspend: specify a command for suspending (defaults to 'i3exit suspend') * system.hibernate: specify a command for hibernating (defaults to 'i3exit hibernate') + * system.popupcmd: specify a command to run instead of opening the default menu Requirements: tkinter (python3-tk package on debian based systems either you can install it as python package) From 5c166beebfd4b4a7fb8053e2ddb142801368d9cc Mon Sep 17 00:00:00 2001 From: FraSharp Date: Thu, 26 May 2022 13:00:40 +0200 Subject: [PATCH 318/506] [modules]: introduce pamixer module Signed-off-by: FraSharp --- bumblebee_status/modules/contrib/pamixer.py | 88 +++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 bumblebee_status/modules/contrib/pamixer.py diff --git a/bumblebee_status/modules/contrib/pamixer.py b/bumblebee_status/modules/contrib/pamixer.py new file mode 100644 index 0000000..2396949 --- /dev/null +++ b/bumblebee_status/modules/contrib/pamixer.py @@ -0,0 +1,88 @@ +"""get volume level or control it + +Requires the following executable: + * pamixer + +Parameters: + * pamixer.percent_change: How much to change volume by when scrolling on the module (default is 4%) + +heavily based on amixer module +""" +import re + +import core.module +import core.widget +import core.input + +import util.cli +import util.format + +class Module(core.module.Module): + def __init__(self, config, theme): + super().__init__(config, theme, core.widget.Widget(self.volume)) + + self.__level = "volume 0%" + self.__muted = True + self.__change = util.format.asint( + self.parameter("percent_change", "4%").strip("%"), 0, 200 + ) + + events = [ + { + "type": "mute", + "action": self.toggle, + "button": core.input.LEFT_MOUSE, + }, + { + "type": "volume", + "action": self.increase_volume, + "button": core.input.WHEEL_UP, + }, + { + "type": "volume", + "action": self.decrease_volume, + "button": core.input.WHEEL_DOWN, + }, + ] + + for event in events: + core.input.register(self, button=event["button"], cmd=event["action"]) + + def toggle(self, event): + self.set_parameter("--set-level 0") + + def increase_volume(self, event): + self.set_parameter("--increase {}".format(self.__change)) + + def decrease_volume(self, event): + self.set_parameter("--decrease {}".format(self.__change)) + + def set_parameter(self, parameter): + util.cli.execute("pamixer {}".format(parameter)) + + def volume(self, widget): + if self.__level == "volume 0%": + self.__muted = True + return self.__level + m = re.search(r"([\d]+)\%", self.__level) + if m: + if m.group(1) != "0%" in self.__level: + self.__muted = False + return "volume {}%".format(m.group(1)) + else: + return "volume 0%" + + def update(self): + try: + volume = util.cli.execute("pamixer --get-volume-human".format()) + self.__level = volume + self.__muted = False + except Exception as e: + self.__level = "volume 0%" + + def state(self, widget): + if self.__muted: + return ["warning", "muted"] + return ["unmuted"] + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From c001f031a15170069a8fc370d27d50f2012c4397 Mon Sep 17 00:00:00 2001 From: benthetechguy Date: Tue, 7 Jun 2022 19:14:51 -0400 Subject: [PATCH 319/506] Change nonexistent image to link --- docs/FAQ.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/FAQ.rst b/docs/FAQ.rst index eeaff82..8c927cf 100644 --- a/docs/FAQ.rst +++ b/docs/FAQ.rst @@ -29,9 +29,9 @@ didn’t have background color support for the status bar. Some of the icons don’t render correctly ---------------------------------------- -Please check that you have |Font Awesome| installed (version 4). +Please check that you have `Font Awesome`_ installed (version 4). -.. note:: The |Font Awesome| is required for all themes that +.. note:: The `Font Awesome`_ is required for all themes that contain icons (because that is the font that includes these icons). Please refer to your distribution’s package management on how to install them, or get them from their website directly. Also, please note that @@ -52,4 +52,4 @@ Please check that you have |Font Awesome| installed (version 4). # Other # see https://github.com/gabrielelana/awesome-terminal-fonts -.. |Font Awesome| image:: https://fontawesome.com/ +.. _Font Awesome: https://fontawesome.com/ From 4afb8d863674509df5a42020b5e782c3f2e27017 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Wed, 8 Jun 2022 08:32:35 +0200 Subject: [PATCH 320/506] [doc] add man pages Many thanks to https://github.com/benthetechguy, who wrote man pages for bumblebee-status and bumblebee-ctl fixes #882 --- man/bumblebee-ctl.1 | 29 +++++++++++++++++++++ man/bumblebee-status.1 | 58 ++++++++++++++++++++++++++++++++++++++++++ setup.py | 1 + 3 files changed, 88 insertions(+) create mode 100644 man/bumblebee-ctl.1 create mode 100644 man/bumblebee-status.1 diff --git a/man/bumblebee-ctl.1 b/man/bumblebee-ctl.1 new file mode 100644 index 0000000..33c50aa --- /dev/null +++ b/man/bumblebee-ctl.1 @@ -0,0 +1,29 @@ +.TH BUMBLEBEE-CTL "1" "June 2022" "bumblebee-status" +.SH NAME +bumblebee-ctl \- Send commands to bumblebee-status +.SH SYNOPSIS +.B bumblebee-ctl +[\fB\-h\fR] [\fB\-b\fR \fIbutton\fR] [\fB\-i\fR \fIID\fR] \fB-m\fR \fImodule\fR +.SH DESCRIPTION +.B bumblebee-ctl +can be used to send commands to bumblebee-status. +.SH OPTIONS +.TP +\fB\-h\fR, \fB\-\-help\fR +show this help message and exit +.TP +\fB\-b\fR, \fB\-\-button\fR