roundabout,
created on Tuesday, 23 September 2025, 18:37:27 (1758652647),
received on Tuesday, 23 September 2025, 18:37:31 (1758652651)
Author identity: Vlad <vlad.muntoiu@gmail.com>
dc8ba7fb93f066c283d32469ebfbb181095627af
.idea/panorama-panel.iml
@@ -2,6 +2,8 @@
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/shared" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/shared/panorama_panel.py" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.12 (panorama-panel)" jdkType="Python SDK" />
applets/battery/__init__.py
@@ -0,0 +1,108 @@
"""
D-Bus laptop battery monitor applet for the Panorama panel.
Copyright 2025, roundabout-host.com <vlad@roundabout-host.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public Licence as published by
the Free Software Foundation, either version 3 of the Licence, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public Licence for more details.
You should have received a copy of the GNU General Public Licence
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations
import panorama_panel
import locale
from pathlib import Path
from pydbus import SystemBus
import gi
gi.require_version("Gtk", "4.0")
from gi.repository import Gtk, Gdk, GLib
module_directory = Path(__file__).resolve().parent
locale.bindtextdomain("panorama-battery-monitor", module_directory / "locale")
_ = lambda x: locale.dgettext("panorama-battery-monitor", x)
def get_battery_icon(fraction, charging):
if fraction < 1/32:
base_icon = "battery-empty"
elif fraction < 1/16:
base_icon = "battery-caution"
elif fraction < 1/4:
base_icon = "battery-low"
elif fraction < 1/2:
base_icon = "battery-medium"
elif fraction < 7/8:
base_icon = "battery-good"
else:
base_icon = "battery-full"
if charging:
return base_icon + "-charging"
return base_icon
class BatteryMonitor(panorama_panel.Applet):
name = _("Laptop battery")
description = _("Check laptop battery charge")
def __init__(self, orientation=Gtk.Orientation.HORIZONTAL, config=None):
super().__init__(orientation=orientation, config=config)
if config is None:
config = {}
self.bus = SystemBus()
self.upower = self.bus.get("org.freedesktop.UPower")
self.button = Gtk.MenuButton()
self.icon = Gtk.Image(pixel_size=config.get("icon_size", 24))
self.button.set_child(self.icon)
self.append(self.button)
self.update_batteries()
GLib.timeout_add_seconds(10, self.update_batteries)
def update_batteries(self):
total_energy = max_energy = 0
any_charging = False
for device_path in self.upower.EnumerateDevices():
device = self.bus.get("org.freedesktop.UPower", device_path)
# Filter only batteries
if device.Type == 2:
# This is in Wh but we don't care
total_energy += device.Energy
max_energy += device.EnergyFull
if device.State == 1:
any_charging = True
print(total_energy, max_energy)
self.button.set_tooltip_text(f"{locale.format_string("%d", total_energy / max_energy * 1000)}‰")
self.icon.set_from_icon_name(get_battery_icon(total_energy / max_energy, any_charging))
return True
def get_config(self):
return {
"icon_size": self.icon.get_pixel_size(),
}
def shutdown(self, app):
self.publishing.unpublish()
config.yaml
@@ -48,10 +48,18 @@ panels:
trigger_name: app-menu
icon_name: start-here-symbolic
icon_size: 24
- DesktopFileButton:
app_id: nemo.desktop
- DesktopFileButton:
app_id: org.gnome.Terminal.desktop
- DesktopFileButton:
app_id: chromium.desktop
centre:
- NotifierApplet:
icon_size: 24
right:
- BatteryMonitor:
icon_size: 24
- Volume:
percentage_reveal: 1000
popdown_after_manual: 2000