Added panel widget

This commit is contained in:
tr1x_em committed 2025-08-10 09:20:22 +05:30
1 parent cec950422d
commit 95a93cadbf
3 files changed
+448 -10

No files matched your search

+340
View File
@@ -1,9 +1,16 @@
import re
import subprocess
import os
import gi
gi.require_version("GdkPixbuf", "2.0")
gi.require_version("Gtk", "3.0")
from gi.repository import GdkPixbuf, Gtk # type: ignore
from fabric.utils.helpers import get_relative_path
from utils.roam import modus_service
from utils.icon_resolver import IconResolver
def read_dmi(field):
@@ -43,6 +50,339 @@ def get_gpu_name():
return "Unknown GPU"
def get_executable_path(exec_string):
"""Extract and find the actual executable path from Exec field"""
if not exec_string:
return None
# Remove common exec modifiers and arguments
exec_parts = exec_string.split()
if not exec_parts:
return None
executable = exec_parts[0]
# Remove common prefixes
prefixes_to_remove = ['env', 'bash', 'sh', '/usr/bin/env']
while executable in prefixes_to_remove and len(exec_parts) > 1:
exec_parts.pop(0)
executable = exec_parts[0]
# If it's already an absolute path, check if it exists
if executable.startswith('/'):
if os.path.exists(executable):
return executable
return None
# Search in PATH
try:
result = subprocess.run(['which', executable], capture_output=True, text=True)
if result.returncode == 0:
return result.stdout.strip()
except Exception:
pass
return None
def get_app_info(wmclass):
"""Get comprehensive application information from .desktop file"""
if not wmclass:
return {
"name": "Desktop",
"version": "",
"comment": "Desktop Environment",
"icon": "desktop",
"exec": "",
"location": "",
"categories": "",
"desktop_file": ""
}
desktop_paths = [
"/usr/share/applications",
"/var/lib/flatpak/exports/share/applications",
os.path.expanduser("~/.local/share/applications"),
"/usr/local/share/applications"
]
for path in desktop_paths:
if not os.path.exists(path):
continue
# Try exact match first
exact_matches = [f for f in os.listdir(path)
if f.lower() == f"{wmclass.lower()}.desktop"]
# Then try starts with
startswith_matches = [f for f in os.listdir(path)
if f.startswith(wmclass.lower()) and f.endswith('.desktop')]
# Finally try contains
contains_matches = [f for f in os.listdir(path)
if wmclass.lower() in f.lower() and f.endswith('.desktop')]
# Process matches in order of preference
for matches in [exact_matches, startswith_matches, contains_matches]:
for filename in matches:
desktop_file = os.path.join(path, filename)
try:
with open(desktop_file, 'r', encoding='utf-8') as f:
content = f.read()
name = wmclass.title()
version = ""
comment = ""
icon = wmclass.lower()
exec_cmd = ""
categories = ""
# Parse desktop file
in_desktop_entry = False
for line in content.split('\n'):
line = line.strip()
if line == '[Desktop Entry]':
in_desktop_entry = True
continue
elif line.startswith('[') and line.endswith(']'):
in_desktop_entry = False
continue
if not in_desktop_entry or '=' not in line:
continue
key, value = line.split('=', 1)
if key == 'Name':
name = value
elif key == 'Version':
version = value
elif key == 'Comment':
comment = value
elif key == 'GenericName' and not comment:
# Use GenericName as fallback description
comment = value
elif key == 'Icon':
icon = value
elif key == 'Exec':
exec_cmd = value
elif key == 'Categories':
categories = value
# Get executable location
location = get_executable_path(exec_cmd)
return {
"name": name,
"version": version,
"comment": comment,
"icon": icon,
"exec": exec_cmd,
"location": location or "",
"categories": categories,
"desktop_file": desktop_file
}
except Exception:
continue
# Fallback: try to find executable in PATH
location = ""
try:
result = subprocess.run(['which', wmclass.lower()], capture_output=True, text=True)
if result.returncode == 0:
location = result.stdout.strip()
except Exception:
pass
return {
"name": wmclass.title() if wmclass else "Unknown Application",
"version": "",
"comment": "",
"icon": wmclass.lower() if wmclass else "application-x-executable",
"exec": "",
"location": location,
"categories": "",
"desktop_file": ""
}
class AboutApp(Gtk.Window):
def __init__(self, app_name="Unknown Application", wmclass=""):
super().__init__(title=f"About {app_name}")
self.app_name = app_name
self.wmclass = wmclass
self.icon_resolver = IconResolver()
self.set_default_size(400, 500)
self.set_size_request(400, 480)
self.set_resizable(False)
self.set_title(f"About {app_name}")
self.set_name("about-app")
self.set_visible(False)
self.setup_ui()
def setup_ui(self):
app_info = get_app_info(self.wmclass)
main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
main_box.set_margin_top(20)
main_box.set_margin_bottom(20)
main_box.set_margin_start(25)
main_box.set_margin_end(25)
# App Icon
logo_box = Gtk.Box(halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER)
try:
# Use IconResolver's get_icon_pixbuf method like other parts of the project
icon_pixbuf = self.icon_resolver.get_icon_pixbuf(app_info["icon"], 128)
if icon_pixbuf:
logo = Gtk.Image.new_from_pixbuf(icon_pixbuf)
else:
raise Exception("Icon pixbuf not found")
except Exception:
# Fallback: try direct file path if it's an absolute path
try:
if app_info["icon"].startswith('/') and os.path.exists(app_info["icon"]):
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
app_info["icon"], 128, 128, preserve_aspect_ratio=True
)
logo = Gtk.Image.new_from_pixbuf(pixbuf)
else:
raise Exception("Direct path failed")
except Exception:
# Final fallback: emoji
logo = Gtk.Label()
logo.set_markup("<span size='72000'>📱</span>")
logo_box.pack_start(logo, False, False, 0)
logo_box.set_margin_bottom(15)
# App Name
app_name_label = Gtk.Label()
app_name_label.set_markup(f"<b><span size='18000'>{app_info['name']}</span></b>")
app_name_label.set_halign(Gtk.Align.CENTER)
app_name_label.set_margin_bottom(5)
# Version (if available)
version_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
if app_info["version"]:
version_label = Gtk.Label(label=f"Version {app_info['version']}")
version_label.set_name("version-label")
version_label.set_halign(Gtk.Align.CENTER)
version_box.pack_start(version_label, False, False, 0)
version_box.set_margin_bottom(15)
# Description/Comment - Make it more prominent
description_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
if app_info["comment"]:
# Create a frame for the description to make it stand out
desc_frame = Gtk.Frame()
desc_frame.set_shadow_type(Gtk.ShadowType.IN)
description_label = Gtk.Label()
description_label.set_markup(f"<i>{app_info['comment']}</i>")
description_label.set_justify(Gtk.Justification.CENTER)
description_label.set_halign(Gtk.Align.CENTER)
description_label.set_line_wrap(True)
description_label.set_max_width_chars(45)
description_label.set_margin_top(8)
description_label.set_margin_bottom(8)
description_label.set_margin_start(12)
description_label.set_margin_end(12)
desc_frame.add(description_label)
description_box.pack_start(desc_frame, False, False, 0)
else:
# Show a placeholder if no description is available
placeholder_label = Gtk.Label()
placeholder_label.set_markup("<i><span foreground='#888888'>No description available</span></i>")
placeholder_label.set_halign(Gtk.Align.CENTER)
description_box.pack_start(placeholder_label, False, False, 0)
description_box.set_margin_bottom(15)
# Information Grid
info_frame = Gtk.Frame()
info_frame.set_label("Application Information")
info_frame.set_label_align(0.5, 0.5)
info_grid = Gtk.Grid()
info_grid.set_row_spacing(8)
info_grid.set_column_spacing(15)
info_grid.set_margin_top(10)
info_grid.set_margin_bottom(10)
info_grid.set_margin_start(15)
info_grid.set_margin_end(15)
info_grid.set_valign(Gtk.Align.CENTER)
info_grid.set_halign(Gtk.Align.FILL)
def make_info_row(label_text, value_text, row):
"""Create a row in the info grid"""
label = Gtk.Label(label=f"{label_text}:")
label.set_halign(Gtk.Align.END)
label.set_markup(f"<b>{label_text}:</b>")
value = Gtk.Label(label=value_text)
value.set_halign(Gtk.Align.START)
value.set_line_wrap(True)
value.set_max_width_chars(30)
value.set_name("info-value-label")
info_grid.attach(label, 0, row, 1, 1)
info_grid.attach(value, 1, row, 1, 1)
row = 0
# Application Name
make_info_row("Name", app_info["name"], row)
row += 1
# Version
if app_info["version"]:
make_info_row("Version", app_info["version"], row)
row += 1
# Executable Location
if app_info["location"]:
make_info_row("Location", app_info["location"], row)
row += 1
# Window Class
if self.wmclass:
make_info_row("Window Class", self.wmclass, row)
row += 1
# Categories
if app_info["categories"]:
categories = app_info["categories"].replace(";", ", ").strip(", ")
make_info_row("Categories", categories, row)
row += 1
# Desktop File
if app_info["desktop_file"]:
desktop_file_name = os.path.basename(app_info["desktop_file"])
make_info_row("Desktop File", desktop_file_name, row)
row += 1
info_frame.add(info_grid)
# Layout
main_box.pack_start(logo_box, False, False, 0)
main_box.pack_start(app_name_label, False, False, 0)
main_box.pack_start(version_box, False, False, 0)
main_box.pack_start(description_box, False, False, 0)
main_box.pack_start(info_frame, False, False, 0)
self.add(main_box)
def toggle(self, b):
if self.get_visible():
self.hide()
else:
self.show_all()
class About(Gtk.Window):
def __init__(self):
super().__init__(title="About Menu")
+79 -8
View File
@@ -11,6 +11,7 @@ from fabric.widgets.centerbox import CenterBox
from fabric.widgets.label import Label
from fabric.widgets.scale import Scale
from fabric.widgets.svg import Svg
from fabric.widgets.revealer import Revealer
from modules.controlcenter.bluetooth import BluetoothConnections
from modules.controlcenter.wifi import WifiConnections
from services.brightness import Brightness
@@ -350,15 +351,29 @@ class ModusControlCenter(Window):
self.per_app_volume_widgets = None
self.expanded_player_widgets = None
# Create main content boxes
self.center_box = CenterBox(start_children=[self.widgets])
self.bluetooth_center_box = None
self.wifi_center_box = None
self.per_app_volume_center_box = None
self.expanded_player_center_box = None
# Create revealers for crossfade transitions
self.main_revealer = Revealer(
child=self.center_box,
transition_type="crossfade",
transition_duration=250,
child_revealed=True,
)
self.expanded_player_revealer = None # Will be created lazily
self.widgets.set_size_request(300, -1)
self.children = self.center_box
self.children = self.main_revealer
# Track current state for smooth transitions
self.current_view = "main" # main, expanded_player
# Connect to visibility changes for cleanup
self.connect("notify::visible", self._on_visibility_changed)
@@ -511,6 +526,15 @@ class ModusControlCenter(Window):
start_children=[self.expanded_player_widgets]
)
self.expanded_player_center_box.set_size_request(300, -1)
# Create revealer for expanded player
if self.expanded_player_revealer is None:
self.expanded_player_revealer = Revealer(
child=self.expanded_player_center_box,
transition_type="crossfade",
transition_duration=250,
child_revealed=False,
)
def set_dont_disturb(self, *_):
self.focus_mode = not self.focus_mode
@@ -632,35 +656,82 @@ class ModusControlCenter(Window):
self.has_wifi_open = True
def close_bluetooth(self, *_):
idle_add(lambda *_: self.set_children(self.center_box))
if self.current_view == "expanded_player":
self._crossfade_to_view("main")
else:
idle_add(lambda *_: self.set_children(self.center_box))
self.has_bluetooth_open = False
def close_wifi(self, *_):
idle_add(lambda *_: self.set_children(self.center_box))
if self.current_view == "expanded_player":
self._crossfade_to_view("main")
else:
idle_add(lambda *_: self.set_children(self.center_box))
self.has_wifi_open = False
def open_per_app_volume(self, *_):
self._ensure_per_app_volume_widgets()
idle_add(lambda *_: self.set_children(self.per_app_volume_center_box))
if self.current_view == "expanded_player":
# If coming from expanded player, use crossfade
self._crossfade_to_view("main")
GLib.timeout_add(250, lambda: idle_add(lambda *_: self.set_children(self.per_app_volume_center_box)))
else:
idle_add(lambda *_: self.set_children(self.per_app_volume_center_box))
self.has_per_app_volume_open = True
# Refresh the app list when opening
if self._per_app_volume_widget:
self._per_app_volume_widget.refresh()
def close_per_app_volume(self, *_):
idle_add(lambda *_: self.set_children(self.center_box))
if self.current_view == "expanded_player":
self._crossfade_to_view("main")
else:
idle_add(lambda *_: self.set_children(self.center_box))
self.has_per_app_volume_open = False
def _crossfade_to_view(self, target_view):
"""Handle crossfade transition between views"""
if self.current_view == target_view:
return
if target_view == "expanded_player":
# Transitioning to expanded player
self._ensure_expanded_player_widgets()
# Add expanded player revealer to window if not already added
if self.expanded_player_revealer not in self.get_children():
self.add(self.expanded_player_revealer)
# Start crossfade: hide main, show expanded player
self.main_revealer.set_reveal_child(False)
GLib.timeout_add(125, lambda: self.expanded_player_revealer.set_reveal_child(True))
elif target_view == "main":
# Transitioning back to main
if self.expanded_player_revealer:
# Start crossfade: hide expanded player, show main
self.expanded_player_revealer.set_reveal_child(False)
GLib.timeout_add(125, lambda: self.main_revealer.set_reveal_child(True))
# Clean up expanded player revealer after transition
GLib.timeout_add(375, self._cleanup_expanded_player_revealer)
self.current_view = target_view
def _cleanup_expanded_player_revealer(self):
"""Remove expanded player revealer from window after transition"""
if self.expanded_player_revealer and self.expanded_player_revealer in self.get_children():
self.remove(self.expanded_player_revealer)
return False
def open_expanded_player(self, *_):
self._ensure_expanded_player_widgets()
idle_add(lambda *_: self.set_children(self.expanded_player_center_box))
self._crossfade_to_view("expanded_player")
self.has_expanded_player_open = True
# Refresh the player when opening
if self._expanded_player_widget:
self._expanded_player_widget.refresh()
def close_expanded_player(self, *_):
idle_add(lambda *_: self.set_children(self.center_box))
self._crossfade_to_view("main")
self.has_expanded_player_open = False
def _set_mousecapture(self, visible: bool):
+29 -2
View File
@@ -1,5 +1,6 @@
import os
import subprocess
import json
from fabric.hyprland.widgets import HyprlandActiveWindow as ActiveWindow
from fabric.utils import FormattedString
@@ -7,7 +8,7 @@ from fabric.widgets.box import Box
from fabric.widgets.button import Button
from fabric.widgets.centerbox import CenterBox
from fabric.widgets.label import Label
from modules.about import About
from modules.about import About, AboutApp
from utils.roam import modus_service
from widgets.dropdown import ModusDropdown, dropdown_divider
from widgets.mousecapture import DropDownMouseCapture
@@ -92,6 +93,31 @@ def format_window(title, wmclass):
return name
def show_about_app():
"""Show about dialog for current active application"""
try:
# Get current window info from Hyprland
result = subprocess.run(
["hyprctl", "activewindow", "-j"],
capture_output=True,
text=True,
check=True
)
import json
window_info = json.loads(result.stdout)
wmclass = window_info.get("class", "")
title = window_info.get("title", "")
app_name = modus_service.current_active_app_name or "Unknown Application"
about_window = AboutApp(app_name=app_name, wmclass=wmclass)
about_window.toggle(None)
except Exception as e:
# Fallback: show about dialog with just the app name
app_name = modus_service.current_active_app_name or "Unknown Application"
about_window = AboutApp(app_name=app_name, wmclass="")
about_window.toggle(None)
def dropdown_option(
label: str = "",
keybind: str = "",
@@ -186,7 +212,8 @@ class MenuBarDropdowns:
# Global menu dropdowns
self.global_title_menu_about = dropdown_option(
f"About {modus_service.current_active_app_name}"
f"About {modus_service.current_active_app_name}",
on_clicked=lambda _: show_about_app()
)
self.global_menu_title = DropDownMouseCapture(
layer="bottom",