feat(update): huge changes

This commit is contained in:
sankalp committed 2025-04-25 06:33:51 +05:45
1 parent 53c077106f
commit 6b9b419aa9
98 files changed
+9636 -5575

No files matched your search

+4 -5
View File
@@ -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
-237
View File
@@ -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")
-83
View File
@@ -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
+406
View File
@@ -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()
+169 -45
View File
@@ -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")
+27 -15
View File
@@ -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),
+14 -3
View File
@@ -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)