407 lines
14 KiB
Python
407 lines
14 KiB
Python
import subprocess
|
|
import json
|
|
import psutil
|
|
from gi.repository import GLib, Gdk
|
|
|
|
from fabric.widgets.label import Label
|
|
from fabric.widgets.box import Box
|
|
from fabric.widgets.circularprogressbar import CircularProgressBar
|
|
from fabric.widgets.revealer import Revealer
|
|
from fabric.core.fabricator import Fabricator
|
|
from fabric.widgets.button import Button
|
|
import utils.icons as icons
|
|
import config.data as data
|
|
|
|
|
|
class MetricsProvider:
|
|
def __init__(self):
|
|
self.cpu = 0.0
|
|
self.mem = 0.0
|
|
self.swap = 0.0
|
|
self.disk = []
|
|
self.gpu = []
|
|
self.bat_percent = 0.0
|
|
self.bat_charging = None
|
|
|
|
GLib.timeout_add_seconds(1, self._update)
|
|
|
|
def _update(self):
|
|
# Get non-blocking usage percentages (interval=0)
|
|
# The first call may return 0, but subsequent calls will provide consistent values.
|
|
self.cpu = psutil.cpu_percent(interval=0)
|
|
mem = psutil.virtual_memory()
|
|
self.mem = mem.percent
|
|
swap = psutil.swap_memory()
|
|
self.swap = swap.percent
|
|
self.disk = [psutil.disk_usage(path).percent for path in data.BAR_METRICS_DISKS]
|
|
info = self.get_gpu_info()
|
|
self.gpu = [int(v["gpu_util"][:-1]) for v in info]
|
|
|
|
battery = psutil.sensors_battery()
|
|
if battery is None:
|
|
self.bat_percent = 0.0
|
|
self.bat_charging = None
|
|
else:
|
|
self.bat_percent = battery.percent
|
|
self.bat_charging = battery.power_plugged
|
|
return True
|
|
|
|
def get_metrics(self):
|
|
return (self.cpu, self.mem, self.swap, self.disk)
|
|
|
|
def get_battery(self):
|
|
return (self.bat_percent, self.bat_charging)
|
|
|
|
def get_gpu_info(self):
|
|
try:
|
|
return json.loads(subprocess.check_output(["nvtop", "-s"]))
|
|
except:
|
|
return []
|
|
|
|
|
|
shared_provider = MetricsProvider()
|
|
|
|
|
|
class SingularMetric:
|
|
def __init__(self, id, name, icon):
|
|
self.is_vertical_layout = data.VERTICAL
|
|
self.name_markup = name
|
|
self.icon_markup = icon
|
|
|
|
self.icon = Label(name="metrics-icon", markup=icon)
|
|
self.circle = CircularProgressBar(
|
|
name="metrics-circle",
|
|
value=0,
|
|
size=26,
|
|
line_width=2,
|
|
start_angle=150,
|
|
end_angle=390,
|
|
style_classes=id,
|
|
child=self.icon,
|
|
)
|
|
|
|
self.circle_button = Button(
|
|
name=f"metrics-{id}-button",
|
|
child=self.circle,
|
|
style="padding: 0; margin: 0; background: none;",
|
|
)
|
|
|
|
self.circle_button.add_events(
|
|
Gdk.EventMask.ENTER_NOTIFY_MASK | Gdk.EventMask.LEAVE_NOTIFY_MASK
|
|
)
|
|
self.circle_button.connect("enter-notify-event", self.on_button_enter)
|
|
self.circle_button.connect("leave-notify-event", self.on_button_leave)
|
|
|
|
self.circle_button.connect("clicked", self.on_circle_clicked)
|
|
|
|
self.level = Label(
|
|
name="metrics-level",
|
|
style_classes=id,
|
|
label="0%",
|
|
style="margin: 4px 0px;" if self.is_vertical_layout else "margin: 0 4px;",
|
|
)
|
|
self.revealer = Revealer(
|
|
name=f"metrics-{id}-revealer",
|
|
transition_duration=250,
|
|
transition_type="slide-up" if self.is_vertical_layout else "slide-left",
|
|
child=self.level,
|
|
child_revealed=False,
|
|
)
|
|
|
|
self.box = Box(
|
|
name=f"metrics-{id}-box",
|
|
orientation="v" if self.is_vertical_layout else "h",
|
|
spacing=0,
|
|
children=[self.circle_button, self.revealer],
|
|
)
|
|
|
|
# Connect events for cursor changes to the box as well
|
|
self.box.connect("enter-notify-event", self.on_button_enter)
|
|
self.box.connect("leave-notify-event", self.on_button_leave)
|
|
|
|
# Set initial tooltip - moved after box is created
|
|
self.update_tooltip("0%")
|
|
|
|
def update_tooltip(self, value_text):
|
|
tooltip = f"{self.icon_markup} {self.name_markup}: {value_text}"
|
|
# self.box.set_tooltip_markup(tooltip)
|
|
self.circle_button.set_tooltip_markup(tooltip)
|
|
self.circle.set_tooltip_markup(tooltip)
|
|
self.icon.set_tooltip_markup(tooltip)
|
|
|
|
def markup(self):
|
|
"""Get formatted text for tooltip display"""
|
|
return f"{self.icon_markup} {self.name_markup}: {self.level.get_label()}"
|
|
|
|
def on_button_enter(self, widget, event):
|
|
window = widget.get_window()
|
|
if window:
|
|
window.set_cursor(Gdk.Cursor(Gdk.CursorType.HAND2))
|
|
|
|
def on_button_leave(self, widget, event):
|
|
window = widget.get_window()
|
|
if window:
|
|
window.set_cursor(None)
|
|
|
|
def on_circle_clicked(self, button):
|
|
# Toggle the revealed state of the label
|
|
current_state = self.revealer.get_child_revealed()
|
|
self.revealer.set_reveal_child(not current_state)
|
|
|
|
|
|
class Metrics(Box):
|
|
def __init__(self, **kwargs):
|
|
# Determine if we should use vertical layout for components
|
|
self.is_vertical_layout = data.VERTICAL
|
|
|
|
super().__init__(
|
|
name="metrics",
|
|
spacing=0,
|
|
orientation="h" if not self.is_vertical_layout else "v",
|
|
visible=True,
|
|
all_visible=True,
|
|
style="padding:4px 0px" if self.is_vertical_layout else "padding: 0px 4px;",
|
|
**kwargs,
|
|
)
|
|
|
|
self.batt_fabricator = Fabricator(
|
|
poll_from=lambda v: shared_provider.get_battery(),
|
|
on_changed=lambda f, v: self.update_battery,
|
|
interval=1000,
|
|
stream=False,
|
|
default_value=0,
|
|
)
|
|
self.batt_fabricator.changed.connect(self.update_battery)
|
|
GLib.idle_add(self.update_battery, None, shared_provider.get_battery())
|
|
|
|
visible = getattr(
|
|
data,
|
|
"METRICS_VISIBLE",
|
|
{"cpu": True, "ram": True, "disk": True, "gpu": False, "swap": True},
|
|
)
|
|
disks = (
|
|
[
|
|
SingularMetric(
|
|
"disk",
|
|
f"DISK ({path})" if len(data.BAR_METRICS_DISKS) != 1 else "DISK",
|
|
icons.disk,
|
|
)
|
|
for path in data.BAR_METRICS_DISKS
|
|
]
|
|
if visible.get("disk", True)
|
|
else []
|
|
)
|
|
gpu_info = shared_provider.get_gpu_info()
|
|
gpus = (
|
|
[
|
|
SingularMetric(
|
|
f"gpu",
|
|
f"GPU ({v['device_name']})" if len(gpu_info) != 1 else "GPU",
|
|
icons.gpu,
|
|
)
|
|
for v in gpu_info
|
|
]
|
|
if visible.get("gpu", True)
|
|
else []
|
|
)
|
|
|
|
self.cpu = (
|
|
SingularMetric("cpu", "CPU", icons.cpu)
|
|
if visible.get("cpu", True)
|
|
else None
|
|
)
|
|
self.ram = (
|
|
SingularMetric("ram", "RAM", icons.memory)
|
|
if visible.get("ram", True)
|
|
else None
|
|
)
|
|
self.swap = (
|
|
SingularMetric("swap", "SWAP", icons.swap)
|
|
if visible.get("swap", True)
|
|
else None
|
|
)
|
|
self.bat = (
|
|
SingularMetric("bat", "BATTERY", icons.battery)
|
|
if visible.get("bat", True)
|
|
else None
|
|
)
|
|
self.disk = disks
|
|
self.gpu = gpus
|
|
|
|
# Add only enabled metrics
|
|
for disk in self.disk:
|
|
self.add(disk.box)
|
|
self.add(Box(name="metrics-sep"))
|
|
if self.ram:
|
|
self.add(self.ram.box)
|
|
self.add(Box(name="metrics-sep"))
|
|
if self.cpu:
|
|
self.add(self.cpu.box)
|
|
for gpu in self.gpu:
|
|
self.add(Box(name="metrics-sep"))
|
|
self.add(gpu.box)
|
|
if self.swap:
|
|
self.add(Box(name="metrics-sep"))
|
|
self.add(self.swap.box)
|
|
if self.bat:
|
|
self.add(Box(name="metrics-sep"))
|
|
self.add(self.bat.box)
|
|
|
|
# Connect events directly to the button
|
|
self.connect("enter-notify-event", self.on_mouse_enter)
|
|
self.connect("leave-notify-event", self.on_mouse_leave)
|
|
|
|
GLib.timeout_add_seconds(1, self.update_metrics)
|
|
|
|
self.hide_timer = None
|
|
self.hover_counter = 0
|
|
|
|
def _format_percentage(self, value: int) -> str:
|
|
return f"{value}%"
|
|
|
|
def on_mouse_enter(self, widget, event):
|
|
window = widget.get_window()
|
|
if window:
|
|
window.set_cursor(Gdk.Cursor(Gdk.CursorType.HAND2))
|
|
|
|
if not data.VERTICAL:
|
|
self.hover_counter += 1
|
|
if self.hide_timer is not None:
|
|
GLib.source_remove(self.hide_timer)
|
|
self.hide_timer = None
|
|
if self.cpu:
|
|
self.cpu.revealer.set_reveal_child(True)
|
|
if self.ram:
|
|
self.ram.revealer.set_reveal_child(True)
|
|
for disk in self.disk:
|
|
disk.revealer.set_reveal_child(True)
|
|
for gpu in self.gpu:
|
|
gpu.revealer.set_reveal_child(True)
|
|
if self.swap:
|
|
self.swap.revealer.set_reveal_child(True)
|
|
if self.bat:
|
|
self.bat.revealer.set_reveal_child(True)
|
|
return False
|
|
|
|
def on_mouse_leave(self, widget, event):
|
|
window = widget.get_window()
|
|
if window:
|
|
window.set_cursor(None)
|
|
|
|
if not data.VERTICAL:
|
|
if self.hover_counter > 0:
|
|
self.hover_counter -= 1
|
|
if self.hover_counter == 0:
|
|
if self.hide_timer is not None:
|
|
GLib.source_remove(self.hide_timer)
|
|
self.hide_timer = GLib.timeout_add(500, self.hide_revealer)
|
|
return False
|
|
|
|
def hide_revealer(self):
|
|
if not data.VERTICAL:
|
|
if self.cpu:
|
|
self.cpu.revealer.set_reveal_child(False)
|
|
if self.ram:
|
|
self.ram.revealer.set_reveal_child(False)
|
|
for disk in self.disk:
|
|
disk.revealer.set_reveal_child(False)
|
|
for gpu in self.gpu:
|
|
gpu.revealer.set_reveal_child(False)
|
|
self.hide_timer = None
|
|
return False
|
|
|
|
def update_metrics(self):
|
|
cpu, mem, swap, disk = shared_provider.get_metrics()
|
|
gpu = shared_provider.gpu
|
|
if self.cpu:
|
|
self.cpu.circle.set_value(cpu / 100.0)
|
|
value_text = self._format_percentage(int(cpu))
|
|
self.cpu.level.set_label(value_text)
|
|
self.cpu.update_tooltip(value_text)
|
|
if self.ram:
|
|
self.ram.circle.set_value(mem / 100.0)
|
|
value_text = self._format_percentage(int(mem))
|
|
self.ram.level.set_label(value_text)
|
|
self.ram.update_tooltip(value_text)
|
|
for i, disk_metric in enumerate(self.disk):
|
|
if i < len(disk):
|
|
value_text = self._format_percentage(int(disk[i]))
|
|
disk_metric.circle.set_value(disk[i] / 100.0)
|
|
disk_metric.level.set_label(value_text)
|
|
disk_metric.update_tooltip(value_text)
|
|
for i, gpu_metric in enumerate(self.gpu):
|
|
if i < len(gpu):
|
|
value_text = self._format_percentage(int(gpu[i]))
|
|
gpu_metric.circle.set_value(gpu[i] / 100.0)
|
|
gpu_metric.level.set_label(value_text)
|
|
gpu_metric.update_tooltip(value_text)
|
|
if self.swap:
|
|
self.swap.circle.set_value(swap / 100.0)
|
|
value_text = self._format_percentage(int(swap))
|
|
self.swap.level.set_label(value_text)
|
|
self.swap.update_tooltip(value_text)
|
|
if self.bat:
|
|
bat_percent, _ = shared_provider.get_battery()
|
|
self.bat.circle.set_value(bat_percent / 100.0)
|
|
value_text = self._format_percentage(int(bat_percent))
|
|
self.bat.level.set_label(value_text)
|
|
self.bat.update_tooltip(value_text)
|
|
|
|
# Tooltip: only show enabled metrics
|
|
tooltip_metrics = []
|
|
if self.disk: tooltip_metrics.extend(self.disk)
|
|
if self.swap: tooltip_metrics.append(self.swap)
|
|
if self.bat: tooltip_metrics.append(self.bat)
|
|
if self.ram: tooltip_metrics.append(self.ram)
|
|
if self.cpu: tooltip_metrics.append(self.cpu)
|
|
if self.gpu: tooltip_metrics.extend(self.gpu)
|
|
self.set_tooltip_markup((" - " if not data.VERTICAL else "\n").join([v.markup() for v in tooltip_metrics]))
|
|
|
|
return True
|
|
|
|
def update_battery(self, sender, battery_data):
|
|
if self.bat is None:
|
|
return
|
|
|
|
value, charging = battery_data
|
|
if value == 0:
|
|
if self.bat:
|
|
self.bat.box.set_visible(False)
|
|
else:
|
|
if self.bat:
|
|
self.bat.box.set_visible(True)
|
|
self.bat.circle.set_value(value / 100)
|
|
percentage = int(value)
|
|
self.bat.level.set_label(self._format_percentage(percentage))
|
|
|
|
# Apply alert styling if battery is low AND not charging
|
|
if percentage <= 15 and charging == False:
|
|
self.bat.icon.add_style_class("alert")
|
|
self.bat.circle.add_style_class("alert")
|
|
else:
|
|
self.bat.icon.remove_style_class("alert")
|
|
self.bat.circle.remove_style_class("alert")
|
|
|
|
# Choose the icon based on charging state first, then battery level
|
|
if percentage == 100:
|
|
self.bat.icon.set_markup(icons.battery)
|
|
charging_status = f"{icons.bat_full} Fully Charged"
|
|
elif charging == True:
|
|
self.bat.icon.set_markup(icons.charging)
|
|
charging_status = f"{icons.bat_charging} Charging"
|
|
elif percentage <= 15 and charging == False:
|
|
self.bat.icon.set_markup(icons.alert)
|
|
charging_status = f"{icons.bat_low} Low Battery"
|
|
elif charging == False:
|
|
self.bat.icon.set_markup(icons.discharging)
|
|
charging_status = f"{icons.bat_discharging} Discharging"
|
|
else:
|
|
self.bat.icon.set_markup(icons.battery)
|
|
charging_status = "Battery"
|
|
|
|
# Update individual tooltip for battery
|
|
self.bat.update_tooltip(f"{charging_status}: {self._format_percentage(percentage)}")
|
|
|
|
# Update the combined tooltip
|
|
self.update_metrics()
|