chore(initial):

This commit is contained in:
sankalp committed 2025-05-11 12:17:00 +05:45
1 parent b0b8c19562
commit 7f956a59b7
42 files changed
+21627

No files matched your search

View File
Whitespace-only changes.
+102
View File
@@ -0,0 +1,102 @@
import json
import os
import re
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import GLib, Gtk
from loguru import logger
import config.data as data
ICON_CACHE_FILE = data.CACHE_DIR + "/icons.json"
if not os.path.exists(data.CACHE_DIR):
os.makedirs(data.CACHE_DIR)
class IconResolver:
def __init__(self, default_applicaiton_icon: str = "application-x-executable-symbolic"):
if os.path.exists(ICON_CACHE_FILE):
with open(ICON_CACHE_FILE) as f:
try:
self._icon_dict = json.load(f)
except json.JSONDecodeError:
logger.info("[ICONS] Cache file does not exist or is corrupted")
self._icon_dict = {}
else:
self._icon_dict = {}
self.default_applicaiton_icon = default_applicaiton_icon
def get_icon_name(self, app_id: str):
if app_id in self._icon_dict:
return self._icon_dict[app_id]
new_icon = self._compositor_find_icon(app_id)
logger.info(
f"[ICONS] found new icon: '{new_icon}' for app id: '{app_id}', storing..."
)
self._store_new_icon(app_id, new_icon)
return new_icon
def get_icon_pixbuf(self, app_id: str, size: int = 16):
icon_theme = Gtk.IconTheme.get_default()
icon_name = self.get_icon_name(app_id)
try:
# Try to load the resolved icon.
return icon_theme.load_icon(icon_name, size, Gtk.IconLookupFlags.FORCE_SIZE)
except GLib.Error as primary_error:
logger.warning(
f"Warning: Icon '{icon_name}' not found in theme. Error: {primary_error}"
)
try:
# Fallback to the default application icon.
return icon_theme.load_icon(
self.default_applicaiton_icon, size, Gtk.IconLookupFlags.FORCE_SIZE
)
except GLib.Error as fallback_error:
logger.error(
f"Error: Fallback icon '{self.default_applicaiton_icon}' also not found. Error: {fallback_error}"
)
return None
def _store_new_icon(self, app_id: str, icon: str):
self._icon_dict[app_id] = icon
with open(ICON_CACHE_FILE, "w") as f:
json.dump(self._icon_dict, f)
def _get_icon_from_desktop_file(self, desktop_file_path: str):
# Retrieve the icon specified in the [Desktop Entry] section.
with open(desktop_file_path) as f:
for line in f.readlines():
if "Icon=" in line:
return "".join(line[5:].split())
return self.default_applicaiton_icon
def _get_desktop_file(self, app_id: str) -> str | None:
data_dirs = GLib.get_system_data_dirs()
for data_dir in data_dirs:
data_dir = os.path.join(data_dir, "applications")
if os.path.exists(data_dir):
files = os.listdir(data_dir)
matching = [s for s in files if "".join(app_id.lower().split()) in s.lower()]
if matching:
return os.path.join(data_dir, matching[0])
for word in list(filter(None, re.split(r"-|\.|_|\s", app_id))):
matching = [s for s in files if word.lower() in s.lower()]
if matching:
return os.path.join(data_dir, matching[0])
return None
def _compositor_find_icon(self, app_id: str):
icon_theme = Gtk.IconTheme.get_default()
if icon_theme.has_icon(app_id):
return app_id
if icon_theme.has_icon(app_id + "-desktop"):
return app_id + "-desktop"
desktop_file = self._get_desktop_file(app_id)
return (
self._get_icon_from_desktop_file(desktop_file)
if desktop_file
else self.default_applicaiton_icon
)
+185
View File
@@ -0,0 +1,185 @@
# Parameters
font_family: str = "tabler-icons"
font_weight: str = "normal"
span: str = f"<span font-family='{font_family}' font-weight='{font_weight}'>"
# Panels
apps: str = "&#xebde;"
dashboard: str = "&#xea87;"
chat: str = "&#xf59f;"
wallpapers: str = "&#xeb01;"
windows: str = "&#xefe6;"
# Bar
colorpicker: str = "&#xebe6;"
media: str = "&#xf00d;"
# Circles
temp: str = "&#xeb38;"
disk: str = "&#xea88;"
battery: str = "&#xea38;"
memory: str = "&#xfa97;"
cpu: str = "&#xef8e;"
swap: str = "&#xeb31;"
# Toolbox
toolbox: str = "&#xebca;"
ssfull: str = "&#xec3c;"
ssregion: str = "&#xf201;"
screenshots: str = "&#xeb0a;"
screenrecord: str = "&#xed22;"
recordings: str = "&#xeafa;"
ocr: str = "&#xfcc3;"
gamemode: str = "&#xf026;"
gamemode_off: str = "&#xf111;"
close: str = "&#xeb55;"
bell: str = "&#xeb55;"
bell_off: str = "&#xece9"
notifications_on: str = "&#xea35;"
notifications_off: str = "&#xece9;"
notifications_clear: str = "&#xf814;"
notifications: str = "&#xeafe;"
# Wallpapers
add: str = "&#xeb0b;"
sort: str = "&#xeb5a;"
circle: str = "&#xf671;"
check_box: str = "&#xf63d;"
box: str = "&#xf59a;"
scan: str = "&#xeb13;"
scan_stop: str = "&#xed4a;"
record: str = "&#xf692;"
update: str = "&#xfa0a;"
updated: str = "&#xf704;"
# Chevrons
chevron_up: str = "&#xea62;"
chevron_down: str = "&#xea5f;"
chevron_left: str = "&#xea60;"
chevron_right: str = "&#xea61;"
# Power
lock: str = "&#xeae2;"
suspend: str = "&#xece7;"
logout: str = "&#xeba8;"
reboot: str = "&#xeb13;"
shutdown: str = "&#xeb0d;"
# Power Manager
power_saving: str = "&#xed4f;"
power_balanced: str = "&#xfa77;"
power_performance: str = "&#xec45;"
charging: str = "&#xefef;"
discharging: str = "&#xefe9;"
alert: str = "&#xea06;"
bat_charging: str = "&#xeeca;"
bat_discharging: str = "&#xf0a1;"
bat_low: str = "&#xff1d;"
bat_full: str = "&#xea38;"
moderate_signal: str = "&#xeba4;"
good_signal: str = "&#xeba5;"
no_signal: str = "&#xf063;"
weak_signal: str = "&#xeba3;"
# Applets
lan: str = "&#xeb9d;"
wifi_0: str = "&#xeba3;"
wifi_1: str = "&#xeba4;"
wifi_2: str = "&#xeba5;"
wifi_3: str = "&#xeb52;"
wifi: str = "&#xeb52;"
world: str = "&#xeb54;"
world_off: str = "&#xf1ca;"
bluetooth: str = "&#xea37;"
night: str = "&#xeaf8;"
coffee: str = "&#xef0e;"
dnd: str = "&#xea35;"
darkmode: str = "&#xfe56;"
wifi_off: str = "&#xecfa;"
bluetooth_off: str = "&#xeceb;"
night_off: str = "&#xf162;"
dnd_off: str = "&#xece9;"
# Bluetooth
bluetooth_connected: str = "&#xecea;"
bluetooth_disconnected: str = "&#xf081;"
# Player
pause: str = "&#xf690;"
play: str = "&#xf691;"
stop: str = "&#xf695;"
skip_back: str = "&#xf693;"
skip_forward: str = "&#xf694;"
prev: str = "&#xf697;"
next: str = "&#xf696;"
shuffle: str = "&#xf000;"
repeat: str = "&#xeb72;"
music: str = "&#xeafc;"
rewind_backward_5: str = "&#xfabf;"
rewind_forward_5: str = "&#xfac7;"
# Volume
vol_off: str = "&#xf1c3;"
vol_mute: str = "&#xeb50;"
vol_medium: str = "&#xeb4f;"
vol_high: str = "&#xeb51;"
mic: str = "&#xeaf0;"
mic_muted: str = "&#xed16;"
# Overview
circle_plus: str = "&#xea69;"
# Pins
copy_plus: str = "&#xfdae;"
# Clipboard Manager
clipboard: str = "&#xea6f;"
clip_text: str = "&#xf089;"
# Confirm
accept: str = "&#xea5e;"
cancel: str = "&#xeb55;"
trash: str = "&#xeb41;"
clear: str = "&#xef88;"
# Config
config: str = "&#xeb20;"
# Icons
firefox: str = "&#xecfd;"
chromium: str = "&#xec18;"
spotify: str = "&#xfe86;"
disc: str = "&#x1003e;"
disc_off: str = "&#xf118;"
# Misc
dot: str = "&#xf698;"
palette: str = "&#xeb01;"
cloud_off: str = "&#xed3e;"
loader: str = "&#xeca3;"
radar: str = "&#xf017;"
emoji: str = "&#xeaf7;"
keyboard: str = "&#xebd6;"
terminal: str = "&#xebef;"
exceptions: list[str] = ["font_family", "font_weight", "span"]
def apply_span() -> None:
global_dict = globals()
for key in global_dict:
if key not in exceptions and not key.startswith("__"):
global_dict[key] = f"{span}{global_dict[key]}</span>"
apply_span()
+141
View File
@@ -0,0 +1,141 @@
import subprocess
import json
import config.data as data
def get_current_workspace():
"""
Get the current workspace ID using hyprctl.
"""
try:
result = subprocess.run(
["hyprctl", "activeworkspace"], capture_output=True, text=True
)
# Assume the output similar to: "ID <number>"
# Extracting the number from the output
parts = result.stdout.split()
for i, part in enumerate(parts):
if part == "ID" and i + 1 < len(parts):
return int(parts[i + 1])
except Exception as e:
print(f"Error getting current workspace: {e}")
return -1
def get_screen_dimensions():
"""
Get screen dimensions from hyprctl.
Returns:
tuple: (width, height) of the monitor containing the current workspace
"""
try:
# Get current workspace
workspace_id = get_current_workspace()
# Get monitor information
result = subprocess.run(
["hyprctl", "-j", "monitors"], capture_output=True, text=True
)
monitors = json.loads(result.stdout)
# Find the monitor containing our workspace
for monitor in monitors:
if monitor.get("activeWorkspace", {}).get("id") == workspace_id:
return monitor.get("width", data.CURRENT_WIDTH), monitor.get(
"height", data.CURRENT_HEIGHT
)
# Fallback to first monitor
if monitors:
return monitors[0].get("width", data.CURRENT_WIDTH), monitors[0].get(
"height", data.CURRENT_HEIGHT
)
except Exception as e:
print(f"Error getting screen dimensions: {e}")
# Default fallback values
return data.CURRENT_WIDTH, data.CURRENT_HEIGHT
def check_occlusion(occlusion_region, workspace=None):
"""
Check if a region is occupied by any window on a given workspace.
Parameters:
occlusion_region: Can be one of:
- tuple (side, size): where side is "top", "bottom", "left", or "right"
and size is the pixel width of the region
- tuple (x, y, width, height): The full region coordinates (legacy format)
workspace (int, optional): The workspace ID to check. If None, the current workspace is used.
Returns:
bool: True if any window overlaps with the occlusion region, False otherwise.
"""
if workspace is None:
workspace = get_current_workspace()
# Handle simplified side-based format
if isinstance(occlusion_region, tuple) and len(occlusion_region) == 2:
side, size = occlusion_region
if isinstance(side, str):
# Convert side-based format to coordinates
screen_width, screen_height = get_screen_dimensions()
if side.lower() == "bottom":
occlusion_region = (0, screen_height - size, screen_width, size)
elif side.lower() == "top":
occlusion_region = (0, 0, screen_width, size)
elif side.lower() == "left":
occlusion_region = (0, 0, size, screen_height)
elif side.lower() == "right":
occlusion_region = (screen_width - size, 0, size, screen_height)
# Ensure occlusion_region is in the correct format (x, y, width, height)
if not isinstance(occlusion_region, tuple) or len(occlusion_region) != 4:
print(f"Invalid occlusion region format: {occlusion_region}")
return False
try:
result = subprocess.run(
["hyprctl", "-j", "clients"], capture_output=True, text=True
)
clients = json.loads(result.stdout)
except Exception as e:
print(f"Error retrieving client windows: {e}")
return False
occ_x, occ_y, occ_width, occ_height = occlusion_region
occ_x2 = occ_x + occ_width
occ_y2 = occ_y + occ_height
for client in clients:
# Check if client is mapped
if not client.get("mapped", False):
continue
# Ensure client has proper workspace information and matches the workspace
client_workspace = client.get("workspace", {})
if client_workspace.get("id") != workspace:
continue
# Ensure client has position and size info
position = client.get("at")
size = client.get("size")
if not position or not size:
continue
x, y = position
width, height = size
win_x1, win_y1 = x, y
win_x2, win_y2 = x + width, y + height
# Check for intersection between the window and occlusion region
if not (
win_x2 <= occ_x or win_x1 >= occ_x2 or win_y2 <= occ_y or win_y1 >= occ_y2
):
return True # Occlusion region is occupied
return False # No window overlaps the occlusion region
+279
View File
@@ -0,0 +1,279 @@
from typing import ClassVar
import gi
from fabric.hyprland.service import HyprlandEvent
from fabric.hyprland.widgets import get_hyprland_connection
from fabric.widgets.box import Box
from fabric.widgets.wayland import WaylandWindow
from fabric.widgets.widget import Widget
from gi.repository import Gdk, GLib, GObject, GtkLayerShell
from loguru import logger
gi.require_versions(
{"Gtk": "3.0", "Gdk": "3.0", "GtkLayerShell": "0.1", "GObject": "2.0"}
)
class PopoverManager:
"""Singleton manager to handle shared resources for popovers."""
_instance = None
@classmethod
def get_instance(cls):
if cls._instance is None:
cls._instance = cls()
return cls._instance
def __init__(self):
# Shared overlay window for all popovers
self.overlay = WaylandWindow(
name="popover-overlay",
style_classes="popover-overlay",
title="fabric-shell-popover-overlay",
anchor="left top right bottom",
margin="-50px 0px 0px 0px",
exclusivity="auto",
layer="overlay",
type="top-level",
visible=False,
all_visible=False,
style="background-color: rgba(0,0,0,0.0);",
)
# Add empty box so GTK doesn't complain
self.overlay.add(Box())
# Keep track of active popovers
self.active_popover = None
self.available_windows = []
# Close popover when clicking overlay
self.overlay.connect("button-press-event", self._on_overlay_clicked)
self._hyprland_connection = get_hyprland_connection()
self._hyprland_connection.connect(
"event::focusedmonv2", self._on_monitor_change
)
def _on_monitor_change(self, _, event: HyprlandEvent):
if self.active_popover:
self.active_popover.hide_popover()
return True
def _on_overlay_clicked(self, widget, event):
if self.active_popover:
self.active_popover.hide_popover()
return True
def get_popover_window(self):
"""Get an available popover window or create a new one."""
if self.available_windows:
return self.available_windows.pop()
window = WaylandWindow(
type="popup",
layer="overlay",
name="popover-window",
anchor="left top",
visible=False,
all_visible=False,
)
GtkLayerShell.set_keyboard_interactivity(window, True)
window.set_keep_above(True)
return window
def return_popover_window(self, window):
"""Return a popover window to the pool."""
# Remove any children
for child in window.get_children():
window.remove(child)
window.hide()
# Only keep a reasonable number of windows in the pool
if len(self.available_windows) < 5:
self.available_windows.append(window)
else:
# Let the window be garbage collected
window.destroy()
def activate_popover(self, popover):
"""Set the active popover and show overlay."""
if self.active_popover and self.active_popover != popover:
self.active_popover.hide_popover()
self.active_popover = popover
self.overlay.show()
@GObject.Signal(
flags=GObject.SignalFlags.RUN_LAST, return_type=GObject.TYPE_NONE, arg_types=()
)
def popover_opened(widget: Widget): ...
@GObject.Signal(
flags=GObject.SignalFlags.RUN_LAST, return_type=GObject.TYPE_NONE, arg_types=()
)
def popover_closed(widget: Widget): ...
@GObject.type_register
class Popover(Widget):
"""Memory-efficient popover implementation."""
__gsignals__: ClassVar = {
"popover-opened": (GObject.SignalFlags.RUN_LAST, GObject.TYPE_NONE, ()),
"popover-closed": (GObject.SignalFlags.RUN_LAST, GObject.TYPE_NONE, ()),
}
def __init__(
self,
point_to,
content_factory=None,
content=None,
):
super().__init__()
"""
Initialize a popover.
Args:
content_factory: Function that returns content widget when called
point_to: Widget to position the popover next to
"""
self._content_factory = content_factory
self._point_to = point_to
self._content_window = None
self._content = content
self._visible = False
self._destroy_timeout = None
# Use weak reference to avoid circular reference issues
self._manager = PopoverManager.get_instance()
def set_content_factory(self, content_factory):
"""Set the content factory for the popover."""
self._content_factory = content_factory
def set_content(self, content):
"""Set the content for the popover."""
self._content = content
def _on_key_press(self, widget, event):
if event.keyval == Gdk.KEY_Escape and self._manager.active_popover:
self._manager.active_popover.hide_popover()
def open(self):
if self._destroy_timeout is not None:
GLib.source_remove(self._destroy_timeout)
self._destroy_timeout = None
if not self._content_window:
try:
self._create_popover()
except Exception as e:
logger.error(f"Could not create popover! Error: {e}")
else:
self._manager.activate_popover(self)
self._content_window.show()
self._content_window.steal_input()
self._visible = True
self.emit("popover-opened")
def _calculate_margins(self):
widget_allocation = self._point_to.get_allocation()
popover_size = self._content_window.get_size()
display = Gdk.Display.get_default()
screen = display.get_default()
monitor_at_window = screen.get_monitor_at_window(self._point_to.get_window())
monitor_geometry = monitor_at_window.get_geometry()
x = (
widget_allocation.x
+ (widget_allocation.width / 2)
- (popover_size.width / 2)
)
# Position below the button instead of above
y = widget_allocation.y + widget_allocation.height + 5 # 5px gap below button
# Ensure popover stays within monitor bounds
if x <= 0:
x = widget_allocation.x
elif x + popover_size.width >= monitor_geometry.width:
x = widget_allocation.x - popover_size.width + widget_allocation.width
# If popover would go below monitor, position it above the button
if y + popover_size.height > monitor_geometry.y + monitor_geometry.height:
y = widget_allocation.y - popover_size.height - 5 # 5px gap above button
return [y, 0, 0, x]
def set_position(self, position: tuple[int, int, int, int] | None = None):
if position is None:
self._content_window.set_margin(self._calculate_margins())
return False
self._content_window.set_margin(position)
return False
def _on_content_ready(self, widget, event):
self.set_position()
def _create_popover(self):
if self._content is None and self._content_factory is not None:
self._content = self._content_factory()
# Get a window from the pool
self._content_window = self._manager.get_popover_window()
# This is a hack to fix wrong positioning for widgets that are not rendered
# immediately (e.g., Gtk.Calendar())
self._content.connect("draw", self._on_content_ready)
# Add content to window
self._content_window.add(
Box(style_classes="popover-content", children=self._content)
)
self._content_window.connect("focus-out-event", self._on_popover_focus_out)
self._content_window.connect("key-press-event", self._on_key_press)
self._manager.activate_popover(self)
self._content_window.show()
self._content_window.steal_input()
self._visible = True
def _on_popover_focus_out(self, widget, event):
# This helps with keyboard focus issues
GLib.timeout_add(100, self.hide_popover)
return False
def hide_popover(self):
if not self._visible or not self._content_window:
return False
self._content_window.hide()
self._manager.overlay.hide()
self._visible = False
if not self._destroy_timeout:
self._destroy_timeout = GLib.timeout_add(1000 * 5, self._destroy_popover)
self.emit("popover-closed")
return False
def _destroy_popover(self):
"""Return resources to the pool and clear references."""
self._destroy_timeout = None
self._visible = False
if self._content_window:
# Return window to the pool
self._manager.return_popover_window(self._content_window)
self._content_window = None
# Allow content to be garbage collected if no longer needed
self._content = None
return False