diff --git a/config/config.json b/config/config.json new file mode 100644 index 0000000..603bca0 --- /dev/null +++ b/config/config.json @@ -0,0 +1,66 @@ +{ + "prefix_restart": "SUPER ALT", + "suffix_restart": "B", + "prefix_axmsg": "SUPER", + "suffix_axmsg": "A", + "prefix_dash": "SUPER", + "suffix_dash": "D", + "prefix_bluetooth": "SUPER SHIFT", + "suffix_bluetooth": "B", + "prefix_pins": "SUPER", + "suffix_pins": "Q", + "prefix_kanban": "SUPER", + "suffix_kanban": "N", + "prefix_launcher": "SUPER", + "suffix_launcher": "R", + "prefix_tmux": "SUPER", + "suffix_tmux": "T", + "prefix_cliphist": "SUPER", + "suffix_cliphist": "V", + "prefix_toolbox": "SUPER", + "suffix_toolbox": "S", + "prefix_overview": "SUPER", + "suffix_overview": "TAB", + "prefix_wallpapers": "SUPER", + "suffix_wallpapers": "COMMA", + "prefix_randwall": "SUPER SHIFT", + "suffix_randwall": "COMMA", + "prefix_emoji": "SUPER", + "suffix_emoji": "PERIOD", + "prefix_power": "SUPER", + "suffix_power": "ESCAPE", + "prefix_caffeine": "SUPER SHIFT", + "suffix_caffeine": "M", + "prefix_css": "SUPER SHIFT", + "suffix_css": "B", + "prefix_restart_inspector": "SUPER CTRL ALT", + "suffix_restart_inspector": "B", + "wallpapers_dir": "/home/sankalp/Pictures/wallpapers", + "dock_position": "Bottom", + "vertical": false, + "dock_enabled": true, + "dock_auto_hide": true, + "dock_always_occluded": false, + "dock_icon_size": 20, + "terminal_command": "kitty -e", + "corners_visible": true, + "dock_theme": "Pills", + "notif_pos": "Top", + "dock_workspace_visible": true, + "dock_metrics_visible": true, + "dock_date_time_visible": true, + "dock_battery_visible": true, + "dock_controls_visible": true, + "dock_indicators_visible": true, + "dock_applications_visible": true, + "metrics_visible": { + "cpu": true, + "ram": true, + "swap": true, + "disk": false, + "gpu": false + }, + "metrics_disks": [ + "/" + ] +} \ No newline at end of file diff --git a/config/data.py b/config/data.py index 133f0e6..0b14d9f 100644 --- a/config/data.py +++ b/config/data.py @@ -7,7 +7,7 @@ import gi gi.require_version("Gtk", "3.0") -APP_NAME = "modusv1" +APP_NAME = "modus" APP_NAME_CAP = "Modus" @@ -60,6 +60,7 @@ if os.path.exists(CONFIG_FILE): DOCK_AUTO_HIDE = config.get("dock_auto_hide", True) DOCK_ALWAYS_OCCLUDED = config.get("dock_always_occluded", False) DOCK_THEME = config.get("dock_theme", "Pills") + DOCK_ICON_SIZE = config.get("dock_icon_size", 20) NOTIF_POS = config.get(NOTIF_POS_KEY, NOTIF_POS_DEFAULT) @@ -70,7 +71,8 @@ if os.path.exists(CONFIG_FILE): "date_time": config.get("dock_date_time_visible", True), "controls": config.get("dock_controls_visible", True), "indicators": config.get("dock_indicators_visible", True), - "tray": config.get("dock_tray_visible", True), + "systray": config.get("dock_tray_visible", True), + "applications": config.get("dock_applications_visible", True), } METRICS_DISKS = config.get("metrics_disks", ["/"]) @@ -87,6 +89,7 @@ else: DOCK_AUTO_HIDE = True TERMINAL_COMMAND = "kitty -e" DOCK_THEME = "Pills" + DOCK_ICON_SIZE = 20 NOTIF_POS = NOTIF_POS_DEFAULT @@ -97,7 +100,8 @@ else: "date_time": True, "controls": True, "indicators": True, - "tray": True, + "systray": True, + "applications": True, } METRICS_DISKS = ["/"] diff --git a/config/dock.json b/config/dock.json new file mode 100644 index 0000000..7564bbf --- /dev/null +++ b/config/dock.json @@ -0,0 +1,3 @@ +{ + "pinned_apps": [] +} \ No newline at end of file diff --git a/config/hypr/ax-shell.conf b/config/hypr/ax-shell.conf deleted file mode 100644 index 504d44b..0000000 --- a/config/hypr/ax-shell.conf +++ /dev/null @@ -1,75 +0,0 @@ -exec-once = uwsm-app $(python /home/sankalp/.config/Ax-Shell/main.py) -exec = pgrep -x "hypridle" > /dev/null || uwsm app -- hypridle -exec = uwsm app -- swww-daemon -exec-once = wl-paste --type text --watch cliphist store -exec-once = wl-paste --type image --watch cliphist store - -$fabricSend = fabric-cli exec ax-shell -$axMessage = notify-send "Axenide" "FIRE IN THE HOLE‼️🗣️🔥🕳️" -i "/home/sankalp/.config/Ax-Shell/assets/ax.png" -A "🗣️" -A "🔥" -A "🕳️" -a "Source Code" - -bind = SUPER ALT, B, exec, killall ax-shell; uwsm-app $(python /home/sankalp/.config/Ax-Shell/main.py) # Reload Ax-Shell -bind = SUPER, A, exec, $axMessage # Message -bind = SUPER, D, exec, $fabricSend 'notch.open_notch("dashboard")' # Dashboard -bind = SUPER SHIFT, B, exec, $fabricSend 'notch.open_notch("bluetooth")' # Bluetooth -bind = SUPER, Q, exec, $fabricSend 'notch.open_notch("pins")' # Pins -bind = SUPER, N, exec, $fabricSend 'notch.open_notch("kanban")' # Kanban -bind = SUPER, R, exec, $fabricSend 'notch.open_notch("launcher")' # App Launcher -bind = SUPER, T, exec, $fabricSend 'notch.open_notch("tmux")' # Tmux -bind = SUPER, V, exec, $fabricSend 'notch.open_notch("cliphist")' # Clipboard History -bind = SUPER, S, exec, $fabricSend 'notch.open_notch("tools")' # Toolbox -bind = SUPER, TAB, exec, $fabricSend 'notch.open_notch("overview")' # Overview -bind = SUPER, COMMA, exec, $fabricSend 'notch.open_notch("wallpapers")' # Wallpapers -bind = SUPER SHIFT, COMMA, exec, $fabricSend 'notch.dashboard.wallpapers.set_random_wallpaper(None, external=True)' # Random Wallpaper -bind = SUPER, PERIOD, exec, $fabricSend 'notch.open_notch("emoji")' # Emoji Picker -bind = SUPER, ESCAPE, exec, $fabricSend 'notch.open_notch("power")' # Power Menu -bind = SUPER SHIFT, M, exec, $fabricSend 'notch.dashboard.widgets.buttons.caffeine_button.toggle_inhibit(external=True)' # Toggle Caffeine -bind = SUPER SHIFT, B, exec, $fabricSend 'app.set_css()' # Reload CSS -bind = SUPER CTRL ALT, B, exec, killall ax-shell; uwsm-app $(GTK_DEBUG=interactive python /home/sankalp/.config/Ax-Shell/main.py) # Restart with inspector - -# Wallpapers directory: /home/sankalp/Pictures/wallpapers - -source = /home/sankalp/.config/Ax-Shell/config/hypr/colors.conf - -layerrule = noanim, fabric - -exec = cp $wallpaper ~/.current.wall - -general { - col.active_border = rgb($primary) - col.inactive_border = rgb($surface) - gaps_in = 2 - gaps_out = 4 - border_size = 2 - layout = dwindle -} - -cursor { - no_warps=true -} - -decoration { - blur { - enabled = yes - size = 5 - passes = 3 - new_optimizations = yes - contrast = 1 - brightness = 1 - } - rounding = 14 - shadow { - enabled = true - range = 10 - render_power = 2 - color = rgba(0, 0, 0, 0.25) - } -} - -animations { - enabled = yes - bezier = myBezier, 0.4, 0.0, 0.2, 1.0 - animation = windows, 1, 2.5, myBezier, popin 80% - animation = border, 1, 2.5, myBezier - animation = fade, 1, 2.5, myBezier - animation = workspaces, 1, 2.5, myBezier, slidefadevert 20% -} diff --git a/config/hypr/colors.conf b/config/hypr/colors.conf index 3553bf5..5846e3f 100644 --- a/config/hypr/colors.conf +++ b/config/hypr/colors.conf @@ -1,27 +1,27 @@ -$wallpaper = /home/sankalp/Pictures/wallpapers/56.png -$background = 18120b -$foreground = ede0d4 +$wallpaper = /home/sankalp/Pictures/wallpapers/165.jpg +$background = 16130b +$foreground = eae1d4 -$primary = f5bc6f -$secondary = dec2a2 -$tertiary = b9cda0 -$surface = 18120b -$surface_bright = 3f3830 -$outline = 9c8f80 +$primary = e3c46c +$secondary = d5c5a1 +$tertiary = adcfae +$surface = 16130b +$surface_bright = 3d392f +$outline = 989080 $error = ff5945 $shadow = 000000 $red = ffb595 $green = b7d085 -$yellow = dec56e +$yellow = d7c770 $blue = cebdfe $magenta = fcb0d5 $cyan = 84d5c4 $white = 82d3e0 $red_dim = ff6e2f $green_dim = 89ad42 -$yellow_dim = bb9b2b +$yellow_dim = b09c31 $blue_dim = 8359fc $magenta_dim = f84ea1 $cyan_dim = 3eb59c diff --git a/config/hypr/modus.conf b/config/hypr/modus.conf index edc4f58..f076ea5 100644 --- a/config/hypr/modus.conf +++ b/config/hypr/modus.conf @@ -7,23 +7,34 @@ exec-once = wl-paste --type image --watch cliphist store $fabricSend = fabric-cli exec modus $axMessage = notify-send "Axenide" "FIRE IN THE HOLE‼️🗣️🔥🕳️" -i "/home/sankalp/.config/Modus/assets/ax.png" -A "🗣️" -A "🔥" -A "🕳️" -a "Source Code" -bind = SUPER ALT, B, exec, killall modus; uwsm-app $(python /home/sankalp/.config/Modus/main.py) # Reload Modus -bind = SUPER, A, exec, $axMessage # Message -bind = SUPER, D, exec, $fabricSend 'notch.open_notch("dashboard")' # Dashboard -bind = SUPER SHIFT, B, exec, $fabricSend 'notch.open_notch("bluetooth")' # Bluetooth +# Reload Modus +bind = SUPER ALT, B, exec, killall modus; uwsm-app $(python /home/sankalp/.config/Modus/main.py) +# Message +bind = SUPER, A, exec, $axMessage +# Dashboard +bind = SUPER, D, exec, $fabricSend 'notch.open_notch("dashboard")' +# Bluetooth +bind = SUPER SHIFT, B, exec, $fabricSend 'notch.open_notch("bluetooth")' bind = SUPER, Q, exec, $fabricSend 'notch.open_notch("pins")' # Pins bind = SUPER, N, exec, $fabricSend 'notch.open_notch("kanban")' # Kanban -bind = SUPER, R, exec, $fabricSend 'notch.open_notch("launcher")' # App Launcher +# App Launcher +bind = SUPER, R, exec, $fabricSend 'notch.open_notch("launcher")' bind = SUPER, T, exec, $fabricSend 'notch.open_notch("tmux")' # Tmux -bind = SUPER, V, exec, $fabricSend 'notch.open_notch("cliphist")' # Clipboard History -bind = SUPER, S, exec, $fabricSend 'notch.open_notch("tools")' # Toolbox +# Clipboard History +bind = SUPER, V, exec, $fabricSend 'notch.open_notch("cliphist")' +# Toolbox +bind = SUPER, S, exec, $fabricSend 'notch.open_notch("tools")' bind = SUPER, TAB, exec, $fabricSend 'notch.open_notch("overview")' # Overview -bind = SUPER, COMMA, exec, $fabricSend 'notch.open_notch("wallpapers")' # Wallpapers +# Wallpapers +bind = SUPER, COMMA, exec, $fabricSend 'notch.open_notch("wallpapers")' bind = SUPER SHIFT, COMMA, exec, $fabricSend 'notch.dashboard.wallpapers.set_random_wallpaper(None, external=True)' # Random Wallpaper -bind = SUPER, PERIOD, exec, $fabricSend 'notch.open_notch("emoji")' # Emoji Picker -bind = SUPER, ESCAPE, exec, $fabricSend 'notch.open_notch("power")' # Power Menu +# Emoji Picker +bind = SUPER, PERIOD, exec, $fabricSend 'notch.open_notch("emoji")' +# Power Menu +bind = SUPER, ESCAPE, exec, $fabricSend 'notch.open_notch("power")' bind = SUPER SHIFT, M, exec, $fabricSend 'notch.dashboard.widgets.buttons.caffeine_button.toggle_inhibit(external=True)' # Toggle Caffeine -bind = SUPER SHIFT, B, exec, $fabricSend 'app.set_css()' # Reload CSS +# Reload CSS +bind = SUPER SHIFT, B, exec, $fabricSend 'app.set_css()' bind = SUPER CTRL ALT, B, exec, killall modus; uwsm-app $(GTK_DEBUG=interactive python /home/sankalp/.config/Modus/main.py) # Restart with inspector # Wallpapers directory: /home/sankalp/Pictures/wallpapers diff --git a/config/settings/constants.py b/config/settings/constants.py index 1530636..b89b2c4 100644 --- a/config/settings/constants.py +++ b/config/settings/constants.py @@ -1,4 +1,10 @@ -from config.data import (APP_NAME, APP_NAME_CAP, NOTIF_POS_DEFAULT, NOTIF_POS_KEY,WALLPAPERS_DIR_DEFAULT) +from config.data import ( + APP_NAME, + APP_NAME_CAP, + NOTIF_POS_DEFAULT, + NOTIF_POS_KEY, + WALLPAPERS_DIR_DEFAULT, +) SOURCE_STRING = f""" # {APP_NAME_CAP} @@ -6,64 +12,66 @@ source = ~/{APP_NAME_CAP}/config/hypr/{APP_NAME}.conf """ DEFAULTS = { - "prefix_restart": "SUPER ALT", - "suffix_restart": "B", - "prefix_axmsg": "SUPER", - "suffix_axmsg": "A", - "prefix_dash": "SUPER", - "suffix_dash": "D", - "prefix_bluetooth": "SUPER SHIFT", - "suffix_bluetooth": "B", - "prefix_pins": "SUPER", - "suffix_pins": "Q", - "prefix_kanban": "SUPER", - "suffix_kanban": "N", - "prefix_launcher": "SUPER", - "suffix_launcher": "R", - "prefix_tmux": "SUPER", - "suffix_tmux": "T", - "prefix_cliphist": "SUPER", - "suffix_cliphist": "V", - "prefix_toolbox": "SUPER", - "suffix_toolbox": "S", - "prefix_overview": "SUPER", - "suffix_overview": "TAB", - "prefix_wallpapers": "SUPER", - "suffix_wallpapers": "COMMA", - "prefix_randwall": "SUPER SHIFT", - "suffix_randwall": "COMMA", - "prefix_emoji": "SUPER", - "suffix_emoji": "PERIOD", - "prefix_power": "SUPER", - "suffix_power": "ESCAPE", - "prefix_caffeine": "SUPER SHIFT", - "suffix_caffeine": "M", - "prefix_css": "SUPER SHIFT", - "suffix_css": "B", - "prefix_restart_inspector": "SUPER CTRL ALT", - "suffix_restart_inspector": "B", - "wallpapers_dir": "/home/sankalp/Pictures/wallpapers", - "vertical": True, - "dock_enabled": True, - "dock_always_occluded": False, - "dock_auto_hide": True, - "terminal_command": "kitty -e", - "corners_visible": False, - "dock_theme": "Pills", - "notif_pos": "Top", - "dock_position": "Bottom", - "dock_workspaces_visible": True, - "dock_metrics_visible": True, - "dock_battery_visible": True, - "dock_date_time_visible": True, - "dock_controls_visible": True, - "dock_indicators_visible": True, - "metrics_visible": { - "cpu": True, - "ram": True, - "disk": True, - "swap": True, - "gpu": False - }, - "bar_metrics_disks": [] + "prefix_restart": "SUPER ALT", + "suffix_restart": "B", + "prefix_axmsg": "SUPER", + "suffix_axmsg": "A", + "prefix_dash": "SUPER", + "suffix_dash": "D", + "prefix_bluetooth": "SUPER SHIFT", + "suffix_bluetooth": "B", + "prefix_pins": "SUPER", + "suffix_pins": "Q", + "prefix_kanban": "SUPER", + "suffix_kanban": "N", + "prefix_launcher": "SUPER", + "suffix_launcher": "R", + "prefix_tmux": "SUPER", + "suffix_tmux": "T", + "prefix_cliphist": "SUPER", + "suffix_cliphist": "V", + "prefix_toolbox": "SUPER", + "suffix_toolbox": "S", + "prefix_overview": "SUPER", + "suffix_overview": "TAB", + "prefix_wallpapers": "SUPER", + "suffix_wallpapers": "COMMA", + "prefix_randwall": "SUPER SHIFT", + "suffix_randwall": "COMMA", + "prefix_emoji": "SUPER", + "suffix_emoji": "PERIOD", + "prefix_power": "SUPER", + "suffix_power": "ESCAPE", + "prefix_caffeine": "SUPER SHIFT", + "suffix_caffeine": "M", + "prefix_css": "SUPER SHIFT", + "suffix_css": "B", + "prefix_restart_inspector": "SUPER CTRL ALT", + "suffix_restart_inspector": "B", + "wallpapers_dir": "/home/sankalp/Pictures/wallpapers", + "vertical": True, + "dock_enabled": True, + "dock_always_occluded": False, + "dock_auto_hide": True, + "terminal_command": "kitty -e", + "corners_visible": False, + "dock_theme": "Pills", + "notif_pos": "Top", + "dock_position": "Bottom", + "dock_icon_size": 20, + "dock_workspaces_visible": True, + "dock_metrics_visible": True, + "dock_battery_visible": True, + "dock_date_time_visible": True, + "dock_controls_visible": True, + "dock_indicators_visible": True, + "dock_applications_visible": True, + "metrics_visible": { + "cpu": True, + "ram": True, + "disk": True, + "swap": True, + "gpu": False, + }, + "metrics_disks": ["/"], } diff --git a/config/settings/gui.py b/config/settings/gui.py index 435fc72..f5e0665 100644 --- a/config/settings/gui.py +++ b/config/settings/gui.py @@ -286,6 +286,29 @@ class HyprConfGUI(Window): dock_theme_combo_container.add(self.dock_theme_combo) layout_grid.attach(dock_theme_combo_container, 3, 0, 1, 1) + # Add icon size scale + icon_size_label = Label(label="Icon Size", h_align="start", v_align="center") + layout_grid.attach(icon_size_label, 0, 3, 1, 1) + + icon_size_scale_container = Gtk.Box( + orientation=Gtk.Orientation.HORIZONTAL, + halign=Gtk.Align.START, + valign=Gtk.Align.CENTER, + hexpand=True + ) + + self.icon_size_scale = Gtk.Scale.new_with_range( + Gtk.Orientation.HORIZONTAL, 20, 30, 2 + ) + self.icon_size_scale.set_value(bind_vars.get("dock_icon_size", 20)) + self.icon_size_scale.set_size_request(150, -1) + self.icon_size_scale.set_draw_value(True) + self.icon_size_scale.set_value_pos(Gtk.PositionType.RIGHT) + self.icon_size_scale.set_tooltip_text("Adjust the size of dock icons") + + icon_size_scale_container.add(self.icon_size_scale) + layout_grid.attach(icon_size_scale_container, 1, 3, 3, 1) + dock_label = Label(label="Show Dock", h_align="start", v_align="center") layout_grid.attach(dock_label, 0, 1, 1, 1) dock_switch_container = Gtk.Box( @@ -388,6 +411,7 @@ class HyprConfGUI(Window): "battery": "Battery Indicator", "controls": "Control Panel", "indicators": "Indicators", + "applications": "Taskbar" } self.corners_switch = Gtk.Switch(active=bind_vars.get("corners_visible", True)) @@ -535,8 +559,8 @@ class HyprConfGUI(Window): metric_names = { "cpu": "CPU", "ram": "RAM", - "disk": "Disk", "swap": "Swap", + "disk": "Disk", "gpu": "GPU", } @@ -633,18 +657,18 @@ class HyprConfGUI(Window): repo_box.add(repo_link) vbox.add(repo_box) - def on_kofi_clicked(_): - import webbrowser + # def on_kofi_clicked(_): + # import webbrowser - webbrowser.open("https://ko-fi.com/S4NKALP") + # webbrowser.open("https://github.com/S4NKALP") - kofi_btn = Button( - label="Support on Ko-Fi ❤️", - on_clicked=on_kofi_clicked, - tooltip_text="Support S4NKALP on Ko-Fi", - style="margin-top: 18px; min-width: 160px;", - ) - vbox.add(kofi_btn) + # kofi_btn = Button( + # label="Support on Ko-Fi ❤️", + # on_clicked=on_kofi_clicked, + # tooltip_text="Support S4NKALP on Ko-Fi", + # style="margin-top: 18px; min-width: 160px;", + # ) + # vbox.add(kofi_btn) vbox.add(Box(v_expand=True)) return vbox @@ -727,6 +751,7 @@ class HyprConfGUI(Window): current_bind_vars_snapshot["dock_always_occluded"] = ( self.dock_hover_switch.get_active() ) + current_bind_vars_snapshot["dock_icon_size"] = int(self.icon_size_scale.get_value()) current_bind_vars_snapshot["terminal_command"] = self.terminal_entry.get_text() current_bind_vars_snapshot["corners_visible"] = self.corners_switch.get_active() current_bind_vars_snapshot["dock_theme"] = ( @@ -1016,6 +1041,8 @@ class HyprConfGUI(Window): self.lock_switch.set_active(False) if self.idle_switch: self.idle_switch.set_active(False) + # Add reset for icon size scale + self.icon_size_scale.set_value(utils.bind_vars.get("dock_icon_size", 20)) print("Settings reset to defaults.") def on_close(self, widget): diff --git a/config/settings/utils.py b/config/settings/utils.py index 1c7e2a4..3edd67e 100644 --- a/config/settings/utils.py +++ b/config/settings/utils.py @@ -1,3 +1,10 @@ +from config.data import ( # CONFIG_DIR, HOME_DIR no se usan aquí directamente + APP_NAME, + APP_NAME_CAP, +) +from config.settings import constants as settings_constants +from gi.repository import GLib +from fabric.utils.helpers import exec_shell_command_async import json import os import shutil @@ -8,17 +15,13 @@ from pathlib import Path import gi import toml -gi.require_version('Gtk', '3.0') -from fabric.utils.helpers import exec_shell_command_async -from gi.repository import GLib +gi.require_version("Gtk", "3.0") # Importar settings_constants para DEFAULTS -from config.settings import constants as settings_constants -from config.data import ( # CONFIG_DIR, HOME_DIR no se usan aquí directamente - APP_NAME, APP_NAME_CAP) # Global variable to store binding variables, managed by this module -bind_vars = {} # Se inicializa vacío, load_bind_vars lo poblará +bind_vars = {} # Se inicializa vacío, load_bind_vars lo poblará + def deep_update(target: dict, update: dict) -> dict: """ @@ -33,7 +36,8 @@ def deep_update(target: dict, update: dict) -> dict: else: # De lo contrario, simplemente establece/sobrescribe el valor. target[key] = value - return target # Aunque modifica in-place, devolverlo es una convención común + return target # Aunque modifica in-place, devolverlo es una convención común + def ensure_matugen_config(): """ @@ -41,54 +45,64 @@ def ensure_matugen_config(): with the expected settings. """ expected_config = { - 'config': { - 'reload_apps': True, - 'wallpaper': { - 'command': 'swww', - 'arguments': [ - 'img', '-t', 'outer', - '--transition-duration', '1.5', - '--transition-step', '255', - '--transition-fps', '60', - '-f', 'Nearest' + "config": { + "reload_apps": True, + "wallpaper": { + "command": "swww", + "arguments": [ + "img", + "-t", + "outer", + "--transition-duration", + "1.5", + "--transition-step", + "255", + "--transition-fps", + "60", + "-f", + "Nearest", ], - 'set': True + "set": True, + }, + "custom_colors": { + "red": {"color": "#FF0000", "blend": True}, + "green": {"color": "#00FF00", "blend": True}, + "yellow": {"color": "#FFFF00", "blend": True}, + "blue": {"color": "#0000FF", "blend": True}, + "magenta": {"color": "#FF00FF", "blend": True}, + "cyan": {"color": "#00FFFF", "blend": True}, + "white": {"color": "#FFFFFF", "blend": True}, }, - 'custom_colors': { - 'red': {'color': "#FF0000", 'blend': True}, - 'green': {'color': "#00FF00", 'blend': True}, - 'yellow': {'color': "#FFFF00", 'blend': True}, - 'blue': {'color': "#0000FF", 'blend': True}, - 'magenta': {'color': "#FF00FF", 'blend': True}, - 'cyan': {'color': "#00FFFF", 'blend': True}, - 'white': {'color': "#FFFFFF", 'blend': True} - } }, - 'templates': { - 'hyprland': { - 'input_path': f'~/{APP_NAME_CAP}/config/matugen/templates/hyprland-colors.conf', - 'output_path': f'~/{APP_NAME_CAP}/config/hypr/colors.conf' + "templates": { + "hyprland": { + "input_path": f"~/{APP_NAME_CAP}/config/matugen/templates/hyprland-colors.conf", + "output_path": f"~/{APP_NAME_CAP}/config/hypr/colors.conf", }, - f'{APP_NAME}': { - 'input_path': f'~/{APP_NAME_CAP}/config/matugen/templates/{APP_NAME}.css', - 'output_path': f'~/{APP_NAME_CAP}/styles/colors.css', - 'post_hook': f"fabric-cli exec {APP_NAME} 'app.set_css()' &" - } - } + f"{APP_NAME}": { + "input_path": f"~/{APP_NAME_CAP}/config/matugen/templates/{APP_NAME}.css", + "output_path": f"~/{APP_NAME_CAP}/styles/colors.css", + "post_hook": f"fabric-cli exec {APP_NAME} 'app.set_css()' &", + }, + }, } - config_path = os.path.expanduser('~/.config/matugen/config.toml') + config_path = os.path.expanduser("~/.config/matugen/config.toml") os.makedirs(os.path.dirname(config_path), exist_ok=True) existing_config = {} if os.path.exists(config_path): try: - with open(config_path, 'r') as f: + with open(config_path, "r") as f: existing_config = toml.load(f) - shutil.copyfile(config_path, config_path + '.bak') + shutil.copyfile(config_path, config_path + ".bak") except toml.TomlDecodeError: - print(f"Warning: Could not decode TOML from {config_path}. A new default config will be created.") - existing_config = {} # Resetear si está corrupto + print( + f"Warning: Could not decode TOML from { + config_path + }. A new default config will be created." + ) + existing_config = {} # Resetear si está corrupto except Exception as e: print(f"Error reading or backing up {config_path}: {e}") # existing_config podría estar parcialmente cargado o vacío. @@ -100,39 +114,50 @@ def ensure_matugen_config(): # Para ser más seguros, podemos pasar una copia si existing_config no debe cambiar. # merged_config = deep_update(existing_config.copy(), expected_config) # O si existing_config puede ser modificado: - merged_config = deep_update(existing_config, expected_config) # existing_config se modifica in-place + # existing_config se modifica in-place + merged_config = deep_update(existing_config, expected_config) try: - with open(config_path, 'w') as f: + with open(config_path, "w") as f: toml.dump(merged_config, f) except Exception as e: print(f"Error writing matugen config to {config_path}: {e}") - current_wall = os.path.expanduser("~/.current.wall") hypr_colors = os.path.expanduser(f"~/{APP_NAME_CAP}/config/hypr/colors.conf") css_colors = os.path.expanduser(f"~/{APP_NAME_CAP}/styles/colors.css") - - if not os.path.exists(current_wall) or not os.path.exists(hypr_colors) or not os.path.exists(css_colors): + + if ( + not os.path.exists(current_wall) + or not os.path.exists(hypr_colors) + or not os.path.exists(css_colors) + ): os.makedirs(os.path.dirname(hypr_colors), exist_ok=True) os.makedirs(os.path.dirname(css_colors), exist_ok=True) - + image_path = "" if not os.path.exists(current_wall): - example_wallpaper_path = os.path.expanduser(f"~/.config/{APP_NAME_CAP}/assets/wallpapers_example/example-1.jpg") + example_wallpaper_path = os.path.expanduser( + f"~/.config/{APP_NAME_CAP}/assets/wallpapers_example/example-1.jpg" + ) if os.path.exists(example_wallpaper_path): try: # Si ya existe (posiblemente un enlace roto o archivo regular), eliminar y re-enlazar - if os.path.lexists(current_wall): # lexists para no seguir el enlace si es uno + # lexists para no seguir el enlace si es uno + if os.path.lexists(current_wall): os.remove(current_wall) os.symlink(example_wallpaper_path, current_wall) image_path = example_wallpaper_path except Exception as e: print(f"Error creating symlink for wallpaper: {e}") else: - image_path = os.path.realpath(current_wall) if os.path.islink(current_wall) else current_wall - - if image_path and os.path.exists(image_path): + image_path = ( + os.path.realpath(current_wall) + if os.path.islink(current_wall) + else current_wall + ) + + if image_path and os.path.exists(image_path): print(f"Generating color theme from wallpaper: {image_path}") try: matugen_cmd = f"matugen image '{image_path}'" @@ -143,9 +168,15 @@ def ensure_matugen_config(): except Exception as e: print(f"Error initiating matugen: {e}") elif not image_path: - print("Warning: No wallpaper path determined to generate matugen theme from.") - else: # image_path existe pero el archivo no - print(f"Warning: Wallpaper at {image_path} not found. Cannot generate matugen theme.") + print( + "Warning: No wallpaper path determined to generate matugen theme from." + ) + else: # image_path existe pero el archivo no + print( + f"Warning: Wallpaper at { + image_path + } not found. Cannot generate matugen theme." + ) def load_bind_vars(): @@ -153,17 +184,18 @@ def load_bind_vars(): Load saved key binding variables from JSON, if available. Populates the global `bind_vars` in-place. """ - global bind_vars # Necesario para modificar el objeto global bind_vars + global bind_vars # Necesario para modificar el objeto global bind_vars # 1. Limpiar el diccionario bind_vars existente. bind_vars.clear() # 2. Actualizarlo con una copia de DEFAULTS. - bind_vars.update(settings_constants.DEFAULTS.copy()) # Usar .copy() para no modificar DEFAULTS accidentalmente + # Usar .copy() para no modificar DEFAULTS accidentalmente + bind_vars.update(settings_constants.DEFAULTS.copy()) - config_json = os.path.expanduser(f'~/{APP_NAME_CAP}/config/config.json') + config_json = os.path.expanduser(f"~/{APP_NAME_CAP}/config/config.json") if os.path.exists(config_json): try: - with open(config_json, 'r') as f: + with open(config_json, "r") as f: saved_vars = json.load(f) # 3. Usar deep_update para fusionar saved_vars en el bind_vars existente. deep_update(bind_vars, saved_vars) @@ -171,7 +203,7 @@ def load_bind_vars(): # La lógica para asegurar la estructura de diccionarios anidados # como 'metrics_visible' y 'metrics_small_visible' # debe operar sobre el 'bind_vars' ya actualizado. - for vis_key in ['metrics_visible']: + for vis_key in ["metrics_visible"]: # Asegurar que la clave exista en DEFAULTS como referencia de estructura if vis_key in settings_constants.DEFAULTS: default_sub_dict = settings_constants.DEFAULTS[vis_key] @@ -186,26 +218,34 @@ def load_bind_vars(): if m_key not in current_sub_dict: current_sub_dict[m_key] = m_val except json.JSONDecodeError: - print(f"Warning: Could not decode JSON from {config_json}. Using defaults (already initialized).") + print( + f"Warning: Could not decode JSON from { + config_json + }. Using defaults (already initialized)." + ) # bind_vars ya está poblado con DEFAULTS, no se necesita acción adicional aquí. except Exception as e: - print(f"Error loading config from {config_json}: {e}. Using defaults (already initialized).") + print( + f"Error loading config from {config_json}: { + e + }. Using defaults (already initialized)." + ) # bind_vars ya está poblado con DEFAULTS. # else: - # Si config_json no existe, bind_vars ya está poblado con DEFAULTS. - # print(f"Config file {config_json} not found. Using defaults (already initialized).") + # Si config_json no existe, bind_vars ya está poblado con DEFAULTS. + # print(f"Config file {config_json} not found. Using defaults (already initialized).") def generate_hyprconf() -> str: """ Generate the Hypr configuration string using the current bind_vars. """ - home = os.path.expanduser('~') + home = os.path.expanduser("~") # Determine animation type based on bar position - bar_position = bind_vars.get('bar_position', 'Top') + bar_position = bind_vars.get("bar_position", "Top") is_vertical = bar_position in ["Left", "Right"] animation_type = "slidefadevert" if is_vertical else "slidefade" - + return f"""exec-once = uwsm-app $(python {home}/{APP_NAME_CAP}/main.py) exec = pgrep -x "hypridle" > /dev/null || uwsm app -- hypridle exec = uwsm app -- swww-daemon @@ -215,26 +255,37 @@ exec-once = wl-paste --type image --watch cliphist store $fabricSend = fabric-cli exec {APP_NAME} $axMessage = notify-send "Axenide" "FIRE IN THE HOLE‼️🗣️🔥🕳️" -i "{home}/.config/{APP_NAME_CAP}/assets/ax.png" -A "🗣️" -A "🔥" -A "🕳️" -a "Source Code" -bind = {bind_vars.get('prefix_restart', 'SUPER ALT')}, {bind_vars.get('suffix_restart', 'B')}, exec, killall {APP_NAME}; uwsm-app $(python {home}/.config/{APP_NAME_CAP}/main.py) # Reload {APP_NAME_CAP} -bind = {bind_vars.get('prefix_axmsg', 'SUPER')}, {bind_vars.get('suffix_axmsg', 'A')}, exec, $axMessage # Message -bind = {bind_vars.get('prefix_dash', 'SUPER')}, {bind_vars.get('suffix_dash', 'D')}, exec, $fabricSend 'notch.open_notch("dashboard")' # Dashboard -bind = {bind_vars.get('prefix_bluetooth', 'SUPER')}, {bind_vars.get('suffix_bluetooth', 'B')}, exec, $fabricSend 'notch.open_notch("bluetooth")' # Bluetooth -bind = {bind_vars.get('prefix_pins', 'SUPER')}, {bind_vars.get('suffix_pins', 'Q')}, exec, $fabricSend 'notch.open_notch("pins")' # Pins -bind = {bind_vars.get('prefix_kanban', 'SUPER')}, {bind_vars.get('suffix_kanban', 'N')}, exec, $fabricSend 'notch.open_notch("kanban")' # Kanban -bind = {bind_vars.get('prefix_launcher', 'SUPER')}, {bind_vars.get('suffix_launcher', 'R')}, exec, $fabricSend 'notch.open_notch("launcher")' # App Launcher -bind = {bind_vars.get('prefix_tmux', 'SUPER')}, {bind_vars.get('suffix_tmux', 'T')}, exec, $fabricSend 'notch.open_notch("tmux")' # Tmux -bind = {bind_vars.get('prefix_cliphist', 'SUPER')}, {bind_vars.get('suffix_cliphist', 'V')}, exec, $fabricSend 'notch.open_notch("cliphist")' # Clipboard History -bind = {bind_vars.get('prefix_toolbox', 'SUPER')}, {bind_vars.get('suffix_toolbox', 'S')}, exec, $fabricSend 'notch.open_notch("tools")' # Toolbox -bind = {bind_vars.get('prefix_overview', 'SUPER')}, {bind_vars.get('suffix_overview', 'TAB')}, exec, $fabricSend 'notch.open_notch("overview")' # Overview -bind = {bind_vars.get('prefix_wallpapers', 'SUPER')}, {bind_vars.get('suffix_wallpapers', 'COMMA')}, exec, $fabricSend 'notch.open_notch("wallpapers")' # Wallpapers -bind = {bind_vars.get('prefix_randwall', 'SUPER')}, {bind_vars.get('suffix_randwall', 'COMMA')}, exec, $fabricSend 'notch.dashboard.wallpapers.set_random_wallpaper(None, external=True)' # Random Wallpaper -bind = {bind_vars.get('prefix_emoji', 'SUPER')}, {bind_vars.get('suffix_emoji', 'PERIOD')}, exec, $fabricSend 'notch.open_notch("emoji")' # Emoji Picker -bind = {bind_vars.get('prefix_power', 'SUPER')}, {bind_vars.get('suffix_power', 'ESCAPE')}, exec, $fabricSend 'notch.open_notch("power")' # Power Menu -bind = {bind_vars.get('prefix_caffeine', 'SUPER SHIFT')}, {bind_vars.get('suffix_caffeine', 'M')}, exec, $fabricSend 'notch.dashboard.widgets.buttons.caffeine_button.toggle_inhibit(external=True)' # Toggle Caffeine -bind = {bind_vars.get('prefix_css', 'SUPER SHIFT')}, {bind_vars.get('suffix_css', 'B')}, exec, $fabricSend 'app.set_css()' # Reload CSS -bind = {bind_vars.get('prefix_restart_inspector', 'SUPER CTRL ALT')}, {bind_vars.get('suffix_restart_inspector', 'B')}, exec, killall {APP_NAME}; uwsm-app $(GTK_DEBUG=interactive python {home}/.config/{APP_NAME_CAP}/main.py) # Restart with inspector +# Reload {APP_NAME_CAP} +bind = {bind_vars.get("prefix_restart", "SUPER ALT")}, {bind_vars.get("suffix_restart", "B")}, exec, killall {APP_NAME}; uwsm-app $(python {home}/.config/{APP_NAME_CAP}/main.py) +# Message +bind = {bind_vars.get("prefix_axmsg", "SUPER")}, {bind_vars.get("suffix_axmsg", "A")}, exec, $axMessage +# Dashboard +bind = {bind_vars.get("prefix_dash", "SUPER")}, {bind_vars.get("suffix_dash", "D")}, exec, $fabricSend 'notch.open_notch("dashboard")' +# Bluetooth +bind = {bind_vars.get("prefix_bluetooth", "SUPER")}, {bind_vars.get("suffix_bluetooth", "B")}, exec, $fabricSend 'notch.open_notch("bluetooth")' +bind = {bind_vars.get("prefix_pins", "SUPER")}, {bind_vars.get("suffix_pins", "Q")}, exec, $fabricSend 'notch.open_notch("pins")' # Pins +bind = {bind_vars.get("prefix_kanban", "SUPER")}, {bind_vars.get("suffix_kanban", "N")}, exec, $fabricSend 'notch.open_notch("kanban")' # Kanban +# App Launcher +bind = {bind_vars.get("prefix_launcher", "SUPER")}, {bind_vars.get("suffix_launcher", "R")}, exec, $fabricSend 'notch.open_notch("launcher")' +bind = {bind_vars.get("prefix_tmux", "SUPER")}, {bind_vars.get("suffix_tmux", "T")}, exec, $fabricSend 'notch.open_notch("tmux")' # Tmux +# Clipboard History +bind = {bind_vars.get("prefix_cliphist", "SUPER")}, {bind_vars.get("suffix_cliphist", "V")}, exec, $fabricSend 'notch.open_notch("cliphist")' +# Toolbox +bind = {bind_vars.get("prefix_toolbox", "SUPER")}, {bind_vars.get("suffix_toolbox", "S")}, exec, $fabricSend 'notch.open_notch("tools")' +bind = {bind_vars.get("prefix_overview", "SUPER")}, {bind_vars.get("suffix_overview", "TAB")}, exec, $fabricSend 'notch.open_notch("overview")' # Overview +# Wallpapers +bind = {bind_vars.get("prefix_wallpapers", "SUPER")}, {bind_vars.get("suffix_wallpapers", "COMMA")}, exec, $fabricSend 'notch.open_notch("wallpapers")' +bind = {bind_vars.get("prefix_randwall", "SUPER")}, {bind_vars.get("suffix_randwall", "COMMA")}, exec, $fabricSend 'notch.dashboard.wallpapers.set_random_wallpaper(None, external=True)' # Random Wallpaper +# Emoji Picker +bind = {bind_vars.get("prefix_emoji", "SUPER")}, {bind_vars.get("suffix_emoji", "PERIOD")}, exec, $fabricSend 'notch.open_notch("emoji")' +# Power Menu +bind = {bind_vars.get("prefix_power", "SUPER")}, {bind_vars.get("suffix_power", "ESCAPE")}, exec, $fabricSend 'notch.open_notch("power")' +bind = {bind_vars.get("prefix_caffeine", "SUPER SHIFT")}, {bind_vars.get("suffix_caffeine", "M")}, exec, $fabricSend 'notch.dashboard.widgets.buttons.caffeine_button.toggle_inhibit(external=True)' # Toggle Caffeine +# Reload CSS +bind = {bind_vars.get("prefix_css", "SUPER SHIFT")}, {bind_vars.get("suffix_css", "B")}, exec, $fabricSend 'app.set_css()' +bind = {bind_vars.get("prefix_restart_inspector", "SUPER CTRL ALT")}, {bind_vars.get("suffix_restart_inspector", "B")}, exec, killall {APP_NAME}; uwsm-app $(GTK_DEBUG=interactive python {home}/.config/{APP_NAME_CAP}/main.py) # Restart with inspector -# Wallpapers directory: {bind_vars.get('wallpapers_dir', '~/Modus/assets/wallpapers_example')} +# Wallpapers directory: {bind_vars.get("wallpapers_dir", "~/Modus/assets/wallpapers_example")} source = {home}/{APP_NAME_CAP}/config/hypr/colors.conf @@ -283,6 +334,7 @@ animations {{ }} """ + def ensure_face_icon(): """ Ensure the face icon exists. If not, copy the default icon. @@ -295,6 +347,7 @@ def ensure_face_icon(): except Exception as e: print(f"Error copying default face icon: {e}") + def backup_and_replace(src: str, dest: str, config_name: str): """ Backup the existing configuration file and replace it with a new one. @@ -306,7 +359,8 @@ def backup_and_replace(src: str, dest: str, config_name: str): # os.makedirs(os.path.dirname(backup_path), exist_ok=True) shutil.copy(dest, backup_path) print(f"{config_name} config backed up to {backup_path}") - os.makedirs(os.path.dirname(dest), exist_ok=True) # Ensure dest directory exists + # Ensure dest directory exists + os.makedirs(os.path.dirname(dest), exist_ok=True) shutil.copy(src, dest) print(f"{config_name} config replaced from {src}") except Exception as e: @@ -338,12 +392,21 @@ def start_config(): print(f"{time.time():.4f}: start_config: Initiating hyprctl reload...") try: # subprocess.run(["hyprctl", "reload"], check=True, capture_output=True, text=True) - exec_shell_command_async("hyprctl reload") # Mantener async para no bloquear - print(f"{time.time():.4f}: start_config: Hyprland configuration reload initiated.") + # Mantener async para no bloquear + exec_shell_command_async("hyprctl reload") + print( + f"{time.time():.4f}: start_config: Hyprland configuration reload initiated." + ) except FileNotFoundError: - print("Error: hyprctl command not found. Cannot reload Hyprland.") - except subprocess.CalledProcessError as e: # Si usáramos subprocess.run con check=True - print(f"Error reloading Hyprland with hyprctl: {e}\nOutput:\n{e.stdout}\n{e.stderr}") + print("Error: hyprctl command not found. Cannot reload Hyprland.") + except ( + subprocess.CalledProcessError + ) as e: # Si usáramos subprocess.run con check=True + print( + f"Error reloading Hyprland with hyprctl: {e}\nOutput:\n{e.stdout}\n{ + e.stderr + }" + ) except Exception as e: - print(f"An error occurred initiating hyprctl reload: {e}") + print(f"An error occurred initiating hyprctl reload: {e}") print(f"{time.time():.4f}: start_config: Finished initiating hyprctl reload.") diff --git a/dock.py b/dock.py new file mode 100644 index 0000000..88a3f77 --- /dev/null +++ b/dock.py @@ -0,0 +1,1040 @@ +import json +import logging + +import cairo +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 + +import config.data as data +from modules.corners import MyCorner +from utils.icon_resolver import IconResolver +from utils.occlusion import check_occlusion +from widgets.wayland import WaylandWindow as Window + + +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 + + +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 = [] + + def __init__(self, integrated_mode: bool = False, **kwargs): + self.integrated_mode = integrated_mode + self.icon_size = 20 if self.integrated_mode else data.DOCK_ICON_SIZE + self.effective_occlusion_size = 36 + self.icon_size + + anchor_to_set: str + revealer_transition_type: str + + self.actual_dock_is_horizontal: bool + main_box_orientation_val: Gtk.Orientation + main_box_h_align_val: str + dock_wrapper_orientation_val: Gtk.Orientation + + if not self.integrated_mode: + self.actual_dock_is_horizontal = not data.VERTICAL + + if self.actual_dock_is_horizontal: + anchor_to_set = "bottom" + revealer_transition_type = "slide-up" + main_box_orientation_val = Gtk.Orientation.VERTICAL + main_box_h_align_val = "center" + dock_wrapper_orientation_val = Gtk.Orientation.HORIZONTAL + else: + if data.BAR_POSITION == "Left": + anchor_to_set = "right" + revealer_transition_type = "slide-left" + elif data.BAR_POSITION == "Right": + anchor_to_set = "left" + revealer_transition_type = "slide-right" + else: + anchor_to_set = "right" + revealer_transition_type = "slide-left" + + main_box_orientation_val = Gtk.Orientation.HORIZONTAL + main_box_h_align_val = "end" if anchor_to_set == "right" else "start" + dock_wrapper_orientation_val = Gtk.Orientation.VERTICAL + + super().__init__( + name="dock-window", + layer="top", + anchor=anchor_to_set, + margin="0px 0px 0px 0px", + exclusivity="none", + **kwargs, + ) + Dock._instances.append(self) + else: + self.actual_dock_is_horizontal = True + dock_wrapper_orientation_val = Gtk.Orientation.HORIZONTAL + + anchor_to_set = "bottom" + revealer_transition_type = "slide-up" + main_box_orientation_val = Gtk.Orientation.VERTICAL + main_box_h_align_val = "center" + + 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 + self._drag_in_progress = False + self.always_occluded = ( + data.DOCK_ALWAYS_OCCLUDED if not self.integrated_mode else False + ) + self.is_mouse_over_dock_area = False + self._prevent_occlusion = False + + self.view = Box(name="viewport", spacing=4) + self.wrapper = Box( + name="dock", + children=[self.view], + style_classes=["left"] if data.BAR_POSITION == "Right" else [], + ) + + self.wrapper.set_orientation(dock_wrapper_orientation_val) + self.view.set_orientation(dock_wrapper_orientation_val) + + if self.integrated_mode: + self.wrapper.add_style_class("integrated") + else: + if dock_wrapper_orientation_val == Gtk.Orientation.VERTICAL: + self.wrapper.add_style_class("vertical") + else: + self.wrapper.remove_style_class("vertical") + + match data.DOCK_THEME: + case "Pills": + self.wrapper.add_style_class("pills") + case "Dense": + self.wrapper.add_style_class("dense") + case "Edge": + self.wrapper.add_style_class("edge") + case _: + self.wrapper.add_style_class("pills") + + if not self.integrated_mode: + self.dock_eventbox = EventBox() + self.dock_eventbox.add(self.wrapper) + self.dock_eventbox.connect("enter-notify-event", self._on_dock_enter) + self.dock_eventbox.connect("leave-notify-event", self._on_dock_leave) + + self.corner_left = Box() + self.corner_right = Box() + self.corner_top = Box() + self.corner_bottom = Box() + + if self.actual_dock_is_horizontal: + self.corner_left = Box( + name="dock-corner-left", + orientation=Gtk.Orientation.VERTICAL, + h_align="start", + children=[ + Box(v_expand=True, v_align="fill"), + MyCorner("bottom-right"), + ], + ) + self.corner_right = Box( + name="dock-corner-right", + orientation=Gtk.Orientation.VERTICAL, + h_align="end", + children=[ + Box(v_expand=True, v_align="fill"), + MyCorner("bottom-left"), + ], + ) + self.dock_full = Box( + name="dock-full", + orientation=Gtk.Orientation.HORIZONTAL, + h_expand=True, + h_align="fill", + children=[self.corner_left, self.dock_eventbox, self.corner_right], + ) + else: + if anchor_to_set == "right": + self.corner_top = Box( + name="dock-corner-top", + orientation=Gtk.Orientation.HORIZONTAL, + v_align="start", + children=[ + Box(h_expand=True, h_align="fill"), + MyCorner("bottom-right"), + ], + ) + self.corner_bottom = Box( + name="dock-corner-bottom", + orientation=Gtk.Orientation.HORIZONTAL, + v_align="end", + children=[ + Box(h_expand=True, h_align="fill"), + MyCorner("top-right"), + ], + ) + else: + self.corner_top = Box( + name="dock-corner-top", + orientation=Gtk.Orientation.HORIZONTAL, + v_align="start", + children=[ + MyCorner("bottom-left"), + Box(h_expand=True, h_align="fill"), + ], + ) + self.corner_bottom = Box( + name="dock-corner-bottom", + orientation=Gtk.Orientation.HORIZONTAL, + v_align="end", + children=[ + MyCorner("top-left"), + Box(h_expand=True, h_align="fill"), + ], + ) + + self.dock_full = Box( + name="dock-full", + orientation=Gtk.Orientation.VERTICAL, + v_expand=True, + v_align="fill", + children=[self.corner_top, self.dock_eventbox, self.corner_bottom], + ) + + self.dock_revealer = Revealer( + name="dock-revealer", + transition_type=revealer_transition_type, + transition_duration=250, + child_revealed=False, + child=self.dock_full, + ) + + 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, + ) + 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.BAR_POSITION != "Right" + else [self.dock_revealer, self.hover_activator], + h_align=main_box_h_align_val, + ) + self.add(self.main_box) + + if data.DOCK_THEME in ["Edge", "Dense"]: + for corner in [ + self.corner_left, + self.corner_right, + self.corner_top, + self.corner_bottom, + ]: + corner.set_visible(False) + + if not data.DOCK_ENABLED or data.BAR_POSITION in ["Top", "Bottom"]: + self.set_visible(False) + + 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() + if not self.integrated_mode: + GLib.timeout_add(250, self.check_occlusion_state) + else: + self.conn.connect("event::ready", self.update_dock) + if not self.integrated_mode: + 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) + + if not self.integrated_mode: + 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): + if self.integrated_mode: + return + self.is_mouse_over_dock_area = True + if self.hide_id: + GLib.source_remove(self.hide_id) + self.hide_id = None + self.dock_revealer.set_reveal_child(True) + if not self.always_occluded: + self.dock_full.remove_style_class("occluded") + + def _on_hover_leave(self, *args): + if self.integrated_mode: + return + self.is_mouse_over_dock_area = False + self.delay_hide() + + def _on_dock_enter(self, widget, event): + if self.integrated_mode: + return True + self.is_mouse_over_dock_area = True + if self.hide_id: + GLib.source_remove(self.hide_id) + self.hide_id = None + self.dock_revealer.set_reveal_child(True) + if not self.always_occluded: + self.dock_full.remove_style_class("occluded") + return True + + def _on_dock_leave(self, widget, event): + if self.integrated_mode: + return True + if event.detail == Gdk.NotifyType.INFERIOR: + return False + + self.is_mouse_over_dock_area = False + self.delay_hide() + + if self.always_occluded: + 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): + if self.integrated_mode: + return False + 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.integrated_mode: + return + if self.hide_id: + GLib.source_remove(self.hide_id) + self.hide_id = GLib.timeout_add(250, self.hide_dock_if_not_hovered) + + def hide_dock_if_not_hovered(self): + if self.integrated_mode: + return False + self.hide_id = None + if ( + not self.is_mouse_over_dock_area + and not self._drag_in_progress + and not self._prevent_occlusion + ): + if self.always_occluded: + self.dock_revealer.set_reveal_child(False) + else: + occlusion_region = ( + ("bottom", self.effective_occlusion_size) + if self.actual_dock_is_horizontal + else ("right", self.effective_occlusion_size) + ) + if check_occlusion(occlusion_region) or not self.view.get_children(): + self.dock_revealer.set_reveal_child(False) + return False + + def check_hide(self, *args): + if self.integrated_mode: + return + if ( + self.is_mouse_over_dock_area + or self._drag_in_progress + or self._prevent_occlusion + ): + return + + clients = self.get_clients() + current_ws = self.get_workspace() + ws_clients = [w for w in clients if w["workspace"]["id"] == current_ws] + + if not self.always_occluded: + if not ws_clients: + if not self.dock_revealer.get_reveal_child(): + self.dock_revealer.set_reveal_child(True) + self.dock_full.remove_style_class("occluded") + elif any( + not w.get("floating") and not w.get("fullscreen") for w in ws_clients + ): + self.check_occlusion_state() + else: + if not self.dock_revealer.get_reveal_child(): + self.dock_revealer.set_reveal_child(True) + self.dock_full.remove_style_class("occluded") + else: + if self.dock_revealer.get_reveal_child(): + 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 = pinned_buttons + separator_orientation = ( + Gtk.Orientation.VERTICAL + if self.view.get_orientation() == Gtk.Orientation.HORIZONTAL + else Gtk.Orientation.HORIZONTAL + ) + if pinned_buttons and open_buttons: + children += [ + Box( + orientation=separator_orientation, + v_expand=False, + h_expand=False, + h_align="center", + v_align="center", + name="dock-separator", + ) + ] + children += open_buttons + + self.view.children = children + if not self.integrated_mode: + idle_add(self._update_size) + self._drag_in_progress = False + if not self.integrated_mode: + self.check_occlusion_state() + + def _update_size(self): + if self.integrated_mode: + return False + width, _ = self.view.get_preferred_width() + self.set_size_request(width, -1) + return False + + def get_clients(self): + try: + return json.loads(self.conn.send_command("j/clients").reply.decode()) + except json.JSONDecodeError: + return [] + + def get_focused(self): + try: + return json.loads( + self.conn.send_command("j/activewindow").reply.decode() + ).get("address", "") + except json.JSONDecodeError: + return "" + + def get_workspace(self): + try: + return json.loads( + self.conn.send_command("j/activeworkspace").reply.decode() + ).get("id", 0) + except json.JSONDecodeError: + return 0 + + def check_occlusion_state(self): + if self.integrated_mode: + return False + + if ( + self.is_mouse_over_dock_area + or self._drag_in_progress + or self._prevent_occlusion + ): + 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) + self.dock_full.add_style_class("occluded") + return True + + occlusion_region = ( + ("bottom", self.effective_occlusion_size) + if self.actual_dock_is_horizontal + else ("right", self.effective_occlusion_size) + ) + is_occluded_by_window = check_occlusion(occlusion_region) + is_empty = not self.view.get_children() + + if is_occluded_by_window or is_empty: + if self.dock_revealer.get_reveal_child(): + self.dock_revealer.set_reveal_child(False) + self.dock_full.add_style_class("occluded") + else: + if not self.dock_revealer.get_reveal_child(): + self.dock_revealer.set_reveal_child(True) + self.dock_full.remove_style_class("occluded") + + 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(): + display = Gdk.Display.get_default() + _, x, y, _ = display.get_pointer() + + # Get the widget's allocation to check if drag ended outside + alloc = self.view.get_allocation() + widget_x = alloc.x + widget_y = alloc.y + widget_width = alloc.width + widget_height = 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 + 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 + if not self.integrated_mode: + self.check_occlusion_state() + + GLib.idle_add(process_drag_end) + + def check_config_change(self): + new_config = read_config() + if not self.integrated_mode: + 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 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: + GLib.idle_add(dock_instance.check_config_change_immediate) + + def check_config_change_immediate(self): + new_config = read_config() + + if not self.integrated_mode: + 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 + + @staticmethod + def update_visibility(visible): + for dock in Dock._instances: + dock.set_visible(visible) + if visible: + GLib.idle_add(dock.check_occlusion_state) + else: + if ( + hasattr(dock, "dock_revealer") + and dock.dock_revealer.get_reveal_child() + ): + dock.dock_revealer.set_reveal_child(False) diff --git a/main.py b/main.py index bd75a0d..e14e541 100644 --- a/main.py +++ b/main.py @@ -8,7 +8,7 @@ from loguru import logger from config.data import APP_NAME, APP_NAME_CAP, CONFIG_FILE from modules.corners import Corners -from modules.dock.main import Dock +from modules.dock.main import Dock gi.require_version("GLib", "2.0") diff --git a/modules/dock/applications.py b/modules/dock/applications.py new file mode 100644 index 0000000..b2431a1 --- /dev/null +++ b/modules/dock/applications.py @@ -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 diff --git a/modules/dock/battery.py b/modules/dock/battery.py index 1669e10..96accce 100644 --- a/modules/dock/battery.py +++ b/modules/dock/battery.py @@ -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") diff --git a/modules/dock/components.py b/modules/dock/components.py index eae4f34..f5dc442 100644 --- a/modules/dock/components.py +++ b/modules/dock/components.py @@ -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 \ No newline at end of file + return None diff --git a/modules/dock/main.py b/modules/dock/main.py index 30a96d3..8a51183 100644 --- a/modules/dock/main.py +++ b/modules/dock/main.py @@ -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: diff --git a/services/brightness.py b/services/brightness.py index 62179cf..49527b9 100644 --- a/services/brightness.py +++ b/services/brightness.py @@ -6,12 +6,7 @@ from gi.repository import GLib from loguru import logger - - def exec_brightnessctl_async(args: str): - if not helpers.executable_exists("brightnessctl"): - logger.error(f"Command brightnessctl not found") - exec_shell_command_async(f"brightnessctl {args}", lambda _: None) @@ -20,9 +15,7 @@ try: screen_device = os.listdir("/sys/class/backlight") screen_device = screen_device[0] if screen_device else "" except FileNotFoundError: - logger.error( - f"No backlight devices found, brightness control disabled" - ) + logger.error(f"No backlight devices found, brightness control disabled") screen_device = "" @@ -67,9 +60,7 @@ class Brightness(Service): ) # Log the initialization of the service - logger.info( - f"Brightness service initialized for device: {screen_device}" - ) + logger.info(f"Brightness service initialized for device: {screen_device}") def do_read_max_brightness(self, path: str) -> int: # Reads the maximum brightness value from the specified path. @@ -86,9 +77,7 @@ class Brightness(Service): if os.path.exists(brightness_path): with open(brightness_path) as f: return int(f.readline()) - logger.warning( - f"Brightness file does not exist: {brightness_path}" - ) + logger.warning(f"Brightness file does not exist: {brightness_path}") return -1 # Return -1 if file doesn't exist, indicating error. @screen_brightness.setter @@ -100,11 +89,8 @@ class Brightness(Service): try: exec_brightnessctl_async(f"--device '{screen_device}' set {value}") self.emit("screen", int((value / self.max_screen) * 100)) - logger.info( - f"Set screen brightness to {value} " - f"(out of {self.max_screen})" - ) + logger.info(f"Set screen brightness to {value} (out of {self.max_screen})") except GLib.Error as e: logger.error(f"Error setting screen brightness: {e.message}") except Exception as e: - logger.exception(f"Unexpected error setting screen brightness: {e}") \ No newline at end of file + logger.exception(f"Unexpected error setting screen brightness: {e}") diff --git a/styles/colors.css b/styles/colors.css index 584d0b6..a35f805 100644 --- a/styles/colors.css +++ b/styles/colors.css @@ -1,23 +1,23 @@ :vars { - --foreground: #f1dedc; - --background: #1a1110; - --cursor: #f1dedc; - --primary: #ffb3ad; - --on-primary: #571e1b; - --secondary: #e7bdb9; - --on-secondary: #442927; - --tertiary: #e1c28c; - --on-tertiary: #402d04; - --surface: #1a1110; - --surface-bright: #423735; + --foreground: #ede0d4; + --background: #18120b; + --cursor: #ede0d4; + --primary: #f5bc6f; + --on-primary: #452b00; + --secondary: #dec2a2; + --on-secondary: #3e2d16; + --tertiary: #b9cda0; + --on-tertiary: #253515; + --surface: #18120b; + --surface-bright: #3f3830; --error: #ffb4ab; --error-dim: #ff8678; --on-error: #690005; --error-container: #93000a; - --outline: #a08c8a; + --outline: #9c8f80; --shadow: #000000; - --red: #ffb4aa; - --red-dim: #ff8777; + --red: #ffb595; + --red-dim: #ff9162; --green: #b7d085; --green-dim: #a1c260; --yellow: #dec56e; diff --git a/styles/dock.css b/styles/dock.css index 396c7e6..8a3f976 100644 --- a/styles/dock.css +++ b/styles/dock.css @@ -40,6 +40,34 @@ border-right: none; } +#applications-dock { + background-color: var(--surface); + padding: 5px; + margin: 0; + border-radius: 16px; + border: none; + transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275); +} + +#dock.integrated { + background-color: var(--shadow); + padding: 4px; + margin: 0; + border-radius: 16px; + border: none; + transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275); +} + +#dock.integrated.edge, +#dock.integrated.dense { + background-color: var(--surface); + padding: 4px; + margin: 0; + border-radius: 12px; + border: none; + transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275); +} + #dock-full { transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275); opacity: 1;