This commit is contained in:
sankalp committed 2025-06-11 14:42:26 +05:45
1 parent b6a5343a42
commit a3c423c8b8
18 files changed
+2023 -966

No files matched your search

+477
View File
@@ -0,0 +1,477 @@
import json
import logging
import cairo
from fabric.widgets.box import Box
from fabric.widgets.button import Button
from fabric.widgets.image import Image
from fabric.utils import exec_shell_command, exec_shell_command_async, get_relative_path
from fabric.utils.helpers import get_desktop_applications
from fabric.hyprland.widgets import get_hyprland_connection
from gi.repository import Gdk, GLib, Gtk
import config.data as data
from utils.icon_resolver import IconResolver
def read_config():
"""Read and return the full configuration from the JSON file, handling missing file."""
config_path = get_relative_path("../../config/dock.json")
try:
with open(config_path, "r") as file:
config_data = json.load(file)
if (
"pinned_apps" in config_data
and config_data["pinned_apps"]
and isinstance(config_data["pinned_apps"][0], str)
):
all_apps = get_desktop_applications()
app_map = {app.name: app for app in all_apps if app.name}
old_pinned = config_data["pinned_apps"]
config_data["pinned_apps"] = []
for app_id in old_pinned:
app = app_map.get(app_id)
if app:
app_data_obj = {
"name": app.name,
"display_name": app.display_name,
"window_class": app.window_class,
"executable": app.executable,
"command_line": app.command_line,
}
config_data["pinned_apps"].append(app_data_obj)
else:
config_data["pinned_apps"].append({"name": app_id})
except (FileNotFoundError, json.JSONDecodeError):
config_data = {"pinned_apps": []}
return config_data
class Applications(Box):
def __init__(self, orientation_val="h", **kwargs):
super().__init__(
name="applications-dock",
orientation=orientation_val,
spacing=4,
**kwargs
)
self.config = read_config()
self.conn = get_hyprland_connection()
self.icon_resolver = IconResolver()
self.pinned = self.config.get("pinned_apps", [])
self.config_path = get_relative_path("../../config/dock.json")
self.app_map = {}
self._all_apps = get_desktop_applications()
self.app_identifiers = self._build_app_identifiers_map()
self._drag_in_progress = False
# Initialize with empty content
self.update_applications()
# Connect to Hyprland events for live updates
if self.conn.ready:
self.update_applications()
else:
self.conn.connect("event::ready", lambda *args: self.update_applications())
# Connect to window events to update in real-time
for ev in ("activewindow", "openwindow", "closewindow", "changefloatingmode"):
self.conn.connect(f"event::{ev}", lambda *args: self.update_applications())
# Set up periodic refresh for config changes
GLib.timeout_add_seconds(1, self.check_config_change)
def _build_app_identifiers_map(self):
app_map = {}
for app in self._all_apps:
if app.window_class:
app_map[app.window_class.lower()] = app
if app.executable:
app_map[app.executable.lower()] = app
if app.name:
app_map[app.name.lower()] = app
return app_map
def find_app_by_key(self, key):
if not key:
return None
key_lower = key.lower() if isinstance(key, str) else ""
return self.app_identifiers.get(key_lower)
def find_app(self, app_identifier):
if not app_identifier:
return None
if isinstance(app_identifier, dict):
for key in ["window_class", "executable", "command_line", "name", "display_name"]:
if key in app_identifier and app_identifier[key]:
app = self.find_app_by_key(app_identifier[key])
if app:
return app
return None
return self.find_app_by_key(app_identifier)
def create_button(self, app_identifier, instances):
desktop_app = self.find_app(app_identifier)
icon_img = None
display_name = None
icon_size = data.DOCK_ICON_SIZE
if desktop_app:
icon_img = desktop_app.get_icon_pixbuf(size=icon_size)
display_name = desktop_app.display_name or desktop_app.name
id_value = (
app_identifier["name"]
if isinstance(app_identifier, dict)
else app_identifier
)
if not icon_img:
icon_img = self.icon_resolver.get_icon_pixbuf(id_value, icon_size)
if not icon_img:
icon_img = self.icon_resolver.get_icon_pixbuf(
"application-x-executable-symbolic", icon_size
)
if not icon_img:
icon_img = self.icon_resolver.get_icon_pixbuf(
"image-missing", icon_size
)
items = [Image(pixbuf=icon_img)]
tooltip = display_name or (id_value if isinstance(id_value, str) else "Unknown")
if not display_name and instances and instances[0].get("title"):
tooltip = instances[0]["title"]
button = Button(
child=Box(
name="dock-icon", orientation="v", h_align="center", children=items
),
on_clicked=lambda *a: self.handle_app(
app_identifier, instances, desktop_app
),
tooltip_text=tooltip,
name="dock-app-button",
)
button.app_identifier = app_identifier
button.desktop_app = desktop_app
button.instances = instances
if instances:
button.add_style_class("instance")
button.drag_source_set(
Gdk.ModifierType.BUTTON1_MASK,
[Gtk.TargetEntry.new("text/plain", Gtk.TargetFlags.SAME_APP, 0)],
Gdk.DragAction.MOVE,
)
# Add drag destination capabilities
button.drag_dest_set(
Gtk.DestDefaults.ALL,
[Gtk.TargetEntry.new("text/plain", Gtk.TargetFlags.SAME_APP, 0)],
Gdk.DragAction.MOVE,
)
# Connect drag signals
button.connect("drag-begin", self.on_drag_begin)
button.connect("drag-data-get", self.on_drag_data_get)
button.connect("drag-data-received", self.on_drag_data_received)
button.connect("drag-end", self.on_drag_end)
button.connect("button-press-event", self.on_button_press)
return button
def handle_app(self, app_identifier, instances, desktop_app=None):
if not instances:
if not desktop_app:
desktop_app = self.find_app(app_identifier)
if desktop_app:
launch_success = desktop_app.launch()
if not launch_success:
if desktop_app.command_line:
exec_shell_command_async(f"nohup {desktop_app.command_line} &")
elif desktop_app.executable:
exec_shell_command_async(f"nohup {desktop_app.executable} &")
else:
cmd_to_run = None
if isinstance(app_identifier, dict):
if (
"command_line" in app_identifier
and app_identifier["command_line"]
):
cmd_to_run = app_identifier["command_line"]
elif (
"executable" in app_identifier and app_identifier["executable"]
):
cmd_to_run = app_identifier["executable"]
elif "name" in app_identifier and app_identifier["name"]:
cmd_to_run = app_identifier["name"]
else:
cmd_to_run = app_identifier
if cmd_to_run:
exec_shell_command_async(f"nohup {cmd_to_run} &")
else:
address = instances[0].get("address")
if address:
exec_shell_command(f"hyprctl dispatch focuswindow address:{address}")
def update_pinned_apps_file(self):
config_path = get_relative_path("../../config/dock.json")
try:
with open(config_path, "w") as file:
json.dump(self.config, file, indent=4)
return True
except Exception as e:
logging.error(f"Failed to write dock config: {e}")
return False
def check_config_change(self):
new_config = read_config()
if new_config.get("pinned_apps", []) != self.config.get("pinned_apps", []):
self.config = new_config
self.pinned = self.config.get("pinned_apps", [])
self.update_app_map()
self.update_applications()
return True
def update_app_map(self):
self._all_apps = get_desktop_applications()
self.app_identifiers = self._build_app_identifiers_map()
def _normalize_window_class(self, class_name):
# Implementation from dock.py
return class_name.lower()
def update_applications(self):
# Clear existing children
for child in self.get_children():
self.remove(child)
# Get running windows from Hyprland
clients = []
try:
clients_data = self.conn.send_command("j/clients").reply
if clients_data:
clients = json.loads(clients_data.decode("utf-8"))
except Exception as e:
logging.error(f"Failed to get clients: {e}")
# Group clients by window class
running_windows = {}
for client in clients:
class_name = client.get("class", "").lower()
if class_name:
if class_name not in running_windows:
running_windows[class_name] = []
running_windows[class_name].append(client)
# Create pinned app buttons
pinned_buttons = []
used_window_classes = set()
for app_data_item in self.pinned:
app = self.find_app(app_data_item)
instances = []
matched_class = None
possible_identifiers = []
if isinstance(app_data_item, dict):
for key in ["window_class", "executable", "command_line", "name", "display_name"]:
if key in app_data_item and app_data_item[key]:
possible_identifiers.append(app_data_item[key].lower())
elif isinstance(app_data_item, str):
possible_identifiers.append(app_data_item.lower())
# Try to match with running windows
for class_name, window_instances in running_windows.items():
for identifier in possible_identifiers:
if identifier in class_name or class_name in identifier:
instances = window_instances
matched_class = class_name
break
if matched_class:
break
if matched_class:
used_window_classes.add(matched_class)
pinned_buttons.append(self.create_button(app_data_item, instances))
# Create buttons for non-pinned running apps
open_buttons = []
for class_name, instances in running_windows.items():
if class_name not in used_window_classes:
app = self.find_app_by_key(class_name)
if app:
app_data_obj = {
"name": app.name,
"display_name": app.display_name,
"window_class": app.window_class,
"executable": app.executable,
"command_line": app.command_line,
}
identifier = app_data_obj
else:
identifier = class_name
open_buttons.append(self.create_button(identifier, instances))
# Add all buttons to the dock
children = pinned_buttons
if pinned_buttons and open_buttons:
separator_orientation = (
Gtk.Orientation.VERTICAL
if self.get_orientation() == Gtk.Orientation.HORIZONTAL
else Gtk.Orientation.HORIZONTAL
)
children.append(
Box(
orientation=separator_orientation,
v_expand=False,
h_expand=False,
h_align="center",
v_align="center",
name="dock-separator",
)
)
children.extend(open_buttons)
# Add all children to the dock
for child in children:
self.add(child)
# Show or hide based on whether there are any applications
if len(children) > 0:
self.show_all()
else:
self.hide()
return True
def on_button_press(self, widget, event):
# Handle right-click to pin/unpin applications
if event.button == 3: # Right mouse button
app_identifier = widget.app_identifier
instances = widget.instances
# Check if app is already pinned
is_pinned = False
for pinned_app in self.pinned:
if (isinstance(app_identifier, dict) and isinstance(pinned_app, dict) and
app_identifier.get("name") == pinned_app.get("name")):
is_pinned = True
break
elif app_identifier == pinned_app:
is_pinned = True
break
# If not pinned, add to pinned apps
if not is_pinned:
if hasattr(widget, "desktop_app") and widget.desktop_app:
app = widget.desktop_app
app_data_obj = {
"name": app.name,
"display_name": app.display_name,
"window_class": app.window_class,
"executable": app.executable,
"command_line": app.command_line,
}
self.pinned.append(app_data_obj)
else:
self.pinned.append(app_identifier)
self.config["pinned_apps"] = self.pinned
self.update_pinned_apps_file()
self.update_applications()
return True
return False
def on_drag_begin(self, widget, drag_context):
self._drag_in_progress = True
Gtk.drag_set_icon_surface(drag_context, self.create_surface_from_widget(widget))
def create_surface_from_widget(self, widget):
# Create a surface from the widget for drag icon
allocation = widget.get_allocation()
surface = cairo.ImageSurface(
cairo.FORMAT_ARGB32, allocation.width, allocation.height
)
ctx = cairo.Context(surface)
widget.draw(ctx)
return surface
def on_drag_data_get(self, widget, drag_context, data_obj, info, time):
# Find the index of the button being dragged
children = self.get_children()
index = children.index(widget)
data_obj.set_text(str(index), -1)
def on_drag_data_received(self, widget, drag_context, x, y, data_obj, info, time):
# Get source and target indices
try:
source_index = int(data_obj.get_text())
except (TypeError, ValueError):
return
children = self.get_children()
target_index = children.index(widget)
if source_index != target_index:
# Reorder the pinned apps
if source_index < len(self.pinned) and target_index < len(self.pinned):
item = self.pinned.pop(source_index)
self.pinned.insert(target_index, item)
self.config["pinned_apps"] = self.pinned
self.update_pinned_apps_file()
self.update_applications()
def on_drag_end(self, widget, drag_context):
if not hasattr(self, "_drag_in_progress") or not self._drag_in_progress:
return
# Get pointer position
display = widget.get_display()
seat = display.get_default_seat()
pointer = seat.get_pointer()
screen, x, y = pointer.get_position()
# Get widget position and size
widget_alloc = widget.get_allocation()
widget_x, widget_y = widget.translate_coordinates(self, 0, 0)
if widget_x is None: # Widget might not be in the same window
widget_x, widget_y = 0, 0
widget_width, widget_height = widget_alloc.width, widget_alloc.height
# Check if pointer is outside the dock area
if not (widget_x <= x <= widget_x + widget_width and
widget_y <= y <= widget_y + widget_height):
app_id_dragged = widget.app_identifier
instances_dragged = widget.instances
# Remove pinned app if it's in the pinned list
app_index_dragged = -1
for i, pinned_app_item in enumerate(self.pinned):
if isinstance(app_id_dragged, dict) and isinstance(pinned_app_item, dict):
if app_id_dragged.get("name") == pinned_app_item.get("name"):
app_index_dragged = i
break
elif app_id_dragged == pinned_app_item:
app_index_dragged = i
break
if app_index_dragged >= 0:
self.pinned.pop(app_index_dragged)
self.config["pinned_apps"] = self.pinned
self.update_pinned_apps_file()
self.update_applications()
elif instances_dragged:
address = instances_dragged[0].get("address")
if address:
exec_shell_command(f"hyprctl dispatch focuswindow address:{address}")
self._drag_in_progress = False
+7 -1
View File
@@ -211,7 +211,13 @@ class Battery(Box):
# Update circle progress
self.circle.set_value(percentage / 100.0)
self.level.set_label(f"{int(percentage)}%")
# Only show percentage label if not at 100%
if percentage < 100:
self.level.set_label(f"{int(percentage)}%")
self.level.set_visible(True)
else:
self.level.set_visible(False)
# Remove any existing profile classes
self.circle.remove_style_class("power-saver")
+26 -10
View File
@@ -10,15 +10,13 @@ from modules.dock.metrics import Metrics
from modules.dock.controls import Controls
from modules.dock.workspaces import workspace
from modules.dock.indicators import Indicators
from modules.dock.applications import Applications
class DockComponents(Box):
def __init__(self, orientation_val="h", **kwargs):
super().__init__(
name="system-tray",
orientation=orientation_val,
spacing=4,
**kwargs
name="system-tray", orientation=orientation_val, spacing=4, **kwargs
)
# Initialize component visibility from data
@@ -42,7 +40,13 @@ class DockComponents(Box):
self.controls = Controls()
self.indicators = Indicators()
self.systray = SystemTray(
icon_size=16, name="tray", orientation="h" if not data.VERTICAL else "v"
icon_size=18,
spacing=4,
name="tray",
orientation="h" if not data.VERTICAL else "v",
)
self.applications = Applications(
orientation_val=orientation_val
)
# Add components based on position
@@ -50,6 +54,7 @@ class DockComponents(Box):
self.add(self.date_time)
self.add(self.controls)
self.add(self.indicators)
self.add(self.applications)
self.add(self.battery)
self.add(self.metrics)
self.add(self.workspaces)
@@ -58,6 +63,7 @@ class DockComponents(Box):
self.add(self.workspaces)
self.add(self.metrics)
self.add(self.controls)
self.add(self.applications)
self.add(self.indicators)
self.add(self.battery)
self.add(self.date_time)
@@ -75,6 +81,7 @@ class DockComponents(Box):
"controls": self.controls,
"indicators": self.indicators,
"systray": self.systray,
"applications": self.applications,
}
for component_name, widget in components.items():
@@ -90,19 +97,28 @@ class DockComponents(Box):
"controls": self.controls,
"indicators": self.indicators,
"systray": self.systray,
"applications": self.applications,
}
if component_name in components and component_name in self.component_visibility:
self.component_visibility[component_name] = not self.component_visibility[component_name]
components[component_name].set_visible(self.component_visibility[component_name])
self.component_visibility[component_name] = not self.component_visibility[
component_name
]
components[component_name].set_visible(
self.component_visibility[component_name]
)
config_file = os.path.expanduser(f"~/.config/{data.APP_NAME}/config/config.json")
config_file = os.path.expanduser(
f"~/.config/{data.APP_NAME}/config/config.json"
)
if os.path.exists(config_file):
try:
with open(config_file, "r") as f:
config = json.load(f)
config[f"dock_{component_name}_visible"] = self.component_visibility[component_name]
config[f"dock_{component_name}_visible"] = (
self.component_visibility[component_name]
)
with open(config_file, "w") as f:
json.dump(config, f, indent=4)
@@ -111,4 +127,4 @@ class DockComponents(Box):
return self.component_visibility[component_name]
return None
return None
+51 -654
View File
@@ -1,52 +1,17 @@
import json
import logging
import cairo
import config.data as data
from fabric.hyprland.widgets import get_hyprland_connection
from fabric.utils import (
exec_shell_command,
exec_shell_command_async,
get_relative_path,
idle_add,
remove_handler,
)
from fabric.utils.helpers import get_desktop_applications
from fabric.widgets.box import Box
from fabric.widgets.button import Button
from fabric.widgets.eventbox import EventBox
from fabric.widgets.image import Image
from fabric.widgets.revealer import Revealer
from gi.repository import Gdk, GLib, Gtk
from modules.corners import MyCorner
from utils.icon_resolver import IconResolver
from utils.occlusion import check_occlusion
from utils.wayland import WaylandWindow as Window
from modules.dock.components import DockComponents
def read_config():
config_path = get_relative_path("../../config/dock.json")
with open(config_path, "r") as file:
data = json.load(file)
return data
def createSurfaceFromWidget(widget: Gtk.Widget) -> cairo.ImageSurface:
alloc = widget.get_allocation()
surface = cairo.ImageSurface(
cairo.Format.ARGB32,
alloc.width,
alloc.height,
)
cr = cairo.Context(surface)
cr.set_source_rgba(255, 255, 255, 0)
cr.rectangle(0, 0, alloc.width, alloc.height)
cr.fill()
widget.draw(cr)
return surface
class Dock(Window):
_instances = []
@@ -72,12 +37,13 @@ class Dock(Window):
main_box_h_align_val = "center"
dock_wrapper_orientation_val = Gtk.Orientation.HORIZONTAL
else:
if data.DOCK_POSITION == "Right":
anchor_to_set = "right"
revealer_transition_type = "slide-left"
elif data.DOCK_POSITION == "Left":
# Use DOCK_POSITION directly instead of BAR_POSITION
if data.DOCK_POSITION == "Left":
anchor_to_set = "left"
revealer_transition_type = "slide-right"
elif data.DOCK_POSITION == "Right":
anchor_to_set = "right"
revealer_transition_type = "slide-left"
else:
anchor_to_set = "right"
revealer_transition_type = "slide-left"
@@ -90,20 +56,13 @@ class Dock(Window):
name="dock-window",
layer="top",
anchor=anchor_to_set,
margin="0px 0px 0px 0px",
exclusivity="none",
margin="0px 0px 0px 0px", # Set all margins to 0
exclusivity="auto" if not data.DOCK_AUTO_HIDE else "none",
**kwargs,
)
Dock._instances.append(self)
self.config = read_config()
self.conn = get_hyprland_connection()
self.icon_resolver = IconResolver()
self.pinned = self.config.get("pinned_apps", [])
self.config_path = get_relative_path("../../config/dock.json")
self.app_map = {}
self._all_apps = get_desktop_applications()
self.app_identifiers = self._build_app_identifiers_map()
self.hide_id = None
self._arranger_handler = None
@@ -146,15 +105,10 @@ class Dock(Window):
self.dock_eventbox.connect("enter-notify-event", self._on_dock_enter)
self.dock_eventbox.connect("leave-notify-event", self._on_dock_leave)
# Define separator orientation
separator_orientation = (
Gtk.Orientation.VERTICAL
if self.view.get_orientation() == Gtk.Orientation.HORIZONTAL
else Gtk.Orientation.HORIZONTAL
)
# Create components using DockComponents
self.components = DockComponents(orientation_val="h" if not data.VERTICAL else "v")
self.components = DockComponents(
orientation_val="h" if not data.VERTICAL else "v"
)
# Add components based on position
if self.actual_dock_is_horizontal: # Bottom dock
@@ -164,30 +118,6 @@ class Dock(Window):
else: # Right position
self.view.add(self.components)
# Add system tray to dock
if data.DOCK_POSITION == "Right":
self.view.add(
Box(
orientation=separator_orientation,
v_expand=False,
h_expand=False,
h_align="center",
v_align="center",
name="dock-separator",
)
)
else:
self.view.add(
Box(
orientation=separator_orientation,
v_expand=False,
h_expand=False,
h_align="center",
v_align="center",
name="dock-separator",
)
)
self.corner_left = Box()
self.corner_right = Box()
self.corner_top = Box()
@@ -255,6 +185,7 @@ class Dock(Window):
orientation=Gtk.Orientation.VERTICAL,
v_expand=True,
v_align="fill",
margin=0, # Add explicit margin=0
children=[self.corner_top, self.dock_eventbox, self.corner_bottom],
)
@@ -268,20 +199,41 @@ class Dock(Window):
self.hover_activator = EventBox()
self.hover_activator.set_size_request(
-1 if self.actual_dock_is_horizontal else 1,
1 if self.actual_dock_is_horizontal else -1,
)
# Adjust hover activator size based on position and dock position
if self.actual_dock_is_horizontal:
# Bottom dock
self.hover_activator.set_size_request(-1, 1)
else:
# Vertical dock (Left or Right)
self.hover_activator.set_size_request(1, -1)
self.hover_activator.connect("enter-notify-event", self._on_hover_enter)
self.hover_activator.connect("leave-notify-event", self._on_hover_leave)
self.main_box = Box(
orientation=main_box_orientation_val,
children=[self.hover_activator, self.dock_revealer]
if data.DOCK_POSITION != "Right"
else [self.dock_revealer, self.hover_activator],
h_align=main_box_h_align_val,
)
# Create main box with correct child order based on position
if self.actual_dock_is_horizontal:
# Bottom dock
self.main_box = Box(
orientation=main_box_orientation_val,
children=[self.hover_activator, self.dock_revealer],
h_align=main_box_h_align_val,
)
else:
# Vertical dock
if data.DOCK_POSITION == "Left":
# Left dock - revealer first, then hover activator
self.main_box = Box(
orientation=main_box_orientation_val,
children=[self.dock_revealer, self.hover_activator],
h_align=main_box_h_align_val,
)
else:
# Right dock - hover activator first, then revealer
self.main_box = Box(
orientation=main_box_orientation_val,
children=[self.hover_activator, self.dock_revealer],
h_align=main_box_h_align_val,
)
self.add(self.main_box)
if data.DOCK_THEME in ["Edge", "Dense"]:
@@ -299,74 +251,18 @@ class Dock(Window):
if self.always_occluded:
self.dock_full.add_style_class("occluded")
self.view.drag_source_set(
Gdk.ModifierType.BUTTON1_MASK,
[Gtk.TargetEntry.new("text/plain", Gtk.TargetFlags.SAME_APP, 0)],
Gdk.DragAction.MOVE,
)
self.view.drag_dest_set(
Gtk.DestDefaults.ALL,
[Gtk.TargetEntry.new("text/plain", Gtk.TargetFlags.SAME_APP, 0)],
Gdk.DragAction.MOVE,
)
self.view.connect("drag-data-get", self.on_drag_data_get)
self.view.connect("drag-data-received", self.on_drag_data_received)
self.view.connect("drag-begin", self.on_drag_begin)
self.view.connect("drag-end", self.on_drag_end)
if self.conn.ready:
self.update_dock()
GLib.timeout_add(250, self.check_occlusion_state)
else:
self.conn.connect("event::ready", self.update_dock)
self.conn.connect(
"event::ready",
lambda *args: GLib.timeout_add(250, self.check_occlusion_state),
)
for ev in ("activewindow", "openwindow", "closewindow", "changefloatingmode"):
self.conn.connect(f"event::{ev}", self.update_dock)
self.conn.connect("event::workspace", self.check_hide)
GLib.timeout_add_seconds(1, self.check_config_change)
def _build_app_identifiers_map(self):
identifiers = {}
for app in self._all_apps:
if app.name:
identifiers[app.name.lower()] = app
if app.display_name:
identifiers[app.display_name.lower()] = app
if app.window_class:
identifiers[app.window_class.lower()] = app
if app.executable:
identifiers[app.executable.split("/")[-1].lower()] = app
if app.command_line:
identifiers[app.command_line.split()[0].split("/")[-1].lower()] = app
return identifiers
def _normalize_window_class(self, class_name):
if not class_name:
return ""
normalized = class_name.lower()
suffixes = [".bin", ".exe", ".so", "-bin", "-gtk"]
for suffix in suffixes:
if normalized.endswith(suffix):
normalized = normalized[: -len(suffix)]
return normalized
def _classes_match(self, class1, class2):
if not class1 or not class2:
return False
norm1 = self._normalize_window_class(class1)
norm2 = self._normalize_window_class(class2)
return norm1 == norm2
def on_drag_begin(self, widget, drag_context):
self._drag_in_progress = True
Gtk.drag_set_icon_surface(drag_context, createSurfaceFromWidget(widget))
def _on_hover_enter(self, *args):
self.is_mouse_over_dock_area = True
if self.hide_id:
@@ -401,160 +297,6 @@ class Dock(Window):
self.dock_full.add_style_class("occluded")
return True
def find_app(self, app_identifier):
if not app_identifier:
return None
if isinstance(app_identifier, dict):
for key in [
"window_class",
"executable",
"command_line",
"name",
"display_name",
]:
if key in app_identifier and app_identifier[key]:
app = self.find_app_by_key(app_identifier[key])
if app:
return app
return None
return self.find_app_by_key(app_identifier)
def find_app_by_key(self, key_value):
if not key_value:
return None
normalized_id = str(key_value).lower()
if normalized_id in self.app_identifiers:
return self.app_identifiers[normalized_id]
for app in self._all_apps:
if app.name and normalized_id in app.name.lower():
return app
if app.display_name and normalized_id in app.display_name.lower():
return app
if app.window_class and normalized_id in app.window_class.lower():
return app
if app.executable and normalized_id in app.executable.lower():
return app
if app.command_line and normalized_id in app.command_line.lower():
return app
return None
def update_app_map(self):
self._all_apps = get_desktop_applications()
self.app_map = {app.name: app for app in self._all_apps if app.name}
self.app_identifiers = self._build_app_identifiers_map()
def create_button(self, app_identifier, instances):
desktop_app = self.find_app(app_identifier)
icon_img = None
display_name = None
if desktop_app:
icon_img = desktop_app.get_icon_pixbuf(size=self.icon_size)
display_name = desktop_app.display_name or desktop_app.name
id_value = (
app_identifier["name"]
if isinstance(app_identifier, dict)
else app_identifier
)
if not icon_img:
icon_img = self.icon_resolver.get_icon_pixbuf(id_value, self.icon_size)
if not icon_img:
icon_img = self.icon_resolver.get_icon_pixbuf(
"application-x-executable-symbolic", self.icon_size
)
if not icon_img:
icon_img = self.icon_resolver.get_icon_pixbuf(
"image-missing", self.icon_size
)
items = [Image(pixbuf=icon_img)]
tooltip = display_name or (id_value if isinstance(id_value, str) else "Unknown")
if not display_name and instances and instances[0].get("title"):
tooltip = instances[0]["title"]
button = Button(
child=Box(
name="dock-icon", orientation="v", h_align="center", children=items
),
on_clicked=lambda *a: self.handle_app(
app_identifier, instances, desktop_app
),
tooltip_text=tooltip,
name="dock-app-button",
)
button.app_identifier = app_identifier
button.desktop_app = desktop_app
button.instances = instances
if instances:
button.add_style_class("instance")
button.drag_source_set(
Gdk.ModifierType.BUTTON1_MASK,
[Gtk.TargetEntry.new("text/plain", Gtk.TargetFlags.SAME_APP, 0)],
Gdk.DragAction.MOVE,
)
button.connect("drag-begin", self.on_drag_begin)
button.connect("drag-end", self.on_drag_end)
button.drag_dest_set(
Gtk.DestDefaults.ALL,
[Gtk.TargetEntry.new("text/plain", Gtk.TargetFlags.SAME_APP, 0)],
Gdk.DragAction.MOVE,
)
button.connect("drag-data-get", self.on_drag_data_get)
button.connect("drag-data-received", self.on_drag_data_received)
button.connect("enter-notify-event", self._on_child_enter)
return button
def handle_app(self, app_identifier, instances, desktop_app=None):
if not instances:
if not desktop_app:
desktop_app = self.find_app(app_identifier)
if desktop_app:
launch_success = desktop_app.launch()
if not launch_success:
if desktop_app.command_line:
exec_shell_command_async(f"nohup {desktop_app.command_line} &")
elif desktop_app.executable:
exec_shell_command_async(f"nohup {desktop_app.executable} &")
else:
cmd_to_run = None
if isinstance(app_identifier, dict):
if (
"command_line" in app_identifier
and app_identifier["command_line"]
):
cmd_to_run = app_identifier["command_line"]
elif (
"executable" in app_identifier and app_identifier["executable"]
):
cmd_to_run = app_identifier["executable"]
elif "name" in app_identifier and app_identifier["name"]:
cmd_to_run = app_identifier["name"]
elif isinstance(app_identifier, str):
cmd_to_run = app_identifier
if cmd_to_run:
exec_shell_command_async(f"nohup {cmd_to_run} &")
else:
focused = self.get_focused()
idx = next(
(i for i, inst in enumerate(instances) if inst["address"] == focused),
-1,
)
next_inst = instances[(idx + 1) % len(instances)]
exec_shell_command(
f"hyprctl dispatch focuswindow address:{next_inst['address']}"
)
def _on_child_enter(self, widget, event):
self.is_mouse_over_dock_area = True
if self.hide_id:
GLib.source_remove(self.hide_id)
self.hide_id = None
return False
def delay_hide(self):
if self.hide_id:
GLib.source_remove(self.hide_id)
@@ -609,218 +351,6 @@ class Dock(Window):
self.dock_revealer.set_reveal_child(False)
self.dock_full.add_style_class("occluded")
def update_dock(self, *args):
self.update_app_map()
arranger_handler = getattr(self, "_arranger_handler", None)
if arranger_handler:
remove_handler(arranger_handler)
clients = self.get_clients()
running_windows = {}
for c in clients:
window_id = None
if class_name := c.get("initialClass", "").lower():
window_id = class_name
elif class_name := c.get("class", "").lower():
window_id = class_name
elif title := c.get("title", "").lower():
possible_name = title.split(" - ")[0].strip()
if possible_name and len(possible_name) > 1:
window_id = possible_name
else:
window_id = title
if not window_id:
window_id = "unknown-app"
running_windows.setdefault(window_id, []).append(c)
normalized_id = self._normalize_window_class(window_id)
if normalized_id != window_id:
running_windows.setdefault(normalized_id, []).extend(
running_windows[window_id]
)
pinned_buttons = []
used_window_classes = set()
for app_data_item in self.pinned:
app = self.find_app(app_data_item)
instances = []
matched_class = None
possible_identifiers = []
if isinstance(app_data_item, dict):
for key in [
"window_class",
"executable",
"command_line",
"name",
"display_name",
]:
if key in app_data_item and app_data_item[key]:
possible_identifiers.append(app_data_item[key].lower())
elif isinstance(app_data_item, str):
possible_identifiers.append(app_data_item.lower())
if app:
if app.window_class:
possible_identifiers.append(app.window_class.lower())
if app.executable:
possible_identifiers.append(app.executable.split("/")[-1].lower())
if app.command_line:
cmd_parts = app.command_line.split()
if cmd_parts:
possible_identifiers.append(cmd_parts[0].split("/")[-1].lower())
if app.name:
possible_identifiers.append(app.name.lower())
if app.display_name:
possible_identifiers.append(app.display_name.lower())
possible_identifiers = list(set(possible_identifiers))
for identifier in possible_identifiers:
if identifier in running_windows:
instances = running_windows[identifier]
matched_class = identifier
break
normalized = self._normalize_window_class(identifier)
if normalized in running_windows:
instances = running_windows[normalized]
matched_class = normalized
break
for window_class_key in running_windows:
if len(identifier) >= 3 and identifier in window_class_key:
instances = running_windows[window_class_key]
matched_class = window_class_key
break
if matched_class:
break
if matched_class:
used_window_classes.add(matched_class)
used_window_classes.add(self._normalize_window_class(matched_class))
pinned_buttons.append(self.create_button(app_data_item, instances))
open_buttons = []
for class_name, instances in running_windows.items():
if class_name not in used_window_classes:
app = None
app = self.app_identifiers.get(class_name)
if not app:
norm_class = self._normalize_window_class(class_name)
app = self.app_identifiers.get(norm_class)
if not app:
app = self.find_app_by_key(class_name)
if not app and instances and instances[0].get("title"):
title = instances[0].get("title", "")
potential_name = title.split(" - ")[0].strip()
if len(potential_name) > 2:
app = self.find_app_by_key(potential_name)
if app:
app_data_obj = {
"name": app.name,
"display_name": app.display_name,
"window_class": app.window_class,
"executable": app.executable,
"command_line": app.command_line,
}
identifier = app_data_obj
else:
identifier = class_name
open_buttons.append(self.create_button(identifier, instances))
children = []
separator_orientation = (
Gtk.Orientation.VERTICAL
if self.view.get_orientation() == Gtk.Orientation.HORIZONTAL
else Gtk.Orientation.HORIZONTAL
)
# Add components based on position
if self.actual_dock_is_horizontal: # Bottom dock
children.append(self.components)
if pinned_buttons or open_buttons:
children.append(
Box(
orientation=separator_orientation,
v_expand=False,
h_expand=False,
h_align="center",
v_align="center",
name="dock-separator",
)
)
children.extend(pinned_buttons)
if pinned_buttons and open_buttons:
children.append(
Box(
orientation=separator_orientation,
v_expand=False,
h_expand=False,
h_align="center",
v_align="center",
name="dock-separator",
)
)
children.extend(open_buttons)
elif data.DOCK_POSITION == "Left":
children.append(self.components)
if pinned_buttons or open_buttons:
children.append(
Box(
orientation=separator_orientation,
v_expand=False,
h_expand=False,
h_align="center",
v_align="center",
name="dock-separator",
)
)
children.extend(pinned_buttons)
if pinned_buttons and open_buttons:
children.append(
Box(
orientation=separator_orientation,
v_expand=False,
h_expand=False,
h_align="center",
v_align="center",
name="dock-separator",
)
)
children.extend(open_buttons)
else: # Right position
children.extend(pinned_buttons)
if pinned_buttons and open_buttons:
children.append(
Box(
orientation=separator_orientation,
v_expand=False,
h_expand=False,
h_align="center",
v_align="center",
name="dock-separator",
)
)
children.extend(open_buttons)
if pinned_buttons or open_buttons:
children.append(
Box(
orientation=separator_orientation,
v_expand=False,
h_expand=False,
h_align="center",
v_align="center",
name="dock-separator",
)
)
children.append(self.components)
self.view.children = children
idle_add(self._update_size)
self._drag_in_progress = False
self.check_occlusion_state()
def get_clients(self):
try:
return json.loads(self.conn.send_command("j/clients").reply.decode())
@@ -855,6 +385,13 @@ class Dock(Window):
self.dock_full.remove_style_class("occluded")
return True
if not data.DOCK_AUTO_HIDE:
if not self.dock_revealer.get_reveal_child():
self.dock_revealer.set_reveal_child(True)
if not self.always_occluded:
self.dock_full.remove_style_class("occluded")
return True
if self.always_occluded:
if self.dock_revealer.get_reveal_child():
self.dock_revealer.set_reveal_child(False)
@@ -880,166 +417,26 @@ class Dock(Window):
return True
def _find_drag_target(self, widget):
children = self.view.get_children()
while widget is not None and widget not in children:
widget = widget.get_parent() if hasattr(widget, "get_parent") else None
return widget
def on_drag_data_get(self, widget, drag_context, data_obj, info, time):
target = self._find_drag_target(
widget.get_parent() if isinstance(widget, Box) else widget
)
if target is not None:
index = self.view.get_children().index(target)
data_obj.set_text(str(index), -1)
def on_drag_data_received(self, widget, drag_context, x, y, data_obj, info, time):
target = self._find_drag_target(
widget.get_parent() if isinstance(widget, Box) else widget
)
if target is None:
return
try:
source_index = int(data_obj.get_text())
except (TypeError, ValueError):
return
children = self.view.get_children()
try:
target_index = children.index(target)
except ValueError:
return
if source_index != target_index:
separator_index = -1
for i, child_item_loop in enumerate(children):
if child_item_loop.get_name() == "dock-separator":
separator_index = i
break
cross_section_drag = separator_index != -1 and (
(source_index < separator_index and target_index > separator_index)
or (source_index > separator_index and target_index < separator_index)
)
child_item_to_move = children.pop(source_index)
children.insert(target_index, child_item_to_move)
self.view.children = children
self.update_pinned_apps(skip_update=not cross_section_drag)
if cross_section_drag:
GLib.idle_add(self.update_dock)
def on_drag_end(self, widget, drag_context):
if not self._drag_in_progress:
return
def process_drag_end():
if self.get_mapped():
display = Gdk.Display.get_default()
_, x, y, _ = display.get_pointer()
window = self.get_window()
# Only proceed if pointer is outside dock window
if window:
win_x, win_y, width, height = window.get_geometry()
if not (win_x <= x <= win_x + width and win_y <= y <= win_y + height):
app_id_dragged = widget.app_identifier
instances_dragged = widget.instances
# Remove pinned app
app_index_dragged = -1
for i, pinned_app_item in enumerate(self.pinned):
if isinstance(app_id_dragged, dict) and isinstance(pinned_app_item, dict):
if app_id_dragged.get("name") == pinned_app_item.get("name"):
app_index_dragged = i
break
elif app_id_dragged == pinned_app_item:
app_index_dragged = i
break
if app_index_dragged >= 0:
self.pinned.pop(app_index_dragged)
self.config["pinned_apps"] = self.pinned
self.update_pinned_apps_file()
self.update_dock()
elif instances_dragged:
address = instances_dragged[0].get("address")
if address:
exec_shell_command(f"hyprctl dispatch focuswindow address:{address}")
self._drag_in_progress = False
GLib.idle_add(process_drag_end)
def _update_size(self):
width, _ = self.view.get_preferred_width()
self.set_size_request(width, -1)
return False
def check_config_change(self):
new_config = read_config()
new_always_occluded = data.DOCK_ALWAYS_OCCLUDED
if self.always_occluded != new_always_occluded:
self.always_occluded = new_always_occluded
self.check_occlusion_state()
if new_config.get("pinned_apps", []) != self.config.get("pinned_apps", []):
self.config = new_config
self.pinned = self.config.get("pinned_apps", [])
self.update_app_map()
self.update_dock()
return True
def check_config_change_immediate(self):
new_config = read_config()
previous_always_occluded = self.always_occluded
self.always_occluded = data.DOCK_ALWAYS_OCCLUDED
if previous_always_occluded != self.always_occluded:
self.check_occlusion_state()
if new_config.get("pinned_apps", []) != self.config.get("pinned_apps", []):
self.config = new_config
self.pinned = self.config.get("pinned_apps", [])
self.update_app_map()
self.update_dock()
return False
def update_pinned_apps_file(self):
config_path = get_relative_path("../../config/dock.json")
try:
with open(config_path, "w") as file:
json.dump(self.config, file, indent=4)
return True
except Exception as e:
logging.error(f"Failed to write dock config: {e}")
return False
def update_pinned_apps(self, skip_update=False):
pinned_children_data = []
for child_widget in self.view.get_children():
if child_widget.get_name() == "dock-separator":
break
if hasattr(child_widget, "app_identifier"):
if hasattr(child_widget, "desktop_app") and child_widget.desktop_app:
app = child_widget.desktop_app
app_data_obj = {
"name": app.name,
"display_name": app.display_name,
"window_class": app.window_class,
"executable": app.executable,
"command_line": app.command_line,
}
pinned_children_data.append(app_data_obj)
else:
pinned_children_data.append(child_widget.app_identifier)
self.config["pinned_apps"] = pinned_children_data
self.pinned = pinned_children_data
file_updated = self.update_pinned_apps_file()
if file_updated and not skip_update:
self.update_dock()
@staticmethod
def notify_config_change():
for dock_instance in Dock._instances: