From c339a163655cd35316f7b44982237c53ca603608 Mon Sep 17 00:00:00 2001 From: Bernhard B Date: Sun, 30 Aug 2020 09:03:34 +0200 Subject: [PATCH 001/194] 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 002/194] 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 003/194] 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 004/194] 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 005/194] 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 006/194] 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 007/194] 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 008/194] 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 009/194] 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 010/194] 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 011/194] 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 012/194] 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 013/194] 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 014/194] 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 015/194] 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 016/194] 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 017/194] [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 018/194] [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 019/194] [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 020/194] [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 021/194] [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 022/194] [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 023/194] 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 024/194] 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 025/194] [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 026/194] [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 027/194] [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 028/194] 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 029/194] [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 030/194] [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 031/194] 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 032/194] 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 033/194] 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 034/194] 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 035/194] [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 036/194] 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 037/194] 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 038/194] 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 039/194] 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 040/194] 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 041/194] 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 042/194] [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 043/194] 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 044/194] 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 045/194] 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 046/194] 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 047/194] [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 048/194] [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 049/194] 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 050/194] 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 051/194] [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 052/194] [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 053/194] 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 054/194] [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 055/194] [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 056/194] 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 057/194] [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 058/194] [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 059/194] 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 060/194] 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 061/194] .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 062/194] 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 063/194] .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 064/194] [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 065/194] [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 066/194] [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 067/194] .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 068/194] 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 069/194] .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 070/194] .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 071/194] 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 072/194] [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 073/194] [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 074/194] 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 075/194] [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 076/194] [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 077/194] [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 078/194] [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 079/194] [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 080/194] [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 081/194] [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 082/194] [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 083/194] [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 084/194] [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 085/194] [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 086/194] [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 087/194] [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 088/194] [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 089/194] [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 090/194] [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 091/194] [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 092/194] [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 093/194] 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 094/194] 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 095/194] [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 096/194] [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 097/194] [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 098/194] [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 099/194] [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 100/194] [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 101/194] [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 102/194] [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 103/194] [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 104/194] [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 105/194] 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 106/194] [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 107/194] [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 108/194] [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 109/194] [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 110/194] [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 111/194] [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 112/194] [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 113/194] [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 114/194] [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 115/194] [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 116/194] 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 117/194] 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 118/194] [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 119/194] [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 120/194] [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 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 +#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 121/194] [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 122/194] [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 123/194] [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 124/194] 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 125/194] [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 126/194] 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 127/194] 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 128/194] 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 129/194] 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 130/194] 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 131/194] 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 132/194] 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 133/194] 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 134/194] 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 135/194] 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 136/194] 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 137/194] 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 138/194] 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 139/194] 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 140/194] 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 141/194] 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 142/194] 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 143/194] 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 144/194] 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 145/194] 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 146/194] [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 147/194] [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 148/194] 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 149/194] 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 150/194] 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 151/194] 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 152/194] [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 153/194] [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 154/194] [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 155/194] 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 156/194] 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 157/194] 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 158/194] 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 159/194] 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 160/194] 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 161/194] [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 162/194] [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 163/194] [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 164/194] [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 165/194] 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 166/194] 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 167/194] [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 168/194] [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 169/194] [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 170/194] [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 171/194] 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 172/194] 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 173/194] 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 174/194] 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 175/194] 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 176/194] 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 177/194] 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 178/194] [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 179/194] [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 c40a1744637b84682731d015420456d35b004182 Mon Sep 17 00:00:00 2001 From: Logan Connolly Date: Tue, 25 Jan 2022 17:50:25 +0100 Subject: [PATCH 180/194] 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 181/194] 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 182/194] [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 183/194] [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 184/194] [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 185/194] 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 186/194] 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 187/194] [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 188/194] [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 189/194] 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 07200c466bd746b4076db0547093f467d8c70254 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Wed, 2 Mar 2022 12:33:02 +0100 Subject: [PATCH 190/194] [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 191/194] [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 192/194] [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 193/194] [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 194/194] [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": "" },