feat(update): huge changes
This commit is contained in:
98 files changed
+9636
-5575
No files matched your search
@@ -1,5 +1,4 @@
|
||||
from .battery import *
|
||||
from .metric import *
|
||||
from .workspace import *
|
||||
from .system_indicators import *
|
||||
from .updates import *
|
||||
from .metrics import Metrics
|
||||
from .workspace import workspace
|
||||
from .system_indicators import SystemIndicators
|
||||
from .updates import UpdatesWidget
|
||||
@@ -1,237 +0,0 @@
|
||||
import subprocess
|
||||
import re
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.eventbox import EventBox
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.circularprogressbar import CircularProgressBar
|
||||
from fabric.widgets.overlay import Overlay
|
||||
from fabric.widgets.revealer import Revealer
|
||||
from fabric.core.fabricator import Fabricator
|
||||
from fabric.utils.helpers import exec_shell_command_async
|
||||
from gi.repository import GLib
|
||||
import utils.icons as icons
|
||||
|
||||
|
||||
class Battery(Box):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(
|
||||
name="battery",
|
||||
orientation="h",
|
||||
spacing=0,
|
||||
)
|
||||
|
||||
self.bat_save = Button(
|
||||
name="battery-save",
|
||||
child=Label(name="battery-save-label", markup=icons.power_saving),
|
||||
on_clicked=lambda *_: self.set_power_mode("powersave"),
|
||||
)
|
||||
self.bat_balanced = Button(
|
||||
name="battery-balanced",
|
||||
child=Label(name="battery-balanced-label", markup=icons.power_balanced),
|
||||
on_clicked=lambda *_: self.set_power_mode("balanced"),
|
||||
)
|
||||
self.bat_perf = Button(
|
||||
name="battery-performance",
|
||||
child=Label(
|
||||
name="battery-performance-label", markup=icons.power_performance
|
||||
),
|
||||
on_clicked=lambda *_: self.set_power_mode("performance"),
|
||||
)
|
||||
|
||||
# Attach mouse enter/leave events to buttons as well
|
||||
for btn in [self.bat_save, self.bat_balanced, self.bat_perf]:
|
||||
btn.connect("enter-notify-event", self.on_mouse_enter)
|
||||
btn.connect("leave-notify-event", self.on_mouse_leave)
|
||||
|
||||
# Group the mode buttons into a container.
|
||||
self.mode_switcher = Box(
|
||||
name="power-mode-switcher",
|
||||
orientation="h",
|
||||
spacing=4,
|
||||
children=[self.bat_save, self.bat_balanced, self.bat_perf],
|
||||
)
|
||||
|
||||
self.bat_icon = Label(name="battery-icon", markup=icons.battery)
|
||||
|
||||
self.bat_circle = CircularProgressBar(
|
||||
name="battery-circle",
|
||||
value=0,
|
||||
size=28,
|
||||
line_width=2,
|
||||
start_angle=150,
|
||||
end_angle=390,
|
||||
)
|
||||
|
||||
self.bat_overlay = Overlay(
|
||||
name="battery-overlay",
|
||||
visible=False,
|
||||
child=self.bat_circle,
|
||||
overlays=[self.bat_icon],
|
||||
)
|
||||
|
||||
self.bat_level = Label(
|
||||
name="battery-level",
|
||||
label="100%",
|
||||
)
|
||||
|
||||
self.bat_revealer = Revealer(
|
||||
name="battery-revealer",
|
||||
transition_duration=250,
|
||||
transition_type="slide-left",
|
||||
child=self.bat_level,
|
||||
child_revealed=False,
|
||||
)
|
||||
|
||||
# Create an inner container to hold the battery elements.
|
||||
inner_container = Box(orientation="h", spacing=3)
|
||||
inner_container.add(self.bat_overlay)
|
||||
inner_container.add(self.bat_revealer)
|
||||
inner_container.add(self.mode_switcher)
|
||||
|
||||
# Wrap the inner container in an EventBox.
|
||||
self.event_box = EventBox(
|
||||
events=["enter-notify-event", "leave-notify-event"],
|
||||
name="battery-eventbox",
|
||||
)
|
||||
self.event_box.connect("enter-notify-event", self.on_mouse_enter)
|
||||
self.event_box.connect("leave-notify-event", self.on_mouse_leave)
|
||||
self.event_box.add(inner_container)
|
||||
|
||||
# Add the EventBox to the Battery widget.
|
||||
self.add(self.event_box)
|
||||
|
||||
self.current_mode = None
|
||||
# Initialize hide_timer for delayed hiding.
|
||||
self.hide_timer = None
|
||||
# Initialize counter to track hover status across all related widgets.
|
||||
self.hover_counter = 0
|
||||
|
||||
# Initialize Fabricator for battery polling every 5 seconds.
|
||||
self.batt_fabricator = Fabricator(
|
||||
lambda *args, **kwargs: self.poll_battery(),
|
||||
interval=1000,
|
||||
stream=False,
|
||||
default_value=0,
|
||||
)
|
||||
self.batt_fabricator.changed.connect(self.update_battery)
|
||||
|
||||
# Run update_battery immediately at startup.
|
||||
GLib.idle_add(self.update_battery, None, self.poll_battery())
|
||||
self.set_power_mode("balanced")
|
||||
|
||||
def on_mouse_enter(self, widget, event):
|
||||
"""Reveal battery level on hover."""
|
||||
self.hover_counter += 1
|
||||
if self.hide_timer is not None:
|
||||
GLib.source_remove(self.hide_timer)
|
||||
self.hide_timer = None
|
||||
self.bat_revealer.set_reveal_child(True)
|
||||
return False
|
||||
|
||||
def on_mouse_leave(self, widget, event):
|
||||
"""Schedule hiding the battery level after a 0.5s delay only if not hovering any element."""
|
||||
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):
|
||||
self.bat_revealer.set_reveal_child(False)
|
||||
self.hide_timer = None
|
||||
return False
|
||||
|
||||
def poll_battery(self):
|
||||
"""
|
||||
Polls the battery status by running the "acpi -b" command.
|
||||
If no battery information is found, returns (0, None).
|
||||
Otherwise, returns a tuple: (battery percentage as a float between 0 and 1, battery status string)
|
||||
"""
|
||||
try:
|
||||
output = subprocess.check_output(["acpi", "-b"]).decode("utf-8").strip()
|
||||
if "Battery" not in output:
|
||||
return (0, None)
|
||||
match_percent = re.search(r"(\d+)%", output)
|
||||
match_status = re.search(r"Battery \d+: (\w+)", output)
|
||||
if match_percent:
|
||||
percent = int(match_percent.group(1))
|
||||
status = match_status.group(1) if match_status else None
|
||||
return (percent / 100.0, status)
|
||||
except Exception:
|
||||
pass
|
||||
return (0, None)
|
||||
|
||||
def update_battery(self, sender, battery_data):
|
||||
"""
|
||||
Updates the battery widget...
|
||||
"""
|
||||
value, status = battery_data
|
||||
if value == 0:
|
||||
self.bat_overlay.set_visible(False)
|
||||
self.bat_revealer.set_visible(False)
|
||||
else:
|
||||
self.bat_overlay.set_visible(True)
|
||||
self.bat_revealer.set_visible(True)
|
||||
self.bat_circle.set_value(value)
|
||||
|
||||
percentage = int(value * 100)
|
||||
self.bat_level.set_label(f"{percentage}%")
|
||||
|
||||
# Actualizar el icono de la batería basado en el nivel y el estado de carga.
|
||||
if percentage <= 15:
|
||||
self.bat_icon.set_markup(icons.alert)
|
||||
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")
|
||||
if (
|
||||
status == "Discharging"
|
||||
): # Muestra discharging en cualquier nivel, incluso al 100%.
|
||||
self.bat_icon.set_markup(icons.discharging)
|
||||
elif percentage == 100:
|
||||
self.bat_icon.set_markup(icons.battery)
|
||||
elif status == "Charging":
|
||||
self.bat_icon.set_markup(icons.charging)
|
||||
else:
|
||||
self.bat_icon.set_markup(icons.battery)
|
||||
|
||||
def set_power_mode(self, mode):
|
||||
"""
|
||||
Switches power mode by running the corresponding auto-cpufreq command.
|
||||
mode: one of 'powersave', 'balanced', or 'performance'
|
||||
"""
|
||||
commands = {
|
||||
"powersave": "sudo auto-cpufreq --force powersave",
|
||||
"balanced": "sudo auto-cpufreq --force reset",
|
||||
"performance": "sudo auto-cpufreq --force performance",
|
||||
}
|
||||
if mode in commands:
|
||||
try:
|
||||
exec_shell_command_async(commands[mode])
|
||||
self.current_mode = mode
|
||||
self.update_button_styles()
|
||||
except Exception as err:
|
||||
# Optionally, handle errors or display a notification.
|
||||
print(f"Error setting power mode: {err}")
|
||||
|
||||
def update_button_styles(self):
|
||||
"""
|
||||
Optionally updates button styles to reflect the current mode.
|
||||
Adjust the styling method based on your toolkit's capabilities.
|
||||
"""
|
||||
if self.current_mode == "powersave":
|
||||
self.bat_save.add_style_class("active")
|
||||
self.bat_balanced.remove_style_class("active")
|
||||
self.bat_perf.remove_style_class("active")
|
||||
elif self.current_mode == "balanced":
|
||||
self.bat_save.remove_style_class("active")
|
||||
self.bat_balanced.add_style_class("active")
|
||||
self.bat_perf.remove_style_class("active")
|
||||
elif self.current_mode == "performance":
|
||||
self.bat_save.remove_style_class("active")
|
||||
self.bat_balanced.remove_style_class("active")
|
||||
self.bat_perf.add_style_class("active")
|
||||
@@ -1,83 +0,0 @@
|
||||
import psutil
|
||||
from gi.repository import GLib
|
||||
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.overlay import Overlay
|
||||
from fabric.widgets.circularprogressbar import CircularProgressBar
|
||||
import utils.icons as icons
|
||||
|
||||
|
||||
class Metrics(Box):
|
||||
ICONS = {
|
||||
"CPU": icons.cpu,
|
||||
"RAM": icons.memory,
|
||||
"Swap": icons.swap,
|
||||
"Disk": icons.disk,
|
||||
"Temp": icons.temp,
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(
|
||||
name="metrics",
|
||||
spacing=8,
|
||||
h_align="center",
|
||||
v_align="fill",
|
||||
visible=True,
|
||||
all_visible=True,
|
||||
)
|
||||
|
||||
self.progress_bars = {
|
||||
system: CircularProgressBar(
|
||||
name="metric-circle",
|
||||
line_style="round",
|
||||
line_width=2,
|
||||
size=28,
|
||||
start_angle=150,
|
||||
end_angle=390,
|
||||
)
|
||||
for system in self.ICONS
|
||||
}
|
||||
|
||||
for system, icon_name in self.ICONS.items():
|
||||
overlay = Overlay(
|
||||
child=self.progress_bars[system],
|
||||
overlays=[Label(name="metric-icon", markup=icon_name)],
|
||||
)
|
||||
self.add(overlay)
|
||||
|
||||
GLib.timeout_add_seconds(1, self._update_system_info)
|
||||
|
||||
def _update_system_info(self):
|
||||
ram_usage = psutil.virtual_memory().percent
|
||||
swap_usage = psutil.swap_memory().percent
|
||||
disk_usage = psutil.disk_usage("/").percent
|
||||
cpu_usage = psutil.cpu_percent(interval=0)
|
||||
|
||||
temp = self._get_device_temperature()
|
||||
temp_usage = temp if temp is not None else 0
|
||||
|
||||
usages = {
|
||||
"CPU": cpu_usage,
|
||||
"RAM": ram_usage,
|
||||
"Swap": swap_usage,
|
||||
"Disk": disk_usage,
|
||||
"Temp": temp_usage,
|
||||
}
|
||||
|
||||
for system, usage in usages.items():
|
||||
self.progress_bars[system].value = usage / 100.0
|
||||
self.progress_bars[system].set_tooltip_text(f"{system} {usage:.1f}%")
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _get_device_temperature():
|
||||
try:
|
||||
temps = psutil.sensors_temperatures()
|
||||
for key in ("coretemp", "cpu_thermal"):
|
||||
if key in temps and temps[key]:
|
||||
return round(temps[key][0].current, 1)
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
@@ -0,0 +1,406 @@
|
||||
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()
|
||||
@@ -2,14 +2,22 @@ import subprocess
|
||||
from fabric import Fabricator
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.bluetooth import BluetoothClient
|
||||
from services import NetworkClient, audio
|
||||
from services import network_client, audio, notification_service
|
||||
import utils.icons as icons
|
||||
import config.data as data
|
||||
|
||||
|
||||
class SystemIndicators(Box):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(orientation="h", spacing=2, **kwargs)
|
||||
# Determine if we should use vertical layout for components
|
||||
is_vertical_layout = data.VERTICAL
|
||||
|
||||
super().__init__(
|
||||
orientation="h" if not is_vertical_layout else "v", spacing=0, **kwargs
|
||||
)
|
||||
self.launcher = kwargs.get("launcher", None)
|
||||
|
||||
self.bluetooth_icon = Label(name="system-indicator-icon")
|
||||
self.wifi_icon = Label(name="system-indicator-icon")
|
||||
@@ -17,6 +25,11 @@ class SystemIndicators(Box):
|
||||
self.microphone_icon = Label(name="system-indicator-icon")
|
||||
self.idle_label = Label(name="system-indicator-icon")
|
||||
self.night_label = Label(name="system-indicator-icon")
|
||||
self.power_label = Label(name="system-indicator-icon")
|
||||
self.notification_icon = Button(name="system-indicator-icon")
|
||||
self.notification_label = Label(name="system-notif-label")
|
||||
self.notification_icon.add(self.notification_label)
|
||||
self.notification_icon.connect("clicked", self.open_notif_center)
|
||||
|
||||
for widget in [
|
||||
self.bluetooth_icon,
|
||||
@@ -25,18 +38,25 @@ class SystemIndicators(Box):
|
||||
self.microphone_icon,
|
||||
self.idle_label,
|
||||
self.night_label,
|
||||
self.power_label,
|
||||
self.notification_icon,
|
||||
]:
|
||||
self.add(widget)
|
||||
|
||||
self.audio_service = audio
|
||||
self.bluetooth_client = BluetoothClient()
|
||||
self.network_client = NetworkClient()
|
||||
self.network_client = network_client
|
||||
self.notification_service = notification_service
|
||||
|
||||
# Connect signals
|
||||
self.bluetooth_client.connect("changed", self.update_bluetooth_status)
|
||||
self.audio_service.connect("microphone_changed", self.update_mic_status)
|
||||
self.audio_service.connect("changed", self.update_volume_status)
|
||||
self.network_client.connect("device_ready", self.update_network_status)
|
||||
self.notification_service.connect("dnd", self.update_notification_status)
|
||||
self.notification_service.connect(
|
||||
"notification_count", self.update_notification_status
|
||||
)
|
||||
|
||||
if self.network_client.wifi_device:
|
||||
self.network_client.wifi_device.connect(
|
||||
@@ -51,84 +71,188 @@ class SystemIndicators(Box):
|
||||
Fabricator(interval=1000, poll_from=self.update_all_statuses)
|
||||
|
||||
self.update_bluetooth_status()
|
||||
self.update_notification_status()
|
||||
|
||||
def update_all_statuses(self, *_args):
|
||||
self.update_idle_night_status()
|
||||
self.update_power_profile()
|
||||
self.update_network_status()
|
||||
|
||||
def update_idle_night_status(self, *_):
|
||||
"""Updates the idle and night mode status icons."""
|
||||
self.idle_label.set_markup(
|
||||
icons.coffee
|
||||
if subprocess.run(
|
||||
["pgrep", "-x", "wlinhibit"], capture_output=True
|
||||
).returncode
|
||||
# Update idle status
|
||||
has_idle = (
|
||||
subprocess.run(["pgrep", "-x", "wlinhibit"], capture_output=True).returncode
|
||||
== 0
|
||||
else ""
|
||||
)
|
||||
self.idle_label.set_visible(has_idle)
|
||||
self.idle_label.set_markup(icons.coffee if has_idle else "")
|
||||
|
||||
self.night_label.set_markup(
|
||||
icons.night
|
||||
if subprocess.run(
|
||||
# Update night mode status
|
||||
has_night = (
|
||||
subprocess.run(
|
||||
["pgrep", "-x", "hyprsunset"], capture_output=True
|
||||
).returncode
|
||||
== 0
|
||||
else ""
|
||||
)
|
||||
self.night_label.set_visible(has_night)
|
||||
self.night_label.set_markup(icons.night if has_night else "")
|
||||
|
||||
def update_volume_status(self, *_):
|
||||
"""Updates the volume icon based on speaker status."""
|
||||
stream = self.audio_service.speaker
|
||||
if stream:
|
||||
volume_level = stream.volume
|
||||
self.volume_icon_button.set_markup(
|
||||
icons.vol_off
|
||||
if stream.muted
|
||||
else (
|
||||
icons.vol_mute
|
||||
if volume_level < 1
|
||||
else (icons.vol_medium if volume_level >= 1 else icons.vol_high)
|
||||
)
|
||||
)
|
||||
if not stream:
|
||||
self.volume_icon_button.set_visible(False)
|
||||
return
|
||||
|
||||
self.volume_icon_button.set_visible(True)
|
||||
# Normalize volume to integer percentage (0-100)
|
||||
volume_level = stream.volume
|
||||
if volume_level > 1:
|
||||
volume_level = min(int(volume_level), 100)
|
||||
else:
|
||||
volume_level = int(volume_level * 100)
|
||||
|
||||
is_muted = stream.muted
|
||||
|
||||
if is_muted:
|
||||
icon = icons.vol_off
|
||||
elif volume_level > 74:
|
||||
icon = icons.vol_high
|
||||
elif volume_level > 0:
|
||||
icon = icons.vol_medium
|
||||
else:
|
||||
icon = icons.vol_mute
|
||||
|
||||
self.volume_icon_button.set_markup(icon)
|
||||
|
||||
tooltip_text = "Muted" if is_muted else f"Volume: {volume_level}%"
|
||||
self.volume_icon_button.set_tooltip_text(tooltip_text)
|
||||
|
||||
def update_mic_status(self, *_):
|
||||
"""Updates the microphone icon based on mute status."""
|
||||
mic = self.audio_service.microphone
|
||||
self.microphone_icon.set_markup(icons.mic_off if mic and mic.muted else "")
|
||||
self.microphone_icon.set_visible(bool(mic))
|
||||
if not mic:
|
||||
self.microphone_icon.set_visible(False)
|
||||
return
|
||||
|
||||
self.microphone_icon.set_visible(True)
|
||||
# Normalize volume to integer percentage (0-100)
|
||||
volume_level = mic.volume
|
||||
if volume_level > 1:
|
||||
volume_level = min(int(volume_level), 100)
|
||||
else:
|
||||
volume_level = int(volume_level * 100)
|
||||
|
||||
is_muted = mic.muted
|
||||
icon = icons.mic_muted if is_muted else icons.mic
|
||||
|
||||
self.microphone_icon.set_markup(icon)
|
||||
tooltip_text = "Muted" if is_muted else f"Microphone: {volume_level}%"
|
||||
self.microphone_icon.set_tooltip_text(tooltip_text)
|
||||
|
||||
def update_bluetooth_status(self, *_):
|
||||
"""Updates the Bluetooth icon based on enabled status."""
|
||||
self.bluetooth_icon.set_markup(
|
||||
icons.bluetooth if self.bluetooth_client.enabled else icons.bluetooth_off
|
||||
)
|
||||
if self.bluetooth_client.enabled:
|
||||
self.bluetooth_icon.set_visible(True)
|
||||
# Get a list of connected devices
|
||||
connected_devices = [
|
||||
device.name
|
||||
for device in self.bluetooth_client.devices
|
||||
if device.connected
|
||||
]
|
||||
|
||||
if connected_devices:
|
||||
self.bluetooth_icon.set_markup(icons.bluetooth_connected)
|
||||
self.bluetooth_icon.set_tooltip_text(f"{', '.join(connected_devices)}")
|
||||
else:
|
||||
self.bluetooth_icon.set_markup(icons.bluetooth)
|
||||
self.bluetooth_icon.set_tooltip_text(
|
||||
"Bluetooth is enabled, no devices connected"
|
||||
)
|
||||
else:
|
||||
self.bluetooth_icon.set_visible(True) # Keep it visible
|
||||
self.bluetooth_icon.set_markup(icons.bluetooth_off)
|
||||
self.bluetooth_icon.set_tooltip_text("Bluetooth is disabled")
|
||||
|
||||
def update_network_status(self, *_):
|
||||
primary_device = self.network_client.primary_device
|
||||
self.wifi_icon.set_visible(True)
|
||||
|
||||
if primary_device == "wifi" and self.network_client.wifi_device:
|
||||
wifi_device = self.network_client.wifi_device
|
||||
self.set_tooltip_text(self.network_client.wifi_device.ssid)
|
||||
|
||||
self.wifi_icon.set_tooltip_text(
|
||||
wifi_device.ssid if wifi_device.ssid else "No WiFi Connection"
|
||||
)
|
||||
|
||||
if wifi_device.enabled:
|
||||
wifi_strength = wifi_device.strength
|
||||
strength = wifi_device.strength
|
||||
|
||||
if wifi_strength == -1: # No active connection
|
||||
icon_label = icons.wifi_off
|
||||
if strength > 0:
|
||||
if strength < 25:
|
||||
icon_label = icons.wifi_0
|
||||
elif strength < 50:
|
||||
icon_label = icons.wifi_1
|
||||
elif strength < 75:
|
||||
icon_label = icons.wifi_2
|
||||
else:
|
||||
icon_label = icons.wifi_3
|
||||
else:
|
||||
icon_label = {
|
||||
80: icons.wifi,
|
||||
60: icons.good_signal,
|
||||
40: icons.moderate_signal,
|
||||
20: icons.weak_signal,
|
||||
0: icons.no_signal,
|
||||
}.get(min(80, 20 * round(wifi_strength / 20)), icons.wifi_off)
|
||||
icon_label = icons.wifi_off
|
||||
else:
|
||||
icon_label = icons.wifi_off
|
||||
|
||||
elif primary_device == "wired":
|
||||
icon_label = icons.lan
|
||||
self.wifi_icon.set_tooltip_text("Wired Connection")
|
||||
|
||||
else:
|
||||
icon_label = icons.wifi_off
|
||||
icon_label = icons.wifi_off # No network
|
||||
|
||||
self.wifi_icon.set_markup(icon_label)
|
||||
|
||||
def update_power_profile(self, *_):
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["powerprofilesctl", "get"], capture_output=True, text=True
|
||||
)
|
||||
current_profile = result.stdout.strip()
|
||||
|
||||
profile_icons = {
|
||||
"performance": icons.power_performance,
|
||||
"balanced": "",
|
||||
"power-saver": icons.power_saving,
|
||||
}
|
||||
|
||||
icon = profile_icons.get(current_profile)
|
||||
tooltip = (
|
||||
f"Power Profile: {current_profile.capitalize()}"
|
||||
if current_profile
|
||||
else "Unknown Profile"
|
||||
)
|
||||
|
||||
self.power_label.set_visible(bool(icon)) # Only show if there's an icon
|
||||
self.power_label.set_markup(icon)
|
||||
self.power_label.set_tooltip_text(tooltip)
|
||||
|
||||
except Exception as e:
|
||||
self.power_label.set_visible(False)
|
||||
self.power_label.set_tooltip_text(f"Error fetching power profile: {e}")
|
||||
|
||||
def update_notification_status(self, *_):
|
||||
"""Update the notification icon based on DND state and notification count."""
|
||||
count = self.notification_service.count
|
||||
|
||||
if count == 0 and not self.notification_service.dont_disturb:
|
||||
self.notification_icon.set_visible(False)
|
||||
return
|
||||
|
||||
if self.notification_service.dont_disturb:
|
||||
self.notification_label.set_markup(icons.notifications_off)
|
||||
self.notification_icon.set_tooltip_text("Do Not Disturb: On")
|
||||
self.notification_icon.set_visible(True)
|
||||
else:
|
||||
# At this point count must be > 0
|
||||
self.notification_label.set_markup(icons.notifications_clear)
|
||||
self.notification_icon.set_tooltip_text(f"Notifications: {count}")
|
||||
self.notification_icon.set_visible(True)
|
||||
|
||||
def open_notif_center(self, *_):
|
||||
self.launcher.open("notification-center")
|
||||
@@ -5,9 +5,9 @@ from fabric.widgets.centerbox import CenterBox
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.stack import Stack
|
||||
from loguru import logger
|
||||
from gi.repository import Gtk
|
||||
import utils.icons as icons
|
||||
import config.data as data
|
||||
|
||||
|
||||
class UpdatesWidget(Button):
|
||||
@@ -17,8 +17,15 @@ class UpdatesWidget(Button):
|
||||
self,
|
||||
**kwargs,
|
||||
):
|
||||
# Determine if we should use vertical layout for components
|
||||
is_vertical_layout = data.VERTICAL
|
||||
|
||||
# Initialize the button with specific name and style
|
||||
super().__init__(name="button-bar", **kwargs)
|
||||
super().__init__(
|
||||
name="update",
|
||||
orientation="h" if not is_vertical_layout else "v",
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
self.script_file = get_relative_path("../../../config/scripts/systemupdates.sh")
|
||||
|
||||
@@ -31,45 +38,49 @@ class UpdatesWidget(Button):
|
||||
self.update_icon = Label(name="update-icon", markup=icons.update)
|
||||
self.updated_icon = Label(name="updated-icon", markup=icons.updated)
|
||||
|
||||
# Create boxes with proper orientation
|
||||
self.update_box = CenterBox(
|
||||
orientation="v" if is_vertical_layout else "h",
|
||||
center_children=[self.update_icon, self.update_level_label],
|
||||
)
|
||||
|
||||
self.updated_box = CenterBox(
|
||||
orientation="v" if is_vertical_layout else "h",
|
||||
center_children=[self.updated_icon],
|
||||
)
|
||||
|
||||
# Stack to switch between states
|
||||
self.stack = Stack()
|
||||
self.stack.add_named(self.update_box, "update_box")
|
||||
self.stack.add_named(self.updated_box, "updated_box")
|
||||
self.stack.set_visible_child_name("update_box")
|
||||
|
||||
self.children = Box(
|
||||
orientation="v" if is_vertical_layout else "h",
|
||||
children=self.stack,
|
||||
)
|
||||
|
||||
# Ensure the stack resizes to the current visible child
|
||||
# Configure stack transitions
|
||||
self.stack.set_homogeneous(False)
|
||||
self.stack.set_transition_type(Gtk.StackTransitionType.CROSSFADE)
|
||||
|
||||
# Connect the button press event
|
||||
# Connect click handler
|
||||
self.connect("button-press-event", self.on_button_press)
|
||||
|
||||
# Set up a repeater to call the update method at specified intervals
|
||||
# Check for updates every minute
|
||||
invoke_repeater(60000, self.update, initial_call=True)
|
||||
|
||||
def update_values(self, value: str):
|
||||
logger.info(f"[Updates] Received update JSON: {value}")
|
||||
|
||||
def update_values(self, value: str) -> bool:
|
||||
"""Update the widget state based on update check results"""
|
||||
try:
|
||||
value = json.loads(value)
|
||||
except json.JSONDecodeError:
|
||||
logger.error("[Updates] Failed to parse update JSON")
|
||||
return False
|
||||
|
||||
update_count = str(value["total"])
|
||||
self.update_level_label.set_label(update_count)
|
||||
|
||||
# Show appropriate state based on update count
|
||||
if update_count == "0":
|
||||
self.stack.set_visible_child_name("updated_box")
|
||||
self.update_level_label.set_visible(False)
|
||||
@@ -80,8 +91,10 @@ class UpdatesWidget(Button):
|
||||
self.set_tooltip_text(value.get("tooltip", ""))
|
||||
return True
|
||||
|
||||
def on_button_press(self, _, event):
|
||||
if event.button == 1:
|
||||
def on_button_press(self, _, event) -> bool:
|
||||
"""Handle button press events"""
|
||||
if event.button == 1: # Left click
|
||||
# Run update command
|
||||
exec_shell_command_async(
|
||||
f"{self.script_file} -arch -up",
|
||||
lambda _: None,
|
||||
@@ -89,12 +102,11 @@ class UpdatesWidget(Button):
|
||||
self.update()
|
||||
return True
|
||||
else:
|
||||
# Other clicks just refresh
|
||||
self.update()
|
||||
|
||||
def update(self):
|
||||
logger.info("[Updates] Checking for updates...")
|
||||
|
||||
# Execute the update script asynchronously and update values
|
||||
def update(self) -> bool:
|
||||
"""Check for system updates"""
|
||||
exec_shell_command_async(
|
||||
f"{self.script_file} -arch",
|
||||
lambda output: self.update_values(output),
|
||||
|
||||
@@ -2,18 +2,29 @@ import json
|
||||
|
||||
from fabric.hyprland.service import Hyprland
|
||||
from fabric.widgets.label import Label
|
||||
import config.data as data
|
||||
|
||||
connection = Hyprland()
|
||||
|
||||
workspaceData = connection.send_command("j/activeworkspace").reply
|
||||
activeWorkspace = json.loads(workspaceData.decode("utf-8"))["name"]
|
||||
workspace = Label(label=f"Workspace {activeWorkspace}")
|
||||
|
||||
# Create the workspace label with proper initialization
|
||||
if not data.VERTICAL:
|
||||
workspace = Label(label=f"Workspace {activeWorkspace}", name="workspace-name")
|
||||
else:
|
||||
workspace = Label(
|
||||
v_align="center",
|
||||
label="W\no\nr\nk\ns\np\na\nc\ne\n\n" + activeWorkspace,
|
||||
name="workspace-name",
|
||||
)
|
||||
|
||||
def on_workspace(obj, signal):
|
||||
global activeWorkspace
|
||||
activeWorkspace = json.loads(signal.data[0])
|
||||
workspace.set_label(f"Workspace {activeWorkspace}")
|
||||
|
||||
if not data.VERTICAL:
|
||||
workspace.set_label(f"Workspace {activeWorkspace}")
|
||||
else:
|
||||
workspace.set_label(f"W\no\nr\nk\ns\np\na\nc\ne\n\n{activeWorkspace}")
|
||||
|
||||
connection.connect("event::workspace", on_workspace)
|
||||
Reference in new issue
Block a user