diff --git a/README.md b/README.md deleted file mode 100644 index 9ac9fae..0000000 --- a/README.md +++ /dev/null @@ -1,133 +0,0 @@ -
-

It may not work on your machine

-
- -

- Logo -

-

Sparkles A modular and extensible shell for Hyprland, written on Fabric. Sparkles

- -

- - -People - - -Stars - - -Forks - - -Watches - - -Last Updated -

- -
- -
- fabric -
-
-
- -> [!WARNING] -> This configuration is tailored to my preferences with little regard for others. It's not designed to be "good"—just comfortable for me. - -

Package Installation

- -> [!NOTE] -> You need a functioning Hyprland installation. - -### Arch Linux - -> [!NOTE] -> You need to clone it into the Home directory. - -> [!TIP] -> This command also works for updating an existing installation! - -```bash -curl -fsSL https://raw.githubusercontent.com/S4NKALP/Modus/main/install.sh | bash -``` - -### Manual Installation - -1. Dependencies: - -```sh -fabric fabric-cli gnome-bluetooth-3.0 wf-recorder cliphist wl-clipboard grimblast power-profile-daemon adw-gtk-theme brightnessctl swww hypridle hyprlock imagemagick libnotify swappy libadwaita wlinhibit -``` - -```sh -python-materialyoucolor-git python-pillow python-toml python-ijson python-setproctitle python-requests python-numpy python-pywayland python-pyxdg -``` - -```sh -ttf-font-awesome otf-font-awesome ttf-material-symbols-variable-git ttf-google-sans ttf-opensans ttf-robot -``` - -- Icon: - -```sh -git clone https://github.com/vinceliuice/Tela-icon-theme -cd Tela-icon-theme -./install.sh nord -``` - -

Camera with Flash Screenshots

- - - - - - - - - - - - - - - - - - - -
- -

Rocket Todo

- -- [x] App Launcher -- [x] Power Menu -- [x] Wallpaper Picker -- [x] Todo -- [x] Bluetooth Manager -- [x] Clipboard Manager -- [x] OSD -- [x] Sh Runner -- [x] Emoji Picker -- [x] Update Screenshot -- [x] Installation script -- [x] Network Manager -- [x] Calendar -- [x] Power Manager -- [x] Dock -- [x] Web search in Launcher -- [ ] Notification Panel -- [ ] Improve CSS Styling -- [ ] Update Screenshot - -## Special Thanks - -A big thank you to the following people for their incredible help with code and creative ideas. Your help made a real difference! - -- [darsh](https://github.com/its-darsh): for creating Fabric, which made everything possible. -- [gummy bear album](https://github.com/muhchaudhary): for sharing fantastic code snippets that saved me time and effort. -- [axenide](https://github.com/Axenide): for the amazing config that not only inspired parts of mine but also provided some gems I couldn’t resist borrowing. -- [koeqaife](https://github.com/koeqaife): thanks for material-colors scripts - -I truly appreciate your support diff --git a/assets/logo.svg b/assets/logo.svg deleted file mode 100644 index 5d19dc1..0000000 --- a/assets/logo.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/screenshots/bluetooth.png b/assets/screenshots/bluetooth.png deleted file mode 100644 index 7033d48..0000000 Binary files a/assets/screenshots/bluetooth.png and /dev/null differ diff --git a/assets/screenshots/cliphist.png b/assets/screenshots/cliphist.png deleted file mode 100644 index 6e21db0..0000000 Binary files a/assets/screenshots/cliphist.png and /dev/null differ diff --git a/assets/screenshots/emoji.png b/assets/screenshots/emoji.png deleted file mode 100644 index 3202bf9..0000000 Binary files a/assets/screenshots/emoji.png and /dev/null differ diff --git a/assets/screenshots/home.png b/assets/screenshots/home.png deleted file mode 100644 index c7ba7c3..0000000 Binary files a/assets/screenshots/home.png and /dev/null differ diff --git a/assets/screenshots/notification.png b/assets/screenshots/notification.png deleted file mode 100644 index c8c9f30..0000000 Binary files a/assets/screenshots/notification.png and /dev/null differ diff --git a/assets/screenshots/osd.png b/assets/screenshots/osd.png deleted file mode 100644 index a02dec6..0000000 Binary files a/assets/screenshots/osd.png and /dev/null differ diff --git a/assets/screenshots/powermenu.png b/assets/screenshots/powermenu.png deleted file mode 100644 index 165fc0e..0000000 Binary files a/assets/screenshots/powermenu.png and /dev/null differ diff --git a/assets/screenshots/shrunner.png b/assets/screenshots/shrunner.png deleted file mode 100644 index 40f184b..0000000 Binary files a/assets/screenshots/shrunner.png and /dev/null differ diff --git a/assets/screenshots/todo.png b/assets/screenshots/todo.png deleted file mode 100644 index 0718afb..0000000 Binary files a/assets/screenshots/todo.png and /dev/null differ diff --git a/assets/screenshots/wallpaper.png b/assets/screenshots/wallpaper.png deleted file mode 100644 index 4e6789b..0000000 Binary files a/assets/screenshots/wallpaper.png and /dev/null differ diff --git a/assets/wallpaper/example-1.png b/assets/wallpaper/example-1.png deleted file mode 100644 index 137ceee..0000000 Binary files a/assets/wallpaper/example-1.png and /dev/null differ diff --git a/assets/wallpaper/example-2.png b/assets/wallpaper/example-2.png deleted file mode 100644 index 6d22cf9..0000000 Binary files a/assets/wallpaper/example-2.png and /dev/null differ diff --git a/assets/wallpaper/example-3.jpg b/assets/wallpaper/example-3.jpg deleted file mode 100755 index 5c2fd0d..0000000 Binary files a/assets/wallpaper/example-3.jpg and /dev/null differ diff --git a/assets/wallpaper/example-4.jpeg b/assets/wallpaper/example-4.jpeg deleted file mode 100644 index 7ca7ae6..0000000 Binary files a/assets/wallpaper/example-4.jpeg and /dev/null differ diff --git a/assets/wallpaper/example-5.jpg b/assets/wallpaper/example-5.jpg deleted file mode 100644 index eced4ab..0000000 Binary files a/assets/wallpaper/example-5.jpg and /dev/null differ diff --git a/bar.py b/bar.py new file mode 100644 index 0000000..7ef36a1 --- /dev/null +++ b/bar.py @@ -0,0 +1,564 @@ +import json +import os + +from fabric.hyprland.service import HyprlandEvent +from fabric.hyprland.widgets import ( + Language, + WorkspaceButton, + Workspaces, + get_hyprland_connection, +) +from fabric.utils.helpers import exec_shell_command_async +from fabric.widgets.box import Box +from fabric.widgets.button import Button +from fabric.widgets.centerbox import CenterBox +from fabric.widgets.datetime import DateTime +from fabric.widgets.label import Label +from fabric.widgets.revealer import Revealer +from gi.repository import Gdk, Gtk + +import config.data as data +import modules.icons as icons +from modules.controls import ControlSmall +from modules.dock import Dock +from modules.metrics import Battery, MetricsSmall, NetworkApplet +from modules.systemtray import SystemTray +from modules.weather import Weather +from widgets.wayland import WaylandWindow as Window + +CHINESE_NUMERALS = ["一", "二", "三", "四", "五", "六", "七", "八", "九", "〇"] + +# Tooltips +tooltip_apps = f"""Launcher +• Apps: Type to search. + +• Calculator [Prefix "="]: Solve a math expression. + e.g. "=2+2" + +• Converter [Prefix ";"]: Convert between units. + e.g. ";100 USD to EUR", ";10 km to miles" + +• Special Commands [Prefix ":"]: + :update - Open {data.APP_NAME_CAP}'s updater. + :d - Open Dashboard. + :w - Open Wallpapers.""" + +tooltip_power = """Power Menu""" +tooltip_tools = """Toolbox""" +tooltip_overview = """Overview""" + + +class Bar(Window): + def __init__(self, **kwargs): + super().__init__( + name="bar", + layer="top", + exclusivity="auto", + visible=True, + all_visible=True, + ) + + self.anchor_var = "" + self.margin_var = "" + + match data.BAR_POSITION: + case "Top": + self.anchor_var = "left top right" + case "Bottom": + self.anchor_var = "left bottom right" + case "Left": + self.anchor_var = "left" if data.CENTERED_BAR else "left top bottom" + case "Right": + self.anchor_var = "right" if data.CENTERED_BAR else "top right bottom" + case _: + self.anchor_var = "left top right" + + if data.VERTICAL: + match data.BAR_THEME: + case "Edge": + self.margin_var = "-8px -8px -8px -8px" + case _: + self.margin_var = "-4px -8px -4px -4px" + else: + match data.BAR_THEME: + case "Edge": + self.margin_var = "-8px -8px -8px -8px" + case _: + if data.BAR_POSITION == "Bottom": + self.margin_var = "-8px -4px -4px -4px" + else: + self.margin_var = "-4px -4px -8px -4px" + + self.set_anchor(self.anchor_var) + self.set_margin(self.margin_var) + + self.notch = kwargs.get("notch", None) + self.component_visibility = data.BAR_COMPONENTS_VISIBILITY + + self.dock_instance = None + self.integrated_dock_widget = None + + self.workspaces = Workspaces( + name="workspaces", + invert_scroll=True, + empty_scroll=True, + v_align="fill", + orientation="h" if not data.VERTICAL else "v", + spacing=8, + buttons=[ + WorkspaceButton( + h_expand=False, + v_expand=False, + h_align="center", + v_align="center", + id=i, + label=None, + style_classes=["vertical"] if data.VERTICAL else None, + ) + for i in range(1, 11) + ], + ) + + self.workspaces_num = Workspaces( + name="workspaces-num", + invert_scroll=True, + empty_scroll=True, + v_align="fill", + orientation="h" if not data.VERTICAL else "v", + spacing=0 if not data.BAR_WORKSPACE_USE_CHINESE_NUMERALS else 4, + buttons=[ + WorkspaceButton( + h_expand=False, + v_expand=False, + h_align="center", + v_align="center", + id=i, + label=CHINESE_NUMERALS[i - 1] + if data.BAR_WORKSPACE_USE_CHINESE_NUMERALS + and 1 <= i <= len(CHINESE_NUMERALS) + else str(i), + ) + for i in range(1, 11) + ], + ) + + self.ws_container = Box( + name="workspaces-container", + children=self.workspaces + if not data.BAR_WORKSPACE_SHOW_NUMBER + else self.workspaces_num, + ) + + self.button_tools = Button( + name="button-bar", + tooltip_markup=tooltip_tools, + on_clicked=lambda *_: self.tools_menu(), + child=Label(name="button-bar-label", markup=icons.toolbox), + ) + + self.connection = get_hyprland_connection() + self.button_tools.connect("enter_notify_event", self.on_button_enter) + self.button_tools.connect("leave_notify_event", self.on_button_leave) + + self.systray = SystemTray() + self.weather = Weather() + self.network = NetworkApplet() + + self.lang_label = Label(name="lang-label") + self.language = Button( + name="language", h_align="center", v_align="center", child=self.lang_label + ) + self.on_language_switch() + self.connection.connect("event::activelayout", self.on_language_switch) + + self.date_time = DateTime( + name="date-time", + formatters=["%H:%M"] if not data.VERTICAL else ["%H\n%M"], + h_align="center" if not data.VERTICAL else "fill", + v_align="center", + h_expand=True, + v_expand=True, + ) + + self.button_apps = Button( + name="button-bar", + tooltip_markup=tooltip_apps, + on_clicked=lambda *_: self.search_apps(), + child=Label(name="button-bar-label", markup=icons.apps), + ) + self.button_apps.connect("enter_notify_event", self.on_button_enter) + self.button_apps.connect("leave_notify_event", self.on_button_leave) + + self.button_power = Button( + name="button-bar", + tooltip_markup=tooltip_power, + on_clicked=lambda *_: self.power_menu(), + child=Label(name="button-bar-label", markup=icons.shutdown), + ) + self.button_power.connect("enter_notify_event", self.on_button_enter) + self.button_power.connect("leave_notify_event", self.on_button_leave) + + self.button_overview = Button( + name="button-bar", + tooltip_markup=tooltip_overview, + on_clicked=lambda *_: self.overview(), + child=Label(name="button-bar-label", markup=icons.windows), + ) + self.button_overview.connect("enter_notify_event", self.on_button_enter) + self.button_overview.connect("leave_notify_event", self.on_button_leave) + + self.control = ControlSmall() + self.metrics = MetricsSmall() + self.battery = Battery() + + self.apply_component_props() + + self.rev_right = [ + self.metrics, + self.control, + ] + + self.revealer_right = Revealer( + name="bar-revealer", + transition_type="slide-left", + child_revealed=True, + child=Box( + name="bar-revealer-box", + orientation="h", + spacing=4, + children=self.rev_right if not data.VERTICAL else None, + ), + ) + + self.boxed_revealer_right = Box( + name="boxed-revealer", + children=[ + self.revealer_right, + ], + ) + + self.rev_left = [ + self.weather, + self.network, + ] + + self.revealer_left = Revealer( + name="bar-revealer", + transition_type="slide-right", + child_revealed=True, + child=Box( + name="bar-revealer-box", + orientation="h", + spacing=4, + children=self.rev_left if not data.VERTICAL else None, + ), + ) + + self.boxed_revealer_left = Box( + name="boxed-revealer", + children=[ + self.revealer_left, + ], + ) + + self.h_start_children = [ + self.button_apps, + self.ws_container, + self.button_overview, + self.boxed_revealer_left, + ] + + self.h_end_children = [ + self.boxed_revealer_right, + self.battery, + self.systray, + self.button_tools, + self.language, + self.date_time, + self.button_power, + ] + + self.v_start_children = [ + self.button_apps, + self.systray, + self.control, + self.network, + self.button_tools, + ] + + self.v_center_children = [ + self.button_overview, + self.ws_container, + self.weather, + ] + + self.v_end_children = [ + self.battery, + self.metrics, + self.language, + self.date_time, + self.button_power, + ] + + self.v_all_children = [] + self.v_all_children.extend(self.v_start_children) + self.v_all_children.extend(self.v_center_children) + self.v_all_children.extend(self.v_end_children) + + if ( + data.DOCK_ENABLED + and data.BAR_POSITION == "Bottom" + or data.PANEL_THEME == "Panel" + and data.BAR_POSITION in ["Top", "Bottom"] + ): + if not data.VERTICAL: + self.dock_instance = Dock(integrated_mode=True) + self.integrated_dock_widget = self.dock_instance.wrapper + + is_centered_bar = data.VERTICAL and getattr(data, "CENTERED_BAR", False) + + bar_center_actual_children = None + + if self.integrated_dock_widget is not None: + bar_center_actual_children = self.integrated_dock_widget + elif data.VERTICAL: + bar_center_actual_children = Box( + orientation=Gtk.Orientation.VERTICAL, + spacing=4, + children=self.v_all_children + if is_centered_bar + else self.v_center_children, + ) + + self.bar_inner = CenterBox( + name="bar-inner", + orientation=Gtk.Orientation.HORIZONTAL + if not data.VERTICAL + else Gtk.Orientation.VERTICAL, + h_align="fill", + v_align="fill", + start_children=None + if is_centered_bar + else Box( + name="start-container", + spacing=4, + orientation=Gtk.Orientation.HORIZONTAL + if not data.VERTICAL + else Gtk.Orientation.VERTICAL, + children=self.h_start_children + if not data.VERTICAL + else self.v_start_children, + ), + center_children=bar_center_actual_children, + end_children=None + if is_centered_bar + else Box( + name="end-container", + spacing=4, + orientation=Gtk.Orientation.HORIZONTAL + if not data.VERTICAL + else Gtk.Orientation.VERTICAL, + children=self.h_end_children + if not data.VERTICAL + else self.v_end_children, + ), + ) + + self.children = self.bar_inner + + self.hidden = False + + self.themed_children = [ + self.button_apps, + self.button_overview, + self.button_power, + self.button_tools, + self.language, + self.date_time, + self.ws_container, + self.weather, + self.network, + self.battery, + self.metrics, + self.systray, + self.control, + ] + if self.integrated_dock_widget: + self.themed_children.append(self.integrated_dock_widget) + + current_theme = data.BAR_THEME + theme_classes = ["pills", "dense", "edge", "edgecenter"] + for tc in theme_classes: + self.bar_inner.remove_style_class(tc) + + self.style = None + match current_theme: + case "Pills": + self.style = "pills" + case "Dense": + self.style = "dense" + case "Edge": + if data.VERTICAL and data.CENTERED_BAR: + self.style = "edgecenter" + else: + self.style = "edge" + case _: + self.style = "pills" + + self.bar_inner.add_style_class(self.style) + + if self.integrated_dock_widget and hasattr( + self.integrated_dock_widget, "add_style_class" + ): + for theme_class_to_remove in ["pills", "dense", "edge"]: + style_context = self.integrated_dock_widget.get_style_context() + if style_context.has_class(theme_class_to_remove): + self.integrated_dock_widget.remove_style_class( + theme_class_to_remove + ) + self.integrated_dock_widget.add_style_class(self.style) + + if data.BAR_THEME == "Dense" or data.BAR_THEME == "Edge": + for child in self.themed_children: + if hasattr(child, "add_style_class"): + child.add_style_class("invert") + + match data.BAR_POSITION: + case "Top": + self.bar_inner.add_style_class("top") + case "Bottom": + self.bar_inner.add_style_class("bottom") + case "Left": + self.bar_inner.add_style_class("left") + case "Right": + self.bar_inner.add_style_class("right") + case _: + self.bar_inner.add_style_class("top") + + if data.VERTICAL: + self.bar_inner.add_style_class("vertical") + + self.systray._update_visibility() + self.chinese_numbers() + + def apply_component_props(self): + components = { + "button_apps": self.button_apps, + "systray": self.systray, + "control": self.control, + "network": self.network, + "button_tools": self.button_tools, + "button_overview": self.button_overview, + "ws_container": self.ws_container, + "weather": self.weather, + "battery": self.battery, + "metrics": self.metrics, + "language": self.language, + "date_time": self.date_time, + "button_power": self.button_power, + } + + for component_name, widget in components.items(): + if component_name in self.component_visibility: + widget.set_visible(self.component_visibility[component_name]) + + def toggle_component_visibility(self, component_name): + components = { + "button_apps": self.button_apps, + "systray": self.systray, + "control": self.control, + "network": self.network, + "button_tools": self.button_tools, + "button_overview": self.button_overview, + "ws_container": self.ws_container, + "weather": self.weather, + "battery": self.battery, + "metrics": self.metrics, + "language": self.language, + "date_time": self.date_time, + "button_power": self.button_power, + } + + 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] + ) + + 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"bar_{component_name}_visible"] = self.component_visibility[ + component_name + ] + + with open(config_file, "w") as f: + json.dump(config, f, indent=4) + except Exception as e: + print(f"Error updating config file: {e}") + + return self.component_visibility[component_name] + + return None + + def on_button_enter(self, widget, event): + window = widget.get_window() + if window: + window.set_cursor(Gdk.Cursor.new_from_name(widget.get_display(), "hand2")) + + def on_button_leave(self, widget, event): + window = widget.get_window() + if window: + window.set_cursor(None) + + def on_button_clicked(self, *args): + exec_shell_command_async("notify-send 'Botón presionado' '¡Funciona!'") + + def search_apps(self): + if self.notch: + self.notch.open_notch("launcher") + + def overview(self): + if self.notch: + self.notch.open_notch("overview") + + def power_menu(self): + if self.notch: + self.notch.open_notch("power") + + def tools_menu(self): + if self.notch: + self.notch.open_notch("tools") + + def on_language_switch(self, _=None, event: HyprlandEvent = None): + lang_data = ( + event.data[1] + if event and event.data and len(event.data) > 1 + else Language().get_label() + ) + self.language.set_tooltip_text(lang_data) + if not data.VERTICAL: + self.lang_label.set_label(lang_data[:3].upper()) + else: + self.lang_label.add_style_class("icon") + self.lang_label.set_markup(icons.keyboard) + + def toggle_hidden(self): + self.hidden = not self.hidden + if self.hidden: + self.bar_inner.add_style_class("hidden") + else: + self.bar_inner.remove_style_class("hidden") + + def chinese_numbers(self): + if data.BAR_WORKSPACE_USE_CHINESE_NUMERALS: + self.workspaces_num.add_style_class("chinese") + else: + self.workspaces_num.remove_style_class("chinese") diff --git a/config/__init__.py b/config/__init__.py index f94e240..4cde1a6 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -1,6 +1,6 @@ -from .data import * - +""" +Ax-Shell configuration package. +""" # Import only specific names actually defined in data.py # This prevents circular imports by not importing everything - -from .data import APP_NAME, APP_NAME_CAP, CACHE_DIR, CONFIG_FILE, STATE_DIR +from .data import APP_NAME, APP_NAME_CAP, CACHE_DIR, CONFIG_FILE diff --git a/config/assets/config.json b/config/assets/config.json deleted file mode 100644 index 2cdda50..0000000 --- a/config/assets/config.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "prefix_restart": "SHIFT ALT", - "suffix_restart": "T", - "prefix_bluetooth": "SHIFT ALT", - "suffix_bluetooth": "B", - "prefix_emoji": "SUPER", - "suffix_emoji": "E", - "prefix_cliphist": "SUPER", - "suffix_cliphist": "V", - "prefix_kanban": "SUPER", - "suffix_kanban": "T", - "prefix_toolbox": "SUPER", - "suffix_toolbox": "S", - "prefix_walls": "SUPER", - "suffix_walls": "W", - "prefix_launcher": "SUPER", - "suffix_launcher": "D", - "prefix_power": "SUPER", - "suffix_power": "X", - "prefix_tmux": "SUPER", - "suffix_tmux": "Z", - "prefix_notifications": "SUPER", - "suffix_notifications": "N", - "prefix_window_switcher": "ALT", - "suffix_window_switcher": "W", - "prefix_restart_inspector": "SUPER SHIFT", - "suffix_restart_inspector": "T", - "vertical": false, - "vertical_right_align": false, - "bottom_bar": true, - "centered_bar": true, - "terminal_command": "kitty -e", - "wallpapers_dir": "/home/sankalp/Pictures/wallpapers", - "dock_enabled": true, - "dock_always_occluded": false, - "dock_icon_size": 28, - "osd_enabled": true, - "bar_button_app_visible": true, - "bar_tray_visible": true, - "bar_workspaces_visible": true, - "bar_metrics_visible": true, - "bar_language_visible": true, - "bar_date_time_visible": true, - "bar_updates_visible": true, - "bar_indicators_visible": true, - "corners_visible": true, - "bar_metrics_disks": [ - "/" - ], - "metrics_visible": { - "cpu": true, - "ram": true, - "disk": true, - "swap": true, - "gpu": false - }, - "enabled_osd": true -} \ No newline at end of file diff --git a/config/assets/emoji.json b/config/assets/emoji.json deleted file mode 100644 index fa174ff..0000000 --- a/config/assets/emoji.json +++ /dev/null @@ -1,15509 +0,0 @@ -{ - "😀": { - "name": "grinning face", - "slug": "grinning_face", - "group": "Smileys & Emotion", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "😃": { - "name": "grinning face with big eyes", - "slug": "grinning_face_with_big_eyes", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "😄": { - "name": "grinning face with smiling eyes", - "slug": "grinning_face_with_smiling_eyes", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "😁": { - "name": "beaming face with smiling eyes", - "slug": "beaming_face_with_smiling_eyes", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "😆": { - "name": "grinning squinting face", - "slug": "grinning_squinting_face", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "😅": { - "name": "grinning face with sweat", - "slug": "grinning_face_with_sweat", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🤣": { - "name": "rolling on the floor laughing", - "slug": "rolling_on_the_floor_laughing", - "group": "Smileys & Emotion", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "😂": { - "name": "face with tears of joy", - "slug": "face_with_tears_of_joy", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🙂": { - "name": "slightly smiling face", - "slug": "slightly_smiling_face", - "group": "Smileys & Emotion", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🙃": { - "name": "upside-down face", - "slug": "upside_down_face", - "group": "Smileys & Emotion", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🫠": { - "name": "melting face", - "slug": "melting_face", - "group": "Smileys & Emotion", - "emoji_version": "14.0", - "unicode_version": "14.0", - "skin_tone_support": false - }, - "😉": { - "name": "winking face", - "slug": "winking_face", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "😊": { - "name": "smiling face with smiling eyes", - "slug": "smiling_face_with_smiling_eyes", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "😇": { - "name": "smiling face with halo", - "slug": "smiling_face_with_halo", - "group": "Smileys & Emotion", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🥰": { - "name": "smiling face with hearts", - "slug": "smiling_face_with_hearts", - "group": "Smileys & Emotion", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "😍": { - "name": "smiling face with heart-eyes", - "slug": "smiling_face_with_heart_eyes", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🤩": { - "name": "star-struck", - "slug": "star_struck", - "group": "Smileys & Emotion", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "😘": { - "name": "face blowing a kiss", - "slug": "face_blowing_a_kiss", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "😗": { - "name": "kissing face", - "slug": "kissing_face", - "group": "Smileys & Emotion", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "☺️": { - "name": "smiling face", - "slug": "smiling_face", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "😚": { - "name": "kissing face with closed eyes", - "slug": "kissing_face_with_closed_eyes", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "😙": { - "name": "kissing face with smiling eyes", - "slug": "kissing_face_with_smiling_eyes", - "group": "Smileys & Emotion", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🥲": { - "name": "smiling face with tear", - "slug": "smiling_face_with_tear", - "group": "Smileys & Emotion", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "😋": { - "name": "face savoring food", - "slug": "face_savoring_food", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "😛": { - "name": "face with tongue", - "slug": "face_with_tongue", - "group": "Smileys & Emotion", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "😜": { - "name": "winking face with tongue", - "slug": "winking_face_with_tongue", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🤪": { - "name": "zany face", - "slug": "zany_face", - "group": "Smileys & Emotion", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "😝": { - "name": "squinting face with tongue", - "slug": "squinting_face_with_tongue", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🤑": { - "name": "money-mouth face", - "slug": "money_mouth_face", - "group": "Smileys & Emotion", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🤗": { - "name": "smiling face with open hands", - "slug": "smiling_face_with_open_hands", - "group": "Smileys & Emotion", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🤭": { - "name": "face with hand over mouth", - "slug": "face_with_hand_over_mouth", - "group": "Smileys & Emotion", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🫢": { - "name": "face with open eyes and hand over mouth", - "slug": "face_with_open_eyes_and_hand_over_mouth", - "group": "Smileys & Emotion", - "emoji_version": "14.0", - "unicode_version": "14.0", - "skin_tone_support": false - }, - "🫣": { - "name": "face with peeking eye", - "slug": "face_with_peeking_eye", - "group": "Smileys & Emotion", - "emoji_version": "14.0", - "unicode_version": "14.0", - "skin_tone_support": false - }, - "🤫": { - "name": "shushing face", - "slug": "shushing_face", - "group": "Smileys & Emotion", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🤔": { - "name": "thinking face", - "slug": "thinking_face", - "group": "Smileys & Emotion", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🫡": { - "name": "saluting face", - "slug": "saluting_face", - "group": "Smileys & Emotion", - "emoji_version": "14.0", - "unicode_version": "14.0", - "skin_tone_support": false - }, - "🤐": { - "name": "zipper-mouth face", - "slug": "zipper_mouth_face", - "group": "Smileys & Emotion", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🤨": { - "name": "face with raised eyebrow", - "slug": "face_with_raised_eyebrow", - "group": "Smileys & Emotion", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "😐": { - "name": "neutral face", - "slug": "neutral_face", - "group": "Smileys & Emotion", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "😑": { - "name": "expressionless face", - "slug": "expressionless_face", - "group": "Smileys & Emotion", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "😶": { - "name": "face without mouth", - "slug": "face_without_mouth", - "group": "Smileys & Emotion", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🫥": { - "name": "dotted line face", - "slug": "dotted_line_face", - "group": "Smileys & Emotion", - "emoji_version": "14.0", - "unicode_version": "14.0", - "skin_tone_support": false - }, - "😶‍🌫️": { - "name": "face in clouds", - "slug": "face_in_clouds", - "group": "Smileys & Emotion", - "emoji_version": "13.1", - "unicode_version": "13.1", - "skin_tone_support": false - }, - "😏": { - "name": "smirking face", - "slug": "smirking_face", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "😒": { - "name": "unamused face", - "slug": "unamused_face", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🙄": { - "name": "face with rolling eyes", - "slug": "face_with_rolling_eyes", - "group": "Smileys & Emotion", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "😬": { - "name": "grimacing face", - "slug": "grimacing_face", - "group": "Smileys & Emotion", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "😮‍💨": { - "name": "face exhaling", - "slug": "face_exhaling", - "group": "Smileys & Emotion", - "emoji_version": "13.1", - "unicode_version": "13.1", - "skin_tone_support": false - }, - "🤥": { - "name": "lying face", - "slug": "lying_face", - "group": "Smileys & Emotion", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🫨": { - "name": "shaking face", - "slug": "shaking_face", - "group": "Smileys & Emotion", - "emoji_version": "15.0", - "unicode_version": "15.0", - "skin_tone_support": false - }, - "🙂‍↔️": { - "name": "head shaking horizontally", - "slug": "head_shaking_horizontally", - "group": "Smileys & Emotion", - "emoji_version": "15.1", - "unicode_version": "15.1", - "skin_tone_support": false - }, - "🙂‍↕️": { - "name": "head shaking vertically", - "slug": "head_shaking_vertically", - "group": "Smileys & Emotion", - "emoji_version": "15.1", - "unicode_version": "15.1", - "skin_tone_support": false - }, - "😌": { - "name": "relieved face", - "slug": "relieved_face", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "😔": { - "name": "pensive face", - "slug": "pensive_face", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "😪": { - "name": "sleepy face", - "slug": "sleepy_face", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🤤": { - "name": "drooling face", - "slug": "drooling_face", - "group": "Smileys & Emotion", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "😴": { - "name": "sleeping face", - "slug": "sleeping_face", - "group": "Smileys & Emotion", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "😷": { - "name": "face with medical mask", - "slug": "face_with_medical_mask", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🤒": { - "name": "face with thermometer", - "slug": "face_with_thermometer", - "group": "Smileys & Emotion", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🤕": { - "name": "face with head-bandage", - "slug": "face_with_head_bandage", - "group": "Smileys & Emotion", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🤢": { - "name": "nauseated face", - "slug": "nauseated_face", - "group": "Smileys & Emotion", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🤮": { - "name": "face vomiting", - "slug": "face_vomiting", - "group": "Smileys & Emotion", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🤧": { - "name": "sneezing face", - "slug": "sneezing_face", - "group": "Smileys & Emotion", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🥵": { - "name": "hot face", - "slug": "hot_face", - "group": "Smileys & Emotion", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🥶": { - "name": "cold face", - "slug": "cold_face", - "group": "Smileys & Emotion", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🥴": { - "name": "woozy face", - "slug": "woozy_face", - "group": "Smileys & Emotion", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "😵": { - "name": "face with crossed-out eyes", - "slug": "face_with_crossed_out_eyes", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "😵‍💫": { - "name": "face with spiral eyes", - "slug": "face_with_spiral_eyes", - "group": "Smileys & Emotion", - "emoji_version": "13.1", - "unicode_version": "13.1", - "skin_tone_support": false - }, - "🤯": { - "name": "exploding head", - "slug": "exploding_head", - "group": "Smileys & Emotion", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🤠": { - "name": "cowboy hat face", - "slug": "cowboy_hat_face", - "group": "Smileys & Emotion", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🥳": { - "name": "partying face", - "slug": "partying_face", - "group": "Smileys & Emotion", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🥸": { - "name": "disguised face", - "slug": "disguised_face", - "group": "Smileys & Emotion", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "😎": { - "name": "smiling face with sunglasses", - "slug": "smiling_face_with_sunglasses", - "group": "Smileys & Emotion", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🤓": { - "name": "nerd face", - "slug": "nerd_face", - "group": "Smileys & Emotion", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🧐": { - "name": "face with monocle", - "slug": "face_with_monocle", - "group": "Smileys & Emotion", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "😕": { - "name": "confused face", - "slug": "confused_face", - "group": "Smileys & Emotion", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🫤": { - "name": "face with diagonal mouth", - "slug": "face_with_diagonal_mouth", - "group": "Smileys & Emotion", - "emoji_version": "14.0", - "unicode_version": "14.0", - "skin_tone_support": false - }, - "😟": { - "name": "worried face", - "slug": "worried_face", - "group": "Smileys & Emotion", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🙁": { - "name": "slightly frowning face", - "slug": "slightly_frowning_face", - "group": "Smileys & Emotion", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "☹️": { - "name": "frowning face", - "slug": "frowning_face", - "group": "Smileys & Emotion", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "😮": { - "name": "face with open mouth", - "slug": "face_with_open_mouth", - "group": "Smileys & Emotion", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "😯": { - "name": "hushed face", - "slug": "hushed_face", - "group": "Smileys & Emotion", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "😲": { - "name": "astonished face", - "slug": "astonished_face", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "😳": { - "name": "flushed face", - "slug": "flushed_face", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🥺": { - "name": "pleading face", - "slug": "pleading_face", - "group": "Smileys & Emotion", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🥹": { - "name": "face holding back tears", - "slug": "face_holding_back_tears", - "group": "Smileys & Emotion", - "emoji_version": "14.0", - "unicode_version": "14.0", - "skin_tone_support": false - }, - "😦": { - "name": "frowning face with open mouth", - "slug": "frowning_face_with_open_mouth", - "group": "Smileys & Emotion", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "😧": { - "name": "anguished face", - "slug": "anguished_face", - "group": "Smileys & Emotion", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "😨": { - "name": "fearful face", - "slug": "fearful_face", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "😰": { - "name": "anxious face with sweat", - "slug": "anxious_face_with_sweat", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "😥": { - "name": "sad but relieved face", - "slug": "sad_but_relieved_face", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "😢": { - "name": "crying face", - "slug": "crying_face", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "😭": { - "name": "loudly crying face", - "slug": "loudly_crying_face", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "😱": { - "name": "face screaming in fear", - "slug": "face_screaming_in_fear", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "😖": { - "name": "confounded face", - "slug": "confounded_face", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "😣": { - "name": "persevering face", - "slug": "persevering_face", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "😞": { - "name": "disappointed face", - "slug": "disappointed_face", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "😓": { - "name": "downcast face with sweat", - "slug": "downcast_face_with_sweat", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "😩": { - "name": "weary face", - "slug": "weary_face", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "😫": { - "name": "tired face", - "slug": "tired_face", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🥱": { - "name": "yawning face", - "slug": "yawning_face", - "group": "Smileys & Emotion", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "😤": { - "name": "face with steam from nose", - "slug": "face_with_steam_from_nose", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "😡": { - "name": "enraged face", - "slug": "enraged_face", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "😠": { - "name": "angry face", - "slug": "angry_face", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🤬": { - "name": "face with symbols on mouth", - "slug": "face_with_symbols_on_mouth", - "group": "Smileys & Emotion", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "😈": { - "name": "smiling face with horns", - "slug": "smiling_face_with_horns", - "group": "Smileys & Emotion", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "👿": { - "name": "angry face with horns", - "slug": "angry_face_with_horns", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "💀": { - "name": "skull", - "slug": "skull", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "☠️": { - "name": "skull and crossbones", - "slug": "skull_and_crossbones", - "group": "Smileys & Emotion", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "💩": { - "name": "pile of poo", - "slug": "pile_of_poo", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🤡": { - "name": "clown face", - "slug": "clown_face", - "group": "Smileys & Emotion", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "👹": { - "name": "ogre", - "slug": "ogre", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "👺": { - "name": "goblin", - "slug": "goblin", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "👻": { - "name": "ghost", - "slug": "ghost", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "👽": { - "name": "alien", - "slug": "alien", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "👾": { - "name": "alien monster", - "slug": "alien_monster", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🤖": { - "name": "robot", - "slug": "robot", - "group": "Smileys & Emotion", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "😺": { - "name": "grinning cat", - "slug": "grinning_cat", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "😸": { - "name": "grinning cat with smiling eyes", - "slug": "grinning_cat_with_smiling_eyes", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "😹": { - "name": "cat with tears of joy", - "slug": "cat_with_tears_of_joy", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "😻": { - "name": "smiling cat with heart-eyes", - "slug": "smiling_cat_with_heart_eyes", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "😼": { - "name": "cat with wry smile", - "slug": "cat_with_wry_smile", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "😽": { - "name": "kissing cat", - "slug": "kissing_cat", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🙀": { - "name": "weary cat", - "slug": "weary_cat", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "😿": { - "name": "crying cat", - "slug": "crying_cat", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "😾": { - "name": "pouting cat", - "slug": "pouting_cat", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🙈": { - "name": "see-no-evil monkey", - "slug": "see_no_evil_monkey", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🙉": { - "name": "hear-no-evil monkey", - "slug": "hear_no_evil_monkey", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🙊": { - "name": "speak-no-evil monkey", - "slug": "speak_no_evil_monkey", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "💌": { - "name": "love letter", - "slug": "love_letter", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "💘": { - "name": "heart with arrow", - "slug": "heart_with_arrow", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "💝": { - "name": "heart with ribbon", - "slug": "heart_with_ribbon", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "💖": { - "name": "sparkling heart", - "slug": "sparkling_heart", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "💗": { - "name": "growing heart", - "slug": "growing_heart", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "💓": { - "name": "beating heart", - "slug": "beating_heart", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "💞": { - "name": "revolving hearts", - "slug": "revolving_hearts", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "💕": { - "name": "two hearts", - "slug": "two_hearts", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "💟": { - "name": "heart decoration", - "slug": "heart_decoration", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "❣️": { - "name": "heart exclamation", - "slug": "heart_exclamation", - "group": "Smileys & Emotion", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "💔": { - "name": "broken heart", - "slug": "broken_heart", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "❤️‍🔥": { - "name": "heart on fire", - "slug": "heart_on_fire", - "group": "Smileys & Emotion", - "emoji_version": "13.1", - "unicode_version": "13.1", - "skin_tone_support": false - }, - "❤️‍🩹": { - "name": "mending heart", - "slug": "mending_heart", - "group": "Smileys & Emotion", - "emoji_version": "13.1", - "unicode_version": "13.1", - "skin_tone_support": false - }, - "❤️": { - "name": "red heart", - "slug": "red_heart", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🩷": { - "name": "pink heart", - "slug": "pink_heart", - "group": "Smileys & Emotion", - "emoji_version": "15.0", - "unicode_version": "15.0", - "skin_tone_support": false - }, - "🧡": { - "name": "orange heart", - "slug": "orange_heart", - "group": "Smileys & Emotion", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "💛": { - "name": "yellow heart", - "slug": "yellow_heart", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "💚": { - "name": "green heart", - "slug": "green_heart", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "💙": { - "name": "blue heart", - "slug": "blue_heart", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🩵": { - "name": "light blue heart", - "slug": "light_blue_heart", - "group": "Smileys & Emotion", - "emoji_version": "15.0", - "unicode_version": "15.0", - "skin_tone_support": false - }, - "💜": { - "name": "purple heart", - "slug": "purple_heart", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🤎": { - "name": "brown heart", - "slug": "brown_heart", - "group": "Smileys & Emotion", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🖤": { - "name": "black heart", - "slug": "black_heart", - "group": "Smileys & Emotion", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🩶": { - "name": "grey heart", - "slug": "grey_heart", - "group": "Smileys & Emotion", - "emoji_version": "15.0", - "unicode_version": "15.0", - "skin_tone_support": false - }, - "🤍": { - "name": "white heart", - "slug": "white_heart", - "group": "Smileys & Emotion", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "💋": { - "name": "kiss mark", - "slug": "kiss_mark", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "💯": { - "name": "hundred points", - "slug": "hundred_points", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "💢": { - "name": "anger symbol", - "slug": "anger_symbol", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "💥": { - "name": "collision", - "slug": "collision", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "💫": { - "name": "dizzy", - "slug": "dizzy", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "💦": { - "name": "sweat droplets", - "slug": "sweat_droplets", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "💨": { - "name": "dashing away", - "slug": "dashing_away", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🕳️": { - "name": "hole", - "slug": "hole", - "group": "Smileys & Emotion", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "💬": { - "name": "speech balloon", - "slug": "speech_balloon", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "👁️‍🗨️": { - "name": "eye in speech bubble", - "slug": "eye_in_speech_bubble", - "group": "Smileys & Emotion", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🗨️": { - "name": "left speech bubble", - "slug": "left_speech_bubble", - "group": "Smileys & Emotion", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🗯️": { - "name": "right anger bubble", - "slug": "right_anger_bubble", - "group": "Smileys & Emotion", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "💭": { - "name": "thought balloon", - "slug": "thought_balloon", - "group": "Smileys & Emotion", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "💤": { - "name": "ZZZ", - "slug": "zzz", - "group": "Smileys & Emotion", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "👋": { - "name": "waving hand", - "slug": "waving_hand", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🤚": { - "name": "raised back of hand", - "slug": "raised_back_of_hand", - "group": "People & Body", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "3.0" - }, - "🖐️": { - "name": "hand with fingers splayed", - "slug": "hand_with_fingers_splayed", - "group": "People & Body", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "✋": { - "name": "raised hand", - "slug": "raised_hand", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🖖": { - "name": "vulcan salute", - "slug": "vulcan_salute", - "group": "People & Body", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🫱": { - "name": "rightwards hand", - "slug": "rightwards_hand", - "group": "People & Body", - "emoji_version": "14.0", - "unicode_version": "14.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "14.0" - }, - "🫲": { - "name": "leftwards hand", - "slug": "leftwards_hand", - "group": "People & Body", - "emoji_version": "14.0", - "unicode_version": "14.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "14.0" - }, - "🫳": { - "name": "palm down hand", - "slug": "palm_down_hand", - "group": "People & Body", - "emoji_version": "14.0", - "unicode_version": "14.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "14.0" - }, - "🫴": { - "name": "palm up hand", - "slug": "palm_up_hand", - "group": "People & Body", - "emoji_version": "14.0", - "unicode_version": "14.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "14.0" - }, - "🫷": { - "name": "leftwards pushing hand", - "slug": "leftwards_pushing_hand", - "group": "People & Body", - "emoji_version": "15.0", - "unicode_version": "15.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "15.0" - }, - "🫸": { - "name": "rightwards pushing hand", - "slug": "rightwards_pushing_hand", - "group": "People & Body", - "emoji_version": "15.0", - "unicode_version": "15.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "15.0" - }, - "👌": { - "name": "OK hand", - "slug": "ok_hand", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🤌": { - "name": "pinched fingers", - "slug": "pinched_fingers", - "group": "People & Body", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "13.0" - }, - "🤏": { - "name": "pinching hand", - "slug": "pinching_hand", - "group": "People & Body", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.0" - }, - "✌️": { - "name": "victory hand", - "slug": "victory_hand", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🤞": { - "name": "crossed fingers", - "slug": "crossed_fingers", - "group": "People & Body", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "3.0" - }, - "🫰": { - "name": "hand with index finger and thumb crossed", - "slug": "hand_with_index_finger_and_thumb_crossed", - "group": "People & Body", - "emoji_version": "14.0", - "unicode_version": "14.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "14.0" - }, - "🤟": { - "name": "love-you gesture", - "slug": "love_you_gesture", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "5.0" - }, - "🤘": { - "name": "sign of the horns", - "slug": "sign_of_the_horns", - "group": "People & Body", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🤙": { - "name": "call me hand", - "slug": "call_me_hand", - "group": "People & Body", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "3.0" - }, - "👈": { - "name": "backhand index pointing left", - "slug": "backhand_index_pointing_left", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "👉": { - "name": "backhand index pointing right", - "slug": "backhand_index_pointing_right", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "👆": { - "name": "backhand index pointing up", - "slug": "backhand_index_pointing_up", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🖕": { - "name": "middle finger", - "slug": "middle_finger", - "group": "People & Body", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "👇": { - "name": "backhand index pointing down", - "slug": "backhand_index_pointing_down", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "☝️": { - "name": "index pointing up", - "slug": "index_pointing_up", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🫵": { - "name": "index pointing at the viewer", - "slug": "index_pointing_at_the_viewer", - "group": "People & Body", - "emoji_version": "14.0", - "unicode_version": "14.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "14.0" - }, - "👍": { - "name": "thumbs up", - "slug": "thumbs_up", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "👎": { - "name": "thumbs down", - "slug": "thumbs_down", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "✊": { - "name": "raised fist", - "slug": "raised_fist", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "👊": { - "name": "oncoming fist", - "slug": "oncoming_fist", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🤛": { - "name": "left-facing fist", - "slug": "left_facing_fist", - "group": "People & Body", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "3.0" - }, - "🤜": { - "name": "right-facing fist", - "slug": "right_facing_fist", - "group": "People & Body", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "3.0" - }, - "👏": { - "name": "clapping hands", - "slug": "clapping_hands", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🙌": { - "name": "raising hands", - "slug": "raising_hands", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🫶": { - "name": "heart hands", - "slug": "heart_hands", - "group": "People & Body", - "emoji_version": "14.0", - "unicode_version": "14.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "14.0" - }, - "👐": { - "name": "open hands", - "slug": "open_hands", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🤲": { - "name": "palms up together", - "slug": "palms_up_together", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "5.0" - }, - "🤝": { - "name": "handshake", - "slug": "handshake", - "group": "People & Body", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "14.0" - }, - "🙏": { - "name": "folded hands", - "slug": "folded_hands", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "✍️": { - "name": "writing hand", - "slug": "writing_hand", - "group": "People & Body", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "💅": { - "name": "nail polish", - "slug": "nail_polish", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🤳": { - "name": "selfie", - "slug": "selfie", - "group": "People & Body", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "3.0" - }, - "💪": { - "name": "flexed biceps", - "slug": "flexed_biceps", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🦾": { - "name": "mechanical arm", - "slug": "mechanical_arm", - "group": "People & Body", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🦿": { - "name": "mechanical leg", - "slug": "mechanical_leg", - "group": "People & Body", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🦵": { - "name": "leg", - "slug": "leg", - "group": "People & Body", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "11.0" - }, - "🦶": { - "name": "foot", - "slug": "foot", - "group": "People & Body", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "11.0" - }, - "👂": { - "name": "ear", - "slug": "ear", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🦻": { - "name": "ear with hearing aid", - "slug": "ear_with_hearing_aid", - "group": "People & Body", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.0" - }, - "👃": { - "name": "nose", - "slug": "nose", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🧠": { - "name": "brain", - "slug": "brain", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🫀": { - "name": "anatomical heart", - "slug": "anatomical_heart", - "group": "People & Body", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🫁": { - "name": "lungs", - "slug": "lungs", - "group": "People & Body", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🦷": { - "name": "tooth", - "slug": "tooth", - "group": "People & Body", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🦴": { - "name": "bone", - "slug": "bone", - "group": "People & Body", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "👀": { - "name": "eyes", - "slug": "eyes", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "👁️": { - "name": "eye", - "slug": "eye", - "group": "People & Body", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "👅": { - "name": "tongue", - "slug": "tongue", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "👄": { - "name": "mouth", - "slug": "mouth", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🫦": { - "name": "biting lip", - "slug": "biting_lip", - "group": "People & Body", - "emoji_version": "14.0", - "unicode_version": "14.0", - "skin_tone_support": false - }, - "👶": { - "name": "baby", - "slug": "baby", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🧒": { - "name": "child", - "slug": "child", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "5.0" - }, - "👦": { - "name": "boy", - "slug": "boy", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "👧": { - "name": "girl", - "slug": "girl", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🧑": { - "name": "person", - "slug": "person", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "5.0" - }, - "👱": { - "name": "person blond hair", - "slug": "person_blond_hair", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "👨": { - "name": "man", - "slug": "man", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🧔": { - "name": "person beard", - "slug": "person_beard", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "5.0" - }, - "🧔‍♂️": { - "name": "man beard", - "slug": "man_beard", - "group": "People & Body", - "emoji_version": "13.1", - "unicode_version": "13.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "13.1" - }, - "🧔‍♀️": { - "name": "woman beard", - "slug": "woman_beard", - "group": "People & Body", - "emoji_version": "13.1", - "unicode_version": "13.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "13.1" - }, - "👨‍🦰": { - "name": "man red hair", - "slug": "man_red_hair", - "group": "People & Body", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "11.0" - }, - "👨‍🦱": { - "name": "man curly hair", - "slug": "man_curly_hair", - "group": "People & Body", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "11.0" - }, - "👨‍🦳": { - "name": "man white hair", - "slug": "man_white_hair", - "group": "People & Body", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "11.0" - }, - "👨‍🦲": { - "name": "man bald", - "slug": "man_bald", - "group": "People & Body", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "11.0" - }, - "👩": { - "name": "woman", - "slug": "woman", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "👩‍🦰": { - "name": "woman red hair", - "slug": "woman_red_hair", - "group": "People & Body", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "11.0" - }, - "🧑‍🦰": { - "name": "person red hair", - "slug": "person_red_hair", - "group": "People & Body", - "emoji_version": "12.1", - "unicode_version": "12.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.1" - }, - "👩‍🦱": { - "name": "woman curly hair", - "slug": "woman_curly_hair", - "group": "People & Body", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "11.0" - }, - "🧑‍🦱": { - "name": "person curly hair", - "slug": "person_curly_hair", - "group": "People & Body", - "emoji_version": "12.1", - "unicode_version": "12.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.1" - }, - "👩‍🦳": { - "name": "woman white hair", - "slug": "woman_white_hair", - "group": "People & Body", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "11.0" - }, - "🧑‍🦳": { - "name": "person white hair", - "slug": "person_white_hair", - "group": "People & Body", - "emoji_version": "12.1", - "unicode_version": "12.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.1" - }, - "👩‍🦲": { - "name": "woman bald", - "slug": "woman_bald", - "group": "People & Body", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "11.0" - }, - "🧑‍🦲": { - "name": "person bald", - "slug": "person_bald", - "group": "People & Body", - "emoji_version": "12.1", - "unicode_version": "12.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.1" - }, - "👱‍♀️": { - "name": "woman blond hair", - "slug": "woman_blond_hair", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "👱‍♂️": { - "name": "man blond hair", - "slug": "man_blond_hair", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🧓": { - "name": "older person", - "slug": "older_person", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "5.0" - }, - "👴": { - "name": "old man", - "slug": "old_man", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "👵": { - "name": "old woman", - "slug": "old_woman", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🙍": { - "name": "person frowning", - "slug": "person_frowning", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🙍‍♂️": { - "name": "man frowning", - "slug": "man_frowning", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🙍‍♀️": { - "name": "woman frowning", - "slug": "woman_frowning", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🙎": { - "name": "person pouting", - "slug": "person_pouting", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🙎‍♂️": { - "name": "man pouting", - "slug": "man_pouting", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🙎‍♀️": { - "name": "woman pouting", - "slug": "woman_pouting", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🙅": { - "name": "person gesturing NO", - "slug": "person_gesturing_no", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🙅‍♂️": { - "name": "man gesturing NO", - "slug": "man_gesturing_no", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🙅‍♀️": { - "name": "woman gesturing NO", - "slug": "woman_gesturing_no", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🙆": { - "name": "person gesturing OK", - "slug": "person_gesturing_ok", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🙆‍♂️": { - "name": "man gesturing OK", - "slug": "man_gesturing_ok", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🙆‍♀️": { - "name": "woman gesturing OK", - "slug": "woman_gesturing_ok", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "💁": { - "name": "person tipping hand", - "slug": "person_tipping_hand", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "💁‍♂️": { - "name": "man tipping hand", - "slug": "man_tipping_hand", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "💁‍♀️": { - "name": "woman tipping hand", - "slug": "woman_tipping_hand", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🙋": { - "name": "person raising hand", - "slug": "person_raising_hand", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🙋‍♂️": { - "name": "man raising hand", - "slug": "man_raising_hand", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🙋‍♀️": { - "name": "woman raising hand", - "slug": "woman_raising_hand", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🧏": { - "name": "deaf person", - "slug": "deaf_person", - "group": "People & Body", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.0" - }, - "🧏‍♂️": { - "name": "deaf man", - "slug": "deaf_man", - "group": "People & Body", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.0" - }, - "🧏‍♀️": { - "name": "deaf woman", - "slug": "deaf_woman", - "group": "People & Body", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.0" - }, - "🙇": { - "name": "person bowing", - "slug": "person_bowing", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🙇‍♂️": { - "name": "man bowing", - "slug": "man_bowing", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🙇‍♀️": { - "name": "woman bowing", - "slug": "woman_bowing", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🤦": { - "name": "person facepalming", - "slug": "person_facepalming", - "group": "People & Body", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "3.0" - }, - "🤦‍♂️": { - "name": "man facepalming", - "slug": "man_facepalming", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🤦‍♀️": { - "name": "woman facepalming", - "slug": "woman_facepalming", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🤷": { - "name": "person shrugging", - "slug": "person_shrugging", - "group": "People & Body", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "3.0" - }, - "🤷‍♂️": { - "name": "man shrugging", - "slug": "man_shrugging", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🤷‍♀️": { - "name": "woman shrugging", - "slug": "woman_shrugging", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🧑‍⚕️": { - "name": "health worker", - "slug": "health_worker", - "group": "People & Body", - "emoji_version": "12.1", - "unicode_version": "12.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.1" - }, - "👨‍⚕️": { - "name": "man health worker", - "slug": "man_health_worker", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "👩‍⚕️": { - "name": "woman health worker", - "slug": "woman_health_worker", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🧑‍🎓": { - "name": "student", - "slug": "student", - "group": "People & Body", - "emoji_version": "12.1", - "unicode_version": "12.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.1" - }, - "👨‍🎓": { - "name": "man student", - "slug": "man_student", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "👩‍🎓": { - "name": "woman student", - "slug": "woman_student", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🧑‍🏫": { - "name": "teacher", - "slug": "teacher", - "group": "People & Body", - "emoji_version": "12.1", - "unicode_version": "12.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.1" - }, - "👨‍🏫": { - "name": "man teacher", - "slug": "man_teacher", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "👩‍🏫": { - "name": "woman teacher", - "slug": "woman_teacher", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🧑‍⚖️": { - "name": "judge", - "slug": "judge", - "group": "People & Body", - "emoji_version": "12.1", - "unicode_version": "12.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.1" - }, - "👨‍⚖️": { - "name": "man judge", - "slug": "man_judge", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "👩‍⚖️": { - "name": "woman judge", - "slug": "woman_judge", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🧑‍🌾": { - "name": "farmer", - "slug": "farmer", - "group": "People & Body", - "emoji_version": "12.1", - "unicode_version": "12.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.1" - }, - "👨‍🌾": { - "name": "man farmer", - "slug": "man_farmer", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "👩‍🌾": { - "name": "woman farmer", - "slug": "woman_farmer", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🧑‍🍳": { - "name": "cook", - "slug": "cook", - "group": "People & Body", - "emoji_version": "12.1", - "unicode_version": "12.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.1" - }, - "👨‍🍳": { - "name": "man cook", - "slug": "man_cook", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "👩‍🍳": { - "name": "woman cook", - "slug": "woman_cook", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🧑‍🔧": { - "name": "mechanic", - "slug": "mechanic", - "group": "People & Body", - "emoji_version": "12.1", - "unicode_version": "12.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.1" - }, - "👨‍🔧": { - "name": "man mechanic", - "slug": "man_mechanic", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "👩‍🔧": { - "name": "woman mechanic", - "slug": "woman_mechanic", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🧑‍🏭": { - "name": "factory worker", - "slug": "factory_worker", - "group": "People & Body", - "emoji_version": "12.1", - "unicode_version": "12.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.1" - }, - "👨‍🏭": { - "name": "man factory worker", - "slug": "man_factory_worker", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "👩‍🏭": { - "name": "woman factory worker", - "slug": "woman_factory_worker", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🧑‍💼": { - "name": "office worker", - "slug": "office_worker", - "group": "People & Body", - "emoji_version": "12.1", - "unicode_version": "12.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.1" - }, - "👨‍💼": { - "name": "man office worker", - "slug": "man_office_worker", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "👩‍💼": { - "name": "woman office worker", - "slug": "woman_office_worker", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🧑‍🔬": { - "name": "scientist", - "slug": "scientist", - "group": "People & Body", - "emoji_version": "12.1", - "unicode_version": "12.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.1" - }, - "👨‍🔬": { - "name": "man scientist", - "slug": "man_scientist", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "👩‍🔬": { - "name": "woman scientist", - "slug": "woman_scientist", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🧑‍💻": { - "name": "technologist", - "slug": "technologist", - "group": "People & Body", - "emoji_version": "12.1", - "unicode_version": "12.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.1" - }, - "👨‍💻": { - "name": "man technologist", - "slug": "man_technologist", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "👩‍💻": { - "name": "woman technologist", - "slug": "woman_technologist", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🧑‍🎤": { - "name": "singer", - "slug": "singer", - "group": "People & Body", - "emoji_version": "12.1", - "unicode_version": "12.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.1" - }, - "👨‍🎤": { - "name": "man singer", - "slug": "man_singer", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "👩‍🎤": { - "name": "woman singer", - "slug": "woman_singer", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🧑‍🎨": { - "name": "artist", - "slug": "artist", - "group": "People & Body", - "emoji_version": "12.1", - "unicode_version": "12.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.1" - }, - "👨‍🎨": { - "name": "man artist", - "slug": "man_artist", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "👩‍🎨": { - "name": "woman artist", - "slug": "woman_artist", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🧑‍✈️": { - "name": "pilot", - "slug": "pilot", - "group": "People & Body", - "emoji_version": "12.1", - "unicode_version": "12.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.1" - }, - "👨‍✈️": { - "name": "man pilot", - "slug": "man_pilot", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "👩‍✈️": { - "name": "woman pilot", - "slug": "woman_pilot", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🧑‍🚀": { - "name": "astronaut", - "slug": "astronaut", - "group": "People & Body", - "emoji_version": "12.1", - "unicode_version": "12.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.1" - }, - "👨‍🚀": { - "name": "man astronaut", - "slug": "man_astronaut", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "👩‍🚀": { - "name": "woman astronaut", - "slug": "woman_astronaut", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🧑‍🚒": { - "name": "firefighter", - "slug": "firefighter", - "group": "People & Body", - "emoji_version": "12.1", - "unicode_version": "12.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.1" - }, - "👨‍🚒": { - "name": "man firefighter", - "slug": "man_firefighter", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "👩‍🚒": { - "name": "woman firefighter", - "slug": "woman_firefighter", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "👮": { - "name": "police officer", - "slug": "police_officer", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "👮‍♂️": { - "name": "man police officer", - "slug": "man_police_officer", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "👮‍♀️": { - "name": "woman police officer", - "slug": "woman_police_officer", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🕵️": { - "name": "detective", - "slug": "detective", - "group": "People & Body", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "2.0" - }, - "🕵️‍♂️": { - "name": "man detective", - "slug": "man_detective", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🕵️‍♀️": { - "name": "woman detective", - "slug": "woman_detective", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "💂": { - "name": "guard", - "slug": "guard", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "💂‍♂️": { - "name": "man guard", - "slug": "man_guard", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "💂‍♀️": { - "name": "woman guard", - "slug": "woman_guard", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🥷": { - "name": "ninja", - "slug": "ninja", - "group": "People & Body", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "13.0" - }, - "👷": { - "name": "construction worker", - "slug": "construction_worker", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "👷‍♂️": { - "name": "man construction worker", - "slug": "man_construction_worker", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "👷‍♀️": { - "name": "woman construction worker", - "slug": "woman_construction_worker", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🫅": { - "name": "person with crown", - "slug": "person_with_crown", - "group": "People & Body", - "emoji_version": "14.0", - "unicode_version": "14.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "14.0" - }, - "🤴": { - "name": "prince", - "slug": "prince", - "group": "People & Body", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "3.0" - }, - "👸": { - "name": "princess", - "slug": "princess", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "👳": { - "name": "person wearing turban", - "slug": "person_wearing_turban", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "👳‍♂️": { - "name": "man wearing turban", - "slug": "man_wearing_turban", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "👳‍♀️": { - "name": "woman wearing turban", - "slug": "woman_wearing_turban", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "👲": { - "name": "person with skullcap", - "slug": "person_with_skullcap", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🧕": { - "name": "woman with headscarf", - "slug": "woman_with_headscarf", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "5.0" - }, - "🤵": { - "name": "person in tuxedo", - "slug": "person_in_tuxedo", - "group": "People & Body", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "3.0" - }, - "🤵‍♂️": { - "name": "man in tuxedo", - "slug": "man_in_tuxedo", - "group": "People & Body", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "13.0" - }, - "🤵‍♀️": { - "name": "woman in tuxedo", - "slug": "woman_in_tuxedo", - "group": "People & Body", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "13.0" - }, - "👰": { - "name": "person with veil", - "slug": "person_with_veil", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "👰‍♂️": { - "name": "man with veil", - "slug": "man_with_veil", - "group": "People & Body", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "13.0" - }, - "👰‍♀️": { - "name": "woman with veil", - "slug": "woman_with_veil", - "group": "People & Body", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "13.0" - }, - "🤰": { - "name": "pregnant woman", - "slug": "pregnant_woman", - "group": "People & Body", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "3.0" - }, - "🫃": { - "name": "pregnant man", - "slug": "pregnant_man", - "group": "People & Body", - "emoji_version": "14.0", - "unicode_version": "14.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "14.0" - }, - "🫄": { - "name": "pregnant person", - "slug": "pregnant_person", - "group": "People & Body", - "emoji_version": "14.0", - "unicode_version": "14.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "14.0" - }, - "🤱": { - "name": "breast-feeding", - "slug": "breast_feeding", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "5.0" - }, - "👩‍🍼": { - "name": "woman feeding baby", - "slug": "woman_feeding_baby", - "group": "People & Body", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "13.0" - }, - "👨‍🍼": { - "name": "man feeding baby", - "slug": "man_feeding_baby", - "group": "People & Body", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "13.0" - }, - "🧑‍🍼": { - "name": "person feeding baby", - "slug": "person_feeding_baby", - "group": "People & Body", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "13.0" - }, - "👼": { - "name": "baby angel", - "slug": "baby_angel", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🎅": { - "name": "Santa Claus", - "slug": "santa_claus", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🤶": { - "name": "Mrs. Claus", - "slug": "mrs_claus", - "group": "People & Body", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "3.0" - }, - "🧑‍🎄": { - "name": "mx claus", - "slug": "mx_claus", - "group": "People & Body", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "13.0" - }, - "🦸": { - "name": "superhero", - "slug": "superhero", - "group": "People & Body", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "11.0" - }, - "🦸‍♂️": { - "name": "man superhero", - "slug": "man_superhero", - "group": "People & Body", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "11.0" - }, - "🦸‍♀️": { - "name": "woman superhero", - "slug": "woman_superhero", - "group": "People & Body", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "11.0" - }, - "🦹": { - "name": "supervillain", - "slug": "supervillain", - "group": "People & Body", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "11.0" - }, - "🦹‍♂️": { - "name": "man supervillain", - "slug": "man_supervillain", - "group": "People & Body", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "11.0" - }, - "🦹‍♀️": { - "name": "woman supervillain", - "slug": "woman_supervillain", - "group": "People & Body", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "11.0" - }, - "🧙": { - "name": "mage", - "slug": "mage", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "5.0" - }, - "🧙‍♂️": { - "name": "man mage", - "slug": "man_mage", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "5.0" - }, - "🧙‍♀️": { - "name": "woman mage", - "slug": "woman_mage", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "5.0" - }, - "🧚": { - "name": "fairy", - "slug": "fairy", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "5.0" - }, - "🧚‍♂️": { - "name": "man fairy", - "slug": "man_fairy", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "5.0" - }, - "🧚‍♀️": { - "name": "woman fairy", - "slug": "woman_fairy", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "5.0" - }, - "🧛": { - "name": "vampire", - "slug": "vampire", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "5.0" - }, - "🧛‍♂️": { - "name": "man vampire", - "slug": "man_vampire", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "5.0" - }, - "🧛‍♀️": { - "name": "woman vampire", - "slug": "woman_vampire", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "5.0" - }, - "🧜": { - "name": "merperson", - "slug": "merperson", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "5.0" - }, - "🧜‍♂️": { - "name": "merman", - "slug": "merman", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "5.0" - }, - "🧜‍♀️": { - "name": "mermaid", - "slug": "mermaid", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "5.0" - }, - "🧝": { - "name": "elf", - "slug": "elf", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "5.0" - }, - "🧝‍♂️": { - "name": "man elf", - "slug": "man_elf", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "5.0" - }, - "🧝‍♀️": { - "name": "woman elf", - "slug": "woman_elf", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "5.0" - }, - "🧞": { - "name": "genie", - "slug": "genie", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🧞‍♂️": { - "name": "man genie", - "slug": "man_genie", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🧞‍♀️": { - "name": "woman genie", - "slug": "woman_genie", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🧟": { - "name": "zombie", - "slug": "zombie", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🧟‍♂️": { - "name": "man zombie", - "slug": "man_zombie", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🧟‍♀️": { - "name": "woman zombie", - "slug": "woman_zombie", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🧌": { - "name": "troll", - "slug": "troll", - "group": "People & Body", - "emoji_version": "14.0", - "unicode_version": "14.0", - "skin_tone_support": false - }, - "💆": { - "name": "person getting massage", - "slug": "person_getting_massage", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "💆‍♂️": { - "name": "man getting massage", - "slug": "man_getting_massage", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "💆‍♀️": { - "name": "woman getting massage", - "slug": "woman_getting_massage", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "💇": { - "name": "person getting haircut", - "slug": "person_getting_haircut", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "💇‍♂️": { - "name": "man getting haircut", - "slug": "man_getting_haircut", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "💇‍♀️": { - "name": "woman getting haircut", - "slug": "woman_getting_haircut", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🚶": { - "name": "person walking", - "slug": "person_walking", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🚶‍♂️": { - "name": "man walking", - "slug": "man_walking", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🚶‍♀️": { - "name": "woman walking", - "slug": "woman_walking", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🚶‍➡️": { - "name": "person walking facing right", - "slug": "person_walking_facing_right", - "group": "People & Body", - "emoji_version": "15.1", - "unicode_version": "15.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "15.1" - }, - "🚶‍♀️‍➡️": { - "name": "woman walking facing right", - "slug": "woman_walking_facing_right", - "group": "People & Body", - "emoji_version": "15.1", - "unicode_version": "15.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "15.1" - }, - "🚶‍♂️‍➡️": { - "name": "man walking facing right", - "slug": "man_walking_facing_right", - "group": "People & Body", - "emoji_version": "15.1", - "unicode_version": "15.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "15.1" - }, - "🧍": { - "name": "person standing", - "slug": "person_standing", - "group": "People & Body", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.0" - }, - "🧍‍♂️": { - "name": "man standing", - "slug": "man_standing", - "group": "People & Body", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.0" - }, - "🧍‍♀️": { - "name": "woman standing", - "slug": "woman_standing", - "group": "People & Body", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.0" - }, - "🧎": { - "name": "person kneeling", - "slug": "person_kneeling", - "group": "People & Body", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.0" - }, - "🧎‍♂️": { - "name": "man kneeling", - "slug": "man_kneeling", - "group": "People & Body", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.0" - }, - "🧎‍♀️": { - "name": "woman kneeling", - "slug": "woman_kneeling", - "group": "People & Body", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.0" - }, - "🧎‍➡️": { - "name": "person kneeling facing right", - "slug": "person_kneeling_facing_right", - "group": "People & Body", - "emoji_version": "15.1", - "unicode_version": "15.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "15.1" - }, - "🧎‍♀️‍➡️": { - "name": "woman kneeling facing right", - "slug": "woman_kneeling_facing_right", - "group": "People & Body", - "emoji_version": "15.1", - "unicode_version": "15.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "15.1" - }, - "🧎‍♂️‍➡️": { - "name": "man kneeling facing right", - "slug": "man_kneeling_facing_right", - "group": "People & Body", - "emoji_version": "15.1", - "unicode_version": "15.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "15.1" - }, - "🧑‍🦯": { - "name": "person with white cane", - "slug": "person_with_white_cane", - "group": "People & Body", - "emoji_version": "12.1", - "unicode_version": "12.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.1" - }, - "🧑‍🦯‍➡️": { - "name": "person with white cane facing right", - "slug": "person_with_white_cane_facing_right", - "group": "People & Body", - "emoji_version": "15.1", - "unicode_version": "15.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "15.1" - }, - "👨‍🦯": { - "name": "man with white cane", - "slug": "man_with_white_cane", - "group": "People & Body", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.0" - }, - "👨‍🦯‍➡️": { - "name": "man with white cane facing right", - "slug": "man_with_white_cane_facing_right", - "group": "People & Body", - "emoji_version": "15.1", - "unicode_version": "15.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "15.1" - }, - "👩‍🦯": { - "name": "woman with white cane", - "slug": "woman_with_white_cane", - "group": "People & Body", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.0" - }, - "👩‍🦯‍➡️": { - "name": "woman with white cane facing right", - "slug": "woman_with_white_cane_facing_right", - "group": "People & Body", - "emoji_version": "15.1", - "unicode_version": "15.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "15.1" - }, - "🧑‍🦼": { - "name": "person in motorized wheelchair", - "slug": "person_in_motorized_wheelchair", - "group": "People & Body", - "emoji_version": "12.1", - "unicode_version": "12.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.1" - }, - "🧑‍🦼‍➡️": { - "name": "person in motorized wheelchair facing right", - "slug": "person_in_motorized_wheelchair_facing_right", - "group": "People & Body", - "emoji_version": "15.1", - "unicode_version": "15.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "15.1" - }, - "👨‍🦼": { - "name": "man in motorized wheelchair", - "slug": "man_in_motorized_wheelchair", - "group": "People & Body", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.0" - }, - "👨‍🦼‍➡️": { - "name": "man in motorized wheelchair facing right", - "slug": "man_in_motorized_wheelchair_facing_right", - "group": "People & Body", - "emoji_version": "15.1", - "unicode_version": "15.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "15.1" - }, - "👩‍🦼": { - "name": "woman in motorized wheelchair", - "slug": "woman_in_motorized_wheelchair", - "group": "People & Body", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.0" - }, - "👩‍🦼‍➡️": { - "name": "woman in motorized wheelchair facing right", - "slug": "woman_in_motorized_wheelchair_facing_right", - "group": "People & Body", - "emoji_version": "15.1", - "unicode_version": "15.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "15.1" - }, - "🧑‍🦽": { - "name": "person in manual wheelchair", - "slug": "person_in_manual_wheelchair", - "group": "People & Body", - "emoji_version": "12.1", - "unicode_version": "12.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.1" - }, - "🧑‍🦽‍➡️": { - "name": "person in manual wheelchair facing right", - "slug": "person_in_manual_wheelchair_facing_right", - "group": "People & Body", - "emoji_version": "15.1", - "unicode_version": "15.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "15.1" - }, - "👨‍🦽": { - "name": "man in manual wheelchair", - "slug": "man_in_manual_wheelchair", - "group": "People & Body", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.0" - }, - "👨‍🦽‍➡️": { - "name": "man in manual wheelchair facing right", - "slug": "man_in_manual_wheelchair_facing_right", - "group": "People & Body", - "emoji_version": "15.1", - "unicode_version": "15.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "15.1" - }, - "👩‍🦽": { - "name": "woman in manual wheelchair", - "slug": "woman_in_manual_wheelchair", - "group": "People & Body", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.0" - }, - "👩‍🦽‍➡️": { - "name": "woman in manual wheelchair facing right", - "slug": "woman_in_manual_wheelchair_facing_right", - "group": "People & Body", - "emoji_version": "15.1", - "unicode_version": "15.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "15.1" - }, - "🏃": { - "name": "person running", - "slug": "person_running", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🏃‍♂️": { - "name": "man running", - "slug": "man_running", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🏃‍♀️": { - "name": "woman running", - "slug": "woman_running", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🏃‍➡️": { - "name": "person running facing right", - "slug": "person_running_facing_right", - "group": "People & Body", - "emoji_version": "15.1", - "unicode_version": "15.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "15.1" - }, - "🏃‍♀️‍➡️": { - "name": "woman running facing right", - "slug": "woman_running_facing_right", - "group": "People & Body", - "emoji_version": "15.1", - "unicode_version": "15.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "15.1" - }, - "🏃‍♂️‍➡️": { - "name": "man running facing right", - "slug": "man_running_facing_right", - "group": "People & Body", - "emoji_version": "15.1", - "unicode_version": "15.1", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "15.1" - }, - "💃": { - "name": "woman dancing", - "slug": "woman_dancing", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🕺": { - "name": "man dancing", - "slug": "man_dancing", - "group": "People & Body", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "3.0" - }, - "🕴️": { - "name": "person in suit levitating", - "slug": "person_in_suit_levitating", - "group": "People & Body", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "👯": { - "name": "people with bunny ears", - "slug": "people_with_bunny_ears", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "👯‍♂️": { - "name": "men with bunny ears", - "slug": "men_with_bunny_ears", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": false - }, - "👯‍♀️": { - "name": "women with bunny ears", - "slug": "women_with_bunny_ears", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": false - }, - "🧖": { - "name": "person in steamy room", - "slug": "person_in_steamy_room", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "5.0" - }, - "🧖‍♂️": { - "name": "man in steamy room", - "slug": "man_in_steamy_room", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "5.0" - }, - "🧖‍♀️": { - "name": "woman in steamy room", - "slug": "woman_in_steamy_room", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "5.0" - }, - "🧗": { - "name": "person climbing", - "slug": "person_climbing", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "5.0" - }, - "🧗‍♂️": { - "name": "man climbing", - "slug": "man_climbing", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "5.0" - }, - "🧗‍♀️": { - "name": "woman climbing", - "slug": "woman_climbing", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "5.0" - }, - "🤺": { - "name": "person fencing", - "slug": "person_fencing", - "group": "People & Body", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🏇": { - "name": "horse racing", - "slug": "horse_racing", - "group": "People & Body", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "⛷️": { - "name": "skier", - "slug": "skier", - "group": "People & Body", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🏂": { - "name": "snowboarder", - "slug": "snowboarder", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🏌️": { - "name": "person golfing", - "slug": "person_golfing", - "group": "People & Body", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🏌️‍♂️": { - "name": "man golfing", - "slug": "man_golfing", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🏌️‍♀️": { - "name": "woman golfing", - "slug": "woman_golfing", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🏄": { - "name": "person surfing", - "slug": "person_surfing", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🏄‍♂️": { - "name": "man surfing", - "slug": "man_surfing", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🏄‍♀️": { - "name": "woman surfing", - "slug": "woman_surfing", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🚣": { - "name": "person rowing boat", - "slug": "person_rowing_boat", - "group": "People & Body", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🚣‍♂️": { - "name": "man rowing boat", - "slug": "man_rowing_boat", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🚣‍♀️": { - "name": "woman rowing boat", - "slug": "woman_rowing_boat", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🏊": { - "name": "person swimming", - "slug": "person_swimming", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🏊‍♂️": { - "name": "man swimming", - "slug": "man_swimming", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🏊‍♀️": { - "name": "woman swimming", - "slug": "woman_swimming", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "⛹️": { - "name": "person bouncing ball", - "slug": "person_bouncing_ball", - "group": "People & Body", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "2.0" - }, - "⛹️‍♂️": { - "name": "man bouncing ball", - "slug": "man_bouncing_ball", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "⛹️‍♀️": { - "name": "woman bouncing ball", - "slug": "woman_bouncing_ball", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🏋️": { - "name": "person lifting weights", - "slug": "person_lifting_weights", - "group": "People & Body", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "2.0" - }, - "🏋️‍♂️": { - "name": "man lifting weights", - "slug": "man_lifting_weights", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🏋️‍♀️": { - "name": "woman lifting weights", - "slug": "woman_lifting_weights", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🚴": { - "name": "person biking", - "slug": "person_biking", - "group": "People & Body", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🚴‍♂️": { - "name": "man biking", - "slug": "man_biking", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🚴‍♀️": { - "name": "woman biking", - "slug": "woman_biking", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🚵": { - "name": "person mountain biking", - "slug": "person_mountain_biking", - "group": "People & Body", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🚵‍♂️": { - "name": "man mountain biking", - "slug": "man_mountain_biking", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🚵‍♀️": { - "name": "woman mountain biking", - "slug": "woman_mountain_biking", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🤸": { - "name": "person cartwheeling", - "slug": "person_cartwheeling", - "group": "People & Body", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "3.0" - }, - "🤸‍♂️": { - "name": "man cartwheeling", - "slug": "man_cartwheeling", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🤸‍♀️": { - "name": "woman cartwheeling", - "slug": "woman_cartwheeling", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🤼": { - "name": "people wrestling", - "slug": "people_wrestling", - "group": "People & Body", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🤼‍♂️": { - "name": "men wrestling", - "slug": "men_wrestling", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": false - }, - "🤼‍♀️": { - "name": "women wrestling", - "slug": "women_wrestling", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": false - }, - "🤽": { - "name": "person playing water polo", - "slug": "person_playing_water_polo", - "group": "People & Body", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "3.0" - }, - "🤽‍♂️": { - "name": "man playing water polo", - "slug": "man_playing_water_polo", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🤽‍♀️": { - "name": "woman playing water polo", - "slug": "woman_playing_water_polo", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🤾": { - "name": "person playing handball", - "slug": "person_playing_handball", - "group": "People & Body", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "3.0" - }, - "🤾‍♂️": { - "name": "man playing handball", - "slug": "man_playing_handball", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🤾‍♀️": { - "name": "woman playing handball", - "slug": "woman_playing_handball", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🤹": { - "name": "person juggling", - "slug": "person_juggling", - "group": "People & Body", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "3.0" - }, - "🤹‍♂️": { - "name": "man juggling", - "slug": "man_juggling", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🤹‍♀️": { - "name": "woman juggling", - "slug": "woman_juggling", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🧘": { - "name": "person in lotus position", - "slug": "person_in_lotus_position", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "5.0" - }, - "🧘‍♂️": { - "name": "man in lotus position", - "slug": "man_in_lotus_position", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "5.0" - }, - "🧘‍♀️": { - "name": "woman in lotus position", - "slug": "woman_in_lotus_position", - "group": "People & Body", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "5.0" - }, - "🛀": { - "name": "person taking bath", - "slug": "person_taking_bath", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "1.0" - }, - "🛌": { - "name": "person in bed", - "slug": "person_in_bed", - "group": "People & Body", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "4.0" - }, - "🧑‍🤝‍🧑": { - "name": "people holding hands", - "slug": "people_holding_hands", - "group": "People & Body", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.0" - }, - "👭": { - "name": "women holding hands", - "slug": "women_holding_hands", - "group": "People & Body", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.0" - }, - "👫": { - "name": "woman and man holding hands", - "slug": "woman_and_man_holding_hands", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.0" - }, - "👬": { - "name": "men holding hands", - "slug": "men_holding_hands", - "group": "People & Body", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "12.0" - }, - "💏": { - "name": "kiss", - "slug": "kiss", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "13.1" - }, - "👩‍❤️‍💋‍👨": { - "name": "kiss woman, man", - "slug": "kiss_woman_man", - "group": "People & Body", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "13.1" - }, - "👨‍❤️‍💋‍👨": { - "name": "kiss man, man", - "slug": "kiss_man_man", - "group": "People & Body", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "13.1" - }, - "👩‍❤️‍💋‍👩": { - "name": "kiss woman, woman", - "slug": "kiss_woman_woman", - "group": "People & Body", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "13.1" - }, - "💑": { - "name": "couple with heart", - "slug": "couple_with_heart", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "13.1" - }, - "👩‍❤️‍👨": { - "name": "couple with heart woman, man", - "slug": "couple_with_heart_woman_man", - "group": "People & Body", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "13.1" - }, - "👨‍❤️‍👨": { - "name": "couple with heart man, man", - "slug": "couple_with_heart_man_man", - "group": "People & Body", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "13.1" - }, - "👩‍❤️‍👩": { - "name": "couple with heart woman, woman", - "slug": "couple_with_heart_woman_woman", - "group": "People & Body", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": true, - "skin_tone_support_unicode_version": "13.1" - }, - "👨‍👩‍👦": { - "name": "family man, woman, boy", - "slug": "family_man_woman_boy", - "group": "People & Body", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "👨‍👩‍👧": { - "name": "family man, woman, girl", - "slug": "family_man_woman_girl", - "group": "People & Body", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "👨‍👩‍👧‍👦": { - "name": "family man, woman, girl, boy", - "slug": "family_man_woman_girl_boy", - "group": "People & Body", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "👨‍👩‍👦‍👦": { - "name": "family man, woman, boy, boy", - "slug": "family_man_woman_boy_boy", - "group": "People & Body", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "👨‍👩‍👧‍👧": { - "name": "family man, woman, girl, girl", - "slug": "family_man_woman_girl_girl", - "group": "People & Body", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "👨‍👨‍👦": { - "name": "family man, man, boy", - "slug": "family_man_man_boy", - "group": "People & Body", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "👨‍👨‍👧": { - "name": "family man, man, girl", - "slug": "family_man_man_girl", - "group": "People & Body", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "👨‍👨‍👧‍👦": { - "name": "family man, man, girl, boy", - "slug": "family_man_man_girl_boy", - "group": "People & Body", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "👨‍👨‍👦‍👦": { - "name": "family man, man, boy, boy", - "slug": "family_man_man_boy_boy", - "group": "People & Body", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "👨‍👨‍👧‍👧": { - "name": "family man, man, girl, girl", - "slug": "family_man_man_girl_girl", - "group": "People & Body", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "👩‍👩‍👦": { - "name": "family woman, woman, boy", - "slug": "family_woman_woman_boy", - "group": "People & Body", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "👩‍👩‍👧": { - "name": "family woman, woman, girl", - "slug": "family_woman_woman_girl", - "group": "People & Body", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "👩‍👩‍👧‍👦": { - "name": "family woman, woman, girl, boy", - "slug": "family_woman_woman_girl_boy", - "group": "People & Body", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "👩‍👩‍👦‍👦": { - "name": "family woman, woman, boy, boy", - "slug": "family_woman_woman_boy_boy", - "group": "People & Body", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "👩‍👩‍👧‍👧": { - "name": "family woman, woman, girl, girl", - "slug": "family_woman_woman_girl_girl", - "group": "People & Body", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "👨‍👦": { - "name": "family man, boy", - "slug": "family_man_boy", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": false - }, - "👨‍👦‍👦": { - "name": "family man, boy, boy", - "slug": "family_man_boy_boy", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": false - }, - "👨‍👧": { - "name": "family man, girl", - "slug": "family_man_girl", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": false - }, - "👨‍👧‍👦": { - "name": "family man, girl, boy", - "slug": "family_man_girl_boy", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": false - }, - "👨‍👧‍👧": { - "name": "family man, girl, girl", - "slug": "family_man_girl_girl", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": false - }, - "👩‍👦": { - "name": "family woman, boy", - "slug": "family_woman_boy", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": false - }, - "👩‍👦‍👦": { - "name": "family woman, boy, boy", - "slug": "family_woman_boy_boy", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": false - }, - "👩‍👧": { - "name": "family woman, girl", - "slug": "family_woman_girl", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": false - }, - "👩‍👧‍👦": { - "name": "family woman, girl, boy", - "slug": "family_woman_girl_boy", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": false - }, - "👩‍👧‍👧": { - "name": "family woman, girl, girl", - "slug": "family_woman_girl_girl", - "group": "People & Body", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": false - }, - "🗣️": { - "name": "speaking head", - "slug": "speaking_head", - "group": "People & Body", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "👤": { - "name": "bust in silhouette", - "slug": "bust_in_silhouette", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "👥": { - "name": "busts in silhouette", - "slug": "busts_in_silhouette", - "group": "People & Body", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🫂": { - "name": "people hugging", - "slug": "people_hugging", - "group": "People & Body", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "👪": { - "name": "family", - "slug": "family", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🧑‍🧑‍🧒": { - "name": "family adult, adult, child", - "slug": "family_adult_adult_child", - "group": "People & Body", - "emoji_version": "15.1", - "unicode_version": "15.1", - "skin_tone_support": false - }, - "🧑‍🧑‍🧒‍🧒": { - "name": "family adult, adult, child, child", - "slug": "family_adult_adult_child_child", - "group": "People & Body", - "emoji_version": "15.1", - "unicode_version": "15.1", - "skin_tone_support": false - }, - "🧑‍🧒": { - "name": "family adult, child", - "slug": "family_adult_child", - "group": "People & Body", - "emoji_version": "15.1", - "unicode_version": "15.1", - "skin_tone_support": false - }, - "🧑‍🧒‍🧒": { - "name": "family adult, child, child", - "slug": "family_adult_child_child", - "group": "People & Body", - "emoji_version": "15.1", - "unicode_version": "15.1", - "skin_tone_support": false - }, - "👣": { - "name": "footprints", - "slug": "footprints", - "group": "People & Body", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🐵": { - "name": "monkey face", - "slug": "monkey_face", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🐒": { - "name": "monkey", - "slug": "monkey", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🦍": { - "name": "gorilla", - "slug": "gorilla", - "group": "Animals & Nature", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🦧": { - "name": "orangutan", - "slug": "orangutan", - "group": "Animals & Nature", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🐶": { - "name": "dog face", - "slug": "dog_face", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🐕": { - "name": "dog", - "slug": "dog", - "group": "Animals & Nature", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🦮": { - "name": "guide dog", - "slug": "guide_dog", - "group": "Animals & Nature", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🐕‍🦺": { - "name": "service dog", - "slug": "service_dog", - "group": "Animals & Nature", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🐩": { - "name": "poodle", - "slug": "poodle", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🐺": { - "name": "wolf", - "slug": "wolf", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🦊": { - "name": "fox", - "slug": "fox", - "group": "Animals & Nature", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🦝": { - "name": "raccoon", - "slug": "raccoon", - "group": "Animals & Nature", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🐱": { - "name": "cat face", - "slug": "cat_face", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🐈": { - "name": "cat", - "slug": "cat", - "group": "Animals & Nature", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🐈‍⬛": { - "name": "black cat", - "slug": "black_cat", - "group": "Animals & Nature", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🦁": { - "name": "lion", - "slug": "lion", - "group": "Animals & Nature", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🐯": { - "name": "tiger face", - "slug": "tiger_face", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🐅": { - "name": "tiger", - "slug": "tiger", - "group": "Animals & Nature", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🐆": { - "name": "leopard", - "slug": "leopard", - "group": "Animals & Nature", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🐴": { - "name": "horse face", - "slug": "horse_face", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🫎": { - "name": "moose", - "slug": "moose", - "group": "Animals & Nature", - "emoji_version": "15.0", - "unicode_version": "15.0", - "skin_tone_support": false - }, - "🫏": { - "name": "donkey", - "slug": "donkey", - "group": "Animals & Nature", - "emoji_version": "15.0", - "unicode_version": "15.0", - "skin_tone_support": false - }, - "🐎": { - "name": "horse", - "slug": "horse", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🦄": { - "name": "unicorn", - "slug": "unicorn", - "group": "Animals & Nature", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🦓": { - "name": "zebra", - "slug": "zebra", - "group": "Animals & Nature", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🦌": { - "name": "deer", - "slug": "deer", - "group": "Animals & Nature", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🦬": { - "name": "bison", - "slug": "bison", - "group": "Animals & Nature", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🐮": { - "name": "cow face", - "slug": "cow_face", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🐂": { - "name": "ox", - "slug": "ox", - "group": "Animals & Nature", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🐃": { - "name": "water buffalo", - "slug": "water_buffalo", - "group": "Animals & Nature", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🐄": { - "name": "cow", - "slug": "cow", - "group": "Animals & Nature", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🐷": { - "name": "pig face", - "slug": "pig_face", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🐖": { - "name": "pig", - "slug": "pig", - "group": "Animals & Nature", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🐗": { - "name": "boar", - "slug": "boar", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🐽": { - "name": "pig nose", - "slug": "pig_nose", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🐏": { - "name": "ram", - "slug": "ram", - "group": "Animals & Nature", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🐑": { - "name": "ewe", - "slug": "ewe", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🐐": { - "name": "goat", - "slug": "goat", - "group": "Animals & Nature", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🐪": { - "name": "camel", - "slug": "camel", - "group": "Animals & Nature", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🐫": { - "name": "two-hump camel", - "slug": "two_hump_camel", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🦙": { - "name": "llama", - "slug": "llama", - "group": "Animals & Nature", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🦒": { - "name": "giraffe", - "slug": "giraffe", - "group": "Animals & Nature", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🐘": { - "name": "elephant", - "slug": "elephant", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🦣": { - "name": "mammoth", - "slug": "mammoth", - "group": "Animals & Nature", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🦏": { - "name": "rhinoceros", - "slug": "rhinoceros", - "group": "Animals & Nature", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🦛": { - "name": "hippopotamus", - "slug": "hippopotamus", - "group": "Animals & Nature", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🐭": { - "name": "mouse face", - "slug": "mouse_face", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🐁": { - "name": "mouse", - "slug": "mouse", - "group": "Animals & Nature", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🐀": { - "name": "rat", - "slug": "rat", - "group": "Animals & Nature", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🐹": { - "name": "hamster", - "slug": "hamster", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🐰": { - "name": "rabbit face", - "slug": "rabbit_face", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🐇": { - "name": "rabbit", - "slug": "rabbit", - "group": "Animals & Nature", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🐿️": { - "name": "chipmunk", - "slug": "chipmunk", - "group": "Animals & Nature", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🦫": { - "name": "beaver", - "slug": "beaver", - "group": "Animals & Nature", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🦔": { - "name": "hedgehog", - "slug": "hedgehog", - "group": "Animals & Nature", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🦇": { - "name": "bat", - "slug": "bat", - "group": "Animals & Nature", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🐻": { - "name": "bear", - "slug": "bear", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🐻‍❄️": { - "name": "polar bear", - "slug": "polar_bear", - "group": "Animals & Nature", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🐨": { - "name": "koala", - "slug": "koala", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🐼": { - "name": "panda", - "slug": "panda", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🦥": { - "name": "sloth", - "slug": "sloth", - "group": "Animals & Nature", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🦦": { - "name": "otter", - "slug": "otter", - "group": "Animals & Nature", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🦨": { - "name": "skunk", - "slug": "skunk", - "group": "Animals & Nature", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🦘": { - "name": "kangaroo", - "slug": "kangaroo", - "group": "Animals & Nature", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🦡": { - "name": "badger", - "slug": "badger", - "group": "Animals & Nature", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🐾": { - "name": "paw prints", - "slug": "paw_prints", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🦃": { - "name": "turkey", - "slug": "turkey", - "group": "Animals & Nature", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🐔": { - "name": "chicken", - "slug": "chicken", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🐓": { - "name": "rooster", - "slug": "rooster", - "group": "Animals & Nature", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🐣": { - "name": "hatching chick", - "slug": "hatching_chick", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🐤": { - "name": "baby chick", - "slug": "baby_chick", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🐥": { - "name": "front-facing baby chick", - "slug": "front_facing_baby_chick", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🐦": { - "name": "bird", - "slug": "bird", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🐧": { - "name": "penguin", - "slug": "penguin", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🕊️": { - "name": "dove", - "slug": "dove", - "group": "Animals & Nature", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🦅": { - "name": "eagle", - "slug": "eagle", - "group": "Animals & Nature", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🦆": { - "name": "duck", - "slug": "duck", - "group": "Animals & Nature", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🦢": { - "name": "swan", - "slug": "swan", - "group": "Animals & Nature", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🦉": { - "name": "owl", - "slug": "owl", - "group": "Animals & Nature", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🦤": { - "name": "dodo", - "slug": "dodo", - "group": "Animals & Nature", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🪶": { - "name": "feather", - "slug": "feather", - "group": "Animals & Nature", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🦩": { - "name": "flamingo", - "slug": "flamingo", - "group": "Animals & Nature", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🦚": { - "name": "peacock", - "slug": "peacock", - "group": "Animals & Nature", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🦜": { - "name": "parrot", - "slug": "parrot", - "group": "Animals & Nature", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🪽": { - "name": "wing", - "slug": "wing", - "group": "Animals & Nature", - "emoji_version": "15.0", - "unicode_version": "15.0", - "skin_tone_support": false - }, - "🐦‍⬛": { - "name": "black bird", - "slug": "black_bird", - "group": "Animals & Nature", - "emoji_version": "15.0", - "unicode_version": "15.0", - "skin_tone_support": false - }, - "🪿": { - "name": "goose", - "slug": "goose", - "group": "Animals & Nature", - "emoji_version": "15.0", - "unicode_version": "15.0", - "skin_tone_support": false - }, - "🐦‍🔥": { - "name": "phoenix", - "slug": "phoenix", - "group": "Animals & Nature", - "emoji_version": "15.1", - "unicode_version": "15.1", - "skin_tone_support": false - }, - "🐸": { - "name": "frog", - "slug": "frog", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🐊": { - "name": "crocodile", - "slug": "crocodile", - "group": "Animals & Nature", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🐢": { - "name": "turtle", - "slug": "turtle", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🦎": { - "name": "lizard", - "slug": "lizard", - "group": "Animals & Nature", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🐍": { - "name": "snake", - "slug": "snake", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🐲": { - "name": "dragon face", - "slug": "dragon_face", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🐉": { - "name": "dragon", - "slug": "dragon", - "group": "Animals & Nature", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🦕": { - "name": "sauropod", - "slug": "sauropod", - "group": "Animals & Nature", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🦖": { - "name": "T-Rex", - "slug": "t_rex", - "group": "Animals & Nature", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🐳": { - "name": "spouting whale", - "slug": "spouting_whale", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🐋": { - "name": "whale", - "slug": "whale", - "group": "Animals & Nature", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🐬": { - "name": "dolphin", - "slug": "dolphin", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🦭": { - "name": "seal", - "slug": "seal", - "group": "Animals & Nature", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🐟": { - "name": "fish", - "slug": "fish", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🐠": { - "name": "tropical fish", - "slug": "tropical_fish", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🐡": { - "name": "blowfish", - "slug": "blowfish", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🦈": { - "name": "shark", - "slug": "shark", - "group": "Animals & Nature", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🐙": { - "name": "octopus", - "slug": "octopus", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🐚": { - "name": "spiral shell", - "slug": "spiral_shell", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🪸": { - "name": "coral", - "slug": "coral", - "group": "Animals & Nature", - "emoji_version": "14.0", - "unicode_version": "14.0", - "skin_tone_support": false - }, - "🪼": { - "name": "jellyfish", - "slug": "jellyfish", - "group": "Animals & Nature", - "emoji_version": "15.0", - "unicode_version": "15.0", - "skin_tone_support": false - }, - "🐌": { - "name": "snail", - "slug": "snail", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🦋": { - "name": "butterfly", - "slug": "butterfly", - "group": "Animals & Nature", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🐛": { - "name": "bug", - "slug": "bug", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🐜": { - "name": "ant", - "slug": "ant", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🐝": { - "name": "honeybee", - "slug": "honeybee", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🪲": { - "name": "beetle", - "slug": "beetle", - "group": "Animals & Nature", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🐞": { - "name": "lady beetle", - "slug": "lady_beetle", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🦗": { - "name": "cricket", - "slug": "cricket", - "group": "Animals & Nature", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🪳": { - "name": "cockroach", - "slug": "cockroach", - "group": "Animals & Nature", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🕷️": { - "name": "spider", - "slug": "spider", - "group": "Animals & Nature", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🕸️": { - "name": "spider web", - "slug": "spider_web", - "group": "Animals & Nature", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🦂": { - "name": "scorpion", - "slug": "scorpion", - "group": "Animals & Nature", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🦟": { - "name": "mosquito", - "slug": "mosquito", - "group": "Animals & Nature", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🪰": { - "name": "fly", - "slug": "fly", - "group": "Animals & Nature", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🪱": { - "name": "worm", - "slug": "worm", - "group": "Animals & Nature", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🦠": { - "name": "microbe", - "slug": "microbe", - "group": "Animals & Nature", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "💐": { - "name": "bouquet", - "slug": "bouquet", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🌸": { - "name": "cherry blossom", - "slug": "cherry_blossom", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "💮": { - "name": "white flower", - "slug": "white_flower", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🪷": { - "name": "lotus", - "slug": "lotus", - "group": "Animals & Nature", - "emoji_version": "14.0", - "unicode_version": "14.0", - "skin_tone_support": false - }, - "🏵️": { - "name": "rosette", - "slug": "rosette", - "group": "Animals & Nature", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🌹": { - "name": "rose", - "slug": "rose", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🥀": { - "name": "wilted flower", - "slug": "wilted_flower", - "group": "Animals & Nature", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🌺": { - "name": "hibiscus", - "slug": "hibiscus", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🌻": { - "name": "sunflower", - "slug": "sunflower", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🌼": { - "name": "blossom", - "slug": "blossom", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🌷": { - "name": "tulip", - "slug": "tulip", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🪻": { - "name": "hyacinth", - "slug": "hyacinth", - "group": "Animals & Nature", - "emoji_version": "15.0", - "unicode_version": "15.0", - "skin_tone_support": false - }, - "🌱": { - "name": "seedling", - "slug": "seedling", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🪴": { - "name": "potted plant", - "slug": "potted_plant", - "group": "Animals & Nature", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🌲": { - "name": "evergreen tree", - "slug": "evergreen_tree", - "group": "Animals & Nature", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🌳": { - "name": "deciduous tree", - "slug": "deciduous_tree", - "group": "Animals & Nature", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🌴": { - "name": "palm tree", - "slug": "palm_tree", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🌵": { - "name": "cactus", - "slug": "cactus", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🌾": { - "name": "sheaf of rice", - "slug": "sheaf_of_rice", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🌿": { - "name": "herb", - "slug": "herb", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "☘️": { - "name": "shamrock", - "slug": "shamrock", - "group": "Animals & Nature", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🍀": { - "name": "four leaf clover", - "slug": "four_leaf_clover", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍁": { - "name": "maple leaf", - "slug": "maple_leaf", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍂": { - "name": "fallen leaf", - "slug": "fallen_leaf", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍃": { - "name": "leaf fluttering in wind", - "slug": "leaf_fluttering_in_wind", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🪹": { - "name": "empty nest", - "slug": "empty_nest", - "group": "Animals & Nature", - "emoji_version": "14.0", - "unicode_version": "14.0", - "skin_tone_support": false - }, - "🪺": { - "name": "nest with eggs", - "slug": "nest_with_eggs", - "group": "Animals & Nature", - "emoji_version": "14.0", - "unicode_version": "14.0", - "skin_tone_support": false - }, - "🍄": { - "name": "mushroom", - "slug": "mushroom", - "group": "Animals & Nature", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍇": { - "name": "grapes", - "slug": "grapes", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍈": { - "name": "melon", - "slug": "melon", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍉": { - "name": "watermelon", - "slug": "watermelon", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍊": { - "name": "tangerine", - "slug": "tangerine", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍋": { - "name": "lemon", - "slug": "lemon", - "group": "Food & Drink", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🍋‍🟩": { - "name": "lime", - "slug": "lime", - "group": "Food & Drink", - "emoji_version": "15.1", - "unicode_version": "15.1", - "skin_tone_support": false - }, - "🍌": { - "name": "banana", - "slug": "banana", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍍": { - "name": "pineapple", - "slug": "pineapple", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🥭": { - "name": "mango", - "slug": "mango", - "group": "Food & Drink", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🍎": { - "name": "red apple", - "slug": "red_apple", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍏": { - "name": "green apple", - "slug": "green_apple", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍐": { - "name": "pear", - "slug": "pear", - "group": "Food & Drink", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🍑": { - "name": "peach", - "slug": "peach", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍒": { - "name": "cherries", - "slug": "cherries", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍓": { - "name": "strawberry", - "slug": "strawberry", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🫐": { - "name": "blueberries", - "slug": "blueberries", - "group": "Food & Drink", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🥝": { - "name": "kiwi fruit", - "slug": "kiwi_fruit", - "group": "Food & Drink", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🍅": { - "name": "tomato", - "slug": "tomato", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🫒": { - "name": "olive", - "slug": "olive", - "group": "Food & Drink", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🥥": { - "name": "coconut", - "slug": "coconut", - "group": "Food & Drink", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🥑": { - "name": "avocado", - "slug": "avocado", - "group": "Food & Drink", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🍆": { - "name": "eggplant", - "slug": "eggplant", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🥔": { - "name": "potato", - "slug": "potato", - "group": "Food & Drink", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🥕": { - "name": "carrot", - "slug": "carrot", - "group": "Food & Drink", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🌽": { - "name": "ear of corn", - "slug": "ear_of_corn", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🌶️": { - "name": "hot pepper", - "slug": "hot_pepper", - "group": "Food & Drink", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🫑": { - "name": "bell pepper", - "slug": "bell_pepper", - "group": "Food & Drink", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🥒": { - "name": "cucumber", - "slug": "cucumber", - "group": "Food & Drink", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🥬": { - "name": "leafy green", - "slug": "leafy_green", - "group": "Food & Drink", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🥦": { - "name": "broccoli", - "slug": "broccoli", - "group": "Food & Drink", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🧄": { - "name": "garlic", - "slug": "garlic", - "group": "Food & Drink", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🧅": { - "name": "onion", - "slug": "onion", - "group": "Food & Drink", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🥜": { - "name": "peanuts", - "slug": "peanuts", - "group": "Food & Drink", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🫘": { - "name": "beans", - "slug": "beans", - "group": "Food & Drink", - "emoji_version": "14.0", - "unicode_version": "14.0", - "skin_tone_support": false - }, - "🌰": { - "name": "chestnut", - "slug": "chestnut", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🫚": { - "name": "ginger root", - "slug": "ginger_root", - "group": "Food & Drink", - "emoji_version": "15.0", - "unicode_version": "15.0", - "skin_tone_support": false - }, - "🫛": { - "name": "pea pod", - "slug": "pea_pod", - "group": "Food & Drink", - "emoji_version": "15.0", - "unicode_version": "15.0", - "skin_tone_support": false - }, - "🍄‍🟫": { - "name": "brown mushroom", - "slug": "brown_mushroom", - "group": "Food & Drink", - "emoji_version": "15.1", - "unicode_version": "15.1", - "skin_tone_support": false - }, - "🍞": { - "name": "bread", - "slug": "bread", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🥐": { - "name": "croissant", - "slug": "croissant", - "group": "Food & Drink", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🥖": { - "name": "baguette bread", - "slug": "baguette_bread", - "group": "Food & Drink", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🫓": { - "name": "flatbread", - "slug": "flatbread", - "group": "Food & Drink", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🥨": { - "name": "pretzel", - "slug": "pretzel", - "group": "Food & Drink", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🥯": { - "name": "bagel", - "slug": "bagel", - "group": "Food & Drink", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🥞": { - "name": "pancakes", - "slug": "pancakes", - "group": "Food & Drink", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🧇": { - "name": "waffle", - "slug": "waffle", - "group": "Food & Drink", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🧀": { - "name": "cheese wedge", - "slug": "cheese_wedge", - "group": "Food & Drink", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🍖": { - "name": "meat on bone", - "slug": "meat_on_bone", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍗": { - "name": "poultry leg", - "slug": "poultry_leg", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🥩": { - "name": "cut of meat", - "slug": "cut_of_meat", - "group": "Food & Drink", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🥓": { - "name": "bacon", - "slug": "bacon", - "group": "Food & Drink", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🍔": { - "name": "hamburger", - "slug": "hamburger", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍟": { - "name": "french fries", - "slug": "french_fries", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍕": { - "name": "pizza", - "slug": "pizza", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🌭": { - "name": "hot dog", - "slug": "hot_dog", - "group": "Food & Drink", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🥪": { - "name": "sandwich", - "slug": "sandwich", - "group": "Food & Drink", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🌮": { - "name": "taco", - "slug": "taco", - "group": "Food & Drink", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🌯": { - "name": "burrito", - "slug": "burrito", - "group": "Food & Drink", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🫔": { - "name": "tamale", - "slug": "tamale", - "group": "Food & Drink", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🥙": { - "name": "stuffed flatbread", - "slug": "stuffed_flatbread", - "group": "Food & Drink", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🧆": { - "name": "falafel", - "slug": "falafel", - "group": "Food & Drink", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🥚": { - "name": "egg", - "slug": "egg", - "group": "Food & Drink", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🍳": { - "name": "cooking", - "slug": "cooking", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🥘": { - "name": "shallow pan of food", - "slug": "shallow_pan_of_food", - "group": "Food & Drink", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🍲": { - "name": "pot of food", - "slug": "pot_of_food", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🫕": { - "name": "fondue", - "slug": "fondue", - "group": "Food & Drink", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🥣": { - "name": "bowl with spoon", - "slug": "bowl_with_spoon", - "group": "Food & Drink", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🥗": { - "name": "green salad", - "slug": "green_salad", - "group": "Food & Drink", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🍿": { - "name": "popcorn", - "slug": "popcorn", - "group": "Food & Drink", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🧈": { - "name": "butter", - "slug": "butter", - "group": "Food & Drink", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🧂": { - "name": "salt", - "slug": "salt", - "group": "Food & Drink", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🥫": { - "name": "canned food", - "slug": "canned_food", - "group": "Food & Drink", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🍱": { - "name": "bento box", - "slug": "bento_box", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍘": { - "name": "rice cracker", - "slug": "rice_cracker", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍙": { - "name": "rice ball", - "slug": "rice_ball", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍚": { - "name": "cooked rice", - "slug": "cooked_rice", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍛": { - "name": "curry rice", - "slug": "curry_rice", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍜": { - "name": "steaming bowl", - "slug": "steaming_bowl", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍝": { - "name": "spaghetti", - "slug": "spaghetti", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍠": { - "name": "roasted sweet potato", - "slug": "roasted_sweet_potato", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍢": { - "name": "oden", - "slug": "oden", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍣": { - "name": "sushi", - "slug": "sushi", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍤": { - "name": "fried shrimp", - "slug": "fried_shrimp", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍥": { - "name": "fish cake with swirl", - "slug": "fish_cake_with_swirl", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🥮": { - "name": "moon cake", - "slug": "moon_cake", - "group": "Food & Drink", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🍡": { - "name": "dango", - "slug": "dango", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🥟": { - "name": "dumpling", - "slug": "dumpling", - "group": "Food & Drink", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🥠": { - "name": "fortune cookie", - "slug": "fortune_cookie", - "group": "Food & Drink", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🥡": { - "name": "takeout box", - "slug": "takeout_box", - "group": "Food & Drink", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🦀": { - "name": "crab", - "slug": "crab", - "group": "Food & Drink", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🦞": { - "name": "lobster", - "slug": "lobster", - "group": "Food & Drink", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🦐": { - "name": "shrimp", - "slug": "shrimp", - "group": "Food & Drink", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🦑": { - "name": "squid", - "slug": "squid", - "group": "Food & Drink", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🦪": { - "name": "oyster", - "slug": "oyster", - "group": "Food & Drink", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🍦": { - "name": "soft ice cream", - "slug": "soft_ice_cream", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍧": { - "name": "shaved ice", - "slug": "shaved_ice", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍨": { - "name": "ice cream", - "slug": "ice_cream", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍩": { - "name": "doughnut", - "slug": "doughnut", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍪": { - "name": "cookie", - "slug": "cookie", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🎂": { - "name": "birthday cake", - "slug": "birthday_cake", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍰": { - "name": "shortcake", - "slug": "shortcake", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🧁": { - "name": "cupcake", - "slug": "cupcake", - "group": "Food & Drink", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🥧": { - "name": "pie", - "slug": "pie", - "group": "Food & Drink", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🍫": { - "name": "chocolate bar", - "slug": "chocolate_bar", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍬": { - "name": "candy", - "slug": "candy", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍭": { - "name": "lollipop", - "slug": "lollipop", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍮": { - "name": "custard", - "slug": "custard", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍯": { - "name": "honey pot", - "slug": "honey_pot", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍼": { - "name": "baby bottle", - "slug": "baby_bottle", - "group": "Food & Drink", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🥛": { - "name": "glass of milk", - "slug": "glass_of_milk", - "group": "Food & Drink", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "☕": { - "name": "hot beverage", - "slug": "hot_beverage", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🫖": { - "name": "teapot", - "slug": "teapot", - "group": "Food & Drink", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🍵": { - "name": "teacup without handle", - "slug": "teacup_without_handle", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍶": { - "name": "sake", - "slug": "sake", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍾": { - "name": "bottle with popping cork", - "slug": "bottle_with_popping_cork", - "group": "Food & Drink", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🍷": { - "name": "wine glass", - "slug": "wine_glass", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍸": { - "name": "cocktail glass", - "slug": "cocktail_glass", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍹": { - "name": "tropical drink", - "slug": "tropical_drink", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍺": { - "name": "beer mug", - "slug": "beer_mug", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🍻": { - "name": "clinking beer mugs", - "slug": "clinking_beer_mugs", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🥂": { - "name": "clinking glasses", - "slug": "clinking_glasses", - "group": "Food & Drink", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🥃": { - "name": "tumbler glass", - "slug": "tumbler_glass", - "group": "Food & Drink", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🫗": { - "name": "pouring liquid", - "slug": "pouring_liquid", - "group": "Food & Drink", - "emoji_version": "14.0", - "unicode_version": "14.0", - "skin_tone_support": false - }, - "🥤": { - "name": "cup with straw", - "slug": "cup_with_straw", - "group": "Food & Drink", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🧋": { - "name": "bubble tea", - "slug": "bubble_tea", - "group": "Food & Drink", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🧃": { - "name": "beverage box", - "slug": "beverage_box", - "group": "Food & Drink", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🧉": { - "name": "mate", - "slug": "mate", - "group": "Food & Drink", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🧊": { - "name": "ice", - "slug": "ice", - "group": "Food & Drink", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🥢": { - "name": "chopsticks", - "slug": "chopsticks", - "group": "Food & Drink", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🍽️": { - "name": "fork and knife with plate", - "slug": "fork_and_knife_with_plate", - "group": "Food & Drink", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🍴": { - "name": "fork and knife", - "slug": "fork_and_knife", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🥄": { - "name": "spoon", - "slug": "spoon", - "group": "Food & Drink", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🔪": { - "name": "kitchen knife", - "slug": "kitchen_knife", - "group": "Food & Drink", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🫙": { - "name": "jar", - "slug": "jar", - "group": "Food & Drink", - "emoji_version": "14.0", - "unicode_version": "14.0", - "skin_tone_support": false - }, - "🏺": { - "name": "amphora", - "slug": "amphora", - "group": "Food & Drink", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🌍": { - "name": "globe showing Europe-Africa", - "slug": "globe_showing_europe_africa", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🌎": { - "name": "globe showing Americas", - "slug": "globe_showing_americas", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🌏": { - "name": "globe showing Asia-Australia", - "slug": "globe_showing_asia_australia", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🌐": { - "name": "globe with meridians", - "slug": "globe_with_meridians", - "group": "Travel & Places", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🗺️": { - "name": "world map", - "slug": "world_map", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🗾": { - "name": "map of Japan", - "slug": "map_of_japan", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🧭": { - "name": "compass", - "slug": "compass", - "group": "Travel & Places", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🏔️": { - "name": "snow-capped mountain", - "slug": "snow_capped_mountain", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "⛰️": { - "name": "mountain", - "slug": "mountain", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🌋": { - "name": "volcano", - "slug": "volcano", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🗻": { - "name": "mount fuji", - "slug": "mount_fuji", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🏕️": { - "name": "camping", - "slug": "camping", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🏖️": { - "name": "beach with umbrella", - "slug": "beach_with_umbrella", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🏜️": { - "name": "desert", - "slug": "desert", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🏝️": { - "name": "desert island", - "slug": "desert_island", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🏞️": { - "name": "national park", - "slug": "national_park", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🏟️": { - "name": "stadium", - "slug": "stadium", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🏛️": { - "name": "classical building", - "slug": "classical_building", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🏗️": { - "name": "building construction", - "slug": "building_construction", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🧱": { - "name": "brick", - "slug": "brick", - "group": "Travel & Places", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🪨": { - "name": "rock", - "slug": "rock", - "group": "Travel & Places", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🪵": { - "name": "wood", - "slug": "wood", - "group": "Travel & Places", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🛖": { - "name": "hut", - "slug": "hut", - "group": "Travel & Places", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🏘️": { - "name": "houses", - "slug": "houses", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🏚️": { - "name": "derelict house", - "slug": "derelict_house", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🏠": { - "name": "house", - "slug": "house", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🏡": { - "name": "house with garden", - "slug": "house_with_garden", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🏢": { - "name": "office building", - "slug": "office_building", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🏣": { - "name": "Japanese post office", - "slug": "japanese_post_office", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🏤": { - "name": "post office", - "slug": "post_office", - "group": "Travel & Places", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🏥": { - "name": "hospital", - "slug": "hospital", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🏦": { - "name": "bank", - "slug": "bank", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🏨": { - "name": "hotel", - "slug": "hotel", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🏩": { - "name": "love hotel", - "slug": "love_hotel", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🏪": { - "name": "convenience store", - "slug": "convenience_store", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🏫": { - "name": "school", - "slug": "school", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🏬": { - "name": "department store", - "slug": "department_store", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🏭": { - "name": "factory", - "slug": "factory", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🏯": { - "name": "Japanese castle", - "slug": "japanese_castle", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🏰": { - "name": "castle", - "slug": "castle", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "💒": { - "name": "wedding", - "slug": "wedding", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🗼": { - "name": "Tokyo tower", - "slug": "tokyo_tower", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🗽": { - "name": "Statue of Liberty", - "slug": "statue_of_liberty", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "⛪": { - "name": "church", - "slug": "church", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🕌": { - "name": "mosque", - "slug": "mosque", - "group": "Travel & Places", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🛕": { - "name": "hindu temple", - "slug": "hindu_temple", - "group": "Travel & Places", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🕍": { - "name": "synagogue", - "slug": "synagogue", - "group": "Travel & Places", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "⛩️": { - "name": "shinto shrine", - "slug": "shinto_shrine", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🕋": { - "name": "kaaba", - "slug": "kaaba", - "group": "Travel & Places", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "⛲": { - "name": "fountain", - "slug": "fountain", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "⛺": { - "name": "tent", - "slug": "tent", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🌁": { - "name": "foggy", - "slug": "foggy", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🌃": { - "name": "night with stars", - "slug": "night_with_stars", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🏙️": { - "name": "cityscape", - "slug": "cityscape", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🌄": { - "name": "sunrise over mountains", - "slug": "sunrise_over_mountains", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🌅": { - "name": "sunrise", - "slug": "sunrise", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🌆": { - "name": "cityscape at dusk", - "slug": "cityscape_at_dusk", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🌇": { - "name": "sunset", - "slug": "sunset", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🌉": { - "name": "bridge at night", - "slug": "bridge_at_night", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "♨️": { - "name": "hot springs", - "slug": "hot_springs", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🎠": { - "name": "carousel horse", - "slug": "carousel_horse", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🛝": { - "name": "playground slide", - "slug": "playground_slide", - "group": "Travel & Places", - "emoji_version": "14.0", - "unicode_version": "14.0", - "skin_tone_support": false - }, - "🎡": { - "name": "ferris wheel", - "slug": "ferris_wheel", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🎢": { - "name": "roller coaster", - "slug": "roller_coaster", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "💈": { - "name": "barber pole", - "slug": "barber_pole", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🎪": { - "name": "circus tent", - "slug": "circus_tent", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🚂": { - "name": "locomotive", - "slug": "locomotive", - "group": "Travel & Places", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🚃": { - "name": "railway car", - "slug": "railway_car", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🚄": { - "name": "high-speed train", - "slug": "high_speed_train", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🚅": { - "name": "bullet train", - "slug": "bullet_train", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🚆": { - "name": "train", - "slug": "train", - "group": "Travel & Places", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🚇": { - "name": "metro", - "slug": "metro", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🚈": { - "name": "light rail", - "slug": "light_rail", - "group": "Travel & Places", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🚉": { - "name": "station", - "slug": "station", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🚊": { - "name": "tram", - "slug": "tram", - "group": "Travel & Places", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🚝": { - "name": "monorail", - "slug": "monorail", - "group": "Travel & Places", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🚞": { - "name": "mountain railway", - "slug": "mountain_railway", - "group": "Travel & Places", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🚋": { - "name": "tram car", - "slug": "tram_car", - "group": "Travel & Places", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🚌": { - "name": "bus", - "slug": "bus", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🚍": { - "name": "oncoming bus", - "slug": "oncoming_bus", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🚎": { - "name": "trolleybus", - "slug": "trolleybus", - "group": "Travel & Places", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🚐": { - "name": "minibus", - "slug": "minibus", - "group": "Travel & Places", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🚑": { - "name": "ambulance", - "slug": "ambulance", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🚒": { - "name": "fire engine", - "slug": "fire_engine", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🚓": { - "name": "police car", - "slug": "police_car", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🚔": { - "name": "oncoming police car", - "slug": "oncoming_police_car", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🚕": { - "name": "taxi", - "slug": "taxi", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🚖": { - "name": "oncoming taxi", - "slug": "oncoming_taxi", - "group": "Travel & Places", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🚗": { - "name": "automobile", - "slug": "automobile", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🚘": { - "name": "oncoming automobile", - "slug": "oncoming_automobile", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🚙": { - "name": "sport utility vehicle", - "slug": "sport_utility_vehicle", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🛻": { - "name": "pickup truck", - "slug": "pickup_truck", - "group": "Travel & Places", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🚚": { - "name": "delivery truck", - "slug": "delivery_truck", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🚛": { - "name": "articulated lorry", - "slug": "articulated_lorry", - "group": "Travel & Places", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🚜": { - "name": "tractor", - "slug": "tractor", - "group": "Travel & Places", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🏎️": { - "name": "racing car", - "slug": "racing_car", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🏍️": { - "name": "motorcycle", - "slug": "motorcycle", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🛵": { - "name": "motor scooter", - "slug": "motor_scooter", - "group": "Travel & Places", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🦽": { - "name": "manual wheelchair", - "slug": "manual_wheelchair", - "group": "Travel & Places", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🦼": { - "name": "motorized wheelchair", - "slug": "motorized_wheelchair", - "group": "Travel & Places", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🛺": { - "name": "auto rickshaw", - "slug": "auto_rickshaw", - "group": "Travel & Places", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🚲": { - "name": "bicycle", - "slug": "bicycle", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🛴": { - "name": "kick scooter", - "slug": "kick_scooter", - "group": "Travel & Places", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🛹": { - "name": "skateboard", - "slug": "skateboard", - "group": "Travel & Places", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🛼": { - "name": "roller skate", - "slug": "roller_skate", - "group": "Travel & Places", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🚏": { - "name": "bus stop", - "slug": "bus_stop", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🛣️": { - "name": "motorway", - "slug": "motorway", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🛤️": { - "name": "railway track", - "slug": "railway_track", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🛢️": { - "name": "oil drum", - "slug": "oil_drum", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "⛽": { - "name": "fuel pump", - "slug": "fuel_pump", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🛞": { - "name": "wheel", - "slug": "wheel", - "group": "Travel & Places", - "emoji_version": "14.0", - "unicode_version": "14.0", - "skin_tone_support": false - }, - "🚨": { - "name": "police car light", - "slug": "police_car_light", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🚥": { - "name": "horizontal traffic light", - "slug": "horizontal_traffic_light", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🚦": { - "name": "vertical traffic light", - "slug": "vertical_traffic_light", - "group": "Travel & Places", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🛑": { - "name": "stop sign", - "slug": "stop_sign", - "group": "Travel & Places", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🚧": { - "name": "construction", - "slug": "construction", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "⚓": { - "name": "anchor", - "slug": "anchor", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🛟": { - "name": "ring buoy", - "slug": "ring_buoy", - "group": "Travel & Places", - "emoji_version": "14.0", - "unicode_version": "14.0", - "skin_tone_support": false - }, - "⛵": { - "name": "sailboat", - "slug": "sailboat", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🛶": { - "name": "canoe", - "slug": "canoe", - "group": "Travel & Places", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🚤": { - "name": "speedboat", - "slug": "speedboat", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🛳️": { - "name": "passenger ship", - "slug": "passenger_ship", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "⛴️": { - "name": "ferry", - "slug": "ferry", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🛥️": { - "name": "motor boat", - "slug": "motor_boat", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🚢": { - "name": "ship", - "slug": "ship", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "✈️": { - "name": "airplane", - "slug": "airplane", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🛩️": { - "name": "small airplane", - "slug": "small_airplane", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🛫": { - "name": "airplane departure", - "slug": "airplane_departure", - "group": "Travel & Places", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🛬": { - "name": "airplane arrival", - "slug": "airplane_arrival", - "group": "Travel & Places", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🪂": { - "name": "parachute", - "slug": "parachute", - "group": "Travel & Places", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "💺": { - "name": "seat", - "slug": "seat", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🚁": { - "name": "helicopter", - "slug": "helicopter", - "group": "Travel & Places", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🚟": { - "name": "suspension railway", - "slug": "suspension_railway", - "group": "Travel & Places", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🚠": { - "name": "mountain cableway", - "slug": "mountain_cableway", - "group": "Travel & Places", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🚡": { - "name": "aerial tramway", - "slug": "aerial_tramway", - "group": "Travel & Places", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🛰️": { - "name": "satellite", - "slug": "satellite", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🚀": { - "name": "rocket", - "slug": "rocket", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🛸": { - "name": "flying saucer", - "slug": "flying_saucer", - "group": "Travel & Places", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🛎️": { - "name": "bellhop bell", - "slug": "bellhop_bell", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🧳": { - "name": "luggage", - "slug": "luggage", - "group": "Travel & Places", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "⌛": { - "name": "hourglass done", - "slug": "hourglass_done", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "⏳": { - "name": "hourglass not done", - "slug": "hourglass_not_done", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "⌚": { - "name": "watch", - "slug": "watch", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "⏰": { - "name": "alarm clock", - "slug": "alarm_clock", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "⏱️": { - "name": "stopwatch", - "slug": "stopwatch", - "group": "Travel & Places", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "⏲️": { - "name": "timer clock", - "slug": "timer_clock", - "group": "Travel & Places", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🕰️": { - "name": "mantelpiece clock", - "slug": "mantelpiece_clock", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🕛": { - "name": "twelve o’clock", - "slug": "twelve_o_clock", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🕧": { - "name": "twelve-thirty", - "slug": "twelve_thirty", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🕐": { - "name": "one o’clock", - "slug": "one_o_clock", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🕜": { - "name": "one-thirty", - "slug": "one_thirty", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🕑": { - "name": "two o’clock", - "slug": "two_o_clock", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🕝": { - "name": "two-thirty", - "slug": "two_thirty", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🕒": { - "name": "three o’clock", - "slug": "three_o_clock", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🕞": { - "name": "three-thirty", - "slug": "three_thirty", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🕓": { - "name": "four o’clock", - "slug": "four_o_clock", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🕟": { - "name": "four-thirty", - "slug": "four_thirty", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🕔": { - "name": "five o’clock", - "slug": "five_o_clock", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🕠": { - "name": "five-thirty", - "slug": "five_thirty", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🕕": { - "name": "six o’clock", - "slug": "six_o_clock", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🕡": { - "name": "six-thirty", - "slug": "six_thirty", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🕖": { - "name": "seven o’clock", - "slug": "seven_o_clock", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🕢": { - "name": "seven-thirty", - "slug": "seven_thirty", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🕗": { - "name": "eight o’clock", - "slug": "eight_o_clock", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🕣": { - "name": "eight-thirty", - "slug": "eight_thirty", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🕘": { - "name": "nine o’clock", - "slug": "nine_o_clock", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🕤": { - "name": "nine-thirty", - "slug": "nine_thirty", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🕙": { - "name": "ten o’clock", - "slug": "ten_o_clock", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🕥": { - "name": "ten-thirty", - "slug": "ten_thirty", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🕚": { - "name": "eleven o’clock", - "slug": "eleven_o_clock", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🕦": { - "name": "eleven-thirty", - "slug": "eleven_thirty", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🌑": { - "name": "new moon", - "slug": "new_moon", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🌒": { - "name": "waxing crescent moon", - "slug": "waxing_crescent_moon", - "group": "Travel & Places", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🌓": { - "name": "first quarter moon", - "slug": "first_quarter_moon", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🌔": { - "name": "waxing gibbous moon", - "slug": "waxing_gibbous_moon", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🌕": { - "name": "full moon", - "slug": "full_moon", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🌖": { - "name": "waning gibbous moon", - "slug": "waning_gibbous_moon", - "group": "Travel & Places", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🌗": { - "name": "last quarter moon", - "slug": "last_quarter_moon", - "group": "Travel & Places", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🌘": { - "name": "waning crescent moon", - "slug": "waning_crescent_moon", - "group": "Travel & Places", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🌙": { - "name": "crescent moon", - "slug": "crescent_moon", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🌚": { - "name": "new moon face", - "slug": "new_moon_face", - "group": "Travel & Places", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🌛": { - "name": "first quarter moon face", - "slug": "first_quarter_moon_face", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🌜": { - "name": "last quarter moon face", - "slug": "last_quarter_moon_face", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🌡️": { - "name": "thermometer", - "slug": "thermometer", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "☀️": { - "name": "sun", - "slug": "sun", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🌝": { - "name": "full moon face", - "slug": "full_moon_face", - "group": "Travel & Places", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🌞": { - "name": "sun with face", - "slug": "sun_with_face", - "group": "Travel & Places", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🪐": { - "name": "ringed planet", - "slug": "ringed_planet", - "group": "Travel & Places", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "⭐": { - "name": "star", - "slug": "star", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🌟": { - "name": "glowing star", - "slug": "glowing_star", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🌠": { - "name": "shooting star", - "slug": "shooting_star", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🌌": { - "name": "milky way", - "slug": "milky_way", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "☁️": { - "name": "cloud", - "slug": "cloud", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "⛅": { - "name": "sun behind cloud", - "slug": "sun_behind_cloud", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "⛈️": { - "name": "cloud with lightning and rain", - "slug": "cloud_with_lightning_and_rain", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🌤️": { - "name": "sun behind small cloud", - "slug": "sun_behind_small_cloud", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🌥️": { - "name": "sun behind large cloud", - "slug": "sun_behind_large_cloud", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🌦️": { - "name": "sun behind rain cloud", - "slug": "sun_behind_rain_cloud", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🌧️": { - "name": "cloud with rain", - "slug": "cloud_with_rain", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🌨️": { - "name": "cloud with snow", - "slug": "cloud_with_snow", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🌩️": { - "name": "cloud with lightning", - "slug": "cloud_with_lightning", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🌪️": { - "name": "tornado", - "slug": "tornado", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🌫️": { - "name": "fog", - "slug": "fog", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🌬️": { - "name": "wind face", - "slug": "wind_face", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🌀": { - "name": "cyclone", - "slug": "cyclone", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🌈": { - "name": "rainbow", - "slug": "rainbow", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🌂": { - "name": "closed umbrella", - "slug": "closed_umbrella", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "☂️": { - "name": "umbrella", - "slug": "umbrella", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "☔": { - "name": "umbrella with rain drops", - "slug": "umbrella_with_rain_drops", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "⛱️": { - "name": "umbrella on ground", - "slug": "umbrella_on_ground", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "⚡": { - "name": "high voltage", - "slug": "high_voltage", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "❄️": { - "name": "snowflake", - "slug": "snowflake", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "☃️": { - "name": "snowman", - "slug": "snowman", - "group": "Travel & Places", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "⛄": { - "name": "snowman without snow", - "slug": "snowman_without_snow", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "☄️": { - "name": "comet", - "slug": "comet", - "group": "Travel & Places", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🔥": { - "name": "fire", - "slug": "fire", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "💧": { - "name": "droplet", - "slug": "droplet", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🌊": { - "name": "water wave", - "slug": "water_wave", - "group": "Travel & Places", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🎃": { - "name": "jack-o-lantern", - "slug": "jack_o_lantern", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🎄": { - "name": "Christmas tree", - "slug": "christmas_tree", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🎆": { - "name": "fireworks", - "slug": "fireworks", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🎇": { - "name": "sparkler", - "slug": "sparkler", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🧨": { - "name": "firecracker", - "slug": "firecracker", - "group": "Activities", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "✨": { - "name": "sparkles", - "slug": "sparkles", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🎈": { - "name": "balloon", - "slug": "balloon", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🎉": { - "name": "party popper", - "slug": "party_popper", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🎊": { - "name": "confetti ball", - "slug": "confetti_ball", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🎋": { - "name": "tanabata tree", - "slug": "tanabata_tree", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🎍": { - "name": "pine decoration", - "slug": "pine_decoration", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🎎": { - "name": "Japanese dolls", - "slug": "japanese_dolls", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🎏": { - "name": "carp streamer", - "slug": "carp_streamer", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🎐": { - "name": "wind chime", - "slug": "wind_chime", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🎑": { - "name": "moon viewing ceremony", - "slug": "moon_viewing_ceremony", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🧧": { - "name": "red envelope", - "slug": "red_envelope", - "group": "Activities", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🎀": { - "name": "ribbon", - "slug": "ribbon", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🎁": { - "name": "wrapped gift", - "slug": "wrapped_gift", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🎗️": { - "name": "reminder ribbon", - "slug": "reminder_ribbon", - "group": "Activities", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🎟️": { - "name": "admission tickets", - "slug": "admission_tickets", - "group": "Activities", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🎫": { - "name": "ticket", - "slug": "ticket", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🎖️": { - "name": "military medal", - "slug": "military_medal", - "group": "Activities", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🏆": { - "name": "trophy", - "slug": "trophy", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🏅": { - "name": "sports medal", - "slug": "sports_medal", - "group": "Activities", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🥇": { - "name": "1st place medal", - "slug": "1st_place_medal", - "group": "Activities", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🥈": { - "name": "2nd place medal", - "slug": "2nd_place_medal", - "group": "Activities", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🥉": { - "name": "3rd place medal", - "slug": "3rd_place_medal", - "group": "Activities", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "⚽": { - "name": "soccer ball", - "slug": "soccer_ball", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "⚾": { - "name": "baseball", - "slug": "baseball", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🥎": { - "name": "softball", - "slug": "softball", - "group": "Activities", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🏀": { - "name": "basketball", - "slug": "basketball", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🏐": { - "name": "volleyball", - "slug": "volleyball", - "group": "Activities", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🏈": { - "name": "american football", - "slug": "american_football", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🏉": { - "name": "rugby football", - "slug": "rugby_football", - "group": "Activities", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🎾": { - "name": "tennis", - "slug": "tennis", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🥏": { - "name": "flying disc", - "slug": "flying_disc", - "group": "Activities", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🎳": { - "name": "bowling", - "slug": "bowling", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🏏": { - "name": "cricket game", - "slug": "cricket_game", - "group": "Activities", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🏑": { - "name": "field hockey", - "slug": "field_hockey", - "group": "Activities", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🏒": { - "name": "ice hockey", - "slug": "ice_hockey", - "group": "Activities", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🥍": { - "name": "lacrosse", - "slug": "lacrosse", - "group": "Activities", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🏓": { - "name": "ping pong", - "slug": "ping_pong", - "group": "Activities", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🏸": { - "name": "badminton", - "slug": "badminton", - "group": "Activities", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🥊": { - "name": "boxing glove", - "slug": "boxing_glove", - "group": "Activities", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🥋": { - "name": "martial arts uniform", - "slug": "martial_arts_uniform", - "group": "Activities", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🥅": { - "name": "goal net", - "slug": "goal_net", - "group": "Activities", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "⛳": { - "name": "flag in hole", - "slug": "flag_in_hole", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "⛸️": { - "name": "ice skate", - "slug": "ice_skate", - "group": "Activities", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🎣": { - "name": "fishing pole", - "slug": "fishing_pole", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🤿": { - "name": "diving mask", - "slug": "diving_mask", - "group": "Activities", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🎽": { - "name": "running shirt", - "slug": "running_shirt", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🎿": { - "name": "skis", - "slug": "skis", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🛷": { - "name": "sled", - "slug": "sled", - "group": "Activities", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🥌": { - "name": "curling stone", - "slug": "curling_stone", - "group": "Activities", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🎯": { - "name": "bullseye", - "slug": "bullseye", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🪀": { - "name": "yo-yo", - "slug": "yo_yo", - "group": "Activities", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🪁": { - "name": "kite", - "slug": "kite", - "group": "Activities", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🔫": { - "name": "water pistol", - "slug": "water_pistol", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🎱": { - "name": "pool 8 ball", - "slug": "pool_8_ball", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🔮": { - "name": "crystal ball", - "slug": "crystal_ball", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🪄": { - "name": "magic wand", - "slug": "magic_wand", - "group": "Activities", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🎮": { - "name": "video game", - "slug": "video_game", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🕹️": { - "name": "joystick", - "slug": "joystick", - "group": "Activities", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🎰": { - "name": "slot machine", - "slug": "slot_machine", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🎲": { - "name": "game die", - "slug": "game_die", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🧩": { - "name": "puzzle piece", - "slug": "puzzle_piece", - "group": "Activities", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🧸": { - "name": "teddy bear", - "slug": "teddy_bear", - "group": "Activities", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🪅": { - "name": "piñata", - "slug": "pinata", - "group": "Activities", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🪩": { - "name": "mirror ball", - "slug": "mirror_ball", - "group": "Activities", - "emoji_version": "14.0", - "unicode_version": "14.0", - "skin_tone_support": false - }, - "🪆": { - "name": "nesting dolls", - "slug": "nesting_dolls", - "group": "Activities", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "♠️": { - "name": "spade suit", - "slug": "spade_suit", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "♥️": { - "name": "heart suit", - "slug": "heart_suit", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "♦️": { - "name": "diamond suit", - "slug": "diamond_suit", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "♣️": { - "name": "club suit", - "slug": "club_suit", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "♟️": { - "name": "chess pawn", - "slug": "chess_pawn", - "group": "Activities", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🃏": { - "name": "joker", - "slug": "joker", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🀄": { - "name": "mahjong red dragon", - "slug": "mahjong_red_dragon", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🎴": { - "name": "flower playing cards", - "slug": "flower_playing_cards", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🎭": { - "name": "performing arts", - "slug": "performing_arts", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🖼️": { - "name": "framed picture", - "slug": "framed_picture", - "group": "Activities", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🎨": { - "name": "artist palette", - "slug": "artist_palette", - "group": "Activities", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🧵": { - "name": "thread", - "slug": "thread", - "group": "Activities", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🪡": { - "name": "sewing needle", - "slug": "sewing_needle", - "group": "Activities", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🧶": { - "name": "yarn", - "slug": "yarn", - "group": "Activities", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🪢": { - "name": "knot", - "slug": "knot", - "group": "Activities", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "👓": { - "name": "glasses", - "slug": "glasses", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🕶️": { - "name": "sunglasses", - "slug": "sunglasses", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🥽": { - "name": "goggles", - "slug": "goggles", - "group": "Objects", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🥼": { - "name": "lab coat", - "slug": "lab_coat", - "group": "Objects", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🦺": { - "name": "safety vest", - "slug": "safety_vest", - "group": "Objects", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "👔": { - "name": "necktie", - "slug": "necktie", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "👕": { - "name": "t-shirt", - "slug": "t_shirt", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "👖": { - "name": "jeans", - "slug": "jeans", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🧣": { - "name": "scarf", - "slug": "scarf", - "group": "Objects", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🧤": { - "name": "gloves", - "slug": "gloves", - "group": "Objects", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🧥": { - "name": "coat", - "slug": "coat", - "group": "Objects", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🧦": { - "name": "socks", - "slug": "socks", - "group": "Objects", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "👗": { - "name": "dress", - "slug": "dress", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "👘": { - "name": "kimono", - "slug": "kimono", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🥻": { - "name": "sari", - "slug": "sari", - "group": "Objects", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🩱": { - "name": "one-piece swimsuit", - "slug": "one_piece_swimsuit", - "group": "Objects", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🩲": { - "name": "briefs", - "slug": "briefs", - "group": "Objects", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🩳": { - "name": "shorts", - "slug": "shorts", - "group": "Objects", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "👙": { - "name": "bikini", - "slug": "bikini", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "👚": { - "name": "woman’s clothes", - "slug": "woman_s_clothes", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🪭": { - "name": "folding hand fan", - "slug": "folding_hand_fan", - "group": "Objects", - "emoji_version": "15.0", - "unicode_version": "15.0", - "skin_tone_support": false - }, - "👛": { - "name": "purse", - "slug": "purse", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "👜": { - "name": "handbag", - "slug": "handbag", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "👝": { - "name": "clutch bag", - "slug": "clutch_bag", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🛍️": { - "name": "shopping bags", - "slug": "shopping_bags", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🎒": { - "name": "backpack", - "slug": "backpack", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🩴": { - "name": "thong sandal", - "slug": "thong_sandal", - "group": "Objects", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "👞": { - "name": "man’s shoe", - "slug": "man_s_shoe", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "👟": { - "name": "running shoe", - "slug": "running_shoe", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🥾": { - "name": "hiking boot", - "slug": "hiking_boot", - "group": "Objects", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🥿": { - "name": "flat shoe", - "slug": "flat_shoe", - "group": "Objects", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "👠": { - "name": "high-heeled shoe", - "slug": "high_heeled_shoe", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "👡": { - "name": "woman’s sandal", - "slug": "woman_s_sandal", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🩰": { - "name": "ballet shoes", - "slug": "ballet_shoes", - "group": "Objects", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "👢": { - "name": "woman’s boot", - "slug": "woman_s_boot", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🪮": { - "name": "hair pick", - "slug": "hair_pick", - "group": "Objects", - "emoji_version": "15.0", - "unicode_version": "15.0", - "skin_tone_support": false - }, - "👑": { - "name": "crown", - "slug": "crown", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "👒": { - "name": "woman’s hat", - "slug": "woman_s_hat", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🎩": { - "name": "top hat", - "slug": "top_hat", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🎓": { - "name": "graduation cap", - "slug": "graduation_cap", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🧢": { - "name": "billed cap", - "slug": "billed_cap", - "group": "Objects", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🪖": { - "name": "military helmet", - "slug": "military_helmet", - "group": "Objects", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "⛑️": { - "name": "rescue worker’s helmet", - "slug": "rescue_worker_s_helmet", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "📿": { - "name": "prayer beads", - "slug": "prayer_beads", - "group": "Objects", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "💄": { - "name": "lipstick", - "slug": "lipstick", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "💍": { - "name": "ring", - "slug": "ring", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "💎": { - "name": "gem stone", - "slug": "gem_stone", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🔇": { - "name": "muted speaker", - "slug": "muted_speaker", - "group": "Objects", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🔈": { - "name": "speaker low volume", - "slug": "speaker_low_volume", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🔉": { - "name": "speaker medium volume", - "slug": "speaker_medium_volume", - "group": "Objects", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🔊": { - "name": "speaker high volume", - "slug": "speaker_high_volume", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📢": { - "name": "loudspeaker", - "slug": "loudspeaker", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📣": { - "name": "megaphone", - "slug": "megaphone", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📯": { - "name": "postal horn", - "slug": "postal_horn", - "group": "Objects", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🔔": { - "name": "bell", - "slug": "bell", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🔕": { - "name": "bell with slash", - "slug": "bell_with_slash", - "group": "Objects", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🎼": { - "name": "musical score", - "slug": "musical_score", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🎵": { - "name": "musical note", - "slug": "musical_note", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🎶": { - "name": "musical notes", - "slug": "musical_notes", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🎙️": { - "name": "studio microphone", - "slug": "studio_microphone", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🎚️": { - "name": "level slider", - "slug": "level_slider", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🎛️": { - "name": "control knobs", - "slug": "control_knobs", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🎤": { - "name": "microphone", - "slug": "microphone", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🎧": { - "name": "headphone", - "slug": "headphone", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📻": { - "name": "radio", - "slug": "radio", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🎷": { - "name": "saxophone", - "slug": "saxophone", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🪗": { - "name": "accordion", - "slug": "accordion", - "group": "Objects", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🎸": { - "name": "guitar", - "slug": "guitar", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🎹": { - "name": "musical keyboard", - "slug": "musical_keyboard", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🎺": { - "name": "trumpet", - "slug": "trumpet", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🎻": { - "name": "violin", - "slug": "violin", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🪕": { - "name": "banjo", - "slug": "banjo", - "group": "Objects", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🥁": { - "name": "drum", - "slug": "drum", - "group": "Objects", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🪘": { - "name": "long drum", - "slug": "long_drum", - "group": "Objects", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🪇": { - "name": "maracas", - "slug": "maracas", - "group": "Objects", - "emoji_version": "15.0", - "unicode_version": "15.0", - "skin_tone_support": false - }, - "🪈": { - "name": "flute", - "slug": "flute", - "group": "Objects", - "emoji_version": "15.0", - "unicode_version": "15.0", - "skin_tone_support": false - }, - "📱": { - "name": "mobile phone", - "slug": "mobile_phone", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📲": { - "name": "mobile phone with arrow", - "slug": "mobile_phone_with_arrow", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "☎️": { - "name": "telephone", - "slug": "telephone", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📞": { - "name": "telephone receiver", - "slug": "telephone_receiver", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📟": { - "name": "pager", - "slug": "pager", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📠": { - "name": "fax machine", - "slug": "fax_machine", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🔋": { - "name": "battery", - "slug": "battery", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🪫": { - "name": "low battery", - "slug": "low_battery", - "group": "Objects", - "emoji_version": "14.0", - "unicode_version": "14.0", - "skin_tone_support": false - }, - "🔌": { - "name": "electric plug", - "slug": "electric_plug", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "💻": { - "name": "laptop", - "slug": "laptop", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🖥️": { - "name": "desktop computer", - "slug": "desktop_computer", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🖨️": { - "name": "printer", - "slug": "printer", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "⌨️": { - "name": "keyboard", - "slug": "keyboard", - "group": "Objects", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🖱️": { - "name": "computer mouse", - "slug": "computer_mouse", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🖲️": { - "name": "trackball", - "slug": "trackball", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "💽": { - "name": "computer disk", - "slug": "computer_disk", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "💾": { - "name": "floppy disk", - "slug": "floppy_disk", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "💿": { - "name": "optical disk", - "slug": "optical_disk", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📀": { - "name": "dvd", - "slug": "dvd", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🧮": { - "name": "abacus", - "slug": "abacus", - "group": "Objects", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🎥": { - "name": "movie camera", - "slug": "movie_camera", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🎞️": { - "name": "film frames", - "slug": "film_frames", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "📽️": { - "name": "film projector", - "slug": "film_projector", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🎬": { - "name": "clapper board", - "slug": "clapper_board", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📺": { - "name": "television", - "slug": "television", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📷": { - "name": "camera", - "slug": "camera", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📸": { - "name": "camera with flash", - "slug": "camera_with_flash", - "group": "Objects", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "📹": { - "name": "video camera", - "slug": "video_camera", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📼": { - "name": "videocassette", - "slug": "videocassette", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🔍": { - "name": "magnifying glass tilted left", - "slug": "magnifying_glass_tilted_left", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🔎": { - "name": "magnifying glass tilted right", - "slug": "magnifying_glass_tilted_right", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🕯️": { - "name": "candle", - "slug": "candle", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "💡": { - "name": "light bulb", - "slug": "light_bulb", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🔦": { - "name": "flashlight", - "slug": "flashlight", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🏮": { - "name": "red paper lantern", - "slug": "red_paper_lantern", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🪔": { - "name": "diya lamp", - "slug": "diya_lamp", - "group": "Objects", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "📔": { - "name": "notebook with decorative cover", - "slug": "notebook_with_decorative_cover", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📕": { - "name": "closed book", - "slug": "closed_book", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📖": { - "name": "open book", - "slug": "open_book", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📗": { - "name": "green book", - "slug": "green_book", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📘": { - "name": "blue book", - "slug": "blue_book", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📙": { - "name": "orange book", - "slug": "orange_book", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📚": { - "name": "books", - "slug": "books", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📓": { - "name": "notebook", - "slug": "notebook", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📒": { - "name": "ledger", - "slug": "ledger", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📃": { - "name": "page with curl", - "slug": "page_with_curl", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📜": { - "name": "scroll", - "slug": "scroll", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📄": { - "name": "page facing up", - "slug": "page_facing_up", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📰": { - "name": "newspaper", - "slug": "newspaper", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🗞️": { - "name": "rolled-up newspaper", - "slug": "rolled_up_newspaper", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "📑": { - "name": "bookmark tabs", - "slug": "bookmark_tabs", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🔖": { - "name": "bookmark", - "slug": "bookmark", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🏷️": { - "name": "label", - "slug": "label", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "💰": { - "name": "money bag", - "slug": "money_bag", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🪙": { - "name": "coin", - "slug": "coin", - "group": "Objects", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "💴": { - "name": "yen banknote", - "slug": "yen_banknote", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "💵": { - "name": "dollar banknote", - "slug": "dollar_banknote", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "💶": { - "name": "euro banknote", - "slug": "euro_banknote", - "group": "Objects", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "💷": { - "name": "pound banknote", - "slug": "pound_banknote", - "group": "Objects", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "💸": { - "name": "money with wings", - "slug": "money_with_wings", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "💳": { - "name": "credit card", - "slug": "credit_card", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🧾": { - "name": "receipt", - "slug": "receipt", - "group": "Objects", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "💹": { - "name": "chart increasing with yen", - "slug": "chart_increasing_with_yen", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "✉️": { - "name": "envelope", - "slug": "envelope", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📧": { - "name": "e-mail", - "slug": "e_mail", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📨": { - "name": "incoming envelope", - "slug": "incoming_envelope", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📩": { - "name": "envelope with arrow", - "slug": "envelope_with_arrow", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📤": { - "name": "outbox tray", - "slug": "outbox_tray", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📥": { - "name": "inbox tray", - "slug": "inbox_tray", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📦": { - "name": "package", - "slug": "package", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📫": { - "name": "closed mailbox with raised flag", - "slug": "closed_mailbox_with_raised_flag", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📪": { - "name": "closed mailbox with lowered flag", - "slug": "closed_mailbox_with_lowered_flag", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📬": { - "name": "open mailbox with raised flag", - "slug": "open_mailbox_with_raised_flag", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "📭": { - "name": "open mailbox with lowered flag", - "slug": "open_mailbox_with_lowered_flag", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "📮": { - "name": "postbox", - "slug": "postbox", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🗳️": { - "name": "ballot box with ballot", - "slug": "ballot_box_with_ballot", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "✏️": { - "name": "pencil", - "slug": "pencil", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "✒️": { - "name": "black nib", - "slug": "black_nib", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🖋️": { - "name": "fountain pen", - "slug": "fountain_pen", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🖊️": { - "name": "pen", - "slug": "pen", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🖌️": { - "name": "paintbrush", - "slug": "paintbrush", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🖍️": { - "name": "crayon", - "slug": "crayon", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "📝": { - "name": "memo", - "slug": "memo", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "💼": { - "name": "briefcase", - "slug": "briefcase", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📁": { - "name": "file folder", - "slug": "file_folder", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📂": { - "name": "open file folder", - "slug": "open_file_folder", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🗂️": { - "name": "card index dividers", - "slug": "card_index_dividers", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "📅": { - "name": "calendar", - "slug": "calendar", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📆": { - "name": "tear-off calendar", - "slug": "tear_off_calendar", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🗒️": { - "name": "spiral notepad", - "slug": "spiral_notepad", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🗓️": { - "name": "spiral calendar", - "slug": "spiral_calendar", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "📇": { - "name": "card index", - "slug": "card_index", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📈": { - "name": "chart increasing", - "slug": "chart_increasing", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📉": { - "name": "chart decreasing", - "slug": "chart_decreasing", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📊": { - "name": "bar chart", - "slug": "bar_chart", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📋": { - "name": "clipboard", - "slug": "clipboard", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📌": { - "name": "pushpin", - "slug": "pushpin", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📍": { - "name": "round pushpin", - "slug": "round_pushpin", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📎": { - "name": "paperclip", - "slug": "paperclip", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🖇️": { - "name": "linked paperclips", - "slug": "linked_paperclips", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "📏": { - "name": "straight ruler", - "slug": "straight_ruler", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📐": { - "name": "triangular ruler", - "slug": "triangular_ruler", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "✂️": { - "name": "scissors", - "slug": "scissors", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🗃️": { - "name": "card file box", - "slug": "card_file_box", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🗄️": { - "name": "file cabinet", - "slug": "file_cabinet", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🗑️": { - "name": "wastebasket", - "slug": "wastebasket", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🔒": { - "name": "locked", - "slug": "locked", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🔓": { - "name": "unlocked", - "slug": "unlocked", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🔏": { - "name": "locked with pen", - "slug": "locked_with_pen", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🔐": { - "name": "locked with key", - "slug": "locked_with_key", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🔑": { - "name": "key", - "slug": "key", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🗝️": { - "name": "old key", - "slug": "old_key", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🔨": { - "name": "hammer", - "slug": "hammer", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🪓": { - "name": "axe", - "slug": "axe", - "group": "Objects", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "⛏️": { - "name": "pick", - "slug": "pick", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "⚒️": { - "name": "hammer and pick", - "slug": "hammer_and_pick", - "group": "Objects", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🛠️": { - "name": "hammer and wrench", - "slug": "hammer_and_wrench", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🗡️": { - "name": "dagger", - "slug": "dagger", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "⚔️": { - "name": "crossed swords", - "slug": "crossed_swords", - "group": "Objects", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "💣": { - "name": "bomb", - "slug": "bomb", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🪃": { - "name": "boomerang", - "slug": "boomerang", - "group": "Objects", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🏹": { - "name": "bow and arrow", - "slug": "bow_and_arrow", - "group": "Objects", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🛡️": { - "name": "shield", - "slug": "shield", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🪚": { - "name": "carpentry saw", - "slug": "carpentry_saw", - "group": "Objects", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🔧": { - "name": "wrench", - "slug": "wrench", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🪛": { - "name": "screwdriver", - "slug": "screwdriver", - "group": "Objects", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🔩": { - "name": "nut and bolt", - "slug": "nut_and_bolt", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "⚙️": { - "name": "gear", - "slug": "gear", - "group": "Objects", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🗜️": { - "name": "clamp", - "slug": "clamp", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "⚖️": { - "name": "balance scale", - "slug": "balance_scale", - "group": "Objects", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🦯": { - "name": "white cane", - "slug": "white_cane", - "group": "Objects", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🔗": { - "name": "link", - "slug": "link", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "⛓️‍💥": { - "name": "broken chain", - "slug": "broken_chain", - "group": "Objects", - "emoji_version": "15.1", - "unicode_version": "15.1", - "skin_tone_support": false - }, - "⛓️": { - "name": "chains", - "slug": "chains", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🪝": { - "name": "hook", - "slug": "hook", - "group": "Objects", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🧰": { - "name": "toolbox", - "slug": "toolbox", - "group": "Objects", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🧲": { - "name": "magnet", - "slug": "magnet", - "group": "Objects", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🪜": { - "name": "ladder", - "slug": "ladder", - "group": "Objects", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "⚗️": { - "name": "alembic", - "slug": "alembic", - "group": "Objects", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🧪": { - "name": "test tube", - "slug": "test_tube", - "group": "Objects", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🧫": { - "name": "petri dish", - "slug": "petri_dish", - "group": "Objects", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🧬": { - "name": "dna", - "slug": "dna", - "group": "Objects", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🔬": { - "name": "microscope", - "slug": "microscope", - "group": "Objects", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🔭": { - "name": "telescope", - "slug": "telescope", - "group": "Objects", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "📡": { - "name": "satellite antenna", - "slug": "satellite_antenna", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "💉": { - "name": "syringe", - "slug": "syringe", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🩸": { - "name": "drop of blood", - "slug": "drop_of_blood", - "group": "Objects", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "💊": { - "name": "pill", - "slug": "pill", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🩹": { - "name": "adhesive bandage", - "slug": "adhesive_bandage", - "group": "Objects", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🩼": { - "name": "crutch", - "slug": "crutch", - "group": "Objects", - "emoji_version": "14.0", - "unicode_version": "14.0", - "skin_tone_support": false - }, - "🩺": { - "name": "stethoscope", - "slug": "stethoscope", - "group": "Objects", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🩻": { - "name": "x-ray", - "slug": "x_ray", - "group": "Objects", - "emoji_version": "14.0", - "unicode_version": "14.0", - "skin_tone_support": false - }, - "🚪": { - "name": "door", - "slug": "door", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🛗": { - "name": "elevator", - "slug": "elevator", - "group": "Objects", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🪞": { - "name": "mirror", - "slug": "mirror", - "group": "Objects", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🪟": { - "name": "window", - "slug": "window", - "group": "Objects", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🛏️": { - "name": "bed", - "slug": "bed", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🛋️": { - "name": "couch and lamp", - "slug": "couch_and_lamp", - "group": "Objects", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🪑": { - "name": "chair", - "slug": "chair", - "group": "Objects", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🚽": { - "name": "toilet", - "slug": "toilet", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🪠": { - "name": "plunger", - "slug": "plunger", - "group": "Objects", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🚿": { - "name": "shower", - "slug": "shower", - "group": "Objects", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🛁": { - "name": "bathtub", - "slug": "bathtub", - "group": "Objects", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🪤": { - "name": "mouse trap", - "slug": "mouse_trap", - "group": "Objects", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🪒": { - "name": "razor", - "slug": "razor", - "group": "Objects", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🧴": { - "name": "lotion bottle", - "slug": "lotion_bottle", - "group": "Objects", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🧷": { - "name": "safety pin", - "slug": "safety_pin", - "group": "Objects", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🧹": { - "name": "broom", - "slug": "broom", - "group": "Objects", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🧺": { - "name": "basket", - "slug": "basket", - "group": "Objects", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🧻": { - "name": "roll of paper", - "slug": "roll_of_paper", - "group": "Objects", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🪣": { - "name": "bucket", - "slug": "bucket", - "group": "Objects", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🧼": { - "name": "soap", - "slug": "soap", - "group": "Objects", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🫧": { - "name": "bubbles", - "slug": "bubbles", - "group": "Objects", - "emoji_version": "14.0", - "unicode_version": "14.0", - "skin_tone_support": false - }, - "🪥": { - "name": "toothbrush", - "slug": "toothbrush", - "group": "Objects", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🧽": { - "name": "sponge", - "slug": "sponge", - "group": "Objects", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🧯": { - "name": "fire extinguisher", - "slug": "fire_extinguisher", - "group": "Objects", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🛒": { - "name": "shopping cart", - "slug": "shopping_cart", - "group": "Objects", - "emoji_version": "3.0", - "unicode_version": "3.0", - "skin_tone_support": false - }, - "🚬": { - "name": "cigarette", - "slug": "cigarette", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "⚰️": { - "name": "coffin", - "slug": "coffin", - "group": "Objects", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🪦": { - "name": "headstone", - "slug": "headstone", - "group": "Objects", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "⚱️": { - "name": "funeral urn", - "slug": "funeral_urn", - "group": "Objects", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🧿": { - "name": "nazar amulet", - "slug": "nazar_amulet", - "group": "Objects", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🪬": { - "name": "hamsa", - "slug": "hamsa", - "group": "Objects", - "emoji_version": "14.0", - "unicode_version": "14.0", - "skin_tone_support": false - }, - "🗿": { - "name": "moai", - "slug": "moai", - "group": "Objects", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🪧": { - "name": "placard", - "slug": "placard", - "group": "Objects", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🪪": { - "name": "identification card", - "slug": "identification_card", - "group": "Objects", - "emoji_version": "14.0", - "unicode_version": "14.0", - "skin_tone_support": false - }, - "🏧": { - "name": "ATM sign", - "slug": "atm_sign", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🚮": { - "name": "litter in bin sign", - "slug": "litter_in_bin_sign", - "group": "Symbols", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🚰": { - "name": "potable water", - "slug": "potable_water", - "group": "Symbols", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "♿": { - "name": "wheelchair symbol", - "slug": "wheelchair_symbol", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🚹": { - "name": "men’s room", - "slug": "men_s_room", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🚺": { - "name": "women’s room", - "slug": "women_s_room", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🚻": { - "name": "restroom", - "slug": "restroom", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🚼": { - "name": "baby symbol", - "slug": "baby_symbol", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🚾": { - "name": "water closet", - "slug": "water_closet", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🛂": { - "name": "passport control", - "slug": "passport_control", - "group": "Symbols", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🛃": { - "name": "customs", - "slug": "customs", - "group": "Symbols", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🛄": { - "name": "baggage claim", - "slug": "baggage_claim", - "group": "Symbols", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🛅": { - "name": "left luggage", - "slug": "left_luggage", - "group": "Symbols", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "⚠️": { - "name": "warning", - "slug": "warning", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🚸": { - "name": "children crossing", - "slug": "children_crossing", - "group": "Symbols", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "⛔": { - "name": "no entry", - "slug": "no_entry", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🚫": { - "name": "prohibited", - "slug": "prohibited", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🚳": { - "name": "no bicycles", - "slug": "no_bicycles", - "group": "Symbols", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🚭": { - "name": "no smoking", - "slug": "no_smoking", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🚯": { - "name": "no littering", - "slug": "no_littering", - "group": "Symbols", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🚱": { - "name": "non-potable water", - "slug": "non_potable_water", - "group": "Symbols", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🚷": { - "name": "no pedestrians", - "slug": "no_pedestrians", - "group": "Symbols", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "📵": { - "name": "no mobile phones", - "slug": "no_mobile_phones", - "group": "Symbols", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🔞": { - "name": "no one under eighteen", - "slug": "no_one_under_eighteen", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "☢️": { - "name": "radioactive", - "slug": "radioactive", - "group": "Symbols", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "☣️": { - "name": "biohazard", - "slug": "biohazard", - "group": "Symbols", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "⬆️": { - "name": "up arrow", - "slug": "up_arrow", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "↗️": { - "name": "up-right arrow", - "slug": "up_right_arrow", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "➡️": { - "name": "right arrow", - "slug": "right_arrow", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "↘️": { - "name": "down-right arrow", - "slug": "down_right_arrow", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "⬇️": { - "name": "down arrow", - "slug": "down_arrow", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "↙️": { - "name": "down-left arrow", - "slug": "down_left_arrow", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "⬅️": { - "name": "left arrow", - "slug": "left_arrow", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "↖️": { - "name": "up-left arrow", - "slug": "up_left_arrow", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "↕️": { - "name": "up-down arrow", - "slug": "up_down_arrow", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "↔️": { - "name": "left-right arrow", - "slug": "left_right_arrow", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "↩️": { - "name": "right arrow curving left", - "slug": "right_arrow_curving_left", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "↪️": { - "name": "left arrow curving right", - "slug": "left_arrow_curving_right", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "⤴️": { - "name": "right arrow curving up", - "slug": "right_arrow_curving_up", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "⤵️": { - "name": "right arrow curving down", - "slug": "right_arrow_curving_down", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🔃": { - "name": "clockwise vertical arrows", - "slug": "clockwise_vertical_arrows", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🔄": { - "name": "counterclockwise arrows button", - "slug": "counterclockwise_arrows_button", - "group": "Symbols", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🔙": { - "name": "BACK arrow", - "slug": "back_arrow", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🔚": { - "name": "END arrow", - "slug": "end_arrow", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🔛": { - "name": "ON! arrow", - "slug": "on_arrow", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🔜": { - "name": "SOON arrow", - "slug": "soon_arrow", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🔝": { - "name": "TOP arrow", - "slug": "top_arrow", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🛐": { - "name": "place of worship", - "slug": "place_of_worship", - "group": "Symbols", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "⚛️": { - "name": "atom symbol", - "slug": "atom_symbol", - "group": "Symbols", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🕉️": { - "name": "om", - "slug": "om", - "group": "Symbols", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "✡️": { - "name": "star of David", - "slug": "star_of_david", - "group": "Symbols", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "☸️": { - "name": "wheel of dharma", - "slug": "wheel_of_dharma", - "group": "Symbols", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "☯️": { - "name": "yin yang", - "slug": "yin_yang", - "group": "Symbols", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "✝️": { - "name": "latin cross", - "slug": "latin_cross", - "group": "Symbols", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "☦️": { - "name": "orthodox cross", - "slug": "orthodox_cross", - "group": "Symbols", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "☪️": { - "name": "star and crescent", - "slug": "star_and_crescent", - "group": "Symbols", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "☮️": { - "name": "peace symbol", - "slug": "peace_symbol", - "group": "Symbols", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🕎": { - "name": "menorah", - "slug": "menorah", - "group": "Symbols", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🔯": { - "name": "dotted six-pointed star", - "slug": "dotted_six_pointed_star", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🪯": { - "name": "khanda", - "slug": "khanda", - "group": "Symbols", - "emoji_version": "15.0", - "unicode_version": "15.0", - "skin_tone_support": false - }, - "♈": { - "name": "Aries", - "slug": "aries", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "♉": { - "name": "Taurus", - "slug": "taurus", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "♊": { - "name": "Gemini", - "slug": "gemini", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "♋": { - "name": "Cancer", - "slug": "cancer", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "♌": { - "name": "Leo", - "slug": "leo", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "♍": { - "name": "Virgo", - "slug": "virgo", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "♎": { - "name": "Libra", - "slug": "libra", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "♏": { - "name": "Scorpio", - "slug": "scorpio", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "♐": { - "name": "Sagittarius", - "slug": "sagittarius", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "♑": { - "name": "Capricorn", - "slug": "capricorn", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "♒": { - "name": "Aquarius", - "slug": "aquarius", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "♓": { - "name": "Pisces", - "slug": "pisces", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "⛎": { - "name": "Ophiuchus", - "slug": "ophiuchus", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🔀": { - "name": "shuffle tracks button", - "slug": "shuffle_tracks_button", - "group": "Symbols", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🔁": { - "name": "repeat button", - "slug": "repeat_button", - "group": "Symbols", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🔂": { - "name": "repeat single button", - "slug": "repeat_single_button", - "group": "Symbols", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "▶️": { - "name": "play button", - "slug": "play_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "⏩": { - "name": "fast-forward button", - "slug": "fast_forward_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "⏭️": { - "name": "next track button", - "slug": "next_track_button", - "group": "Symbols", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "⏯️": { - "name": "play or pause button", - "slug": "play_or_pause_button", - "group": "Symbols", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "◀️": { - "name": "reverse button", - "slug": "reverse_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "⏪": { - "name": "fast reverse button", - "slug": "fast_reverse_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "⏮️": { - "name": "last track button", - "slug": "last_track_button", - "group": "Symbols", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🔼": { - "name": "upwards button", - "slug": "upwards_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "⏫": { - "name": "fast up button", - "slug": "fast_up_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🔽": { - "name": "downwards button", - "slug": "downwards_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "⏬": { - "name": "fast down button", - "slug": "fast_down_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "⏸️": { - "name": "pause button", - "slug": "pause_button", - "group": "Symbols", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "⏹️": { - "name": "stop button", - "slug": "stop_button", - "group": "Symbols", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "⏺️": { - "name": "record button", - "slug": "record_button", - "group": "Symbols", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "⏏️": { - "name": "eject button", - "slug": "eject_button", - "group": "Symbols", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🎦": { - "name": "cinema", - "slug": "cinema", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🔅": { - "name": "dim button", - "slug": "dim_button", - "group": "Symbols", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🔆": { - "name": "bright button", - "slug": "bright_button", - "group": "Symbols", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "📶": { - "name": "antenna bars", - "slug": "antenna_bars", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🛜": { - "name": "wireless", - "slug": "wireless", - "group": "Symbols", - "emoji_version": "15.0", - "unicode_version": "15.0", - "skin_tone_support": false - }, - "📳": { - "name": "vibration mode", - "slug": "vibration_mode", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📴": { - "name": "mobile phone off", - "slug": "mobile_phone_off", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "♀️": { - "name": "female sign", - "slug": "female_sign", - "group": "Symbols", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": false - }, - "♂️": { - "name": "male sign", - "slug": "male_sign", - "group": "Symbols", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": false - }, - "⚧️": { - "name": "transgender symbol", - "slug": "transgender_symbol", - "group": "Symbols", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "✖️": { - "name": "multiply", - "slug": "multiply", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "➕": { - "name": "plus", - "slug": "plus", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "➖": { - "name": "minus", - "slug": "minus", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "➗": { - "name": "divide", - "slug": "divide", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🟰": { - "name": "heavy equals sign", - "slug": "heavy_equals_sign", - "group": "Symbols", - "emoji_version": "14.0", - "unicode_version": "14.0", - "skin_tone_support": false - }, - "♾️": { - "name": "infinity", - "slug": "infinity", - "group": "Symbols", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "‼️": { - "name": "double exclamation mark", - "slug": "double_exclamation_mark", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "⁉️": { - "name": "exclamation question mark", - "slug": "exclamation_question_mark", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "❓": { - "name": "red question mark", - "slug": "red_question_mark", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "❔": { - "name": "white question mark", - "slug": "white_question_mark", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "❕": { - "name": "white exclamation mark", - "slug": "white_exclamation_mark", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "❗": { - "name": "red exclamation mark", - "slug": "red_exclamation_mark", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "〰️": { - "name": "wavy dash", - "slug": "wavy_dash", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "💱": { - "name": "currency exchange", - "slug": "currency_exchange", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "💲": { - "name": "heavy dollar sign", - "slug": "heavy_dollar_sign", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "⚕️": { - "name": "medical symbol", - "slug": "medical_symbol", - "group": "Symbols", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": false - }, - "♻️": { - "name": "recycling symbol", - "slug": "recycling_symbol", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "⚜️": { - "name": "fleur-de-lis", - "slug": "fleur_de_lis", - "group": "Symbols", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🔱": { - "name": "trident emblem", - "slug": "trident_emblem", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "📛": { - "name": "name badge", - "slug": "name_badge", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🔰": { - "name": "Japanese symbol for beginner", - "slug": "japanese_symbol_for_beginner", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "⭕": { - "name": "hollow red circle", - "slug": "hollow_red_circle", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "✅": { - "name": "check mark button", - "slug": "check_mark_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "☑️": { - "name": "check box with check", - "slug": "check_box_with_check", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "✔️": { - "name": "check mark", - "slug": "check_mark", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "❌": { - "name": "cross mark", - "slug": "cross_mark", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "❎": { - "name": "cross mark button", - "slug": "cross_mark_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "➰": { - "name": "curly loop", - "slug": "curly_loop", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "➿": { - "name": "double curly loop", - "slug": "double_curly_loop", - "group": "Symbols", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "〽️": { - "name": "part alternation mark", - "slug": "part_alternation_mark", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "✳️": { - "name": "eight-spoked asterisk", - "slug": "eight_spoked_asterisk", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "✴️": { - "name": "eight-pointed star", - "slug": "eight_pointed_star", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "❇️": { - "name": "sparkle", - "slug": "sparkle", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "©️": { - "name": "copyright", - "slug": "copyright", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "®️": { - "name": "registered", - "slug": "registered", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "™️": { - "name": "trade mark", - "slug": "trade_mark", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "#️⃣": { - "name": "keycap #", - "slug": "keycap_number_sign", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "*️⃣": { - "name": "keycap *", - "slug": "keycap_asterisk", - "group": "Symbols", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "0️⃣": { - "name": "keycap 0", - "slug": "keycap_0", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "1️⃣": { - "name": "keycap 1", - "slug": "keycap_1", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "2️⃣": { - "name": "keycap 2", - "slug": "keycap_2", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "3️⃣": { - "name": "keycap 3", - "slug": "keycap_3", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "4️⃣": { - "name": "keycap 4", - "slug": "keycap_4", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "5️⃣": { - "name": "keycap 5", - "slug": "keycap_5", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "6️⃣": { - "name": "keycap 6", - "slug": "keycap_6", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "7️⃣": { - "name": "keycap 7", - "slug": "keycap_7", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "8️⃣": { - "name": "keycap 8", - "slug": "keycap_8", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "9️⃣": { - "name": "keycap 9", - "slug": "keycap_9", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🔟": { - "name": "keycap 10", - "slug": "keycap_10", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🔠": { - "name": "input latin uppercase", - "slug": "input_latin_uppercase", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🔡": { - "name": "input latin lowercase", - "slug": "input_latin_lowercase", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🔢": { - "name": "input numbers", - "slug": "input_numbers", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🔣": { - "name": "input symbols", - "slug": "input_symbols", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🔤": { - "name": "input latin letters", - "slug": "input_latin_letters", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🅰️": { - "name": "A button (blood type)", - "slug": "a_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🆎": { - "name": "AB button (blood type)", - "slug": "ab_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🅱️": { - "name": "B button (blood type)", - "slug": "b_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🆑": { - "name": "CL button", - "slug": "cl_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🆒": { - "name": "COOL button", - "slug": "cool_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🆓": { - "name": "FREE button", - "slug": "free_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "ℹ️": { - "name": "information", - "slug": "information", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🆔": { - "name": "ID button", - "slug": "id_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "Ⓜ️": { - "name": "circled M", - "slug": "circled_m", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🆕": { - "name": "NEW button", - "slug": "new_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🆖": { - "name": "NG button", - "slug": "ng_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🅾️": { - "name": "O button (blood type)", - "slug": "o_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🆗": { - "name": "OK button", - "slug": "ok_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🅿️": { - "name": "P button", - "slug": "p_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🆘": { - "name": "SOS button", - "slug": "sos_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🆙": { - "name": "UP! button", - "slug": "up_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🆚": { - "name": "VS button", - "slug": "vs_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🈁": { - "name": "Japanese “here” button", - "slug": "japanese_here_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🈂️": { - "name": "Japanese “service charge” button", - "slug": "japanese_service_charge_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🈷️": { - "name": "Japanese “monthly amount” button", - "slug": "japanese_monthly_amount_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🈶": { - "name": "Japanese “not free of charge” button", - "slug": "japanese_not_free_of_charge_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🈯": { - "name": "Japanese “reserved” button", - "slug": "japanese_reserved_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🉐": { - "name": "Japanese “bargain” button", - "slug": "japanese_bargain_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🈹": { - "name": "Japanese “discount” button", - "slug": "japanese_discount_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🈚": { - "name": "Japanese “free of charge” button", - "slug": "japanese_free_of_charge_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🈲": { - "name": "Japanese “prohibited” button", - "slug": "japanese_prohibited_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🉑": { - "name": "Japanese “acceptable” button", - "slug": "japanese_acceptable_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🈸": { - "name": "Japanese “application” button", - "slug": "japanese_application_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🈴": { - "name": "Japanese “passing grade” button", - "slug": "japanese_passing_grade_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🈳": { - "name": "Japanese “vacancy” button", - "slug": "japanese_vacancy_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "㊗️": { - "name": "Japanese “congratulations” button", - "slug": "japanese_congratulations_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "㊙️": { - "name": "Japanese “secret” button", - "slug": "japanese_secret_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🈺": { - "name": "Japanese “open for business” button", - "slug": "japanese_open_for_business_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🈵": { - "name": "Japanese “no vacancy” button", - "slug": "japanese_no_vacancy_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🔴": { - "name": "red circle", - "slug": "red_circle", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🟠": { - "name": "orange circle", - "slug": "orange_circle", - "group": "Symbols", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🟡": { - "name": "yellow circle", - "slug": "yellow_circle", - "group": "Symbols", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🟢": { - "name": "green circle", - "slug": "green_circle", - "group": "Symbols", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🔵": { - "name": "blue circle", - "slug": "blue_circle", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🟣": { - "name": "purple circle", - "slug": "purple_circle", - "group": "Symbols", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🟤": { - "name": "brown circle", - "slug": "brown_circle", - "group": "Symbols", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "⚫": { - "name": "black circle", - "slug": "black_circle", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "⚪": { - "name": "white circle", - "slug": "white_circle", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🟥": { - "name": "red square", - "slug": "red_square", - "group": "Symbols", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🟧": { - "name": "orange square", - "slug": "orange_square", - "group": "Symbols", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🟨": { - "name": "yellow square", - "slug": "yellow_square", - "group": "Symbols", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🟩": { - "name": "green square", - "slug": "green_square", - "group": "Symbols", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🟦": { - "name": "blue square", - "slug": "blue_square", - "group": "Symbols", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🟪": { - "name": "purple square", - "slug": "purple_square", - "group": "Symbols", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "🟫": { - "name": "brown square", - "slug": "brown_square", - "group": "Symbols", - "emoji_version": "12.0", - "unicode_version": "12.0", - "skin_tone_support": false - }, - "⬛": { - "name": "black large square", - "slug": "black_large_square", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "⬜": { - "name": "white large square", - "slug": "white_large_square", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "◼️": { - "name": "black medium square", - "slug": "black_medium_square", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "◻️": { - "name": "white medium square", - "slug": "white_medium_square", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "◾": { - "name": "black medium-small square", - "slug": "black_medium_small_square", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "◽": { - "name": "white medium-small square", - "slug": "white_medium_small_square", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "▪️": { - "name": "black small square", - "slug": "black_small_square", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "▫️": { - "name": "white small square", - "slug": "white_small_square", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🔶": { - "name": "large orange diamond", - "slug": "large_orange_diamond", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🔷": { - "name": "large blue diamond", - "slug": "large_blue_diamond", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🔸": { - "name": "small orange diamond", - "slug": "small_orange_diamond", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🔹": { - "name": "small blue diamond", - "slug": "small_blue_diamond", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🔺": { - "name": "red triangle pointed up", - "slug": "red_triangle_pointed_up", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🔻": { - "name": "red triangle pointed down", - "slug": "red_triangle_pointed_down", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "💠": { - "name": "diamond with a dot", - "slug": "diamond_with_a_dot", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🔘": { - "name": "radio button", - "slug": "radio_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🔳": { - "name": "white square button", - "slug": "white_square_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🔲": { - "name": "black square button", - "slug": "black_square_button", - "group": "Symbols", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🏁": { - "name": "chequered flag", - "slug": "chequered_flag", - "group": "Flags", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🚩": { - "name": "triangular flag", - "slug": "triangular_flag", - "group": "Flags", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🎌": { - "name": "crossed flags", - "slug": "crossed_flags", - "group": "Flags", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🏴": { - "name": "black flag", - "slug": "black_flag", - "group": "Flags", - "emoji_version": "1.0", - "unicode_version": "1.0", - "skin_tone_support": false - }, - "🏳️": { - "name": "white flag", - "slug": "white_flag", - "group": "Flags", - "emoji_version": "0.7", - "unicode_version": "0.7", - "skin_tone_support": false - }, - "🏳️‍🌈": { - "name": "rainbow flag", - "slug": "rainbow_flag", - "group": "Flags", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": false - }, - "🏳️‍⚧️": { - "name": "transgender flag", - "slug": "transgender_flag", - "group": "Flags", - "emoji_version": "13.0", - "unicode_version": "13.0", - "skin_tone_support": false - }, - "🏴‍☠️": { - "name": "pirate flag", - "slug": "pirate_flag", - "group": "Flags", - "emoji_version": "11.0", - "unicode_version": "11.0", - "skin_tone_support": false - }, - "🇦🇨": { - "name": "flag Ascension Island", - "slug": "flag_ascension_island", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇦🇩": { - "name": "flag Andorra", - "slug": "flag_andorra", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇦🇪": { - "name": "flag United Arab Emirates", - "slug": "flag_united_arab_emirates", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇦🇫": { - "name": "flag Afghanistan", - "slug": "flag_afghanistan", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇦🇬": { - "name": "flag Antigua & Barbuda", - "slug": "flag_antigua_barbuda", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇦🇮": { - "name": "flag Anguilla", - "slug": "flag_anguilla", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇦🇱": { - "name": "flag Albania", - "slug": "flag_albania", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇦🇲": { - "name": "flag Armenia", - "slug": "flag_armenia", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇦🇴": { - "name": "flag Angola", - "slug": "flag_angola", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇦🇶": { - "name": "flag Antarctica", - "slug": "flag_antarctica", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇦🇷": { - "name": "flag Argentina", - "slug": "flag_argentina", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇦🇸": { - "name": "flag American Samoa", - "slug": "flag_american_samoa", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇦🇹": { - "name": "flag Austria", - "slug": "flag_austria", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇦🇺": { - "name": "flag Australia", - "slug": "flag_australia", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇦🇼": { - "name": "flag Aruba", - "slug": "flag_aruba", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇦🇽": { - "name": "flag Åland Islands", - "slug": "flag_aland_islands", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇦🇿": { - "name": "flag Azerbaijan", - "slug": "flag_azerbaijan", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇧🇦": { - "name": "flag Bosnia & Herzegovina", - "slug": "flag_bosnia_herzegovina", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇧🇧": { - "name": "flag Barbados", - "slug": "flag_barbados", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇧🇩": { - "name": "flag Bangladesh", - "slug": "flag_bangladesh", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇧🇪": { - "name": "flag Belgium", - "slug": "flag_belgium", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇧🇫": { - "name": "flag Burkina Faso", - "slug": "flag_burkina_faso", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇧🇬": { - "name": "flag Bulgaria", - "slug": "flag_bulgaria", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇧🇭": { - "name": "flag Bahrain", - "slug": "flag_bahrain", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇧🇮": { - "name": "flag Burundi", - "slug": "flag_burundi", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇧🇯": { - "name": "flag Benin", - "slug": "flag_benin", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇧🇱": { - "name": "flag St. Barthélemy", - "slug": "flag_st_barthelemy", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇧🇲": { - "name": "flag Bermuda", - "slug": "flag_bermuda", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇧🇳": { - "name": "flag Brunei", - "slug": "flag_brunei", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇧🇴": { - "name": "flag Bolivia", - "slug": "flag_bolivia", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇧🇶": { - "name": "flag Caribbean Netherlands", - "slug": "flag_caribbean_netherlands", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇧🇷": { - "name": "flag Brazil", - "slug": "flag_brazil", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇧🇸": { - "name": "flag Bahamas", - "slug": "flag_bahamas", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇧🇹": { - "name": "flag Bhutan", - "slug": "flag_bhutan", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇧🇻": { - "name": "flag Bouvet Island", - "slug": "flag_bouvet_island", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇧🇼": { - "name": "flag Botswana", - "slug": "flag_botswana", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇧🇾": { - "name": "flag Belarus", - "slug": "flag_belarus", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇧🇿": { - "name": "flag Belize", - "slug": "flag_belize", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇨🇦": { - "name": "flag Canada", - "slug": "flag_canada", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇨🇨": { - "name": "flag Cocos (Keeling) Islands", - "slug": "flag_cocos_islands", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇨🇩": { - "name": "flag Congo - Kinshasa", - "slug": "flag_congo_kinshasa", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇨🇫": { - "name": "flag Central African Republic", - "slug": "flag_central_african_republic", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇨🇬": { - "name": "flag Congo - Brazzaville", - "slug": "flag_congo_brazzaville", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇨🇭": { - "name": "flag Switzerland", - "slug": "flag_switzerland", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇨🇮": { - "name": "flag Côte d’Ivoire", - "slug": "flag_cote_d_ivoire", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇨🇰": { - "name": "flag Cook Islands", - "slug": "flag_cook_islands", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇨🇱": { - "name": "flag Chile", - "slug": "flag_chile", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇨🇲": { - "name": "flag Cameroon", - "slug": "flag_cameroon", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇨🇳": { - "name": "flag China", - "slug": "flag_china", - "group": "Flags", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🇨🇴": { - "name": "flag Colombia", - "slug": "flag_colombia", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇨🇵": { - "name": "flag Clipperton Island", - "slug": "flag_clipperton_island", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇨🇷": { - "name": "flag Costa Rica", - "slug": "flag_costa_rica", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇨🇺": { - "name": "flag Cuba", - "slug": "flag_cuba", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇨🇻": { - "name": "flag Cape Verde", - "slug": "flag_cape_verde", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇨🇼": { - "name": "flag Curaçao", - "slug": "flag_curacao", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇨🇽": { - "name": "flag Christmas Island", - "slug": "flag_christmas_island", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇨🇾": { - "name": "flag Cyprus", - "slug": "flag_cyprus", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇨🇿": { - "name": "flag Czechia", - "slug": "flag_czechia", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇩🇪": { - "name": "flag Germany", - "slug": "flag_germany", - "group": "Flags", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🇩🇬": { - "name": "flag Diego Garcia", - "slug": "flag_diego_garcia", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇩🇯": { - "name": "flag Djibouti", - "slug": "flag_djibouti", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇩🇰": { - "name": "flag Denmark", - "slug": "flag_denmark", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇩🇲": { - "name": "flag Dominica", - "slug": "flag_dominica", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇩🇴": { - "name": "flag Dominican Republic", - "slug": "flag_dominican_republic", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇩🇿": { - "name": "flag Algeria", - "slug": "flag_algeria", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇪🇦": { - "name": "flag Ceuta & Melilla", - "slug": "flag_ceuta_melilla", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇪🇨": { - "name": "flag Ecuador", - "slug": "flag_ecuador", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇪🇪": { - "name": "flag Estonia", - "slug": "flag_estonia", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇪🇬": { - "name": "flag Egypt", - "slug": "flag_egypt", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇪🇭": { - "name": "flag Western Sahara", - "slug": "flag_western_sahara", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇪🇷": { - "name": "flag Eritrea", - "slug": "flag_eritrea", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇪🇸": { - "name": "flag Spain", - "slug": "flag_spain", - "group": "Flags", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🇪🇹": { - "name": "flag Ethiopia", - "slug": "flag_ethiopia", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇪🇺": { - "name": "flag European Union", - "slug": "flag_european_union", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇫🇮": { - "name": "flag Finland", - "slug": "flag_finland", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇫🇯": { - "name": "flag Fiji", - "slug": "flag_fiji", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇫🇰": { - "name": "flag Falkland Islands", - "slug": "flag_falkland_islands", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇫🇲": { - "name": "flag Micronesia", - "slug": "flag_micronesia", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇫🇴": { - "name": "flag Faroe Islands", - "slug": "flag_faroe_islands", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇫🇷": { - "name": "flag France", - "slug": "flag_france", - "group": "Flags", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🇬🇦": { - "name": "flag Gabon", - "slug": "flag_gabon", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇬🇧": { - "name": "flag United Kingdom", - "slug": "flag_united_kingdom", - "group": "Flags", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🇬🇩": { - "name": "flag Grenada", - "slug": "flag_grenada", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇬🇪": { - "name": "flag Georgia", - "slug": "flag_georgia", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇬🇫": { - "name": "flag French Guiana", - "slug": "flag_french_guiana", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇬🇬": { - "name": "flag Guernsey", - "slug": "flag_guernsey", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇬🇭": { - "name": "flag Ghana", - "slug": "flag_ghana", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇬🇮": { - "name": "flag Gibraltar", - "slug": "flag_gibraltar", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇬🇱": { - "name": "flag Greenland", - "slug": "flag_greenland", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇬🇲": { - "name": "flag Gambia", - "slug": "flag_gambia", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇬🇳": { - "name": "flag Guinea", - "slug": "flag_guinea", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇬🇵": { - "name": "flag Guadeloupe", - "slug": "flag_guadeloupe", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇬🇶": { - "name": "flag Equatorial Guinea", - "slug": "flag_equatorial_guinea", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇬🇷": { - "name": "flag Greece", - "slug": "flag_greece", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇬🇸": { - "name": "flag South Georgia & South Sandwich Islands", - "slug": "flag_south_georgia_south_sandwich_islands", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇬🇹": { - "name": "flag Guatemala", - "slug": "flag_guatemala", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇬🇺": { - "name": "flag Guam", - "slug": "flag_guam", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇬🇼": { - "name": "flag Guinea-Bissau", - "slug": "flag_guinea_bissau", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇬🇾": { - "name": "flag Guyana", - "slug": "flag_guyana", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇭🇰": { - "name": "flag Hong Kong SAR China", - "slug": "flag_hong_kong_sar_china", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇭🇲": { - "name": "flag Heard & McDonald Islands", - "slug": "flag_heard_mcdonald_islands", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇭🇳": { - "name": "flag Honduras", - "slug": "flag_honduras", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇭🇷": { - "name": "flag Croatia", - "slug": "flag_croatia", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇭🇹": { - "name": "flag Haiti", - "slug": "flag_haiti", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇭🇺": { - "name": "flag Hungary", - "slug": "flag_hungary", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇮🇨": { - "name": "flag Canary Islands", - "slug": "flag_canary_islands", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇮🇩": { - "name": "flag Indonesia", - "slug": "flag_indonesia", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇮🇪": { - "name": "flag Ireland", - "slug": "flag_ireland", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇮🇱": { - "name": "flag Israel", - "slug": "flag_israel", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇮🇲": { - "name": "flag Isle of Man", - "slug": "flag_isle_of_man", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇮🇳": { - "name": "flag India", - "slug": "flag_india", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇮🇴": { - "name": "flag British Indian Ocean Territory", - "slug": "flag_british_indian_ocean_territory", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇮🇶": { - "name": "flag Iraq", - "slug": "flag_iraq", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇮🇷": { - "name": "flag Iran", - "slug": "flag_iran", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇮🇸": { - "name": "flag Iceland", - "slug": "flag_iceland", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇮🇹": { - "name": "flag Italy", - "slug": "flag_italy", - "group": "Flags", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🇯🇪": { - "name": "flag Jersey", - "slug": "flag_jersey", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇯🇲": { - "name": "flag Jamaica", - "slug": "flag_jamaica", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇯🇴": { - "name": "flag Jordan", - "slug": "flag_jordan", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇯🇵": { - "name": "flag Japan", - "slug": "flag_japan", - "group": "Flags", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🇰🇪": { - "name": "flag Kenya", - "slug": "flag_kenya", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇰🇬": { - "name": "flag Kyrgyzstan", - "slug": "flag_kyrgyzstan", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇰🇭": { - "name": "flag Cambodia", - "slug": "flag_cambodia", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇰🇮": { - "name": "flag Kiribati", - "slug": "flag_kiribati", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇰🇲": { - "name": "flag Comoros", - "slug": "flag_comoros", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇰🇳": { - "name": "flag St. Kitts & Nevis", - "slug": "flag_st_kitts_nevis", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇰🇵": { - "name": "flag North Korea", - "slug": "flag_north_korea", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇰🇷": { - "name": "flag South Korea", - "slug": "flag_south_korea", - "group": "Flags", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🇰🇼": { - "name": "flag Kuwait", - "slug": "flag_kuwait", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇰🇾": { - "name": "flag Cayman Islands", - "slug": "flag_cayman_islands", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇰🇿": { - "name": "flag Kazakhstan", - "slug": "flag_kazakhstan", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇱🇦": { - "name": "flag Laos", - "slug": "flag_laos", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇱🇧": { - "name": "flag Lebanon", - "slug": "flag_lebanon", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇱🇨": { - "name": "flag St. Lucia", - "slug": "flag_st_lucia", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇱🇮": { - "name": "flag Liechtenstein", - "slug": "flag_liechtenstein", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇱🇰": { - "name": "flag Sri Lanka", - "slug": "flag_sri_lanka", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇱🇷": { - "name": "flag Liberia", - "slug": "flag_liberia", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇱🇸": { - "name": "flag Lesotho", - "slug": "flag_lesotho", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇱🇹": { - "name": "flag Lithuania", - "slug": "flag_lithuania", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇱🇺": { - "name": "flag Luxembourg", - "slug": "flag_luxembourg", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇱🇻": { - "name": "flag Latvia", - "slug": "flag_latvia", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇱🇾": { - "name": "flag Libya", - "slug": "flag_libya", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇲🇦": { - "name": "flag Morocco", - "slug": "flag_morocco", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇲🇨": { - "name": "flag Monaco", - "slug": "flag_monaco", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇲🇩": { - "name": "flag Moldova", - "slug": "flag_moldova", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇲🇪": { - "name": "flag Montenegro", - "slug": "flag_montenegro", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇲🇫": { - "name": "flag St. Martin", - "slug": "flag_st_martin", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇲🇬": { - "name": "flag Madagascar", - "slug": "flag_madagascar", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇲🇭": { - "name": "flag Marshall Islands", - "slug": "flag_marshall_islands", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇲🇰": { - "name": "flag North Macedonia", - "slug": "flag_north_macedonia", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇲🇱": { - "name": "flag Mali", - "slug": "flag_mali", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇲🇲": { - "name": "flag Myanmar (Burma)", - "slug": "flag_myanmar", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇲🇳": { - "name": "flag Mongolia", - "slug": "flag_mongolia", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇲🇴": { - "name": "flag Macao SAR China", - "slug": "flag_macao_sar_china", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇲🇵": { - "name": "flag Northern Mariana Islands", - "slug": "flag_northern_mariana_islands", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇲🇶": { - "name": "flag Martinique", - "slug": "flag_martinique", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇲🇷": { - "name": "flag Mauritania", - "slug": "flag_mauritania", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇲🇸": { - "name": "flag Montserrat", - "slug": "flag_montserrat", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇲🇹": { - "name": "flag Malta", - "slug": "flag_malta", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇲🇺": { - "name": "flag Mauritius", - "slug": "flag_mauritius", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇲🇻": { - "name": "flag Maldives", - "slug": "flag_maldives", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇲🇼": { - "name": "flag Malawi", - "slug": "flag_malawi", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇲🇽": { - "name": "flag Mexico", - "slug": "flag_mexico", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇲🇾": { - "name": "flag Malaysia", - "slug": "flag_malaysia", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇲🇿": { - "name": "flag Mozambique", - "slug": "flag_mozambique", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇳🇦": { - "name": "flag Namibia", - "slug": "flag_namibia", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇳🇨": { - "name": "flag New Caledonia", - "slug": "flag_new_caledonia", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇳🇪": { - "name": "flag Niger", - "slug": "flag_niger", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇳🇫": { - "name": "flag Norfolk Island", - "slug": "flag_norfolk_island", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇳🇬": { - "name": "flag Nigeria", - "slug": "flag_nigeria", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇳🇮": { - "name": "flag Nicaragua", - "slug": "flag_nicaragua", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇳🇱": { - "name": "flag Netherlands", - "slug": "flag_netherlands", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇳🇴": { - "name": "flag Norway", - "slug": "flag_norway", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇳🇵": { - "name": "flag Nepal", - "slug": "flag_nepal", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇳🇷": { - "name": "flag Nauru", - "slug": "flag_nauru", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇳🇺": { - "name": "flag Niue", - "slug": "flag_niue", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇳🇿": { - "name": "flag New Zealand", - "slug": "flag_new_zealand", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇴🇲": { - "name": "flag Oman", - "slug": "flag_oman", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇵🇦": { - "name": "flag Panama", - "slug": "flag_panama", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇵🇪": { - "name": "flag Peru", - "slug": "flag_peru", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇵🇫": { - "name": "flag French Polynesia", - "slug": "flag_french_polynesia", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇵🇬": { - "name": "flag Papua New Guinea", - "slug": "flag_papua_new_guinea", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇵🇭": { - "name": "flag Philippines", - "slug": "flag_philippines", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇵🇰": { - "name": "flag Pakistan", - "slug": "flag_pakistan", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇵🇱": { - "name": "flag Poland", - "slug": "flag_poland", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇵🇲": { - "name": "flag St. Pierre & Miquelon", - "slug": "flag_st_pierre_miquelon", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇵🇳": { - "name": "flag Pitcairn Islands", - "slug": "flag_pitcairn_islands", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇵🇷": { - "name": "flag Puerto Rico", - "slug": "flag_puerto_rico", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇵🇸": { - "name": "flag Palestinian Territories", - "slug": "flag_palestinian_territories", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇵🇹": { - "name": "flag Portugal", - "slug": "flag_portugal", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇵🇼": { - "name": "flag Palau", - "slug": "flag_palau", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇵🇾": { - "name": "flag Paraguay", - "slug": "flag_paraguay", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇶🇦": { - "name": "flag Qatar", - "slug": "flag_qatar", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇷🇪": { - "name": "flag Réunion", - "slug": "flag_reunion", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇷🇴": { - "name": "flag Romania", - "slug": "flag_romania", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇷🇸": { - "name": "flag Serbia", - "slug": "flag_serbia", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇷🇺": { - "name": "flag Russia", - "slug": "flag_russia", - "group": "Flags", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🇷🇼": { - "name": "flag Rwanda", - "slug": "flag_rwanda", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇸🇦": { - "name": "flag Saudi Arabia", - "slug": "flag_saudi_arabia", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇸🇧": { - "name": "flag Solomon Islands", - "slug": "flag_solomon_islands", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇸🇨": { - "name": "flag Seychelles", - "slug": "flag_seychelles", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇸🇩": { - "name": "flag Sudan", - "slug": "flag_sudan", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇸🇪": { - "name": "flag Sweden", - "slug": "flag_sweden", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇸🇬": { - "name": "flag Singapore", - "slug": "flag_singapore", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇸🇭": { - "name": "flag St. Helena", - "slug": "flag_st_helena", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇸🇮": { - "name": "flag Slovenia", - "slug": "flag_slovenia", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇸🇯": { - "name": "flag Svalbard & Jan Mayen", - "slug": "flag_svalbard_jan_mayen", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇸🇰": { - "name": "flag Slovakia", - "slug": "flag_slovakia", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇸🇱": { - "name": "flag Sierra Leone", - "slug": "flag_sierra_leone", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇸🇲": { - "name": "flag San Marino", - "slug": "flag_san_marino", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇸🇳": { - "name": "flag Senegal", - "slug": "flag_senegal", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇸🇴": { - "name": "flag Somalia", - "slug": "flag_somalia", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇸🇷": { - "name": "flag Suriname", - "slug": "flag_suriname", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇸🇸": { - "name": "flag South Sudan", - "slug": "flag_south_sudan", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇸🇹": { - "name": "flag São Tomé & Príncipe", - "slug": "flag_sao_tome_principe", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇸🇻": { - "name": "flag El Salvador", - "slug": "flag_el_salvador", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇸🇽": { - "name": "flag Sint Maarten", - "slug": "flag_sint_maarten", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇸🇾": { - "name": "flag Syria", - "slug": "flag_syria", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇸🇿": { - "name": "flag Eswatini", - "slug": "flag_eswatini", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇹🇦": { - "name": "flag Tristan da Cunha", - "slug": "flag_tristan_da_cunha", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇹🇨": { - "name": "flag Turks & Caicos Islands", - "slug": "flag_turks_caicos_islands", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇹🇩": { - "name": "flag Chad", - "slug": "flag_chad", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇹🇫": { - "name": "flag French Southern Territories", - "slug": "flag_french_southern_territories", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇹🇬": { - "name": "flag Togo", - "slug": "flag_togo", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇹🇭": { - "name": "flag Thailand", - "slug": "flag_thailand", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇹🇯": { - "name": "flag Tajikistan", - "slug": "flag_tajikistan", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇹🇰": { - "name": "flag Tokelau", - "slug": "flag_tokelau", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇹🇱": { - "name": "flag Timor-Leste", - "slug": "flag_timor_leste", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇹🇲": { - "name": "flag Turkmenistan", - "slug": "flag_turkmenistan", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇹🇳": { - "name": "flag Tunisia", - "slug": "flag_tunisia", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇹🇴": { - "name": "flag Tonga", - "slug": "flag_tonga", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇹🇷": { - "name": "flag Türkiye", - "slug": "flag_turkiye", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇹🇹": { - "name": "flag Trinidad & Tobago", - "slug": "flag_trinidad_tobago", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇹🇻": { - "name": "flag Tuvalu", - "slug": "flag_tuvalu", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇹🇼": { - "name": "flag Taiwan", - "slug": "flag_taiwan", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇹🇿": { - "name": "flag Tanzania", - "slug": "flag_tanzania", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇺🇦": { - "name": "flag Ukraine", - "slug": "flag_ukraine", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇺🇬": { - "name": "flag Uganda", - "slug": "flag_uganda", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇺🇲": { - "name": "flag U.S. Outlying Islands", - "slug": "flag_u_s_outlying_islands", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇺🇳": { - "name": "flag United Nations", - "slug": "flag_united_nations", - "group": "Flags", - "emoji_version": "4.0", - "unicode_version": "4.0", - "skin_tone_support": false - }, - "🇺🇸": { - "name": "flag United States", - "slug": "flag_united_states", - "group": "Flags", - "emoji_version": "0.6", - "unicode_version": "0.6", - "skin_tone_support": false - }, - "🇺🇾": { - "name": "flag Uruguay", - "slug": "flag_uruguay", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇺🇿": { - "name": "flag Uzbekistan", - "slug": "flag_uzbekistan", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇻🇦": { - "name": "flag Vatican City", - "slug": "flag_vatican_city", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇻🇨": { - "name": "flag St. Vincent & Grenadines", - "slug": "flag_st_vincent_grenadines", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇻🇪": { - "name": "flag Venezuela", - "slug": "flag_venezuela", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇻🇬": { - "name": "flag British Virgin Islands", - "slug": "flag_british_virgin_islands", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇻🇮": { - "name": "flag U.S. Virgin Islands", - "slug": "flag_u_s_virgin_islands", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇻🇳": { - "name": "flag Vietnam", - "slug": "flag_vietnam", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇻🇺": { - "name": "flag Vanuatu", - "slug": "flag_vanuatu", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇼🇫": { - "name": "flag Wallis & Futuna", - "slug": "flag_wallis_futuna", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇼🇸": { - "name": "flag Samoa", - "slug": "flag_samoa", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇽🇰": { - "name": "flag Kosovo", - "slug": "flag_kosovo", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇾🇪": { - "name": "flag Yemen", - "slug": "flag_yemen", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇾🇹": { - "name": "flag Mayotte", - "slug": "flag_mayotte", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇿🇦": { - "name": "flag South Africa", - "slug": "flag_south_africa", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇿🇲": { - "name": "flag Zambia", - "slug": "flag_zambia", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🇿🇼": { - "name": "flag Zimbabwe", - "slug": "flag_zimbabwe", - "group": "Flags", - "emoji_version": "2.0", - "unicode_version": "2.0", - "skin_tone_support": false - }, - "🏴󠁧󠁢󠁥󠁮󠁧󠁿": { - "name": "flag England", - "slug": "flag_england", - "group": "Flags", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🏴󠁧󠁢󠁳󠁣󠁴󠁿": { - "name": "flag Scotland", - "slug": "flag_scotland", - "group": "Flags", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - }, - "🏴󠁧󠁢󠁷󠁬󠁳󠁿": { - "name": "flag Wales", - "slug": "flag_wales", - "group": "Flags", - "emoji_version": "5.0", - "unicode_version": "5.0", - "skin_tone_support": false - } -} diff --git a/config/assets/modus.json b/config/assets/modus.json deleted file mode 100644 index 530c99d..0000000 --- a/config/assets/modus.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "commands": { - "/wp": "launcher.open(\"wallpapers\")", - "/em": "launcher.open(\"emoji\")", - "/ch": "launcher.open(\"cliphist\")", - "/kb": "launcher.open(\"kanban\")", - "/bt": "launcher.open(\"bluetooth\")", - "/sh": "launcher.open(\"sh\")", - "/pw": "launcher.open(\"power\")", - "/nw": "launcher.open(\"wifi\")", - "/cl": "launcher.open(\"calendar\")", - "/tl": "launcher.open(\"tools\")", - "/tx": "launcher.open(\"tmux\")", - "/ws": "launcher.open(\"window-switcher\")" - }, - "pinned_apps": [] -} \ No newline at end of file diff --git a/config/config.json b/config/config.json new file mode 100644 index 0000000..95a331b --- /dev/null +++ b/config/config.json @@ -0,0 +1,63 @@ +{ + "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, + "terminal_command": "kitty -e", + "corners_visible": false, + "dock_theme": "Pills", + "notif_pos": "Top", + "bar_workspace_visible": true, + "bar_metrics_visible": true, + "bar_date_time_visible": true, + "bar_battery_visible": true, + "bar_controls_visible": true, + "bar_indicators_visible": true, + "bar_systray_visible": true, + "metrics_visible": { + "cpu": true, + "ram": true, + "disk": false, + "swap": true, + "gpu": false + }, + "metrics_disks": [] +} \ No newline at end of file diff --git a/config/config.py b/config/config.py index 44b7424..f86acbd 100644 --- a/config/config.py +++ b/config/config.py @@ -1,1246 +1,35 @@ import os -import shutil -import json import sys from pathlib import Path -import random -import subprocess -import toml -import gi -gi.require_version("Gtk", "3.0") -from gi.repository import Gtk -# Fabric imports +def _configure_sys_path_for_direct_execution(): + """ + Ajusta sys.path si este script se ejecuta directamente, + para asegurar que las importaciones relativas dentro del paquete 'config' funcionen. + Esto permite ejecutar `python config/config.py` desde cualquier directorio. + """ + if __name__ == "__main__": + current_file_dir = Path(__file__).resolve().parent + project_root = current_file_dir.parent + + if str(project_root) not in sys.path: + sys.path.insert(0, str(project_root)) + +_configure_sys_path_for_direct_execution() + +import shutil + from fabric import Application -from fabric.widgets.window import Window -from fabric.widgets.box import Box -from fabric.widgets.label import Label -from fabric.widgets.button import Button -from fabric.widgets.entry import Entry -from fabric.widgets.scrolledwindow import ScrolledWindow -from fabric.widgets.image import Image -from fabric.widgets.stack import Stack -from fabric.widgets.scale import Scale -from fabric.utils import exec_shell_command, exec_shell_command_async -# Assuming data.py exists in the same directory or is accessible via sys.path -# If data.py is in ./config/data.py relative to this script's original location: -try: - # Adjust path relative to the *original* location if needed - sys.path.insert(0, str(Path(__file__).resolve().parent / "../config")) - from data import ( - APP_NAME, - APP_NAME_CAP, - WALLPAPERS_DIR_DEFAULT, - ) -except ImportError as e: - print(f"Error importing data constants: {e}") - # Provide fallback defaults if import fails - APP_NAME = "modus" - APP_NAME_CAP = "Modus" - CONFIG_DIR = "~/Modus" - HOME_DIR = "~" - WALLPAPERS_DIR_DEFAULT = "~/Pictures/wallpapers" - -SOURCE_STRING = f""" -# {APP_NAME_CAP} -source = ~/{APP_NAME_CAP}/config/hypr/{APP_NAME}.conf -""" - -DEFAULTS = { - "prefix_restart": "SHIFT ALT", - "suffix_restart": "T", - "prefix_bluetooth": "SHIFT ALT", - "suffix_bluetooth": "B", - "prefix_emoji": "SUPER", - "suffix_emoji": "E", - "prefix_cliphist": "SUPER", - "suffix_cliphist": "V", - "prefix_kanban": "SUPER", - "suffix_kanban": "T", - "prefix_toolbox": "SUPER", - "suffix_toolbox": "S", - "prefix_walls": "SUPER", - "suffix_walls": "W", - "prefix_launcher": "SUPER", - "suffix_launcher": "D", - "prefix_power": "SUPER", - "suffix_power": "X", - "prefix_tmux": "SUPER", - "suffix_tmux": "Z", - "prefix_notifications": "SUPER", - "suffix_notifications": "N", - "prefix_window_switcher": "ALT", - "suffix_window_switcher": "W", - "prefix_restart_inspector": "SUPER SHIFT", - "suffix_restart_inspector": "T", - "vertical": False, - "vertical_right_align": True, - "bottom_bar": False, - "centered_bar": False, - "terminal_command": "kitty -e", - "wallpapers_dir": WALLPAPERS_DIR_DEFAULT, - "dock_enabled": True, - "dock_always_occluded": False, # Added default - "dock_icon_size": 28, - "osd_enabled": True, # Added default - # Defaults for bar components (assuming True initially) - "bar_button_app_visible": True, - "bar_tray_visible": True, - "bar_workspaces_visible": True, - "bar_metrics_visible": True, - "bar_language_visible": True, - "bar_date_time_visible": True, - "bar_updates_visible": True, - "bar_indicators_visible": True, - "corners_visible": True, # Added default for corners visibility - "bar_metrics_disks": ["/"], - # Add metric visibility defaults - "metrics_visible": { - "cpu": True, - "ram": True, - "disk": True, - "swap": True, - }, -} - -bind_vars = DEFAULTS.copy() - - -def deep_update(target: dict, update: dict) -> dict: - """ - Recursively update a nested dictionary with values from another dictionary. - """ - for key, value in update.items(): - if isinstance(value, dict): - target[key] = deep_update(target.get(key, {}), value) - else: - target[key] = value - return target - - -def ensure_matugen_config(): - """ - Ensure that the matugen configuration file exists and is updated - 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", - ], - "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}, - }, - }, - "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()' &", - }, - }, - } - - config_path = os.path.expanduser("~/.config/matugen/config.toml") - os.makedirs(os.path.dirname(config_path), exist_ok=True) - - # Load any existing configuration - existing_config = {} - if os.path.exists(config_path): - with open(config_path, "r") as f: - existing_config = toml.load(f) - # Backup existing configuration - shutil.copyfile(config_path, config_path + ".bak") - - # Merge configurations - merged_config = deep_update(existing_config, expected_config) - with open(config_path, "w") as f: - toml.dump(merged_config, f) - - # Check if colors.css exists in styles directory - colors_css_path = os.path.expanduser(f"~/{APP_NAME_CAP}/styles/colors.css") - if not os.path.exists(colors_css_path): - # Get the wallpapers directory from bind_vars or use default - wallpapers_dir = bind_vars.get("wallpapers_dir", WALLPAPERS_DIR_DEFAULT) - wallpapers_dir = os.path.expanduser(wallpapers_dir) - - # Get list of image files in the directory - image_extensions = (".png", ".jpg", ".jpeg", ".gif", ".bmp") - image_files = [ - f - for f in os.listdir(wallpapers_dir) - if f.lower().endswith(image_extensions) - ] - - if image_files: - # Select a random image - random_image = random.choice(image_files) - image_path = os.path.join(wallpapers_dir, random_image) - # Apply the random wallpaper using matugen - os.system(f"matugen image {image_path}") - else: - # If no images found in user's directory, try the default directory - default_dir = os.path.expanduser(f"~/{APP_NAME_CAP}/assets/wallpaper/") - default_images = [ - f - for f in os.listdir(default_dir) - if f.lower().endswith(image_extensions) - ] - - random_image = random.choice(default_images) - image_path = os.path.join(default_dir, random_image) - os.system(f"matugen image {image_path}") - - -def load_bind_vars(): - """ - Load saved key binding variables from JSON, if available. - """ - config_json = os.path.expanduser(f"~/{APP_NAME_CAP}/config/assets/config.json") - if os.path.exists(config_json): - try: - with open(config_json, "r") as f: - saved_vars = json.load(f) - # Update defaults with saved values, ensuring all keys exist - for key in DEFAULTS: - if key in saved_vars: - bind_vars[key] = saved_vars[key] - else: - bind_vars[key] = DEFAULTS[ - key - ] # Use default if missing in saved - # Add any new keys from DEFAULTS not present in saved_vars - for key in saved_vars: - if key not in bind_vars: - bind_vars[key] = saved_vars[ - key - ] # Keep saved if it's not in new defaults (less likely) - for vis_key in ["metrics_visible"]: - if vis_key in DEFAULTS: - if vis_key not in bind_vars or not isinstance( - bind_vars[vis_key], dict - ): - bind_vars[vis_key] = DEFAULTS[vis_key].copy() - else: - for m in DEFAULTS[vis_key]: - if m not in bind_vars[vis_key]: - bind_vars[vis_key][m] = DEFAULTS[vis_key][m] - except json.JSONDecodeError: - print(f"Warning: Could not decode JSON from {config_json}. Using defaults.") - bind_vars.update(DEFAULTS) # Ensure defaults on error - except Exception as e: - print(f"Error loading config from {config_json}: {e}. Using defaults.") - bind_vars.update(DEFAULTS) # Ensure defaults on error - else: - # Ensure defaults are set if file doesn't exist - bind_vars.update(DEFAULTS) - - -def generate_hyprconf() -> str: - """ - Generate the Hypr configuration string using the current bind_vars. - """ - home = os.path.expanduser("~") - return f"""# Generated by {APP_NAME_CAP} - DO NOT EDIT MANUALLY -exec-once = python {home}/{APP_NAME_CAP}/main.py -exec = pgrep -x "hypridle" > /dev/null || hypridle -exec = swww-daemon -exec-once = wl-paste --type text --watch cliphist store -exec-once = wl-paste --type image --watch cliphist store - -$fabricSend = fabric-cli exec {APP_NAME} -$scriptsDir = {home}/{APP_NAME_CAP}/config/scripts/ - -# Key Bindings -bind = {bind_vars["prefix_restart"]}, {bind_vars["suffix_restart"]}, exec, killall {APP_NAME}; python {home}/{APP_NAME_CAP}/main.py # Reload {APP_NAME_CAP} -bind = {bind_vars["prefix_bluetooth"]}, {bind_vars["suffix_bluetooth"]}, exec, $fabricSend 'launcher.open("bluetooth")' # Bluetooth -bind = {bind_vars["prefix_notifications"]}, {bind_vars["suffix_notifications"]}, exec, $fabricSend 'launcher.open("notification-center")' # Notifications -bind = {bind_vars["prefix_emoji"]}, {bind_vars["suffix_emoji"]}, exec, $fabricSend 'launcher.open("emoji")' # Emoji -bind = {bind_vars["prefix_cliphist"]}, {bind_vars["suffix_cliphist"]}, exec, $fabricSend 'launcher.open("cliphist")' # Clipboard History -bind = {bind_vars["prefix_kanban"]}, {bind_vars["suffix_kanban"]}, exec, $fabricSend 'launcher.open("kanban")' # Kanban -bind = {bind_vars["prefix_tmux"]}, {bind_vars["suffix_tmux"]}, exec, $fabricSend 'launcher.open("tmux")' # Tmux Manager -bind = {bind_vars["prefix_toolbox"]}, {bind_vars["suffix_toolbox"]}, exec, $fabricSend 'launcher.open("tools")' # Toolbox -bind = {bind_vars["prefix_walls"]}, {bind_vars["suffix_walls"]}, exec, $fabricSend 'launcher.open("wallpapers")' # Wallpaper Selector -bind = {bind_vars["prefix_launcher"]}, {bind_vars["suffix_launcher"]}, exec, $fabricSend 'launcher.open("launcher")' # App Launcher -bind = {bind_vars["prefix_window_switcher"]}, {bind_vars["suffix_window_switcher"]}, exec, $fabricSend 'launcher.open("window-switcher")' # Window Switcher -bind = {bind_vars["prefix_power"]}, {bind_vars["suffix_power"]}, exec, $fabricSend 'launcher.open("power")' # Power Menu -bind = {bind_vars["prefix_restart_inspector"]}, {bind_vars["suffix_restart_inspector"]}, exec, killall {APP_NAME}; GTK_DEBUG=interactive python {home}/{APP_NAME_CAP}/main.py # Restart with inspector - -# Wallpapers directory: {bind_vars["wallpapers_dir"]} - -# Source color scheme -source = {home}/{APP_NAME_CAP}/config/hypr/colors.conf - -general {{ - col.active_border = 0xff$primary - col.inactive_border = 0xff$surface - gaps_in = 2 - gaps_out = 4 - border_size = 2 - layout = dwindle -}} - -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 = true - # Animation curves - bezier = linear, 0, 0, 1, 1 - bezier = md3_standard, 0.2, 0, 0, 1 - bezier = md3_decel, 0.05, 0.7, 0.1, 1 - bezier = md3_accel, 0.3, 0, 0.8, 0.15 - bezier = overshot, 0.05, 0.9, 0.1, 1.1 - bezier = crazyshot, 0.1, 1.5, 0.76, 0.92 - bezier = hyprnostretch, 0.05, 0.9, 0.1, 1.0 - bezier = menu_decel, 0.1, 1, 0, 1 - bezier = menu_accel, 0.38, 0.04, 1, 0.07 - bezier = easeInOutCirc, 0.85, 0, 0.15, 1 - bezier = easeOutCirc, 0, 0.55, 0.45, 1 - bezier = easeOutExpo, 0.16, 1, 0.3, 1 - bezier = softAcDecel, 0.26, 0.26, 0.15, 1 - bezier = md2, 0.4, 0, 0.2, 1 - - animation = windows, 1, 3, md3_decel, popin 60% - animation = windowsIn, 1, 3, md3_decel, popin 60% - animation = windowsOut, 1, 3, md3_accel, popin 60% - animation = border, 1, 10, default - animation = fade, 1, 3, md3_decel - animation = layers, 1, 2, md3_decel, slide - animation = layersIn, 1, 3, menu_decel, slide - animation = layersOut, 1, 1.6, menu_accel, slide - animation = fadeLayersIn, 1, 2, menu_decel - animation = fadeLayersOut, 1, 4.5, menu_accel - animation = workspaces, 1, 7, menu_decel, slide - animation = specialWorkspace, 1, 3, md3_decel, slidevert -}} -""" - - -def backup_and_replace(src: str, dest: str, config_name: str): - """ - Backup the existing configuration file and replace it with a new one. - """ - if os.path.exists(dest): - backup_path = dest + ".bak" - shutil.copy(dest, backup_path) - print(f"{config_name} config backed up to {backup_path}") - shutil.copy(src, dest) - print(f"{config_name} config replaced from {src}") - - -class HyprConfGUI(Window): - def __init__(self, show_lock_checkbox: bool, show_idle_checkbox: bool, **kwargs): - super().__init__( - title="Modus Settings", - name="modus-settings-widnow", - size=(650, 550), - **kwargs, - ) - self.set_resizable(False) - - self.show_lock_checkbox = show_lock_checkbox - self.show_idle_checkbox = show_idle_checkbox - - # Overall vertical box to hold the main content and bottom buttons - root_box = Box(orientation="v", spacing=10, style="margin: 10px;") - self.add(root_box) - - # Main horizontal box for switcher and stack - main_content_box = Box(orientation="h", spacing=6, v_expand=True, h_expand=True) - root_box.add(main_content_box) - - # --- Tab Control --- - self.tab_stack = Stack( - transition_type="slide-up-down", # Change transition for vertical feel - transition_duration=250, - v_expand=True, - h_expand=True, - ) - - # Create tabs and add to stack - self.key_bindings_tab_content = self.create_key_bindings_tab() - self.appearance_tab_content = self.create_appearance_tab() - self.system_tab_content = self.create_system_tab() - self.about_tab_content = self.create_about_tab() - - self.tab_stack.add_titled( - self.key_bindings_tab_content, "key_bindings", "Key Bindings" - ) - self.tab_stack.add_titled( - self.appearance_tab_content, "appearance", "Appearance" - ) - self.tab_stack.add_titled(self.system_tab_content, "system", "System") - self.tab_stack.add_titled( - self.about_tab_content, "about", "About" - ) # Add About tab to stack - - # Use Gtk.StackSwitcher vertically on the left - tab_switcher = Gtk.StackSwitcher() - tab_switcher.set_stack(self.tab_stack) - tab_switcher.set_orientation( - Gtk.Orientation.VERTICAL - ) # Set vertical orientation - # Optional: Adjust alignment if needed - # tab_switcher.set_valign(Gtk.Align.START) - - # Add switcher to the left of the main content box - main_content_box.add(tab_switcher) - - # Add stack to the right of the main content box - main_content_box.add(self.tab_stack) - - # --- Bottom Buttons --- - button_box = Box(orientation="h", spacing=10, h_align="end") - - reset_btn = Button(label="Reset to Defaults", on_clicked=self.on_reset) - button_box.add(reset_btn) - - # Add Close button back - close_btn = Button(label="Close", on_clicked=self.on_close) - button_box.add(close_btn) - - accept_btn = Button(label="Apply & Reload", on_clicked=self.on_accept) - button_box.add(accept_btn) - - # Add button box to the bottom of the root box - root_box.add(button_box) - - def create_key_bindings_tab(self): - """Create tab for key bindings configuration using Fabric widgets and Gtk.Grid.""" - scrolled_window = ScrolledWindow( - h_scrollbar_policy="never", - v_scrollbar_policy="automatic", - h_expand=True, - v_expand=True, - ) - # Remove fixed height constraints to allow stack to fill space - scrolled_window.set_min_content_height(300) - scrolled_window.set_max_content_height(300) - - # Main container with padding - main_vbox = Box(orientation="v", spacing=10, style="margin: 15px;") - scrolled_window.add(main_vbox) - - # Create a grid for key bindings - keybind_grid = Gtk.Grid() - keybind_grid.set_column_spacing(10) - keybind_grid.set_row_spacing(8) - keybind_grid.set_margin_start(5) - keybind_grid.set_margin_end(5) - keybind_grid.set_margin_top(5) - keybind_grid.set_margin_bottom(5) - - # Header Row - action_label = Label( - markup="Action", h_align="start", style="margin-bottom: 5px;" - ) - modifier_label = Label( - markup="Modifier", h_align="start", style="margin-bottom: 5px;" - ) - separator_label = Label( - label="+", h_align="center", style="margin-bottom: 5px;" - ) - key_label = Label( - markup="Key", h_align="start", style="margin-bottom: 5px;" - ) - - keybind_grid.attach(action_label, 0, 0, 1, 1) - keybind_grid.attach(modifier_label, 1, 0, 1, 1) - keybind_grid.attach(separator_label, 2, 0, 1, 1) - keybind_grid.attach(key_label, 3, 0, 1, 1) - - self.entries = [] - bindings = [ - (f"Reload {APP_NAME_CAP}", "prefix_restart", "suffix_restart"), - ("Bluetooth", "prefix_bluetooth", "suffix_bluetooth"), - ("Notifications", "prefix_notifications", "suffix_notifications"), - ("Kanban", "prefix_kanban", "suffix_kanban"), - ("App Launcher", "prefix_launcher", "suffix_launcher"), - ("Tmux", "prefix_tmux", "suffix_tmux"), - ("Toolbox", "prefix_toolbox", "suffix_toolbox"), - ("Wallpapers", "prefix_walls", "suffix_walls"), - ("Emoji Picker", "prefix_emoji", "suffix_emoji"), - ("Clipboard History", "prefix_cliphist", "suffix_cliphist"), - ("Window Switcher", "prefix_window_switcher", "suffix_window_switcher"), - ("Power Menu", "prefix_power", "suffix_power"), - ( - "Restart with inspector", - "prefix_restart_inspector", - "suffix_restart_inspector", - ), - ] - - # Populate the grid with entries - for i, (label_text, prefix_key, suffix_key) in enumerate(bindings): - row = i + 1 # Start at row 1 after headers - - # Action label - binding_label = Label(label=label_text, h_align="start") - keybind_grid.attach(binding_label, 0, row, 1, 1) - - # Prefix entry - prefix_entry = Entry(text=bind_vars[prefix_key]) - keybind_grid.attach(prefix_entry, 1, row, 1, 1) - - # Plus separator - plus_label = Label(label="+", h_align="center") - keybind_grid.attach(plus_label, 2, row, 1, 1) - - # Suffix entry - suffix_entry = Entry(text=bind_vars[suffix_key]) - keybind_grid.attach(suffix_entry, 3, row, 1, 1) - - self.entries.append((prefix_key, suffix_key, prefix_entry, suffix_entry)) - - main_vbox.add(keybind_grid) - return scrolled_window - - def create_appearance_tab(self): - """Create tab for appearance settings using Fabric widgets and Gtk.Grid.""" - scrolled_window = ScrolledWindow( - h_scrollbar_policy="never", - v_scrollbar_policy="automatic", - h_expand=True, - v_expand=True, - ) - scrolled_window.set_min_content_height(300) - scrolled_window.set_max_content_height(300) - - # Main container with padding - vbox = Box(orientation="v", spacing=15, style="margin: 15px;") - scrolled_window.add(vbox) - - # === WALLPAPERS SECTION === - wall_header = Label(markup="Wallpapers", h_align="start") - vbox.add(wall_header) - - wall_box = Box(orientation="h", spacing=10, style="margin-left: 10px;") - vbox.add(wall_box) - - wall_label = Label(label="Directory:", h_align="start", v_align="center") - wall_box.add(wall_label) - - self.wall_dir_chooser = Gtk.FileChooserButton( - title="Select a folder", action=Gtk.FileChooserAction.SELECT_FOLDER - ) - self.wall_dir_chooser.set_tooltip_text( - "Select the directory containing your wallpaper images" - ) - self.wall_dir_chooser.set_filename(bind_vars["wallpapers_dir"]) - self.wall_dir_chooser.set_size_request(180, -1) - wall_box.add(self.wall_dir_chooser) - - # === LAYOUT OPTIONS SECTION === - layout_header = Label(markup="Layout Options", h_align="start") - vbox.add(layout_header) - - # Create a grid for layout options - layout_grid = Gtk.Grid() - layout_grid.set_column_spacing(20) - layout_grid.set_row_spacing(15) - layout_grid.set_margin_start(10) - layout_grid.set_margin_top(5) - layout_grid.set_margin_bottom(10) - vbox.add(layout_grid) - - # Column 1 (Left) - # Vertical Layout - vertical_label = Label( - label="Vertical Layout", h_align="start", v_align="center" - ) - layout_grid.attach(vertical_label, 0, 0, 1, 1) - - vertical_switch_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - vertical_switch_container.set_halign(Gtk.Align.START) - vertical_switch_container.set_valign(Gtk.Align.CENTER) - - self.vertical_switch = Gtk.Switch() - self.vertical_switch.set_active(bind_vars.get("vertical", False)) - self.vertical_switch.connect("notify::active", self.on_vertical_changed) - vertical_switch_container.add(self.vertical_switch) - - layout_grid.attach(vertical_switch_container, 1, 0, 1, 1) - - # Vertical Right Align (new) - vertical_right_label = Label( - label="Right Side (Vertical)", h_align="start", v_align="center" - ) - layout_grid.attach(vertical_right_label, 0, 1, 1, 1) - - vertical_right_switch_container = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL - ) - vertical_right_switch_container.set_halign(Gtk.Align.START) - vertical_right_switch_container.set_valign(Gtk.Align.CENTER) - - self.vertical_right_switch = Gtk.Switch() - self.vertical_right_switch.set_active( - bind_vars.get("vertical_right_align", True) - ) - self.vertical_right_switch.set_sensitive(self.vertical_switch.get_active()) - vertical_right_switch_container.add(self.vertical_right_switch) - layout_grid.attach(vertical_right_switch_container, 1, 1, 1, 1) - - # Bottom Layout - bottom_bar_label = Label( - label="Bottom Layout", h_align="start", v_align="center" - ) - layout_grid.attach(bottom_bar_label, 0, 2, 1, 1) - - bottom_bar_switch_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - bottom_bar_switch_container.set_halign(Gtk.Align.START) - bottom_bar_switch_container.set_valign(Gtk.Align.CENTER) - - self.bottom_bar_switch = Gtk.Switch() - self.bottom_bar_switch.set_active(bind_vars.get("bottom_bar", False)) - self.bottom_bar_switch.connect("notify::active", self.on_bottom_bar_changed) - bottom_bar_switch_container.add(self.bottom_bar_switch) - layout_grid.attach(bottom_bar_switch_container, 1, 2, 1, 1) - - # Dock Options - dock_label = Label(label="Show Dock", h_align="start", v_align="center") - layout_grid.attach(dock_label, 0, 3, 1, 1) - - dock_switch_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - dock_switch_container.set_halign(Gtk.Align.START) - dock_switch_container.set_valign(Gtk.Align.CENTER) - - self.dock_switch = Gtk.Switch() - self.dock_switch.set_active(bind_vars.get("dock_enabled", True)) - self.dock_switch.connect("notify::active", self.on_dock_enabled_changed) - dock_switch_container.add(self.dock_switch) - layout_grid.attach(dock_switch_container, 1, 3, 1, 1) - - # Column 2 (Right) - # Centered Bar - centered_label = Label( - label="Centered Bar (Vertical Only)", h_align="start", v_align="center" - ) - layout_grid.attach(centered_label, 2, 0, 1, 1) - - centered_switch_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - centered_switch_container.set_halign(Gtk.Align.START) - centered_switch_container.set_valign(Gtk.Align.CENTER) - - self.centered_switch = Gtk.Switch() - self.centered_switch.set_active(bind_vars.get("centered_bar", False)) - self.centered_switch.set_sensitive(self.vertical_switch.get_active()) - centered_switch_container.add(self.centered_switch) - layout_grid.attach(centered_switch_container, 3, 0, 1, 1) - - # Dock Hover - dock_hover_label = Label( - label="Show Dock Only on Hover", h_align="start", v_align="center" - ) - layout_grid.attach(dock_hover_label, 2, 1, 1, 1) - - dock_hover_switch_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - dock_hover_switch_container.set_halign(Gtk.Align.START) - dock_hover_switch_container.set_valign(Gtk.Align.CENTER) - - self.dock_hover_switch = Gtk.Switch() - self.dock_hover_switch.set_active(bind_vars.get("dock_always_occluded", False)) - self.dock_hover_switch.set_sensitive(self.dock_switch.get_active()) - dock_hover_switch_container.add(self.dock_hover_switch) - layout_grid.attach(dock_hover_switch_container, 3, 1, 1, 1) - - # Show OSD - osd_label = Label(label="Show OSD", h_align="start", v_align="center") - layout_grid.attach(osd_label, 2, 2, 1, 1) - - osd_hover_switch_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - osd_hover_switch_container.set_halign(Gtk.Align.START) - osd_hover_switch_container.set_valign(Gtk.Align.CENTER) - - self.osd_switch = Gtk.Switch() - self.osd_switch.set_active(bind_vars.get("osd_enabled", True)) - osd_hover_switch_container.add(self.osd_switch) - layout_grid.attach(osd_hover_switch_container, 3, 2, 1, 1) - - # Dock Icon Size (Full width) - dock_size_label = Label( - label="Dock Icon Size", h_align="start", v_align="center" - ) - layout_grid.attach(dock_size_label, 0, 4, 1, 1) - - self.dock_size_scale = Scale( - min_value=16, - max_value=48, - value=bind_vars.get("dock_icon_size", 28), - increments=(2, 4), - draw_value=True, - value_position="right", - digits=0, - h_expand=True, - ) - layout_grid.attach(self.dock_size_scale, 1, 4, 3, 1) - - # === BAR COMPONENTS SECTION === - components_header = Label(markup="Bar Components", h_align="start") - vbox.add(components_header) - - # Create a grid for bar components - components_grid = Gtk.Grid() - components_grid.set_column_spacing(15) - components_grid.set_row_spacing(8) - components_grid.set_margin_start(10) - components_grid.set_margin_top(5) - vbox.add(components_grid) - - self.component_switches = {} - component_display_names = { - "button_app": "App Launcher Button", - "tray": "System Tray", - "workspaces": "Workspaces", - "metrics": "System Metrics", - "language": "Language Indicator", - "date_time": "Date & Time", - "updates": "Update Indicator", - "indicators": "System Indicators", - } - - # Add corners visibility switch - self.corners_switch = Gtk.Switch() - self.corners_switch.set_active(bind_vars.get("corners_visible", True)) - - # Calculate number of rows needed (we'll use 2 columns) - num_components = len(component_display_names) + 1 # +1 for corners - rows_per_column = (num_components + 1) // 2 # Ceiling division - - # First add corners to the top of first column - corners_label = Label( - label="Rounded Corners", h_align="start", v_align="center" - ) - components_grid.attach(corners_label, 0, 0, 1, 1) - - switch_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - switch_container.set_halign(Gtk.Align.START) - switch_container.set_valign(Gtk.Align.CENTER) - switch_container.add(self.corners_switch) - components_grid.attach(switch_container, 1, 0, 1, 1) - - # Add components to grid in two columns - for i, (component_name, display_name) in enumerate( - component_display_names.items() - ): - # Determine position: first half in column 0, second half in column 2 - # Start at row 1 to account for corners at row 0 - row = (i + 1) % rows_per_column # +1 to start after corners - col = 0 if i < (rows_per_column - 1) else 2 # Adjust column calculation - - component_label = Label( - label=display_name, h_align="start", v_align="center" - ) - components_grid.attach(component_label, col, row, 1, 1) - - # Container for switch to prevent stretching - switch_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - switch_container.set_halign(Gtk.Align.START) - switch_container.set_valign(Gtk.Align.CENTER) - - component_switch = Gtk.Switch() - config_key = f"bar_{component_name}_visible" - component_switch.set_active(bind_vars.get(config_key, True)) - switch_container.add(component_switch) - - components_grid.attach(switch_container, col + 1, row, 1, 1) - - self.component_switches[component_name] = component_switch - - return scrolled_window - - def create_system_tab(self): - """Create tab for system configurations using Fabric widgets and Gtk.Grid.""" - scrolled_window = ScrolledWindow( - h_scrollbar_policy="never", - v_scrollbar_policy="automatic", - h_expand=True, - v_expand=True, - ) - # Remove fixed height constraints - scrolled_window.set_min_content_height(300) - scrolled_window.set_max_content_height(300) - - # Main container with padding - vbox = Box(orientation="v", spacing=15, style="margin: 15px;") - scrolled_window.add(vbox) - - # Create a grid for system settings - system_grid = Gtk.Grid() - system_grid.set_column_spacing(20) - system_grid.set_row_spacing(10) - system_grid.set_margin_bottom(15) - vbox.add(system_grid) - - # === TERMINAL SETTINGS === - terminal_header = Label(markup="Terminal Settings", h_align="start") - system_grid.attach(terminal_header, 0, 0, 2, 1) - - terminal_label = Label(label="Command:", h_align="start", v_align="center") - system_grid.attach(terminal_label, 0, 1, 1, 1) - - self.terminal_entry = Entry( - text=bind_vars["terminal_command"], - tooltip_text="Command used to launch terminal apps (e.g., 'kitty -e')", - h_expand=True, - ) - system_grid.attach(self.terminal_entry, 1, 1, 1, 1) - - hint_label = Label( - markup="Examples: 'kitty -e', 'alacritty -e', 'foot -e'", - h_align="start", - ) - system_grid.attach(hint_label, 0, 2, 2, 1) - - # === HYPRLAND INTEGRATION === - hypr_header = Label(markup="Hyprland Integration", h_align="start") - system_grid.attach(hypr_header, 2, 0, 2, 1) - - row = 1 - - # Hyprland locks and idle settings - self.lock_switch = None - if self.show_lock_checkbox: - lock_label = Label( - label="Replace Hyprlock config", h_align="start", v_align="center" - ) - system_grid.attach(lock_label, 2, row, 1, 1) - - # Container for switch to prevent stretching - lock_switch_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - lock_switch_container.set_halign(Gtk.Align.START) - lock_switch_container.set_valign(Gtk.Align.CENTER) - - self.lock_switch = Gtk.Switch() - self.lock_switch.set_tooltip_text( - "Replace Hyprlock configuration with Ax-Shell's custom config" - ) - lock_switch_container.add(self.lock_switch) - - system_grid.attach(lock_switch_container, 3, row, 1, 1) - row += 1 - - self.idle_switch = None - if self.show_idle_checkbox: - idle_label = Label( - label="Replace Hypridle config", h_align="start", v_align="center" - ) - system_grid.attach(idle_label, 2, row, 1, 1) - - # Container for switch to prevent stretching - idle_switch_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - idle_switch_container.set_halign(Gtk.Align.START) - idle_switch_container.set_valign(Gtk.Align.CENTER) - - self.idle_switch = Gtk.Switch() - self.idle_switch.set_tooltip_text( - "Replace Hypridle configuration with Ax-Shell's custom config" - ) - idle_switch_container.add(self.idle_switch) - - system_grid.attach(idle_switch_container, 3, row, 1, 1) - row += 1 - - if self.show_lock_checkbox or self.show_idle_checkbox: - note_label = Label( - markup="Existing configs will be backed up", - h_align="start", - ) - system_grid.attach(note_label, 2, row, 2, 1) - - # --- System Metrics Options (moved from appearance tab) --- - metrics_header = Label(markup="System Metrics Options", h_align="start") - vbox.add(metrics_header) - - # Metrics visibility toggles - metrics_grid = Gtk.Grid() - metrics_grid.set_column_spacing(15) - metrics_grid.set_row_spacing(8) - metrics_grid.set_margin_start(10) - metrics_grid.set_margin_top(5) - vbox.add(metrics_grid) - - self.metrics_switches = {} - - metric_names = { - "cpu": "CPU", - "ram": "RAM", - "disk": "Disk", - "swap": "Swap", - } - - # metrics toggles - metrics_grid.attach(Label(label="Show in Metrics", h_align="start"), 2, 0, 1, 1) - for i, (key, label) in enumerate(metric_names.items()): - switch = Gtk.Switch() - switch.set_active(bind_vars.get("metrics_visible", {}).get(key, True)) - self.metrics_switches[key] = switch - metrics_grid.attach(Label(label=label, h_align="start"), 2, i + 1, 1, 1) - metrics_grid.attach(switch, 4, i + 1, 1, 1) - - # Enforce minimum 4 enabled metrics - def enforce_minimum_metrics(switch_dict): - enabled = [k for k, s in switch_dict.items() if s.get_active()] - for k, s in switch_dict.items(): - if len(enabled) <= 4 and s.get_active(): - s.set_sensitive(False) - else: - s.set_sensitive(True) - - def on_metric_toggle(switch, gparam, which): - enforce_minimum_metrics(self.metrics_switches) - - for k, s in self.metrics_switches.items(): - s.connect("notify::active", on_metric_toggle, k) - enforce_minimum_metrics(self.metrics_switches) - - # Disk directories - disks_label = Label(label="Disk directories", h_align="start", v_align="center") - vbox.add(disks_label) - - self.disk_entries = Box(orientation="v", spacing=8, h_align="start") - - def create_disk_edit(path): - bar = Box(orientation="h", spacing=10, h_align="start") - entry = Entry(text=path, h_expand=True) - bar.add(entry) - x = Button(label="X", on_clicked=lambda _: self.disk_entries.remove(bar)) - bar.add(x) - self.disk_entries.add(bar) - - vbox.add(self.disk_entries) - for path in bind_vars.get("bar_metrics_disks"): - create_disk_edit(path) - add_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - add_container.set_halign(Gtk.Align.START) - add_container.set_valign(Gtk.Align.CENTER) - add = Button(label="Add new disk", on_clicked=lambda _: create_disk_edit("/")) - add_container.add(add) - vbox.add(add_container) - - return scrolled_window - - def create_about_tab(self): - """Create an About tab with project info, repo link, and Ko-Fi button.""" - vbox = Box(orientation="v", spacing=18, style="margin: 30px;") - # Project title - vbox.add( - Label( - markup=f"{APP_NAME_CAP}", - h_align="start", - style="font-size: 1.5em; margin-bottom: 8px;", - ) - ) - # Description - vbox.add( - Label( - label="A hackable shell for Hyprland, powered by Fabric.", - h_align="start", - style="margin-bottom: 12px;", - ) - ) - # Repo link - repo_box = Box(orientation="h", spacing=6, h_align="start") - repo_label = Label(label="GitHub:", h_align="start") - repo_link = Label() - repo_link.set_markup( - f'https://github.com/S4NKALP/Modus' - ) - repo_box.add(repo_label) - repo_box.add(repo_link) - vbox.add(repo_box) - - # Ko-Fi button - def on_kofi_clicked(_): - import webbrowser - - webbrowser.open("https://ko-fi.com/S4NKALP") - - kofi_btn = Button( - label="Support on Ko-Fi ❤️", - on_clicked=on_kofi_clicked, - tooltip_text="Support S4NKALP on Ko-Fi", - ) - kofi_btn.set_style("margin-top: 18px; min-width: 160px;") - vbox.add(kofi_btn) - # Spacer - vbox.add(Box(v_expand=True)) - return vbox - - def on_vertical_changed(self, switch, gparam): - """Callback for vertical switch.""" - is_active = switch.get_active() - self.centered_switch.set_sensitive(is_active) - self.vertical_right_switch.set_sensitive(is_active) - - if is_active: - # Disable bottom bar when vertical layout is enabled - bind_vars["bottom_bar"] = False - self.bottom_bar_switch.set_active(False) - - def on_dock_enabled_changed(self, switch, gparam): - """Callback for dock enabled switch.""" - is_active = switch.get_active() - self.dock_hover_switch.set_sensitive(is_active) - if not is_active: - self.dock_hover_switch.set_active(False) - - def on_bottom_bar_changed(self, switch, gparam): - """Handle bottom bar switch changes""" - bind_vars["bottom_bar"] = switch.get_active() - if switch.get_active(): - # Disable vertical layout - bind_vars["vertical"] = False - self.vertical_switch.set_active(False) - - # Only disable centered bar without turning it off - self.centered_switch.set_sensitive(False) - - # Disable right side option and reset it to False - self.vertical_right_switch.set_sensitive(False) - self.vertical_right_switch.set_active(False) - bind_vars["vertical_right_align"] = False - - def on_accept(self, widget): - """ - Save the configuration and update the necessary files without closing the window. - """ - for prefix_key, suffix_key, prefix_entry, suffix_entry in self.entries: - bind_vars[prefix_key] = prefix_entry.get_text() - bind_vars[suffix_key] = suffix_entry.get_text() - - bind_vars["wallpapers_dir"] = self.wall_dir_chooser.get_filename() - bind_vars["vertical"] = self.vertical_switch.get_active() - bind_vars["vertical_right_align"] = self.vertical_right_switch.get_active() - bind_vars["bottom_bar"] = self.bottom_bar_switch.get_active() - bind_vars["enabled_osd"] = self.osd_switch.get_active() - bind_vars["centered_bar"] = self.centered_switch.get_active() - bind_vars["dock_enabled"] = self.dock_switch.get_active() - bind_vars["dock_always_occluded"] = self.dock_hover_switch.get_active() - bind_vars["dock_icon_size"] = int(self.dock_size_scale.get_value()) - bind_vars["terminal_command"] = self.terminal_entry.get_text() - bind_vars["corners_visible"] = self.corners_switch.get_active() - - for component_name, switch in self.component_switches.items(): - config_key = f"bar_{component_name}_visible" - bind_vars[config_key] = switch.get_active() - - # Save metrics visibility - bind_vars["metrics_visible"] = { - k: s.get_active() for k, s in self.metrics_switches.items() - } - - bind_vars["bar_metrics_disks"] = [] - for entry in self.disk_entries.children: - bind_vars["bar_metrics_disks"].append(entry.children[0].get_text()) - - config_json = os.path.expanduser(f"~/{APP_NAME_CAP}/config/assets/config.json") - os.makedirs(os.path.dirname(config_json), exist_ok=True) - try: - with open(config_json, "w") as f: - json.dump(bind_vars, f, indent=4) - except Exception as e: - print(f"Error saving config.json: {e}") - - if self.lock_switch and self.lock_switch.get_active(): - src_lock = os.path.expanduser(f"~/{APP_NAME_CAP}/config/hypr/hyprlock.conf") - dest_lock = os.path.expanduser("~/.config/hypr/hyprlock.conf") - if os.path.exists(src_lock): - backup_and_replace(src_lock, dest_lock, "Hyprlock") - else: - print(f"Warning: Source hyprlock config not found at {src_lock}") - - if self.idle_switch and self.idle_switch.get_active(): - src_idle = os.path.expanduser(f"~/{APP_NAME_CAP}/config/hypr/hypridle.conf") - dest_idle = os.path.expanduser("~/.config/hypr/hypridle.conf") - if os.path.exists(src_idle): - backup_and_replace(src_idle, dest_idle, "Hypridle") - else: - print(f"Warning: Source hypridle config not found at {src_idle}") - - hyprland_config_path = os.path.expanduser("~/.config/hypr/hyprland.conf") - try: - needs_append = True - if os.path.exists(hyprland_config_path): - with open(hyprland_config_path, "r") as f: - content = f.read() - if SOURCE_STRING.strip() in content: - needs_append = False - else: - os.makedirs(os.path.dirname(hyprland_config_path), exist_ok=True) - - if needs_append: - with open(hyprland_config_path, "a") as f: - f.write("\n" + SOURCE_STRING) - print(f"Appended source string to {hyprland_config_path}") - - except Exception as e: - print(f"Error updating {hyprland_config_path}: {e}") - - start_config() - - # Restart Modus using fabric's async command executor - main_script_path = os.path.expanduser(f"~/{APP_NAME_CAP}/main.py") - kill_cmd = f"killall {APP_NAME}" - start_cmd = f"python {main_script_path}" - - try: - # Use fabric's helper to run the command asynchronously - exec_shell_command(kill_cmd) - exec_shell_command_async(start_cmd) - print(f"{APP_NAME_CAP} restart initiated.") - except Exception as e: - print(f"Error restarting {APP_NAME_CAP}: {e}") - - def on_reset(self, widget): - """ - Reset all settings to default values. Uses Gtk.MessageDialog. - """ - dialog = Gtk.MessageDialog( - transient_for=self.get_toplevel(), - flags=0, - message_type=Gtk.MessageType.QUESTION, - buttons=Gtk.ButtonsType.YES_NO, - text="Reset all settings to defaults?", - ) - dialog.format_secondary_text( - "This will reset all keybindings and appearance settings to their default values." - ) - response = dialog.run() - dialog.destroy() - - if response == Gtk.ResponseType.YES: - global bind_vars - bind_vars = DEFAULTS.copy() - - for prefix_key, suffix_key, prefix_entry, suffix_entry in self.entries: - prefix_entry.set_text(bind_vars[prefix_key]) - suffix_entry.set_text(bind_vars[suffix_key]) - - self.wall_dir_chooser.set_filename(bind_vars["wallpapers_dir"]) - self.vertical_switch.set_active(bind_vars.get("vertical", False)) - self.vertical_right_switch.set_active( - bind_vars.get("vertical_right_align", True) - ) - self.centered_switch.set_active(bind_vars.get("centered_bar", False)) - self.centered_switch.set_sensitive(self.vertical_switch.get_active()) - self.vertical_right_switch.set_sensitive(self.vertical_switch.get_active()) - self.dock_switch.set_active(bind_vars.get("dock_enabled", True)) - self.dock_hover_switch.set_active( - bind_vars.get("dock_always_occluded", False) - ) - self.dock_hover_switch.set_sensitive(self.dock_switch.get_active()) - self.dock_size_scale.value = bind_vars.get("dock_icon_size", 28) - self.terminal_entry.set_text(bind_vars["terminal_command"]) - self.osd_switch.set_active(bind_vars.get("osd_enabled", True)) - self.bottom_bar_switch.set_active(bind_vars.get("bottom_bar", False)) - - for component_name, switch in self.component_switches.items(): - config_key = f"bar_{component_name}_visible" - switch.set_active(bind_vars.get(config_key, True)) - - self.corners_switch.set_active(bind_vars.get("corners_visible", True)) - - # Reset metrics visibility - for k, s in self.metrics_switches.items(): - s.set_active(DEFAULTS["metrics_visible"][k]) - - # Reset disk entries - if True: - for i in self.disk_entries.children: - self.disk_entries.remove(i) - bar = Box(orientation="h", spacing=10, h_align="start") - entry = Entry(text="/", h_expand=True) - bar.add(entry) - x = Button( - label="X", on_clicked=lambda _: self.disk_entries.remove(bar) - ) - bar.add(x) - self.disk_entries.add(bar) - - if self.lock_switch: - self.lock_switch.set_active(False) - if self.idle_switch: - self.idle_switch.set_active(False) - - print("Settings reset to defaults.") - - def on_close(self, widget): - """Close the settings window.""" - if self.application: - self.application.quit() - else: - self.destroy() - - -def start_config(): - """ - Run final configuration steps: ensure necessary configs, write the hyprconf, and reload. - """ - ensure_matugen_config() - - hypr_config_dir = os.path.expanduser(f"~/{APP_NAME_CAP}/config/hypr/") - os.makedirs(hypr_config_dir, exist_ok=True) - hypr_conf_path = os.path.join(hypr_config_dir, f"{APP_NAME}.conf") - try: - with open(hypr_conf_path, "w") as f: - f.write(generate_hyprconf()) - print(f"Generated Hyprland config at {hypr_conf_path}") - except Exception as e: - print(f"Error writing Hyprland config: {e}") - - try: - subprocess.run(["hyprctl", "reload"], check=True, capture_output=True) - print("Hyprland configuration reloaded.") - except subprocess.CalledProcessError as e: - print(f"Error reloading Hyprland: {e}\nstderr: {e.stderr.decode()}") - except FileNotFoundError: - print("Error: hyprctl command not found. Cannot reload Hyprland.") - except Exception as e: - print(f"An unexpected error occurred during hyprctl reload: {e}") +if __name__ == "__main__" and (__package__ is None or __package__ == ''): + from config.data import APP_NAME, APP_NAME_CAP + from config.settings.gui import HyprConfGUI + from config.settings.utils import load_bind_vars +else: + from .data import APP_NAME, APP_NAME_CAP + from .settings.gui import HyprConfGUI + from .settings.utils import load_bind_vars def open_config(): @@ -1249,22 +38,22 @@ def open_config(): """ load_bind_vars() + show_lock_checkbox = True dest_lock = os.path.expanduser("~/.config/hypr/hyprlock.conf") src_lock = os.path.expanduser(f"~/{APP_NAME_CAP}/config/hypr/hyprlock.conf") - show_lock_checkbox = True if not os.path.exists(dest_lock) and os.path.exists(src_lock): try: os.makedirs(os.path.dirname(dest_lock), exist_ok=True) shutil.copy(src_lock, dest_lock) - show_lock_checkbox = False + show_lock_checkbox = False print(f"Copied default hyprlock config to {dest_lock}") except Exception as e: print(f"Error copying default hyprlock config: {e}") show_lock_checkbox = os.path.exists(src_lock) + show_idle_checkbox = True dest_idle = os.path.expanduser("~/.config/hypr/hypridle.conf") src_idle = os.path.expanduser(f"~/{APP_NAME_CAP}/config/hypr/hypridle.conf") - show_idle_checkbox = True if not os.path.exists(dest_idle) and os.path.exists(src_idle): try: os.makedirs(os.path.dirname(dest_idle), exist_ok=True) @@ -1280,7 +69,7 @@ def open_config(): show_lock_checkbox=show_lock_checkbox, show_idle_checkbox=show_idle_checkbox, application=app, - on_destroy=lambda *_: app.quit(), + on_destroy=lambda *_: app.quit() ) app.add_window(window) diff --git a/config/data.py b/config/data.py index f3b3271..abd0f52 100644 --- a/config/data.py +++ b/config/data.py @@ -1,18 +1,20 @@ -import os +from gi.repository import Gdk, GLib +from fabric.utils.helpers import get_relative_path import json +import os + import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gdk, GLib - -from fabric.utils import get_relative_path APP_NAME = "modus" APP_NAME_CAP = "Modus" -CACHE_DIR = str(GLib.get_user_cache_dir()) + f"/{APP_NAME}" -STATE_DIR = str(GLib.get_user_state_dir() + f"/{APP_NAME}") +NOTIF_POS_KEY = "notif_pos" +NOTIF_POS_DEFAULT = "Top" + +CACHE_DIR = str(GLib.get_user_cache_dir()) + f"/{APP_NAME}" USERNAME = os.getlogin() HOSTNAME = os.uname().nodename @@ -24,97 +26,76 @@ screen = Gdk.Screen.get_default() CURRENT_WIDTH = screen.get_width() CURRENT_HEIGHT = screen.get_height() -WALLPAPERS_DIR_DEFAULT = get_relative_path("../assets/wallpaper") + +WALLPAPERS_DIR_DEFAULT = get_relative_path("../assets/wallpapers_example") +CONFIG_FILE = get_relative_path("../config/config.json") MATUGEN_STATE_FILE = os.path.join(CONFIG_DIR, "matugen") -CONFIG_FILE = get_relative_path("../config/assets/config.json") -if os.path.exists(CONFIG_FILE): - with open(CONFIG_FILE, "r") as f: - config = json.load(f) - WALLPAPERS_DIR = config.get("wallpapers_dir", WALLPAPERS_DIR_DEFAULT) - VERTICAL = config.get("vertical", False) # Use saved value or False as default - VERTICAL_RIGHT_ALIGN = config.get("vertical_right_align", False) # Right side by default - BOTTOM_BAR = config.get("bottom_bar", False) # Use saved value or False as default - CENTERED_BAR = config.get("centered_bar", False) # Load centered bar setting - TERMINAL_COMMAND = config.get( - "terminal_command", "kitty -e" - ) # Load terminal command - DOCK_ENABLED = config.get("dock_enabled", True) # Load dock visibility setting - DOCK_ALWAYS_OCCLUDED = config.get( - "dock_always_occluded", False - ) # Load dock hover-only setting - DOCK_ICON_SIZE = config.get("dock_icon_size", 28) # Load dock icon size setting - OSD_ENABLED = config.get("osd_enabled", True) # Load OSD visibility setting +DOCK_THEME = "Pills" - # Load bar component visibility settings - BAR_COMPONENTS_VISIBILITY = { - "button_app": config.get("bar_button_app_visible", True), - "tray": config.get("bar_tray_visible", True), - # "button_tools": config.get("bar_button_tools_visible", True), - "workspaces": config.get("bar_workspaes_visible", True), - "metrics": config.get("bar_metrics_visible", True), - "language": config.get("bar_language_visible", True), - "date_time": config.get("bar_date_time_visible", True), - "updates": config.get("bar_updates_visible", True), - "indicators": config.get("bar_indicators", True), - } - - BAR_METRICS_DISKS = config.get("bar_metrics_disks", ["/"]) - METRICS_VISIBLE = config.get( - "metrics_visible", - {"cpu": True, "ram": True, "disk": True, "swap": True}, - ) -else: - WALLPAPERS_DIR = WALLPAPERS_DIR_DEFAULT - VERTICAL = False # Default value when no config exists - VERTICAL_RIGHT_ALIGN = False # Default to right alignment - BOTTOM_BAR = False # Default value for bottom bar - CENTERED_BAR = False # Default value for centered bar - DOCK_ENABLED = True # Default value for dock visibility - DOCK_ALWAYS_OCCLUDED = False # Default value for dock hover-only mode - OSD_ENABLED = True # Default value for OSD visibility - TERMINAL_COMMAND = "kitty -e" # Default terminal command when no config - DOCK_ICON_SIZE = 28 # Default dock icon size when no config - - # Default values for component visibility (all visible) - BAR_COMPONENTS_VISIBILITY = { - "button_aps": True, - "tray": True, - # "button_tools": True, - "workspaces": True, - "metrics": True, - "language": True, - "date_time": True, - "indicators": True, - "updates": True, - } - - BAR_METRICS_DISKS = ["/"] - METRICS_VISIBLE = { - "cpu": True, - "ram": True, - "disk": True, - "swap": True, - } - - -def read_config(): - config_path = get_relative_path("./assets/modus.json") - - with open(config_path, "r") as file: - # Load JSON data into a Python dictionary - data = json.load(file) - return data def load_config(): """Load the configuration from config.json""" - config_path = get_relative_path("./assets/config.json") + config_path = os.path.expanduser(f"~/{APP_NAME_CAP}/config/config.json") config = {} + if os.path.exists(config_path): try: with open(config_path, "r") as f: config = json.load(f) except Exception as e: print(f"Error loading config: {e}") + return config + + +if os.path.exists(CONFIG_FILE): + with open(CONFIG_FILE, "r") as f: + config = json.load(f) + WALLPAPERS_DIR = config.get("wallpapers_dir", WALLPAPERS_DIR_DEFAULT) + DOCK_POSITION = config.get("dock_position", "Bottom") + VERTICAL = DOCK_POSITION in ["Left", "Right"] + TERMINAL_COMMAND = config.get("terminal_command", "kitty -e") + DOCK_ENABLED = config.get("dock_enabled", True) + 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") + + NOTIF_POS = config.get(NOTIF_POS_KEY, NOTIF_POS_DEFAULT) + + DOCK_COMPONENTS_VISIBILITY = { + "workspaces": config.get("dock_workspaces_visible", True), + "metrics": config.get("dock_metrics_visible", True), + "battery": config.get("dock_battery_visible", True), + "date_time": config.get("dock_date_time_visible", True), + "controls": config.get("dock_controls_visible", True), + "indicators": config.get("dock_indicators_visible", True), + } + + METRICS_DISKS = config.get("metrics_disks", ["/"]) + METRICS_VISIBLE = config.get("metrics_visible", {"cpu": True, "ram": True, "disk": True, "swap": True, "gpu": False}) +else: + WALLPAPERS_DIR = WALLPAPERS_DIR_DEFAULT + DOCK_POSITION = "Bottom" + VERTICAL = False + DOCK_ENABLED = True + DOCK_ALWAYS_OCCLUDED = False + DOCK_AUTO_HIDE = True + TERMINAL_COMMAND = "kitty -e" + DOCK_THEME = "Pills" + + NOTIF_POS = NOTIF_POS_DEFAULT + + + DOCK_COMPONENTS_VISIBILITY = { + "workspaces": True, + "metrics": True, + "battery": True, + "date_time": True, + "controls": True, + "indicators": True, + } + + METRICS_DISKS = ["/"] + METRICS_VISIBLE = {"cpu": True, "ram": True, "disk": True, "swap": True, "gpu": False} diff --git a/config/dock.json b/config/dock.json new file mode 100644 index 0000000..d4fbc6a --- /dev/null +++ b/config/dock.json @@ -0,0 +1,12 @@ +{ + "pinned_apps": [ + { + "name": "Brave", + "display_name": "Brave", + "window_class": "brave-browser", + "executable": "brave", + "command_line": "brave %U", + "icon_name": "brave-desktop" + } + ] +} diff --git a/config/hypr/ax-shell.conf b/config/hypr/ax-shell.conf new file mode 100644 index 0000000..504d44b --- /dev/null +++ b/config/hypr/ax-shell.conf @@ -0,0 +1,75 @@ +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 e839fe7..3553bf5 100644 --- a/config/hypr/colors.conf +++ b/config/hypr/colors.conf @@ -1,11 +1,27 @@ -$wallpaper = /home/sankalp/Pictures/wallpapers/background.png -$background = 0e1514 -$foreground = dde4e2 +$wallpaper = /home/sankalp/Pictures/wallpapers/56.png +$background = 18120b +$foreground = ede0d4 -$primary = 81d5ca -$secondary = b1ccc7 -$tertiary = aec9e6 -$surface = 0e1514 -$surface_bright = 343a39 -$outline = 899390 +$primary = f5bc6f +$secondary = dec2a2 +$tertiary = b9cda0 +$surface = 18120b +$surface_bright = 3f3830 +$outline = 9c8f80 $error = ff5945 + +$shadow = 000000 + +$red = ffb595 +$green = b7d085 +$yellow = dec56e +$blue = cebdfe +$magenta = fcb0d5 +$cyan = 84d5c4 +$white = 82d3e0 +$red_dim = ff6e2f +$green_dim = 89ad42 +$yellow_dim = bb9b2b +$blue_dim = 8359fc +$magenta_dim = f84ea1 +$cyan_dim = 3eb59c diff --git a/config/hypr/hypridle.conf b/config/hypr/hypridle.conf index 48bd7d7..340ff14 100644 --- a/config/hypr/hypridle.conf +++ b/config/hypr/hypridle.conf @@ -1,12 +1,5 @@ - -listener { - timeout = 500 # in seconds. - on-timeout = notify-send "You are idle!" # command to run when timeout has passed. - on-resume = notify-send "Welcome back!" # command to run when activity is detected after timeout has fired. -} - general { - lock_cmd = pidof hyprlock || hyprlock # avoid starting multiple hyprlock instances. + lock_cmd = pidof hyprlock || hyprlock # avoid starting multiple hyprlock instances. before_sleep_cmd = loginctl lock-session # lock before suspend. after_sleep_cmd = hyprctl dispatch dpms on # to avoid having to press a key twice to turn on the display. } @@ -17,13 +10,6 @@ listener { on-resume = brightnessctl -r # monitor backlight restore. } -# turn off keyboard backlight, comment out this section if you dont have a keyboard backlight. -listener { - timeout = 150 # 2.5min. - on-timeout = brightnessctl -sd rgb:kbd_backlight set 0 # turn off keyboard backlight. - on-resume = brightnessctl -rd rgb:kbd_backlight # turn on keyboard backlight. -} - listener { timeout = 300 # 5min on-timeout = loginctl lock-session # lock screen when timeout has passed diff --git a/config/hypr/hyprlock.conf b/config/hypr/hyprlock.conf index 80977af..7da736f 100644 --- a/config/hypr/hyprlock.conf +++ b/config/hypr/hyprlock.conf @@ -1,150 +1,91 @@ +source = ~/.config/Ax-Shell/config/hypr/colors.conf +# BACKGROUND +background { + monitor = + path = ~/.current.wall #path to background image + blur_passes = 3 + blur_size = 3 + contrast = 1.0 + brightness = 0.5 + vibrancy = 0.0 + vibrancy_darkness = 0.0 +} -source = ~/Modus/config/hypr/colors.conf +# GENERAL +general { + no_fade_in = false + grace = 0 + disable_loading_bar = true + hide_cursor = true +} -# Configuration - # (general settings) - general { - grace = 2 - hide_cursor = true - ignore_empty_input = true - text_trim = true - } - # (background config) - background { - monitor = - path = $wallpaper # only png supported for now - # color = rgba(26,17,16, 1.0) - blur_passes = 3 # 0 disables blurring - blur_size = 1 - noise = 0.01 - contrast = 0.8916 - brightness = 0.7 - vibrancy = 0.1696 - vibrancy_darkness = 0.0 - } +# INPUT FIELD +input-field { + monitor = + size = 256, 48 + outline_thickness = 0 + dots_size = 0.2 # Scale of input-field height, 0.2 - 0.8 + dots_spacing = 0.5 # Scale of dots' absolute size, 0.0 - 1.0 + dots_center = true + outer_color = rgba(00000000) + inner_color = rgba(0, 0, 0, 1) + font_color = rgb($foreground) + fail_color = rgb($error) + check_color = rgb($tertiary) + capslock_color = rgb($secondary) + fade_on_empty = false + font_family = Iosevka Nerd Font + placeholder_text = ... #text for input password + hide_input = false + position = 0, -100 + halign = center + valign = center + shadow_passes = 1 + shadow_size = 5 + shadow_boost = 0.5 +} +# TIME +label { + monitor = + text = cmd[update:1000] echo "$(date +"%H:%M:%S")" + color = rgb($foreground) + font_size = 14 + font_family = Iosevka Nerd Font Bold + position = 0, -150 + halign = center + valign = center + shadow_passes = 1 + shadow_size = 5 + shadow_boost = 0.5 +} -# Password input - input-field { - monitor = - size = 256, 48 - outline_thickness = -1 - dots_size = 0.33 - dots_spacing = 0.45 - dots_center = true - dots_rounding = -1 - outer_color = rgba(0,0,0,0) - inner_color = rgba(0,0,0,0) - font_color = 0xff$foreground - fade_on_empty = true - fade_timeout = 1000 - font_family = Product Sans - placeholder_text = - hide_input = false - rounding = 4 - check_color = 0xff$tertiary - fail_color = 0xff$error - capslock_color = 0xff$secondary - fail_text = $FAIL ($ATTEMPTS) - fail_transition = 200 - capslock_color = -1 - numlock_color = -1 - bothlock_color = -1 - invert_numlock = false - swap_font_color = false - position = 0, 150 - halign = center - valign = bottom - } -# - - -# Time - # (hours) - label { - monitor = - text = cmd[update:1000] echo -e "$(date +"%H")" - color = 0xff$foreground - shadow_pass = 2 - shadow_size = 3 - shadow_color = rgb(0,0,0) - shadow_boost = 1.2 - font_size = 150 - font_family = Product Sans Light - position = 0, 90 - halign = center - valign = center - } - # (minutes) - label { - monitor = - text = cmd[update:1000] echo -e "$(date +"%M")" - color = 0xff$foreground - font_size = 150 - font_family = Product Sans Light - position = 0, -90 - halign = center - valign = center - } -# - -# Date - # (label) - label { - monitor = - text = cmd[update:1000] echo -e "$(date +"%A, %B %d")" - color = 0xff$foreground - font_size = 15 - font_family = Product Sans Medium - position = 100, -75 - shadow_passes = 5 - shadow_size = 10 - halign = left - valign = top - } - # (icon) - label { - monitor = - text = calendar_today - color = 0xff$foreground - font_size = 15 - font_family = Material Symbols Outlined - position = 75, -75 - shadow_passes = 5 - shadow_size = 10 - halign = left - valign = top - } -# - -# Input layout - # (label) - label { - monitor = - text = $LAYOUT - color = 0xff$foreground - font_size = 15 - font_family = Product Sans Medium - position = 100, -110 - shadow_passes = 5 - shadow_size = 10 - halign = left - valign = top - } - # (icon) - label { - monitor = - text = keyboard - color = 0xff$foreground - font_size = 15 - font_family = Material Symbols Outlined - position = 75, -112 - shadow_passes = 5 - shadow_size = 10 - halign = left - valign = top - } -# - +# USER +label { + monitor = + text = cmd[update:1000] echo "$USER@$(hostname)" + color = rgb($foreground) + font_size = 14 + font_family = Iosevka Nerd Font Bold Italic + position = 0, -50 + halign = center + valign = center + shadow_passes = 1 + shadow_size = 5 + shadow_boost = 0.5 +} +# PICTURE +image { + path = .face.icon + size = 200 + position = 0, 75 + halign = center + valign = center + border_size = 3 + border_color = rgb($primary) + shadow_passes = 1 + shadow_size = 5 + shadow_boost = 0.5 +} diff --git a/config/hypr/modus.conf b/config/hypr/modus.conf index edd9ec3..edc4f58 100644 --- a/config/hypr/modus.conf +++ b/config/hypr/modus.conf @@ -1,42 +1,52 @@ -# Generated by Modus - DO NOT EDIT MANUALLY -exec-once = python /home/sankalp/Modus/main.py -exec = pgrep -x "hypridle" > /dev/null || hypridle -exec = swww-daemon -exec-once = wl-paste --type text --watch cliphist store -exec-once = wl-paste --type image --watch cliphist store +exec-once = uwsm-app $(python /home/sankalp/Modus/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 modus -$scriptsDir = /home/sankalp/Modus/config/scripts/ +$axMessage = notify-send "Axenide" "FIRE IN THE HOLE‼️🗣️🔥🕳️" -i "/home/sankalp/.config/Modus/assets/ax.png" -A "🗣️" -A "🔥" -A "🕳️" -a "Source Code" -# Key Bindings -bind = SHIFT ALT, T, exec, killall modus; python /home/sankalp/Modus/main.py # Reload Modus -bind = SHIFT ALT, B, exec, $fabricSend 'launcher.open("bluetooth")' # Bluetooth -bind = SUPER, N, exec, $fabricSend 'launcher.open("notification-center")' # Notifications -bind = SUPER, E, exec, $fabricSend 'launcher.open("emoji")' # Emoji -bind = SUPER, V, exec, $fabricSend 'launcher.open("cliphist")' # Clipboard History -bind = SUPER, T, exec, $fabricSend 'launcher.open("kanban")' # Kanban -bind = SUPER, Z, exec, $fabricSend 'launcher.open("tmux")' # Tmux Manager -bind = SUPER, S, exec, $fabricSend 'launcher.open("tools")' # Toolbox -bind = SUPER, W, exec, $fabricSend 'launcher.open("wallpapers")' # Wallpaper Selector -bind = SUPER, D, exec, $fabricSend 'launcher.open("launcher")' # App Launcher -bind = ALT, W, exec, $fabricSend 'launcher.open("window-switcher")' # Window Switcher -bind = SUPER, X, exec, $fabricSend 'launcher.open("power")' # Power Menu -bind = SUPER SHIFT, T, exec, killall modus; GTK_DEBUG=interactive python /home/sankalp/Modus/main.py # Restart with inspector +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 +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 modus; uwsm-app $(GTK_DEBUG=interactive python /home/sankalp/.config/Modus/main.py) # Restart with inspector # Wallpapers directory: /home/sankalp/Pictures/wallpapers -# Source color scheme source = /home/sankalp/Modus/config/hypr/colors.conf +layerrule = noanim, fabric + +exec = cp $wallpaper ~/.current.wall + general { - col.active_border = 0xff$primary - col.inactive_border = 0xff$surface + 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 @@ -48,41 +58,18 @@ decoration { } rounding = 14 shadow { - enabled = true - range = 10 - render_power = 2 - color = rgba(0, 0, 0, 0.25) + enabled = true + range = 10 + render_power = 2 + color = rgba(0, 0, 0, 0.25) } } animations { - enabled = true - # Animation curves - bezier = linear, 0, 0, 1, 1 - bezier = md3_standard, 0.2, 0, 0, 1 - bezier = md3_decel, 0.05, 0.7, 0.1, 1 - bezier = md3_accel, 0.3, 0, 0.8, 0.15 - bezier = overshot, 0.05, 0.9, 0.1, 1.1 - bezier = crazyshot, 0.1, 1.5, 0.76, 0.92 - bezier = hyprnostretch, 0.05, 0.9, 0.1, 1.0 - bezier = menu_decel, 0.1, 1, 0, 1 - bezier = menu_accel, 0.38, 0.04, 1, 0.07 - bezier = easeInOutCirc, 0.85, 0, 0.15, 1 - bezier = easeOutCirc, 0, 0.55, 0.45, 1 - bezier = easeOutExpo, 0.16, 1, 0.3, 1 - bezier = softAcDecel, 0.26, 0.26, 0.15, 1 - bezier = md2, 0.4, 0, 0.2, 1 - - animation = windows, 1, 3, md3_decel, popin 60% - animation = windowsIn, 1, 3, md3_decel, popin 60% - animation = windowsOut, 1, 3, md3_accel, popin 60% - animation = border, 1, 10, default - animation = fade, 1, 3, md3_decel - animation = layers, 1, 2, md3_decel, slide - animation = layersIn, 1, 3, menu_decel, slide - animation = layersOut, 1, 1.6, menu_accel, slide - animation = fadeLayersIn, 1, 2, menu_decel - animation = fadeLayersOut, 1, 4.5, menu_accel - animation = workspaces, 1, 7, menu_decel, slide - animation = specialWorkspace, 1, 3, md3_decel, slidevert + 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, slidefade 20% } diff --git a/config/matugen/templates/hyprland-colors.conf b/config/matugen/templates/hyprland-colors.conf index a375c24..9aeb090 100644 --- a/config/matugen/templates/hyprland-colors.conf +++ b/config/matugen/templates/hyprland-colors.conf @@ -9,3 +9,19 @@ $surface = {{colors.surface.default.hex_stripped}} $surface_bright = {{colors.surface_bright.default.hex_stripped}} $outline = {{colors.outline.default.hex_stripped}} $error = {{colors.error.default.hex_stripped | set_lightness: -20.0}} + +$shadow = {{colors.shadow.default.hex_stripped}} + +$red = {{colors.red.default.hex_stripped}} +$green = {{colors.green.default.hex_stripped}} +$yellow = {{colors.yellow.default.hex_stripped}} +$blue = {{colors.blue.default.hex_stripped}} +$magenta = {{colors.magenta.default.hex_stripped}} +$cyan = {{colors.cyan.default.hex_stripped}} +$white = {{colors.white.default.hex_stripped}} +$red_dim = {{colors.red.default.hex_stripped | set_lightness: -20.0}} +$green_dim = {{colors.green.default.hex_stripped | set_lightness: -20.0}} +$yellow_dim = {{colors.yellow.default.hex_stripped | set_lightness: -20.0}} +$blue_dim = {{colors.blue.default.hex_stripped | set_lightness: -20.0}} +$magenta_dim = {{colors.magenta.default.hex_stripped | set_lightness: -20.0}} +$cyan_dim = {{colors.cyan.default.hex_stripped | set_lightness: -20.0}} diff --git a/config/matugen/templates/modus.css b/config/matugen/templates/modus.css index db192d7..7d8c607 100644 --- a/config/matugen/templates/modus.css +++ b/config/matugen/templates/modus.css @@ -3,29 +3,18 @@ --background: {{colors.background.default.hex}}; --cursor: {{colors.on_background.default.hex}}; --primary: {{colors.primary.default.hex}}; - --primary-container: {{colors.primary_container.default.hex}}; --on-primary: {{colors.on_primary.default.hex}}; - --on-primary-container: {{colors.on_primary_container.default.hex}}; --secondary: {{colors.secondary.default.hex}}; --on-secondary: {{colors.on_secondary.default.hex}}; - --secondary-container: {{colors.secondary_container.default.hex}}; --tertiary: {{colors.tertiary.default.hex}}; - --tertiary-container: {{colors.tertiary_container.default.hex}}; --on-tertiary: {{colors.on_tertiary.default.hex}}; - --on-tertiary-container: {{colors.on_tertiary_container.default.hex}}; --surface: {{colors.surface.default.hex}}; - --surface-container: {{colors.surface_container.default.hex}}; --surface-bright: {{colors.surface_bright.default.hex}}; - --surface-variant: {{colors.surface_variant.default.hex}}; - --surface-container-low: {{colors.surface_container_low.default.hex}}; - --on-surface: {{colors.on_surface.default.hex}}; --error: {{colors.error.default.hex}}; --error-dim: {{colors.error.default.hex | set_lightness: -10.0}}; --on-error: {{colors.on_error.default.hex}}; --error-container: {{colors.error_container.default.hex}}; - --on-error-container: {{colors.on_error_container.default.hex}}; --outline: {{colors.outline.default.hex}}; - --outline-variant: {{colors.outline_variant.default.hex}}; --shadow: {{colors.shadow.default.hex}}; --red: {{colors.red.default.hex}}; --red-dim: {{colors.red.default.hex | set_lightness: -10.0}}; @@ -40,5 +29,4 @@ --cyan: {{colors.cyan.default.hex}}; --cyan-dim: {{colors.cyan.default.hex | set_lightness: -10.0}}; --white: {{colors.white.default.hex}}; - } diff --git a/config/matugen/templates/modus.scss b/config/matugen/templates/modus.scss deleted file mode 100644 index aba4239..0000000 --- a/config/matugen/templates/modus.scss +++ /dev/null @@ -1,29 +0,0 @@ -@use "sass:color"; -<* for name, value in colors *> -${{name}}: {{value.default.hex}}; -<* endfor *> - -// Custom Color -$red: {{colors.red.default.hex}}; -$red-dim: {{colors.red.default.hex | set_lightness: -10.0}}; -$green: {{colors.green.default.hex}}; -$green-dim: {{colors.green.default.hex | set_lightness: -10.0}}; -$yellow: {{colors.yellow.default.hex}}; -$yellow-dim: {{colors.yellow.default.hex | set_lightness: -10.0}}; -$blue: {{colors.blue.default.hex}}; -$blue-dim: {{colors.blue.default.hex | set_lightness: -10.0}}; -$magenta: {{colors.magenta.default.hex}}; -$magenta-dim: {{colors.magenta.default.hex | set_lightness: -10.0}}; -$cyan: {{colors.cyan.default.hex}}; -$cyan-dim: {{colors.cyan.default.hex | set_lightness: -10.0}}; -$white: {{colors.white.default.hex}}; - -$theme: 'dark'; -$surface_transparent: color.scale($surface, $alpha: -20%); -$inverse_surface_transparent: color.scale($inverse_surface, $alpha: -92%); -$inverse_surface_transparent_variant: color.scale( - $inverse_surface, - $alpha: -82% -); -$inverse_surface_transparent_overlay: linear-gradient(0deg, $inverse_surface_transparent, $inverse_surface_transparent); -$inverse_surface_transparent_variant_overlay: linear-gradient(0deg, $inverse_surface_transparent_variant, $inverse_surface_transparent_variant); diff --git a/config/settings/constants.py b/config/settings/constants.py new file mode 100644 index 0000000..1530636 --- /dev/null +++ b/config/settings/constants.py @@ -0,0 +1,69 @@ +from config.data import (APP_NAME, APP_NAME_CAP, NOTIF_POS_DEFAULT, NOTIF_POS_KEY,WALLPAPERS_DIR_DEFAULT) + +SOURCE_STRING = f""" +# {APP_NAME_CAP} +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": [] +} diff --git a/config/settings/gui.py b/config/settings/gui.py new file mode 100644 index 0000000..9e3d209 --- /dev/null +++ b/config/settings/gui.py @@ -0,0 +1,773 @@ +import json +import os +import shutil +import subprocess +import threading +import time +from pathlib import Path + +import gi + +gi.require_version('Gtk', '3.0') +from fabric.widgets.box import Box +from fabric.widgets.button import Button +from fabric.widgets.entry import Entry +from fabric.widgets.image import Image as FabricImage +from fabric.widgets.label import Label +from fabric.widgets.scale import Scale +from fabric.widgets.scrolledwindow import ScrolledWindow +from fabric.widgets.stack import Stack +from fabric.widgets.window import Window +from gi.repository import GdkPixbuf, GLib, Gtk +from PIL import Image + +from config.data import (APP_NAME, APP_NAME_CAP, NOTIF_POS_DEFAULT, NOTIF_POS_KEY) +from config.settings.utils import backup_and_replace, bind_vars, start_config + + +class HyprConfGUI(Window): + def __init__(self, show_lock_checkbox: bool, show_idle_checkbox: bool, **kwargs): + super().__init__( + title="Ax-Shell Settings", + name="axshell-settings-window", + size=(640, 640), + **kwargs, + ) + + self.set_resizable(False) + self.themes = ["Pills", "Dense", "Edge"] + self.selected_face_icon = None + self.show_lock_checkbox = show_lock_checkbox + self.show_idle_checkbox = show_idle_checkbox + + root_box = Box(orientation="v", spacing=10, style="margin: 10px;") + self.add(root_box) + + main_content_box = Box(orientation="h", spacing=6, v_expand=True, h_expand=True) + root_box.add(main_content_box) + + self.tab_stack = Stack( + transition_type="slide-up-down", + transition_duration=250, + v_expand=True, h_expand=True + ) + + self.key_bindings_tab_content = self.create_key_bindings_tab() + self.appearance_tab_content = self.create_appearance_tab() + self.system_tab_content = self.create_system_tab() + self.about_tab_content = self.create_about_tab() + + self.tab_stack.add_titled(self.key_bindings_tab_content, "key_bindings", "Key Bindings") + self.tab_stack.add_titled(self.appearance_tab_content, "appearance", "Appearance") + self.tab_stack.add_titled(self.system_tab_content, "system", "System") + self.tab_stack.add_titled(self.about_tab_content, "about", "About") + + tab_switcher = Gtk.StackSwitcher() + tab_switcher.set_stack(self.tab_stack) + tab_switcher.set_orientation(Gtk.Orientation.VERTICAL) + main_content_box.add(tab_switcher) + main_content_box.add(self.tab_stack) + + button_box = Box(orientation="h", spacing=10, h_align="end") + reset_btn = Button(label="Reset to Defaults", on_clicked=self.on_reset) + button_box.add(reset_btn) + close_btn = Button(label="Close", on_clicked=self.on_close) + button_box.add(close_btn) + accept_btn = Button(label="Apply & Reload", on_clicked=self.on_accept) + button_box.add(accept_btn) + root_box.add(button_box) + + def create_key_bindings_tab(self): + scrolled_window = ScrolledWindow( + h_scrollbar_policy="never", + v_scrollbar_policy="automatic", + h_expand=True, + v_expand=True, + propagate_width=False, + propagate_height=False + ) + + main_vbox = Box(orientation="v", spacing=10, style="margin: 15px;") + scrolled_window.add(main_vbox) + + keybind_grid = Gtk.Grid() + keybind_grid.set_column_spacing(10) + keybind_grid.set_row_spacing(8) + keybind_grid.set_margin_start(5) + keybind_grid.set_margin_end(5) + keybind_grid.set_margin_top(5) + keybind_grid.set_margin_bottom(5) + + action_label = Label(markup="Action", h_align="start", style="margin-bottom: 5px;") + modifier_label = Label(markup="Modifier", h_align="start", style="margin-bottom: 5px;") + separator_label = Label(label="+", h_align="center", style="margin-bottom: 5px;") + key_label = Label(markup="Key", h_align="start", style="margin-bottom: 5px;") + + keybind_grid.attach(action_label, 0, 0, 1, 1) + keybind_grid.attach(modifier_label, 1, 0, 1, 1) + keybind_grid.attach(separator_label, 2, 0, 1, 1) + keybind_grid.attach(key_label, 3, 0, 1, 1) + + self.entries = [] + bindings = [ + (f"Reload {APP_NAME_CAP}", 'prefix_restart', 'suffix_restart'), + ("Message", 'prefix_axmsg', 'suffix_axmsg'), + ("Dashboard", 'prefix_dash', 'suffix_dash'), + ("Bluetooth", 'prefix_bluetooth', 'suffix_bluetooth'), + ("Pins", 'prefix_pins', 'suffix_pins'), + ("Kanban", 'prefix_kanban', 'suffix_kanban'), + ("App Launcher", 'prefix_launcher', 'suffix_launcher'), + ("Tmux", 'prefix_tmux', 'suffix_tmux'), + ("Clipboard History", 'prefix_cliphist', 'suffix_cliphist'), + ("Toolbox", 'prefix_toolbox', 'suffix_toolbox'), + ("Overview", 'prefix_overview', 'suffix_overview'), + ("Wallpapers", 'prefix_wallpapers', 'suffix_wallpapers'), + ("Random Wallpaper", 'prefix_randwall', 'suffix_randwall'), + ("Emoji Picker", 'prefix_emoji', 'suffix_emoji'), + ("Power Menu", 'prefix_power', 'suffix_power'), + ("Toggle Caffeine", 'prefix_caffeine', 'suffix_caffeine'), + ("Reload CSS", 'prefix_css', 'suffix_css'), + ("Restart with inspector", 'prefix_restart_inspector', 'suffix_restart_inspector'), + ] + + for i, (label_text, prefix_key, suffix_key) in enumerate(bindings): + row = i + 1 + binding_label = Label(label=label_text, h_align="start") + keybind_grid.attach(binding_label, 0, row, 1, 1) + prefix_entry = Entry(text=bind_vars.get(prefix_key, '')) + keybind_grid.attach(prefix_entry, 1, row, 1, 1) + plus_label = Label(label="+", h_align="center") + keybind_grid.attach(plus_label, 2, row, 1, 1) + suffix_entry = Entry(text=bind_vars.get(suffix_key, '')) + keybind_grid.attach(suffix_entry, 3, row, 1, 1) + self.entries.append((prefix_key, suffix_key, prefix_entry, suffix_entry)) + + main_vbox.add(keybind_grid) + return scrolled_window + + def create_appearance_tab(self): + scrolled_window = ScrolledWindow( + h_scrollbar_policy="never", + v_scrollbar_policy="automatic", + h_expand=True, + v_expand=True, + propagate_width=False, + propagate_height=False + ) + + vbox = Box(orientation="v", spacing=15, style="margin: 15px;") + scrolled_window.add(vbox) + + top_grid = Gtk.Grid() + top_grid.set_column_spacing(20) + top_grid.set_row_spacing(5) + top_grid.set_margin_bottom(10) + vbox.add(top_grid) + + wall_header = Label(markup="Wallpapers", h_align="start") + top_grid.attach(wall_header, 0, 0, 1, 1) + wall_label = Label(label="Directory:", h_align="start", v_align="center") + top_grid.attach(wall_label, 0, 1, 1, 1) + + chooser_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + chooser_container.set_halign(Gtk.Align.START) + chooser_container.set_valign(Gtk.Align.CENTER) + self.wall_dir_chooser = Gtk.FileChooserButton( + title="Select a folder", action=Gtk.FileChooserAction.SELECT_FOLDER + ) + self.wall_dir_chooser.set_tooltip_text("Select the directory containing your wallpaper images") + self.wall_dir_chooser.set_filename(bind_vars.get('wallpapers_dir','')) + self.wall_dir_chooser.set_size_request(180, -1) + chooser_container.add(self.wall_dir_chooser) + top_grid.attach(chooser_container, 1, 1, 1, 1) + + face_header = Label(markup="Profile Icon", h_align="start") + top_grid.attach(face_header, 2, 0, 2, 1) + current_face = os.path.expanduser("~/.face.icon") + face_image_container = Box(style_classes=["image-frame"], h_align="center", v_align="center") + self.face_image = FabricImage(size=64) + try: + if os.path.exists(current_face): + pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(current_face, 64, 64) + self.face_image.set_from_pixbuf(pixbuf) + else: + self.face_image.set_from_icon_name("user-info", Gtk.IconSize.DIALOG) + except Exception as e: + print(f"Error loading face icon: {e}") + self.face_image.set_from_icon_name("image-missing", Gtk.IconSize.DIALOG) + face_image_container.add(self.face_image) + top_grid.attach(face_image_container, 2, 1, 1, 1) + + browse_btn_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + browse_btn_container.set_halign(Gtk.Align.START) + browse_btn_container.set_valign(Gtk.Align.CENTER) + face_btn = Button(label="Browse...", + tooltip_text="Select a square image for your profile icon", + on_clicked=self.on_select_face_icon) + browse_btn_container.add(face_btn) + top_grid.attach(browse_btn_container, 3, 1, 1, 1) + self.face_status_label = Label(label="", h_align="start") + top_grid.attach(self.face_status_label, 2, 2, 2, 1) + + separator1 = Box(style="min-height: 1px; background-color: alpha(@fg_color, 0.2); margin: 5px 0px;", h_expand=True) + vbox.add(separator1) + + layout_header = Label(markup="Layout Options", h_align="start") + vbox.add(layout_header) + layout_grid = Gtk.Grid() + layout_grid.set_column_spacing(20) + layout_grid.set_row_spacing(10) + layout_grid.set_margin_start(10) + layout_grid.set_margin_top(5) + vbox.add(layout_grid) + + position_label = Label(label="Dock Position", h_align="start", v_align="center") + layout_grid.attach(position_label, 0, 0, 1, 1) + position_combo_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, halign=Gtk.Align.START, valign=Gtk.Align.CENTER) + self.position_combo = Gtk.ComboBoxText() + self.position_combo.set_tooltip_text("Select the position of the dock") + positions = ["Bottom", "Left", "Right"] + for pos in positions: + self.position_combo.append_text(pos) + current_position = bind_vars.get('dock_position', "Bottom") + try: + self.position_combo.set_active(positions.index(current_position)) + except ValueError: + self.position_combo.set_active(0) + self.position_combo.connect("changed", self.on_position_changed) + position_combo_container.add(self.position_combo) + layout_grid.attach(position_combo_container, 1, 0, 1, 1) + + dock_theme_label = Label(label="Dock Theme", h_align="start", v_align="center") + layout_grid.attach(dock_theme_label, 2, 0, 1, 1) + dock_theme_combo_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, halign=Gtk.Align.START, valign=Gtk.Align.CENTER) + self.dock_theme_combo = Gtk.ComboBoxText() + self.dock_theme_combo.set_tooltip_text("Select the visual theme for the dock") + for theme in self.themes: + self.dock_theme_combo.append_text(theme) + current_dock_theme = bind_vars.get('dock_theme', "Pills") + try: + self.dock_theme_combo.set_active(self.themes.index(current_dock_theme)) + except ValueError: + self.dock_theme_combo.set_active(0) + dock_theme_combo_container.add(self.dock_theme_combo) + layout_grid.attach(dock_theme_combo_container, 3, 0, 1, 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(orientation=Gtk.Orientation.HORIZONTAL, halign=Gtk.Align.START, valign=Gtk.Align.CENTER) + self.dock_switch = Gtk.Switch(active=bind_vars.get('dock_enabled', True)) + self.dock_switch.connect("notify::active", self.on_dock_enabled_changed) + dock_switch_container.add(self.dock_switch) + layout_grid.attach(dock_switch_container, 1, 1, 1, 1) + + dock_auto_hide_label = Label(label="Auto-hide Dock", h_align="start", v_align="center") + layout_grid.attach(dock_auto_hide_label, 0, 2, 1, 1) + dock_auto_hide_switch_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, halign=Gtk.Align.START, valign=Gtk.Align.CENTER) + self.dock_auto_hide_switch = Gtk.Switch(active=bind_vars.get('dock_auto_hide', False), sensitive=self.dock_switch.get_active()) + dock_auto_hide_switch_container.add(self.dock_auto_hide_switch) + layout_grid.attach(dock_auto_hide_switch_container, 1, 2, 1, 1) + + dock_hover_label = Label(label="Show Dock Only on Hover", h_align="start", v_align="center") + layout_grid.attach(dock_hover_label, 2, 1, 1, 1) + dock_hover_switch_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, halign=Gtk.Align.START, valign=Gtk.Align.CENTER) + self.dock_hover_switch = Gtk.Switch(active=bind_vars.get('dock_always_occluded', False), sensitive=self.dock_switch.get_active()) + dock_hover_switch_container.add(self.dock_hover_switch) + layout_grid.attach(dock_hover_switch_container, 3, 1, 1, 1) + + notification_pos_label = Label(label="Notification Position", h_align="start", v_align="center") + layout_grid.attach(notification_pos_label, 0, 7, 1, 1) + + notification_pos_combo_container = Box(orientation=Gtk.Orientation.HORIZONTAL, halign=Gtk.Align.START, valign=Gtk.Align.CENTER) + + self.notification_pos_combo = Gtk.ComboBoxText() + self.notification_pos_combo.set_tooltip_text("Select where notifications appear on the screen.") + + notification_positions_list = ["Top", "Bottom"] + for pos in notification_positions_list: + self.notification_pos_combo.append_text(pos) + + current_notif_pos = bind_vars.get(NOTIF_POS_KEY, NOTIF_POS_DEFAULT) + try: + self.notification_pos_combo.set_active(notification_positions_list.index(current_notif_pos)) + except ValueError: + self.notification_pos_combo.set_active(0) + + self.notification_pos_combo.connect("changed", self.on_notification_position_changed) + + notification_pos_combo_container.add(self.notification_pos_combo) + layout_grid.attach(notification_pos_combo_container, 1, 7, 3, 1) + + separator2 = Box(style="min-height: 1px; background-color: alpha(@fg_color, 0.2); margin: 5px 0px;", h_expand=True) + vbox.add(separator2) + + components_header = Label(markup="Modules", h_align="start") + vbox.add(components_header) + components_grid = Gtk.Grid() + components_grid.set_column_spacing(15) + components_grid.set_row_spacing(8) + components_grid.set_margin_start(10) + components_grid.set_margin_top(5) + vbox.add(components_grid) + + self.component_switches = {} + component_display_names = { + 'workspace':"Workspaces", + 'metrics':"System Metrics", + 'date_time':"Date & Time", + 'battery':"Battery Indicator", + 'controls':"Control Panel", + 'indicators':"Indicators", + 'systray':"System Tray", + } + + self.corners_switch = Gtk.Switch(active=bind_vars.get('corners_visible', True)) + num_components = len(component_display_names) + 1 + rows_per_column = (num_components + 1) // 2 + + corners_label = Label(label="Rounded Corners", h_align="start", v_align="center") + components_grid.attach(corners_label, 0, 0, 1, 1) + switch_container_corners = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, halign=Gtk.Align.START, valign=Gtk.Align.CENTER) + switch_container_corners.add(self.corners_switch) + components_grid.attach(switch_container_corners, 1, 0, 1, 1) + + current_row = 0 + current_col = 0 + item_idx = 0 + for i, (name, display) in enumerate(component_display_names.items()): + if item_idx < (rows_per_column -1) : + row = item_idx + 1 + col = 0 + else: + row = (item_idx - (rows_per_column -1)) + col = 2 + + component_label = Label(label=display, h_align="start", v_align="center") + components_grid.attach(component_label, col, row, 1, 1) + + switch_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, halign=Gtk.Align.START, valign=Gtk.Align.CENTER) + component_switch = Gtk.Switch(active=bind_vars.get(f'bar_{name}_visible', True)) + switch_container.add(component_switch) + components_grid.attach(switch_container, col + 1, row, 1, 1) + self.component_switches[name] = component_switch + item_idx +=1 + + + return scrolled_window + + + + def on_notification_position_changed(self, combo: Gtk.ComboBoxText): + selected_text = combo.get_active_text() + if selected_text: + bind_vars[NOTIF_POS_KEY] = selected_text + print(f"Notification position updated in bind_vars: {bind_vars[NOTIF_POS_KEY]}") + + def create_system_tab(self): + scrolled_window = ScrolledWindow( + h_scrollbar_policy="never", + v_scrollbar_policy="automatic", + h_expand=True, + v_expand=True, + propagate_width=False, + propagate_height=False + ) + + vbox = Box(orientation="v", spacing=15, style="margin: 15px;") + scrolled_window.add(vbox) + + system_grid = Gtk.Grid() + system_grid.set_column_spacing(20) + system_grid.set_row_spacing(10) + system_grid.set_margin_bottom(15) + vbox.add(system_grid) + + terminal_header = Label(markup="Terminal Settings", h_align="start") + system_grid.attach(terminal_header, 0, 0, 2, 1) + terminal_label = Label(label="Command:", h_align="start", v_align="center") + system_grid.attach(terminal_label, 0, 1, 1, 1) + self.terminal_entry = Entry( + text=bind_vars.get('terminal_command', "kitty -e"), + tooltip_text="Command used to launch terminal apps (e.g., 'kitty -e')", + h_expand=True + ) + system_grid.attach(self.terminal_entry, 1, 1, 1, 1) + hint_label = Label(markup="Examples: 'kitty -e', 'alacritty -e', 'foot -e'", h_align="start") + system_grid.attach(hint_label, 0, 2, 2, 1) + + hypr_header = Label(markup="Hyprland Integration", h_align="start") + system_grid.attach(hypr_header, 2, 0, 2, 1) + row = 1 + self.lock_switch = None + if self.show_lock_checkbox: + lock_label = Label(label="Replace Hyprlock config", h_align="start", v_align="center") + system_grid.attach(lock_label, 2, row, 1, 1) + lock_switch_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, halign=Gtk.Align.START, valign=Gtk.Align.CENTER) + self.lock_switch = Gtk.Switch(tooltip_text="Replace Hyprlock configuration with Ax-Shell's custom config") + lock_switch_container.add(self.lock_switch) + system_grid.attach(lock_switch_container, 3, row, 1, 1) + row += 1 + self.idle_switch = None + if self.show_idle_checkbox: + idle_label = Label(label="Replace Hypridle config", h_align="start", v_align="center") + system_grid.attach(idle_label, 2, row, 1, 1) + idle_switch_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, halign=Gtk.Align.START, valign=Gtk.Align.CENTER) + self.idle_switch = Gtk.Switch(tooltip_text="Replace Hypridle configuration with Ax-Shell's custom config") + idle_switch_container.add(self.idle_switch) + system_grid.attach(idle_switch_container, 3, row, 1, 1) + row += 1 + if self.show_lock_checkbox or self.show_idle_checkbox: + note_label = Label(markup="Existing configs will be backed up", h_align="start") + system_grid.attach(note_label, 2, row, 2, 1) + + metrics_header = Label(markup="System Metrics Options", h_align="start") + vbox.add(metrics_header) + metrics_grid = Gtk.Grid(column_spacing=15, row_spacing=8, margin_start=10, margin_top=5) + vbox.add(metrics_grid) + + self.metrics_switches = {} + metric_names = {'cpu': "CPU", 'ram': "RAM", 'disk': "Disk", 'swap': "Swap", 'gpu': "GPU"} + + metrics_grid.attach(Label(label="Show in Metrics", h_align="start"), 0, 0, 1, 1) + for i, (key, label_text) in enumerate(metric_names.items()): + switch = Gtk.Switch(active=bind_vars.get('metrics_visible', {}).get(key, True)) + self.metrics_switches[key] = switch + metrics_grid.attach(Label(label=label_text, h_align="start"), 0, i + 1, 1, 1) + metrics_grid.attach(switch, 1, i + 1, 1, 1) + + + def enforce_minimum_metrics(switch_dict): + enabled_switches = [s for s in switch_dict.values() if s.get_active()] + can_disable = len(enabled_switches) > 1 + for s in switch_dict.values(): + s.set_sensitive(True if can_disable or not s.get_active() else False) + + def on_metric_toggle(switch, gparam, switch_dict): enforce_minimum_metrics(switch_dict) + for k_s, s_s in self.metrics_switches.items(): s_s.connect("notify::active", on_metric_toggle, self.metrics_switches) + enforce_minimum_metrics(self.metrics_switches) + + disks_label = Label(label="Disk directories for Metrics", h_align="start", v_align="center") + vbox.add(disks_label) + self.disk_entries = Box(orientation="v", spacing=8, h_align="start") + + self._create_disk_edit_entry_func = lambda path: self._add_disk_entry_widget(path) + + for p in bind_vars.get('bar_metrics_disks', ["/"]): self._create_disk_edit_entry_func(p) + vbox.add(self.disk_entries) + + add_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, halign=Gtk.Align.START, valign=Gtk.Align.CENTER) + add_btn = Button(label="Add new disk", on_clicked=lambda _: self._create_disk_edit_entry_func("/")) + add_container.add(add_btn) + vbox.add(add_container) + + return scrolled_window + + def _add_disk_entry_widget(self, path): + """Helper para añadir una fila de entrada de disco al Box disk_entries.""" + bar = Box(orientation="h", spacing=10, h_align="start") + entry = Entry(text=path, h_expand=True) + bar.add(entry) + x_btn = Button(label="X") + x_btn.connect("clicked", lambda _, current_bar_to_remove=bar: self.disk_entries.remove(current_bar_to_remove)) + bar.add(x_btn) + self.disk_entries.add(bar) + self.disk_entries.show_all() + + + def create_about_tab(self): + vbox = Box(orientation="v", spacing=18, style="margin: 30px;") + vbox.add(Label(markup=f"{APP_NAME_CAP}", h_align="start", style="font-size: 1.5em; margin-bottom: 8px;")) + vbox.add(Label(label="A hackable shell for Hyprland, powered by Fabric.", h_align="start", style="margin-bottom: 12px;")) + repo_box = Box(orientation="h", spacing=6, h_align="start") + repo_label = Label(label="GitHub:", h_align="start") + repo_link = Label(markup='https://github.com/S4NKALP/Modus') + repo_box.add(repo_label); repo_box.add(repo_link) + vbox.add(repo_box) + def on_kofi_clicked(_): import webbrowser; webbrowser.open("https://ko-fi.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) + vbox.add(Box(v_expand=True)) + return vbox + + def on_ws_num_changed(self, switch, gparam): + is_active = switch.get_active() + self.ws_chinese_switch.set_sensitive(is_active) + if not is_active: self.ws_chinese_switch.set_active(False) + + def on_position_changed(self, combo): + position = combo.get_active_text() + is_vertical = position in ["Left", "Right"] + self.centered_switch.set_sensitive(is_vertical) + if not is_vertical: + self.centered_switch.set_active(False) + + def on_dock_enabled_changed(self, switch, gparam): + is_active = switch.get_active() + self.dock_hover_switch.set_sensitive(is_active) + self.dock_auto_hide_switch.set_sensitive(is_active) + if not is_active: + self.dock_hover_switch.set_active(False) + self.dock_auto_hide_switch.set_active(False) + + def on_select_face_icon(self, widget): + dialog = Gtk.FileChooserDialog( + title="Select Face Icon", transient_for=self.get_toplevel(), action=Gtk.FileChooserAction.OPEN + ) + dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK) + image_filter = Gtk.FileFilter() + image_filter.set_name("Image files") + for mime in ["image/png", "image/jpeg"]: image_filter.add_mime_type(mime) + for pattern in ["*.png", "*.jpg", "*.jpeg"]: image_filter.add_pattern(pattern) + dialog.add_filter(image_filter) + if dialog.run() == Gtk.ResponseType.OK: + self.selected_face_icon = dialog.get_filename() + self.face_status_label.label = f"Selected: {os.path.basename(self.selected_face_icon)}" + try: + pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(self.selected_face_icon, 64, 64) + self.face_image.set_from_pixbuf(pixbuf) + except Exception as e: + print(f"Error loading selected face icon preview: {e}") + self.face_image.set_from_icon_name("image-missing", Gtk.IconSize.DIALOG) + dialog.destroy() + + def on_accept(self, widget): + + current_bind_vars_snapshot = {} + for prefix_key, suffix_key, prefix_entry, suffix_entry in self.entries: + current_bind_vars_snapshot[prefix_key] = prefix_entry.get_text() + current_bind_vars_snapshot[suffix_key] = suffix_entry.get_text() + + current_bind_vars_snapshot['wallpapers_dir'] = self.wall_dir_chooser.get_filename() + + current_bind_vars_snapshot['dock_position'] = self.position_combo.get_active_text() + current_bind_vars_snapshot['vertical'] = current_bind_vars_snapshot['dock_position'] in ["Left", "Right"] + + current_bind_vars_snapshot['dock_enabled'] = self.dock_switch.get_active() + current_bind_vars_snapshot['dock_auto_hide'] = self.dock_auto_hide_switch.get_active() + current_bind_vars_snapshot['dock_always_occluded'] = self.dock_hover_switch.get_active() + 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'] = self.dock_theme_combo.get_active_text() + + selected_notif_pos_text = self.notification_pos_combo.get_active_text() + if selected_notif_pos_text: + current_bind_vars_snapshot[NOTIF_POS_KEY] = selected_notif_pos_text + else: + current_bind_vars_snapshot[NOTIF_POS_KEY] = NOTIF_POS_DEFAULT + + for component_name, switch in self.component_switches.items(): + current_bind_vars_snapshot[f'bar_{component_name}_visible'] = switch.get_active() + + current_bind_vars_snapshot['metrics_visible'] = {k: s.get_active() for k, s in self.metrics_switches.items()} + current_bind_vars_snapshot['metrics_disks'] = [ + child.get_children()[0].get_text() for child in self.disk_entries.get_children() if isinstance(child, Gtk.Box) and child.get_children() and isinstance(child.get_children()[0], Entry) + ] + + + selected_icon_path = self.selected_face_icon + replace_lock = self.lock_switch and self.lock_switch.get_active() + replace_idle = self.idle_switch and self.idle_switch.get_active() + + if self.selected_face_icon: + self.selected_face_icon = None + self.face_status_label.label = "" + + def _apply_and_reload_task_thread(): + nonlocal current_bind_vars_snapshot + + from . import utils + utils.bind_vars.clear() + utils.bind_vars.update(current_bind_vars_snapshot) + + + start_time = time.time() + print(f"{start_time:.4f}: Background task started.") + + config_json = os.path.expanduser(f'~/{APP_NAME_CAP}/config/config.json') + os.makedirs(os.path.dirname(config_json), exist_ok=True) + try: + with open(config_json, 'w') as f: + json.dump(utils.bind_vars, f, indent=4) + print(f"{time.time():.4f}: Saved config.json.") + except Exception as e: print(f"Error saving config.json: {e}") + + if selected_icon_path: + print(f"{time.time():.4f}: Processing face icon...") + try: + img = Image.open(selected_icon_path) + side = min(img.size); left = (img.width - side)//2; top = (img.height - side)//2 + cropped_img = img.crop((left, top, left + side, top + side)) + face_icon_dest = os.path.expanduser("~/.face.icon") + cropped_img.save(face_icon_dest, format='PNG') + print(f"{time.time():.4f}: Face icon saved to {face_icon_dest}") + GLib.idle_add(self._update_face_image_widget, face_icon_dest) + except Exception as e: print(f"Error processing face icon: {e}") + print(f"{time.time():.4f}: Finished processing face icon.") + + if replace_lock: + print(f"{time.time():.4f}: Replacing hyprlock config...") + src = os.path.expanduser(f"~/{APP_NAME_CAP}/config/hypr/hyprlock.conf") + dest = os.path.expanduser("~/.config/hypr/hyprlock.conf") + if os.path.exists(src): backup_and_replace(src, dest, "Hyprlock") + else: print(f"Warning: Source hyprlock config not found at {src}") + print(f"{time.time():.4f}: Finished replacing hyprlock config.") + + if replace_idle: + print(f"{time.time():.4f}: Replacing hypridle config...") + src = os.path.expanduser(f"~/{APP_NAME_CAP}/config/hypr/hypridle.conf") + dest = os.path.expanduser("~/.config/hypr/hypridle.conf") + if os.path.exists(src): backup_and_replace(src, dest, "Hypridle") + else: print(f"Warning: Source hypridle config not found at {src}") + print(f"{time.time():.4f}: Finished replacing hypridle config.") + + print(f"{time.time():.4f}: Checking/Appending hyprland.conf source string...") + hypr_path = os.path.expanduser("~/.config/hypr/hyprland.conf") + try: + from .constants import SOURCE_STRING + needs_append = True + if os.path.exists(hypr_path): + with open(hypr_path, "r") as f: + if SOURCE_STRING.strip() in f.read(): needs_append = False + else: os.makedirs(os.path.dirname(hypr_path), exist_ok=True) + + if needs_append: + with open(hypr_path, "a") as f: f.write("\n" + SOURCE_STRING) + print(f"Appended source string to {hypr_path}") + except Exception as e: print(f"Error updating {hypr_path}: {e}") + print(f"{time.time():.4f}: Finished checking/appending hyprland.conf source string.") + + print(f"{time.time():.4f}: Running start_config()...") + start_config() + print(f"{time.time():.4f}: Finished start_config().") + + print(f"{time.time():.4f}: Initiating Ax-Shell restart using Popen...") + main_py = os.path.expanduser(f"~/{APP_NAME_CAP}/main.py") + kill_cmd = f"killall {APP_NAME}" + start_cmd = ["uwsm", "app", "--", "python", main_py] + try: + kill_proc = subprocess.Popen(kill_cmd, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + kill_proc.wait(timeout=2) + print(f"{time.time():.4f}: killall process finished (o timed out).") + except subprocess.TimeoutExpired: print("Warning: killall command timed out.") + except Exception as e: print(f"Error running killall: {e}") + + try: + subprocess.Popen(start_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True) + print(f"{APP_NAME_CAP} restart initiated via Popen.") + except FileNotFoundError as e: print(f"Error restarting {APP_NAME_CAP}: Command not found ({e})") + except Exception as e: print(f"Error restarting {APP_NAME_CAP} via Popen: {e}") + + print(f"{time.time():.4f}: Ax-Shell restart commands issued via Popen.") + end_time = time.time() + print(f"{end_time:.4f}: Background task finished (Total: {end_time - start_time:.4f}s).") + + thread = threading.Thread(target=_apply_and_reload_task_thread) + thread.daemon = True + thread.start() + print("Configuration apply/reload task started in background.") + + def _update_face_image_widget(self, icon_path): + try: + if self.face_image and self.face_image.get_window(): + pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(icon_path, 64, 64) + self.face_image.set_from_pixbuf(pixbuf) + except Exception as e: + print(f"Error reloading face icon preview: {e}") + if self.face_image and self.face_image.get_window(): + self.face_image.set_from_icon_name("image-missing", Gtk.IconSize.DIALOG) + return GLib.SOURCE_REMOVE + + def on_reset(self, widget): + dialog = Gtk.MessageDialog( + transient_for=self.get_toplevel(), flags=0, message_type=Gtk.MessageType.QUESTION, + buttons=Gtk.ButtonsType.YES_NO, text="Reset all settings to defaults?" + ) + dialog.format_secondary_text("This will reset all keybindings and appearance settings to their default values.") + response = dialog.run() + dialog.hide() # Hide the dialog first + dialog.destroy() # Then destroy it + + # Process any pending events + while Gtk.events_pending(): + Gtk.main_iteration() + + if response == Gtk.ResponseType.YES: + from . import utils + from .constants import DEFAULTS + + utils.bind_vars.clear() + utils.bind_vars.update(DEFAULTS.copy()) + + for prefix_key, suffix_key, prefix_entry, suffix_entry in self.entries: + prefix_entry.set_text(utils.bind_vars[prefix_key]) + suffix_entry.set_text(utils.bind_vars[suffix_key]) + + self.wall_dir_chooser.set_filename(utils.bind_vars['wallpapers_dir']) + + positions = ["Bottom", "Left", "Right"] + default_position = DEFAULTS.get('dock_position', "Top") + try: + self.position_combo.set_active(positions.index(default_position)) + except ValueError: + self.position_combo.set_active(0) + + + self.dock_switch.set_active(utils.bind_vars.get('dock_enabled', True)) + self.dock_auto_hide_switch.set_active(utils.bind_vars.get('dock_auto_hide', False)) + self.dock_hover_switch.set_active(utils.bind_vars.get('dock_always_occluded', False)) + self.dock_hover_switch.set_sensitive(self.dock_switch.get_active()) + self.dock_auto_hide_switch.set_sensitive(self.dock_switch.get_active()) + self.terminal_entry.set_text(utils.bind_vars['terminal_command']) + + default_dock_theme_val = DEFAULTS.get('dock_theme', "Pills") + try: + self.dock_theme_combo.set_active(self.themes.index(default_dock_theme_val)) + except ValueError: + self.dock_theme_combo.set_active(0) + + default_notif_pos_val = DEFAULTS.get(NOTIF_POS_KEY, NOTIF_POS_DEFAULT) + notification_positions_list = ["Top", "Bottom"] + try: + self.notification_pos_combo.set_active(notification_positions_list.index(default_notif_pos_val)) + except ValueError: + self.notification_pos_combo.set_active(0) + + for name, switch in self.component_switches.items(): + switch.set_active(utils.bind_vars.get(f'bar_{name}_visible', True)) + self.corners_switch.set_active(utils.bind_vars.get('corners_visible', True)) + + metrics_vis_defaults = DEFAULTS.get('metrics_visible', {}) + for k, s_widget in self.metrics_switches.items(): s_widget.set_active(metrics_vis_defaults.get(k, True)) + + + def enforce_minimum_metrics(switch_dict): + enabled_switches = [s for s in switch_dict.values() if s.get_active()] + can_disable = len(enabled_switches) > 1 + for s in switch_dict.values(): + s.set_sensitive(True if can_disable or not s.get_active() else False) + enforce_minimum_metrics(self.metrics_switches) + + + for child in list(self.disk_entries.get_children()): self.disk_entries.remove(child) + + for p in DEFAULTS.get('metrics_disks', ["/"]): + self._add_disk_edit_entry_func(p) + + self._update_panel_position_sensitivity() + + self.selected_face_icon = None + self.face_status_label.label = "" + current_face = os.path.expanduser("~/.face.icon") + try: + pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(current_face, 64, 64) if os.path.exists(current_face) else None + if pixbuf: self.face_image.set_from_pixbuf(pixbuf) + else: self.face_image.set_from_icon_name("user-info", Gtk.IconSize.DIALOG) + except Exception: self.face_image.set_from_icon_name("image-missing", Gtk.IconSize.DIALOG) + + if self.lock_switch: self.lock_switch.set_active(False) + if self.idle_switch: self.idle_switch.set_active(False) + print("Settings reset to defaults.") + + def on_close(self, widget): + if self.application: self.application.quit() + else: self.destroy() diff --git a/config/settings/utils.py b/config/settings/utils.py new file mode 100644 index 0000000..1c7e2a4 --- /dev/null +++ b/config/settings/utils.py @@ -0,0 +1,349 @@ +import json +import os +import shutil +import subprocess +import time +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 + +# 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á + +def deep_update(target: dict, update: dict) -> dict: + """ + Recursively update a nested dictionary with values from another dictionary. + Modifies target in-place. + """ + for key, value in update.items(): + if isinstance(value, dict) and key in target and isinstance(target[key], dict): + # Si el valor es un diccionario y la clave ya existe en target como diccionario, + # entonces actualiza recursivamente. + deep_update(target[key], value) + 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 + +def ensure_matugen_config(): + """ + Ensure that the matugen configuration file exists and is updated + 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' + ], + '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} + } + }, + '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()' &" + } + } + } + + 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: + existing_config = toml.load(f) + 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 + except Exception as e: + print(f"Error reading or backing up {config_path}: {e}") + # existing_config podría estar parcialmente cargado o vacío. + # Continuar para intentar fusionar con defaults. + + # Usamos una copia de existing_config para deep_update si no queremos modificarlo directamente + # o asegurarse que deep_update no lo haga si no es deseado. + # La implementación actual de deep_update modifica 'target'. + # 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 + + try: + 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): + 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") + 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 + 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): + print(f"Generating color theme from wallpaper: {image_path}") + try: + matugen_cmd = f"matugen image '{image_path}'" + exec_shell_command_async(matugen_cmd) + print("Matugen color theme generation initiated.") + except FileNotFoundError: + print("Error: matugen command not found. Please install matugen.") + 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.") + + +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 + + # 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 + + 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: + saved_vars = json.load(f) + # 3. Usar deep_update para fusionar saved_vars en el bind_vars existente. + deep_update(bind_vars, saved_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']: + # 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] + # Si la clave no está en bind_vars o no es un diccionario después de deep_update, + # restaurarla desde una copia de DEFAULTS para esa clave. + if not isinstance(bind_vars.get(vis_key), dict): + bind_vars[vis_key] = default_sub_dict.copy() + else: + # Si es un diccionario, asegurar que todas las sub-claves de DEFAULTS estén presentes. + current_sub_dict = bind_vars[vis_key] + for m_key, m_val in default_sub_dict.items(): + 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).") + # 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).") + # 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).") + + +def generate_hyprconf() -> str: + """ + Generate the Hypr configuration string using the current bind_vars. + """ + home = os.path.expanduser('~') + # Determine animation type based on bar position + 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 +exec-once = wl-paste --type text --watch cliphist store +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 + +# Wallpapers directory: {bind_vars.get('wallpapers_dir', '~/Modus/assets/wallpapers_example')} + +source = {home}/{APP_NAME_CAP}/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, {animation_type} 20% +}} +""" + +def ensure_face_icon(): + """ + Ensure the face icon exists. If not, copy the default icon. + """ + face_icon_path = os.path.expanduser("~/.face.icon") + default_icon_path = os.path.expanduser(f"~/{APP_NAME_CAP}/assets/default.png") + if not os.path.exists(face_icon_path) and os.path.exists(default_icon_path): + try: + shutil.copy(default_icon_path, face_icon_path) + 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. + """ + try: + if os.path.exists(dest): + backup_path = dest + ".bak" + # Asegurarse que el directorio de backup existe si es diferente + # 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 + shutil.copy(src, dest) + print(f"{config_name} config replaced from {src}") + except Exception as e: + print(f"Error backing up/replacing {config_name} config: {e}") + + +def start_config(): + """ + Run final configuration steps: ensure necessary configs, write the hyprconf, and reload. + """ + print(f"{time.time():.4f}: start_config: Ensuring matugen config...") + ensure_matugen_config() + print(f"{time.time():.4f}: start_config: Ensuring face icon...") + ensure_face_icon() + print(f"{time.time():.4f}: start_config: Generating hypr conf...") + + hypr_config_dir = os.path.expanduser(f"~/{APP_NAME_CAP}/config/hypr/") + os.makedirs(hypr_config_dir, exist_ok=True) + # Usar APP_NAME para el nombre del archivo .conf para que coincida con SOURCE_STRING corregido + hypr_conf_path = os.path.join(hypr_config_dir, f"{APP_NAME}.conf") + try: + with open(hypr_conf_path, "w") as f: + f.write(generate_hyprconf()) + print(f"Generated Hyprland config at {hypr_conf_path}") + except Exception as e: + print(f"Error writing Hyprland config: {e}") + print(f"{time.time():.4f}: start_config: Finished generating hypr conf.") + + 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.") + 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}") + except Exception as e: + print(f"An error occurred initiating hyprctl reload: {e}") + print(f"{time.time():.4f}: start_config: Finished initiating hyprctl reload.") diff --git a/main.css b/main.css new file mode 100644 index 0000000..e83999a --- /dev/null +++ b/main.css @@ -0,0 +1,45 @@ +@import url("./styles/dock.css"); +@import url("./styles/colors.css"); +@import url("./styles/components.css"); +@import url("./styles/controls.css"); +@import url("./styles/indicators.css"); + +* { + all: unset; + color: var(--foreground); + font-size: unset; + font-family: unset; + border-radius: 16px; +} + +#corner { + background-color: var(--shadow); + border-radius: 0; +} + +#corner-container { + min-width: 20px; + min-height: 20px; +} + +tooltip { + border: solid 1px; + border-color: var(--surface); + background-color: var(--shadow); + animation: tooltipShow 0.25s cubic-bezier(0.5, 0.25, 0, 1); +} + +@keyframes tooltipShow { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +tooltip > * { + padding: 6px 10px; + border-radius: 10px; +} diff --git a/main.py b/main.py index 7488f5d..bd75a0d 100644 --- a/main.py +++ b/main.py @@ -1,18 +1,16 @@ import os +import gi import setproctitle from fabric import Application -from fabric.utils import get_relative_path, monitor_file, exec_shell_command_async +from fabric.utils import exec_shell_command_async, get_relative_path, monitor_file from loguru import logger -from modules.bar.bar import Bar, Corners -from modules.launcher.launcher import Launcher - -from modules.dock import Dock -from modules.notification_popup import NotificationPopup -from modules.osd import OSD -from config.data import APP_NAME, CACHE_DIR, CONFIG_FILE, DOCK_ICON_SIZE, VERTICAL, APP_NAME_CAP +from config.data import APP_NAME, APP_NAME_CAP, CONFIG_FILE +from modules.corners import Corners +from modules.dock.main import Dock +gi.require_version("GLib", "2.0") for log in [ "fabric.hyprland.widgets", @@ -21,23 +19,31 @@ for log in [ ]: logger.disable(log) - - if __name__ == "__main__": setproctitle.setproctitle(APP_NAME) if not os.path.isfile(CONFIG_FILE): - exec_shell_command_async(f"python {get_relative_path('../config/config.py')}") - bar = Bar() - dock = Dock() - corners = Corners() - osd = OSD() - notif = NotificationPopup() - launcher = Launcher() - bar.launcher = launcher - launcher.bar = bar + config_script_path = get_relative_path("config/config.py") + exec_shell_command_async(f"python {config_script_path}") - app = Application(f"{APP_NAME}", bar, launcher, osd, dock) + current_wallpaper = os.path.expanduser("~/.current.wall") + if not os.path.exists(current_wallpaper): + example_wallpaper = os.path.expanduser( + f"~/.config/{APP_NAME_CAP}/assets/wallpapers_example/example-1.jpg" + ) + os.symlink(example_wallpaper, current_wallpaper) + + # Load configuration + from config.data import load_config + + config = load_config() + + corners = Corners() + dock = Dock() + + # Set corners visibility based on config + corners_visible = config.get("corners_visible", True) + corners.set_visible(corners_visible) # Monitor CSS files for changes css_file = monitor_file(get_relative_path("styles")) @@ -46,13 +52,14 @@ if __name__ == "__main__": color_css_file = monitor_file(get_relative_path("./styles/colors.css")) _ = color_css_file.connect("changed", lambda *_: set_css()) + app = Application( + f"{APP_NAME}", dock, corners + ) # Make sure corners is added to the app + def set_css(): - logger.info("[Main] Applying CSS") - app.set_stylesheet_from_file(get_relative_path("styles/main.css"), - exposed_functions={ - "dock_nmargin": lambda: f"margin-bottom: -{32 + DOCK_ICON_SIZE}px;" if not VERTICAL else f"margin-right: -{32 + DOCK_ICON_SIZE}px;", - "dock_sep": lambda: f"margin: 8px 0;" if not VERTICAL else f"margin: 0 8px;", - },) + app.set_stylesheet_from_file( + get_relative_path("main.css"), + ) app.set_css = set_css diff --git a/modules/bar/bar.py b/modules/bar/bar.py deleted file mode 100644 index 61a2653..0000000 --- a/modules/bar/bar.py +++ /dev/null @@ -1,312 +0,0 @@ -from fabric.hyprland.widgets import Language, get_hyprland_connection -from fabric.hyprland.service import HyprlandEvent -from fabric.system_tray.widgets import SystemTray -from fabric.widgets.shapes import Corner -from fabric.widgets.box import Box -from fabric.widgets.button import Button -from fabric.widgets.centerbox import CenterBox -from fabric.widgets.datetime import DateTime -from fabric.widgets.label import Label -from fabric.widgets.wayland import WaylandWindow as Window -from modules.bar.components import Metrics, workspace, SystemIndicators, UpdatesWidget -import config.data as data -from gi.repository import Gdk, GLib -import utils.icons as icons -import os -import json - - -class Bar(Window): - def __init__(self, **kwargs): - super().__init__( - name="bar", - layer="top", - anchor="left top right" - if not data.VERTICAL and not data.BOTTOM_BAR - else "top right bottom" - if data.VERTICAL and data.VERTICAL_RIGHT_ALIGN - else "top left bottom" - if data.VERTICAL - else "bottom left right", - exclusivity="auto", - visible=True, - all_visible=True, - ) - - self.launcher = kwargs.get("launcher", None) - self.component_visibility = data.BAR_COMPONENTS_VISIBILITY - - # Determine if we should use vertical layout for components - is_vertical_layout = data.VERTICAL - - self.workspaces = Button( - child=workspace, - name="workspaces", - ) - - self.connection = get_hyprland_connection() - self.tray = SystemTray( - name="tray", - spacing=4, - icon_size=16, - orientation="h" if not is_vertical_layout else "v", - ) - - self.date_time = Button( - name="datetime", - child=DateTime( - formatters=["%-I:%M:%p 󰧞 %a %d %b"] - if not is_vertical_layout - else ["%I\n%M\n%p"], - h_align="center" if not is_vertical_layout else "fill", - v_align="center", - ), - on_clicked=lambda *_: self.calendar(), - ) - self.date_time.connect("enter_notify_event", self.on_button_enter) - self.date_time.connect("leave_notify_event", self.on_button_leave) - - self.button_app = Button( - name="button-bar", - child=Label(name="button-bar-label", markup=icons.apps), - on_clicked=lambda *_: self.search_apps(), - ) - self.button_app.connect("enter_notify_event", self.on_button_enter) - self.button_app.connect("leave_notify_event", self.on_button_leave) - - self.updates = UpdatesWidget() - self.updates.connect("enter_notify_event", self.on_button_enter) - self.updates.connect("leave_notify_event", self.on_button_leave) - - self.lang_label = Label(name="lang-label") - self.language = Button( - name="language", h_align="center", v_align="center", child=self.lang_label - ) - self.on_language_switch() - self.connection.connect("event::activelayout", self.on_language_switch) - - self.metrics = Metrics() - self.indicators = SystemIndicators(launcher=self.launcher) - - self.applets = Box( - name="system-indicator", - spacing=4, - orientation="h" if not is_vertical_layout else "v", - children=[self.language, self.indicators], - ) - - # Apply visibility settings to components - self.apply_component_visibility() - - self.h_start_children = [self.button_app, self.updates] - - self.h_center_children = [ - self.date_time, - self.workspaces, - self.metrics, - self.applets, - ] - - self.h_end_children = [self.tray] - - self.v_start_children = [ - self.button_app, - self.updates, - self.workspaces, - self.metrics, - ] - self.v_end_children = [self.tray, self.applets, self.date_time] - - self.v_all_children = [] - self.v_all_children.extend(self.v_start_children) - self.v_all_children.extend(self.v_end_children) - - # Use centered layout when both vertical and centered_bar are enabled - is_centered_bar = is_vertical_layout and getattr(data, "CENTERED_BAR", False) - - self.bar = CenterBox( - name="bar", - orientation="h" if not is_vertical_layout else "v", - h_align="fill", - v_align="fill", - start_children=None - if is_centered_bar - else Box( - name="start-container", - spacing=4, - orientation="h" if not is_vertical_layout else "v", - children=self.h_start_children - if not is_vertical_layout - else self.v_start_children, - ), - center_children=Box( - orientation="v", - spacing=4, - children=self.v_all_children - if is_centered_bar - else Box( - name="center-container", - spacing=4, - orientation="h" if not is_vertical_layout else "v", - children=self.h_center_children if not is_vertical_layout else None, - ), - ) - if is_vertical_layout - else Box( - name="center-container", - spacing=4, - orientation="h" if not is_vertical_layout else "v", - children=self.h_center_children if not is_vertical_layout else None, - ), - end_children=None - if is_centered_bar - else Box( - name="end-container", - spacing=4, - orientation="h" if not is_vertical_layout else "v", - children=self.h_end_children - if not is_vertical_layout - else self.v_end_children, - ), - ) - - self.children = self.bar - - def apply_component_visibility(self): - """Apply saved visibility settings to all components""" - components = { - "metrics": self.metrics, - "indicators": self.applets, - "updates": self.updates, - "workspaces": self.workspaces, - "button_app": self.button_app, - # "button_tools": self.button_tools, - "language": self.language, - "date_time": self.date_time, - "tray": self.tray, - } - - for component_name, widget in components.items(): - if component_name in self.component_visibility: - widget.set_visible(self.component_visibility[component_name]) - - def toggle_component_visibility(self, component_name): - """Toggle visibility for a specific component""" - components = { - "metrics": self.metrics, - "indicators": self.applets, - "updates": self.updates, - "workspaces": self.workspaces, - "button_app": self.button_app, - # "button_tools": self.button_tools, - "language": self.language, - "date_time": self.date_time, - "tray": self.tray, - } - - if component_name in components and component_name in self.component_visibility: - # Toggle the visibility state - self.component_visibility[component_name] = not self.component_visibility[ - component_name - ] - # Apply the new state - components[component_name].set_visible( - self.component_visibility[component_name] - ) - - # Update the configuration - config_file = os.path.expanduser( - f"~/{data.APP_NAME}/config/assets/config.json" - ) - try: - if os.path.exists(config_file): - with open(config_file, "r") as f: - config = json.load(f) - - # Update the config with the new visibility state - config[f"bar_{component_name}_visible"] = self.component_visibility[ - component_name - ] - - # Write the updated config back to file - with open(config_file, "w") as f: - json.dump(config, f, indent=4) - except (json.JSONDecodeError, IOError) as e: - print(f"Error updating config file: {e}") - # Revert the visibility change if config update fails - self.component_visibility[ - component_name - ] = not self.component_visibility[component_name] - components[component_name].set_visible( - self.component_visibility[component_name] - ) - - return self.component_visibility[component_name] - - return None - - def on_button_enter(self, widget, event): - window = widget.get_window() - if window: - window.set_cursor(Gdk.Cursor(Gdk.CursorType.HAND2)) - - def on_button_leave(self, widget, event): - window = widget.get_window() - if window: - window.set_cursor(None) - - def search_apps(self): - self.launcher.open("launcher") - - def calendar(self): - self.launcher.open("calendar") - - def on_language_switch(self, _=None, event: HyprlandEvent = None): - lang = event.data[1] if event else Language().get_label() - self.language.set_tooltip_text(lang) - if not data.VERTICAL: - self.lang_label.set_label(lang[:2].lower()) - else: - self.lang_label.add_style_class("icon") - self.lang_label.set_markup(icons.keyboard) - - -class MyCorner(Box): - def __init__(self, position): - super().__init__( - name="corner-container", - children=Corner( - name="corner", - orientation=position, - size=20, - ), - ) - - -class Corners(Window): - def __init__(self): - super().__init__( - layer="top", - anchor="top left bottom right", - pass_through=True, - child=Box( - orientation="vertical", - children=[ - Box( - children=[ - MyCorner("top-left"), - Box(h_expand=True), - MyCorner("top-right"), - ] - ), - Box(v_expand=True), - Box( - children=[ - MyCorner("bottom-left"), - Box(h_expand=True), - MyCorner("bottom-right"), - ] - ), - ], - ), - ) diff --git a/modules/bar/components/__init__.py b/modules/bar/components/__init__.py deleted file mode 100644 index 161c235..0000000 --- a/modules/bar/components/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .metrics import Metrics -from .workspace import workspace -from .system_indicators import SystemIndicators -from .updates import UpdatesWidget diff --git a/modules/bar/components/metrics.py b/modules/bar/components/metrics.py deleted file mode 100644 index b716b4d..0000000 --- a/modules/bar/components/metrics.py +++ /dev/null @@ -1,368 +0,0 @@ -import subprocess -import json -import psutil -from gi.repository import GLib, Gdk - -from fabric.widgets.label import Label -from fabric.widgets.box import Box -from fabric.widgets.circularprogressbar import CircularProgressBar -from fabric.widgets.revealer import Revealer -from fabric.core.fabricator import Fabricator -from fabric.widgets.button import Button -import utils.icons as icons -import config.data as data - - -class MetricsProvider: - def __init__(self): - self.cpu = 0.0 - self.mem = 0.0 - self.swap = 0.0 - self.disk = [] - self.bat_percent = 0.0 - self.bat_charging = None - - GLib.timeout_add_seconds(1, self._update) - - def _update(self): - # Get non-blocking usage percentages (interval=0) - # The first call may return 0, but subsequent calls will provide consistent values. - self.cpu = psutil.cpu_percent(interval=0) - mem = psutil.virtual_memory() - self.mem = mem.percent - swap = psutil.swap_memory() - self.swap = swap.percent - self.disk = [psutil.disk_usage(path).percent for path in data.BAR_METRICS_DISKS] - - battery = psutil.sensors_battery() - if battery is None: - self.bat_percent = 0.0 - self.bat_charging = None - else: - self.bat_percent = battery.percent - self.bat_charging = battery.power_plugged - return True - - def get_metrics(self): - return (self.cpu, self.mem, self.swap, self.disk) - - def get_battery(self): - return (self.bat_percent, self.bat_charging) - - -shared_provider = MetricsProvider() - - -class SingularMetric: - def __init__(self, id, name, icon): - self.is_vertical_layout = data.VERTICAL - self.name_markup = name - self.icon_markup = icon - - self.icon = Label(name="metrics-icon", markup=icon) - self.circle = CircularProgressBar( - name="metrics-circle", - value=0, - size=26, - line_width=2, - start_angle=150, - end_angle=390, - style_classes=id, - child=self.icon, - ) - - self.circle_button = Button( - name=f"metrics-{id}-button", - child=self.circle, - style="padding: 0; margin: 0; background: none;", - ) - - self.circle_button.add_events( - Gdk.EventMask.ENTER_NOTIFY_MASK | Gdk.EventMask.LEAVE_NOTIFY_MASK - ) - self.circle_button.connect("enter-notify-event", self.on_button_enter) - self.circle_button.connect("leave-notify-event", self.on_button_leave) - - self.circle_button.connect("clicked", self.on_circle_clicked) - - self.level = Label( - name="metrics-level", - style_classes=id, - label="0%", - style="margin: 4px 0px;" if self.is_vertical_layout else "margin: 0 4px;", - ) - self.revealer = Revealer( - name=f"metrics-{id}-revealer", - transition_duration=250, - transition_type="slide-up" if self.is_vertical_layout else "slide-left", - child=self.level, - child_revealed=False, - ) - - self.box = Box( - name=f"metrics-{id}-box", - orientation="v" if self.is_vertical_layout else "h", - spacing=0, - children=[self.circle_button, self.revealer], - ) - - # Connect events for cursor changes to the box as well - self.box.connect("enter-notify-event", self.on_button_enter) - self.box.connect("leave-notify-event", self.on_button_leave) - - # Set initial tooltip - moved after box is created - self.update_tooltip("0%") - - def update_tooltip(self, value_text): - tooltip = f"{self.icon_markup} {self.name_markup}: {value_text}" - # self.box.set_tooltip_markup(tooltip) - self.circle_button.set_tooltip_markup(tooltip) - self.circle.set_tooltip_markup(tooltip) - self.icon.set_tooltip_markup(tooltip) - - def markup(self): - """Get formatted text for tooltip display""" - return f"{self.icon_markup} {self.name_markup}: {self.level.get_label()}" - - def on_button_enter(self, widget, event): - window = widget.get_window() - if window: - window.set_cursor(Gdk.Cursor(Gdk.CursorType.HAND2)) - - def on_button_leave(self, widget, event): - window = widget.get_window() - if window: - window.set_cursor(None) - - def on_circle_clicked(self, button): - # Toggle the revealed state of the label - current_state = self.revealer.get_child_revealed() - self.revealer.set_reveal_child(not current_state) - - -class Metrics(Box): - def __init__(self, **kwargs): - # Determine if we should use vertical layout for components - self.is_vertical_layout = data.VERTICAL - - super().__init__( - name="metrics", - spacing=0, - orientation="h" if not self.is_vertical_layout else "v", - visible=True, - all_visible=True, - style="padding:4px 0px" if self.is_vertical_layout else "padding: 0px 4px;", - **kwargs, - ) - - self.batt_fabricator = Fabricator( - poll_from=lambda v: shared_provider.get_battery(), - on_changed=lambda f, v: self.update_battery, - interval=1000, - stream=False, - default_value=0, - ) - self.batt_fabricator.changed.connect(self.update_battery) - GLib.idle_add(self.update_battery, None, shared_provider.get_battery()) - - visible = getattr( - data, - "METRICS_VISIBLE", - {"cpu": True, "ram": True, "disk": True, "swap": True}, - ) - disks = ( - [ - SingularMetric( - "disk", - f"DISK ({path})" if len(data.BAR_METRICS_DISKS) != 1 else "DISK", - icons.disk, - ) - for path in data.BAR_METRICS_DISKS - ] - if visible.get("disk", True) - else [] - ) - - self.cpu = ( - SingularMetric("cpu", "CPU", icons.cpu) - if visible.get("cpu", True) - else None - ) - self.ram = ( - SingularMetric("ram", "RAM", icons.memory) - if visible.get("ram", True) - else None - ) - self.swap = ( - SingularMetric("swap", "SWAP", icons.swap) - if visible.get("swap", True) - else None - ) - self.bat = ( - SingularMetric("bat", "BATTERY", icons.battery) - if visible.get("bat", True) - else None - ) - self.disk = disks - - # Add only enabled metrics - for disk in self.disk: - self.add(disk.box) - self.add(Box(name="metrics-sep")) - if self.ram: - self.add(self.ram.box) - self.add(Box(name="metrics-sep")) - if self.cpu: - self.add(self.cpu.box) - if self.swap: - self.add(Box(name="metrics-sep")) - self.add(self.swap.box) - if self.bat: - self.add(Box(name="metrics-sep")) - self.add(self.bat.box) - - # Connect events directly to the button - self.connect("enter-notify-event", self.on_mouse_enter) - self.connect("leave-notify-event", self.on_mouse_leave) - - GLib.timeout_add_seconds(1, self.update_metrics) - - self.hide_timer = None - self.hover_counter = 0 - - def _format_percentage(self, value: int) -> str: - return f"{value}%" - - def on_mouse_enter(self, widget, event): - window = widget.get_window() - if window: - window.set_cursor(Gdk.Cursor(Gdk.CursorType.HAND2)) - - if not data.VERTICAL: - self.hover_counter += 1 - if self.hide_timer is not None: - GLib.source_remove(self.hide_timer) - self.hide_timer = None - if self.cpu: - self.cpu.revealer.set_reveal_child(True) - if self.ram: - self.ram.revealer.set_reveal_child(True) - for disk in self.disk: - disk.revealer.set_reveal_child(True) - if self.swap: - self.swap.revealer.set_reveal_child(True) - if self.bat: - self.bat.revealer.set_reveal_child(True) - return False - - def on_mouse_leave(self, widget, event): - window = widget.get_window() - if window: - window.set_cursor(None) - - if not data.VERTICAL: - if self.hover_counter > 0: - self.hover_counter -= 1 - if self.hover_counter == 0: - if self.hide_timer is not None: - GLib.source_remove(self.hide_timer) - self.hide_timer = GLib.timeout_add(500, self.hide_revealer) - return False - - def hide_revealer(self): - if not data.VERTICAL: - if self.cpu: - self.cpu.revealer.set_reveal_child(False) - if self.ram: - self.ram.revealer.set_reveal_child(False) - for disk in self.disk: - disk.revealer.set_reveal_child(False) - self.hide_timer = None - return False - - def update_metrics(self): - cpu, mem, swap, disk = shared_provider.get_metrics() - if self.cpu: - self.cpu.circle.set_value(cpu / 100.0) - value_text = self._format_percentage(int(cpu)) - self.cpu.level.set_label(value_text) - self.cpu.update_tooltip(value_text) - if self.ram: - self.ram.circle.set_value(mem / 100.0) - value_text = self._format_percentage(int(mem)) - self.ram.level.set_label(value_text) - self.ram.update_tooltip(value_text) - for i, disk_metric in enumerate(self.disk): - if i < len(disk): - value_text = self._format_percentage(int(disk[i])) - disk_metric.circle.set_value(disk[i] / 100.0) - disk_metric.level.set_label(value_text) - disk_metric.update_tooltip(value_text) - if self.swap: - self.swap.circle.set_value(swap / 100.0) - value_text = self._format_percentage(int(swap)) - self.swap.level.set_label(value_text) - self.swap.update_tooltip(value_text) - if self.bat: - bat_percent, _ = shared_provider.get_battery() - self.bat.circle.set_value(bat_percent / 100.0) - value_text = self._format_percentage(int(bat_percent)) - self.bat.level.set_label(value_text) - self.bat.update_tooltip(value_text) - - # Tooltip: only show enabled metrics - tooltip_metrics = [] - if self.disk: tooltip_metrics.extend(self.disk) - if self.swap: tooltip_metrics.append(self.swap) - if self.bat: tooltip_metrics.append(self.bat) - if self.ram: tooltip_metrics.append(self.ram) - if self.cpu: tooltip_metrics.append(self.cpu) - self.set_tooltip_markup((" - " if not data.VERTICAL else "\n").join([v.markup() for v in tooltip_metrics])) - - return True - - def update_battery(self, sender, battery_data): - if self.bat is None: - return - - value, charging = battery_data - if value == 0: - if self.bat: - self.bat.box.set_visible(False) - else: - if self.bat: - self.bat.box.set_visible(True) - self.bat.circle.set_value(value / 100) - percentage = int(value) - self.bat.level.set_label(self._format_percentage(percentage)) - - # Apply alert styling if battery is low AND not charging - if percentage <= 15 and charging == False: - self.bat.icon.add_style_class("alert") - self.bat.circle.add_style_class("alert") - else: - self.bat.icon.remove_style_class("alert") - self.bat.circle.remove_style_class("alert") - - # Choose the icon based on charging state first, then battery level - if percentage == 100: - self.bat.icon.set_markup(icons.battery) - charging_status = f"{icons.bat_full} Fully Charged" - elif charging == True: - self.bat.icon.set_markup(icons.charging) - charging_status = f"{icons.bat_charging} Charging" - elif percentage <= 15 and charging == False: - self.bat.icon.set_markup(icons.alert) - charging_status = f"{icons.bat_low} Low Battery" - elif charging == False: - self.bat.icon.set_markup(icons.discharging) - charging_status = f"{icons.bat_discharging} Discharging" - else: - self.bat.icon.set_markup(icons.battery) - charging_status = "Battery" - - # Update individual tooltip for battery - self.bat.update_tooltip(f"{charging_status}: {self._format_percentage(percentage)}") - - # Update the combined tooltip - self.update_metrics() diff --git a/modules/bar/components/system_indicators.py b/modules/bar/components/system_indicators.py deleted file mode 100644 index 7d9b453..0000000 --- a/modules/bar/components/system_indicators.py +++ /dev/null @@ -1,258 +0,0 @@ -import subprocess -from fabric import Fabricator -from fabric.widgets.label import Label -from fabric.widgets.box import Box -from fabric.widgets.button import Button -from fabric.bluetooth import BluetoothClient -from services import network_client, audio, notification_service -import utils.icons as icons -import config.data as data - - -class SystemIndicators(Box): - def __init__(self, **kwargs): - # Determine if we should use vertical layout for components - is_vertical_layout = data.VERTICAL - - super().__init__( - orientation="h" if not is_vertical_layout else "v", spacing=0, **kwargs - ) - self.launcher = kwargs.get("launcher", None) - - self.bluetooth_icon = Label(name="system-indicator-icon") - self.wifi_icon = Label(name="system-indicator-icon") - self.volume_icon_button = Label(name="system-indicator-icon") - self.microphone_icon = Label(name="system-indicator-icon") - self.idle_label = Label(name="system-indicator-icon") - self.night_label = Label(name="system-indicator-icon") - self.power_label = Label(name="system-indicator-icon") - self.notification_icon = Button(name="system-indicator-icon") - self.notification_label = Label(name="system-notif-label") - self.notification_icon.add(self.notification_label) - self.notification_icon.connect("clicked", self.open_notif_center) - - for widget in [ - self.bluetooth_icon, - self.wifi_icon, - self.volume_icon_button, - self.microphone_icon, - self.idle_label, - self.night_label, - self.power_label, - self.notification_icon, - ]: - self.add(widget) - - self.audio_service = audio - self.bluetooth_client = BluetoothClient() - self.network_client = network_client - self.notification_service = notification_service - - # Connect signals - self.bluetooth_client.connect("changed", self.update_bluetooth_status) - self.audio_service.connect("microphone_changed", self.update_mic_status) - self.audio_service.connect("changed", self.update_volume_status) - self.network_client.connect("device_ready", self.update_network_status) - self.notification_service.connect("dnd", self.update_notification_status) - self.notification_service.connect( - "notification_count", self.update_notification_status - ) - - if self.network_client.wifi_device: - self.network_client.wifi_device.connect( - "changed", self.update_network_status - ) - - if self.network_client.ethernet_device: - self.network_client.ethernet_device.connect( - "changed", self.update_network_status - ) - - Fabricator(interval=1000, poll_from=self.update_all_statuses) - - self.update_bluetooth_status() - self.update_notification_status() - - def update_all_statuses(self, *_args): - self.update_idle_night_status() - self.update_power_profile() - self.update_network_status() - - def update_idle_night_status(self, *_): - # Update idle status - has_idle = ( - subprocess.run(["pgrep", "-x", "wlinhibit"], capture_output=True).returncode - == 0 - ) - self.idle_label.set_visible(has_idle) - self.idle_label.set_markup(icons.coffee if has_idle else "") - - # Update night mode status - has_night = ( - subprocess.run( - ["pgrep", "-x", "hyprsunset"], capture_output=True - ).returncode - == 0 - ) - self.night_label.set_visible(has_night) - self.night_label.set_markup(icons.night if has_night else "") - - def update_volume_status(self, *_): - stream = self.audio_service.speaker - if not stream: - self.volume_icon_button.set_visible(False) - return - - self.volume_icon_button.set_visible(True) - # Normalize volume to integer percentage (0-100) - volume_level = stream.volume - if volume_level > 1: - volume_level = min(int(volume_level), 100) - else: - volume_level = int(volume_level * 100) - - is_muted = stream.muted - - if is_muted: - icon = icons.vol_off - elif volume_level > 74: - icon = icons.vol_high - elif volume_level > 0: - icon = icons.vol_medium - else: - icon = icons.vol_mute - - self.volume_icon_button.set_markup(icon) - - tooltip_text = "Muted" if is_muted else f"Volume: {volume_level}%" - self.volume_icon_button.set_tooltip_text(tooltip_text) - - def update_mic_status(self, *_): - mic = self.audio_service.microphone - if not mic: - self.microphone_icon.set_visible(False) - return - - self.microphone_icon.set_visible(True) - # Normalize volume to integer percentage (0-100) - volume_level = mic.volume - if volume_level > 1: - volume_level = min(int(volume_level), 100) - else: - volume_level = int(volume_level * 100) - - is_muted = mic.muted - icon = icons.mic_muted if is_muted else icons.mic - - self.microphone_icon.set_markup(icon) - tooltip_text = "Muted" if is_muted else f"Microphone: {volume_level}%" - self.microphone_icon.set_tooltip_text(tooltip_text) - - def update_bluetooth_status(self, *_): - if self.bluetooth_client.enabled: - self.bluetooth_icon.set_visible(True) - # Get a list of connected devices - connected_devices = [ - device.name - for device in self.bluetooth_client.devices - if device.connected - ] - - if connected_devices: - self.bluetooth_icon.set_markup(icons.bluetooth_connected) - self.bluetooth_icon.set_tooltip_text(f"{', '.join(connected_devices)}") - else: - self.bluetooth_icon.set_markup(icons.bluetooth) - self.bluetooth_icon.set_tooltip_text( - "Bluetooth is enabled, no devices connected" - ) - else: - self.bluetooth_icon.set_visible(True) # Keep it visible - self.bluetooth_icon.set_markup(icons.bluetooth_off) - self.bluetooth_icon.set_tooltip_text("Bluetooth is disabled") - - def update_network_status(self, *_): - primary_device = self.network_client.primary_device - self.wifi_icon.set_visible(True) - - if primary_device == "wifi" and self.network_client.wifi_device: - wifi_device = self.network_client.wifi_device - - self.wifi_icon.set_tooltip_text( - wifi_device.ssid if wifi_device.ssid else "No WiFi Connection" - ) - - if wifi_device.enabled: - strength = wifi_device.strength - - if strength > 0: - if strength < 25: - icon_label = icons.wifi_0 - elif strength < 50: - icon_label = icons.wifi_1 - elif strength < 75: - icon_label = icons.wifi_2 - else: - icon_label = icons.wifi_3 - else: - icon_label = icons.wifi_off - else: - icon_label = icons.wifi_off - - elif primary_device == "wired": - icon_label = icons.lan - self.wifi_icon.set_tooltip_text("Wired Connection") - - else: - icon_label = icons.wifi_off # No network - - self.wifi_icon.set_markup(icon_label) - - def update_power_profile(self, *_): - try: - result = subprocess.run( - ["powerprofilesctl", "get"], capture_output=True, text=True - ) - current_profile = result.stdout.strip() - - profile_icons = { - "performance": icons.power_performance, - "balanced": "", - "power-saver": icons.power_saving, - } - - icon = profile_icons.get(current_profile) - tooltip = ( - f"Power Profile: {current_profile.capitalize()}" - if current_profile - else "Unknown Profile" - ) - - self.power_label.set_visible(bool(icon)) # Only show if there's an icon - self.power_label.set_markup(icon) - self.power_label.set_tooltip_text(tooltip) - - except Exception as e: - self.power_label.set_visible(False) - self.power_label.set_tooltip_text(f"Error fetching power profile: {e}") - - def update_notification_status(self, *_): - """Update the notification icon based on DND state and notification count.""" - count = self.notification_service.count - - if count == 0 and not self.notification_service.dont_disturb: - self.notification_icon.set_visible(False) - return - - if self.notification_service.dont_disturb: - self.notification_label.set_markup(icons.notifications_off) - self.notification_icon.set_tooltip_text("Do Not Disturb: On") - self.notification_icon.set_visible(True) - else: - # At this point count must be > 0 - self.notification_label.set_markup(icons.notifications_clear) - self.notification_icon.set_tooltip_text(f"Notifications: {count}") - self.notification_icon.set_visible(True) - - def open_notif_center(self, *_): - self.launcher.open("notification-center") diff --git a/modules/bar/components/updates.py b/modules/bar/components/updates.py deleted file mode 100644 index f43add2..0000000 --- a/modules/bar/components/updates.py +++ /dev/null @@ -1,115 +0,0 @@ -import json -from fabric.utils import exec_shell_command_async, get_relative_path, invoke_repeater -from fabric.widgets.box import Box -from fabric.widgets.centerbox import CenterBox -from fabric.widgets.label import Label -from fabric.widgets.button import Button -from fabric.widgets.stack import Stack -from gi.repository import Gtk -import utils.icons as icons -import config.data as data - - -class UpdatesWidget(Button): - """A widget to display the number of available updates.""" - - def __init__( - self, - **kwargs, - ): - # Determine if we should use vertical layout for components - is_vertical_layout = data.VERTICAL - - # Initialize the button with specific name and style - super().__init__( - name="update", - orientation="h" if not is_vertical_layout else "v", - **kwargs, - ) - - self.script_file = get_relative_path("../../../config/scripts/systemupdates.sh") - - self.update_level_label = Label( - name="update-label", - label="0", - visible=False, # Initially hidden - ) - - self.update_icon = Label(name="update-icon", markup=icons.update) - self.updated_icon = Label(name="updated-icon", markup=icons.updated) - - # Create boxes with proper orientation - self.update_box = CenterBox( - orientation="v" if is_vertical_layout else "h", - center_children=[self.update_icon, self.update_level_label], - ) - - self.updated_box = CenterBox( - orientation="v" if is_vertical_layout else "h", - center_children=[self.updated_icon], - ) - - # Stack to switch between states - self.stack = Stack() - self.stack.add_named(self.update_box, "update_box") - self.stack.add_named(self.updated_box, "updated_box") - self.stack.set_visible_child_name("update_box") - - self.children = Box( - orientation="v" if is_vertical_layout else "h", - children=self.stack, - ) - - # Configure stack transitions - self.stack.set_homogeneous(False) - self.stack.set_transition_type(Gtk.StackTransitionType.CROSSFADE) - - # Connect click handler - self.connect("button-press-event", self.on_button_press) - - # Check for updates every minute - invoke_repeater(60000, self.update, initial_call=True) - - def update_values(self, value: str) -> bool: - """Update the widget state based on update check results""" - try: - value = json.loads(value) - except json.JSONDecodeError: - return False - - update_count = str(value["total"]) - self.update_level_label.set_label(update_count) - - # Show appropriate state based on update count - if update_count == "0": - self.stack.set_visible_child_name("updated_box") - self.update_level_label.set_visible(False) - else: - self.stack.set_visible_child_name("update_box") - self.update_level_label.set_visible(True) - - self.set_tooltip_text(value.get("tooltip", "")) - return True - - def on_button_press(self, _, event) -> bool: - """Handle button press events""" - if event.button == 1: # Left click - # Run update command - exec_shell_command_async( - f"{self.script_file} -arch -up", - lambda _: None, - ) - self.update() - return True - else: - # Other clicks just refresh - self.update() - - def update(self) -> bool: - """Check for system updates""" - exec_shell_command_async( - f"{self.script_file} -arch", - lambda output: self.update_values(output), - ) - - return True diff --git a/modules/bar/components/workspace.py b/modules/bar/components/workspace.py deleted file mode 100644 index 072364b..0000000 --- a/modules/bar/components/workspace.py +++ /dev/null @@ -1,30 +0,0 @@ -import json - -from fabric.hyprland.service import Hyprland -from fabric.widgets.label import Label -import config.data as data - -connection = Hyprland() - -workspaceData = connection.send_command("j/activeworkspace").reply -activeWorkspace = json.loads(workspaceData.decode("utf-8"))["name"] - -# Create the workspace label with proper initialization -if not data.VERTICAL: - workspace = Label(label=f"Workspace {activeWorkspace}", name="workspace-name") -else: - workspace = Label( - v_align="center", - label="W\no\nr\nk\ns\np\na\nc\ne\n\n" + activeWorkspace, - name="workspace-name", - ) - -def on_workspace(obj, signal): - global activeWorkspace - activeWorkspace = json.loads(signal.data[0]) - if not data.VERTICAL: - workspace.set_label(f"Workspace {activeWorkspace}") - else: - workspace.set_label(f"W\no\nr\nk\ns\np\na\nc\ne\n\n{activeWorkspace}") - -connection.connect("event::workspace", on_workspace) diff --git a/modules/corners.py b/modules/corners.py new file mode 100644 index 0000000..1d24238 --- /dev/null +++ b/modules/corners.py @@ -0,0 +1,71 @@ +from fabric.widgets.box import Box +from fabric.widgets.button import Button +from fabric.widgets.centerbox import CenterBox +from fabric.widgets.shapes import Corner + +from utils.wayland import WaylandWindow as Window + + +class MyCorner(Box): + def __init__(self, corner): + super().__init__( + name="corner-container", + children=Corner( + name="corner", + orientation=corner, + h_expand=False, + v_expand=False, + h_align="center", + v_align="center", + size=20, + ), + ) + + +class Corners(Window): + def __init__(self): + super().__init__( + name="corners", + layer="top", + anchor="top bottom left right", + exclusivity="normal", + pass_through=True, + visible=False, + all_visible=False, + ) + + self.all_corners = Box( + name="all-corners", + orientation="v", + h_expand=True, + v_expand=True, + h_align="fill", + v_align="fill", + children=[ + Box( + name="top-corners", + orientation="h", + h_align="fill", + children=[ + MyCorner("top-left"), + Box(h_expand=True), + MyCorner("top-right"), + ], + ), + Box(v_expand=True), + Box( + name="bottom-corners", + orientation="h", + h_align="fill", + children=[ + MyCorner("bottom-left"), + Box(h_expand=True), + MyCorner("bottom-right"), + ], + ), + ], + ) + + self.add(self.all_corners) + + self.show_all() diff --git a/modules/dock.py b/modules/dock.py deleted file mode 100644 index fb89eb7..0000000 --- a/modules/dock.py +++ /dev/null @@ -1,1133 +0,0 @@ -import json -from gi.repository import GLib, Gtk, Gdk -import cairo -from utils.icon_resolver import IconResolver -from utils.occlusion import check_occlusion -from fabric.widgets.box import Box -from fabric.widgets.button import Button -from fabric.widgets.image import Image -from fabric.widgets.eventbox import EventBox -from fabric.widgets.wayland import WaylandWindow as Window -from fabric.hyprland.widgets import get_hyprland_connection -from fabric.utils import ( - exec_shell_command, - exec_shell_command_async, - idle_add, - remove_handler, - get_relative_path, -) -from fabric.utils.helpers import get_desktop_applications -from modules.bar.bar import MyCorner # Add this import for corners - -import config.data as data -import logging - -OCCLUSION = 36 + data.DOCK_ICON_SIZE - - -# Credit to Aylur for the createSurfaceFromWidget code -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): - # Static registry to track all active dock instances - _instances = [] - - def __init__(self, **kwargs): - super().__init__( - name="dock-window", - layer="top", - anchor="bottom center" - if not data.VERTICAL and not data.BOTTOM_BAR - else "left center" - if data.VERTICAL_RIGHT_ALIGN - else "right center", - margin="-8px 0 -8px 0" - if not data.VERTICAL and not data.BOTTOM_BAR - else "-8px 0 -80px 0" - if data.VERTICAL_RIGHT_ALIGN - else "0 -80px 0 -8px", - exclusivity="none", - **kwargs, - ) - # Add this instance to the registry - Dock._instances.append(self) - - # Initialize configuration - self.config = data.read_config() - self.conn = get_hyprland_connection() - self.icon = IconResolver() - self.pinned = self.config.get("pinned_apps", []) - self.config_path = get_relative_path("../config/assets/modus.json") - self.app_map = {} # Initialize the app map - self._all_apps = get_desktop_applications() # Get all apps for lookup - # Create app identifiers mapping - self.app_identifiers = self._build_app_identifiers_map() - self.is_hidden = False - self.hide_id = None - self._arranger_handler = None - self._drag_in_progress = False # Drag lock flag - self.is_hovered = False - self.always_occluded = ( - data.DOCK_ALWAYS_OCCLUDED - ) # Track the always occluded setting - - # Set up UI containers - self.view = Box( - name="viewport", - orientation="h" - if not data.VERTICAL - and not data.VERTICAL_RIGHT_ALIGN - and not data.BOTTOM_BAR - else "v", - spacing=4, - ) - self.wrapper = Box(name="dock", orientation="v", children=[self.view]) - - # Main dock container with hover handling - 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) - - # Helper to create corner boxes - def create_corner_box(name, orientation, align, corner_position): - return Box( - name=name, - orientation=orientation, - **align, - children=[ - Box( - **( - {"v_expand": True, "v_align": "fill"} - if orientation == "v" - else {"h_expand": True, "h_align": "fill"} - ) - ), - MyCorner(corner_position), - ], - ) - - # Determine dock layout based on orientation - if not data.VERTICAL and not data.BOTTOM_BAR: - # Horizontal layout: corners at the bottom - self.corner_left = create_corner_box( - name="dock-corner-left", - orientation="v", - align={"h_align": "start"}, - corner_position="bottom-right", - ) - - self.corner_right = create_corner_box( - name="dock-corner-right", - orientation="v", - align={"h_align": "end"}, - corner_position="bottom-left", - ) - - self.dock_full = Box( - name="dock-full", - orientation="h", - h_expand=True, - h_align="fill", - children=[ - self.corner_left, - self.dock_eventbox, - self.corner_right, - ], - ) - - elif data.VERTICAL_RIGHT_ALIGN: - # Vertical right-aligned layout: corners at top and bottom (left-angled corners) - self.corner_top = create_corner_box( - name="dock-corner-top", - orientation="h", - align={"v_align": "start"}, - corner_position="bottom-left", - ) - - self.corner_bottom = create_corner_box( - name="dock-corner-bottom", - orientation="h", - align={"v_align": "end"}, - corner_position="top-left", - ) - - self.dock_full = Box( - name="dock-full", - orientation="v", - v_expand=True, - v_align="fill", - children=[ - self.corner_top, - self.dock_eventbox, - self.corner_bottom, - ], - ) - - else: - # Vertical left/default alignment: corners at top and bottom (right-angled corners) - self.corner_top = create_corner_box( - name="dock-corner-top", - orientation="h", - align={"v_align": "start"}, - corner_position="bottom-right", - ) - - self.corner_bottom = create_corner_box( - name="dock-corner-bottom", - orientation="h", - align={"v_align": "end"}, - corner_position="top-right", - ) - - self.dock_full = Box( - name="dock-full", - orientation="v", - v_expand=True, - v_align="fill", - children=[ - self.corner_top, - self.dock_eventbox, - self.corner_bottom, - ], - ) - # Bottom/side hover activation area - self.hover_activator = EventBox() - self.hover_activator.set_size_request( - -1 - if not data.VERTICAL - and not data.VERTICAL_RIGHT_ALIGN - and not data.BOTTOM_BAR - else 1, - 1 - if not data.VERTICAL - and not data.VERTICAL_RIGHT_ALIGN - and not data.BOTTOM_BAR - else -1, - ) - self.hover_activator.connect("enter-notify-event", self._on_hover_enter) - self.hover_activator.connect("leave-notify-event", self._on_hover_leave) - - # Update main_box to include the dock_full and activator - self.main_box = Box( - orientation="v" - if not data.VERTICAL - and not data.VERTICAL_RIGHT_ALIGN - and not data.BOTTOM_BAR - else "h", - children=[self.dock_full, self.hover_activator], - ) - self.add(self.main_box) - - # Drag-and-drop setup for the viewport - 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) - - # Initialization - if self.conn.ready: - self.update_dock() - GLib.timeout_add(500, self.check_hide) - else: - self.conn.connect("event::ready", self.update_dock) - self.conn.connect("event::ready", self.check_hide) - - for ev in ("activewindow", "openwindow", "closewindow", "changefloatingmode"): - self.conn.connect(f"event::{ev}", self.update_dock) - self.conn.connect("event::workspace", self.check_hide) - - if data.VERTICAL: - self.wrapper.add_style_class("vertical") - else: - self.wrapper.remove_style_class("vertical") - - if data.BOTTOM_BAR: - self.wrapper.add_style_class("vertical-right") - else: - self.wrapper.remove_style_class("vertical-right") - - if data.VERTICAL_RIGHT_ALIGN: - self.wrapper.add_style_class("vertical-left") - else: - self.wrapper.remove_style_class("vertical-left") - - GLib.timeout_add(250, self.check_occlusion_state) - # Monitor modus.json for changes - GLib.timeout_add_seconds(1, self.check_config_change) - - # Check if dock should be visible based on configuration - if not data.DOCK_ENABLED: - self.set_visible(False) - - # Apply always occluded state if enabled in settings - if self.always_occluded: - self.dock_full.add_style_class("occluded") - - def _build_app_identifiers_map(self): - """Build a mapping of app identifiers (class names, executables, names) to DesktopApp objects""" - identifiers = {} - for app in self._all_apps: - # Map by name (lowercase) - if app.name: - identifiers[app.name.lower()] = app - - # Map by display name - if app.display_name: - identifiers[app.display_name.lower()] = app - - # Map by window class if available - if app.window_class: - identifiers[app.window_class.lower()] = app - - # Map by executable name if available - if app.executable: - exe_basename = app.executable.split("/")[-1].lower() - identifiers[exe_basename] = app - - # Map by command line if available (without parameters) - if app.command_line: - cmd_base = app.command_line.split()[0].split("/")[-1].lower() - identifiers[cmd_base] = app - - return identifiers - - def _normalize_window_class(self, class_name): - """Normalize window class by removing common suffixes and lowercase.""" - if not class_name: - return "" - - normalized = class_name.lower() - - # Remove common suffixes - 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): - """Check if two window class names match with stricter comparison.""" - if not class1 or not class2: - return False - - # Normalize both classes - norm1 = self._normalize_window_class(class1) - norm2 = self._normalize_window_class(class2) - - # Direct match after normalization - if norm1 == norm2: - return True - - # Don't do substring matching as it's too error-prone - # This avoids incorrectly matching flatpak apps and others - return False - - def on_drag_begin(self, widget, drag_context): - """Handle drag begin event by setting the drag lock flag.""" - self._drag_in_progress = True - # Set custom drag icon using the widget surface - Gtk.drag_set_icon_surface(drag_context, createSurfaceFromWidget(widget)) - - def _on_hover_enter(self, *args): - """Handle hover over bottom activation area""" - self.toggle_dock(show=True) - - def _on_hover_leave(self, *args): - """Handle leave from bottom activation area""" - self.delay_hide() - - def _on_dock_enter(self, widget, event): - """Handle hover over dock content""" - self.is_hovered = True - self.dock_full.remove_style_class("occluded") - # Cancel any pending hide operations - if self.hide_id: - GLib.source_remove(self.hide_id) - self.hide_id = None - self.toggle_dock(show=True) - return True # Important: Stop event propagation - - def _on_dock_leave(self, widget, event): - """Handle leave from dock content""" - # Only trigger if mouse actually left the entire dock area - if event.detail == Gdk.NotifyType.INFERIOR: - return False # Ignore child-to-child mouse movements - - self.is_hovered = False - self.delay_hide() - # Immediate occlusion check on true leave using simplified format - occlusion_region = ( - ("bottom", OCCLUSION) - if not data.VERTICAL - and not data.VERTICAL_RIGHT_ALIGN - and not data.BOTTOM_BAR - else ("left", OCCLUSION) - if data.VERTICAL_RIGHT_ALIGN - else ("right", OCCLUSION) - ) - # Add occlusion style if not dragging an icon or if always_occluded is enabled - if self.always_occluded or ( - not self._drag_in_progress - and (check_occlusion(occlusion_region) or not self.view.get_children()) - ): - self.dock_full.add_style_class("occluded") - return True - - # Enhanced app lookup methods - def find_app(self, app_identifier): - """Return the DesktopApp object by matching any app identifier. - - If app_identifier is a dict, it will use all available keys for matching. - """ - if not app_identifier: - return None - - # If we got a dict object with app data (new format) - if isinstance(app_identifier, dict): - # Try to find by all available identifiers in priority order - 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 - - # Simple string identifier (backward compatibility) - return self.find_app_by_key(app_identifier) - - def find_app_by_key(self, key_value): - """Find app by a single identifier value""" - if not key_value: - return None - - # Try direct lookup in our identifiers map - normalized_id = str(key_value).lower() - if normalized_id in self.app_identifiers: - return self.app_identifiers[normalized_id] - - # Try fuzzy matching - find apps where the identifier is part of their names - 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 - - # Update the dock's app map using DesktopApp objects from the system. - def update_app_map(self): - """Updates the mapping of commands to DesktopApp objects.""" - self._all_apps = get_desktop_applications() # Refresh all apps - self.app_map = { - app.name: app for app in self._all_apps if app.name - } # Map app names to DesktopApp objects - self.app_identifiers = ( - self._build_app_identifiers_map() - ) # Rebuild identifiers map - - def create_button(self, app_identifier, instances): - """Create dock application button""" - desktop_app = self.find_app(app_identifier) # Find app by identifier - icon_img = None - display_name = None - - if desktop_app: - icon_img = desktop_app.get_icon_pixbuf(size=data.DOCK_ICON_SIZE) - display_name = desktop_app.display_name or desktop_app.name - - # Extract identifier for fallback - id_value = ( - app_identifier["name"] - if isinstance(app_identifier, dict) - else app_identifier - ) - - if not icon_img: - # Fallback to IconResolver with the app command - icon_img = self.icon.get_icon_pixbuf( - id_value, data.DOCK_ICON_SIZE - ) # Use identifier for fallback - - if not icon_img: # Double check after exec path try - # Fallback icon if no DesktopApp is found - icon_img = self.icon.get_icon_pixbuf( - "application-x-executable-symbolic", data.DOCK_ICON_SIZE - ) - # Final fallback - if not icon_img: - icon_img = self.icon.get_icon_pixbuf( - "image-missing", data.DOCK_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 - ), # Pass desktop_app as well - tooltip_text=tooltip, - name="dock-app-button", - ) - - # Store app data with the button for future reference - button.app_identifier = app_identifier - button.desktop_app = desktop_app - button.instances = instances - - if instances: - button.add_style_class("instance") # Style running apps - - # Enable DnD for ALL apps - 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) # Connect drag-end to ALL buttons - - # Make all buttons drop targets for better reordering - 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 - - # Enhanced app launching with multiple fallbacks - def handle_app(self, app_identifier, instances, desktop_app=None): - """Handle application button clicks with improved fallbacks""" - if not instances: - # Try to launch the app with multiple fallback methods - if not desktop_app: - desktop_app = self.find_app(app_identifier) - - if desktop_app: - # Try the standard launch method first - launch_success = desktop_app.launch() - - # If that fails, try command line or executable - 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: - # No desktop entry found, try direct execution - if isinstance(app_identifier, dict): - # Try command_line first, then executable, then name as last resort - if ( - "command_line" in app_identifier - and app_identifier["command_line"] - ): - exec_shell_command_async( - f"nohup {app_identifier['command_line']}" - ) - elif ( - "executable" in app_identifier and app_identifier["executable"] - ): - exec_shell_command_async( - f"nohup {app_identifier['executable']}" - ) - elif "name" in app_identifier and app_identifier["name"]: - exec_shell_command_async(f"nohup {app_identifier['name']}") - elif isinstance(app_identifier, str): - # Try direct execution with the identifier as fallback - exec_shell_command_async(f"nohup {app_identifier}") - else: - # Handle window switching for running instances - 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): - """Maintain hover state when entering child widgets""" - self.is_hovered = True - if self.hide_id: - GLib.source_remove(self.hide_id) - self.hide_id = None - return False # Continue event propagation - - def toggle_dock(self, show): - """Show or hide the dock immediately""" - if show: - if self.is_hidden: - self.is_hidden = False - self.dock_full.add_style_class("show-dock") - self.dock_full.remove_style_class("hide-dock") - if self.hide_id: - GLib.source_remove(self.hide_id) - self.hide_id = None - else: - if not self.is_hidden: - self.is_hidden = True - self.dock_full.add_style_class("hide-dock") - self.dock_full.remove_style_class("show-dock") - - def delay_hide(self): - """Schedule hiding after short delay""" - if self.hide_id: - GLib.source_remove(self.hide_id) - self.hide_id = GLib.timeout_add(1000, self.hide_dock) - - def hide_dock(self): - """Finalize hiding procedure""" - self.toggle_dock(show=False) - self.hide_id = None - return False - - def check_hide(self, *args): - """Determine if dock should auto-hide""" - clients = self.get_clients() - current_ws = self.get_workspace() - ws_clients = [w for w in clients if w["workspace"]["id"] == current_ws] - - if not ws_clients: - self.toggle_dock(show=True) - elif any(not w.get("floating") and not w.get("fullscreen") for w in ws_clients): - self.delay_hide() - else: - self.toggle_dock(show=True) - - def update_dock(self, *args): - """Refresh dock contents and clear drag lock.""" - self.update_app_map() # Update app map before creating buttons - arranger_handler = getattr(self, "_arranger_handler", None) - if arranger_handler: - remove_handler(arranger_handler) - clients = self.get_clients() - - # Create a mapping of window class to instances - running_windows = {} - for c in clients: - # Try multiple identification methods in order of reliability - window_id = None - - # Try initialClass first (most reliable) - if class_name := c.get("initialClass", "").lower(): - window_id = class_name - - # Try class second (fallback) - elif class_name := c.get("class", "").lower(): - window_id = class_name - - # Use title as last resort if both class identifiers are missing - elif title := c.get("title", "").lower(): - # Extract app name from title (common format: "App Name - Document") - possible_name = title.split(" - ")[0].strip() - if ( - possible_name and len(possible_name) > 1 - ): # Avoid single letter app names - window_id = possible_name - else: - window_id = title # Use full title if we can't extract a good name - - # Use generic identifier as absolute fallback - if not window_id: - window_id = "unknown-app" - - # Log window for debugging purposes - logging.debug( - f"Window detected: {window_id} (from {c.get('initialClass', '')}/{c.get('class', '')}/{c.get('title', '')})" - ) - - # Add to running windows - store with both original and normalized keys - running_windows.setdefault(window_id, []).append(c) - - # Also store with normalized key for more flexible matching - normalized_id = self._normalize_window_class(window_id) - if normalized_id != window_id: - running_windows.setdefault(normalized_id, []).extend( - running_windows[window_id] - ) - - # Map pinned apps to their running instances - pinned_buttons = [] - used_window_classes = set() # Track which window classes we've already assigned - - for app_data in self.pinned: - app = self.find_app(app_data) - - # Try to find running instances for this pinned app - instances = [] - matched_class = None - - # Extract all possible identifiers from app_data for matching - possible_identifiers = [] - - if isinstance(app_data, dict): - for key in [ - "window_class", - "executable", - "command_line", - "name", - "display_name", - ]: - if key in app_data and app_data[key]: - possible_identifiers.append(app_data[key].lower()) - elif isinstance(app_data, str): - possible_identifiers.append(app_data.lower()) - - # Add identifiers from DesktopApp if available - 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()) - - # Remove duplicates - possible_identifiers = list(set(possible_identifiers)) - - # Try each identifier for matching - for identifier in possible_identifiers: - # Try direct match - if identifier in running_windows: - instances = running_windows[identifier] - matched_class = identifier - break - - # Try normalized version - normalized = self._normalize_window_class(identifier) - if normalized in running_windows: - instances = running_windows[normalized] - matched_class = normalized - break - - # Try substring matching for window classes (less reliable but helpful) - for window_class in running_windows: - # For substring matching, ensure the identifier is at least 3 chars - # to avoid too broad matches - if len(identifier) >= 3 and identifier in window_class: - instances = running_windows[window_class] - matched_class = window_class - logging.debug( - f"Substring match: {identifier} in {window_class}" - ) - break - - if matched_class: - break - - # Mark the matched class as used - if matched_class: - used_window_classes.add(matched_class) - # Also mark the normalized version as used - used_window_classes.add(self._normalize_window_class(matched_class)) - logging.debug( - f"Matched pinned app {app_data} to running instances via {matched_class}" - ) - - # Create button for this pinned app with any found instances - pinned_buttons.append(self.create_button(app_data, instances)) - - # For any remaining window classes that aren't assigned to pinned apps - open_buttons = [] - for class_name, instances in running_windows.items(): - if class_name not in used_window_classes: - # Enhanced app identification for running windows - app = None - - # Try multiple methods to find the correct app - # 1. Direct lookup by class name - app = self.app_identifiers.get(class_name) - - # 2. Try with normalized class name - if not app: - norm_class = self._normalize_window_class(class_name) - app = self.app_identifiers.get(norm_class) - - # 3. Try with our more robust find_app method - if not app: - app = self.find_app_by_key(class_name) - - # 4. Try using window title which often contains app name - if not app and instances and instances[0].get("title"): - title = instances[0].get("title", "") - # Extract potential app name from title (common format: "App Name - Document") - possible_name = title.split(" - ")[0].strip() - if len(possible_name) > 2: # Avoid very short names - app = self.find_app_by_key(possible_name) - - # Create comprehensive app data if app was found - if app: - app_data = { - "name": app.name, - "display_name": app.display_name, - "window_class": app.window_class, - "executable": app.executable, - "command_line": app.command_line, - } - identifier = app_data - else: - # Fallback to just class name - identifier = class_name - - open_buttons.append(self.create_button(identifier, instances)) - - # Assemble dock layout - children = pinned_buttons - # Only add separator if both pinned and open buttons exist - if pinned_buttons and open_buttons: - children += [ - Box( - orientation="v" - if not data.VERTICAL - and not data.VERTICAL_RIGHT_ALIGN - and not data.BOTTOM_BAR - else "h", - v_expand=not data.VERTICAL - and not data.VERTICAL_RIGHT_ALIGN - and not data.BOTTOM_BAR, - h_expand=data.VERTICAL - and data.VERTICAL_RIGHT_ALIGN - and data.BOTTOM_BAR, - name="dock-separator", - ) - ] - children += open_buttons - - self.view.children = children - idle_add(self._update_size) - self._drag_in_progress = False # Clear the drag lock - - def _update_size(self): - """Update window size based on content""" - width, _ = self.view.get_preferred_width() - self.set_size_request(width, -1) - return False - - def get_clients(self): - """Get current client list""" - try: - return json.loads(self.conn.send_command("j/clients").reply.decode()) - except json.JSONDecodeError: - return [] - - def get_focused(self): - """Get focused window address""" - try: - return json.loads( - self.conn.send_command("j/activewindow").reply.decode() - ).get("address", "") - except json.JSONDecodeError: - return "" - - def get_workspace(self): - """Get current workspace ID""" - 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): - """Periodic occlusion check""" - # Skip occlusion check if hovered or dragging an icon - if self.is_hovered or self._drag_in_progress: - self.dock_full.remove_style_class("occluded") - return True - - # If always_occluded is enabled, keep the dock occluded - if self.always_occluded: - self.dock_full.add_style_class("occluded") - return True - - occlusion_region = ( - ("bottom", OCCLUSION) - if not data.VERTICAL - and not data.VERTICAL_RIGHT_ALIGN - and not data.BOTTOM_BAR - else ("left", OCCLUSION) - if data.VERTICAL_RIGHT_ALIGN - else ("right", OCCLUSION) - ) - if check_occlusion(occlusion_region) or not self.view.get_children(): - self.dock_full.add_style_class("occluded") - else: - self.dock_full.remove_style_class("occluded") - return True - - def _find_drag_target(self, widget): - """Find valid drag target in viewport""" - 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, info, time): - """Handle drag start""" - 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.set_text(str(index), -1) - - def on_drag_data_received(self, widget, drag_context, x, y, data, info, time): - """Handle drop event""" - target = self._find_drag_target( - widget.get_parent() if isinstance(widget, Box) else widget - ) - if target is None: - return - try: - source_index = int(data.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: - # Check for separator to detect cross-section drag - separator_index = -1 - for i, child in enumerate(children): - if child.get_name() == "dock-separator": - separator_index = i - break - - # Detect if we're dragging across the separator (between pinned/unpinned) - 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) - ) - - # Move the item in the children list - child = children.pop(source_index) - children.insert(target_index, child) - self.view.children = children # Update view immediately - - # Update pinned apps configuration - self.update_pinned_apps(skip_update=not cross_section_drag) - - # Force immediate update if we dragged across sections to update separator visibility - if cross_section_drag: - GLib.idle_add(self.update_dock) - - def on_drag_end(self, widget, drag_context): - """Handles drag end, for unpinning and closing apps.""" - # If a drag is already in progress, ignore duplicate calls. - if not self._drag_in_progress: - return - - def process_drag_end(): - """Inner function to handle drag end, safe to call with idle_add.""" - if self.get_mapped(): # Check if the window is mapped - # Get window geometry *before* any modifications - display = Gdk.Display.get_default() - _, x, y, _ = display.get_pointer() # Get pointer relative to the screen - window = self.get_window() - if window: # Check if we got a 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 - ): - # Drag ended outside the dock - get app data from the button - app_id = widget.app_identifier - instances = widget.instances - - # Find this app in pinned list - app_index = -1 - for i, pinned_app in enumerate(self.pinned): - # Match by identifier - if pinned_app == app_id: - app_index = i - break - # For dict format, match by name - elif isinstance(pinned_app, dict) and isinstance( - app_id, dict - ): - if pinned_app.get("name") == app_id.get("name"): - app_index = i - break - - # If it's a pinned app, remove from pinned - if app_index >= 0: - self.pinned.pop(app_index) - self.config["pinned_apps"] = self.pinned - self.update_pinned_apps_file() - self.update_dock() # Update dock after file save - elif instances: # Close if running and unpinned - # Close running app (if not pinned) - address = instances[0].get("address") - if address: - exec_shell_command( - f"hyprctl dispatch closewindow address:{address}" - ) - self.update_dock() # Update dock after closing window - self._drag_in_progress = False # Clear the drag lock - - GLib.idle_add(process_drag_end) # Deferred execution with idle_add - - def check_config_change(self): - """Check if the config file has been modified.""" - new_config = data.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() # Update app_map when config changes - self.update_dock() - return True # Continue the timeout - - def update_pinned_apps_file(self): - """Writes the updated pinned apps list to modus.json""" - config_path = get_relative_path("../config/assets/modus.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): - """Update pinned apps configuration by collecting all items before the separator""" - pinned_children = [] - for child in self.view.get_children(): - if child.get_name() == "dock-separator": - break # Stop at separator - - # Use stored app identifier from the button - if hasattr(child, "app_identifier"): - # If the button has a desktop_app, use it to create comprehensive data - if hasattr(child, "desktop_app") and child.desktop_app: - app = child.desktop_app - app_data = { - "name": app.name, - "display_name": app.display_name, - "window_class": app.window_class, - "executable": app.executable, - "command_line": app.command_line, - } - pinned_children.append(app_data) - else: - # Keep existing app_identifier (might be dict or string) - pinned_children.append(child.app_identifier) - - # Directly update both config and local pinned list - self.config["pinned_apps"] = pinned_children - self.pinned = pinned_children # Update local state immediately - - # Write to file first to ensure persistence - file_updated = self.update_pinned_apps_file() - - # Only update the dock if needed and file was successfully written - if file_updated and not skip_update: - self.update_dock() # Refresh dock only after file is saved - - # Static method to update all dock instances - @staticmethod - def notify_config_change(): - """Notify all dock instances to reload their configuration immediately.""" - for dock in Dock._instances: - # Use idle_add to avoid potential issues with direct calls - GLib.idle_add(dock.check_config_change_immediate) - - def check_config_change_immediate(self): - """Immediately check for configuration changes and update if needed.""" - new_config = data.read_config() - - # Check if always_occluded setting has changed - previous_always_occluded = self.always_occluded - self.always_occluded = data.DOCK_ALWAYS_OCCLUDED - - # If always_occluded changed, update the dock appearance - if previous_always_occluded != self.always_occluded: - if self.always_occluded and not self.is_hovered: - self.dock_full.add_style_class("occluded") - elif not self.always_occluded and not self.is_hovered: - # Perform a normal occlusion check - occlusion_region = ( - ("bottom", OCCLUSION) - if not data.VERTICAL and not data.BOTTOM_BAR - else ("left", OCCLUSION) - if data.VERTICAL_RIGHT_ALIGN - else ("right", OCCLUSION) - ) - if check_occlusion(occlusion_region) or not self.view.get_children(): - self.dock_full.add_style_class("occluded") - else: - self.dock_full.remove_style_class("occluded") - - 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 # Don't repeat - - # Add a static method to handle dock visibility changes - @staticmethod - def update_visibility(visible): - """Update visibility for all dock instances.""" - for dock in Dock._instances: - dock.set_visible(visible) diff --git a/modules/dock/battery.py b/modules/dock/battery.py new file mode 100644 index 0000000..1669e10 --- /dev/null +++ b/modules/dock/battery.py @@ -0,0 +1,246 @@ +from gi.repository import GLib, Gdk + +from fabric.widgets.label import Label +from fabric.widgets.box import Box +from fabric.widgets.circularprogressbar import CircularProgressBar +from fabric.widgets.button import Button +from services.battery import Battery as BatteryService +import utils.icons as icons +import config.data as data + + +class Battery(Box): + def __init__(self, **kwargs): + orientation = "v" if data.VERTICAL else "h" + super().__init__( + name="battery", spacing=0, orientation=orientation, visible=True, **kwargs + ) + + # Initialize battery service + self._battery = BatteryService() + self._battery.changed.connect(self.update_battery) + + # Create the battery icon and circular progress + self.icon = Label(name="battery-icon", markup=icons.battery) + self.circle = CircularProgressBar( + name="battery-circle", + value=0, + size=26, + line_width=2, + start_angle=150, + end_angle=390, + style_classes="battery", + child=self.icon, + ) + + # Create button to hold the circle + self.circle_button = Button( + name="battery-button", + child=self.circle, + style="padding: 0; margin: 0; background: none;", + ) + + # Add event handling for hover effects + self.circle_button.add_events( + Gdk.EventMask.ENTER_NOTIFY_MASK + | Gdk.EventMask.LEAVE_NOTIFY_MASK + | Gdk.EventMask.BUTTON_PRESS_MASK + ) + self.circle_button.connect("enter-notify-event", self.on_button_enter) + self.circle_button.connect("leave-notify-event", self.on_button_leave) + self.circle_button.connect("button-press-event", self.on_button_press) + + # Create percentage label + self.level = Label( + name="battery-level", + style_classes="battery", + label="0%", + ) + + # Create the container box + self.battery_box = Box( + name="battery-box", + orientation=orientation, + spacing=0, + children=[self.circle_button], + ) + + # Only add level label in horizontal mode + if not data.VERTICAL: + self.battery_box.add(self.level) + + # Enable tooltips + self.circle_button.set_has_tooltip(True) + self.battery_box.set_has_tooltip(True) + self.level.set_has_tooltip(True) + + # Connect tooltip signals + self.circle_button.connect("query-tooltip", self.on_query_tooltip) + self.battery_box.connect("query-tooltip", self.on_query_tooltip) + self.level.connect("query-tooltip", self.on_query_tooltip) + + # Add the battery box to self + self.add(self.battery_box) + + # Initial update + GLib.idle_add(self.update_battery) + + def on_button_enter(self, widget, event): + """Handle mouse enter event""" + window = widget.get_window() + if window: + window.set_cursor(Gdk.Cursor(Gdk.CursorType.HAND2)) + + def on_button_leave(self, widget, event): + """Handle mouse leave event""" + window = widget.get_window() + if window: + window.set_cursor(None) + + def on_button_press(self, widget, event): + """Handle button press event - toggle power profiles""" + if event.button == 1: # Left click + try: + if ( + not hasattr(self._battery, "_profile_proxy") + or not self._battery._profile_proxy + ): + return False + + current_profile = self._battery._profile_proxy.ActiveProfile + # Get available profiles from D-Bus property + profiles = self._battery._profile_proxy.Profiles + profile_names = [] + + # Extract profile names from the D-Bus structure + for profile in profiles: + if isinstance(profile, dict) and "Profile" in profile: + profile_names.append(profile["Profile"]) + elif hasattr(profile, "Profile"): + profile_names.append(profile.Profile) + elif isinstance(profile, str): + profile_names.append(profile) + + if profile_names and current_profile in profile_names: + next_index = (profile_names.index(current_profile) + 1) % len( + profile_names + ) + next_profile = profile_names[next_index] + # Set profile directly on the D-Bus proxy + self._battery._profile_proxy.ActiveProfile = next_profile + else: + return False + except Exception: + return False + return True + return False + + def on_query_tooltip(self, widget, x, y, keyboard_mode, tooltip): + """Handle tooltip query""" + tooltip.set_markup(self.get_tooltip_text()) + return True + + def get_tooltip_text(self): + """Get formatted tooltip text in bullet points""" + if not self._battery.is_present: + return "No battery detected" + + percentage = self._battery.percentage + state = self._battery.state + charging = state in ["CHARGING", "FULLY_CHARGED"] + # Get capacity as integer by converting from float + capacity = int(float(self._battery.capacity.rstrip("%"))) + tooltip_points = [] + + # Status and percentage + if state == "FULLY_CHARGED": + status = f"{icons.bat_full} Fully Charged" + elif state == "CHARGING": + status = f"{icons.bat_charging} Charging" + elif percentage <= 15 and state == "DISCHARGING": + status = f"{icons.bat_low} Low Battery" + elif state == "DISCHARGING": + status = f"{icons.bat_discharging} Discharging" + else: + status = "Battery" + tooltip_points.append(f"• Status: {status}") + + # Battery level and capacity + tooltip_points.append(f"• Level: {int(percentage)}%") + tooltip_points.append(f"• Battery Health: {capacity}%") + + # Add power profile if available + power_profile = self._battery.power_profile + if power_profile: + # Add icon based on profile + if power_profile == "power-saver": + profile_icon = icons.power_saving + elif power_profile == "performance": + profile_icon = icons.power_performance + else: # balanced + profile_icon = icons.power_balanced + tooltip_points.append(f"• Profile: {profile_icon} {power_profile}") + + # Add temperature if available + temp = self._battery.temperature + if temp != "N/A": + tooltip_points.append(f"• Temperature: {temp}") + + # Add time estimates + if state == "CHARGING": + time_to_full = self._battery.time_to_full + if time_to_full: + tooltip_points.append(f"• Time until full: {time_to_full}") + elif state == "DISCHARGING": + time_to_empty = self._battery.time_to_empty + if time_to_empty: + tooltip_points.append(f"• Time remaining: {time_to_empty}") + + return "\n".join(tooltip_points) + + def update_battery(self, *args): + """Update battery status and UI""" + if not self._battery.is_present: + self.set_visible(False) + return True + + percentage = self._battery.percentage + state = self._battery.state + charging = state in ["CHARGING", "FULLY_CHARGED"] + power_profile = self._battery.power_profile + + # Update circle progress + self.circle.set_value(percentage / 100.0) + self.level.set_label(f"{int(percentage)}%") + + # Remove any existing profile classes + self.circle.remove_style_class("power-saver") + self.circle.remove_style_class("performance") + self.circle.remove_style_class("balanced") + + # Apply power profile styling + if power_profile: + self.circle.add_style_class(power_profile) + + # Apply alert styling if battery is low AND not charging + if percentage <= 15 and not charging: + self.icon.add_style_class("alert") + self.circle.add_style_class("alert") + else: + self.icon.remove_style_class("alert") + self.circle.remove_style_class("alert") + + # Update icon based on battery state + if state == "FULLY_CHARGED": + self.icon.set_markup(icons.battery) + elif state == "CHARGING": + self.icon.set_markup(icons.charging) + elif percentage <= 15 and state == "DISCHARGING": + self.icon.set_markup(icons.alert) + elif state == "DISCHARGING": + self.icon.set_markup(icons.discharging) + else: + self.icon.set_markup(icons.battery) + + self.set_visible(True) + return True diff --git a/modules/dock/components.py b/modules/dock/components.py new file mode 100644 index 0000000..5ad55ca --- /dev/null +++ b/modules/dock/components.py @@ -0,0 +1,107 @@ +import os +import json +import config.data as data +from fabric.widgets.box import Box +from fabric.widgets.button import Button +from fabric.widgets.datetime import DateTime +from modules.dock.battery import Battery +from modules.dock.metrics import Metrics +from modules.dock.controls import Controls +from modules.dock.workspaces import create_workspace_widget +from modules.dock.indicators import Indicators + + +class DockSystemTray(Box): + def __init__(self, orientation_val="h", **kwargs): + super().__init__( + name="system-tray", + orientation=orientation_val, + spacing=4, + **kwargs + ) + + # Initialize component visibility from data + self.component_visibility = data.DOCK_COMPONENTS_VISIBILITY + + # Create components + self.workspaces = Button( + child=create_workspace_widget(), + name="workspaces", + ) + self.metrics = Metrics() + self.battery = Battery() + self.date_time = DateTime( + name="date-time", + formatters=["%I:%M"] if not data.VERTICAL else ["%I\n%M"], + h_align="center" if not data.VERTICAL else "fill", + v_align="center", + h_expand=True, + v_expand=True, + ) + self.controls = Controls() + self.indicators = Indicators() + + # Add components based on position + if data.DOCK_POSITION == "Right": + self.add(self.date_time) + self.add(self.controls) + self.add(self.indicators) + self.add(self.battery) + self.add(self.metrics) + self.add(self.workspaces) + else: # Bottom or Left position + self.add(self.workspaces) + self.add(self.metrics) + self.add(self.controls) + self.add(self.indicators) + self.add(self.battery) + self.add(self.date_time) + + + # Apply initial visibility + self.apply_component_props() + + def apply_component_props(self): + components = { + "workspaces": self.workspaces, + "metrics": self.metrics, + "battery": self.battery, + "date_time": self.date_time, + "controls": self.controls, + "indicators": self.indicators, + } + + for component_name, widget in components.items(): + if component_name in self.component_visibility: + widget.set_visible(self.component_visibility[component_name]) + + def toggle_component_visibility(self, component_name): + components = { + "workspaces": self.workspaces, + "metrics": self.metrics, + "battery": self.battery, + "date_time": self.date_time, + "controls": self.controls, + "indicators": self.indicators, + } + + 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]) + + 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] + + with open(config_file, "w") as f: + json.dump(config, f, indent=4) + except Exception as e: + print(f"Error updating config file: {e}") + + return self.component_visibility[component_name] + + return None \ No newline at end of file diff --git a/modules/dock/controls.py b/modules/dock/controls.py new file mode 100644 index 0000000..d49ae3d --- /dev/null +++ b/modules/dock/controls.py @@ -0,0 +1,272 @@ +from fabric.audio.service import Audio +from fabric.widgets.box import Box +from fabric.widgets.button import Button +from fabric.widgets.circularprogressbar import CircularProgressBar +from fabric.widgets.eventbox import EventBox +from fabric.widgets.label import Label +from fabric.widgets.overlay import Overlay +from fabric.widgets.scale import Scale +from gi.repository import Gdk, GLib + +import config.data as data +import utils.icons as icons +from services.brightness import Brightness + + +class BrightnessSmall(Box): + def __init__(self, **kwargs): + super().__init__(name="button-bar-brightness", **kwargs) + self.brightness = Brightness.get_initial() + if self.brightness.screen_brightness == -1: + self.destroy() + return + + self.progress_bar = CircularProgressBar( + name="button-brightness", size=28, line_width=2, + start_angle=150, end_angle=390, + ) + self.brightness_label = Label(name="brightness-label", markup=icons.brightness_high) + self.brightness_button = Button(child=self.brightness_label) + self.event_box = EventBox( + events=["scroll", "smooth-scroll"], + child=Overlay( + child=self.progress_bar, + overlays=self.brightness_button + ), + ) + self.event_box.connect("scroll-event", self.on_scroll) + self.add(self.event_box) + self.add_events(Gdk.EventMask.SCROLL_MASK | Gdk.EventMask.SMOOTH_SCROLL_MASK) + + self._updating_from_brightness = False + self._pending_value = None + self._update_source_id = None + + self.progress_bar.connect("notify::value", self.on_progress_value_changed) + self.brightness.connect("screen", self.on_brightness_changed) + self.on_brightness_changed() + + def on_scroll(self, widget, event): + if self.brightness.max_screen == -1: + return + + step_size = 5 + current_norm = self.progress_bar.value + if event.delta_y < 0: + new_norm = min(current_norm + (step_size / self.brightness.max_screen), 1) + elif event.delta_y > 0: + new_norm = max(current_norm - (step_size / self.brightness.max_screen), 0) + else: + return + self.progress_bar.value = new_norm + + def on_progress_value_changed(self, widget, pspec): + if self._updating_from_brightness: + return + new_norm = widget.value + new_brightness = int(new_norm * self.brightness.max_screen) + self._pending_value = new_brightness + if self._update_source_id is None: + self._update_source_id = GLib.timeout_add(50, self._update_brightness_callback) + + def _update_brightness_callback(self): + if self._pending_value is not None and self._pending_value != self.brightness.screen_brightness: + self.brightness.screen_brightness = self._pending_value + self._pending_value = None + return True + else: + self._update_source_id = None + return False + + def on_brightness_changed(self, *args): + if self.brightness.max_screen == -1: + return + normalized = self.brightness.screen_brightness / self.brightness.max_screen + self._updating_from_brightness = True + self.progress_bar.value = normalized + self._updating_from_brightness = False + + brightness_percentage = int(normalized * 100) + if brightness_percentage >= 75: + self.brightness_label.set_markup(icons.brightness_high) + elif brightness_percentage >= 24: + self.brightness_label.set_markup(icons.brightness_medium) + else: + self.brightness_label.set_markup(icons.brightness_low) + self.set_tooltip_text(f"{brightness_percentage}%") + + def destroy(self): + if self._update_source_id is not None: + GLib.source_remove(self._update_source_id) + super().destroy() + +class VolumeSmall(Box): + def __init__(self, **kwargs): + super().__init__(name="button-bar-vol", **kwargs) + self.audio = Audio() + self.progress_bar = CircularProgressBar( + name="button-volume", size=28, line_width=2, + start_angle=150, end_angle=390, + ) + self.vol_label = Label(name="vol-label", markup=icons.vol_high) + self.vol_button = Button(on_clicked=self.toggle_mute, child=self.vol_label) + self.event_box = EventBox( + events=["scroll", "smooth-scroll"], + child=Overlay(child=self.progress_bar, overlays=self.vol_button), + ) + self.audio.connect("notify::speaker", self.on_new_speaker) + if self.audio.speaker: + self.audio.speaker.connect("changed", self.on_speaker_changed) + self.event_box.connect("scroll-event", self.on_scroll) + self.add(self.event_box) + self.on_speaker_changed() + self.add_events(Gdk.EventMask.SCROLL_MASK | Gdk.EventMask.SMOOTH_SCROLL_MASK) + + def on_new_speaker(self, *args): + if self.audio.speaker: + self.audio.speaker.connect("changed", self.on_speaker_changed) + self.on_speaker_changed() + + def toggle_mute(self, event): + current_stream = self.audio.speaker + if current_stream: + current_stream.muted = not current_stream.muted + if current_stream.muted: + self.on_speaker_changed() + self.progress_bar.add_style_class("muted") + self.vol_label.add_style_class("muted") + else: + self.on_speaker_changed() + self.progress_bar.remove_style_class("muted") + self.vol_label.remove_style_class("muted") + + def on_scroll(self, _, event): + if not self.audio.speaker: + return + if event.direction == Gdk.ScrollDirection.SMOOTH: + if abs(event.delta_y) > 0: + self.audio.speaker.volume -= event.delta_y + if abs(event.delta_x) > 0: + self.audio.speaker.volume += event.delta_x + + def on_speaker_changed(self, *_): + if not self.audio.speaker: + return + + vol_high_icon = icons.vol_high + vol_medium_icon = icons.vol_medium + vol_mute_icon = icons.vol_off + vol_off_icon = icons.vol_mute + + if "bluetooth" in self.audio.speaker.icon_name: + vol_high_icon = icons.bluetooth_connected + vol_medium_icon = icons.bluetooth + vol_mute_icon = icons.bluetooth_off + vol_off_icon = icons.bluetooth_disconnected + + self.progress_bar.value = self.audio.speaker.volume / 100 + + if self.audio.speaker.muted: + self.vol_label.set_markup(vol_mute_icon) + self.progress_bar.add_style_class("muted") + self.vol_label.add_style_class("muted") + self.set_tooltip_text("Muted") + return + else: + self.progress_bar.remove_style_class("muted") + self.vol_label.remove_style_class("muted") + self.set_tooltip_text(f"{round(self.audio.speaker.volume)}%") + if self.audio.speaker.volume > 74: + self.vol_label.set_markup(vol_high_icon) + elif self.audio.speaker.volume > 0: + self.vol_label.set_markup(vol_medium_icon) + else: + self.vol_label.set_markup(vol_off_icon) + +class MicSmall(Box): + def __init__(self, **kwargs): + super().__init__(name="button-bar-mic", **kwargs) + self.audio = Audio() + self.progress_bar = CircularProgressBar( + name="button-mic", size=28, line_width=2, + start_angle=150, end_angle=390, + ) + self.mic_label = Label(name="mic-label", markup=icons.mic) + self.mic_button = Button(on_clicked=self.toggle_mute, child=self.mic_label) + self.event_box = EventBox( + events=["scroll", "smooth-scroll"], + child=Overlay(child=self.progress_bar, overlays=self.mic_button), + ) + self.audio.connect("notify::microphone", self.on_new_microphone) + if self.audio.microphone: + self.audio.microphone.connect("changed", self.on_microphone_changed) + self.event_box.connect("scroll-event", self.on_scroll) + self.add_events(Gdk.EventMask.SCROLL_MASK | Gdk.EventMask.SMOOTH_SCROLL_MASK) + self.add(self.event_box) + self.on_microphone_changed() + + def on_new_microphone(self, *args): + if self.audio.microphone: + self.audio.microphone.connect("changed", self.on_microphone_changed) + self.on_microphone_changed() + + def toggle_mute(self, event): + current_stream = self.audio.microphone + if current_stream: + current_stream.muted = not current_stream.muted + if current_stream.muted: + self.mic_label.set_markup(icons.mic_mute) + self.progress_bar.add_style_class("muted") + self.mic_label.add_style_class("muted") + else: + self.on_microphone_changed() + self.progress_bar.remove_style_class("muted") + self.mic_label.remove_style_class("muted") + + def on_scroll(self, _, event): + if not self.audio.microphone: + return + if event.direction == Gdk.ScrollDirection.SMOOTH: + if abs(event.delta_y) > 0: + self.audio.microphone.volume -= event.delta_y + if abs(event.delta_x) > 0: + self.audio.microphone.volume += event.delta_x + + def on_microphone_changed(self, *_): + if not self.audio.microphone: + return + + self.progress_bar.value = self.audio.microphone.volume / 100 + + if self.audio.microphone.muted: + self.mic_label.set_markup(icons.mic_mute) + self.progress_bar.add_style_class("muted") + self.mic_label.add_style_class("muted") + self.set_tooltip_text("Muted") + return + else: + self.progress_bar.remove_style_class("muted") + self.mic_label.remove_style_class("muted") + self.set_tooltip_text(f"{round(self.audio.microphone.volume)}%") + if self.audio.microphone.volume > 74: + self.mic_label.set_markup(icons.mic) + elif self.audio.microphone.volume > 0: + self.mic_label.set_markup(icons.mic) + else: + self.mic_label.set_markup(icons.mic_mute) + +class Controls(Box): + def __init__(self, **kwargs): + brightness = Brightness.get_initial() + children = [] + if (brightness.screen_brightness != -1): + children.append(BrightnessSmall()) + children.extend([VolumeSmall(), MicSmall()]) + super().__init__( + name="control", + orientation="h" if not data.VERTICAL else "v", + spacing=4, + children=children, + **kwargs, + ) + self.show_all() diff --git a/modules/dock/indicators.py b/modules/dock/indicators.py new file mode 100644 index 0000000..e3d77af --- /dev/null +++ b/modules/dock/indicators.py @@ -0,0 +1,196 @@ +from fabric.widgets.box import Box +from fabric.widgets.button import Button +from fabric.widgets.label import Label +from fabric.widgets.overlay import Overlay +from fabric.widgets.circularprogressbar import CircularProgressBar +from gi.repository import Gdk, GLib + + +import config.data as data +import utils.icons as icons +from services.network import NetworkClient +from fabric.bluetooth import BluetoothClient + + +class WifiIndicator(Button): + def __init__(self, **kwargs): + super().__init__(name="button-bar-network", **kwargs) + + # Initialize NetworkClient + self.network = NetworkClient() + # Create progress bar and wifi label + self.progress_bar = CircularProgressBar( + name="button-network", + size=28, + line_width=2, + start_angle=150, + end_angle=390, + ) + self.wifi_label = Label(name="network-label", markup=icons.loader) + + # Create overlay with progress bar and label + self.add(Overlay( + child=self.progress_bar, + overlays=self.wifi_label + )) + + # Connect signals + self.network.connect("ready", self.on_network_ready) + + # If already ready, set up signals immediately + if self.network.is_ready: + self.setup_signals() + + def on_network_ready(self, *args): + self.setup_signals() + + def setup_signals(self): + # Connect to NetworkClient signals + self.network.connect("changed", self.on_network_changed) + self.network.connect("wifi-device-added", self.on_wifi_device_added) + self.network.connect("wifi-device-removed", self.on_wifi_device_removed) + self.network.connect("ethernet-device-added", self.on_ethernet_device_added) + self.network.connect("ethernet-device-removed", self.on_ethernet_device_removed) + + # Connect to WiFi device if available + if self.network.wifi_device: + self.network.wifi_device.connect("changed", self.on_wifi_changed) + self.network.wifi_device.connect("ap-added", self.on_wifi_changed) + self.network.wifi_device.connect("ap-removed", self.on_wifi_changed) + + # Connect to Ethernet device if available + if self.network.ethernet_device: + self.network.ethernet_device.connect("changed", self.on_ethernet_changed) + + self.update_state() + + def update_state(self): + # Check ethernet first + if self.network.ethernet_device and self.network.ethernet_device.state == "activated": + self.wifi_label.set_markup(icons.world) + self.progress_bar.value = 1.0 # Full circle for ethernet + tooltip = f"Connected to Ethernet ({self.network.ethernet_device.speed})" + self.set_tooltip_text(tooltip) + return + + # Then check wifi + if not self.network.wifi_device: + self.wifi_label.set_markup(icons.cloud_off) + self.progress_bar.value = 0 + tooltip = "No WiFi device found" + elif not self.network.wifi_device.wireless_enabled: + self.wifi_label.set_markup(icons.cloud_off) + self.progress_bar.value = 0 + tooltip = "WiFi disabled" + else: + active_ap = self.network.wifi_device.active_access_point + if active_ap: + strength = active_ap.strength + self.progress_bar.value = strength / 100 + if strength >= 75: + self.wifi_label.set_markup(icons.wifi_3) + elif strength >= 50: + self.wifi_label.set_markup(icons.wifi_2) + elif strength >= 25: + self.wifi_label.set_markup(icons.wifi_1) + else: + self.wifi_label.set_markup(icons.wifi_0) + tooltip = f"Connected to {active_ap.ssid} ({strength}%)" + else: + self.wifi_label.set_markup(icons.world_off) + self.progress_bar.value = 0 + tooltip = "Not connected" + + self.set_tooltip_text(tooltip) + + def on_wifi_device_added(self, *args): + self.network.wifi_device.connect("changed", self.on_wifi_changed) + self.network.wifi_device.connect("ap-added", self.on_wifi_changed) + self.network.wifi_device.connect("ap-removed", self.on_wifi_changed) + self.update_state() + + def on_wifi_device_removed(self, *args): + self.update_state() + + def on_network_changed(self, *args): + self.update_state() + + def on_wifi_changed(self, *args): + self.update_state() + + def on_ethernet_device_added(self, *args): + if self.network.ethernet_device: + self.network.ethernet_device.connect("changed", self.on_ethernet_changed) + self.update_state() + + def on_ethernet_device_removed(self, *args): + self.update_state() + + def on_ethernet_changed(self, *args): + self.update_state() + + +class BluetoothIndicator(Button): + def __init__(self, **kwargs): + super().__init__(name="button-bar-bluetooth", **kwargs) + + # Initialize BluetoothClient + self.bluetooth = BluetoothClient() + + # Create bluetooth label + self.bt_label = Label(name="bluetooth-label", markup=icons.loader) + self.add(self.bt_label) + + # Connect signals + self.bluetooth.connect("changed", self.on_bluetooth_changed) + self.bluetooth.connect("device-added", self.on_device_added) + self.bluetooth.connect("device-removed", self.on_device_removed) + + # Initial state update + self.update_state() + + def update_state(self): + if not self.bluetooth.enabled: + self.bt_label.set_markup(icons.bluetooth_off) + tooltip = "Bluetooth disabled" + else: + connected_devices = self.bluetooth.connected_devices + if connected_devices: + self.bt_label.set_markup(icons.bluetooth_connected) + if len(connected_devices) == 1: + device = connected_devices[0] + tooltip = f"Connected to {device.alias}" + if device.battery_percentage > 0: + tooltip += f" ({device.battery_percentage:.0f}%)" + else: + tooltip = f"Connected to {len(connected_devices)} devices" + else: + if self.bluetooth.scanning: + self.bt_label.set_markup(icons.loader) + tooltip = "Scanning for devices..." + else: + self.bt_label.set_markup(icons.bluetooth) + tooltip = "No devices connected" + + self.set_tooltip_text(tooltip) + + def on_bluetooth_changed(self, *args): + self.update_state() + + def on_device_added(self, _, address): + self.update_state() + + def on_device_removed(self, _, address): + self.update_state() + + +class Indicators(Box): + def __init__(self, **kwargs): + super().__init__( + name="indicator", + orientation="h" if not data.VERTICAL else "v", + spacing=4, + children=[WifiIndicator(), BluetoothIndicator()], + **kwargs, + ) + self.show_all() \ No newline at end of file diff --git a/modules/dock/main.py b/modules/dock/main.py new file mode 100644 index 0000000..f18926e --- /dev/null +++ b/modules/dock/main.py @@ -0,0 +1,1047 @@ +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 modules.dock.components import DockSystemTray +from utils.icon_resolver import IconResolver +from utils.occlusion import check_occlusion +from utils.wayland import WaylandWindow as Window + + +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 = [] + + def __init__(self, **kwargs): + self.icon_size = 28 + 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 + + 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.DOCK_POSITION == "Right": + anchor_to_set = "right" + revealer_transition_type = "slide-left" + elif data.DOCK_POSITION == "Left": + 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="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 + self._drag_in_progress = False + self.always_occluded = data.DOCK_ALWAYS_OCCLUDED + 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.DOCK_POSITION == "Left" + else ["right"] + if data.DOCK_POSITION == "Right" + else [], + ) + + self.wrapper.set_orientation(dock_wrapper_orientation_val) + self.view.set_orientation(dock_wrapper_orientation_val) + + 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") + + 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) + + # Create system tray section + separator_orientation = ( + Gtk.Orientation.VERTICAL + if self.view.get_orientation() == Gtk.Orientation.HORIZONTAL + else Gtk.Orientation.HORIZONTAL + ) + + # Initialize system tray with proper orientation + self.system_tray = DockSystemTray( + orientation_val=dock_wrapper_orientation_val + ) + + # Initialize system tray with proper orientation + self.system_tray = DockSystemTray( + orientation_val=dock_wrapper_orientation_val + ) + + # 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", + ) + ) + self.view.add(self.system_tray) + else: + self.view.pack_start(self.system_tray, False, False, 0) + 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() + 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.DOCK_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: + 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() + 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: + 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): + self.is_mouse_over_dock_area = False + self.delay_hide() + + def _on_dock_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 + 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 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): + 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) + self.hide_id = GLib.timeout_add(250, self.hide_dock_if_not_hovered) + + def hide_dock_if_not_hovered(self): + self.hide_id = None + if ( + not self.is_mouse_over_dock_area + and not self._drag_in_progress + and not self._prevent_occlusion + and data.DOCK_AUTO_HIDE + ): + 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.is_mouse_over_dock_area + or self._drag_in_progress + or self._prevent_occlusion + or not data.DOCK_AUTO_HIDE + ): + 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 = [] + separator_orientation = ( + Gtk.Orientation.VERTICAL + if self.view.get_orientation() == Gtk.Orientation.HORIZONTAL + else Gtk.Orientation.HORIZONTAL + ) + + # Add components based on position + if data.DOCK_POSITION == "Right": + 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.system_tray) + else: # Bottom or Left position + children.append(self.system_tray) + 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) + + 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()) + 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.is_mouse_over_dock_area + or self._drag_in_progress + or self._prevent_occlusion + or 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) + 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(): + # Check if dropped outside the dock by checking the pointer position + display = self.get_display() + seat = display.get_default_seat() + pointer = seat.get_pointer() + window = self.get_window() + + if window: + screen, x, y = pointer.get_position() # Fix: Unpack all three values + win_x, win_y = window.get_position() + win_w = window.get_width() + win_h = window.get_height() + + # If pointer is outside dock window bounds + if (x < win_x or x > win_x + win_w or + y < win_y or y > win_y + win_h): + app_id_dragged = widget.app_identifier + instances_dragged = widget.instances + + 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 + self.check_occlusion_state() + + 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: + GLib.idle_add(dock_instance.check_config_change_immediate) + + @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/modules/dock/metrics.py b/modules/dock/metrics.py new file mode 100644 index 0000000..0cf6b5d --- /dev/null +++ b/modules/dock/metrics.py @@ -0,0 +1,299 @@ +import json +import logging +import subprocess + +import psutil +from fabric.widgets.box import Box +from fabric.widgets.button import Button +from fabric.widgets.circularprogressbar import CircularProgressBar +from fabric.widgets.label import Label +from fabric.widgets.revealer import Revealer +from gi.repository import GLib + +import config.data as data +import utils.icons as icons + +logger = logging.getLogger(__name__) + +class MetricsProvider: + """ + Class responsible for obtaining centralized CPU, memory, disk usage, and battery metrics. + It updates periodically so that all widgets querying it display the same values. + """ + def __init__(self): + self.gpu = [] + self.cpu = 0.0 + self.mem = 0.0 + self.swap = 0.0 + self.disk = [] + + self.bat_percent = 0.0 + self.bat_charging = None + self.bat_time = 0 + + self._gpu_update_running = False + + GLib.timeout_add_seconds(1, self._update) + + def _update(self): + self.cpu = psutil.cpu_percent(interval=0) + self.mem = psutil.virtual_memory().percent + self.swap = psutil.swap_memory().percent + self.disk = [psutil.disk_usage(path).percent for path in data.METRICS_DISKS] + + if not self._gpu_update_running: + self._start_gpu_update_async() + + return True + + def _start_gpu_update_async(self): + """Starts a new GLib thread to run nvtop in the background.""" + self._gpu_update_running = True + + GLib.Thread.new("nvtop-thread", lambda _: self._run_nvtop_in_thread(), None) + + def _run_nvtop_in_thread(self): + """Runs nvtop via subprocess in a separate GLib thread.""" + output = None + error_message = None + try: + result = subprocess.check_output(["nvtop", "-s"], text=True, timeout=10) + output = result + except FileNotFoundError: + error_message = "nvtop command not found." + logger.warning(error_message) + except subprocess.CalledProcessError as e: + error_message = f"nvtop failed with exit code {e.returncode}: {e.stderr.strip()}" + logger.error(error_message) + except subprocess.TimeoutExpired: + error_message = "nvtop command timed out." + logger.error(error_message) + except Exception as e: + error_message = f"Unexpected error running nvtop: {e}" + logger.error(error_message) + + GLib.idle_add(self._process_gpu_output, output, error_message) + self._gpu_update_running = False + + def _process_gpu_output(self, output, error_message): + """Process nvtop JSON output on the main loop.""" + try: + if error_message: + logger.error(f"GPU update failed: {error_message}") + self.gpu = [] + elif output: + try: + info = json.loads(output) + if not info or not isinstance(info, list): + logger.warning("Invalid nvtop data format") + self.gpu = [] + return False + + gpu_utils = [] + for gpu in info: + if not isinstance(gpu, dict): + continue + util = gpu.get("gpu_util", "0%") + if not isinstance(util, str): + continue + try: + # Remove % and convert to int, default to 0 if fails + util_value = int(util.rstrip("%") or 0) + gpu_utils.append(util_value) + except (ValueError, TypeError): + continue + + self.gpu = gpu_utils if gpu_utils else [] + except (KeyError, ValueError, TypeError, AttributeError) as e: + logger.error(f"Failed parsing nvtop JSON: {e}") + self.gpu = [] + else: + logger.warning("nvtop returned no output.") + self.gpu = [] + except json.JSONDecodeError as e: + logger.error(f"JSON decode error: {e}") + self.gpu = [] + except Exception as e: + logger.error(f"Error processing nvtop output: {e}") + self.gpu = [] + + return False + + def get_metrics(self): + return (self.cpu, self.mem, self.swap, self.disk, self.gpu) + + def get_battery(self): + return (self.bat_percent, self.bat_charging, self.bat_time) + + def get_gpu_info(self): + try: + result = subprocess.check_output(["nvtop", "-s"], text=True, timeout=5) + if not result.strip(): + return [] + + info = json.loads(result) + if not info or not isinstance(info, list): + return [] + + valid_gpus = [] + for gpu in info: + if not isinstance(gpu, dict): + continue + + gpu_data = { + 'device_name': gpu.get('device_name', 'GPU'), + 'gpu_util': gpu.get('gpu_util', '0%'), + 'mem_util': gpu.get('mem_util', '0%'), + 'temperature': gpu.get('temperature', '0 C'), + 'power': gpu.get('power', '0 W') + } + valid_gpus.append(gpu_data) + + return valid_gpus + + except FileNotFoundError: + logger.warning("nvtop not found; GPU info unavailable.") + return [] + except subprocess.CalledProcessError as e: + logger.error(f"nvtop init sync failed: {e}") + return [] + except subprocess.TimeoutExpired: + logger.error("nvtop init call timed out.") + return [] + except json.JSONDecodeError as e: + logger.error(f"Init JSON parse error: {e}") + return [] + except Exception as e: + logger.error(f"Unexpected error during GPU init: {e}") + return [] + +shared_provider = MetricsProvider() + +class SingularMetric: + def __init__(self, id, name, icon): + self.name_markup = name + self.icon_markup = icon + + self.icon = Label(name="metrics-icon", markup=icon) + self.circle = CircularProgressBar( + name="metrics-circle", + value=0, + size=28, + line_width=2, + start_angle=150, + end_angle=390, + style_classes=id, + child=self.icon, + ) + + self.level = Label(name="metrics-level", style_classes=id, label="0%") + self.revealer = Revealer( + name=f"metrics-{id}-revealer", + transition_duration=250, + transition_type="slide-left", + child=self.level, + child_revealed=False, + ) + + self.box = Box( + name=f"metrics-{id}-box", + orientation="h", + spacing=0, + children=[self.circle, self.revealer], + ) + + def markup(self): + return f"{self.icon_markup} {self.name_markup}: {self.level.get_label()}" + +class Metrics(Button): + def __init__(self, **kwargs): + super().__init__(name="metrics", **kwargs) + + main_box = Box( + spacing=0, + orientation="h" if not data.VERTICAL else "v", + visible=True, + all_visible=True, + ) + + visible = getattr(data, "METRICS_VISIBLE", {'cpu': True, 'ram': True, 'swap': True, 'disk': True, 'gpu': True}) + disks = [SingularMetric("disk", f"DISK ({path})" if len(data.METRICS_DISKS) != 1 else "DISK", icons.disk) + for path in data.METRICS_DISKS] if visible.get('disk', True) else [] + + gpu_info = shared_provider.get_gpu_info() + gpus = [SingularMetric(f"gpu", f"GPU ({v['device_name']})" if len(gpu_info) != 1 else "GPU", icons.gpu) + for v in gpu_info] if visible.get('gpu', True) else [] + + self.cpu = SingularMetric("cpu", "CPU", icons.cpu) if visible.get('cpu', True) else None + self.ram = SingularMetric("ram", "RAM", icons.memory) if visible.get('ram', True) else None + self.swap = SingularMetric("swap", "SWAP", icons.swap) if visible.get('swap', True) else None + self.disk = disks + self.gpu = gpus + + for disk in self.disk: + main_box.add(disk.box) + main_box.add(Box(name="metrics-sep")) + if self.ram: + main_box.add(self.ram.box) + main_box.add(Box(name="metrics-sep")) + if self.swap: + main_box.add(self.swap.box) + main_box.add(Box(name="metrics-sep")) + if self.cpu: + main_box.add(self.cpu.box) + for gpu in self.gpu: + main_box.add(Box(name="metrics-sep")) + main_box.add(gpu.box) + + self.add(main_box) + + self.connect("clicked", self.on_click) + self.labels_visible = False + + GLib.timeout_add_seconds(1, self.update_metrics) + + def _format_percentage(self, value: int) -> str: + """Formato natural del porcentaje sin forzar ancho fijo.""" + return f"{value}%" + + def on_click(self, widget): + if not data.VERTICAL: + self.labels_visible = not self.labels_visible + if self.cpu: self.cpu.revealer.set_reveal_child(self.labels_visible) + if self.ram: self.ram.revealer.set_reveal_child(self.labels_visible) + if self.swap: self.swap.revealer.set_reveal_child(self.labels_visible) + for disk in self.disk: + disk.revealer.set_reveal_child(self.labels_visible) + for gpu in self.gpu: + gpu.revealer.set_reveal_child(self.labels_visible) + + def update_metrics(self): + cpu, mem, swap, disks, gpus = shared_provider.get_metrics() + + if self.cpu: + self.cpu.circle.set_value(cpu / 100.0) + self.cpu.level.set_label(self._format_percentage(int(cpu))) + if self.ram: + self.ram.circle.set_value(mem / 100.0) + self.ram.level.set_label(self._format_percentage(int(mem))) + if self.swap: + self.swap.circle.set_value(swap / 100.0) + self.swap.level.set_label(self._format_percentage(int(swap))) + for i, disk in enumerate(self.disk): + if i < len(disks): + disk.circle.set_value(disks[i] / 100.0) + disk.level.set_label(self._format_percentage(int(disks[i]))) + for i, gpu in enumerate(self.gpu): + if i < len(gpus): + gpu.circle.set_value(gpus[i] / 100.0) + gpu.level.set_label(self._format_percentage(int(gpus[i]))) + + tooltip_metrics = [] + if self.disk: tooltip_metrics.extend(self.disk) + if self.ram: tooltip_metrics.append(self.ram) + if self.swap: tooltip_metrics.append(self.swap) + if self.cpu: tooltip_metrics.append(self.cpu) + if self.gpu: tooltip_metrics.extend(self.gpu) + self.set_tooltip_markup((" - " if not data.VERTICAL else "\n").join([v.markup() for v in tooltip_metrics])) + + return True diff --git a/modules/dock/workspaces.py b/modules/dock/workspaces.py new file mode 100644 index 0000000..2d8e7ba --- /dev/null +++ b/modules/dock/workspaces.py @@ -0,0 +1,19 @@ +import json + +from fabric.hyprland.service import Hyprland +from fabric.widgets.label import Label + +connection = Hyprland() + +def create_workspace_widget(): + workspaceData = connection.send_command("j/activeworkspace").reply + activeWorkspace = json.loads(workspaceData.decode("utf-8"))["name"] + workspace_label = Label(label=f"{activeWorkspace}") + + def on_workspace(obj, signal): + nonlocal workspace_label + activeWorkspace = json.loads(signal.data[0]) + workspace_label.set_label(f"{activeWorkspace}") + + connection.connect("event::workspace", on_workspace) + return workspace_label diff --git a/modules/launcher/components/__init__.py b/modules/launcher/components/__init__.py deleted file mode 100644 index da2ef07..0000000 --- a/modules/launcher/components/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -from .applauncher import * -from .bluetooth import * -from .cliphist import * -from .dashboard import * -from .emoji import * -from .power import * -from .sh import * -from .wallpapers import * -from .wifi import * -from .calendar import * -from .dashboard import * -from .tools import * -from .kanban import * -from .tmux import * -from .window_switcher import * -from .notification_center import * diff --git a/modules/launcher/components/applauncher.py b/modules/launcher/components/applauncher.py deleted file mode 100644 index 83380de..0000000 --- a/modules/launcher/components/applauncher.py +++ /dev/null @@ -1,630 +0,0 @@ -import operator -from collections.abc import Iterator -from fabric.widgets.box import Box -from fabric.widgets.label import Label -from fabric.widgets.button import Button -from fabric.widgets.entry import Entry -from fabric.widgets.image import Image -from fabric.widgets.scrolledwindow import ScrolledWindow -from fabric.utils import ( - DesktopApp, - get_desktop_applications, - idle_add, - remove_handler, - exec_shell_command_async, -) -from fabric.utils.helpers import get_relative_path -from gi.repository import GLib, Gdk -import utils.icons as icons -import config.data as data -import json -import os -import re -import math -import numpy as np -import subprocess -from modules.dock import Dock # Import the Dock class - - -class AppLauncher(Box): - def __init__(self, **kwargs): - super().__init__( - name="app-launcher", - visible=False, - all_visible=False, - **kwargs, - ) - - self.launcher = kwargs["launcher"] - self.selected_index = -1 # Track the selected item index - - self._arranger_handler: int = 0 - self._all_apps = get_desktop_applications() - self.config = data.read_config() - - # Calculator history initialization - self.calc_history_path = f"{data.CACHE_DIR}/calc.json" - if os.path.exists(self.calc_history_path): - with open(self.calc_history_path, "r") as f: - self.calc_history = json.load(f) - else: - self.calc_history = [] - - self.viewport = Box(name="viewport", spacing=4, orientation="v") - self.search_entry = Entry( - name="search-entry", - placeholder="Search Applications...", - h_expand=True, - notify_text=self.notify_text, # Use the method instead of lambda - on_activate=lambda entry, *_: self.on_search_entry_activate( - entry.get_text() - ), - on_key_press_event=self.on_search_entry_key_press, # Handle key presses - ) - self.search_entry.props.xalign = 0.5 - self.scrolled_window = ScrolledWindow( - name="scrolled-window", - spacing=10, - min_content_size=(450, 105), - max_content_size=(450, 105), - child=self.viewport, - ) - - self.header_box = Box( - name="header_box", - spacing=10, - orientation="h", - children=[ - Button( - name="config-button", - child=Label(name="config-label", markup=icons.config), - on_clicked=lambda *_: ( - exec_shell_command_async( - f"python {get_relative_path('../../../config/config.py')}" - ), - self.close_launcher(), - ), - ), - self.search_entry, - ], - ) - - self.launcher_box = Box( - name="launcher-box", - spacing=10, - h_expand=True, - orientation="v", - children=[ - self.header_box, - self.scrolled_window, - ], - ) - - self.resize_viewport() - - self.add(self.launcher_box) - self.show_all() - - def close_launcher(self): - self.viewport.children = [] - self.selected_index = -1 # Reset selection - self.launcher.close() - - def open_launcher(self): - self._all_apps = get_desktop_applications() - self.arrange_viewport() - - def ensure_initialized(self): - """Make sure the launcher is initialized with apps list before opening""" - if not hasattr(self, "_initialized"): - # Force pre-loading apps - self._all_apps = get_desktop_applications() - self._initialized = True - return True # Was initialized for first time - return False # Was already initialized - - def arrange_viewport(self, query: str = ""): - if query.startswith("="): - # In calculator mode, update history view once (not per keystroke) - self.show_scrolled_window() - self.launcher.dashboard.hide() - self.update_calculator_viewport() - return - remove_handler(self._arranger_handler) if self._arranger_handler else None - self.viewport.children = [] - self.selected_index = -1 # Clear selection when viewport changes - - if query.startswith("/"): - self.launcher.dashboard.hide() - return - - if query.strip(): - self.launcher.dashboard.hide() - self.show_scrolled_window() - else: - self.launcher.dashboard.show() - self.hide_scrolled_window() - return False - - filtered_apps_iter = iter( - sorted( - [ - app - for app in self._all_apps - if query.casefold() - in ( - (app.display_name or "") - + (" " + app.name + " ") - + (app.generic_name or "") - ).casefold() - ], - key=lambda app: (app.display_name or "").casefold(), - ) - ) - should_resize = operator.length_hint(filtered_apps_iter) == len(self._all_apps) - - self._arranger_handler = idle_add( - lambda apps_iter: self.add_next_application(apps_iter) - or self.handle_arrange_complete(should_resize, query), - filtered_apps_iter, - pin=True, - ) - - def handle_arrange_complete(self, should_resize, query): - if should_resize: - self.resize_viewport() - # Only auto-select first item if query exists - if query.strip() != "" and self.viewport.get_children(): - self.update_selection(0) - return False - - def add_next_application(self, apps_iter: Iterator[DesktopApp]): - if not (app := next(apps_iter, None)): - return False - self.viewport.add(self.bake_application_slot(app)) - return True - - def show_scrolled_window(self): - if hasattr(self, "scrolled_window") and self.scrolled_window: - self.scrolled_window.show_all() - - def hide_scrolled_window(self): - if hasattr(self, "scrolled_window") and self.scrolled_window: - self.scrolled_window.hide() - - def resize_viewport(self): - self.scrolled_window.set_min_content_width( - self.viewport.get_allocation().width # type: ignore - ) - return False - - def bake_application_slot(self, app: DesktopApp, **kwargs) -> Button: - button = Button( - name="slot-button", - child=Box( - name="slot-box", - orientation="h", - spacing=10, - children=[ - Image( - name="app-icon", - pixbuf=app.get_icon_pixbuf(size=24), - h_align="start", - ), - Label( - name="app-label", - label=app.display_name or "Unknown", - ellipsization="end", - v_align="center", - h_align="center", - ), - Label( - name="app-desc", - label=app.description or "", - ellipsization="end", - v_align="center", - h_align="start", - h_expand=True, - ), - ], - ), - tooltip_text=app.description, - on_clicked=lambda *_: (app.launch(), self.close_launcher()), - **kwargs, - ) - return button - - def update_selection(self, new_index: int): - # Unselect current - if self.selected_index != -1 and self.selected_index < len( - self.viewport.get_children() - ): - current_button = self.viewport.get_children()[self.selected_index] - current_button.get_style_context().remove_class("selected") - # Select new - if new_index != -1 and new_index < len(self.viewport.get_children()): - new_button = self.viewport.get_children()[new_index] - new_button.get_style_context().add_class("selected") - self.selected_index = new_index - self.scroll_to_selected(new_button) - else: - self.selected_index = -1 - - def scroll_to_selected(self, button): - def scroll(): - adj = self.scrolled_window.get_vadjustment() - alloc = button.get_allocation() - if alloc.height == 0: - return False # Retry if allocation isn't ready - - y = alloc.y - height = alloc.height - page_size = adj.get_page_size() - current_value = adj.get_value() - - # Calculate visible boundaries - visible_top = current_value - visible_bottom = current_value + page_size - - if y < visible_top: - # Item above viewport - align to top - adj.set_value(y) - elif y + height > visible_bottom: - # Item below viewport - align to bottom - new_value = y + height - page_size - adj.set_value(new_value) - # No action if already fully visible - return False - - GLib.idle_add(scroll) - - def on_search_entry_activate(self, text): - # This method is now only used for non-Enter key activations - commands = self.config.get("commands", {}) - - # Check if text is a mathematical expression - if text and (text[0].isdigit()): - self.evaluate_calculator_expression(text) - return - - if text in commands: - command = commands[text] - eval(command, {"launcher": self.launcher}) - else: - children = self.viewport.get_children() - if children: - # Only activate if we have selection or non-empty query - if text.strip() == "" and self.selected_index == -1: - return # Prevent accidental activation when empty - selected_index = self.selected_index if self.selected_index != -1 else 0 - if 0 <= selected_index < len(children): - children[selected_index].clicked() - - def on_search_entry_key_press(self, widget, event): - text = widget.get_text() - - # Check if we're in calculator mode - if text.startswith("="): - if event.keyval == Gdk.KEY_Down: - self.move_selection(1) - return True - elif event.keyval == Gdk.KEY_Up: - self.move_selection(-1) - return True - elif event.keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter): - # In calculator mode, if a history item is highlighted: - if self.selected_index != -1 and self.selected_index < len( - self.calc_history - ): - if event.state & Gdk.ModifierType.SHIFT_MASK: - # Shift+Enter deletes the selected calculator history item - self.delete_selected_calc_history() - else: - # Normal Enter copies the result - selected_text = self.calc_history[self.selected_index] - self.copy_text_to_clipboard(selected_text) - # Clear selection so new expressions are evaluated on further Return presses - self.selected_index = -1 - else: - # Force reset selection index - self.selected_index = -1 - # No item selected, evaluate the expression - self.evaluate_calculator_expression(text) - return True - elif event.keyval == Gdk.KEY_Escape: - self.close_launcher() - return True - return False - else: - # Normal app mode behavior - if event.keyval == Gdk.KEY_Down: - self.move_selection(1) - return True - elif event.keyval == Gdk.KEY_Up: - self.move_selection(-1) - return True - elif event.keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter) and ( - event.state & Gdk.ModifierType.SHIFT_MASK - ): - # Only add to dock when in app mode (not calculator mode) - self.add_selected_app_to_dock() - return True - elif event.keyval == Gdk.KEY_Escape: - self.close_launcher() - return True - return False - - def notify_text(self, entry, *_): - """Handle text changes in the search entry""" - text = entry.get_text() - if text.startswith("="): - # In calculator mode, show scrolled window and update viewport - self.show_scrolled_window() - self.launcher.dashboard.hide() - self.update_calculator_viewport() - # Always reset selection when typing a new expression - self.selected_index = -1 - else: - self.arrange_viewport(text) - - def add_selected_app_to_dock(self): - """Adds the currently selected application to the modus.json file with comprehensive metadata.""" - children = self.viewport.get_children() - if ( - not children - or self.selected_index == -1 - or self.selected_index >= len(children) - ): - return # No app selected - - selected_button = children[self.selected_index] - # Extract the app's display name from the label to find the DesktopApp object - selected_app = next( - ( - app - for app in self._all_apps - if app.display_name - == selected_button.get_child().get_children()[1].props.label - ), - None, - ) - if not selected_app: - return - - # Create comprehensive app data dictionary - Include all available properties - # Filter out None values to keep the JSON clean - app_data = { - k: v - for k, v in { - "name": selected_app.name, - "display_name": selected_app.display_name, - "window_class": selected_app.window_class, - "executable": selected_app.executable, - "command_line": selected_app.command_line, - "icon_name": selected_app.icon_name, - }.items() - if v is not None - } - - config_path = get_relative_path("../../../config/assets/modus.json") - try: - with open(config_path, "r") as file: - data = json.load(file) - except (FileNotFoundError, json.JSONDecodeError): - data = {"pinned_apps": []} - - # Check if app is already pinned (by name) - already_pinned = False - for pinned_app in data.get("pinned_apps", []): - if ( - isinstance(pinned_app, dict) - and pinned_app.get("name") == app_data["name"] - ): - already_pinned = True - # Update existing entry with latest app data - pinned_app.update(app_data) - break - elif isinstance(pinned_app, str) and pinned_app == app_data["name"]: - already_pinned = True - # Replace string format with new comprehensive format - data["pinned_apps"].remove(pinned_app) - data["pinned_apps"].append(app_data) - break - - # Add to pinned apps if not already there - if not already_pinned: - data.setdefault("pinned_apps", []).append(app_data) - - # Write the updated config - with open(config_path, "w") as file: - json.dump(data, file, indent=4) - - # Notify dock instances of the configuration change immediately - Dock.notify_config_change() - - def move_selection(self, delta: int): - children = self.viewport.get_children() - if not children: - return - # Allow starting selection from nothing when empty - if self.selected_index == -1 and delta == 1: - new_index = 0 - else: - new_index = self.selected_index + delta - new_index = max(0, min(new_index, len(children) - 1)) - self.update_selection(new_index) - - def save_calc_history(self): - with open(self.calc_history_path, "w") as f: - json.dump(self.calc_history, f) - - def evaluate_calculator_expression(self, text: str): - # Add debug print - print(f"Evaluating calculator expression: {text}") - - # Remove the '=' prefix and extra spaces - expr = text.lstrip("=").strip() - if not expr: - return - - # Common replacements to make expressions more natural - replacements = { - "^": "**", # Power operator - "×": "*", # Multiplication - "÷": "/", # Division - "π": "np.pi", # Pi constant - "pi": "np.pi", # Pi constant (text) - "e": "np.e", # Euler's number - "sin(": "np.sin(", # Sine - "cos(": "np.cos(", # Cosine - "tan(": "np.tan(", # Tangent - "log(": "np.log10(", # Log base 10 - "ln(": "np.log(", # Natural log - "sqrt(": "np.sqrt(", # Square root - "abs(": "np.abs(", # Absolute value - "exp(": "np.exp(", # Exponential - } - - # Apply all replacements - for old, new in replacements.items(): - expr = expr.replace(old, new) - - # Replace factorial: e.g. 5! -> np.factorial(5) - expr = re.sub(r"(\d+)!", r"np.factorial(\1)", expr) - - # Replace brackets: allow [] and {} as () - for old, new in [("[", "("), ("]", ")"), ("{", "("), ("}", ")")]: - expr = expr.replace(old, new) - - # Define the safe execution environment - safe_dict = { - "np": np, - "math": math, - "arange": np.arange, - "linspace": np.linspace, - "array": np.array, - } - - try: - # Evaluate the expression in the safe environment - result = eval(expr, {"__builtins__": None}, safe_dict) - - # Format the result based on its type - if isinstance(result, np.ndarray): - if result.size > 10: # Truncate large arrays - result_str = f"Array of shape {result.shape}" - else: - result_str = str(result) - elif isinstance(result, (int, float, np.number)): - # Format numbers nicely - integers as integers, floats with limited precision - if isinstance(result, (int, np.integer)) or result.is_integer(): - result_str = str(int(result)) - else: - result_str = f"{float(result):.10g}" # Remove trailing zeros - else: - result_str = str(result) - - except Exception as e: - result_str = f"Error: {str(e)}" - - # Prepend to history (newest first) - self.calc_history.insert(0, f"{text} => {result_str}") - self.save_calc_history() - self.update_calculator_viewport() - - def update_calculator_viewport(self): - self.viewport.children = [] - for item in self.calc_history: - btn = self.create_calc_history_button(item) - self.viewport.add(btn) - # Don't reset selection index here automatically - # Ensure selection state stays valid - if self.selected_index >= len(self.calc_history): - self.selected_index = -1 - - def create_calc_history_button(self, text: str) -> Button: - # Parse the result to create a more readable display - if "=>" in text: - parts = text.split("=>") - expression = parts[0].strip() - result = parts[1].strip() - - # For very long results, truncate for display but keep full in tooltip - display_text = text - if len(result) > 50: # Truncate long results - display_text = f"{expression} => {result[:47]}..." - - btn = Button( - name="slot-button", # reuse existing CSS styling - child=Box( - name="calc-slot-box", - orientation="h", - spacing=10, - children=[ - Label( - name="calc-label", - label=display_text, - ellipsization="end", - v_align="center", - h_align="center", - ), - ], - ), - tooltip_text=text, - on_clicked=lambda *_: self.copy_text_to_clipboard(text), - ) - else: - # Fallback for non-calculation entries - btn = Button( - name="slot-button", - child=Box( - name="calc-slot-box", - orientation="h", - spacing=10, - children=[ - Label( - name="calc-label", - label=text, - ellipsization="end", - v_align="center", - h_align="center", - ), - ], - ), - tooltip_text=text, - on_clicked=lambda *_: self.copy_text_to_clipboard(text), - ) - return btn - - def copy_text_to_clipboard(self, text: str): - # Split the text on "=>" and copy only the result part if available - parts = text.split("=>", 1) - copy_text = parts[1].strip() if len(parts) > 1 else text - try: - subprocess.run(["wl-copy"], input=copy_text.encode(), check=True) - except subprocess.CalledProcessError as e: - print(f"Clipboard copy failed: {e}") - - def delete_selected_calc_history(self): - if self.selected_index != -1 and self.selected_index < len(self.calc_history): - # Store the current index before deletion - current_index = self.selected_index - - # Delete the item - del self.calc_history[current_index] - self.save_calc_history() - - # Determine the new selection index - # If we deleted the first item, stay at index 0 - # Otherwise, move to the previous item - new_index = 0 if current_index == 0 else current_index - 1 - - # Reset selection before updating viewport - self.selected_index = -1 - - # Update the viewport - self.update_calculator_viewport() - - # If we still have items, select the determined index - if len(self.calc_history) > 0: - self.update_selection(min(new_index, len(self.calc_history) - 1)) diff --git a/modules/launcher/components/bluetooth.py b/modules/launcher/components/bluetooth.py deleted file mode 100644 index c3f6930..0000000 --- a/modules/launcher/components/bluetooth.py +++ /dev/null @@ -1,170 +0,0 @@ -from fabric.widgets.box import Box -from fabric.widgets.label import Label -from fabric.widgets.image import Image -from fabric.widgets.button import Button -from fabric.widgets.centerbox import CenterBox -from fabric.widgets.scrolledwindow import ScrolledWindow -from fabric.bluetooth import BluetoothClient, BluetoothDevice -import utils.icons as icons - - -class BluetoothDeviceSlot(CenterBox): - def __init__(self, device: BluetoothDevice, **kwargs): - super().__init__(name="bluetooth-device", **kwargs) - self.device = device - self.device.connect("changed", self.on_changed) - self.device.connect( - "notify::closed", lambda *_: self.device.closed and self.destroy() - ) - - self.connection_label = Label( - name="bluetooth-connection", markup=icons.bluetooth_disconnected - ) - self.connect_button = Button( - name="bluetooth-connect", - label="Connect", - on_clicked=lambda *_: self.device.set_connecting(not self.device.connected), - ) - - self.start_children = [ - Box( - spacing=8, - children=[ - Image(icon_name=device.icon_name + "-symbolic", size=32), - Label(label=device.name), - self.connection_label, - ], - ) - ] - self.end_children = self.connect_button - - self.device.emit("changed") # to update display status - - def on_changed(self, *_): - self.connection_label.set_markup( - icons.bluetooth_connected - if self.device.connected - else icons.bluetooth_disconnected - ) - if self.device.connecting: - self.connect_button.set_label( - "Connecting..." if not self.device.connecting else "Disconnecting..." - ) - else: - self.connect_button.set_label( - "Connect" if not self.device.connected else "Disconnect" - ) - return - - -class BluetoothConnections(Box): - def __init__(self, **kwargs): - super().__init__( - name="bluetooth", - spacing=4, - orientation="vertical", - **kwargs, - ) - - self.launcher = kwargs["launcher"] - - self.buttons = self.launcher.dashboard.bluetooth_button - self.bt_status_text = self.buttons.bluetooth_status_text - self.bt_status_button = self.buttons.bluetooth_status_button - self.bt_icon = self.buttons.bluetooth_icon - self.bt_label = self.buttons.bluetooth_label - self.bt_menu_button = self.buttons.bluetooth_menu_button - self.bt_menu_label = self.buttons.bluetooth_menu_label - - self.client = BluetoothClient(on_device_added=self.on_device_added) - self.scan_label = Label(name="bluetooth-scan-label", markup=icons.radar) - self.scan_button = Button( - name="bluetooth-scan", - child=self.scan_label, - tooltip_text="Scan for Bluetooth devices", - on_clicked=lambda *_: self.client.toggle_scan(), - ) - self.back_button = Button( - name="bluetooth-back", - child=Label(name="bluetooth-back-label", markup=icons.chevron_left), - on_clicked=lambda *_: self.launcher.open("launcher"), - ) - - self.client.connect("notify::enabled", lambda *_: self.status_label()) - self.client.connect("notify::scanning", lambda *_: self.update_scan_label()) - - self.paired_box = Box(spacing=2, orientation="vertical") - self.available_box = Box(spacing=2, orientation="vertical") - - # Create a single content container with the required structure: - # [Paired label] [Paired devices list] [Available label] [Available devices list] - content_box = Box(spacing=4, orientation="vertical") - content_box.add(self.paired_box) - content_box.add(Label(name="bluetooth-section", label="Available")) - content_box.add(self.available_box) - - self.children = [ - CenterBox( - name="bluetooth-header", - start_children=self.back_button, - center_children=Label(name="bluetooth-text", label="Bluetooth Devices"), - end_children=self.scan_button, - ), - ScrolledWindow( - name="bluetooth-devices", - min_content_size=(-1, -1), - child=content_box, - v_expand=True, - ), - ] - - # Trigger initial notifications to update status without delay. - self.client.notify("scanning") - self.client.notify("enabled") - - def status_label(self): - print(self.client.enabled) - if self.client.enabled: - self.bt_status_text.set_label("Enabled") - for i in [ - self.bt_status_button, - self.bt_status_text, - self.bt_icon, - self.bt_label, - self.bt_menu_button, - self.bt_menu_label, - ]: - i.remove_style_class("disabled") - self.bt_icon.set_markup(icons.bluetooth) - else: - self.bt_status_text.set_label("Disabled") - for i in [ - self.bt_status_button, - self.bt_status_text, - self.bt_icon, - self.bt_label, - self.bt_menu_button, - self.bt_menu_label, - ]: - i.add_style_class("disabled") - self.bt_icon.set_markup(icons.bluetooth_off) - - def on_device_added(self, client: BluetoothClient, address: str): - if not (device := client.get_device(address)): - return - slot = BluetoothDeviceSlot(device) - - if device.paired: - return self.paired_box.add(slot) - return self.available_box.add(slot) - - def update_scan_label(self): - if self.client.scanning: - self.scan_label.add_style_class("scanning") - self.scan_button.add_style_class("scanning") - self.scan_button.set_tooltip_text("Stop scanning for Bluetooth devices") - else: - self.scan_label.remove_style_class("scanning") - self.scan_button.remove_style_class("scanning") - self.scan_button.set_tooltip_text("Scan for Bluetooth devices") - diff --git a/modules/launcher/components/calendar.py b/modules/launcher/components/calendar.py deleted file mode 100644 index 263e572..0000000 --- a/modules/launcher/components/calendar.py +++ /dev/null @@ -1,217 +0,0 @@ -import calendar -from datetime import datetime -import gi -import utils.icons as icons -from fabric.widgets.label import Label -from fabric.widgets.box import Box -from fabric.widgets.centerbox import CenterBox - -gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, GLib - - -class Calendar(Gtk.Box): - def __init__(self): - super().__init__( - orientation=Gtk.Orientation.VERTICAL, spacing=8, name="calendar" - ) - - self.current_year = datetime.now().year - self.current_month = datetime.now().month - self.current_day = datetime.now().day - # Store previous month/year to determine transition direction. - self.previous_key = (self.current_year, self.current_month) - - # Cache threshold in number of months away from the current view. - self.cache_threshold = 1 # Allow current month +/- 1 month - - # Dictionary to store built views for each month. - self.month_views = {} - - self.prev_month_button = Gtk.Button( - name="prev-month-button", - child=Label(name="month-button-label", markup=icons.chevron_left), - ) - self.prev_month_button.connect("clicked", self.on_prev_month_clicked) - - self.month_label = Gtk.Label(name="month-label") - - self.next_month_button = Gtk.Button( - name="next-month-button", - child=Label(name="month-button-label", markup=icons.chevron_right), - ) - self.next_month_button.connect("clicked", self.on_next_month_clicked) - - self.header = CenterBox( - spacing=4, - name="header", - start_children=[self.prev_month_button], - center_children=[ - self.month_label, - ], - end_children=[self.next_month_button], - ) - - self.add(self.header) - - self.weekday_row = Gtk.Box(spacing=4, name="weekday-row") - self.pack_start(self.weekday_row, False, False, 0) - - # Create a stack to hold month days views. - self.stack = Gtk.Stack(name="calendar-stack") - self.stack.set_transition_duration(250) - self.pack_start(self.stack, True, True, 0) - - self.update_header() - self.update_calendar() - GLib.timeout_add_seconds(60, self.check_date_change) - - def update_header(self): - # Update header month label and weekday row. - self.month_label.set_text( - datetime(self.current_year, self.current_month, 1) - .strftime("%B %Y") - .capitalize() - ) - # Clear existing children from weekday_row. - for child in self.weekday_row.get_children(): - self.weekday_row.remove(child) - days = self.get_weekday_initials() - for day in days: - label = Gtk.Label(label=day.upper(), name="weekday-label") - self.weekday_row.pack_start(label, True, True, 0) - self.weekday_row.show_all() - - def update_calendar(self): - new_key = (self.current_year, self.current_month) - # Set the transition type based on whether we're moving to a later or earlier month. - if new_key > self.previous_key: - self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT) - elif new_key < self.previous_key: - self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_RIGHT) - # Update previous key for next comparison. - self.previous_key = new_key - - # Create the month view if it doesn't exist. - if new_key not in self.month_views: - month_view = self.create_month_view(self.current_year, self.current_month) - self.month_views[new_key] = month_view - self.stack.add_titled( - month_view, - f"{self.current_year}_{self.current_month}", - f"{self.current_year}_{self.current_month}", - ) - # Switch the visible child in the stack. - self.stack.set_visible_child_name(f"{self.current_year}_{self.current_month}") - self.update_header() - self.stack.show_all() - - # Purge any cached month views that are too far away. - self.prune_cache() - - def prune_cache(self): - # Compute a numerical value for a (year, month) key. - def month_index(key): - year, month = key - return year * 12 + (month - 1) - - current_index = month_index((self.current_year, self.current_month)) - keys_to_remove = [] - for key in self.month_views: - if abs(month_index(key) - current_index) > self.cache_threshold: - keys_to_remove.append(key) - for key in keys_to_remove: - widget = self.month_views.pop(key) - self.stack.remove(widget) - - def create_month_view(self, year, month): - grid = Gtk.Grid( - column_homogeneous=True, row_homogeneous=False, name="calendar-grid" - ) - - cal = calendar.Calendar(firstweekday=6) - month_days = cal.monthdayscalendar(year, month) - - prev_month = month - 1 if month > 1 else 12 - next_month = month + 1 if month < 12 else 1 - prev_year = year if month > 1 else year - 1 - next_year = year if month < 12 else year + 1 - - prev_month_days_count = calendar.monthrange(prev_year, prev_month)[1] - next_month_day = 1 - - for row, week in enumerate(month_days): - for col, day in enumerate(week): - day_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, name="day-box") - top_spacer = Gtk.Box(hexpand=True, vexpand=True) - middle_box = Gtk.Box(hexpand=True, vexpand=True) - bottom_spacer = Gtk.Box(hexpand=True, vexpand=True) - - if day == 0: - if row == 0: - day_num = prev_month_days_count - week[:col].count(0) - label = Gtk.Label(label=str(day_num), name="prev-month-day") - else: - label = Gtk.Label( - label=str(next_month_day), name="next-month-day" - ) - next_month_day += 1 - else: - label = Gtk.Label(label=str(day), name="day-label") - if ( - day == self.current_day - and month == datetime.now().month - and year == datetime.now().year - ): - label.get_style_context().add_class("current-day") - - middle_box.pack_start( - Gtk.Box(hexpand=True, vexpand=True), True, True, 0 - ) - middle_box.pack_start(label, False, False, 0) - middle_box.pack_start( - Gtk.Box(hexpand=True, vexpand=True), True, True, 0 - ) - - day_box.pack_start(top_spacer, True, True, 0) - day_box.pack_start(middle_box, True, True, 0) - day_box.pack_start(bottom_spacer, True, True, 0) - - grid.attach(day_box, col, row, 1, 1) - - grid.show_all() - return grid - - def get_weekday_initials(self): - # Returns localized weekday initials. - return [datetime(2023, 1, i + 1).strftime("%a")[:1] for i in range(7)] - - def on_prev_month_clicked(self, widget): - if self.current_month == 1: - self.current_month = 12 - self.current_year -= 1 - else: - self.current_month -= 1 - self.update_calendar() - - def on_next_month_clicked(self, widget): - if self.current_month == 12: - self.current_month = 1 - self.current_year += 1 - else: - self.current_month += 1 - self.update_calendar() - - def check_date_change(self): - now = datetime.now() - if ( - now.day != self.current_day - or now.month != self.current_month - or now.year != self.current_year - ): - self.current_day = now.day - self.current_month = now.month - self.current_year = now.year - self.update_calendar() - return True - diff --git a/modules/launcher/components/cliphist.py b/modules/launcher/components/cliphist.py deleted file mode 100644 index 5747747..0000000 --- a/modules/launcher/components/cliphist.py +++ /dev/null @@ -1,504 +0,0 @@ -from fabric.widgets.box import Box -from fabric.widgets.label import Label -from fabric.widgets.button import Button -from fabric.widgets.entry import Entry -from fabric.widgets.image import Image -from fabric.widgets.scrolledwindow import ScrolledWindow -from fabric.utils import idle_add, remove_handler -from fabric.utils.helpers import get_relative_path -from gi.repository import GLib, Gdk, GdkPixbuf -import subprocess -import os -import re -import sys -import tempfile - -import utils.icons as icons - - -class ClipHistory(Box): - def __init__(self, **kwargs): - super().__init__( - name="clip-history", - visible=False, - all_visible=False, - **kwargs, - ) - - # Create a temporary directory for image icons - self.tmp_dir = tempfile.mkdtemp(prefix="cliphist-") - self.image_cache = {} # Cache for image previews - - self.launcher = kwargs["launcher"] - self.selected_index = -1 # Track the selected item index - self._arranger_handler = 0 - self.clipboard_items = [] - self._loading = False - self._pending_updates = False - - self.viewport = Box(name="viewport", spacing=4, orientation="v") - self.search_entry = Entry( - name="search-entry", - placeholder="Search Clipboard History...", - h_expand=True, - notify_text=self.filter_items, - on_activate=lambda entry, *_: self.use_selected_item(), - on_key_press_event=self.on_search_entry_key_press, - ) - self.search_entry.props.xalign = 0.5 - - self.scrolled_window = ScrolledWindow( - name="scrolled-window", - spacing=10, - min_content_size=(450, 105), - max_content_size=(450, 105), - child=self.viewport, - ) - - self.header_box = Box( - name="header_box", - spacing=10, - orientation="h", - children=[ - Button( - name="clear-button", - child=Label(name="clear-label", markup=icons.trash), - on_clicked=lambda *_: self.clear_history(), - ), - self.search_entry, - ], - ) - - self.history_box = Box( - name="launcher-box", - spacing=10, - h_expand=True, - orientation="v", - children=[ - self.header_box, - self.scrolled_window, - ], - ) - - self.add(self.history_box) - self.show_all() - - def close(self): - """Close the clipboard history panel""" - self.viewport.children = [] - self.selected_index = -1 # Reset selection - self.launcher.close() - - def open(self): - """Open the clipboard history panel and load items""" - self.viewport.set_visible(True) - if self._loading: - return - self._loading = True - self.search_entry.set_text("") # Clear search - self.search_entry.grab_focus() - # Start loading in background using GLib.idle_add - GLib.idle_add(self._load_clipboard_items_thread) - - def _load_clipboard_items_thread(self): - """Worker for loading clipboard items, now runs in main loop via idle_add""" - try: - result = subprocess.run( - ["cliphist", "list"], capture_output=True, text=True, check=True - ) - lines = result.stdout.strip().split("\n") - new_items = [] - for line in lines: - if not line or " 1 else "0" - content = parts[1] if len(parts) > 1 else item - - # Truncate content for display - display_text = content.strip() - if len(display_text) > 100: - display_text = display_text[:97] + "..." - - # Check if this is an image by examining the content - is_image = self.is_image_data(content) - - if is_image: - # For images, create item with image preview - button = Button( - name="slot-button", - child=Box( - name="slot-box", - orientation="h", - spacing=10, - children=[ - Image(name="clip-icon", h_align="start"), # Placeholder - Label( - name="clip-label", - label="[Image]", - ellipsization="end", - v_align="center", - h_align="start", - h_expand=True, - ), - ], - ), - tooltip_text="Image in clipboard", - on_clicked=lambda *_, id=item_id: self.paste_item(id), - ) - # Load image preview in background - self._load_image_preview_async(item_id, button) - else: - # For text, create regular item - button = self.create_text_item_button(item_id, display_text) - - # Add key press event handler for Enter key - button.connect( - "key-press-event", - lambda widget, event, id=item_id: self.on_item_key_press(widget, event, id), - ) - - # Make sure button can receive focus and key events - button.set_can_focus(True) - button.add_events(Gdk.EventMask.KEY_PRESS_MASK) - - return button - - def _load_image_preview_async(self, item_id, button): - """Load image preview using GLib.idle_add instead of threads""" - - def load_image(): - try: - if item_id in self.image_cache: - pixbuf = self.image_cache[item_id] - else: - result = subprocess.run( - ["cliphist", "decode", item_id], capture_output=True, check=True - ) - loader = GdkPixbuf.PixbufLoader() - loader.write(result.stdout) - loader.close() - pixbuf = loader.get_pixbuf() - width, height = pixbuf.get_width(), pixbuf.get_height() - max_size = 72 - if width > height: - new_width = max_size - new_height = int(height * (max_size / width)) - else: - new_height = max_size - new_width = int(width * (max_size / height)) - pixbuf = pixbuf.scale_simple( - new_width, new_height, GdkPixbuf.InterpType.BILINEAR - ) - self.image_cache[item_id] = pixbuf - self._update_image_button(button, pixbuf) - except Exception as e: - print(f"Error loading image preview: {e}", file=sys.stderr) - return False - - GLib.idle_add(load_image) - - def _update_image_button(self, button, pixbuf): - """Update the button with the loaded image preview""" - box = button.get_child() - if box and len(box.get_children()) > 0: - image_widget = box.get_children()[0] - if isinstance(image_widget, Image): - image_widget.set_from_pixbuf(pixbuf) - - def create_text_item_button(self, item_id, display_text): - """Create a button for a text clipboard item""" - return Button( - name="slot-button", - child=Box( - name="slot-box", - orientation="h", - spacing=10, - children=[ - Label( - name="clip-icon", - markup=icons.clip_text, - h_align="start", - ), - Label( - name="clip-label", - label=display_text, - ellipsization="end", - v_align="center", - h_align="start", - h_expand=True, - ), - ], - ), - tooltip_text=display_text, - on_clicked=lambda *_: self.paste_item(item_id), - ) - - def is_image_data(self, content): - """Determine if clipboard content is likely an image""" - # Check for common image data patterns - return ( - content.startswith("data:image/") - or content.startswith("\x89PNG") - or content.startswith("GIF8") - or content.startswith("\xff\xd8\xff") # JPEG - or re.match(r"^\s* visible_bottom: - # Item below viewport - align to bottom - new_value = y + height - page_size - adj.set_value(new_value) - return False - - GLib.idle_add(scroll) - - def use_selected_item(self): - """Use (paste) the selected clipboard item""" - children = self.viewport.get_children() - if ( - not children - or self.selected_index == -1 - or self.selected_index >= len(self.clipboard_items) - ): - return - - # Get the item ID from the first part before the tab - item_line = self.clipboard_items[self.selected_index] - item_id = item_line.split("\t", 1)[0] - self.paste_item(item_id) - - def delete_selected_item(self): - """Delete the selected clipboard item""" - children = self.viewport.get_children() - if not children or self.selected_index == -1: - return - - # Get the item ID from the first part before the tab - item_line = self.clipboard_items[self.selected_index] - item_id = item_line.split("\t", 1)[0] - self.delete_item(item_id) - - def on_item_key_press(self, widget, event, item_id): - """Handle key press events on clipboard items""" - if event.keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter): - # Copy item to clipboard and close - self.paste_item(item_id) - return True - return False - - def __del__(self): - """Clean up temporary files on destruction""" - try: - if hasattr(self, "tmp_dir") and os.path.exists(self.tmp_dir): - import shutil - - shutil.rmtree(self.tmp_dir) - self.image_cache.clear() - except Exception as e: - print(f"Error cleaning up temporary files: {e}", file=sys.stderr) diff --git a/modules/launcher/components/dashboard.py b/modules/launcher/components/dashboard.py deleted file mode 100644 index 49b8ad8..0000000 --- a/modules/launcher/components/dashboard.py +++ /dev/null @@ -1,605 +0,0 @@ -import subprocess -from fabric.widgets.box import Box -from fabric.widgets.label import Label -from fabric.widgets.button import Button -from fabric.utils.helpers import exec_shell_command_async -import gi -from gi.repository import Gtk, Gdk, GLib -from services import network_client -from services.powerprofile import PowerProfiles -from fabric import Fabricator - -gi.require_version("Gtk", "3.0") -import utils.icons as icons - - -def add_hover_cursor(widget): - # Add enter/leave events to change the cursor - widget.add_events(Gdk.EventMask.ENTER_NOTIFY_MASK | Gdk.EventMask.LEAVE_NOTIFY_MASK) - widget.connect( - "enter-notify-event", - lambda w, e: w.get_window().set_cursor( - Gdk.Cursor.new_from_name(w.get_display(), "pointer") - ) - if w.get_window() - else None, - ) - widget.connect( - "leave-notify-event", - lambda w, e: w.get_window().set_cursor(None) if w.get_window() else None, - ) - - -class NetworkButton(Box): - def __init__(self): - self.network_client = network_client - self._animation_timeout_id = None - self._animation_step = 0 - self._animation_direction = 1 - - self.network_icon = Label( - name="network-icon", - markup=None, - ) - self.network_label = Label( - name="network-label", - label="Wi-Fi", - justification="left", - ) - self.network_label_box = Box(children=[self.network_label, Box(h_expand=True)]) - self.network_ssid = Label( - name="network-ssid", - justification="left", - ) - self.network_ssid_box = Box(children=[self.network_ssid, Box(h_expand=True)]) - self.network_text = Box( - name="network-text", - orientation="v", - h_align="start", - v_align="center", - children=[self.network_label_box, self.network_ssid_box], - ) - self.network_status_box = Box( - h_align="start", - v_align="center", - spacing=10, - children=[self.network_icon, self.network_text], - ) - self.network_status_button = Button( - name="network-status-button", - h_expand=True, - child=self.network_status_box, - on_clicked=lambda *_: self.network_client.wifi_device.toggle_wifi() - if self.network_client.wifi_device - else None, - ) - add_hover_cursor(self.network_status_button) # <-- Added hover - - self.network_menu_label = Label( - name="network-menu-label", - markup=icons.chevron_right, - ) - self.network_menu_button = Button( - name="network-menu-button", - child=self.network_menu_label, - ) - add_hover_cursor(self.network_menu_button) # <-- Added hover - - super().__init__( - name="network-button", - orientation="h", - h_align="fill", - v_align="fill", - h_expand=True, - v_expand=True, - spacing=0, - children=[self.network_status_button, self.network_menu_button], - ) - - self.widgets = [ - self, - self.network_icon, - self.network_label, - self.network_ssid, - self.network_status_button, - self.network_menu_button, - self.network_menu_label, - ] - - # Connect to wifi device signals when ready - self.network_client.connect("device-ready", self._on_wifi_ready) - - # Check initial state using idle_add to defer until GTK loop is running - GLib.idle_add(self._initial_update) - - def _initial_update(self): - self.update_state() - return False # Run only once - - def _on_wifi_ready(self, *args): - if self.network_client.wifi_device: - self.network_client.wifi_device.connect( - "notify::enabled", self.update_state - ) - self.network_client.wifi_device.connect("notify::ssid", self.update_state) - self.update_state() - - def _animate_searching(self): - """Animate wifi icon when searching for networks""" - wifi_icons = [ - icons.wifi_0, - icons.wifi_1, - icons.wifi_2, - icons.wifi_3, - icons.wifi_2, - icons.wifi_1, - ] - - # Si el widget no existe o el WiFi está desactivado, detener la animación - wifi = self.network_client.wifi_device - if not self.network_icon or not wifi or not wifi.enabled: - self._stop_animation() - return False - - # Si estamos conectados, detener la animación - if wifi.state == "activated" and wifi.ssid != "Disconnected": - self._stop_animation() - return False - - GLib.idle_add(self.network_icon.set_markup, wifi_icons[self._animation_step]) - - # Reiniciar al principio cuando llegamos al final - self._animation_step = (self._animation_step + 1) % len(wifi_icons) - - return True # Mantener la animación activa - - def _start_animation(self): - if self._animation_timeout_id is None: - self._animation_step = 0 - self._animation_direction = 1 - # Ejecuta la animación cada 500ms sin usar idle_add - self._animation_timeout_id = GLib.timeout_add(500, self._animate_searching) - - def _stop_animation(self): - if self._animation_timeout_id is not None: - GLib.source_remove(self._animation_timeout_id) - self._animation_timeout_id = None - - def update_state(self, *args): - """Update the button state based on network status""" - wifi = self.network_client.wifi_device - ethernet = self.network_client.ethernet_device - - # Update enabled/disabled state - if wifi and not wifi.enabled: - self._stop_animation() - self.network_icon.set_markup(icons.wifi_off) - for widget in self.widgets: - widget.add_style_class("disabled") - self.network_ssid.set_label("Disabled") - return - - # Remove disabled class if we got here - for widget in self.widgets: - widget.remove_style_class("disabled") - - # Update text and animation based on state - if wifi and wifi.enabled: - if wifi.state == "activated" and wifi.ssid != "Disconnected": - self._stop_animation() - self.network_ssid.set_label(wifi.ssid) - # Update icon based on signal strength - if wifi.strength > 0: - strength = wifi.strength - if strength < 25: - self.network_icon.set_markup(icons.wifi_0) - elif strength < 50: - self.network_icon.set_markup(icons.wifi_1) - elif strength < 75: - self.network_icon.set_markup(icons.wifi_2) - else: - self.network_icon.set_markup(icons.wifi_3) - else: - self.network_ssid.set_label("Enabled") - self._start_animation() - - # Handle primary device check safely - try: - primary_device = self.network_client.primary_device - except AttributeError: - primary_device = "wireless" # Default to wireless if error occurs - - # Handle wired connection case - if primary_device == "wired": - self._stop_animation() - if ethernet and ethernet.internet == "activated": - self.network_icon.set_markup(icons.world) - else: - self.network_icon.set_markup(icons.world_off) - else: - if not wifi: - self._stop_animation() - self.network_icon.set_markup(icons.wifi_off) - elif ( - wifi.state == "activated" - and wifi.ssid != "Disconnected" - and wifi.strength > 0 - ): - self._stop_animation() - strength = wifi.strength - if strength < 25: - self.network_icon.set_markup(icons.wifi_0) - elif strength < 50: - self.network_icon.set_markup(icons.wifi_1) - elif strength < 75: - self.network_icon.set_markup(icons.wifi_2) - else: - self.network_icon.set_markup(icons.wifi_3) - else: - self._start_animation() - - -class BluetoothButton(Box): - def __init__(self, launcher): - super().__init__( - name="bluetooth-button", - orientation="h", - h_align="fill", - v_align="fill", - h_expand=True, - v_expand=True, - ) - self.launcher = launcher - - self.bluetooth_icon = Label( - name="bluetooth-icon", - markup=icons.bluetooth, - ) - self.bluetooth_label = Label( - name="bluetooth-label", - label="Bluetooth", - justification="left", - ) - self.bluetooth_label_box = Box( - children=[self.bluetooth_label, Box(h_expand=True)] - ) - self.bluetooth_status_text = Label( - name="bluetooth-status", - label="Disabled", - justification="left", - ) - self.bluetooth_status_box = Box( - children=[self.bluetooth_status_text, Box(h_expand=True)] - ) - self.bluetooth_text = Box( - orientation="v", - h_align="start", - v_align="center", - children=[self.bluetooth_label_box, self.bluetooth_status_box], - ) - self.bluetooth_status_container = Box( - h_align="start", - v_align="center", - spacing=10, - children=[self.bluetooth_icon, self.bluetooth_text], - ) - self.bluetooth_status_button = Button( - name="bluetooth-status-button", - h_expand=True, - child=self.bluetooth_status_container, - on_clicked=lambda *_: self.launcher.bluetooth.client.toggle_power(), - ) - add_hover_cursor(self.bluetooth_status_button) # <-- Added hover - self.bluetooth_menu_label = Label( - name="bluetooth-menu-label", - markup=icons.chevron_right, - ) - self.bluetooth_menu_button = Button( - name="bluetooth-menu-button", - on_clicked=lambda *_: self.launcher.open("bluetooth"), - child=self.bluetooth_menu_label, - ) - add_hover_cursor(self.bluetooth_menu_button) # <-- Added hover - - self.add(self.bluetooth_status_button) - self.add(self.bluetooth_menu_button) - - -class NightModeButton(Button): - def __init__(self): - self.night_mode_icon = Label( - name="night-mode-icon", - markup=icons.night, - ) - self.night_mode_label = Label( - name="night-mode-label", - label="Night Mode", - justification="left", - ) - self.night_mode_label_box = Box( - children=[self.night_mode_label, Box(h_expand=True)] - ) - self.night_mode_status = Label( - name="night-mode-status", - label="Enabled", - justification="left", - ) - self.night_mode_status_box = Box( - children=[self.night_mode_status, Box(h_expand=True)] - ) - self.night_mode_text = Box( - name="night-mode-text", - orientation="v", - h_align="start", - v_align="center", - children=[self.night_mode_label_box, self.night_mode_status_box], - ) - self.night_mode_box = Box( - h_align="start", - v_align="center", - spacing=10, - children=[self.night_mode_icon, self.night_mode_text], - ) - - super().__init__( - name="night-mode-button", - h_expand=True, - child=self.night_mode_box, - on_clicked=self.toggle_hyprsunset, - ) - add_hover_cursor(self) # <-- Added hover - - self.widgets = [ - self, - self.night_mode_label, - self.night_mode_status, - self.night_mode_icon, - ] - # self.check_hyprsunset() - # **Monitor external changes every 3 seconds** - self.hyprsunset_watcher = Fabricator( - self.is_hyprsunset_running, - interval=3000, - stream=False, - default_value=False, - ) - self.hyprsunset_watcher.changed.connect(self.update_night_mode_status) - self.update_night_mode_status(None, self.is_hyprsunset_running()) - - def toggle_hyprsunset(self, *args): - """ - Toggle the 'hyprsunset' process: - - If running, kill it and mark as 'Disabled'. - - If not running, start it and mark as 'Enabled'. - """ - try: - subprocess.check_output(["pgrep", "hyprsunset"]) - exec_shell_command_async("pkill hyprsunset") - self.night_mode_status.set_label("Disabled") - for widget in self.widgets: - widget.add_style_class("disabled") - except subprocess.CalledProcessError: - exec_shell_command_async("hyprsunset -t 3500") - self.night_mode_status.set_label("Enabled") - for widget in self.widgets: - widget.remove_style_class("disabled") - - def is_hyprsunset_running(self, *args): - """Returns True if hyprsunset is running.""" - try: - subprocess.check_output(["pgrep", "hyprsunset"]) - return True - except subprocess.CalledProcessError: - return False - - def update_night_mode_status(self, _, is_running): - """Update UI based on hyprsunset status.""" - if is_running: - self.night_mode_status.set_label("Enabled") - for widget in self.widgets: - widget.remove_style_class("disabled") - else: - self.night_mode_status.set_label("Disabled") - for widget in self.widgets: - widget.add_style_class("disabled") - - -class CaffeineButton(Button): - def __init__(self): - self.caffeine_icon = Label( - name="caffeine-icon", - markup=icons.coffee, - ) - self.caffeine_label = Label( - name="caffeine-label", - label="Caffeine", - justification="left", - ) - self.caffeine_label_box = Box( - children=[self.caffeine_label, Box(h_expand=True)] - ) - self.caffeine_status = Label( - name="caffeine-status", - label="Enabled", - justification="left", - ) - self.caffeine_status_box = Box( - children=[self.caffeine_status, Box(h_expand=True)] - ) - self.caffeine_text = Box( - name="caffeine-text", - orientation="v", - h_align="start", - v_align="center", - children=[self.caffeine_label_box, self.caffeine_status_box], - ) - self.caffeine_box = Box( - h_align="start", - v_align="center", - spacing=10, - children=[self.caffeine_icon, self.caffeine_text], - ) - super().__init__( - name="caffeine-button", - h_expand=True, - child=self.caffeine_box, - on_clicked=self.toggle_wlinhibit, - ) - add_hover_cursor(self) # <-- Added hover - - self.widgets = [ - self, - self.caffeine_label, - self.caffeine_status, - self.caffeine_icon, - ] - # **Monitor external changes every 3 seconds** - self.wlinhibit_watcher = Fabricator( - self.is_wlinhibit_running, - interval=3000, - stream=False, - default_value=False, - ) - self.wlinhibit_watcher.changed.connect(self.update_wlinhibit_status) - self.update_wlinhibit_status(None, self.is_wlinhibit_running()) - - def toggle_wlinhibit(self, *args): - """Toggle the 'wlinhibit' process.""" - if self.is_wlinhibit_running(): - exec_shell_command_async("pkill wlinhibit") - else: - exec_shell_command_async("wlinhibit") - - def is_wlinhibit_running(self, *args): - """Returns True if wlinhibit is running. Accepts extra arguments to avoid Fabricator errors.""" - try: - subprocess.check_output(["pgrep", "wlinhibit"]) - return True - except subprocess.CalledProcessError: - return False - - def update_wlinhibit_status(self, _, is_running): - """Update UI based on wlinhibit status.""" - if is_running: - self.caffeine_status.set_label("Enabled") - for widget in self.widgets: - widget.remove_style_class("disabled") - else: - self.caffeine_status.set_label("Disabled") - for widget in self.widgets: - widget.add_style_class("disabled") - - -class PowerProfileButton(Button): - def __init__(self): - self.power_profile_service = PowerProfiles.get_default() - - self.power_profile_icon = Label( - name="power-profile-icon", - markup=icons.power_balanced, # Default icon - ) - self.power_profile_label = Label( - name="power-profile-label", - label="Power Profile", - justification="left", - ) - self.power_profile_label_box = Box( - children=[self.power_profile_label, Box(h_expand=True)] - ) - self.power_profile_status = Label( - name="power-profile-status", - label="Balanced", - justification="left", - ) - self.power_profile_status_box = Box( - children=[self.power_profile_status, Box(h_expand=True)] - ) - self.power_profile_text = Box( - name="power-profile-text", - orientation="v", - h_align="start", - v_align="center", - children=[self.power_profile_label_box, self.power_profile_status_box], - ) - self.power_profile_box = Box( - h_align="start", - v_align="center", - spacing=10, - children=[self.power_profile_icon, self.power_profile_text], - ) - - super().__init__( - name="power-profile-button", - h_expand=True, - child=self.power_profile_box, - on_clicked=self.toggle_power_profile, - ) - add_hover_cursor(self) - - self.widgets = [ - self, - self.power_profile_label, - self.power_profile_status, - self.power_profile_icon, - ] - - # Connect to power profile service signals - self.power_profile_service.connect("profile", self.update_power_profile_status) - - # Initial update - self.update_power_profile_status(None, self.power_profile_service.get_current_profile()) - - def toggle_power_profile(self, *args): - current_profile = self.power_profile_service.get_current_profile() - profiles = ["power-saver", "balanced", "performance"] - current_index = profiles.index(current_profile) - next_profile = profiles[(current_index + 1) % len(profiles)] - self.power_profile_service.set_power_profile(next_profile) - - def update_power_profile_status(self, _, profile): - # Map profiles to their display names and icons - profile_map = { - "power-saver": {"name": "Power Saver", "icon": icons.power_saving}, - "balanced": {"name": "Balanced", "icon": icons.power_balanced}, - "performance": {"name": "Performance", "icon": icons.power_performance}, - } - - profile_info = profile_map.get(profile, profile_map["balanced"]) - self.power_profile_status.set_label(profile_info["name"]) - self.power_profile_icon.set_markup(profile_info["icon"]) - - if profile == "balanced": - for widget in self.widgets: - widget.add_style_class("disabled") - else: - for widget in self.widgets: - widget.remove_style_class("disabled") - - - -class Dashboard(Gtk.Grid): - def __init__(self, **kwargs): - super().__init__(name="buttons-grid") - self.set_row_homogeneous(True) - self.set_column_homogeneous(True) - self.set_row_spacing(8) - self.set_column_spacing(8) - self.set_vexpand(True) # Prevent vertical expansion - - self.launcher = kwargs["launcher"] - - # Instantiate each button - self.network_button = NetworkButton() - self.bluetooth_button = BluetoothButton(self.launcher) - self.night_mode_button = NightModeButton() - self.caffeine_button = CaffeineButton() - self.power_profile_button = PowerProfileButton() - - # Attach buttons into the grid (two rows, three columns) - self.attach(self.network_button, 0, 0, 1, 1) - self.attach(self.bluetooth_button, 1, 0, 1, 1) - self.attach(self.power_profile_button, 2, 0, 1, 1) - self.attach(self.night_mode_button, 0, 1, 1, 1) - self.attach(self.caffeine_button, 1, 1, 1, 1) - - self.show_all() diff --git a/modules/launcher/components/emoji.py b/modules/launcher/components/emoji.py deleted file mode 100644 index b69355e..0000000 --- a/modules/launcher/components/emoji.py +++ /dev/null @@ -1,330 +0,0 @@ -from fabric.widgets.box import Box -from fabric.widgets.label import Label -from fabric.widgets.button import Button -from fabric.widgets.entry import Entry -from fabric.widgets.stack import Stack -from fabric.utils import remove_handler, get_relative_path -from gi.repository import Gdk -import utils.icons as icons -import os -import subprocess -import ijson - - -class EmojiPicker(Box): - def __init__(self, **kwargs): - super().__init__( - name="emoji", - visible=False, - all_visible=False, - **kwargs, - ) - - self.launcher = kwargs["launcher"] - self.selected_index = -1 - self.emojis_per_page = 9 * 3 - self.current_page_index = 0 - self.filtered_emojis = [] - self.total_pages = 0 - - self._arranger_handler: int = 0 - self._all_emojis = self._load_emoji_data() - - self.stack = Stack( - name="viewport", - spacing=4, - orientation="v", - transition_type="slide-up-down", - transition_duration=200, - ) - self.search_entry = Entry( - name="search-entry", - placeholder="Search Emojis...", - h_expand=True, - notify_text=lambda entry, *_: self.arrange_viewport(entry.get_text()), - on_activate=lambda entry, *_: self.on_search_entry_activate( - entry.get_text() - ), - on_key_press_event=self.on_search_entry_key_press, - ) - self.search_entry.props.xalign = 0.5 - self.header_box = Box( - name="header_box", - spacing=10, - orientation="h", - children=[ - self.search_entry, - # Button( - # name="close-button", - # child=Label(name="close-label", markup=icons.cancel), - # tooltip_text="Exit", - # on_clicked=lambda *_: self.close_picker(), - # ), - ], - ) - - self.picker_box = Box( - name="picker-box", - spacing=10, - h_expand=True, - orientation="v", - children=[ - self.header_box, - self.stack, - ], - ) - - self.resize_viewport() - - self.add(self.picker_box) - self.show_all() - - def _load_emoji_data(self): - emoji_data = {} - emoji_file_path = get_relative_path("../../../config/assets/emoji.json") - if not os.path.exists(emoji_file_path): - print(f"Emoji JSON file not found at: {emoji_file_path}") - return {} - - with open(emoji_file_path, "r") as f: - for emoji_char, emoji_info in ijson.kvitems(f, ""): - emoji_data[emoji_char] = emoji_info - return emoji_data - - def close_picker(self): - self.stack.children = [] - self.selected_index = -1 - self.launcher.close() - - def open_picker(self): - self.search_entry.set_text("") - self.current_page_index = 0 - self.arrange_viewport() - self.search_entry.grab_focus() - - def arrange_viewport(self, query: str = ""): - remove_handler(self._arranger_handler) if self._arranger_handler else None - self.stack.children = [] - self.selected_index = -1 - self.current_page_index = 0 - - self.filtered_emojis = [ - (emoji_char, emoji_info) - for emoji_char, emoji_info in self._all_emojis.items() - if query.casefold() - in ( - emoji_info.get("name", "") + " " + emoji_info.get("group", "") - ).casefold() - ] - self.total_pages = ( - (len(self.filtered_emojis) + self.emojis_per_page - 1) - // self.emojis_per_page - if self.filtered_emojis - else 0 - ) - - self.load_page(self.current_page_index) - - should_resize = not query - - if should_resize: - self.resize_viewport() - if query.strip() != "" and self.get_all_emoji_buttons(): - self.update_selection(0) - - def load_page(self, page_index): - self.update_selection(-1) - page_box = Box(name=f"page-box-{page_index}", orientation="v", spacing=4) - start_index = page_index * self.emojis_per_page - end_index = min( - (page_index + 1) * self.emojis_per_page, len(self.filtered_emojis) - ) - page_emojis = self.filtered_emojis[start_index:end_index] - - grid_box = Box(name="emoji-grid-box", orientation="v", spacing=2) - - row_box = None - for i, (emoji_char, emoji_info) in enumerate(page_emojis): - if i % 9 == 0: - row_box = Box(name="emoji-row-box", orientation="h", spacing=2) - grid_box.add(row_box) - if row_box is not None: - row_box.add(self.bake_emoji_slot(emoji_char, emoji_info)) - page_box.add(grid_box) - self.stack.add_named(page_box, f"page-{page_index}") - self.stack.set_visible_child_name(f"page-{page_index}") - page_box.show_all() - - buttons = self.get_all_emoji_buttons() - if buttons and self.selected_index != -1: - page_relative_index = self.selected_index % self.emojis_per_page - if page_relative_index < len(buttons): - self.update_selection(page_relative_index) - else: - self.update_selection(len(buttons) - 1) - - def resize_viewport(self): - return False - - def bake_emoji_slot(self, emoji_char: str, emoji_info: dict, **kwargs) -> Button: - button = Button( - name="emoji-slot-button", - child=Box( - name="emoji-slot-box", - orientation="horizontal", - halign="center", - valign="center", - children=[ - Label( - name="emoji-char-label", - label=emoji_char, - use_markup=True, - v_align="center", - h_align="center", - css_name="emoji-char-label", - ), - ], - ), - tooltip_text=emoji_info.get("name", "Unknown"), - on_clicked=lambda *_: ( - self.copy_emoji_to_clipboard(emoji_char), - self.close_picker(), - ), - **kwargs, - ) - return button - - def update_selection(self, new_index: int): - buttons = self.get_all_emoji_buttons() - if not buttons: - self.selected_index = -1 - return - - if self.selected_index != -1 and self.selected_index < len(buttons): - current_button = buttons[self.selected_index] - current_button.get_style_context().remove_class("selected") - if not buttons or current_button not in buttons: - self.selected_index = -1 - - if 0 <= new_index < len(buttons): - new_button = buttons[new_index] - new_button.get_style_context().add_class("selected") - self.selected_index = new_index - else: - self.selected_index = -1 - - def get_all_emoji_buttons(self): - buttons = [] - current_page = self.stack.get_visible_child() - if current_page and current_page.get_children(): - if current_page.get_children()[0].get_children(): - for row_box in current_page.get_children()[0].get_children(): - buttons.extend(row_box.get_children()) - return buttons - - def on_search_entry_activate(self, text): - buttons = self.get_all_emoji_buttons() - if buttons: - if self.selected_index != -1: - buttons[self.selected_index].clicked() - elif buttons and text.strip() != "": - buttons[0].clicked() - - def on_search_entry_key_press(self, widget, event): - if event.keyval in (Gdk.KEY_Up, Gdk.KEY_Down, Gdk.KEY_Left, Gdk.KEY_Right): - self.move_selection_2d(event.keyval) - return True - elif event.keyval == Gdk.KEY_Escape: - self.close_picker() - return True - return False - - def move_selection_2d(self, keyval): - buttons = self.get_all_emoji_buttons() - total_items_current_page = len(buttons) - if total_items_current_page == 0: - return - - rows = 3 - columns = 9 - - if self.selected_index == -1: - if keyval in (Gdk.KEY_Down, Gdk.KEY_Right): - new_index = 0 - elif keyval in (Gdk.KEY_Up, Gdk.KEY_Left): - new_index = total_items_current_page - 1 - else: - return - else: - current_index_page = self.selected_index - row = current_index_page // columns - col = current_index_page % columns - - if keyval == Gdk.KEY_Right: - new_col = (col + 1) % columns - new_row = row - if new_col == 0: - new_row = row + 1 - elif keyval == Gdk.KEY_Left: - new_col = (col - 1) % columns - new_row = row - if new_col == (columns - 1): - new_row = row - 1 - elif keyval == Gdk.KEY_Down: - new_row = row + 1 - new_col = col - elif keyval == Gdk.KEY_Up: - new_row = row - 1 - new_col = col - else: - return - - if new_row >= rows: - if self.current_page_index < self.total_pages - 1: - current_col = col # Keep track of current column - self.current_page_index += 1 - self.load_page(self.current_page_index) - new_index = current_col # Try to keep the same column - if ( - new_index >= total_items_current_page - ): # if column is out of bound, select last - new_index = total_items_current_page - 1 - self.selected_index = -1 - self.update_selection(new_index) - return - else: - new_index = total_items_current_page - 1 - elif new_row < 0: - if self.current_page_index > 0: - current_col = col # Keep track of current column - self.current_page_index -= 1 - self.load_page(self.current_page_index) - new_index = ( - rows - 1 - ) * columns + current_col # Select last row, same column - if ( - new_index >= total_items_current_page - ): # if column is out of bound, select last - new_index = total_items_current_page - 1 - self.selected_index = -1 - self.update_selection(new_index) - return - else: - new_index = 0 - else: - new_index = new_row * columns + new_col - if new_index >= total_items_current_page: - new_index = total_items_current_page - 1 - - if new_index < 0: - new_index = 0 - elif new_index >= total_items_current_page: - new_index = total_items_current_page - 1 - - self.update_selection(new_index) - - def copy_emoji_to_clipboard(self, emoji_char: str): - try: - subprocess.run(["wl-copy"], input=emoji_char.encode("utf-8"), check=True) - except subprocess.CalledProcessError as e: - print(f"Clipboard copy failed: {e}") diff --git a/modules/launcher/components/kanban.py b/modules/launcher/components/kanban.py deleted file mode 100644 index 8c2bf8a..0000000 --- a/modules/launcher/components/kanban.py +++ /dev/null @@ -1,365 +0,0 @@ -import gi -import json -import os -from pathlib import Path -import cairo # For rendering the drag preview - -from fabric.widgets.label import Label -from fabric.widgets.box import Box -from fabric.widgets.centerbox import CenterBox -import utils.icons as icons - -gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, Gdk, GObject, GLib - - -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) - # Use a transparent background. - cr.set_source_rgba(0, 0, 0, 0) - cr.rectangle(0, 0, alloc.width, alloc.height) - cr.fill() - widget.draw(cr) - return surface - - -class InlineEditor(Gtk.Box): - __gsignals__ = { - "confirmed": (GObject.SignalFlags.RUN_LAST, None, (str,)), - "canceled": (GObject.SignalFlags.RUN_LAST, None, ()), - } - - def __init__(self, initial_text=""): - super().__init__(name="inline-editor", spacing=4) - # Replace Gtk.Entry with a Gtk.TextView for multiline editing. - self.text_view = Gtk.TextView() - self.text_view.set_wrap_mode(Gtk.WrapMode.WORD) - buffer = self.text_view.get_buffer() - buffer.set_text(initial_text) - # Connect key press events to handle Return and SHIFT+Return. - self.text_view.connect("key-press-event", self.on_key_press) - - confirm_btn = Gtk.Button( - name="kanban-btn", child=Label(name="kanban-btn-label", markup=icons.accept) - ) - confirm_btn.connect("clicked", self.on_confirm) - confirm_btn.get_style_context().add_class("flat") - - cancel_btn = Gtk.Button( - name="kanban-btn", child=Label(name="kanban-btn-neg", markup=icons.cancel) - ) - cancel_btn.connect("clicked", self.on_cancel) - cancel_btn.get_style_context().add_class("flat") - - # Pack the TextView inside a ScrolledWindow for better appearance. - sw = Gtk.ScrolledWindow() - sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) - sw.set_min_content_height(50) - sw.add(self.text_view) - - self.button_box = Box(children=[confirm_btn, cancel_btn], spacing=4) - self.center_box = CenterBox(center_children=[self.button_box], orientation="v") - - self.pack_start(sw, True, True, 0) - self.pack_start(self.center_box, False, False, 0) - self.show_all() - - def on_confirm(self, widget): - buffer = self.text_view.get_buffer() - start, end = buffer.get_bounds() - text = buffer.get_text(start, end, True).strip() - if text: - self.emit("confirmed", text) - else: - self.emit("canceled") - - def on_cancel(self, widget): - self.emit("canceled") - - def on_key_press(self, widget, event): - # Check for Escape to cancel. - if event.keyval == Gdk.KEY_Escape: - self.emit("canceled") - return True - - # If Return is pressed... - if event.keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter): - state = event.get_state() - if state & Gdk.ModifierType.SHIFT_MASK: - # SHIFT+Return: insert a newline. - buffer = self.text_view.get_buffer() - cursor_iter = buffer.get_iter_at_mark(buffer.get_insert()) - buffer.insert(cursor_iter, "\n") - return True # Prevent further handling. - else: - # Plain Return: confirm the edit. - self.on_confirm(widget) - return True - return False - - -class KanbanNote(Gtk.EventBox): - __gsignals__ = { - "changed": (GObject.SignalFlags.RUN_LAST, None, ()), - } - - def __init__(self, text): - super().__init__() - self.text = text - # Variables to store the click offset for drag preview. - self.setup_ui() - self.setup_dnd() - self.connect("button-press-event", self.on_button_press) - - def setup_ui(self): - self.box = Gtk.Box(name="kanban-note", spacing=4) - self.label = Gtk.Label(label=self.text) - self.label.set_line_wrap(True) - # Wrap long lines. - self.label.set_line_wrap_mode(Gtk.WrapMode.WORD) - - self.delete_btn = Gtk.Button( - name="kanban-btn", child=Label(name="kanban-btn-neg", markup=icons.trash) - ) - self.delete_btn.connect("clicked", self.on_delete_clicked) - - self.center_btn = CenterBox(orientation="v", start_children=[self.delete_btn]) - - self.box.pack_start(self.label, True, True, 0) - self.box.pack_start(self.center_btn, False, False, 0) - self.add(self.box) - self.show_all() - - def setup_dnd(self): - self.drag_source_set( - Gdk.ModifierType.BUTTON1_MASK, - [Gtk.TargetEntry.new("UTF8_STRING", Gtk.TargetFlags.SAME_APP, 0)], - Gdk.DragAction.MOVE, - ) - self.connect("drag-data-get", self.on_drag_data_get) - self.connect("drag-data-delete", self.on_drag_data_delete) - # Set up drag-begin to display a preview of the card. - self.connect("drag-begin", self.on_drag_begin) - - def on_button_press(self, widget, event): - if event.type != Gdk.EventType._2BUTTON_PRESS: - return True - self.start_edit() - return False - - def on_drag_begin(self, widget, context): - surface = createSurfaceFromWidget(self) - Gtk.drag_set_icon_surface(context, surface) - - def on_drag_data_get(self, widget, drag_context, data, info, time): - data.set_text(self.label.get_text(), -1) - - def on_drag_data_delete(self, widget, drag_context): - self.get_parent().destroy() - - def on_delete_clicked(self, button): - self.get_parent().destroy() - - def start_edit(self): - row = self.get_parent() - editor = InlineEditor(self.label.get_text()) - - def on_confirmed(editor, text): - self.label.set_text(text) - row.remove(editor) - row.add(self) - row.show_all() - self.emit("changed") - - def on_canceled(editor): - row.remove(editor) - row.add(self) - row.show_all() - - editor.connect("confirmed", on_confirmed) - editor.connect("canceled", on_canceled) - - row.remove(self) - row.add(editor) - row.show_all() - # Usamos un retardo de 50 milisegundos: - GLib.timeout_add(50, lambda: (editor.text_view.grab_focus(), False)) - - -class KanbanColumn(Gtk.Frame): - __gsignals__ = { - "changed": (GObject.SignalFlags.RUN_LAST, None, ()), - } - - def __init__(self, title): - super().__init__(name="kanban-column") - self.title = title - self.setup_ui() - self.setup_dnd() - self.set_hexpand(True) - self.set_vexpand(True) - - def setup_ui(self): - self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) - self.listbox = Gtk.ListBox() - self.listbox.set_selection_mode(Gtk.SelectionMode.NONE) - - self.add_btn = Gtk.Button( - name="kanban-btn-add", - child=Label(name="kanban-btn-label", markup=icons.add), - ) - header = CenterBox( - name="kanban-header", - center_children=[Label(name="column-header", label=self.title)], - end_children=[self.add_btn], - ) - self.box.pack_start(header, False, False, 0) - - self.add_btn.connect("clicked", self.on_add_clicked) - - scrolled = Gtk.ScrolledWindow(name="kanban-scroll") - scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) - scrolled.add(self.listbox) - scrolled.set_vexpand(True) - - self.box.pack_start(scrolled, True, True, 0) - self.box.pack_start(self.add_btn, False, False, 0) - self.add(self.box) - self.show_all() - - def setup_dnd(self): - self.listbox.drag_dest_set( - Gtk.DestDefaults.ALL, - [Gtk.TargetEntry.new("UTF8_STRING", Gtk.TargetFlags.SAME_APP, 0)], - Gdk.DragAction.MOVE, - ) - - self.listbox.connect("drag-data-received", self.on_drag_data_received) - self.listbox.connect("drag-motion", self.on_drag_motion) - self.listbox.connect("drag-leave", self.on_drag_leave) - - def on_add_clicked(self, button): - editor = InlineEditor() - row = Gtk.ListBoxRow(name="kanban-row") - row.add(editor) - self.listbox.add(row) - self.listbox.show_all() - editor.text_view.grab_focus() - - def on_confirmed(editor, text): - note = KanbanNote(text) - note.connect("changed", lambda x: self.emit("changed")) - row.remove(editor) - row.add(note) - self.listbox.show_all() - self.emit("changed") # Emit on add - - def on_canceled(editor): - row.destroy() - - editor.connect("confirmed", on_confirmed) - editor.connect("canceled", on_canceled) - - def add_note(self, text, suppress_signal=False): - note = KanbanNote(text) - note.connect("changed", lambda x: self.emit("changed")) - row = Gtk.ListBoxRow(name="kanban-row") - row.add(note) - row.connect("destroy", lambda x: self.emit("changed")) - self.listbox.add(row) - self.listbox.show_all() - if not suppress_signal: - self.emit("changed") - - def get_notes(self): - return [ - row.get_children()[0].label.get_text() - for row in self.listbox.get_children() - if isinstance(row.get_children()[0], KanbanNote) - ] - - def clear_notes(self, suppress_signal=False): - for row in self.listbox.get_children(): - row.destroy() - if not suppress_signal: - self.emit("changed") - - def on_drag_data_received(self, widget, drag_context, x, y, data, info, time): - text = data.get_text() - if text: - row = self.listbox.get_row_at_y(y) - new_note = KanbanNote(text) - new_note.connect("changed", lambda x: self.emit("changed")) - new_row = Gtk.ListBoxRow(name="kanban-row") - new_row.add(new_note) - new_row.connect("destroy", lambda x: self.emit("changed")) - - if row: - self.listbox.insert(new_row, row.get_index()) - else: - self.listbox.add(new_row) - - self.listbox.show_all() - drag_context.finish(True, False, time) - self.emit("changed") # Emit on move - - def on_drag_motion(self, widget, drag_context, x, y, time): - Gdk.drag_status(drag_context, Gdk.DragAction.MOVE, time) - return True - - def on_drag_leave(self, widget, drag_context, time): - widget.get_parent().get_parent().drag_unhighlight() - - -class Kanban(Gtk.Box): - STATE_FILE = Path(os.path.expanduser("~/.local/state/modus/kanban.json")) - - def __init__(self): - super().__init__(name="kanban") - - self.grid = Gtk.Grid(column_spacing=4, column_homogeneous=True) - self.grid.set_vexpand(True) - self.add(self.grid) - - self.columns = [ - KanbanColumn("To Do"), - KanbanColumn("In Progress"), - KanbanColumn("Done"), - ] - - for i, column in enumerate(self.columns): - self.grid.attach(column, i, 0, 1, 1) - column.connect("changed", lambda x: self.save_state()) - - self.load_state() - self.show_all() - - def save_state(self): - state = { - "columns": [ - {"title": col.title, "notes": col.get_notes()} for col in self.columns - ] - } - try: - with open(self.STATE_FILE, "w") as f: - json.dump(state, f, indent=2) - except Exception as e: - print(f"Error saving state: {e}") - - def load_state(self): - try: - with open(self.STATE_FILE, "r") as f: - state = json.load(f) - for col_data in state["columns"]: - for column in self.columns: - if column.title == col_data["title"]: - column.clear_notes(suppress_signal=True) - for note_text in col_data["notes"]: - column.add_note(note_text, suppress_signal=True) - break - except FileNotFoundError: - pass - except Exception as e: - print(f"Error loading state: {e}") diff --git a/modules/launcher/components/notification_center.py b/modules/launcher/components/notification_center.py deleted file mode 100644 index 86ade48..0000000 --- a/modules/launcher/components/notification_center.py +++ /dev/null @@ -1,409 +0,0 @@ -from fabric.notifications import Notification -from services import notification_service -from fabric.widgets.box import Box -from fabric.widgets.button import Button -from fabric.widgets.label import Label -from fabric.widgets.image import Image -from fabric.widgets.scrolledwindow import ScrolledWindow -from fabric.widgets.revealer import Revealer -from loguru import logger -import utils.icons as icons -from gi.repository import GdkPixbuf, GLib -from utils import CustomImage -import os -from gi.repository import Gtk -from fabric.widgets.centerbox import CenterBox - - -class NotificationCenter(Box): - def __init__(self, **kwargs): - super().__init__( - name="notification-center", - visible=False, - all_visible=False, - **kwargs, - ) - - self.launcher = kwargs["launcher"] - self.notification_service = notification_service - - # Main container for notifications - self.notifications = Box( - v_expand=True, - h_expand=True, - orientation="v", - spacing=8, - ) - self.header_switch = Gtk.Switch(name="matugen-switcher") - self.header_switch.set_vexpand(False) - self.header_switch.set_valign(Gtk.Align.CENTER) - self.header_switch.set_active(self.notification_service.dont_disturb) - self.do_not_disturb_enabled = self.notification_service.dont_disturb - self.header_switch.connect("notify::active", self.on_do_not_disturb_changed) - self.dnd_label = Label(name="dnd-label", markup=icons.notifications_off) - # Header with clear all button - header = CenterBox( - h_expand=True, - start_children=[self.header_switch, self.dnd_label], - center_children=[ - Label( - label="Notifications", - name="nhh", - ) - ], - end_children=[ - Button( - name="nhh-button", - child=Label(name="nhh-button-label", markup=icons.trash), - on_clicked=lambda *args: self.clear_all_notifications(), - ) - ], - ) - - self.scrolledwindow = ScrolledWindow( - name="scrolled-window", - child=self.notifications, - min_content_size=(400, 300), - max_content_size=(400, 300), - v_expand=True, - h_expand=True, - ) - - # Main container - main_container = Box( - v_expand=True, - h_expand=True, - orientation="v", - children=[header, self.scrolledwindow], - ) - - self.add(main_container) - - # Connect to notification service signals - self.notification_service.connect( - "notification-added", self.on_new_notification - ) - self.notification_service.connect("clear_all", self.on_clear_all) - - # Load existing notifications - self.load_notifications() - - def load_notifications(self): - """Load existing notifications from the service.""" - notifications = self.notification_service.get_deserialized() - if not notifications: - # Create a container box to better center the message - container = Box( - name="no-notifications-container", - orientation="v", - h_align="center", - v_align="center", - h_expand=True, - v_expand=True, - ) - - # Show a message if no notifications - label = Label( - name="no-notifications-label", - markup=icons.notifications_clear, - v_align="fill", - h_align="fill", - v_expand=True, - h_expand=True, - justification="center", - ) - - container.add(label) - self.notifications.add(container) - return - - for notification in notifications: - self.add_notification(notification) - - def toggle_notification_body(self, revealer, chevron_label): - """Toggle the visibility of notification body.""" - is_revealed = revealer.get_reveal_child() - revealer.set_reveal_child(not is_revealed) - # Update chevron icon - chevron_label.set_markup( - icons.chevron_up if not is_revealed else icons.chevron_down - ) - - def add_notification(self, notification: Notification): - """Add a notification to the center.""" - # Remove empty state container if it exists - for child in self.notifications.get_children(): - if child.get_name() == "no-notifications-container": - self.notifications.remove(child) - break - - # Get timestamp from notification object - timestamp = getattr(notification, 'timestamp', GLib.get_real_time() / 1000000) - time_str = GLib.DateTime.new_from_unix_local(timestamp).format("%I:%M %p") - - # Create the notification header (summary) box - header_box = Box( - name="notification-header", - # h_expand=True, - spacing=8, - children=[ - Box( - orientation="h", - h_expand=True, - spacing=8, - style="min-width: 0;", - children=[ - # Summary - Label( - name="notification-app-name", - markup=notification.app_name + " | ", - h_align="start", - ellipsization="end", - ), - Label( - markup=notification.summary.replace("\n", " "), - h_align="start", - ellipsization="end", - ), - Label( - name="notification-time", - markup=f" | {time_str}", - h_align="end", - style="color: var(--outline);", - ), - ], - ), - # Add chevron button if there's content to show - *( - [ - Button( - name="chevron-button", - child=Label( - name="chevron-label", markup=icons.chevron_down - ), - ) - ] - if notification.body or notification.image_pixbuf - else [] - ), - Button( - name="notif-close-button", - child=Label(name="notif-close-label", markup=icons.cancel), - on_clicked=lambda *args: self.remove_notification(notification), - ), - ], - ) - - # Create content box that will be revealed - # Create content box in vertical layout for body + actions - content_box = Box( - name="notification-content", - spacing=8, - orientation="v", - ) - - # Create horizontal box for image and body - body_box = Box( - name="notification-body-box", - spacing=8, - orientation="h", - ) - - # Add image if available - if notification.image_pixbuf: - image_box = Box( - name="notification-image", - children=[ - CustomImage( - pixbuf=notification.image_pixbuf.scale_simple( - 48, 48, GdkPixbuf.InterpType.BILINEAR - ) - if notification.image_pixbuf - else self.get_pixbuf(notification.app_icon, 48, 48) - ), - ], - ) - body_box.add(image_box) - - # Add body text if available - if notification.body: - body_box.add( - Label( - markup=notification.body.replace("\n", " "), - name="notification-body", - h_align="start", - ellipsization="end", - ) - ) - - # Add body box to content - content_box.add(body_box) - - # Add action buttons if available - if notification.actions: - action_box = Box( - name="notification-action-buttons", - spacing=4, - h_expand=True, - ) - for i, action in enumerate(notification.actions): - style_class = ( - "start-action" - if i == 0 - else "end-action" - if i == len(notification.actions) - 1 - else "middle-action" - ) - button = Button( - name="action-button", - h_expand=True, - child=Label(name="button-label", label=action.label), - on_clicked=lambda btn, act=action, notif=notification: [ - act.invoke(), - self.notification_service.remove_notification(notif.id), - self.remove_notification(notif) - ], - ) - button.add_style_class(style_class) - action_box.add(button) - content_box.add(action_box) - - # Create revealer for content - body_revealer = None - if notification.body or notification.image_pixbuf: - body_revealer = Revealer( - name="body-revealer", - transition_type="slide-down", - reveal_child=False, - child=content_box, - ) - - # Create main notification box - notification_box = Box( - name="notification-box", - orientation="v", - h_expand=True, - style="margin: 4px; padding: 16px; border-radius: 20px; background-color: var(--surface-container-low); min-width: 200px;", - children=[header_box], - ) - - # Add body revealer if exists - if body_revealer: - notification_box.add(body_revealer) - # Get the chevron button from header_box children - chevron_button = [ - c for c in header_box.get_children() if c.get_name() == "chevron-button" - ][0] - chevron_label = chevron_button.get_child() - chevron_button.connect( - "clicked", - lambda *args: self.toggle_notification_body( - body_revealer, chevron_label - ), - ) - - self.notifications.add(notification_box) - - def on_do_not_disturb_changed(self, switch, pspec): - """Handle Do Not Disturb toggle.""" - self.do_not_disturb_enabled = switch.get_active() - self.notification_service.dont_disturb = self.do_not_disturb_enabled - - def on_new_notification(self, fabric_notif, id): - """Handle new notification event.""" - notification = fabric_notif.get_notification_from_id(id) - self.add_notification(notification) - - def remove_notification(self, notification: Notification): - """Remove a notification from the center.""" - self.notification_service.remove_notification(notification.id) - # Find and remove the notification box - for child in self.notifications.get_children(): - if child.get_name() == "notification-box": - # Get the header box - header_box = child.get_children()[0] - # Get the text container box - text_box = header_box.get_children()[0] - # Get the summary label (second child of text box) - summary_label = text_box.get_children()[1] - if summary_label.get_text() == notification.summary: - self.notifications.remove(child) - break - - def clear_all_notifications(self, *args): - """Clear all notifications.""" - self.notification_service.clear_all_notifications() - # Remove all child widgets - for child in self.notifications.get_children(): - self.notifications.remove(child) - - # Add empty state - container = Box( - name="no-notifications-container", - orientation="v", - h_align="center", - v_align="center", - h_expand=True, - v_expand=True, - ) - - label = Label( - name="no-notifications-label", - markup=icons.notifications_clear, - v_align="fill", - h_align="fill", - v_expand=True, - h_expand=True, - justification="center", - ) - - container.add(label) - self.notifications.add(container) - - def on_clear_all(self, *args): - """Handle clear all event from service.""" - # Remove all child widgets - for child in self.notifications.get_children(): - self.notifications.remove(child) - - # Add empty state - container = Box( - name="no-notifications-container", - orientation="v", - h_align="center", - v_align="center", - h_expand=True, - v_expand=True, - ) - - label = Label( - name="no-notifications-label", - markup=icons.notifications_clear, - v_align="fill", - h_align="fill", - v_expand=True, - h_expand=True, - justification="center", - ) - - container.add(label) - self.notifications.add(container) - - def get_pixbuf(self, icon_path, width, height): - if icon_path.startswith("file://"): - icon_path = icon_path[7:] - - if not os.path.exists(icon_path): - logger.warning(f"Icon path does not exist: {icon_path}") - return None - - try: - pixbuf = GdkPixbuf.Pixbuf.new_from_file(icon_path) - return pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.BILINEAR) - except Exception as e: - logger.error(f"Failed to load or scale icon: {e}") - return None - - def open_center(self): - """Open the notification center.""" - self.show_all() diff --git a/modules/launcher/components/power.py b/modules/launcher/components/power.py deleted file mode 100644 index b441e25..0000000 --- a/modules/launcher/components/power.py +++ /dev/null @@ -1,88 +0,0 @@ -from fabric.widgets.box import Box -from fabric.widgets.label import Label -from fabric.widgets.button import Button -from fabric.utils.helpers import exec_shell_command_async -import utils.icons as icons - - -class PowerMenu(Box): - def __init__(self, **kwargs): - super().__init__( - name="power-menu", - orientation="h", - spacing=10, - v_align="center", - h_align="center", - v_expand=True, - h_expand=True, - visible=True, - **kwargs, - ) - - self.launcher = kwargs["launcher"] - - self.btn_lock = Button( - name="power-menu-button", - child=Label(name="button-label", markup=icons.lock), - on_clicked=self.lock, - ) - - self.btn_suspend = Button( - name="power-menu-button", - child=Label(name="button-label", markup=icons.suspend), - on_clicked=self.suspend, - ) - - self.btn_logout = Button( - name="power-menu-button", - child=Label(name="button-label", markup=icons.logout), - on_clicked=self.logout, - ) - - self.btn_reboot = Button( - name="power-menu-button", - child=Label(name="button-label", markup=icons.reboot), - on_clicked=self.reboot, - ) - - self.btn_shutdown = Button( - name="power-menu-button", - child=Label(name="button-label", markup=icons.shutdown), - on_clicked=self.poweroff, - ) - - self.buttons = [ - self.btn_lock, - self.btn_suspend, - self.btn_logout, - self.btn_reboot, - self.btn_shutdown, - ] - - for button in self.buttons: - self.add(button) - - self.show_all() - - def close_menu(self): - self.launcher.close() - - def lock(self, *args): - exec_shell_command_async("hyprlock --immediate") - self.close_menu() - - def suspend(self, *args): - exec_shell_command_async("systemctl suspend") - self.close_menu() - - def logout(self, *args): - exec_shell_command_async("hyprctl dispatch exit") - self.close_menu() - - def reboot(self, *args): - exec_shell_command_async("systemctl reboot") - self.close_menu() - - def poweroff(self, *args): - exec_shell_command_async("systemctl poweroff") - self.close_menu() diff --git a/modules/launcher/components/sh.py b/modules/launcher/components/sh.py deleted file mode 100644 index 6fef79a..0000000 --- a/modules/launcher/components/sh.py +++ /dev/null @@ -1,139 +0,0 @@ -import os -import subprocess -from typing import List -from fabric.widgets.box import Box -from fabric.widgets.button import Button -from fabric.widgets.entry import Entry -from fabric.widgets.label import Label -from fabric.widgets.scrolledwindow import ScrolledWindow -import config.data as data - -BINS = f"{data.CACHE_DIR}/binaries" - - -class Sh(Box): - def __init__(self, **kwargs): - super().__init__( - all_visible=False, - visible=False, - **kwargs, - ) - - self.launcher = kwargs["launcher"] - self._arranger_handler: int = 0 - self.shell_command_manager = ShellCommandManager(self) - self.viewport = None - - self.search_entry = Entry( - name="search-entry", - placeholder="Search Shell Commands...", - h_expand=True, - notify_text=lambda entry, *_: self.handle_search_input(entry.get_text()), - on_activate=lambda entry, *_: self.handle_search_input(entry.get_text()), - ) - self.search_entry.props.xalign = 0.5 - - self.header_box = Box( - spacing=10, - orientation="h", - children=[self.search_entry], - ) - - self.launcher_box = Box( - spacing=10, - orientation="v", - h_expand=True, - children=[self.header_box], - ) - - self.add(self.launcher_box) - - def open_sh(self): - if not self.viewport: - self.viewport = Box(name="viewport", spacing=10, orientation="v") - self.scrolled_window = ScrolledWindow( - name="scrolled-window", - spacing=10, - h_scrollbar_policy="never", - v_scrollbar_policy="never", - # min_content_size=(-1, -1), - child=self.viewport, - ) - self.launcher_box.add(self.scrolled_window) - - self.viewport.children = [] - self.shell_command_manager.show_shell_commands(self.viewport, "") - - self.viewport.show() - self.search_entry.grab_focus() - - def handle_search_input(self, text: str): - self.shell_command_manager.show_shell_commands(self.viewport, text) - - -class ShellCommandManager: - def __init__(self, launcher): - self.launcher = launcher - self.binaries = set() - self.reload_binaries() - - def reload_binaries(self): - paths = os.getenv("PATH", "").split(":") - self.binaries = set() - - for path in paths: - self.binaries.update(self.list_binaries_in_path(path)) - - with open(BINS, "w") as f: - f.write("\n".join(self.binaries)) - - def list_binaries_in_path(self, path): - if not os.path.exists(path): - return [] - try: - return [ - entry - for entry in os.listdir(path) - if os.access(os.path.join(path, entry), os.X_OK) - ] - except OSError: - return [] - - def show_shell_commands(self, viewport, search_query: str): - if not search_query: - return - - viewport.children = [] - - results = self.query_binaries(search_query) - self.display_results(viewport, results) - - def query_binaries(self, filter_str: str) -> List[str]: - filter_str = filter_str.lower() - filtered_bins = (cmd for cmd in self.binaries if filter_str in cmd) - return list(dict.fromkeys(filtered_bins))[:8] - - def display_results(self, viewport, results: List[str]): - for command in results: - button = Button( - child=Label( - label=command, - v_align="center", - h_align="start", - ), - on_clicked=lambda _, cmd=command: self.execute_command(cmd), - name="app-slot-button", - ) - viewport.add(button) - - def close_menu(self): - self.launcher.close() - - def execute_command(self, cmd: str): - try: - output = subprocess.check_output(cmd, shell=True, text=True) - print(output) - except subprocess.CalledProcessError as e: - print(e.output) - finally: - self.close_menu() diff --git a/modules/launcher/components/tmux.py b/modules/launcher/components/tmux.py deleted file mode 100644 index 583e477..0000000 --- a/modules/launcher/components/tmux.py +++ /dev/null @@ -1,540 +0,0 @@ -import subprocess -import os -from fabric.widgets.box import Box -from fabric.widgets.label import Label -from fabric.widgets.button import Button -from fabric.widgets.entry import Entry -from fabric.widgets.scrolledwindow import ScrolledWindow -from fabric.utils import idle_add, remove_handler, exec_shell_command_async -from gi.repository import GLib, Gdk, Gtk -import utils.icons as icons -import config.data as data - -class TmuxManager(Box): - def __init__(self, **kwargs): - super().__init__( - name="tmux-manager", - visible=False, - all_visible=False, - **kwargs, - ) - - self.launcher = kwargs["launcher"] - self.selected_index = -1 # Track the selected item index - - self._arranger_handler: int = 0 - - self.viewport = Box(name="viewport", spacing=4, orientation="v") - self.session_name_entry = Entry( - name="session-name-entry", - placeholder="Create Tmux Session...", - h_expand=True, - on_activate=lambda entry, *_: self.create_session(entry.get_text()), - on_key_press_event=self.on_entry_key_press, - ) - self.session_name_entry.props.xalign = 0.5 - self.scrolled_window = ScrolledWindow( - name="scrolled-window", - spacing=10, - min_content_size=(450, 105), - max_content_size=(450, 105), - child=self.viewport, - ) - - self.header_box = Box( - name="header_box", - spacing=10, - orientation="h", - children=[ - Button( - name="new-session-button", - child=Label(name="new-session-label", markup=icons.add), - tooltip_text="Create New Session", - on_clicked=lambda *_: self.create_session(self.session_name_entry.get_text()), - ), - self.session_name_entry, - ], - ) - - self.tmux_box = Box( - name="tmux-box", - spacing=10, - h_expand=True, - orientation="v", - children=[ - self.header_box, - self.scrolled_window, - ], - ) - - self.add(self.tmux_box) - self.show_all() - - def close_manager(self): - """Close the tmux manager""" - self.viewport.children = [] - self.selected_index = -1 # Reset selection - self.launcher.close() - - def open_manager(self): - """Open the tmux manager and refresh sessions""" - self.viewport.set_visible(True) - self.refresh_sessions() - self.session_name_entry.set_text("") - self.session_name_entry.grab_focus() - - def refresh_sessions(self): - """Get tmux sessions and populate the viewport""" - remove_handler(self._arranger_handler) if self._arranger_handler else None - self.viewport.children = [] - self.selected_index = -1 # Clear selection when viewport changes - - # Get tmux sessions - sessions = self.get_tmux_sessions() - if not sessions: - # Create a container box to better center the message - container = Box( - name="no-tmux-container", - orientation="v", - h_align="center", - v_align="center", - h_expand=True, - v_expand=True - ) - - # Show a message if no sessions - label = Label( - name="no-tmux", - markup=icons.terminal, - h_align="center", - v_align="center", - ) - - container.add(label) - - self.viewport.add(container) - return - - # Add session slots to viewport - for session in sessions: - self.viewport.add(self.create_session_slot(session)) - - def get_tmux_sessions(self): - """Get list of tmux sessions""" - try: - result = subprocess.run( - ["tmux", "list-sessions", "-F", "#{session_name}"], - capture_output=True, - text=True - ) - if result.returncode == 0: - return [s.strip() for s in result.stdout.strip().split('\n') if s.strip()] - return [] - except Exception as e: - print(f"Error getting tmux sessions: {e}") - return [] - - def create_session_slot(self, session_name): - """Create a button for a tmux session""" - # Create an entry for inline editing (initially hidden) - name_entry = Entry( - name="session-name-entry", - text=session_name, - visible=False, - on_activate=lambda entry, *_: self.finish_rename(button, session_name, entry), - on_key_press_event=self.on_rename_key_press, - ) - - # Create the label showing the session name - name_label = Label( - name="app-label", - label=session_name, - ellipsization="end", - v_align="center", - h_align="center", - ) - - # Session slot content box - slot_box = Box( - name="slot-box", - orientation="h", - spacing=10, - children=[ - Label( - name="tmux-icon", - markup=icons.terminal, # Use existing terminal icon - ), - name_label, - name_entry, - ], - ) - - button = Button( - name="slot-button", # reuse existing CSS styling - child=slot_box, - tooltip_text=f"Attach to session: {session_name}", - on_clicked=lambda *_: self.attach_to_session(session_name), - can_focus=True, # Ensure the button can receive focus - ) - - # Add double-click handler to start renaming - button.connect("button-press-event", self.on_session_click, session_name, name_label, name_entry) - - # Add key press handler for 'r' to rename - button.connect("key-press-event", self.on_slot_key_press, session_name, name_label, name_entry) - - # Store reference to entry and label in button for later access - button.name_entry = name_entry - button.name_label = name_label - button.session_name = session_name - - return button - - def on_session_click(self, button, event, session_name, label, entry): - """Handle clicks on session buttons""" - # Handle double-click to rename - if event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS and event.button == 1: - self.start_rename(button, session_name, label, entry) - return True - # Handle right click for context menu - elif event.button == 3: - menu = Gtk.Menu() - - # Rename option - rename_item = Gtk.MenuItem(label="Rename") - rename_item.connect("activate", lambda _: self.start_rename(button, session_name, label, entry)) - menu.append(rename_item) - - # Kill option - kill_item = Gtk.MenuItem(label="Kill Session") - kill_item.connect("activate", lambda _: self.kill_session(session_name)) - menu.append(kill_item) - - menu.show_all() - menu.popup_at_pointer(event) - return True - - return False - - def start_rename(self, button, session_name, label, entry): - """Start inline renaming of a session""" - # Hide label, show entry - label.set_visible(False) - entry.set_visible(True) - - # Focus entry and select all text - entry.grab_focus() - entry.select_region(0, -1) - - # Mark button as being edited - button.get_style_context().add_class("editing") - - def finish_rename(self, button, old_name, entry): - """Finish renaming a session""" - new_name = entry.get_text().strip() - - # Only rename if the name changed and isn't empty - if new_name and new_name != old_name: - self.rename_session(old_name, new_name) - - # Reset UI state - self.cancel_rename(button) - - def cancel_rename(self, button): - """Cancel renaming operation""" - # Restore original view - button.name_entry.set_visible(False) - button.name_label.set_visible(True) - - # Remove editing style - button.get_style_context().remove_class("editing") - - # Return focus to session name entry - self.session_name_entry.grab_focus() - - def on_rename_key_press(self, entry, event): - """Handle key presses in the rename entry""" - if event.keyval == Gdk.KEY_Escape: - # Find the parent button - parent = entry.get_parent() - while parent and not isinstance(parent, Button): - parent = parent.get_parent() - - if parent: - self.cancel_rename(parent) - return True - - return False - - def on_session_right_click(self, button, event, session_name): - """Handle right-click on a session button to show context menu""" - if event.button == 3: # Right click - menu = Gtk.Menu() - - # Rename option - rename_item = Gtk.MenuItem(label="Rename") - rename_item.connect("activate", lambda _: self.start_rename( - button, - session_name, - button.name_label, - button.name_entry - )) - menu.append(rename_item) - - # Kill option - kill_item = Gtk.MenuItem(label="Kill Session") - kill_item.connect("activate", lambda _: self.kill_session(session_name)) - menu.append(kill_item) - - menu.show_all() - menu.popup_at_pointer(event) - return True - - return False - - def on_entry_key_press(self, widget, event): - """Handle key press events in the entry""" - if event.keyval == Gdk.KEY_Escape: - self.close_manager() - return True - - # Custom navigation with UP/DOWN keys removed - return False - - def scroll_to_selected(self, button): - """Scroll to ensure the selected button is visible""" - def scroll(): - adj = self.scrolled_window.get_vadjustment() - alloc = button.get_allocation() - if alloc.height == 0: - return False # Retry if allocation isn't ready - - y = alloc.y - height = alloc.height - page_size = adj.get_page_size() - current_value = adj.get_value() - - # Calculate visible boundaries - visible_top = current_value - visible_bottom = current_value + page_size - - if y < visible_top: - # Item above viewport - align to top - adj.set_value(y) - elif y + height > visible_bottom: - # Item below viewport - align to bottom - new_value = y + height - page_size - adj.set_value(new_value) - # No action if already fully visible - return False - GLib.idle_add(scroll) - - def create_session(self, session_name): - """Create a new tmux session""" - if not session_name: - # Get existing session names - existing_sessions = self.get_tmux_sessions() - - # Find the next available number - counter = 0 - while str(counter) in existing_sessions: - counter += 1 - - session_name = str(counter) - - try: - # Clean the session name (replace spaces with underscores) - clean_name = session_name.strip().replace(" ", "_") - - # Create session - subprocess.run( - ["tmux", "new-session", "-d", "-s", clean_name], - check=True - ) - - # Refresh the session list - self.refresh_sessions() - - # Clear entry - self.session_name_entry.set_text("") - - # Launch a terminal and attach to this session - terminal_cmd = self.get_terminal_command(f"tmux attach-session -t {clean_name}") - exec_shell_command_async(terminal_cmd) - - # Close manager - self.close_manager() - - except Exception as e: - print(f"Error creating tmux session: {e}") - - def attach_to_session(self, session_name): - """Attach to an existing tmux session""" - try: - # Launch a terminal and attach to this session - terminal_cmd = self.get_terminal_command(f"tmux attach-session -t {session_name}") - exec_shell_command_async(terminal_cmd) - self.close_manager() - except Exception as e: - print(f"Error attaching to tmux session: {e}") - - def get_terminal_command(self, cmd): - """Get terminal command based on configured terminal or available terminals""" - # First try to use the configured terminal command - if hasattr(data, 'TERMINAL_COMMAND') and data.TERMINAL_COMMAND: - parts = data.TERMINAL_COMMAND.split() - terminal = parts[0] - - try: - # Check if the configured terminal is available - subprocess.run(["which", terminal], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - return f"{data.TERMINAL_COMMAND} {cmd}" - except subprocess.CalledProcessError: - # If configured terminal is not available, fall back to defaults - pass - - # Fallback to checking available terminals - terminals = [ - ("kitty", f"kitty -e {cmd}"), - ("alacritty", f"alacritty -e {cmd}"), - ("foot", f"foot {cmd}"), - ("gnome-terminal", f"gnome-terminal -- {cmd}"), - ("konsole", f"konsole -e {cmd}"), - ("xfce4-terminal", f"xfce4-terminal -e '{cmd}'"), - ] - - for term, term_cmd in terminals: - try: - # Check if terminal is available - subprocess.run(["which", term], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - return term_cmd - except subprocess.CalledProcessError: - continue - - # Default fallback - return f"kitty -e {cmd}" - - def rename_session_dialog(self, old_name): - """Show dialog to rename a session""" - dialog = Gtk.Dialog( - title="Rename Session", - transient_for=None, - flags=0 - ) - - dialog.add_buttons( - Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK - ) - - content_area = dialog.get_content_area() - entry = Gtk.Entry() - entry.set_text(old_name) - entry.set_activates_default(True) - content_area.add(entry) - - dialog.set_default_response(Gtk.ResponseType.OK) - dialog.show_all() - - response = dialog.run() - if response == Gtk.ResponseType.OK: - new_name = entry.get_text() - if new_name and new_name != old_name: - self.rename_session(old_name, new_name) - - dialog.destroy() - - def rename_session(self, old_name, new_name): - """Rename a tmux session""" - try: - # Clean the session name (replace spaces with underscores) - clean_name = new_name.strip().replace(" ", "_") - - # Rename session - subprocess.run( - ["tmux", "rename-session", "-t", old_name, clean_name], - check=True - ) - - # Refresh the session list - self.refresh_sessions() - - except Exception as e: - print(f"Error renaming tmux session: {e}") - - def kill_session(self, session_name): - """Kill a tmux session""" - try: - # Kill session - subprocess.run( - ["tmux", "kill-session", "-t", session_name], - check=True - ) - - # Refresh the session list - self.refresh_sessions() - - # Close the launcher after killing session - self.close_manager() - - except Exception as e: - print(f"Error killing tmux session: {e}") - - # Add new method to handle key presses on session slots - def on_slot_key_press(self, button, event, session_name, label, entry): - """Handle key presses on session buttons""" - # Print debugging info - print(f"Key pressed: {event.keyval}, State: {event.state}") - - # Check if 'r' key was pressed for renaming - if event.keyval == Gdk.KEY_r: - self.start_rename(button, session_name, label, entry) - return True - # Check for 'K' (capital K) which indicates Shift is pressed - elif event.keyval == Gdk.KEY_K: - print("Shift+K detected - killing session without confirmation") - self.kill_session(session_name) - return True - # Check for lowercase 'k' - elif event.keyval == Gdk.KEY_k: - print("Regular k detected - showing confirmation") - self.show_kill_confirmation_menu(button, session_name) - return True - # Check if Delete key was pressed for killing session - elif event.keyval == Gdk.KEY_Delete: - self.show_kill_confirmation_menu(button, session_name) - return True - return False - - def show_kill_confirmation_menu(self, button, session_name): - """Show a confirmation menu for killing a session""" - menu = Gtk.Menu() - - # Confirmation message as a disabled menu item - msg_item = Gtk.MenuItem(label=f"Kill session '{session_name}'?") - msg_item.set_sensitive(False) - menu.append(msg_item) - - # Separator - menu.append(Gtk.SeparatorMenuItem()) - - # Confirm option - confirm_item = Gtk.MenuItem(label="Confirm") - confirm_item.connect("activate", lambda _: self.kill_session(session_name)) - menu.append(confirm_item) - - # Cancel option - cancel_item = Gtk.MenuItem(label="Cancel") - # Close launcher on cancel - cancel_item.connect("activate", lambda _: self.close_manager()) - menu.append(cancel_item) - - menu.show_all() - - # Show the menu positioned at the button - menu.popup_at_widget( - button, - Gdk.Gravity.SOUTH_WEST, - Gdk.Gravity.NORTH_WEST, - None - ) \ No newline at end of file diff --git a/modules/launcher/components/tools.py b/modules/launcher/components/tools.py deleted file mode 100644 index d04c39d..0000000 --- a/modules/launcher/components/tools.py +++ /dev/null @@ -1,370 +0,0 @@ -from fabric.widgets.box import Box -from fabric.widgets.label import Label -from fabric.widgets.button import Button -from fabric.utils.helpers import exec_shell_command_async, get_relative_path -import utils.icons as icons -from gi.repository import Gdk, GLib -import os -import subprocess -import threading - - -CAPTURE_SCRIPT = get_relative_path("../../../config/scripts/capture.sh") -GAMEMODE_SCRIPT = get_relative_path("../../../config/scripts/gamemode.sh") - - -class Toolbox(Box): - def __init__(self, **kwargs): - super().__init__( - name="toolbox", - orientation="h", - spacing=4, - v_align="center", - h_align="center", - visible=True, - **kwargs, - ) - - self.launcher = kwargs["launcher"] - - self.btn_ssregion = Button( - name="toolbox-button", - child=Label(name="button-label", markup=icons.ssregion), - on_clicked=self.ssregion, - h_expand=False, - v_expand=False, - h_align="center", - v_align="center", - ) - # Enable keyboard focus and connect events - self.btn_ssregion.set_can_focus(True) - self.btn_ssregion.connect("button-press-event", self.on_ssregion_click) - self.btn_ssregion.connect("key-press-event", self.on_ssregion_key) - - self.btn_ssfull = Button( - name="toolbox-button", - child=Label(name="button-label", markup=icons.ssfull), - on_clicked=self.ssfull, - h_expand=False, - v_expand=False, - h_align="center", - v_align="center", - ) - # Enable keyboard focus and connect events - self.btn_ssfull.set_can_focus(True) - self.btn_ssfull.connect("button-press-event", self.on_ssfull_click) - self.btn_ssfull.connect("key-press-event", self.on_ssfull_key) - - self.btn_screenrecord = Button( - name="toolbox-button", - child=Label(name="button-label", markup=icons.screenrecord), - on_clicked=self.screenrecord, - h_expand=False, - v_expand=False, - h_align="center", - v_align="center", - ) - - self.btn_ocr = Button( - name="toolbox-button", - child=Label(name="button-label", markup=icons.ocr), - on_clicked=self.ocr, - h_expand=False, - v_expand=False, - h_align="center", - v_align="center", - ) - - self.btn_color = Button( - name="toolbox-button", - tooltip_text="Color Picker\nLeft Click: HEX\nMiddle Click: HSV\nRight Click: RGB\n\nKeyboard:\nEnter: HEX\nShift+Enter: RGB\nCtrl+Enter: HSV", - child=Label(name="button-bar-label", markup=icons.colorpicker), - h_expand=False, - v_expand=False, - h_align="center", - v_align="center", - ) - - self.btn_gamemode = Button( - name="toolbox-button", - child=Label(name="button-label", markup=icons.gamemode), - on_clicked=self.gamemode, - h_expand=False, - v_expand=False, - h_align="center", - v_align="center", - ) - - # Enable keyboard focus for the colorpicker button. - self.btn_color.set_can_focus(True) - # Connect both mouse and keyboard events. - self.btn_color.connect("button-press-event", self.colorpicker) - self.btn_color.connect("key_press_event", self.colorpicker_key) - - self.btn_emoji = Button( - name="toolbox-button", - child=Label(name="button-label", markup=icons.emoji), - on_clicked=self.emoji, - h_expand=False, - v_expand=False, - h_align="center", - v_align="center", - ) - - self.btn_screenshots_folder = Button( - name="toolbox-button", - child=Label(name="button-label", markup=icons.screenshots), - on_clicked=self.open_screenshots_folder, - h_expand=False, - v_expand=False, - h_align="center", - v_align="center", - ) - - self.btn_recordings_folder = Button( - name="toolbox-button", - child=Label(name="button-label", markup=icons.recordings), - on_clicked=self.open_recordings_folder, - h_expand=False, - v_expand=False, - h_align="center", - v_align="center", - ) - - self.buttons = [ - self.btn_ssregion, - self.btn_ssfull, - self.btn_screenshots_folder, - Box( - name="tool-sep", - h_expand=False, - v_expand=False, - h_align="center", - v_align="center", - ), - self.btn_screenrecord, - self.btn_recordings_folder, - Box( - name="tool-sep", - h_expand=False, - v_expand=False, - h_align="center", - v_align="center", - ), - self.btn_ocr, - self.btn_color, - Box( - name="tool-sep", - h_expand=False, - v_expand=False, - h_align="center", - v_align="center", - ), - self.btn_gamemode, - self.btn_emoji, - ] - - for button in self.buttons: - self.add(button) - - self.show_all() - - # Start polling for process state every second. - self.recorder_timer_id = GLib.timeout_add_seconds( - 1, self.update_screenrecord_state - ) - - def close_menu(self): - self.launcher.close() - - # Action methods - def ssfull(self, *args): - exec_shell_command_async(f"bash {CAPTURE_SCRIPT} screenshot p") - self.close_menu() - - def on_ssfull_click(self, button, event): - if event.type == Gdk.EventType.BUTTON_PRESS: - if event.button == 1: # Left click - self.ssfull() - elif event.button == 3: # Right click - exec_shell_command_async( - f"bash {CAPTURE_SCRIPT} screenshot p mockup") - self.close_menu() - return True - return False - - def on_ssfull_key(self, widget, event): - if event.keyval in {Gdk.KEY_Return, Gdk.KEY_KP_Enter}: - modifiers = event.get_state() - if modifiers & Gdk.ModifierType.SHIFT_MASK: - exec_shell_command_async( - f"bash {CAPTURE_SCRIPT} screenshot p mockup") - self.close_menu() - else: - self.ssfull() - return True - return False - - def ssregion(self, *args): - exec_shell_command_async(f"bash {CAPTURE_SCRIPT} screenshot sf") - self.close_menu() - - def on_ssregion_click(self, button, event): - if event.type == Gdk.EventType.BUTTON_PRESS: - if event.button == 1: # Left click - self.ssregion() - elif event.button == 3: # Right click - exec_shell_command_async( - f"bash {CAPTURE_SCRIPT} screenshot sf mockup") - self.close_menu() - return True - return False - - def on_ssregion_key(self, widget, event): - if event.keyval in {Gdk.KEY_Return, Gdk.KEY_KP_Enter}: - modifiers = event.get_state() - if modifiers & Gdk.ModifierType.SHIFT_MASK: - exec_shell_command_async( - f"bash {CAPTURE_SCRIPT} screenshot sf mockup") - self.close_menu() - else: - self.ssregion() - return True - return False - - def screenrecord(self, *args): - # Launch screenrecord script in detached mode so that it remains running independently of this program. - exec_shell_command_async( - f"bash -c 'nohup bash {CAPTURE_SCRIPT} record > /dev/null 2>&1 & disown'" - ) - self.close_menu() - - def ocr(self, *args): - exec_shell_command_async(f"bash {CAPTURE_SCRIPT} ocr sf") - self.close_menu() - - def gamemode(self, *args): - exec_shell_command_async(f"bash {GAMEMODE_SCRIPT}") - self.gamemode_check() - self.close_menu() - - def gamemode_check(self): - def check(): - try: - result = subprocess.run( - f"bash {GAMEMODE_SCRIPT} check", - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - enabled = result.stdout == b"t\n" - except Exception: - enabled = False - - def update_ui(): - if enabled: - self.btn_gamemode.get_child().set_markup(icons.gamemode_off) - else: - self.btn_gamemode.get_child().set_markup(icons.gamemode) - return False - - GLib.idle_add(update_ui) - return False - - GLib.idle_add(lambda: threading.Thread(target=check).start()) - return True - - def ssregion(self, *args): - exec_shell_command_async(f"bash {CAPTURE_SCRIPT} screenshot sf") - self.close_menu() - - def colorpicker(self, button, event): - if event.type == Gdk.EventType.BUTTON_PRESS: - cmd = { - 1: "-hex", # Left click - 2: "-hsv", # Middle click - 3: "-rgb", # Right click - }.get(event.button) - - if cmd: - exec_shell_command_async( - f"bash { - get_relative_path( - '../../../config/scripts/hyprpicker.sh') - } {cmd}" - ) - self.close_menu() - - def colorpicker_key(self, widget, event): - if event.keyval in {Gdk.KEY_Return, Gdk.KEY_KP_Enter}: - modifiers = event.get_state() - cmd = "-hex" # Default - - match modifiers & ( - Gdk.ModifierType.SHIFT_MASK | Gdk.ModifierType.CONTROL_MASK - ): - case Gdk.ModifierType.SHIFT_MASK: - cmd = "-rgb" - case Gdk.ModifierType.CONTROL_MASK: - cmd = "-hsv" - - exec_shell_command_async( - f"bash {get_relative_path('../../../config/scripts/hyprpicker.sh')} { - cmd - }" - ) - self.close_menu() - return True - return False - - def update_screenrecord_state(self): - def check(): - try: - result = subprocess.run( - "pgrep -f gpu-screen-recorder", - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - running = result.returncode == 0 - except Exception: - running = False - - def update_ui(): - if running: - self.btn_screenrecord.get_child().set_markup(icons.stop) - self.btn_screenrecord.add_style_class("recording") - else: - self.btn_screenrecord.get_child().set_markup(icons.screenrecord) - self.btn_screenrecord.remove_style_class("recording") - return False - - GLib.idle_add(update_ui) - return False - - GLib.idle_add(lambda: threading.Thread(target=check).start()) - return True - - def open_screenshots_folder(self, *args): - screenshots_dir = os.path.join( - os.environ.get("XDG_PICTURES_DIR", - os.path.expanduser("~/Pictures")), - "Screenshots", - ) - # Create directory if it doesn't exist - os.makedirs(screenshots_dir, exist_ok=True) - exec_shell_command_async(f"xdg-open {screenshots_dir}") - self.close_menu() - - def open_recordings_folder(self, *args): - recordings_dir = os.path.join( - os.environ.get("XDG_VIDEOS_DIR", os.path.expanduser("~/Videos")), - "Recordings", - ) - # Create directory if it doesn't exist - os.makedirs(recordings_dir, exist_ok=True) - exec_shell_command_async(f"xdg-open {recordings_dir}") - self.close_menu() - - def emoji(self, *args): - self.launcher.open("emoji") diff --git a/modules/launcher/components/wallpapers.py b/modules/launcher/components/wallpapers.py deleted file mode 100644 index 45c05b4..0000000 --- a/modules/launcher/components/wallpapers.py +++ /dev/null @@ -1,485 +0,0 @@ -import os -import hashlib -import shutil -import colorsys -from gi.repository import GdkPixbuf, Gtk, GLib, Gio, Gdk, Pango -from fabric.widgets.box import Box -from fabric.widgets.button import Button -from fabric.widgets.centerbox import CenterBox -from fabric.widgets.entry import Entry -from fabric.widgets.scrolledwindow import ScrolledWindow -from fabric.widgets.label import Label -from fabric.utils.helpers import exec_shell_command_async -import utils.icons as icons -import config.data as data -import config.config -from PIL import Image -import concurrent.futures -from concurrent.futures import ThreadPoolExecutor - -class WallpaperSelector(Box): - CACHE_DIR = f"{data.CACHE_DIR}/thumbnails" # Changed from wallpapers to thumbs - - def __init__(self, **kwargs): - - super().__init__(name="wallpapers", spacing=4, orientation="v", h_expand=False, v_expand=False, **kwargs) - os.makedirs(self.CACHE_DIR, exist_ok=True) - - # Process old wallpapers: use os.scandir for efficiency and only loop - # over image files that actually need renaming (they're not already lowercase - # and with hyphens instead of spaces) - with os.scandir(data.WALLPAPERS_DIR) as entries: - for entry in entries: - if entry.is_file() and self._is_image(entry.name): - # Check if the file needs renaming: file should be lowercase and have hyphens instead of spaces - if entry.name != entry.name.lower() or " " in entry.name: - new_name = entry.name.lower().replace(" ", "-") - full_path = os.path.join(data.WALLPAPERS_DIR, entry.name) - new_full_path = os.path.join(data.WALLPAPERS_DIR, new_name) - try: - os.rename(full_path, new_full_path) - print(f"Renamed old wallpaper '{full_path}' to '{new_full_path}'") - except Exception as e: - print(f"Error renaming file {full_path}: {e}") - - # Refresh the file list after potential renaming - self.files = sorted([f for f in os.listdir(data.WALLPAPERS_DIR) if self._is_image(f)]) - self.thumbnails = [] - self.thumbnail_queue = [] - self.executor = ThreadPoolExecutor(max_workers=4) # Shared executor - - # Variable to control the selection (similar to AppLauncher) - self.selected_index = -1 - - # Initialize UI components - self.viewport = Gtk.IconView(name="wallpaper-icons") - self.viewport.set_model(Gtk.ListStore(GdkPixbuf.Pixbuf, str)) - self.viewport.set_pixbuf_column(0) - # Hide text column so only the image is shown - self.viewport.set_text_column(-1) - self.viewport.set_item_width(0) - self.viewport.connect("item-activated", self.on_wallpaper_selected) - # self.viewport.connect("selection-changed", self._on_selection_changed) # Removed connection - - self.scrolled_window = ScrolledWindow( - name="scrolled-window", - spacing=10, - h_expand=True, - v_expand=True, - child=self.viewport, - ) - - self.search_entry = Entry( - name="search-entry-walls", - placeholder="Search Wallpapers...", - h_expand=True, - notify_text=lambda entry, *_: self.arrange_viewport(entry.get_text()), - on_key_press_event=self.on_search_entry_key_press, - ) - self.search_entry.props.xalign = 0.5 - self.search_entry.connect("focus-out-event", self.on_search_entry_focus_out) - - self.schemes = { - "scheme-tonal-spot": "Tonal Spot", - "scheme-content": "Content", - "scheme-expressive": "Expressive", - "scheme-fidelity": "Fidelity", - "scheme-fruit-salad": "Fruit Salad", - "scheme-monochrome": "Monochrome", - "scheme-neutral": "Neutral", - "scheme-rainbow": "Rainbow", - } - - self.scheme_dropdown = Gtk.ComboBoxText() - self.scheme_dropdown.set_name("scheme-dropdown") - self.scheme_dropdown.set_tooltip_text("Select color scheme") - for key, display_name in self.schemes.items(): - self.scheme_dropdown.append(key, display_name) - self.scheme_dropdown.set_active_id("scheme-tonal-spot") - self.scheme_dropdown.connect("changed", self.on_scheme_changed) - - # Load matugen state from the dedicated file - self.matugen_enabled = True # Default to True - try: - with open(data.MATUGEN_STATE_FILE, 'r') as f: - content = f.read().strip().lower() - if content == "false": - self.matugen_enabled = False - elif content == "true": - self.matugen_enabled = True - # Any other content defaults to True - except FileNotFoundError: - # File doesn't exist, keep default True and create it on first toggle - pass - except Exception as e: - print(f"Error reading matugen state file: {e}") - # Keep default True on error - - # Create a switcher to enable/disable Matugen (enabled by default) - self.matugen_switcher = Gtk.Switch(name="matugen-switcher") - self.matugen_switcher.set_vexpand(False) - self.matugen_switcher.set_hexpand(False) - self.matugen_switcher.set_valign(Gtk.Align.CENTER) - self.matugen_switcher.set_halign(Gtk.Align.CENTER) - self.matugen_switcher.set_active(self.matugen_enabled) - self.matugen_switcher.connect("notify::active", self.on_switch_toggled) - - self.mat_icon = Label(name="mat-label", markup=icons.palette) - - # Add the switcher to the header_box's start_children - self.header_box = CenterBox( - name="header-box", - spacing=8, - orientation="h", - # Removed color button and label from here - start_children=[self.matugen_switcher, self.mat_icon], - center_children=[self.search_entry], - end_children=[self.scheme_dropdown], - ) - - self.add(self.header_box) - - # Create the custom color selector components - self.hue_slider = Gtk.Scale( - orientation=Gtk.Orientation.HORIZONTAL, # Changed from VERTICAL - adjustment=Gtk.Adjustment(value=0, lower=0, upper=360, step_increment=1, page_increment=10), - draw_value=False, # Hide the default value text - digits=0, - # inverted=True, # Removed inverted for horizontal - name="hue-slider", # For CSS styling - ) - - # Changed expand/align for horizontal orientation - self.hue_slider.set_hexpand(True) - self.hue_slider.set_halign(Gtk.Align.FILL) - self.hue_slider.set_vexpand(False) # Ensure it doesn't expand vertically - self.hue_slider.set_valign(Gtk.Align.CENTER) # Center vertically within its box - - self.apply_color_button = Button(name="apply-color-button", child=Label(name="apply-color-label", markup=icons.accept)) - self.apply_color_button.connect("clicked", self.on_apply_color_clicked) - self.apply_color_button.set_vexpand(False) # Ensure button doesn't expand vertically - self.apply_color_button.set_valign(Gtk.Align.CENTER) # Center button vertically - - self.custom_color_selector_box = Box( - orientation="h", spacing=5, name="custom-color-selector-box", # Changed orientation to horizontal - h_align="center" # Center the horizontal box - ) - self.custom_color_selector_box.add(self.hue_slider) - self.custom_color_selector_box.add(self.apply_color_button) - self.custom_color_selector_box.set_halign(Gtk.Align.FILL) - - # Add the scrolled window (grid) and the custom color selector box directly - # to the main WallpaperSelector box (which is already vertical) - self.pack_start(self.scrolled_window, True, True, 0) # Add grid, expand - self.pack_start(self.custom_color_selector_box, False, False, 0) # Add custom selector, don't expand - - # Removed the old main_content_box and its add - - self._start_thumbnail_thread() - self.connect("map", self.on_map) # Connect the map signal - # Set initial sensitivity based on loaded state - # self.scheme_dropdown.set_sensitive(self.matugen_enabled) # Ensure sensitivity is set correctly on load - self.setup_file_monitor() # Initialize file monitoring - self.show_all() - # Ensure the search entry gets focus when starting - self.search_entry.grab_focus() - - def setup_file_monitor(self): - gfile = Gio.File.new_for_path(data.WALLPAPERS_DIR) - self.file_monitor = gfile.monitor_directory(Gio.FileMonitorFlags.NONE, None) - self.file_monitor.connect("changed", self.on_directory_changed) - - def on_directory_changed(self, monitor, file, other_file, event_type): - file_name = file.get_basename() - if event_type == Gio.FileMonitorEvent.DELETED: - if file_name in self.files: - self.files.remove(file_name) - cache_path = self._get_cache_path(file_name) - if os.path.exists(cache_path): - try: - os.remove(cache_path) - except Exception as e: - print(f"Error deleting cache {cache_path}: {e}") - self.thumbnails = [(p, n) for p, n in self.thumbnails if n != file_name] - GLib.idle_add(self.arrange_viewport, self.search_entry.get_text()) - elif event_type == Gio.FileMonitorEvent.CREATED: - if self._is_image(file_name): - # Convert filename to lowercase and replace spaces with "-" - new_name = file_name.lower().replace(" ", "-") - full_path = os.path.join(data.WALLPAPERS_DIR, file_name) - new_full_path = os.path.join(data.WALLPAPERS_DIR, new_name) - if new_name != file_name: - try: - os.rename(full_path, new_full_path) - file_name = new_name - print(f"Renamed file '{full_path}' to '{new_full_path}')") - except Exception as e: - print(f"Error renaming file {full_path}: {e}") - if file_name not in self.files: - self.files.append(file_name) - self.files.sort() - self.executor.submit(self._process_file, file_name) - elif event_type == Gio.FileMonitorEvent.CHANGED: - if self._is_image(file_name) and file_name in self.files: - cache_path = self._get_cache_path(file_name) - if os.path.exists(cache_path): - try: - os.remove(cache_path) - except Exception as e: - print(f"Error deleting cache for changed file {file_name}: {e}") - self.executor.submit(self._process_file, file_name) - - def arrange_viewport(self, query: str = ""): - model = self.viewport.get_model() - model.clear() - filtered_thumbnails = [ - (thumb, name) - for thumb, name in self.thumbnails - if query.casefold() in name.casefold() - ] - filtered_thumbnails.sort(key=lambda x: x[1].lower()) - for pixbuf, file_name in filtered_thumbnails: - model.append([pixbuf, file_name]) - # If the search entry is empty, no icon is selected; otherwise, select the first one. - if query.strip() == "": - self.viewport.unselect_all() - self.selected_index = -1 - elif len(model) > 0: - self.update_selection(0) - - def on_wallpaper_selected(self, iconview, path): - model = iconview.get_model() - file_name = model[path][1] - full_path = os.path.join(data.WALLPAPERS_DIR, file_name) - selected_scheme = self.scheme_dropdown.get_active_id() - current_wall = os.path.expanduser(f"~/.current.wall") - if os.path.isfile(current_wall): - os.remove(current_wall) - os.symlink(full_path, current_wall) - if self.matugen_switcher.get_active(): - # Matugen is enabled: run the normal command. - exec_shell_command_async(f'matugen image {full_path} -t {selected_scheme}') - else: - # Matugen is disabled: run the alternative swww command. - exec_shell_command_async( - f'swww img {full_path} -t outer --transition-duration 1.5 --transition-step 255 --transition-fps 60 -f Nearest' - ) - - def on_scheme_changed(self, combo): - selected_scheme = combo.get_active_id() - print(f"Color scheme selected: {selected_scheme}") - - def on_search_entry_key_press(self, widget, event): - if event.state & Gdk.ModifierType.SHIFT_MASK: - if event.keyval in (Gdk.KEY_Up, Gdk.KEY_Down): - schemes_list = list(self.schemes.keys()) - current_id = self.scheme_dropdown.get_active_id() - current_index = schemes_list.index(current_id) if current_id in schemes_list else 0 - new_index = (current_index - 1) % len(schemes_list) if event.keyval == Gdk.KEY_Up else (current_index + 1) % len(schemes_list) - self.scheme_dropdown.set_active(new_index) - return True - elif event.keyval == Gdk.KEY_Right: - self.scheme_dropdown.popup() - return True - - if event.keyval in (Gdk.KEY_Up, Gdk.KEY_Down, Gdk.KEY_Left, Gdk.KEY_Right): - self.move_selection_2d(event.keyval) - return True - elif event.keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter): - if self.selected_index != -1: - path = Gtk.TreePath.new_from_indices([self.selected_index]) - self.on_wallpaper_selected(self.viewport, path) - return True - return False - - # Removed _on_selection_changed method - - def move_selection_2d(self, keyval): - model = self.viewport.get_model() - total_items = len(model) - if total_items == 0: - return - - # --- Determine Column Count --- - columns = self.viewport.get_columns() - - # If get_columns returns 0 or -1 (auto), try to estimate by checking item rows - if columns <= 0 and total_items > 0: - estimated_cols = 0 - try: - # Check the row of the first item (should be 0) - first_item_path = Gtk.TreePath.new_from_indices([0]) - base_row = self.viewport.get_item_row(first_item_path) - - # Find the index of the first item in the *next* row - for i in range(1, total_items): - path = Gtk.TreePath.new_from_indices([i]) - row = self.viewport.get_item_row(path) - if row > base_row: - estimated_cols = i # The number of items in the first row - break - - # If loop finished without finding a new row, all items are in one row - if estimated_cols == 0: - estimated_cols = total_items - - columns = max(1, estimated_cols) - except Exception: - # Fallback if get_item_row fails (e.g., widget not realized) - columns = 1 - elif columns <= 0 and total_items == 0: - columns = 1 # Should not happen due to early return, but safe - - # Ensure columns is at least 1 after all checks - columns = max(1, columns) - - # --- Navigation Logic --- - current_index = self.selected_index - new_index = current_index - - if current_index == -1: - # If nothing is selected, select the first or last item based on direction - if keyval in (Gdk.KEY_Down, Gdk.KEY_Right): - new_index = 0 - elif keyval in (Gdk.KEY_Up, Gdk.KEY_Left): - new_index = total_items - 1 - if total_items == 0: new_index = -1 # Handle edge case - - else: - # Calculate potential new index based on key press - if keyval == Gdk.KEY_Up: - potential_new_index = current_index - columns - # Only update if the new index is valid (>= 0) - if potential_new_index >= 0: - new_index = potential_new_index - elif keyval == Gdk.KEY_Down: - potential_new_index = current_index + columns - # Only update if the new index is valid (< total_items) - if potential_new_index < total_items: - new_index = potential_new_index - elif keyval == Gdk.KEY_Left: - # Only update if not already in the first column (index % columns != 0) - # and the index is greater than 0 - if current_index > 0 and current_index % columns != 0: - new_index = current_index - 1 - elif keyval == Gdk.KEY_Right: - # Only update if not in the last column ((index + 1) % columns != 0) - # and not the very last item (index < total_items - 1) - if current_index < total_items - 1 and (current_index + 1) % columns != 0: - new_index = current_index + 1 - - # Only update if the index actually changed and is valid - if new_index != self.selected_index and 0 <= new_index < total_items: - self.update_selection(new_index) - elif total_items > 0 and self.selected_index == -1 and 0 <= new_index < total_items: - # Handle selecting the first item when starting from -1 - self.update_selection(new_index) - - def update_selection(self, new_index: int): - self.viewport.unselect_all() - path = Gtk.TreePath.new_from_indices([new_index]) - self.viewport.select_path(path) - self.viewport.scroll_to_path(path, False, 0.5, 0.5) # Ensure the selected icon is visible - self.selected_index = new_index - - def _start_thumbnail_thread(self): - thread = GLib.Thread.new("thumbnail-loader", self._preload_thumbnails, None) - - def _preload_thumbnails(self, _data): - futures = [self.executor.submit(self._process_file, file_name) for file_name in self.files] - concurrent.futures.wait(futures) - GLib.idle_add(self._process_batch) - - def _process_file(self, file_name): - full_path = os.path.join(data.WALLPAPERS_DIR, file_name) - cache_path = self._get_cache_path(file_name) - if not os.path.exists(cache_path): - try: - with Image.open(full_path) as img: - width, height = img.size - side = min(width, height) - left = (img.width - side) // 2 - top = (height - side) // 2 - right = left + side - bottom = top + side - img_cropped = img.crop((left, top, right, bottom)) - img_cropped.thumbnail((96, 96), Image.Resampling.LANCZOS) - img_cropped.save(cache_path, "PNG") - except Exception as e: - print(f"Error processing {file_name}: {e}") - return - self.thumbnail_queue.append((cache_path, file_name)) - GLib.idle_add(self._process_batch) - - def _process_batch(self): - batch = self.thumbnail_queue[:10] - del self.thumbnail_queue[:10] - for cache_path, file_name in batch: - try: - pixbuf = GdkPixbuf.Pixbuf.new_from_file(cache_path) - self.thumbnails.append((pixbuf, file_name)) - self.viewport.get_model().append([pixbuf, file_name]) - except Exception as e: - print(f"Error loading thumbnail {cache_path}: {e}") - if self.thumbnail_queue: - GLib.idle_add(self._process_batch) - return False - - def _get_cache_path(self, file_name: str) -> str: - file_hash = hashlib.md5(file_name.encode("utf-8")).hexdigest() - return os.path.join(self.CACHE_DIR, f"{file_hash}.png") - - @staticmethod - def _is_image(file_name: str) -> bool: - return file_name.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif', '.webp')) - - def on_search_entry_focus_out(self, widget, event): - if self.get_mapped(): - widget.grab_focus() - return False - - def on_map(self, widget): - """Handles the map signal to set initial visibility of the color selector.""" - # Set visibility based on the loaded state when the widget becomes visible - self.custom_color_selector_box.set_visible(not self.matugen_enabled) - - def hsl_to_rgb_hex(self, h: float, s: float = 1.0, l: float = 0.5) -> str: - """Converts HSL color value to RGB HEX string.""" - # colorsys uses HLS, not HSL, and expects values between 0.0 and 1.0 - hue = h / 360.0 - r, g, b = colorsys.hls_to_rgb(hue, l, s) # Note the order: H, L, S - r_int, g_int, b_int = int(r * 255), int(g * 255), int(b * 255) - return f"#{r_int:02X}{g_int:02X}{b_int:02X}" - - def rgba_to_hex(self, rgba: Gdk.RGBA) -> str: - """Converts Gdk.RGBA to a HEX color string.""" - r = int(rgba.red * 255) - g = int(rgba.green * 255) - b = int(rgba.blue * 255) - return f"#{r:02X}{g:02X}{b:02X}" - - def on_switch_toggled(self, switch, gparam): - """Handles the toggling of the Matugen switch.""" - is_active = switch.get_active() - self.matugen_enabled = is_active - # self.scheme_dropdown.set_sensitive(is_active) - self.custom_color_selector_box.set_visible(not is_active) # Toggle visibility - - # Save the state to the dedicated file - try: - with open(data.MATUGEN_STATE_FILE, 'w') as f: - f.write(str(is_active)) - except Exception as e: - print(f"Error writing matugen state file: {e}") - - def on_apply_color_clicked(self, button): - """Applies the color selected by the hue slider via matugen.""" - hue_value = self.hue_slider.get_value() # Get value from 0-360 - hex_color = self.hsl_to_rgb_hex(hue_value) # Convert HSL(hue, 1.0, 0.5) to HEX - print(f"Applying color from slider: H={hue_value}, HEX={hex_color}") - selected_scheme = self.scheme_dropdown.get_active_id() - # Run matugen with the chosen hex color and selected scheme - exec_shell_command_async(f'matugen color hex "{hex_color}" -t {selected_scheme}') - # Optionally save the chosen color to config if needed later - # config.config.bind_vars["matugen_hex_color"] = hex_color - # config.config.save_config() # Removed as save_config doesn't exist diff --git a/modules/launcher/components/wifi.py b/modules/launcher/components/wifi.py deleted file mode 100644 index 94e52d6..0000000 --- a/modules/launcher/components/wifi.py +++ /dev/null @@ -1,153 +0,0 @@ -from fabric.widgets.box import Box -from fabric.widgets.label import Label -from fabric.widgets.image import Image -from fabric.widgets.button import Button -from fabric.widgets.centerbox import CenterBox -from fabric.widgets.scrolledwindow import ScrolledWindow -from services import network_client -import utils.icons as icons - - -class WifiNetworkSlot(CenterBox): - def __init__(self, network, client: network_client, **kwargs): - super().__init__(name="wifi-device", **kwargs) - self.network = network - self.client = client - self.connecting = False # Tracks if we are connecting/disconnecting - - self.connection_icon = Label(name="wifi-connection", markup=icons.wifi_off) - self.connect_button = Button( - name="wifi-connect", - label="Connect", - on_clicked=self.on_connect_clicked, - ) - - self.start_children = [ - Box( - spacing=8, - children=[ - Image(icon_name=network.get("icon-name"), size=32), - Label(label=network.get("ssid")), - self.connection_icon, - ], - ) - ] - self.end_children = self.connect_button - - self.visible = True - self.show_all() - - # Listen to WiFi state changes - self.client.wifi_device.connect("changed", lambda *_: self.update_status()) - - self.update_status() - - def on_connect_clicked(self, *_): - connected = self.network.get("ssid") == self.client.wifi_device.ssid - if connected: - self.connect_button.set_label("Disconnecting...") - self.client.wifi_device.enabled = False # Disables WiFi to disconnect - else: - self.connect_button.set_label("Connecting...") - self.client.connect_wifi_bssid(self.network.get("bssid")) - - self.connecting = True - self.connect_button.sensitive = False - - def update_status(self): - state = self.client.wifi_device.state - connected = self.network.get("ssid") == self.client.wifi_device.ssid - - if self.connecting: - if state in ["activating"]: - self.connect_button.set_label("Connecting...") - elif state in ["deactivating"]: - self.connect_button.set_label("Disconnecting...") - else: - self.connecting = False - self.connect_button.sensitive = True - - if not self.connecting: - self.connection_icon.set_markup(icons.wifi if connected else icons.wifi_off) - self.connect_button.set_label("Disconnect" if connected else "Connect") - - -class WifiManager(Box): - def __init__(self, **kwargs): - super().__init__( - name="wifi", - spacing=8, - orientation="vertical", - **kwargs, - ) - - self.client = network_client - self.scan_icon = Label(name="wifi-scan-icon", markup=icons.scan) - self.toggle_icon = Label(name="wifi-toggle-icon", markup=icons.wifi_off) - - self.scan_button = Button( - name="wifi-scan", - child=self.scan_icon, - tooltip_text="Scan", - on_clicked=lambda *_: self.client.wifi_device - and self.client.wifi_device.scan(), - ) - - self.toggle_button = Button( - name="wifi-toggle", - tooltip_text="Toggle WiFi", - child=self.toggle_icon, - on_clicked=lambda *_: self.client.wifi_device - and self.client.wifi_device.toggle_wifi(), - ) - - self.available_box = Box(spacing=2, orientation="vertical") - - self.children = [ - CenterBox( - name="wifi-header", - start_children=self.scan_button, - center_children=Label(name="wifi-text", label="WiFi Networks"), - end_children=self.toggle_button, - ), - ScrolledWindow( - name="wifi-available", - v_expand=True, - h_scrollbar_policy="never", - min_content_size=(-1, -1), - child=self.available_box, - ), - ] - - self.client.connect("device-ready", lambda *_: self.on_device_ready()) - - def on_device_ready(self): - if not self.client.wifi_device: - return - - self.update_toggle_icon() - - self.client.wifi_device.connect( - "notify::enabled", - lambda *_: self.update_toggle_icon(), - ) - - self.build_wifi_options() - - def update_toggle_icon(self): - self.toggle_icon.set_markup( - icons.wifi if self.client.wifi_device.enabled else icons.wifi_off - ) - - def build_wifi_options(self): - self.available_box.children = [] - if not self.client.wifi_device: - return - - aps = self.client.wifi_device.access_points - seen_ssids = set() - for ap in aps: - ssid = ap.get("ssid") - if ap.get("ssid") != "Unknown" and ssid not in seen_ssids: - seen_ssids.add(ssid) - self.available_box.add(WifiNetworkSlot(ap, self.client)) diff --git a/modules/launcher/components/window_switcher.py b/modules/launcher/components/window_switcher.py deleted file mode 100644 index c2ed906..0000000 --- a/modules/launcher/components/window_switcher.py +++ /dev/null @@ -1,312 +0,0 @@ -import subprocess -import json -from fabric.widgets.box import Box -from fabric.widgets.label import Label -from fabric.widgets.button import Button -from fabric.widgets.entry import Entry -from fabric.widgets.scrolledwindow import ScrolledWindow -from fabric.widgets.image import Image -from fabric.utils import idle_add, remove_handler, exec_shell_command_async -from utils import IconResolver -from gi.repository import GLib, Gdk, Gtk -import utils.icons as icons -import config.data as data - -class WindowSwitcher(Box): - def __init__(self, **kwargs): - super().__init__( - name="window-switcher", - visible=False, - all_visible=False, - **kwargs, - ) - - self.launcher = kwargs["launcher"] - self.selected_index = -1 # Track the selected item index - self.icon_resolver = IconResolver() - - self.viewport = Box(name="viewport", spacing=4, orientation="v") - self.search_entry = Entry( - name="search-entry", - placeholder="Search Windows...", - h_expand=True, - notify_text=lambda entry, *_: self.handle_search(entry.get_text()), - on_activate=lambda entry, *_: self.handle_search(entry.get_text()), - on_key_press_event=self.on_entry_key_press, - ) - self.search_entry.props.xalign = 0.5 - self.scrolled_window = ScrolledWindow( - name="scrolled-window", - spacing=10, - min_content_size=(450, 305), - max_content_size=(450, 305), - child=self.viewport, - ) - - self.header_box = Box( - name="header_box", - spacing=10, - orientation="h", - children=[ - self.search_entry, - ], - ) - - self.window_switcher_box = Box( - name="window-switcher-box", - spacing=10, - h_expand=True, - orientation="v", - children=[ - self.header_box, - self.scrolled_window, - ], - ) - - self.add(self.window_switcher_box) - self.show_all() - - def close_switcher(self): - """Close the window switcher""" - self.viewport.hide() - self.selected_index = -1 # Reset selection - self.launcher.close() - - def open_switcher(self): - """Open the window switcher and refresh windows""" - self.refresh_windows() - self.search_entry.set_text("") - self.search_entry.grab_focus() - self.viewport.show() - - def refresh_windows(self): - """Get Hyprland windows and populate the viewport""" - remove_handler(self._arranger_handler) if hasattr(self, '_arranger_handler') else None - self.viewport.children = [] - self.selected_index = -1 # Clear selection when viewport changes - - # Get Hyprland windows - windows = self.get_hyprland_windows() - if not windows: - # Create a container box to better center the message - container = Box( - name="no-windows-container", - orientation="v", - h_align="center", - v_align="center", - h_expand=True, - v_expand=True - ) - - # Show a message if no windows - label = Label( - name="no-windows", - markup=icons.window, - h_align="center", - v_align="center", - ) - - container.add(label) - - self.viewport.add(container) - return - - # Add window slots to viewport - for window in windows: - self.viewport.add(self.create_window_slot(window)) - - def get_hyprland_windows(self): - """Get list of windows from Hyprland""" - try: - # Get clients info from Hyprland - result = subprocess.run( - ["hyprctl", "clients", "-j"], - capture_output=True, - text=True, - check=True - ) - clients = json.loads(result.stdout) - - # Filter out special windows and sort by workspace - windows = [] - for client in clients: - if client.get("class") and not client.get("class").startswith("special"): - windows.append(client) - - # Sort windows by workspace number - windows.sort(key=lambda x: int(x.get("workspace", {}).get("id", 0))) - return windows - - except Exception as e: - print(f"Error getting Hyprland windows: {e}") - return [] - - def create_window_slot(self, window): - """Create a button slot for a window""" - # Get window title or class as fallback - title = window.get("title", window.get("class", "Unknown")) - workspace = window.get("workspace", {}).get("id", 0) - window_class = window.get("class", "") - - # Get window icon using IconResolver - icon = self.get_window_icon(window_class) - - # Create the button with window info - button = Button( - name="slot-button", - child=Box( - name="window-slot-box", - orientation="h", - spacing=10, - children=[ - Image( - name="window-icon", - pixbuf=icon, - h_align="start", - ), - Label( - name="workspace-label", - label=f"Workspace {workspace}", - h_align="start", - ), - Label( - name="window-title", - label=title, - ellipsization="end", - v_align="center", - h_align="center", - ), - ], - ), - on_clicked=lambda *_: self.focus_window(window.get("address")), - on_key_press_event=lambda btn, event, addr=window.get("address"): self.on_slot_key_press(btn, event, addr), - ) - return button - - def get_window_icon(self, window_class): - """Get the appropriate icon for a window class using IconResolver""" - try: - # Try to get icon from IconResolver - icon = self.icon_resolver.get_icon_pixbuf(window_class, size=24) - if icon: - return icon - except Exception as e: - print(f"Error resolving icon for {window_class}: {e}") - - # Fallback to default window icon if resolution fails - return icons.window - - def focus_window(self, address): - """Focus a window by its address""" - try: - subprocess.run( - ["hyprctl", "dispatch", "focuswindow", f"address:{address}"], - check=True - ) - self.close_switcher() - except Exception as e: - print(f"Error focusing window: {e}") - - def handle_search(self, query): - """Handle search input""" - if not query: - self.refresh_windows() - return - - # Filter windows based on search query - windows = self.get_hyprland_windows() - filtered_windows = [ - window for window in windows - if query.lower() in window.get("title", "").lower() or - query.lower() in window.get("class", "").lower() - ] - - # Update viewport with filtered results - self.viewport.children = [] - self.selected_index = -1 - - for window in filtered_windows: - self.viewport.add(self.create_window_slot(window)) - - def on_entry_key_press(self, widget, event): - """Handle key presses on the search entry""" - if event.keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter): - if self.selected_index != -1: - children = self.viewport.get_children() - if 0 <= self.selected_index < len(children): - children[self.selected_index].clicked() - return True - elif event.keyval == Gdk.KEY_Down: - self.move_selection(1) - return True - elif event.keyval == Gdk.KEY_Up: - self.move_selection(-1) - return True - elif event.keyval == Gdk.KEY_Escape: - self.close_switcher() - return True - return False - - def on_slot_key_press(self, button, event, address): - """Handle key presses on window slots""" - if event.keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter): - self.focus_window(address) - return True - elif event.keyval == Gdk.KEY_Escape: - self.close_switcher() - return True - return False - - def move_selection(self, direction): - """Move the selection up or down""" - children = self.viewport.get_children() - if not children: - return - - new_index = self.selected_index + direction - if 0 <= new_index < len(children): - self.update_selection(new_index) - - def update_selection(self, new_index): - """Update the selected item""" - # Unselect current - if self.selected_index != -1 and self.selected_index < len(self.viewport.get_children()): - current_button = self.viewport.get_children()[self.selected_index] - current_button.get_style_context().remove_class("selected") - - # Select new - if new_index != -1 and new_index < len(self.viewport.get_children()): - new_button = self.viewport.get_children()[new_index] - new_button.get_style_context().add_class("selected") - self.selected_index = new_index - self.scroll_to_selected(new_button) - else: - self.selected_index = -1 - - def scroll_to_selected(self, button): - """Scroll to ensure the selected button is visible""" - def scroll(): - adj = self.scrolled_window.get_vadjustment() - alloc = button.get_allocation() - if alloc.height == 0: - return False # Retry if allocation isn't ready - - y = alloc.y - height = alloc.height - page_size = adj.get_page_size() - current_value = adj.get_value() - - # Calculate visible boundaries - visible_top = current_value - visible_bottom = current_value + page_size - - if y < visible_top: - # Item above viewport - align to top - adj.set_value(y) - elif y + height > visible_bottom: - # Item below viewport - align to bottom - new_value = y + height - page_size - adj.set_value(new_value) - # No action if already fully visible - return False - GLib.idle_add(scroll) \ No newline at end of file diff --git a/modules/launcher/launcher.py b/modules/launcher/launcher.py deleted file mode 100644 index 4704e75..0000000 --- a/modules/launcher/launcher.py +++ /dev/null @@ -1,201 +0,0 @@ -from fabric.widgets.centerbox import CenterBox -from fabric.widgets.stack import Stack -from fabric.widgets.wayland import WaylandWindow as Window -from fabric.widgets.box import Box -from modules.launcher.components import ( - AppLauncher, - BluetoothConnections, - EmojiPicker, - PowerMenu, - Sh, - WallpaperSelector, - WifiManager, - Calendar, - Dashboard, - Toolbox, - Kanban, - TmuxManager, - WindowSwitcher, - NotificationCenter, - ClipHistory, -) - - -class Launcher(Window): - def __init__(self, **kwargs): - super().__init__( - layer="top", - anchor="center", - keyboard_mode="none", - exclusivity="normal", - visible=False, - all_visible=False, - **kwargs, - ) - - self.dashboard = Dashboard(launcher=self) - self.wallpapers = WallpaperSelector(launcher=self) - self.power = PowerMenu(launcher=self) - self.emoji = EmojiPicker(launcher=self) - self.bluetooth = BluetoothConnections(launcher=self) - self.sh = Sh(launcher=self) - self.wifi = WifiManager() - self.calendar = Calendar() - self.tools = Toolbox(launcher=self) - self.kanban = Kanban() - self.tmux = TmuxManager(launcher=self) - self.window_switcher = WindowSwitcher(launcher=self) - self.notification_center = NotificationCenter(launcher=self) - self.cliphist = ClipHistory(launcher=self) - - # Wrap the dashboard in a Box container - self.dashboard = Box( - name="dashboard", - orientation="h", - spacing=10, - children=[self.dashboard], - ) - self.launcher = AppLauncher(launcher=self) - - self.stack = Stack( - name="launcher-content", - v_expand=True, - h_expand=True, - transition_type="crossfade", - transition_duration=100, - children=[ - self.launcher, - self.wallpapers, - self.power, - self.emoji, - self.cliphist, - self.bluetooth, - self.sh, - self.wifi, - self.calendar, - self.tools, - self.kanban, - self.tmux, - self.window_switcher, - self.notification_center, - ], - ) - - self.launcher_box = CenterBox( - name="launcher", - orientation="v", - start_children=self.stack, - end_children=self.dashboard, - ) - - self.add(self.launcher_box) - self.show_all() - self.hide() - self.add_keybinding("Escape", lambda *_: self.close()) - - def close(self): - self.set_keyboard_mode("none") - self.hide() - - for widget in [ - self.launcher, - self.wallpapers, - self.power, - self.emoji, - self.cliphist, - self.bluetooth, - self.sh, - self.wifi, - self.calendar, - self.tools, - self.kanban, - self.tmux, - self.window_switcher, - self.notification_center, - ]: - if hasattr(widget, "viewport") and widget.viewport: - widget.viewport.hide() - - for style in [ - "launcher", - "wallpapers", - "power", - "emoji", - "cliphist", - "bluetooth", - "sh", - "wifi", - "calendar", - "tools", - "kanban", - "tmux", - "window-switcher", - "notification-center", - ]: - self.stack.remove_style_class(style) - - return True - - def open(self, widget): - widgets = { - "launcher": self.launcher, - "wallpapers": self.wallpapers, - "power": self.power, - "emoji": self.emoji, - "cliphist": self.cliphist, - "bluetooth": self.bluetooth, - "sh": self.sh, - "wifi": self.wifi, - "calendar": self.calendar, - "tools": self.tools, - "kanban": self.kanban, - "tmux": self.tmux, - "window-switcher": self.window_switcher, - "notification-center": self.notification_center, - } - self.set_keyboard_mode("exclusive") - self.show() - - for w in widgets.values(): - w.hide() - self.dashboard.hide() - for style in widgets.keys(): - self.stack.remove_style_class(style) - - if widget in widgets: - self.stack.get_style_context().add_class(widget) - self.stack.set_visible_child(widgets[widget]) - widgets[widget].show() - - if widget == "launcher": - self.launcher.open_launcher() - self.launcher.search_entry.set_text("") - self.launcher.search_entry.grab_focus() - self.dashboard.show() - - elif widget == "wallpapers": - self.wallpapers.search_entry.set_text("") - self.wallpapers.search_entry.grab_focus() - self.wallpapers.viewport.show() - - elif widget == "tmux": - self.tmux.open_manager() - self.tmux.search_entry.set_text("") - self.tmux.search_entry.grab_focus() - - elif widget == "emoji": - self.emoji.open_picker() - self.emoji.search_entry.set_text("") - self.emoji.search_entry.grab_focus() - - elif widget == "cliphist": - self.cliphist.open() - - elif widget == "sh": - self.sh.open_sh() - - elif widget == "window-switcher": - self.window_switcher.open_switcher() - - elif widget == "notification-center": - self.notification_center.open_center() diff --git a/modules/notification_popup.py b/modules/notification_popup.py deleted file mode 100644 index 82dcb5d..0000000 --- a/modules/notification_popup.py +++ /dev/null @@ -1,302 +0,0 @@ -import os -from gi.repository import GdkPixbuf, GLib, Gdk -from fabric.notifications import ( - Notification, - NotificationAction, - NotificationCloseReason, -) -from loguru import logger -from fabric.widgets.box import Box -from fabric.widgets.button import Button -from fabric.widgets.image import Image -from fabric.widgets.centerbox import CenterBox -from fabric.widgets.label import Label -from fabric.widgets.revealer import Revealer -from fabric.widgets.wayland import WaylandWindow as Window -from utils import CustomImage -import utils.icons as icons -from services import notification_service - - -class ActionButton(Button): - def __init__( - self, action: NotificationAction, index: int, total: int, notification_box - ): - super().__init__( - name="action-button", - h_expand=True, - on_clicked=self.on_clicked, - child=Label(name="button-label", label=action.label), - ) - self.action = action - self.notification_box = notification_box - style_class = ( - "start-action" - if index == 0 - else "end-action" - if index == total - 1 - else "middle-action" - ) - self.add_style_class(style_class) - self.connect( - "enter-notify-event", lambda *_: notification_box.hover_button(self) - ) - self.connect( - "leave-notify-event", lambda *_: notification_box.unhover_button(self) - ) - - def on_clicked(self, *_): - self.action.invoke() - self.action.parent.close("dismissed-by-user") - - -class NotificationWidget(Box): - def __init__(self, notification: Notification, timeout_ms=5000, **kwargs): - super().__init__( - name="notification-box", - orientation="v", - h_align="fill", - h_expand=True, - children=[ - self.create_content(notification), - self.create_action_buttons(notification), - ], - ) - self.notification = notification - self.timeout_ms = timeout_ms - self._timeout_id = None - self.start_timeout() - - def create_header(self, notification): - app_icon = ( - Image( - name="notification-icon", - image_file=notification.app_icon[7:], - size=24, - ) - if "file://" in notification.app_icon - else Image( - name="notification-icon", - icon_name="dialog-information-symbolic" or notification.app_icon, - icon_size=24, - ) - ) - - return CenterBox( - name="notification-title", - start_children=[ - Box( - spacing=4, - children=[ - app_icon, - Label( - notification.app_name, - name="notification-app-name", - h_align="start", - ), - ], - ) - ], - end_children=[self.create_close_button()], - ) - - def create_content(self, notification): - return Box( - name="notification-content", - spacing=8, - children=[ - Box( - name="notification-image", - children=CustomImage( - pixbuf=notification.image_pixbuf.scale_simple( - 48, 48, GdkPixbuf.InterpType.BILINEAR - ) - if notification.image_pixbuf - else self.get_pixbuf(notification.app_icon, 48, 48) - ), - ), - Box( - name="notification-text", - orientation="v", - v_align="center", - children=[ - Box( - name="notification-summary-box", - orientation="h", - children=[ - Label( - name="notification-summary", - markup=notification.summary.replace("\n", " "), - h_align="start", - ellipsization="end", - ), - Label( - name="notification-app-name", - markup=" | " + notification.app_name, - h_align="start", - ellipsization="end", - ), - ], - ), - Label( - markup=notification.body.replace("\n", " "), - h_align="start", - ellipsization="end", - ) - if notification.body - else Box(), - ], - ), - Box(h_expand=True), - Box( - orientation="v", - children=[ - self.create_close_button(), - Box(v_expand=True), - ], - ), - ], - ) - - def get_pixbuf(self, icon_path, width, height): - if icon_path.startswith("file://"): - icon_path = icon_path[7:] - - if not os.path.exists(icon_path): - logger.warning(f"Icon path does not exist: {icon_path}") - return None - - try: - pixbuf = GdkPixbuf.Pixbuf.new_from_file(icon_path) - return pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.BILINEAR) - except Exception as e: - logger.error(f"Failed to load or scale icon: {e}") - return None - - def create_action_buttons(self, notification): - return Box( - name="notification-action-buttons", - spacing=4, - h_expand=True, - children=[ - ActionButton(action, i, len(notification.actions), self) - for i, action in enumerate(notification.actions) - ], - ) - - def create_close_button(self): - close_button = Button( - name="notif-close-button", - child=Label(name="notif-close-label", markup=icons.cancel), - on_clicked=lambda *_: self.notification.close("dismissed-by-user"), - ) - close_button.connect( - "enter-notify-event", lambda *_: self.hover_button(close_button) - ) - close_button.connect( - "leave-notify-event", lambda *_: self.unhover_button(close_button) - ) - return close_button - - def start_timeout(self): - self.stop_timeout() - self._timeout_id = GLib.timeout_add(self.timeout_ms, self.close_notification) - - def stop_timeout(self): - if self._timeout_id is not None: - GLib.source_remove(self._timeout_id) - self._timeout_id = None - - def close_notification(self): - self.notification.close("expired") - self.stop_timeout() - return False - - def pause_timeout(self): - self.stop_timeout() - - def resume_timeout(self): - self.start_timeout() - - def destroy(self): - self.stop_timeout() - super().destroy() - - # @staticmethod - def set_pointer_cursor(self, widget, cursor_name): - window = widget.get_window() - if window: - cursor = Gdk.Cursor.new_from_name(widget.get_display(), cursor_name) - window.set_cursor(cursor) - - def hover_button(self, button): - self.pause_timeout() - self.set_pointer_cursor(button, "hand2") - - def unhover_button(self, button): - self.resume_timeout() - self.set_pointer_cursor(button, "arrow") - - -class NotificationRevealer(Revealer): - def __init__(self, notification: Notification, **kwargs): - self.notif_box = NotificationWidget(notification) - self._notification = notification - super().__init__( - child=Box( - children=[self.notif_box], - ), - transition_duration=250, - transition_type="slide-down", - ) - - self.connect( - "notify::child-revealed", - lambda *_: self.destroy() if not self.get_child_revealed() else None, - ) - - self._notification.connect("closed", self.on_resolved) - - def on_resolved( - self, - notification: Notification, - reason: NotificationCloseReason, - ): - self.set_reveal_child(False) - - -class NotificationPopup(Window): - def __init__(self): - self._server = notification_service - self.cache_notification_service = notification_service - self.notifications = Box( - v_expand=True, - h_expand=True, - style="margin: 1px 0px 1px 1px;", - orientation="v", - spacing=5, - ) - self._server.connect("notification-added", self.on_new_notification) - - super().__init__( - anchor="top right", - child=self.notifications, - layer="overlay", - all_visible=True, - visible=True, - exclusive=False, - ) - - def on_new_notification(self, fabric_notif, id): - notification: Notification = fabric_notif.get_notification_from_id(id) - # Always cache the notification regardless of DND status - self.cache_notification_service.cache_notification(notification) - - # Only show popup if DND is not enabled - if not self.cache_notification_service.dont_disturb: - new_box = NotificationRevealer(fabric_notif.get_notification_from_id(id)) - self.notifications.add(new_box) - new_box.set_reveal_child(True) - logger.info(f"[Notification] New notification from {notification.app_name}") - else: - logger.info(f"[Notification] DND enabled, notification from {notification.app_name} not shown") diff --git a/modules/osd.py b/modules/osd.py deleted file mode 100644 index 6d79a60..0000000 --- a/modules/osd.py +++ /dev/null @@ -1,183 +0,0 @@ -import time -from typing import ClassVar - -from fabric.audio import Audio -from fabric.widgets.box import Box -from fabric.widgets.scale import Scale, ScaleMark -from fabric.widgets.wayland import WaylandWindow as Window -from fabric.utils import invoke_repeater -from gi.repository import GObject -from services.brightness import Brightness -from fabric.widgets.label import Label -from utils import Animator - - -class AnimatedScale(Scale): - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.animator = None # Lazily initialized - - def animate_value(self, value: float): - if not self.animator: - self.animator = Animator( - bezier_curve=(0.34, 1.56, 0.64, 1.0), - duration=0.8, - min_value=self.min_value, - max_value=self.max_value, - tick_widget=self, - notify_value=lambda p, *_: self.set_value(p.value), - ) - self.animator.pause() - self.animator.min_value = self.value - self.animator.max_value = min(max(value, self.min_value), self.max_value) - self.animator.play() - - -class BaseOSDContainer(Box): - def __init__(self, label_text, **kwargs): - super().__init__(**kwargs, orientation="v", spacing=8, name="osd") - - self.header = Box(orientation="h") - self.label = Label( - label=label_text, name="osd-label", h_align="start", h_expand=True - ) - self.value_label = Label(name="osd-label", h_align="end") - self.header.add(self.label) - self.header.add(self.value_label) - - self.scale = AnimatedScale( - marks=(ScaleMark(value=i) for i in range(0, 100, 10)), - value=70, - min_value=0, - max_value=100, - increments=(1, 1), - orientation="h", - ) - - self.add(self.header) - self.add(self.scale) - - def update_value_label(self, value): - self.value_label.set_label(str(round(value))) - - -class BrightnessOSDContainer(BaseOSDContainer): - def __init__(self, **kwargs): - super().__init__(label_text="Brightness", **kwargs) - self.brightness_service = Brightness() - self.update_brightness() - - self.scale.connect("value-changed", lambda *_: self.update_brightness()) - self.brightness_service.connect("screen", self.on_brightness_changed) - - def update_brightness(self) -> None: - current_brightness = self.brightness_service.screen_brightness - if current_brightness != 0: - normalized_brightness = self._normalize_brightness(current_brightness) - self.scale.set_value(normalized_brightness) - self.update_value_label(normalized_brightness) - - def on_brightness_changed(self, _sender: any, value: float, *_args) -> None: - normalized_brightness = self._normalize_brightness(value) - self.scale.animate_value(normalized_brightness) - self.update_value_label(normalized_brightness) - - def _normalize_brightness(self, brightness: float) -> float: - normalized = (brightness / self.brightness_service.max_screen) * 100 - return max(0, min(100, normalized)) # Ensure value stays within bounds - - -class AudioOSDContainer(BaseOSDContainer): - __gsignals__: ClassVar[dict] = { - "volume-changed": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ()), - } - - def __init__(self, **kwargs): - super().__init__(label_text="Volume", **kwargs) - self.audio = Audio() - self.sync_with_audio() - - self.scale.connect("value-changed", self.on_volume_changed) - self.audio.connect("notify::speaker", self.on_audio_speaker_changed) - self.audio.connect("speaker-changed", self.on_speaker_changed) - - def sync_with_audio(self): - if self.audio.speaker: - volume = round(self.audio.speaker.volume) - self.scale.set_value( - max(0, min(100, volume)) - ) # Ensure value stays within bounds - self.update_value_label(volume) - - def on_volume_changed(self, *_): - if self.audio.speaker: - volume = self.scale.value - if 0 <= volume <= 100: - self.audio.speaker.set_volume(volume) - self.update_value_label(volume) - self.emit("volume-changed") - - def on_audio_speaker_changed(self, *_): - if self.audio.speaker: - self.audio.speaker.connect("changed", self.update_volume) - self.update_volume() - - def on_speaker_changed(self, *_): - self.update_volume() - - def update_volume(self, *_): - if self.audio.speaker and not self.is_hovered(): - volume = round(self.audio.speaker.volume) - self.scale.set_value( - max(0, min(100, volume)) - ) # Ensure value stays within bounds - self.update_value_label(volume) - - -class OSD(Window): - def __init__(self, **kwargs): - self.audio_container = AudioOSDContainer() - self.brightness_container = BrightnessOSDContainer() - - self.timeout = 1000 - - self.controls_box = Box( - orientation="h", - spacing=20, - children=[self.brightness_container, self.audio_container], - ) - - super().__init__( - layer="overlay", - anchor="top", - child=self.controls_box, - visible=False, - pass_through=True, - keyboard_mode="on-demand", - **kwargs, - ) - - self.last_activity_time = time.time() - - self.audio_container.audio.connect("notify::speaker", self.show_controls) - self.brightness_container.brightness_service.connect( - "screen", self.show_controls - ) - self.audio_container.connect("volume-changed", self.show_controls) - - invoke_repeater(100, self.check_inactivity, initial_call=True) - - def show_controls(self, *_): - self.set_visible(True) - self.reset_inactivity_timer() - - def start_hide_timer(self): - self.set_visible(False) - - def reset_inactivity_timer(self): - self.last_activity_time = time.time() - - def check_inactivity(self): - if time.time() - self.last_activity_time >= (self.timeout / 1000): - self.start_hide_timer() - return True diff --git a/config/scripts/capture.sh b/scripts/capture.sh similarity index 53% rename from config/scripts/capture.sh rename to scripts/capture.sh index 8204108..465e038 100755 --- a/config/scripts/capture.sh +++ b/scripts/capture.sh @@ -1,3 +1,4 @@ + #!/bin/bash # Print usage information @@ -19,16 +20,28 @@ Modes: EOF } -# OCR functionality +# OCR functionality using hyprshot do_ocr() { sleep 0.25 - ocr_text=$(grimblast --freeze save area - | tesseract -l eng - - 2>/dev/null) + # Temporary file to store the snipped image + temp_ocr_image="$(mktemp --suffix=.png)" + hyprshot -s -z -m region -o "$(dirname "$temp_ocr_image")" -f "$(basename "$temp_ocr_image")" + + if [ ! -f "$temp_ocr_image" ]; then + notify-send -a "Modus" "OCR Failed" "No image captured" + exit 1 + fi + + # Perform OCR with tesseract + ocr_text=$(tesseract "$temp_ocr_image" - -l eng 2>/dev/null) + + rm -f "$temp_ocr_image" # Clean up temp file # Check if OCR was successful if [[ -n "$ocr_text" ]]; then echo -n "$ocr_text" | wl-copy - notify-send -a "Modus" "OCR Success" "Text Copied to Clipboard" + notify-send -a "Modus" "OCR Success" "Text copied to clipboard" else notify-send -a "Modus" "OCR Failed" "No text recognized or operation failed" fi @@ -36,10 +49,8 @@ do_ocr() { # Screen recording functionality do_screenrecord() { - if [ -z "$XDG_VIDEOS_DIR" ]; then XDG_VIDEOS_DIR="$HOME/Videos" - fi local save_dir="${XDG_VIDEOS_DIR:-$HOME/Videos}/Recordings" mkdir -p "$save_dir" @@ -49,8 +60,7 @@ do_screenrecord() { sleep 1 local last_video=$(ls -t "$save_dir"/*.mp4 2>/dev/null | head -n 1) - local action=$(notify-send -a "Modus" "⬜ Recording stopped" \ - -A "view=View" -A "open=Open folder") + local action=$(notify-send -a "Modus" "⬜ Recording stopped" -A "view=View" -A "open=Open folder") if [ "$action" = "view" ] && [ -n "$last_video" ]; then xdg-open "$last_video" @@ -81,10 +91,10 @@ do_screenshot() { local mockup_mode="$2" case $1 in - p) grimblast copysave screen "$full_path" ;; - s) grimblast copysave area "$full_path" ;; - sf) grimblast --freeze copysave area "$full_path" ;; - m) grimblast copysave output "$full_path" ;; + p) hyprshot -s -m output -o "$save_dir" -f "$save_file" ;; + s) hyprshot -s -m region -o "$save_dir" -f "$save_file" ;; + sf) hyprshot -s -z -m region -o "$save_dir" -f "$save_file" ;; + m) hyprshot -s -m output -m active -o "$save_dir" -f "$save_file" ;; *) echo "Invalid screenshot mode" print_usage @@ -93,30 +103,7 @@ do_screenshot() { esac if [ -f "$full_path" ]; then - if [ "$mockup_mode" = "mockup" ]; then - local temp_file="${full_path%.png}_temp.png" - cropped_file="${full_path%.png}_cropped.png" - local mockup_file="${full_path%.png}_mockup.png" - - # First crop the top pixel using a different approach - - # Use +0+1 to start at y-coordinate 1 (skipping the first pixel row) - - magick "$full_path" -crop +0+1 +repage "$cropped_file" - - magick "$cropped_file" \ - \( +clone -alpha extract -draw 'fill black polygon 0,0 0,20 20,0 fill white circle 20,20 20,0' \ - \( +clone -flip \) -compose Multiply -composite \ - \( +clone -flop \) -compose Multiply -composite \ - \) -alpha off -compose CopyOpacity -composite "$temp_file" - - magick "$temp_file" \ - \( +clone -background black -shadow 60x20+0+10 -alpha set -channel A -evaluate multiply 1 +channel \) \ - +swap -background none -layers merge +repage "$mockup_file" - - rm "$temp_file" "$cropped_file" - mv "$mockup_file" "$full_path" - + if [ "$mockup_mode" != "mockup" ]; then if command -v wl-copy >/dev/null 2>&1; then wl-copy <"$full_path" elif command -v xclip >/dev/null 2>&1; then @@ -124,6 +111,54 @@ do_screenshot() { fi fi + # Process as mockup if requested + if [ "$mockup_mode" = "mockup" ]; then + temp_file="${full_path%.png}_temp.png" + cropped_file="${full_path%.png}_cropped.png" + mockup_file="${full_path%.png}_mockup.png" + mockup_success=true + + # Crop top pixel + convert "$full_path" -crop +0+1 +repage "$cropped_file" || mockup_success=false + + # Rounded corners and transparency + if [ "$mockup_success" = true ]; then + convert "$cropped_file" \ + \( +clone -alpha extract -draw 'fill black polygon 0,0 0,20 20,0 fill white circle 20,20 20,0' \ + \( +clone -flip \) -compose Multiply -composite \ + \( +clone -flop \) -compose Multiply -composite \ + \) -alpha off -compose CopyOpacity -composite "$temp_file" || mockup_success=false + fi + + # Add shadow + if [ "$mockup_success" = true ]; then + convert "$temp_file" \ + \( +clone -background black -shadow 60x20+0+10 -alpha set -channel A -evaluate multiply 1 +channel \) \ + +swap -background none -layers merge +repage "$mockup_file" || mockup_success=false + fi + + # Finalize + if [ "$mockup_success" = true ] && [ -f "$mockup_file" ]; then + rm "$temp_file" "$cropped_file" + mv "$mockup_file" "$full_path" + if command -v wl-copy >/dev/null 2>&1; then + wl-copy <"$full_path" + elif command -v xclip >/dev/null 2>&1; then + xclip -selection clipboard -t image/png <"$full_path" + fi + else + echo "Warning: Mockup processing failed for $full_path." >&2 + rm -f "$temp_file" "$cropped_file" "$mockup_file" + if [ "$mockup_mode" = "mockup" ]; then + if command -v wl-copy >/dev/null 2>&1; then + wl-copy <"$full_path" + elif command -v xclip >/dev/null 2>&1; then + xclip -selection clipboard -t image/png <"$full_path" + fi + fi + fi + fi + local action=$(notify-send -a "Modus" -i "$full_path" "Screenshot saved" "in $full_path" \ -A "view=View" -A "edit=Edit" -A "open=Open Folder") diff --git a/config/scripts/gamemode.sh b/scripts/gamemode.sh similarity index 100% rename from config/scripts/gamemode.sh rename to scripts/gamemode.sh diff --git a/config/scripts/hyprpicker.sh b/scripts/hyprpicker.sh similarity index 100% rename from config/scripts/hyprpicker.sh rename to scripts/hyprpicker.sh diff --git a/config/scripts/systemupdates.sh b/scripts/systemupdates.sh similarity index 100% rename from config/scripts/systemupdates.sh rename to scripts/systemupdates.sh diff --git a/services/__init__.py b/services/__init__.py index cdba666..3f03f12 100644 --- a/services/__init__.py +++ b/services/__init__.py @@ -1,10 +1,4 @@ -from fabric.audio import Audio -from .brightness import Brightness -from .network import NetworkClient -from .notification import * -from .powerprofile import * - -network_client = NetworkClient() -brightness = Brightness() -audio = Audio() -notification_service = CustomNotifications() +""" +Modus services package. +Contains background services and utilities for the shell. +""" diff --git a/services/battery.py b/services/battery.py new file mode 100644 index 0000000..e0306e0 --- /dev/null +++ b/services/battery.py @@ -0,0 +1,104 @@ +from fabric.core import Service, Property, Signal +from pydbus import SystemBus + +DeviceState = { + 0: "UNKNOWN", + 1: "CHARGING", + 2: "DISCHARGING", + 3: "EMPTY", + 4: "FULLY_CHARGED", + 5: "PENDING_CHARGE", + 6: "PENDING_DISCHARGE", +} + + +class Battery(Service): + @staticmethod + def seconds_to_hours_minutes(seconds): + hours = seconds // 3600 + minutes = (seconds % 3600) // 60 + return f"{hours}h {minutes}m" if hours else f"{minutes}m" + + @Signal + def changed(self) -> None: ... + + @Signal + def profile_changed(self, value: str) -> None: ... + + @Property(int, "readable") + def percentage(self): + return int(self._battery.Percentage) + + @Property(str, "readable") + def temperature(self): + return f"{self._battery.Temperature}°C" if hasattr(self._battery, "Temperature") else "N/A" + + @Property(str, "readable") + def time_to_empty(self): + return self.seconds_to_hours_minutes(getattr(self._battery, "TimeToEmpty", 0)) + + @Property(str, "readable") + def time_to_full(self): + return self.seconds_to_hours_minutes(getattr(self._battery, "TimeToFull", 0)) + + @Property(str, "readable") + def icon_name(self): + return self._battery.IconName + + @Property(str, "readable") + def state(self): + return DeviceState.get(self._battery.State, "UNKNOWN") + + @Property(str, "readable") + def capacity(self): + return f"{self._battery.Capacity}%" + + @Property(bool, "readable", default_value=False) + def is_present(self): + return self._battery.IsPresent + + @Property(str, "readable") + def power_profile(self): + return self._profile_proxy.ActiveProfile + + def set_power_profile(self, profile: str) -> bool: + if profile not in [p.Profile for p in self._profile_proxy.Profiles]: + return False + try: + self._profile_proxy.ActiveProfile = profile + self.profile_changed.emit(profile) + self.changed.emit() + return True + except Exception as e: + return False + + def __init__(self): + super().__init__() + self._bus = SystemBus() + + # Battery device + try: + self._battery = self._bus.get("org.freedesktop.UPower", "/org/freedesktop/UPower/devices/battery_BAT0") + self._battery.onPropertiesChanged = self.handle_battery_change + except Exception as e: + return + + # PowerProfiles + try: + self._profile_proxy = self._bus.get("net.hadess.PowerProfiles", "/net/hadess/PowerProfiles") + # Use onPropertiesChanged for consistency with battery device + self._profile_proxy.onPropertiesChanged = lambda _, changed, __: self._handle_profile_props_changed(changed) + except Exception as e: + self._profile_proxy = None + + self.changed.emit() + + def _handle_profile_props_changed(self, changed): + """Internal handler for property changes that processes only the changed properties""" + if "ActiveProfile" in changed: + new_profile = changed["ActiveProfile"] + self.profile_changed.emit(new_profile) + self.changed.emit() + + def handle_battery_change(self, iface, changed, invalidated): + self.changed.emit() diff --git a/services/brightness.py b/services/brightness.py index 49e7040..62179cf 100644 --- a/services/brightness.py +++ b/services/brightness.py @@ -1,92 +1,82 @@ import os -import subprocess from fabric.core.service import Property, Service, Signal -from fabric.utils import exec_shell_command, monitor_file +from fabric.utils import exec_shell_command_async, monitor_file from gi.repository import GLib -from loguru import logger # Import loguru logger +from loguru import logger + + -# Helper function to execute brightnessctl asynchronously def exec_brightnessctl_async(args: str): - # Executes brightnessctl command asynchronously, ensuring no resource leaks. - try: - # Use subprocess.Popen to run the command without blocking - process = subprocess.Popen( - f"brightnessctl {args}", - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) + if not helpers.executable_exists("brightnessctl"): + logger.error(f"Command brightnessctl not found") - # Wait for the process to complete - stdout, stderr = process.communicate() - - if process.returncode != 0: - logger.error(f"Error executing brightnessctl: {stderr.decode().strip()}") - else: - logger.debug( - f"brightnessctl output: {stdout.decode().strip()}" - ) # Optional: Log the output - except Exception as e: - logger.exception(f"Exception in exec_brightnessctl_async: {e}") + exec_shell_command_async(f"brightnessctl {args}", lambda _: None) # Discover screen backlight device try: - screen_device = str(exec_shell_command("ls -w1 /sys/class/backlight")).split("\n")[ - 0 - ] -except IndexError: - screen_device = None - - -class NoBrightnessError(ImportError): - # Raised when brightness-related dependencies are missing. - def __init__(self, *args): - super().__init__( - "Brightness control not available or missing dependencies", *args - ) + 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" + ) + screen_device = "" class Brightness(Service): - # Service to manage screen brightness levels. + """Service to manage screen brightness levels.""" + + instance = None + + @staticmethod + def get_initial(): + if Brightness.instance is None: + Brightness.instance = Brightness() + + return Brightness.instance @Signal def screen(self, value: int) -> None: """Signal emitted when screen brightness changes.""" - pass # Implement as needed for your application + # Implement as needed for your application def __init__(self, **kwargs): super().__init__(**kwargs) - if not screen_device: - raise NoBrightnessError() - # Path for screen backlight control self.screen_backlight_path = f"/sys/class/backlight/{screen_device}" # Initialize maximum brightness level - self.max_screen = self._read_max_brightness(self.screen_backlight_path) + self.max_screen = self.do_read_max_brightness(self.screen_backlight_path) + + if screen_device == "": + return # Monitor screen brightness file self.screen_monitor = monitor_file(f"{self.screen_backlight_path}/brightness") + self.screen_monitor.connect( "changed", lambda _, file, *args: self.emit( - "screen", round(int(file.load_bytes()[0].get_data())) + "screen", + round(int(file.load_bytes()[0].get_data())), ), ) # 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 _read_max_brightness(self, path: str) -> int: + def do_read_max_brightness(self, path: str) -> int: # Reads the maximum brightness value from the specified path. max_brightness_path = os.path.join(path, "max_brightness") if os.path.exists(max_brightness_path): with open(max_brightness_path) as f: - return int(f.read().strip()) + return int(f.readline()) return -1 # Return -1 if file doesn't exist, indicating an error. @Property(int, "read-write") @@ -95,8 +85,10 @@ class Brightness(Service): brightness_path = os.path.join(self.screen_backlight_path, "brightness") if os.path.exists(brightness_path): with open(brightness_path) as f: - return int(f.read().strip()) - logger.warning(f"Brightness file does not exist: {brightness_path}") + return int(f.readline()) + logger.warning( + f"Brightness file does not exist: {brightness_path}" + ) return -1 # Return -1 if file doesn't exist, indicating error. @screen_brightness.setter @@ -108,8 +100,11 @@ 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} (out of {self.max_screen})") + logger.info( + f"Set screen brightness to {value} " + f"(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}") + logger.exception(f"Unexpected error setting screen brightness: {e}") \ No newline at end of file diff --git a/services/network.py b/services/network.py index 04ebcae..3058ba4 100644 --- a/services/network.py +++ b/services/network.py @@ -1,16 +1,287 @@ -from typing import Any, List, Literal - +from gi.repository import NM, GLib import gi +from typing import List, Optional from fabric.core.service import Property, Service, Signal -from fabric.utils import bulk_connect, exec_shell_command_async -from gi.repository import Gio +from fabric.utils import bulk_connect, get_enum_member_name, snake_case_to_kebab_case from loguru import logger -try: - gi.require_version("NM", "1.0") - from gi.repository import NM -except ValueError: - logger.error("Failed to start network manager") +gi.require_version("NM", "1.0") # Ensure the correct version is loaded + + +class NetworkClient(Service): + """A service to manage network devices""" + + @Signal + def wifi_device_added(self) -> None: ... + + @Signal + def ethernet_device_added(self) -> None: ... + + @Signal + def wifi_device_removed(self) -> None: ... + + @Signal + def ethernet_device_removed(self) -> None: ... + + @Signal + def changed(self) -> None: ... + + @Signal + def ready(self) -> None: ... + + @Property(bool, "readable", default_value=False) + def is_ready(self) -> bool: + """Returns whether the client is ready to use.""" + return self._client is not None + + @Property(list, "readable") + def connections(self) -> Optional[list]: + """Returns the active connections, if available.""" + return self._client.get_property("connections") if self._client else None + + @Property(object, "readable") + def wifi_device(self) -> Optional[object]: + """Returns the WiFi device if available.""" + return self._wifi_device + + @Property(object, "readable") + def ethernet_device(self) -> Optional[object]: + """Returns the Ethernet device if available.""" + return self._ethernet_device + + @Property(str, "readable") + def primary_connection(self) -> Optional[str]: + """Returns the primary connection if available.""" + return self._client.get_property("primary_connection") if self._client else None + + @Property(str, "readable") + def active_connection(self) -> Optional[str]: + """Returns the active connection if available.""" + return self._client.get_property("active_connection") if self._client else None + + @Property(str, "readable") + def state(self) -> str: + """Returns the current network state.""" + if not self._client: + return "disconnected" + return snake_case_to_kebab_case( + get_enum_member_name(self._client.get_property( + "state"), default="disconnected") + ) + + @Property(str, "readable") + def connectivity(self) -> str: + """Returns the connectivity state.""" + if not self._client: + return "disconnected" + return snake_case_to_kebab_case( + get_enum_member_name(self._client.get_property( + "connectivity"), default="disconnected") + ) + + @Property(list, "readable") + def devices(self) -> Optional[list]: + """Returns the list of network devices if available.""" + return self._client.get_property("devices") if self._client else None + + @Property(str, "readable") + def hostname(self) -> Optional[str]: + """Returns the hostname if available.""" + return self._client.get_property("hostname") if self._client else None + + @Property(bool, "read-write", default_value=False) + def networking_enabled(self) -> bool: + """Checks if networking is enabled.""" + return self._client.get_property("networking_enabled") if self._client else False + + @networking_enabled.setter + def networking_enabled(self, value: bool): + """Sets the networking state if the client is available.""" + if self._client: + self._client.set_property("networking_enabled", value) + + @Property(bool, "read-write", default_value=False) + def wireless_enabled(self) -> bool: + """Checks if wireless networking is enabled.""" + return self._client.get_property("wireless_enabled") if self._client else False + + @wireless_enabled.setter + def wireless_enabled(self, value: bool): + """Sets the wireless networking state if the client is available.""" + if self._client: + self._client.set_property("wireless_enabled", value) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self._client: NM.Client = None + self._wifi_device: Wifi | None = None + self._ethernet_device: Ethernet | None = None + + logger.info("[Network] Initializing client asynchronously...") + + # Start async NM client initialization + NM.Client.new_async(None, self.on_client_ready) + + def on_client_ready(self, source, result): + """Callback when NM.Client is ready.""" + try: + self._client = NM.Client.new_finish( + result) # Retrieve client instance + logger.info("[Network] NM.Client initialized successfully!") + + # Connect signals + bulk_connect( + self._client, + { + "device-added": lambda _, device: self.on_device_added(device=device), + "device-removed": lambda _, device: self.on_device_removed(device=device), + "notify::state": lambda *args: self.notifier('state'), + "notify::networking-enabled": lambda *args: self.notifier('networking-enabled'), + "notify::wireless-enabled": lambda *args: self.notifier('wireless-enabled'), + # "notify::primary-connection": lambda *args: self.notifier('primary-connection'), + # "notify::active-connection": lambda *args: self.notifier('active-connection'), + # "active-connection-added": lambda *args: self.emit("changed"), + # "active-connection-removed": lambda *args: self.emit("changed") + }, + ) + + # Process devices AFTER client is ready + for device in self.do_get_raw_devices(): + self.on_device_added(device=device) + + self.notify('state') + self.notify('networking-enabled') + self.notify('wireless-enabled') + self.notify('is-ready') + self.ready.emit() + + except Exception as e: + logger.error(f"[Network] Error initializing NM.Client: {e}") + + def do_get_raw_devices(self) -> list[NM.Device]: + return [ + dev + for dev in self.devices + if dev.get_device_type() in (NM.DeviceType.WIFI, NM.DeviceType.ETHERNET) + ] + + def on_device_added(self, device): + device_type = device.get_device_type() + if device_type == NM.DeviceType.WIFI and not self._wifi_device: + logger.info("[Network] WiFi device detected, initializing...") + self._wifi_device = Wifi(client=self, device=device) + self.wifi_device_added.emit() + + elif device_type == NM.DeviceType.ETHERNET and not self._ethernet_device: + logger.info("[Network] Ethernet device detected, initializing...") + self._ethernet_device = Ethernet(client=self, device=device) + self.ethernet_device_added.emit() + + def on_device_removed(self, device): + if device == self._wifi_device: + logger.info("[Network] WiFi device removed.") + self._wifi_device = None + self.wifi_device_removed.emit() + + elif device == self._ethernet_device: + logger.info("[Network] Ethernet device removed.") + self._ethernet_device = None + self.ethernet_device_removed.emit() + + def toggle_network(self): + """Enable or disable Network""" + self.networking_enabled = not self.networking_enabled + + def deactivate_connection(self, connection): + """ Disconnect """ + self._client.deactivate_connection_async( + connection, None, None + ) + + def notifier(self, name): + self.notify(name) + self.emit("changed") + + +class AccessPoint(Service): + """A service to manage access points""" + @Signal + def changed(self) -> None: ... + + @Property(object, "readable") + def device(self) -> object: + return self._device + + @Property(int, "readable") + def strength(self) -> int: + return self._ap.get_property('strength') + + @Property(int, "readable") + def frequency(self) -> int: + return self._ap.get_property("frequency") + + @Property(str, "readable") + def bssid(self) -> str: + return self._ap.get_property("bssid") if self._ap else None + + @Property(str, "readable") + def hw_address(self) -> str: + return self._ap.get_property("hw_address") + + @Property(str, "readable") + def ssid(self) -> str: + ssid = self._ap.get_ssid() + return NM.utils_ssid_to_utf8(ssid.get_data()) if ssid else "Unknown" + + @Property(str, "readable") + def icon(self) -> str: + return { + 80: "network-wireless-signal-excellent-symbolic", + 60: "network-wireless-signal-good-symbolic", + 40: "network-wireless-signal-ok-symbolic", + 20: "network-wireless-signal-weak-symbolic", + 00: "network-wireless-signal-none-symbolic", + }.get( + min(80, 20 * round(self.strength / 20)), + "network-wireless-no-route-symbolic", + ) + + @Property(bool, "readable", default_value=False) + def requires_password(self) -> bool: + ssid = self.ssid + settings = self._client.connections + connection = None + for setting in settings: + wifi_setting = setting.get_setting_wireless() + if wifi_setting and NM.utils_ssid_to_utf8(wifi_setting.get_ssid().get_data()) == ssid: + connection = setting + break + if not connection: + return bool(self._ap.get_wpa_flags() or self._ap.get_rsn_flags()) + return False + + @Property(bool, "readable", default_value=False) + def is_active(self) -> bool: + if self._device.active_access_point: + return self.bssid == self._device.active_access_point.get_bssid() + return False + + def __init__(self, device: "Wifi", ap: NM.AccessPoint, **kwargs): + super().__init__(**kwargs) + self._client: NetworkClient = device.client + self._device: Wifi = device + self._ap: NM.AccessPoint = ap + + self._ap.connect("notify::strength", + lambda *args: self.notifier("strength")) + self._device.connect("notify::active-access-point", + lambda *args: self.notifier("is-active")) + + def notifier(self, name: str, *args): + self.notify(name) + self.emit("changed") + return class Wifi(Service): @@ -20,186 +291,232 @@ class Wifi(Service): def changed(self) -> None: ... @Signal - def enabled(self) -> bool: ... + def ap_added(self, ap: AccessPoint) -> None: ... @Signal - def scanning(self, is_scanning: bool) -> None: ... + def ap_removed(self, ap: AccessPoint) -> None: ... - def __init__(self, client: NM.Client, device: NM.DeviceWifi, **kwargs): - self._client: NM.Client = client - self._device: NM.DeviceWifi = device - self._ap: NM.AccessPoint | None = None - self._ap_signal: int | None = None + @Property(NetworkClient, "readable") + def client(self) -> NetworkClient: + """Returns the client """ + return self._client + + @Property(bool, "read-write", default_value=False) + def wireless_enabled(self) -> bool: + """Returns if the wifi is enabled""" + return self._client.get_property("wireless_enabled") + + @wireless_enabled.setter + def wireless_enabled(self, value: bool): + return self._client.set_property("wireless_enabled", value) + + @Property(list[AccessPoint], "readable") + def access_points(self) -> list[AccessPoint]: + return sorted(self._access_points.values(), key=lambda x: x.is_active, reverse=True) + + @Property(AccessPoint, "readable") + def active_access_point(self) -> Optional[AccessPoint]: + return self._active_access_point + + def __init__(self, client: NetworkClient, device: NM.DeviceWifi, **kwargs): super().__init__(**kwargs) + self._client: NetworkClient = client + self._device: NM.DeviceWifi = device + self._active_access_point: NM.AccessPoint | None = None + self._access_points: dict[str, AccessPoint] = {} - self._client.connect( - "notify::wireless-enabled", - lambda *args: self.notifier("enabled", args), + bulk_connect( + self._device, + { + "notify::active-access-point": lambda *args: self.on_access_point_activated(), + "access-point-added": lambda _, ap: self.on_access_point_added(ap=ap), + "access-point-removed": lambda _, ap: self.on_access_point_removed(ap=ap), + # "state-changed": lambda device, new, old, reason: self.on_state_changed(new), + }, ) - if self._device: - bulk_connect( - self._device, - { - "notify::active-access-point": lambda *args: self._activate_ap(), - "access-point-added": lambda *args: self.emit("changed"), - "access-point-removed": lambda *args: self.emit("changed"), - "state-changed": lambda *args: self.ap_update(), - }, - ) - self._activate_ap() - def ap_update(self): + self._client.connect("notify::wireless-enabled", + lambda *args: self.notifier('wireless-enabled')) + + for ap in self.do_get_access_points(): + self.on_access_point_added(ap=ap) + + self.on_access_point_activated() + + def on_state_changed(self, state): self.emit("changed") - for sn in [ - "enabled", - "internet", - "strength", - "frequency", - "access-points", - "ssid", - "state", - "icon-name", - ]: - self.notify(sn) - def _activate_ap(self): - if self._ap: - self._ap.disconnect(self._ap_signal) - self._ap = self._device.get_active_access_point() - if not self._ap: - return + def do_get_access_points(self): + return self._device.get_access_points() - self._ap_signal = self._ap.connect( - "notify::strength", lambda *args: self.ap_update() - ) # type: ignore + def on_access_point_added(self, ap): + ssid = ap.get_ssid() + ssid = NM.utils_ssid_to_utf8(ssid.get_data()) if ssid else "Unknown" - def toggle_wifi(self): - self._client.wireless_set_enabled(not self._client.wireless_get_enabled()) + access_point: AccessPoint = AccessPoint( + ap=ap, + device=self + ) - # def set_active_ap(self, ap): - # self._device.access + self._access_points[ssid] = access_point + + self.ap_added.emit(access_point) + + # self.notifier("access-points") + + logger.info(f"[Wifi] New access point added with ssid: {ssid}") + + def on_access_point_removed(self, ap): + ssid = ap.get_ssid() + ssid = NM.utils_ssid_to_utf8(ssid.get_data()) if ssid else "Unknown" + + if not (access_point := self._access_points.pop(ssid, None)): + return logger.warning( + f"[Network] tried to remove a unknwon access point with ssid {ssid}" + ) + + self.ap_removed.emit(access_point) + + logger.info(f"[Wifi] Access point with ssid: {ssid} removed.") + + def on_access_point_activated(self): + if self._device.get_active_access_point(): + self._active_access_point: AccessPoint = AccessPoint( + ap=self._device.get_active_access_point(), + device=self + ) + + else: + self._active_access_point = None + + self.notifier("active-access-point") + + logger.info("[Wifi] New active connection") + + def disconnect_wifi(self): + """Disconnect from the current WiFi network.""" + active_connection = self._device.get_active_connection() + self._client.deactivate_connection(active_connection) + logger.info("[Wifi] Wifi network disconnected") def scan(self): - """Start scanning for WiFi networks and emit scanning signal""" - if self._device: - self.emit("scanning", True) # Emit signal that scanning has started - self._device.request_scan_async( + self._device.request_scan_async( + None, + lambda device, result: [ + device.request_scan_finish(result), + self.notifier('access-points'), + ], + ) + logger.info("[Wifi] Scan started") + + def toggle_wifi(self): + """Enable or disable WiFi""" + self.wireless_enabled = not self.wireless_enabled + + def connect_to_wifi(self, ap: AccessPoint, password: str = None): + """Connect to a WiFi network.""" + + ssid = ap.ssid + + if ssid == "Unknown": + logger.error("Invalid access point data") + return False + + logger.info(f"Connecting to WiFi SSID: {ssid}") + + # Check for existing connections + settings = self._client.connections + connection = None + + for setting in settings: + wifi_setting = setting.get_setting_wireless() + if wifi_setting and NM.utils_ssid_to_utf8(wifi_setting.get_ssid().get_data()) == ssid: + connection = setting + break + + if not connection: + # Create a new connection profile + logger.info(f"Creating new WiFi connection for SSID '{ssid}'") + connection = NM.SimpleConnection.new() + + # Required connection settings + s_con = NM.SettingConnection.new() + s_con.set_property(NM.SETTING_CONNECTION_ID, ssid) + s_con.set_property(NM.SETTING_CONNECTION_TYPE, "802-11-wireless") + s_con.set_property(NM.SETTING_CONNECTION_INTERFACE_NAME, + self._device.get_iface()) # Set interface name + connection.add_setting(s_con) + + # Wireless settings + s_wifi = NM.SettingWireless.new() + s_wifi.set_property(NM.SETTING_WIRELESS_SSID, + GLib.Bytes.new(ssid.encode())) + s_wifi.set_property(NM.SETTING_WIRELESS_MODE, + "infrastructure") # Ensure mode is correct + connection.add_setting(s_wifi) + + # Security settings (only if password is required and provided) + if ap.requires_password: + if not password: + logger.error("Password required but not provided") + return False + + s_sec = NM.SettingWirelessSecurity.new() + s_sec.set_property( + NM.SETTING_WIRELESS_SECURITY_KEY_MGMT, "wpa-psk") + s_sec.set_property(NM.SETTING_WIRELESS_SECURITY_PSK, password) + connection.add_setting(s_sec) + + # IPv4 settings + s_ipv4 = NM.SettingIP4Config.new() + s_ipv4.set_property("method", "auto") + connection.add_setting(s_ipv4) + + # IPv6 settings + s_ipv6 = NM.SettingIP6Config.new() + s_ipv6.set_property("method", "auto") + connection.add_setting(s_ipv6) + + # Callback for async connection + def on_connection_added(client, result, ssid, device): + try: + new_connection = client.add_connection_finish(result) + if not new_connection: + logger.error( + f"Failed to create connection for '{ssid}'") + return + + logger.info(f"Connection for '{ssid}' added successfully") + + client.activate_connection_async( + new_connection, device, None, None, + lambda c, r: logger.info(f"Connected to '{ssid}'") if c else logger.error( + f"Failed to connect to '{ssid}'") + ) + except Exception as e: + logger.error(f"Failed to add connection: {e}") + + # Save the new connection + self._client._client.add_connection_async( + connection, + True, None, - lambda device, result: [ - device.request_scan_finish(result), - self.emit( - "scanning", False - ), # Emit signal that scanning has stopped - ], + lambda c, r: on_connection_added(c, r, ssid, self._device) ) + # Activate the connection + self._client._client.activate_connection_async( + connection, self._device, None, None, + lambda client, result: logger.info(f"Connected to '{ssid}'") + ) + + return True + def notifier(self, name: str, *args): self.notify(name) self.emit("changed") return - @Property(bool, "read-write", default_value=False) - def enabled(self) -> bool: # type: ignore - return bool(self._client.wireless_get_enabled()) - - @enabled.setter - def enabled(self, value: bool): - self._client.wireless_set_enabled(value) - - @Property(int, "readable") - def strength(self): - return self._ap.get_strength() if self._ap else -1 - - @Property(str, "readable") - def icon_name(self): - if not self._ap: - return "network-wireless-disabled-symbolic" - - if self.internet == "activated": - return { - 80: "network-wireless-signal-excellent-symbolic", - 60: "network-wireless-signal-good-symbolic", - 40: "network-wireless-signal-ok-symbolic", - 20: "network-wireless-signal-weak-symbolic", - 00: "network-wireless-signal-none-symbolic", - }.get( - min(80, 20 * round(self._ap.get_strength() / 20)), - "network-wireless-no-route-symbolic", - ) - if self.internet == "activating": - return "network-wireless-acquiring-symbolic" - - return "network-wireless-offline-symbolic" - - @Property(int, "readable") - def frequency(self): - return self._ap.get_frequency() if self._ap else -1 - - @Property(int, "readable") - def internet(self): - return { - NM.ActiveConnectionState.ACTIVATED: "activated", - NM.ActiveConnectionState.ACTIVATING: "activating", - NM.ActiveConnectionState.DEACTIVATING: "deactivating", - NM.ActiveConnectionState.DEACTIVATED: "deactivated", - }.get( - self._device.get_active_connection().get_state(), - "unknown", - ) - - @Property(object, "readable") - def access_points(self) -> List[object]: - points: list[NM.AccessPoint] = self._device.get_access_points() - - def make_ap_dict(ap: NM.AccessPoint): - return { - "bssid": ap.get_bssid(), - # "address": ap.get_ - "last_seen": ap.get_last_seen(), - "ssid": NM.utils_ssid_to_utf8(ap.get_ssid().get_data()) - if ap.get_ssid() - else "Unknown", - "active-ap": self._ap, - "strength": ap.get_strength(), - "frequency": ap.get_frequency(), - "icon-name": { - 80: "network-wireless-signal-excellent-symbolic", - 60: "network-wireless-signal-good-symbolic", - 40: "network-wireless-signal-ok-symbolic", - 20: "network-wireless-signal-weak-symbolic", - 00: "network-wireless-signal-none-symbolic", - }.get( - min(80, 20 * round(ap.get_strength() / 20)), - "network-wireless-no-route-symbolic", - ), - } - - return list(map(make_ap_dict, points)) - - @Property(str, "readable") - def ssid(self): - if not self._ap: - return "Disconnected" - ssid = self._ap.get_ssid().get_data() - return NM.utils_ssid_to_utf8(ssid) if ssid else "Unknown" - - @Property(int, "readable") - def state(self): - return { - NM.DeviceState.UNMANAGED: "unmanaged", - NM.DeviceState.UNAVAILABLE: "unavailable", - NM.DeviceState.DISCONNECTED: "disconnected", - NM.DeviceState.PREPARE: "prepare", - NM.DeviceState.CONFIG: "config", - NM.DeviceState.NEED_AUTH: "need_auth", - NM.DeviceState.IP_CONFIG: "ip_config", - NM.DeviceState.IP_CHECK: "ip_check", - NM.DeviceState.SECONDARIES: "secondaries", - NM.DeviceState.ACTIVATED: "activated", - NM.DeviceState.DEACTIVATING: "deactivating", - NM.DeviceState.FAILED: "failed", - }.get(self._device.get_state(), "unknown") - class Ethernet(Service): """A service to manage ethernet devices""" @@ -211,124 +528,74 @@ class Ethernet(Service): def enabled(self) -> bool: ... @Property(int, "readable") - def speed(self) -> int: - return self._device.get_speed() + def speed(self) -> str: + speed_mbps = self._device.get_speed() + return f"{speed_mbps} Mb/s" + + @Property(str, "readable") + def state(self) -> str: + return snake_case_to_kebab_case( + get_enum_member_name(self._device.get_state(), + default="disconnected") + ) @Property(str, "readable") def internet(self) -> str: - return { - NM.ActiveConnectionState.ACTIVATED: "activated", - NM.ActiveConnectionState.ACTIVATING: "activating", - NM.ActiveConnectionState.DEACTIVATING: "deactivating", - NM.ActiveConnectionState.DEACTIVATED: "deactivated", - }.get( - self._device.get_active_connection().get_state(), - "disconnected", - ) + if self._active_connection: + return snake_case_to_kebab_case( + get_enum_member_name(self._active_connection.get_state(), + default="disconnected") + ) + return "disconnected" + + @Property(str, "readable") + def iface(self) -> str: + return self._device.get_iface() if self._device else None @Property(str, "readable") def icon_name(self) -> str: network = self.internet if network == "activated": return "network-wired-symbolic" - elif network == "activating": return "network-wired-acquiring-symbolic" - - elif self._device.get_connectivity != NM.ConnectivityState.FULL: - return "network-wired-no-route-symbolic" - return "network-wired-disconnected-symbolic" def __init__(self, client: NM.Client, device: NM.DeviceEthernet, **kwargs) -> None: super().__init__(**kwargs) self._client: NM.Client = client self._device: NM.DeviceEthernet = device + self._active_connection = None - for names in ( - "active-connection", - "icon-name", - "internet", - "speed", - "state", - ): - self._device.connect(f"notify::{names}", lambda *_: self.notifier(names)) + self._device.connect( + "state-changed", lambda *args: self.on_network_state_changed()) - self._device.connect("notify::speed", lambda *_: print(_)) + self.update_active_connection() - def notifier(self, names): - self.notify(names) - self.emit("changed") + def on_network_state_changed(self): + """Called when networking is toggled on/off.""" + if self.state != "unmanaged": + # Re-initialize device when networking is re-enabled + self.update_active_connection() + else: + self._active_connection = None + def update_active_connection(self): + """Updates the active connection and connects to its state change signal.""" + active_connection = self._device.get_active_connection() + if active_connection: + self._active_connection = active_connection + self._active_connection.connect( + "state-changed", lambda *args: self.emit('changed')) -class NetworkClient(Service): - """A service to manage network devices""" - - @Signal - def device_ready(self) -> None: ... - - def __init__(self, **kwargs): - self._client: NM.Client | None = None - self.wifi_device: Wifi | None = None - self.ethernet_device: Ethernet | None = None - super().__init__(**kwargs) - NM.Client.new_async( - cancellable=None, - callback=self._init_network_client, - **kwargs, - ) - - def _init_network_client(self, client: NM.Client, task: Gio.Task, **kwargs): - self._client = client - wifi_device: NM.DeviceWifi | None = self._get_device(NM.DeviceType.WIFI) # type: ignore - ethernet_device: NM.DeviceEthernet | None = self._get_device( - NM.DeviceType.ETHERNET - ) - - if wifi_device: - self.wifi_device = Wifi(self._client, wifi_device) - self.emit("device-ready") - - if ethernet_device: - self.ethernet_device = Ethernet(client=self._client, device=ethernet_device) - self.emit("device-ready") - - self.notify("primary-device") - - def _get_device(self, device_type) -> Any: - devices: List[NM.Device] = self._client.get_devices() # type: ignore - return next( - ( - x - for x in devices - if x.get_device_type() == device_type - and x.get_active_connection() is not None - ), - None, - ) - - def _get_primary_device(self) -> Literal["wifi", "wired"] | None: - if not self._client: - return None - - if self._client.get_primary_connection() is None: - return "wifi" - return ( - "wifi" - if "wireless" - in str(self._client.get_primary_connection().get_connection_type()) - else "wired" - if "ethernet" - in str(self._client.get_primary_connection().get_connection_type()) - else None - ) - - def connect_wifi_bssid(self, bssid): - # We are using nmcli here, idk im lazy - exec_shell_command_async( - f"nmcli device wifi connect {bssid}", lambda *args: print(args) - ) - - @Property(str, "readable") - def primary_device(self) -> Literal["wifi", "wired"] | None: - return self._get_primary_device() + def get_network_stats(self): + """Fetch received and transmitted bytes from the system files""" + try: + # Read data from /sys/class/net for accurate speed + with open(f"/sys/class/net/{self.iface}/statistics/rx_bytes", "r") as rx_file: + rx_bytes = int(rx_file.read().strip()) + with open(f"/sys/class/net/{self.iface}/statistics/tx_bytes", "r") as tx_file: + tx_bytes = int(tx_file.read().strip()) + return rx_bytes, tx_bytes + except FileNotFoundError: + return None, None \ No newline at end of file diff --git a/services/notification.py b/services/notification.py deleted file mode 100644 index 4da918b..0000000 --- a/services/notification.py +++ /dev/null @@ -1,226 +0,0 @@ -import json -import os -from typing import List - -from fabric import Signal -from fabric.notifications import Notification, Notifications -from loguru import logger -import gi -gi.require_version('GLib', '2.0') -from gi.repository import GLib - -import config.data as data - -NOTIFICATION_CACHE_FILE = f"{data.CACHE_DIR}/notifications.json" - - -class CustomNotifications(Notifications): - """A service to manage the notifications.""" - - @Signal - def clear_all(self, value: bool) -> None: - """Signal emitted when notifications are emptied.""" - # Implement as needed for your application - - @Signal - def notification_count(self, value: int) -> None: - """Signal emitted when a new notification is added.""" - # Implement as needed for your application - - @Signal - def dnd(self, value: bool) -> None: - """Signal emitted when dnd is toggled.""" - # Implement as needed for your application - - @property - def count(self) -> int: - """Return the count of notifications.""" - return len(self.all_notifications) - - @property - def dont_disturb(self) -> bool: - """Return the pause status.""" - return self._dont_disturb - - @dont_disturb.setter - def dont_disturb(self, value: bool): - """Set the pause status.""" - self._dont_disturb = value - self.emit("dnd", value) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.all_notifications = [] - self._count = 0 # Will be updated to highest ID when loading - self.deserialized_notifications = [] - self._dont_disturb = False - self._load_notifications() - - def _load_notifications(self): - """Read notifications from the cache file.""" - if os.path.exists(NOTIFICATION_CACHE_FILE): - try: - with open(NOTIFICATION_CACHE_FILE, "r") as file: - notifications = json.load(file) - - def validate_with_id(notif): - """Helper to validate and return ID if valid.""" - try: - self._deserialize_notification(notif) - return (True, notif, notif.get("id", 0)) - except Exception as e: - msg = f"[Notification] Invalid: {str(e)[:50]}" - logger.error(msg) - return (False, None, 0) - - # Validate all notifications at once - results = [validate_with_id(n) for n in notifications] - - # Process results and find highest ID - valid_notifications = [] - highest_id = self._count # Start with current count - for is_valid, notif, notif_id in results: - if is_valid: - valid_notifications.append(notif) - highest_id = max(highest_id, notif_id) - - self.all_notifications = valid_notifications - self._count = highest_id # Update to highest ID seen - self._write_notifications(self.all_notifications) - - except (json.JSONDecodeError, KeyError, ValueError, IndexError) as e: - logger.error(f"[Notification] {e}") - self.all_notifications = [] - self._count = 0 - - def remove_notification(self, id: int): - """Remove the notification of given id.""" - item = next((p for p in self.all_notifications if p["id"] == id), None) - if item: - self.all_notifications.remove(item) - self._write_notifications(self.all_notifications) - self.emit("notification_count", len(self.all_notifications)) - - # Emit clear_all signal if there are no notifications left - if len(self.all_notifications) == 0: - self.emit("clear_all", True) - - def cache_notification(self, data: Notification): - """Cache the notification without widget_config and max_count.""" - - # First clean up any invalid notifications - self._cleanup_invalid_notifications() - - # Create the new notification - self._count += 1 - new_id = self._count - timestamp = GLib.get_real_time() / 1000000 # Convert microseconds to seconds - serialized_data = data.serialize() - serialized_data.update({ - "id": new_id, - "app_name": data.app_name, - "timestamp": timestamp - }) - - # Append the new notification - self.all_notifications.append(serialized_data) - - self._write_notifications(self.all_notifications) - self.emit("notification_count", len(self.all_notifications)) - - def _cleanup_invalid_notifications(self): - """Remove any invalid notifications.""" - - def validate_with_id(notif): - """Helper to validate and return result with ID.""" - try: - self._deserialize_notification(notif) - return (True, notif, None) - except Exception as e: - msg = f"[Notification] Removing invalid: {str(e)[:50]}" - logger.debug(msg) - return (False, None, notif.get("id", 0)) - - # Validate all notifications at once - results = [validate_with_id(n) for n in self.all_notifications] - - # Process results - valid_notifications = [] - invalid_count = 0 - for is_valid, notif, invalid_id in results: - if is_valid: - valid_notifications.append(notif) - else: - invalid_count += 1 - self.emit("notification-closed", invalid_id, "dismissed-by-limit") - - if invalid_count > 0: - self.all_notifications = valid_notifications - self._write_notifications(self.all_notifications) - self.emit("notification_count", len(self.all_notifications)) - - def _deserialize_notification(self, notification): - """Deserialize a notification.""" - try: - notif = Notification.deserialize(notification) - # Store the timestamp in the notification object - notif.timestamp = notification.get("timestamp", GLib.get_real_time() / 1000000) - return notif - except Exception as e: - logger.error( - f"[Notification] Deserialize failed for: {notification}, Error: {e}" - ) - raise - - def clear_all_notifications(self): - """Empty the notifications.""" - logger.info("[Notification] Clearing all notifications") - # Clear notifications but preserve the highest ID we've seen - highest_id = self._count - self.all_notifications = [] - self._write_notifications(self.all_notifications) - self.emit("notification_count", 0) - self.emit("clear_all", True) - # Restore the ID counter so new notifications get unique IDs - self._count = highest_id - - def _write_notifications(self, data): - """Write the notifications to the cache file.""" - try: - with open(NOTIFICATION_CACHE_FILE, "w") as f: - json.dump(data, f, indent=4, ensure_ascii=False) - logger.info("[Notification] Notifications written successfully.") - except Exception as e: - logger.error(f"[Notification] Failed to write notifications: {e}") - - def get_deserialized(self) -> List[Notification]: - """Return the notifications.""" - - def deserialize_with_id(notif): - """Helper to deserialize and return result with ID.""" - try: - return (self._deserialize_notification(notif), None) - except Exception as e: - msg = f"[Notification] Deserialize failed: {str(e)[:50]}" - logger.error(msg) - return (None, notif.get("id")) - - # Process all notifications at once - results = [ - deserialize_with_id(notification) for notification in self.all_notifications - ] - - # Split into successful and failed - deserialized = [] - invalid_ids = [] - for result, error_id in results: - if result is not None: - deserialized.append(result) - elif error_id is not None: - invalid_ids.append(error_id) - - # Clean up invalid notifications - for invalid_id in invalid_ids: - self.remove_notification(invalid_id) - - return deserialized diff --git a/services/powerprofile.py b/services/powerprofile.py deleted file mode 100644 index cccf03e..0000000 --- a/services/powerprofile.py +++ /dev/null @@ -1,88 +0,0 @@ -import dbus -from dbus.mainloop.glib import DBusGMainLoop -from fabric import Service, Signal -from loguru import logger - - -class PowerProfiles(Service): - """Service to interact with the PowerProfiles service.""" - - @Signal - def profile(self, value: str) -> None: - """Signal emitted when profile changes.""" - - instance = None - - @staticmethod - def get_default(): - if PowerProfiles.instance is None: - PowerProfiles.instance = PowerProfiles() - - return PowerProfiles.instance - - def __init__( - self, - **kwargs, - ): - super().__init__( - **kwargs, - ) - - self.bus_name = "net.hadess.PowerProfiles" - self.object_path = "/net/hadess/PowerProfiles" - - self.power_profiles = { - "power-saver": { - "name": "Power Saver", - "icon_name": "power-profile-power-saver-symbolic", - }, - "balanced": { - "name": "Balanced", - "icon_name": "power-profile-balanced-symbolic", - }, - "performance": { - "name": "Performance", - "icon_name": "power-profile-performance-symbolic", - }, - } - - # Set up the dbus main loop - DBusGMainLoop(set_as_default=True) - - self.bus = dbus.SystemBus() - - self.power_profiles_obj = self.bus.get_object(self.bus_name, self.object_path) - - self.iface = dbus.Interface( - self.power_profiles_obj, "org.freedesktop.DBus.Properties" - ) - - # Connect the 'g-properties-changed' signal to the handler - self.iface.connect_to_signal("PropertiesChanged", self.handle_property_change) - - def get_current_profile(self): - try: - return self.iface.Get("net.hadess.PowerProfiles", "ActiveProfile").strip() - - except dbus.DBusException as e: - logger.error(f"[PowerProfile] Error retrieving current power profile: {e}") - return "balanced" - - def set_power_profile(self, profile): - try: - self.iface.Set(self.bus_name, "ActiveProfile", dbus.String(profile)) - logger.info(f"[PowerProfile] Power profile set to {profile}") - except dbus.DBusException as e: - logger.error( - f"[PowerProfile] Could not change power level to {profile}: {e}" - ) - - # Function to handle properties change signals - def handle_property_change(self, proxy, changed, invalidated): - # Print the changed ActiveProfile - if "ActiveProfile" in changed: - logger.info(f"Profile changed: {changed['ActiveProfile']}") - self.emit("profile", changed["ActiveProfile"]) - - def get_profile_icon(self, profile): - return self.power_profiles.get(profile, "balanced").get("icon_name") diff --git a/services/screen_record.py b/services/screen_record.py deleted file mode 100644 index 7fbf6d5..0000000 --- a/services/screen_record.py +++ /dev/null @@ -1,142 +0,0 @@ -import datetime -import subprocess - -from fabric.core.service import Property, Service, Signal -from fabric.utils import exec_shell_command, exec_shell_command_async -from gi.repository import Gio, GLib -from loguru import logger - - -class ScreenRecorder(Service): - @Signal - def recording(self, value: bool) -> None: ... - - def __init__(self, **kwargs): - self.screenshot_path = GLib.get_home_dir() + "/Pictures/Screenshots/" - self.screenrecord_path = GLib.get_home_dir() + "/Videos/Screencasting/" - - super().__init__(**kwargs) - - def screenshot(self, fullscreen=False, save_copy=True): - time = datetime.datetime.today().strftime("%Y-%m-%d_%H-%M-%S") - file_path = self.screenshot_path + str(time) + ".png" - command = ( - ["grimblast", "copysave", "screen", file_path] - if save_copy - else ["grimblast", "copyscreen"] - ) - if not fullscreen: - command[2] = "area" - try: - subprocess.run(command, check=True) - self.send_screenshot_notification( - file_path=file_path if file_path else None, - ) - except Exception as e: - logger.error(f"[SCREENSHOT] Failed to run command: {command}") - - def screencast_start(self, fullscreen=False): - if self.is_recording: - logger.error( - "[SCREENRECORD] Another instance of wf-recorder is already running" - ) - return - time = datetime.datetime.today().strftime("%Y-%m-%d_%H-%M-%S") - file_path = self.screenrecord_path + str(time) + ".mp4" - self._current_screencast_path = file_path - area = "" if fullscreen else f"-g '{exec_shell_command('slurp')}'" - command = f"wf-recorder --file={file_path} --pixel-format yuv420p {area}" - exec_shell_command_async(command) - self.emit("recording", True) - - def screencast_stop(self): - exec_shell_command_async("killall -INT wf-recorder") - self.emit("recording", False) - self.send_screencast_notification(self._current_screencast_path) - - def send_screencast_notification(self, file_path): - # TODO: Generate a thumbnail - # exec_shell_command_async(f"totem-video-thumbnailer {file_path} {FABRIC_CACHE}") - - cmd = ["notify-send"] - cmd.extend( - [ - "-A", - "files=Show in Files", - "-A", - "view=View", - "-i", - "camera-video-symbolic", - "-a", - "Fabric Screenshot Utility", - "Screencast Saved", - f"Saved Screencast at {file_path}", - ] - ) - - proc: Gio.Subprocess = Gio.Subprocess.new(cmd, Gio.SubprocessFlags.STDOUT_PIPE) - - def do_callback(process: Gio.Subprocess, task: Gio.Task): - try: - _, stdout, stderr = process.communicate_utf8_finish(task) - except Exception: - logger.error( - f"[SCREENCAST] Failed read notification action with error {stderr}" - ) - return - - match stdout.strip("\n"): - case "files": - exec_shell_command_async(f"xdg-open {self.screenrecord_path}") - case "view": - exec_shell_command_async(f"xdg-open {file_path}") - - proc.communicate_utf8_async(None, None, do_callback) - - def send_screenshot_notification(self, file_path=None): - cmd = ["notify-send"] - cmd.extend( - [ - "-A", - "files=Show in Files", - "-A", - "view=View", - "-A", - "edit=Edit", - "-i", - "camera-photo-symbolic", - "-a", - "Fabric Screenshot Utility", - "-h", - f"STRING:image-path:{file_path}", - "Screenshot Saved", - f"Saved Screenshot at {file_path}", - ] - if file_path - else ["Screenshot Sent to Clipboard"] - ) - - proc: Gio.Subprocess = Gio.Subprocess.new(cmd, Gio.SubprocessFlags.STDOUT_PIPE) - - def do_callback(process: Gio.Subprocess, task: Gio.Task): - try: - _, stdout, stderr = process.communicate_utf8_finish(task) - except Exception: - logger.error( - f"[SCREENSHOT] Failed read notification action with error {stderr}" - ) - return - - match stdout.strip("\n"): - case "files": - exec_shell_command_async(f"xdg-open {self.screenshot_path}") - case "view": - exec_shell_command_async(f"xdg-open {file_path}") - case "edit": - exec_shell_command_async(f"swappy -f {file_path}") - - proc.communicate_utf8_async(None, None, do_callback) - - @Property(bool, "readable", default_value=False) - def is_recording(self): - return False if len(exec_shell_command("pidof wf-recorder")) == 0 else True diff --git a/styles/bar.css b/styles/bar.css deleted file mode 100644 index 6f4993c..0000000 --- a/styles/bar.css +++ /dev/null @@ -1,214 +0,0 @@ -#bar { - padding: 0px 5px 0px 5px; - margin: 1px; - transition: all 0.5s cubic-bezier(0.5, 0.25, 0, 1); - background-color: var(--surface); - /* Bar background */ -} - -#start-container, -#center-container, -#end-container { - padding-top: 2px; - padding-bottom: 2px; -} - -#bar-corner * { - background-color: var(--surface); -} - -/** - * ── Workspace ───────────────────────────────────────────────────────── - */ -#workspaces { - font-style: normal; - background-color: var(--surface-container); - padding: 4px; - margin-top: 1px; - margin-bottom: 1px; - border-radius: 8px; -} - -/** - * ── Update ──────────────────────────────────────────────────────────── - */ -#update { - background-color: var(--surface-container); - margin-top: 1px; - margin-bottom: 1px; - border-radius: 8px; - padding: 1px; - min-width: 24px; - min-height: 24px; - transition: background-color 0.1s ease; -} - -#update:hover { - background-color: var(--surface-bright); -} - -#update:active { - color: var(--shadow); - background-color: var(--primary); -} - -#update-icon { - color: var(--primary); - font-size: 20px; - padding: 1px; - border-radius: 11px; - transition: all 0.1s ease; -} - -#updated-icon { - color: var(--primary); - font-size: 20px; - padding: 4px; - border-radius: 8px; - transition: all 0.1s ease; -} - -#update-label { - color: var(--primary); - font-size: 10px; - font-weight: bold; - padding: 0px; - margin-right: 4px; - border-radius: 8px; - transition: all 0.1s ease; -} - -/** - * ── Clock ───────────────────────────────────────────────────────────── - */ -#datetime { - background-color: var(--surface-container); - margin-top: 1px; - margin-bottom: 1px; - padding: 4px; - border-radius: 8px; -} - -#datetime:hover { - background-color: var(--surface-bright); -} - -/** - * ── Tray ────────────────────────────────────────────────────────────── - */ -#tray { - background-color: var(--surface-container); - margin-top: 1px; - margin-bottom: 1px; - padding: 4px; - border-radius: 8px; -} - -#tray > button { - margin: 0px; - padding: 0px; -} - -menu { - border: solid 1px var(--outline-variant); - border-radius: 16px; - background-color: var(--surface); - padding: 6px; -} - -menu > menuitem { - border-radius: 10px; - padding: 6px 10px; -} - -menu > menuitem:hover { - background-color: var(--surface-bright); -} - -menu > menuitem:hover > label { - color: var(--on-surface); -} - -/** - * ── Logo ────────────────────────────────────────────────────────────── - */ -#button-bar { - background-color: var(--surface-container); - margin-top: 1px; - padding: 4px; - border-radius: 8px; - margin-bottom: 1px; - transition: all 0.1s ease; -} - -#button-bar-label { - color: var(--primary); - font-size: 24px; - border-radius: 8px; - transition: all 0.1s ease; -} - -#button-bar:hover #button-bar-label { - background-color: var(--surface_bright); -} - -#button-bar:active #button-bar-label { - color: var(--shadow); - background-color: var(--primary); -} - -/** - * ── System Indicator ───────────────────────────────────────────────── - */ -#system-indicator { - background-color: var(--surface-container); - margin-top: 1px; - padding: 4px; - margin-bottom: 1px; - border-radius: 8px; -} - -#system-indicator-icon { - font-size: 20px; -} - -#system-notif-label { - font-size: 18px; -} - -/** - * ── Metric ──────────────────────────────────────────────────────────── - */ -#metrics { - background-color: var(--surface-container); - border-radius: 8px; -} - -#metrics-circle { - color: var(--surface-bright); - border: 3px solid var(--primary); -} - -#metrics-circle.bat { - border: 3px solid var(--green); -} - -#metrics-circle.alert { - border: 3px solid var(--red-dim); -} - -#metrics-icon { - font-size: 15px; -} - -#metrics-icon.alert { - color: var(--red-dim); -} - -#metrics-level { - font-weight: bold; -} - -#metrics-sep { - min-width: 6px; -} diff --git a/styles/colors.css b/styles/colors.css index f30081e..584d0b6 100644 --- a/styles/colors.css +++ b/styles/colors.css @@ -1,34 +1,23 @@ :vars { - --foreground: #f1dfdb; + --foreground: #f1dedc; --background: #1a1110; - --cursor: #f1dfdb; - --primary: #ffb4a5; - --primary-container: #733428; - --on-primary: #561e14; - --on-primary-container: #ffdad3; - --secondary: #e7bdb5; - --on-secondary: #442a24; - --secondary-container: #5d3f39; - --tertiary: #dcc48c; - --tertiary-container: #554519; - --on-tertiary: #3d2f04; - --on-tertiary-container: #f9e0a6; + --cursor: #f1dedc; + --primary: #ffb3ad; + --on-primary: #571e1b; + --secondary: #e7bdb9; + --on-secondary: #442927; + --tertiary: #e1c28c; + --on-tertiary: #402d04; --surface: #1a1110; - --surface-container: #271d1b; - --surface-bright: #423734; - --surface-variant: #534340; - --surface-container-low: #231918; - --on-surface: #f1dfdb; + --surface-bright: #423735; --error: #ffb4ab; --error-dim: #ff8678; --on-error: #690005; --error-container: #93000a; - --on-error-container: #ffdad6; - --outline: #a08c88; - --outline-variant: #534340; + --outline: #a08c8a; --shadow: #000000; - --red: #ffb4a7; - --red-dim: #ff8974; + --red: #ffb4aa; + --red-dim: #ff8777; --green: #b7d085; --green-dim: #a1c260; --yellow: #dec56e; @@ -40,5 +29,4 @@ --cyan: #84d5c4; --cyan-dim: #5ec8b2; --white: #82d3e0; - } diff --git a/styles/components.css b/styles/components.css new file mode 100644 index 0000000..b811643 --- /dev/null +++ b/styles/components.css @@ -0,0 +1,145 @@ +#workspaces { + padding: 8px; + border-radius: 12px; + background-color: var(--surface); + min-width: 18px; + transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275); +} + +#battery { + background-color: var(--surface); + padding: 4px; +} + +#battery-circle { + color: var(--surface-bright); + border: 3px solid var(--primary); +} + +#battery-circle.alert { + border: 2px solid var(--red-dim); +} + +#battery-icon { + font-size: 16px; +} + +#battery-icon.alert { + color: var(--red-dim); +} + +#battery-save, +#battery-balanced, +#battery-performance { + border-radius: 12px; + min-width: 28px; + min-height: 28px; +} + +#battery-save-label, +#battery-balanced-label, +#battery-performance-label { + color: var(--outline); + font-size: 20px; +} + +#battery-save:hover, +#battery-balanced:hover, +#battery-performance:hover { + background-color: var(--surface-bright); +} + +#battery-save:hover #battery-save-label, +#battery-balanced:hover #battery-balanced-label, +#battery-performance:hover #battery-performance-label { + color: var(--primary); +} + +#battery-save.active, +#battery-balanced.active, +#battery-performance.active { + background-color: var(--primary); +} + +#battery-save.active #battery-save-label, +#battery-balanced.active #battery-balanced-label, +#battery-performance.active #battery-performance-label { + color: var(--shadow); +} + +#battery-level { + color: var(--primary); + font-weight: bold; + margin: 0 4px; +} + +#metrics { + background-color: var(--surface); + padding: 4px; +} + +#metrics.invert { + background-color: var(--surface); + border-radius: 12px; +} + +#metrics-circle { + padding: 4px; + color: var(--surface-bright); + border: 3px solid var(--primary); +} + +#metrics-icon { + font-size: 16px; +} + +#metrics-icon.alert { + color: var(--red-dim); +} + +#metrics-level { + color: var(--primary); + font-weight: bold; + margin: 0 4px; +} + +#metrics-sep { + min-width: 4px; +} + +#date-time { + background-color: var(--surface); + padding: 8px; +} + +#date-time.invert { + background-color: var(--surface); + border-radius: 12px; +} + +#tray { + background-color: var(--surface); + border-radius: 12px; + padding: 8px; +} + +menu { + border: solid 1px; + border-radius: 16px; + border-color: var(--surface); + background-color: var(--shadow); + padding: 6px; +} + +menu > menuitem { + border-radius: 10px; + padding: 6px 10px; +} + +menu > menuitem:hover { + background-color: var(--primary); +} + +menu > menuitem:hover > label { + color: var(--shadow); +} diff --git a/styles/controls.css b/styles/controls.css new file mode 100644 index 0000000..604415a --- /dev/null +++ b/styles/controls.css @@ -0,0 +1,36 @@ + + +#control { + padding: 4px; + background-color: var(--surface); +} + +#control.invert { + background-color: var(--surface); + border-radius: 12px; +} + +#button-volume, +#button-mic, +#button-brightness { + color: var(--surface-bright); + border: 3px solid var(--primary); +} + +#button-volume.muted, +#button-mic.muted { + color: var(--surface-bright); + border: 3px solid var(--outline); +} + +#vol-label, +#mic-label, +#brightness-label { + font-size: 16px; +} + +#vol-label.muted, +#mic-label.muted { + color: var(--outline); +} + diff --git a/styles/dashboard.css b/styles/dashboard.css deleted file mode 100644 index 6d99fc2..0000000 --- a/styles/dashboard.css +++ /dev/null @@ -1,163 +0,0 @@ -#buttons-grid { - min-height: 52px; -} - -#network-button, -#bluetooth-button, -#night-mode-button, -#power-profile-button, -#dark-mode-button, -#caffeine-button { - border-radius: 20px; -} - -#network-button, -#bluetooth-button { - background-color: unset; - padding: 0; -} - -#night-mode-button, -#dark-mode-button, -#power-profile-button, -#caffeine-button { - background-color: var(--primary); - padding: 0 10px; -} - -#night-mode-button.disabled, -#dark-mode-button.disabled, -#power-profile-button.disabled, -#caffeine-button.disabled { - background-color: var(--surface); -} - -#network-status-button, -#network-menu-button, -#bluetooth-status-button, -#bluetooth-menu-button { - background-color: var(--primary); - padding: 0 8px; -} - -#network-menu-button, -#bluetooth-menu-button { - border-left: 1px solid var(--outline); - min-width: 24px; -} - -#network-status-button:hover, -#network-menu-button:hover, -#bluetooth-status-button:hover, -#bluetooth-menu-button:hover, -#bluetooth-button:hover, -#night-mode-button:hover, -#power-profile-button:hover, -#dark-mode-button:hover, -#caffeine-button:hover { - background-color: var(--foreground); -} - -/* Estados Disabled */ -#network-status-button.disabled, -#network-menu-button.disabled, -#bluetooth-status-button.disabled, -#bluetooth-menu-button.disabled { - background-color: var(--surface); -} - -#network-menu-button.disabled, -#bluetooth-menu-button.disabled { - border-left: 1px solid var(--surface-bright); -} - -#network-status-button.disabled:hover, -#network-menu-button.disabled:hover, -#bluetooth-status-button.disabled:hover, -#bluetooth-menu-button.disabled:hover, -#night-mode-button.disabled:hover, -#power-profile-button.disabled:hover, -#dark-mode-button.disabled:hover, -#caffeine-button.disabled:hover { - background-color: var(--surface-bright); -} - -#network-status-button, -#bluetooth-status-button { - border-radius: 20px 0 0 20px; -} - -#network-menu-button, -#bluetooth-menu-button { - border-radius: 0 20px 20px 0; -} - -#network-icon, -#bluetooth-icon, -#night-mode-icon, -#dark-mode-icon, -#power-profile-icon, -#caffeine-icon { - color: var(--shadow); - font-size: 24px; -} - -#network-icon.disabled, -#bluetooth-icon.disabled, -#night-mode-icon.disabled, -#power-profile-icon.disabled, -#dark-mode-icon.disabled, -#caffeine-icon.disabled { - color: var(--outline); -} - -#network-menu-label, -#bluetooth-menu-label { - color: var(--shadow); - font-size: 16px; -} - -#network-menu-label.disabled, -#bluetooth-menu-label.disabled { - color: var(--outline); -} - -#network-label, -#bluetooth-label, -#night-mode-label, -#power-profile-label, -#caffeine-label, -#dark-mode-label { - color: var(--shadow); - font-size: 14px; - font-weight: bold; - margin-bottom: -4px; -} - -#network-label.disabled, -#bluetooth-label.disabled, -#night-mode-label.disabled, -#power-profile-label.disabled, -#dark-mode-label.disabled, -#caffeine-label.disabled { - color: var(--outline); -} - -#network-ssid, -#bluetooth-status, -#night-mode-status, -#power-profile-status, -#dark-mode-status, -#caffeine-status { - color: var(--surface-bright); - font-size: 12px; -} - -#network-ssid.disabled, -#bluetooth-status.disabled, -#night-mode-status.disabled, -#power-profile-status.disabled, -#dark-mode-status.disabled, -#caffeine-status.disabled { - color: var(--outline); -} diff --git a/styles/dock.css b/styles/dock.css index c0a9860..396c7e6 100644 --- a/styles/dock.css +++ b/styles/dock.css @@ -1,52 +1,69 @@ #dock { - background-color: var(--surface); + background-color: var(--shadow); padding: 8px; - margin: 8px 8px 7px 8px; + margin: 8px 8px 0 8px; border-radius: 20px 20px 0 0; - transition: all 0.25s cubic-bezier(0.5, 0.25, 0, 1); + transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275); } #dock.vertical { - margin: 8px 79px 8px 8px; + margin: 8px 0 8px 8px; border-radius: 20px 0 0 20px; } -#dock.vertical-left { - margin: 8px 39px 8px 0px; +#dock.vertical.left { + margin: 8px 8px 8px 0; border-radius: 0 20px 20px 0; } -#dock.vertical-right{ - margin: 8px 79px 8px 8px; +#dock.dense { + margin: 8px 8px 4px 8px; + border-radius: 20px; + border: 2px solid var(--surface); +} + +#dock.edge { + border-radius: 20px 20px 0 0; + border: 2px solid var(--surface); + border-bottom: none; +} + +#dock.dense.vertical { + margin: 8px 4px 8px 8px; + border-radius: 20px; + border: 2px solid var(--surface); +} + +#dock.edge.vertical { border-radius: 20px 0 0 20px; + border: 2px solid var(--surface); + border-right: none; } #dock-full { - transition: all 0.25s cubic-bezier(0.5, 0.25, 0, 1); + transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275); opacity: 1; } -#dock-full.occluded { - @apply dock_nmargin(); -} - #dock-separator { - padding: 1px; - @apply dock_sep(); + padding: 2px; border-radius: 16px; background-color: var(--surface-bright); } #dock-app-button { padding: 4px; - border-radius: 12px; + border-radius: 24px; + box-shadow: 0 0 4px alpha(var(--shadow), 0.5); + transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275); } -#dock-app-button:hover, -#dock-app-button:hover.instance { +#dock-app-button:hover { background-color: var(--surface-bright); - border-top: 1px solid var(--primary); - padding-top: 3px; +} + +#dock-app-button:hover.instance { + background-color: var(--outline); } #dock-app-button:active, @@ -55,24 +72,22 @@ } #dock-app-button.instance { - background: linear-gradient(to top, transparent, var(--surface-bright)); - border-bottom: 1px solid var(--surface-container); - border-top: 1px solid var(--outline); - padding: 3px 4px; + background: var(--surface-bright); + border-radius: 12px; } #dock-corner-left { - margin: 0 -8px 7px 0; + margin: 0 -8px 0 0; } #dock-corner-right { - margin: 0 0 7px -8px; + margin: 0 0 0 -8px; } #dock-corner-top { - margin: 0 79px -8px 0; + margin: 0 0 -8px 0; } #dock-corner-bottom { - margin: -8px 79px 0 0; + margin: -8px 0 0 0; } diff --git a/styles/indicators.css b/styles/indicators.css new file mode 100644 index 0000000..6b9f995 --- /dev/null +++ b/styles/indicators.css @@ -0,0 +1,32 @@ +#indicator { + padding: 4px; + background-color: var(--surface); + } + + #indicator.invert { + background-color: var(--surface); + border-radius: 12px; + } + + #button-network, + #button-bluetooth { + color: var(--surface-bright); + border: 3px solid var(--primary); + } + + #button-network.muted, + #button-bluetooth.muted { + color: var(--surface-bright); + border: 3px solid var(--outline); + } + + #network-label, + #bluetooth-label { + font-size: 20px; + } + + #network-label.muted, + #bluetooth-label.muted { + color: var(--outline); + } + \ No newline at end of file diff --git a/styles/launcher.css b/styles/launcher.css deleted file mode 100644 index 8a58328..0000000 --- a/styles/launcher.css +++ /dev/null @@ -1,885 +0,0 @@ -/* - * ── launcher ────────────────────────────────────────────────────────── -*/ -#dashboard { - min-height: 100px; -} - -#launcher { - margin: 18px; - border: 1px solid var(--outline-variant); - background-color: var(--surface); - border-radius: 24px; - padding: 18px; - transition: all 0.5s cubic-bezier(0.5, 0.25, 0, 1); -} - -#launcher entry { - transition: all 0.1s ease; - border-radius: 11px; - padding: 10px; - margin: 12px; - box-shadow: inset 0 0 0 1px var(--outline-variant); -} - -#launcher-content.wallpapers { - padding: 16px; - min-height: 420px; - min-width: 620px; -} - -.applauncher-scrolled-window { - transition: min-height 0.2s ease; -} - -#launcher-content.bluetooth { - min-height: 464px; -} - -#launcher-content.wifi { - min-height: 464px; -} - -#launcher-content.tools { - min-width: 308px; -} - -#launcher-content.calendar { - min-height: 420px; - min-width: 420px; -} - -#launcher-content { - min-width: 485px; -} - -#launcher-content.power { - min-width: 200px; - min-height: 52px; -} - -#launcher-content.kanban { - min-width: 1000px; - min-height: 478px; -} - -#launcher-content.tmux { - min-height: 220px; - min-width: 480px; -} - -#scrolled-window scrollbar { - border-radius: 10px; - background-color: var(--surface); - padding: 4px; - margin-left: 6px; -} - -#scrolled-window slider { - border-radius: 8px; - min-width: 10px; - min-height: 48px; - background-color: var(--primary); -} - -#config-button:active, -#new-session-button:active { - background-color: var(--primary); -} - -#config-label, -#clear-label, -#new-session-label { - color: var(--primary); - font-size: 24px; -} - -#config-button:active #config-label, -#clear-button:active - #clear-label - #new-session-button:active - #new-session-label { - color: var(--shadow); -} - -#config-button, -#clear-button, -#new-session-button { - background-color: var(--surface-variant); - border-radius: 16px; - padding: 8px; - margin-bottom: 12px; - margin-top: 12px; -} - -#config-button:hover, -#config-button:focus, -#clear-button:hover, -#clear-button:focus, -#new-session-button:hover, -#new-session-button:focus { - background-color: var(--surface-bright); -} - -#clear-button:active { - background-color: var(--red-dim); -} - -/* - * ── applauncher ─────────────────────────────────────────────────────── -*/ -#app-desc { - color: var(--outline); - font-size: 12px; - font-style: italic; -} - -#slot-button { - border-radius: 16px; - padding: 16px; - min-height: 0; - transition: padding 0.25s cubic-bezier(0.5, 0.25, 0, 1); - animation: loadSlot 0.5s ease; -} - -@keyframes loadSlot { - 0% { - opacity: 0; - } - - 100% { - opacity: 1; - } -} - -#slot-button:hover, -#slot-button:focus, -#slot-button:selected, -#slot-button.selected { - background-color: var(--surface-bright); - padding-left: 24px; -} - -#slot-button:active { - background-color: var(--primary); -} - -#app-icon { - margin-right: 8px; -} - -#app-label { - color: var(--foreground); - font-weight: bold; -} - -#slot-button:hover #app-label, -#slot-button:focus #app-label, -#slot-button:selected #app-label, -#slot-button.selected #app-label { - color: var(--primary); -} - -#slot-button:active #app-label { - color: var(--shadow); -} - -/* - * ── wallpaper ───────────────────────────────────────────────────────── -*/ -#search-entry-walls { - min-width: 512px; -} - -#wallpaper-icons:selected { - background-color: var(--outline); - border-radius: 0px; -} - -#scheme-dropdown { - background-color: var(--surface); - padding: 8px; -} - -#scheme-dropdown > * > * { - font-weight: bold; - color: var(--shadow); -} - -#scheme-dropdown.box button.box.cellview { - background-color: var(--primary); -} - -#scheme-dropdown.box button.box.cellview:selected { - background-color: var(--surface-bright); -} - -#search-entry-walls { - min-width: 512px; -} - -/* Customize the overall switch appearance */ -#matugen-switcher, -#dnd-switch { - min-width: 40px; - min-height: 20px; - background-color: var(--surface); - border-radius: 15px; - padding: 2px; - transition: background-color 0.3s ease; -} - -/* Style the switch's slider */ -#matugen-switcher slider, -#dnd-switch slider { - background-color: var(--primary); - border-radius: 16px; - min-width: 20px; - min-height: 20px; - transition: - background-color 0.1s cubic-bezier(0.5, 0.25, 0, 1.25), - transform 0.25s cubic-bezier(0.5, 0.25, 0, 1.25); -} - -/* When the switch is active (checked) */ -#matugen-switcher:checked, -#dnd-switch:checked { - background-color: var(--primary); -} - -/* Optional: additional styling for the slider when active */ -#matugen-switcher:checked slider, -#dnd-switch:checked slider { - background-color: var(--shadow); -} - -#matugen-switcher:checked image, -#dnd-switch:checked image { - color: var(--shadow); -} - -#mat-label { - font-size: 24px; - margin-left: 4px; -} - -#custom-color-selector-box { - background-color: var(--surface); - border-radius: 20px; - padding: 8px; -} - -#hue-slider { - margin: 0 24px; -} - -#hue-slider trough { - background-image: linear-gradient( - to right, - #ff0000, - #ffff00, - #00ff00, - #00ffff, - #0000ff, - #ff00ff, - #ff0000 - ); - border-radius: 8px; - min-height: 8px; -} - -#hue-slider slider { - min-height: 16px; - min-width: 8px; - margin: -10px 0; - background-color: var(--primary); - box-shadow: 0 0 4px alpha(var(--shadow), 0.5); -} - -#apply-color-button { - background-color: var(--primary); - color: var(--shadow); - border-radius: 20px; - padding: 8px; - box-shadow: 0 0 4px alpha(var(--shadow), 0.5); -} - -#apply-color-button:hover { - background-color: var(--foreground); -} - -#apply-color-button:active { - background-color: var(--shadow); -} - -#apply-color-label { - font-size: 16px; - color: var(--shadow); -} - -#apply-color-button:active #apply-color-label { - color: var(--foreground); -} -/* - * ── emoji ───────────────────────────────────────────────────────────── -*/ - -#emoji #search-entry { - font-weight: bold; - background-color: var(--surface); - color: var(--foreground); - border-radius: 16px; - padding: 10px; -} - -#emoji #search-entry selection { - color: var(--background); - background-color: var(--primary); -} - -#emoji #close-button { - background-color: var(--surface); - border-radius: 16px; - padding: 8px; -} - -#emoji #close-button:hover, -#emoji #close-button:focus { - background-color: var(--surface-bright); -} - -#emoji #close-button:active { - background-color: var(--red-dim); -} - -#emoji #close-label { - color: var(--red-dim); - font-size: 24px; -} - -#emoji #close-button:active #close-label { - color: var(--shadow); -} - -#emoji #emoji-slot-button { - border-radius: 16px; - padding: 16px; - transition: padding 0.1s ease; -} - -@keyframes loadEmojiSlot { - 0% { - opacity: 0; - } - - 100% { - opacity: 1; - } -} - -#emoji #emoji-slot-button:hover, -#emoji #emoji-slot-button:focus, -#emoji #emoji-slot-button:selected, -#emoji #emoji-slot-button.selected { - background-color: var(--surface-bright); - text-shadow: 0 0 4px var(--shadow); - animation: highlightEmojiSlot 0.25s cubic-bezier(0, 0, 0.5, 1); -} - -@keyframes highlightEmojiSlot { - 0% { - padding: 16px; - text-shadow: 0 4px 4px var(--shadow); - } - - 50% { - padding: 8px 16px 24px 16px; - text-shadow: 0 12px 4px var(--shadow); - } - - 100% { - padding: 16px; - text-shadow: 0 4px 4px var(--shadow); - } -} - -#emoji #emoji-slot-button:active { - background-color: var(--primary); -} - -#emoji #emoji-name-label { - color: var(--foreground); - font-weight: bold; -} - -#emoji #emoji-slot-button:hover #emoji-name-label, -#emoji #emoji-slot-button:focus #emoji-name-label, -#emoji #emoji-slot-button:selected #emoji-name-label, -#emoji #emoji-slot-button.selected #emoji-name-label { - color: var(--primary); -} - -#emoji #emoji-slot-button:active #emoji-name-label { - color: var(--shadow); -} - -#emoji #emoji-char-label { - font-size: 24px; -} - -/* - * ── powermenu ───────────────────────────────────────────────────────── -*/ - -#power-menu-button { - border-radius: 20px; - min-width: 52px; - min-height: 52px; - padding: 5px; -} - -#power-menu-button label { - font-size: 35px; - color: var(--foreground); -} - -#power-menu-button:hover, -#power-menu-button:focus { - background-color: var(--surface-bright); -} - -#power-menu-button:hover label, -#power-menu-button:focus label { - color: var(--primary); -} - -#power-menu-button:active { - background-color: var(--primary); -} - -#power-menu-button:active label { - color: var(--shadow); -} - -/* - * ── sh runner ───────────────────────────────────────────────────────── -*/ - -#sh-item { - border-radius: 11px; - background-color: transparent; - background-image: none; - box-shadow: none; - padding: 0px; -} - -#sh-item { - font-size: 24px; -} - -#sh-item:hover, -#sh-item:focus { - text-shadow: 2px 2px 2px var(--primary); -} - -/* - * ── bluetooth ───────────────────────────────────────────────────────── -*/ - -#bluetooth-header { - border: 2px solid var(--shadow); - padding: 4px; - background-color: var(--surface-container); - border-radius: 12px; -} - -#bluetooth-device { - border: 2px solid var(--shadow); - background-color: var(--surface-container); - border-radius: 12px; - padding: 4px; -} - -#bluetooth-scan, -#bluetooth-back { - background-color: var(--surface); - border-radius: 8px; - padding: 4px; -} - -#bluetooth-back-label { - font-size: 20px; -} - -#bluetooth-scan:hover, -#bluetooth-back:hover { - background-color: var(--surface-bright); -} - -#bluetooth-scan label, -#bluetooth-toggle label { - font-weight: bold; -} - -#bluetooth-text, -#bluetooth-section { - font-weight: bold; -} - -#bluetooth-section { - background-color: var(--surface-container); - border-radius: 12px; - padding: 8px; -} - -#bluetooth-connection { - font-size: 20px; -} - -#bluetooth-connect { - background-color: var(--surface); - border-radius: 8px; - padding: 8px; -} - -#bluetooth-connect:hover { - background-color: var(--surface-bright); -} - -#bluetooth-paired, -#bluetooth-available { - background-color: var(--surface-container); - border-radius: 20px; - padding: 4px; -} - -#bt-sep { - border-radius: 16px; -} - -#bluetooth-scan-label { - font-size: 20px; - color: var(--primary); -} - -#bluetooth-scan.scanning { - animation: blink 0.5s ease infinite; -} - -#bluetooth-scan-label.scanning { - animation: blink 0.5s ease infinite; -} - -@keyframes blink { - 0% { - background-color: var(--blue); - color: var(--shadow); - } - - 50% { - background-color: var(--shadow); - color: var(--blue); - } - - 100% { - background-color: var(--blue); - color: var(--shadow); - } -} - -/* - * ── wifi ────────────────────────────────────────────────────────────── -*/ - -#wifi-header { - background-color: var(--surface-container); - padding: 4px; - border-radius: 20px; -} - -#wifi-device { - background-color: var(--secondary-container); - border-radius: 16px; - padding: 4px; - margin-bottom: 4px; -} - -#wifi-toggle-icon { - font-size: 24px; -} - -#wifi-scan, -#wifi-toggle { - background-color: var(--surface-container); - margin-left: 0px; - min-width: 44px; - min-height: 44px; - border-radius: 100%; -} - -#wifi-scan:hover, -#wifi-toggle:hover { - background-color: var(--surface-bright); -} - -#wifi-scan label, -#wifi-toggle label { - font-weight: bold; -} - -#wifi-text, -#wifi-section { - font-weight: bold; -} - -#wifi-text { - color: var(--primary); -} - -#wifi-connection { - font-size: 20px; -} - -#wifi-connect { - background-color: var(--surface); - border-radius: 16px; - padding: 8px 16px; -} - -#wifi-connect:hover { - background-color: var(--surface-bright); -} - -#wifi-available { - /*background-color: var(--surface-container);*/ - padding: 4px; - border-radius: 20px; -} - -/* - * ── calendar ────────────────────────────────────────────────────────── -*/ - -#header { - border-radius: 12px; - padding: 4px; -} - -#weekday-row { - background-color: var(--surface); - border-radius: 8px; - padding: 4px; -} - -#month-label, -#weekday-label, -#day-label { - font-weight: bold; -} - -#day-label, -#day-empty { - min-width: 38px; - min-height: 22px; -} - -#day-empty { - color: var(--surface-bright); -} - -#weekday-label { - color: var(--primary); -} - -#calendar-grid { - border-radius: 16px; - padding: 4px; -} - -#day-label.current-day { - background-color: var(--primary); - color: var(--shadow); - border-radius: 20px; -} - -#prev-month-button, -#next-month-button { - background-color: var(--surface); - border-radius: 8px; - padding: 4px; -} - -#prev-month-button:hover, -#next-month-button:hover { - background-color: var(--surface-bright); -} - -#prev-month-button:active, -#next-month-button:active { - background-color: var(--primary); -} - -#month-button-label { - color: var(--primary); - font-size: 20px; -} - -#prev-month-button:active #month-button-label, -#next-month-button:active #month-button-label { - color: var(--shadow); -} - -#prev-month-day, -#next-month-day { - color: mix(var(--on-surface), transparent, 0.6); -} - -/* - * ── tools ───────────────────────────────────────────────────────────── -*/ - -#toolbox-button { - border-radius: 20px; - min-width: 52px; - min-height: 52px; - padding: 5px; -} - -#toolbox-button label { - font-size: 35px; - color: var(--foreground); -} - -#toolbox-button.recording label { - color: var(--red-dim); -} - -#toolbox-button:hover, -#toolbox-button:focus { - background-color: var(--surface-bright); -} - -#toolbox-button:hover label, -#toolbox-button:focus label { - color: var(--primary); -} - -#toolbox-button.recording:hover label, -#toolbox-button.recording:focus label { - color: var(--red-dim); -} - -#toolbox-button:active { - background-color: var(--primary); -} - -#toolbox-button:active label { - color: var(--shadow); -} - -#toolbox-button.recording:active { - background-color: var(--red-dim); -} - -#toolbox-button.recording:active label { - color: var(--shadow); -} - -#tool-sep { - min-width: 4px; - min-height: 4px; - margin: 0px 4px; - border-radius: 16px; - background-color: var(--surface-bright); -} - -/* - * ── kanban ──────────────────────────────────────────────────────────── -*/ - -#kanban-header { - border: 2px solid var(--outline); - border-radius: 12px; - padding: 4px; -} - -#column-header { - padding: 4px; - font-weight: bold; - color: var(--primary); -} - -#kanban-note { - background-color: var(--surface-container); - border-radius: 4px; - padding: 8px; - margin-bottom: 4px; -} - -#kanban-row:first-child > * > * { - border-radius: 16px 16px 4px 4px; -} - -#kanban-row:last-child > * > * { - border-radius: 4px 4px 16px 16px; -} - -#inline-editor { - border: 2px solid var(--surface-bright); - border-radius: 12px; - padding: 8px; -} - -#kanban-btn, -#kanban-btn-add { - background-color: var(--surface-container); - border-radius: 8px; - padding: 4px; -} - -#kanban-btn { - background-color: var(--surface); -} - -#kanban-btn:hover, -#kanban-btn-add:hover { - background-color: var(--surface-bright); -} - -#kanban-btn:active, -#kanban-btn-add:active { - background-color: var(--primary); -} - -#kanban-btn-label, -#kanban-btn-neg { - font-size: 20px; - color: var(--primary); -} - -#kanban-btn-neg { - color: var(--red-dim); -} - -#kanban-btn:active #kanban-btn-label, -#kanban-btn-add:active #kanban-btn-label { - color: var(--surface); -} - -/* tmux */ -#no-notif, -#no-tmux, -#no-clip { - font-size: 96px; - color: var(--surface-container); -} - -#clip-label { - font-weight: bold; -} - -#tmux-icon, -#clip-icon { - font-size: 20px; - color: var(--primary); -} diff --git a/styles/main.css b/styles/main.css deleted file mode 100644 index 79e35a8..0000000 --- a/styles/main.css +++ /dev/null @@ -1,51 +0,0 @@ -@import "./colors.css"; -@import "./bar.css"; -@import "./notification.css"; -@import "./osd.css"; -@import "./launcher.css"; -@import "./dashboard.css"; -@import "./dock.css"; -@import "./notif_center.css"; - -* { - all: unset; - color: var(--foreground); - font-size: 15px; - font-family: "Monaspace Radon"; -} - -/* - * ── tooltip ─────────────────────────────────────────────────────────── -*/ -tooltip { - border: solid 1px; - border-radius: 16px; - border-color: var(--outline); - background-color: var(--surface); - animation: tooltipShow 0.25s cubic-bezier(0.5, 0.25, 0, 1); -} - -@keyframes tooltipShow { - from { - opacity: 0; - } - - to { - opacity: 1; - } -} - -tooltip > * { - padding: 6px 10px; - border-radius: 10px; -} - -#corner { - background-color: var(--surface); - border-radius: 0; -} - -#corner-container { - min-width: 20px; - min-height: 20px; -} diff --git a/styles/notif_center.css b/styles/notif_center.css deleted file mode 100644 index 85f0f1d..0000000 --- a/styles/notif_center.css +++ /dev/null @@ -1,28 +0,0 @@ -#nhh { - font-weight: bold; -} - -#nhh-button { - border-radius: 8px; - background-color: var(--surface); - padding: 4px; -} - -#nhh-button:hover { - background-color: var(--surface-bright); -} - -#nhh-button-label { - color: var(--red-dim); - font-size: 20px; -} - -#dnd-label { - font-size: 20px; - margin-left: 8px; -} - -#no-notifications-label { - font-size: 96px; - color: var(--surface-container); -} diff --git a/styles/notification.css b/styles/notification.css deleted file mode 100644 index dddf9bf..0000000 --- a/styles/notification.css +++ /dev/null @@ -1,83 +0,0 @@ -#notification-action-buttons button.start-action { - border-radius: 20px; -} - -#notification-action-buttons button { - background-color: var(--surface); - padding: 8px; - border-radius: 20px; - transition: background-color 0.1s ease; -} - -#notification-action-buttons button:hover { - background-color: var(--surface-bright); -} - -#notification-action-buttons button:active { - background-color: var(--primary); -} - -#notification-action-buttons button:active #button-label { - color: var(--shadow); -} - -#notification-image image { - border-radius: 16px; - color: var(--on-surface); -} - -#notification-summary, -#button-label { - font-weight: bold; -} - -#notification-summary { - font-weight: bold; - color: var(--primary); -} - -#notification-app-name { - color: var(--outline); -} - -#action-button { - margin-top: 8px; -} - -#notification-box { - border-radius: 20px; - padding: 16px; - background-color: var(--surface-container-low); - margin: 4px; - min-width: 336px; - transition: background-color 0.1s ease; -} - -#boxed-notification-revealer { - margin-top: -14px; -} - -#notif-close-button { - background-color: var(--surface); - border-radius: 16px; - padding: 4px; - transition: background-color 0.1s ease; -} - -#notif-close-button:hover, -#notif-close-button:focus { - background-color: var(--surface-bright); -} - -#notif-close-button:active { - background-color: var(--red-dim); -} - -#notif-close-label { - color: var(--red-dim); - font-size: 20px; -} - -#notif-close-button:active #notif-close-label { - color: var(--shadow); -} diff --git a/styles/osd.css b/styles/osd.css deleted file mode 100644 index 017865d..0000000 --- a/styles/osd.css +++ /dev/null @@ -1,47 +0,0 @@ -#osd { - background-color: var(--surface); - padding: 10px; - border-radius: 6px; - margin-top: 6px; -} - -#osd scale { - min-width: 150px; -} - -#osd trough { - background: var(--surface-container); - min-height: 10px; - margin-right: 4px; - border-radius: 99px; -} - -#osd highlight { - border-radius: 99px 0px 0px 99px; - background: var(--primary); - margin-right: 6px; -} - -#osd slider { - color: var(--on-primary); - background-color: var(--primary); - background-size: 20px; - min-width: 5px; - min-height: 10px; - border-radius: 12px; - padding: 0px; - background-repeat: no-repeat; - background-position: center; - margin-left: 0px; - margin-right: 0px; - margin-top: -10px; - margin-bottom: -10px; - box-shadow: - -1px 0px 0px 1px var(--surface-container), - 1px 0px 0px 1px var(--surface-container); -} - -#osd-label { - font-weight: bold; - font-size: 17px; -} diff --git a/utils/__init__.py b/utils/__init__.py index 43abad3..3f03f12 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -1,4 +1,4 @@ -from .custom_image import * -from .icon_resolver import * -from .occlusion import * -from .animator import * +""" +Modus services package. +Contains background services and utilities for the shell. +""" diff --git a/utils/animator.py b/utils/animator.py index 1799ae6..ed60e9e 100644 --- a/utils/animator.py +++ b/utils/animator.py @@ -1,5 +1,6 @@ from typing import cast +import fabric from fabric import Property, Service, Signal from gi.repository import GLib, Gtk diff --git a/utils/circle_image.py b/utils/circle_image.py new file mode 100644 index 0000000..acfdc6d --- /dev/null +++ b/utils/circle_image.py @@ -0,0 +1,118 @@ +import math +from typing import Literal + +import cairo +import gi +from fabric.core.service import Property +from fabric.widgets.widget import Widget + +gi.require_version("Gtk", "3.0") +from gi.repository import Gdk, GdkPixbuf, Gtk # noqa: E402 + + +class CircleImage(Gtk.DrawingArea, Widget): + """A widget that displays an image in a circular shape with a 1:1 aspect ratio.""" + + @Property(int, "read-write") + def angle(self) -> int: + return self._angle + + @angle.setter + def angle(self, value: int): + self._angle = value % 360 + self.queue_draw() + + def __init__( + self, + image_file: str | None = None, + pixbuf: GdkPixbuf.Pixbuf | None = None, + name: str | None = None, + visible: bool = True, + all_visible: bool = False, + style: str | None = None, + tooltip_text: str | None = None, + tooltip_markup: str | None = None, + h_align: Literal["fill", "start", "end", "center", "baseline"] | Gtk.Align | None = None, + v_align: Literal["fill", "start", "end", "center", "baseline"] | Gtk.Align | None = None, + h_expand: bool = False, + v_expand: bool = False, + size: int | None = None, + **kwargs, + ): + Gtk.DrawingArea.__init__(self) + Widget.__init__( + self, + name=name, + visible=visible, + all_visible=all_visible, + style=style, + tooltip_text=tooltip_text, + tooltip_markup=tooltip_markup, + h_align=h_align, + v_align=v_align, + h_expand=h_expand, + v_expand=v_expand, + size=size, + **kwargs, + ) + self.size = size if size is not None else 100 # Default size if not provided + self._angle = 0 + self._orig_image: GdkPixbuf.Pixbuf | None = None # Original image for reprocessing + self._image: GdkPixbuf.Pixbuf | None = None + if image_file: + pix = GdkPixbuf.Pixbuf.new_from_file(image_file) + self._orig_image = pix + self._image = self._process_image(pix) + elif pixbuf: + self._orig_image = pixbuf + self._image = self._process_image(pixbuf) + self.connect("draw", self.on_draw) + + def _process_image(self, pixbuf: GdkPixbuf.Pixbuf) -> GdkPixbuf.Pixbuf: + """Crop the image to a centered square and scale it to the widget’s size.""" + width, height = pixbuf.get_width(), pixbuf.get_height() + if width != height: + square_size = min(width, height) + x_offset = (width - square_size) // 2 + y_offset = (height - square_size) // 2 + pixbuf = pixbuf.new_subpixbuf(x_offset, y_offset, square_size, square_size) + else: + square_size = width + if square_size != self.size: + pixbuf = pixbuf.scale_simple(self.size, self.size, GdkPixbuf.InterpType.BILINEAR) + return pixbuf + + def on_draw(self, widget: "CircleImage", ctx: cairo.Context): + if self._image: + ctx.save() + # Create a circular clipping path + ctx.arc(self.size / 2, self.size / 2, self.size / 2, 0, 2 * math.pi) + ctx.clip() + # Rotate around the center of the square image + ctx.translate(self.size / 2, self.size / 2) + ctx.rotate(self._angle * math.pi / 180.0) + ctx.translate(-self.size / 2, -self.size / 2) + Gdk.cairo_set_source_pixbuf(ctx, self._image, 0, 0) + ctx.paint() + ctx.restore() + + def set_image_from_file(self, new_image_file: str): + if not new_image_file: + return + pixbuf = GdkPixbuf.Pixbuf.new_from_file(new_image_file) + self._orig_image = pixbuf + self._image = self._process_image(pixbuf) + self.queue_draw() + + def set_image_from_pixbuf(self, pixbuf: GdkPixbuf.Pixbuf): + if not pixbuf: + return + self._orig_image = pixbuf + self._image = self._process_image(pixbuf) + self.queue_draw() + + def set_image_size(self, size: int): + self.size = size + if self._orig_image: + self._image = self._process_image(self._orig_image) + self.queue_draw() diff --git a/utils/icon_resolver.py b/utils/icon_resolver.py index b8bedeb..a80c8f6 100644 --- a/utils/icon_resolver.py +++ b/utils/icon_resolver.py @@ -99,4 +99,4 @@ class IconResolver: self._get_icon_from_desktop_file(desktop_file) if desktop_file else self.default_applicaiton_icon - ) \ No newline at end of file + ) diff --git a/utils/icons.py b/utils/icons.py index ed61594..451d116 100644 --- a/utils/icons.py +++ b/utils/icons.py @@ -1,57 +1,51 @@ # Parameters -font_family: str = "tabler-icons" -font_weight: str = "normal" +font_family: str = 'tabler-icons' +font_weight: str = 'normal' span: str = f"" -# Panels -apps: str = "" +#Panels +apps: str = "" dashboard: str = "" chat: str = "" -wallpapers: str = "" windows: str = "" # Bar colorpicker: str = "" media: str = "" +#Toolbox + +toolbox: str = "" +ssfull: str = "" +ssregion: str = "" +sswindow: str = "" +screenshots: str = "" +screenrecord: str = "" +recordings: str = "" +ocr: str = "ﳃ" +gamemode: str = "" +gamemode_off: str = "" +close : str = "" + # Circles temp: str = "" disk: str = "" battery: str = "" memory: str = "流" cpu: str = "" +gpu: str = "" swap: str = "" -# Toolbox -toolbox: str = "" -ssfull: str = "" -ssregion: str = "" -screenshots: str = "" -screenrecord: str = "" -recordings: str = "" -ocr: str = "ﳃ" -gamemode: str = "" -gamemode_off: str = "" -close: str = "" -bell: str = "" -bell_off: str = "" -notifications_on: str = "" -notifications_off: str = "" -notifications_clear: str = "" -notifications: str = "" + +# AIchat +reload: str = "" +detach: str = "" # Wallpapers add: str = "" sort: str = "" circle: str = "" -check_box: str = "" -box: str = "" -scan: str = "" -scan_stop: str = "" -record: str = "" -update: str = "見" -updated: str = "" # Chevrons chevron_up: str = "" @@ -79,37 +73,32 @@ bat_low: str = "=" bat_full: str = "" -moderate_signal: str = "" -good_signal: str = "" -no_signal: str = "" -weak_signal: str = "" # Applets -lan: str = "" wifi_0: str = "" wifi_1: str = "" wifi_2: str = "" wifi_3: str = "" -wifi: str = "" world: str = "" world_off: str = "" bluetooth: str = "" night: str = "" coffee: str = "" -dnd: str = "" -darkmode: str = "﹖" - +notifications: str = "" wifi_off: str = "" bluetooth_off: str = "" night_off: str = "" -dnd_off: str = "" +notifications_off: str = "" +notifications_clear: str = ""; + +download: str = "" +upload: str = "" # Bluetooth bluetooth_connected: str = "" bluetooth_disconnected: str = "" - # Player pause: str = "" play: str = "" @@ -130,16 +119,18 @@ vol_mute: str = "" vol_medium: str = "" vol_high: str = "" - mic: str = "" -mic_muted: str = "" +mic_mute: str = "" +speaker: str = "𐁅" +headphones: str = "屮" +mic_filled: str = "️" # Overview circle_plus: str = "" # Pins -copy_plus: str = "ﶮ" +paperclip: str = "" # Clipboard Manager clipboard: str = "" @@ -149,7 +140,6 @@ clip_text: str = "" accept: str = "" cancel: str = "" trash: str = "" -clear: str = "" # Config config: str = "" @@ -161,6 +151,20 @@ spotify: str = "ﺆ" disc: str = "𐀾" disc_off: str = "" +# Brightness +brightness_low: str = "" +brightness_medium: str = "" +brightness_high: str = "" + +brightness: str = "" + +# Dashboard +widgets: str = "" +pins: str = "" +kanban: str = "" +wallpapers: str = "" +sparkles: str = "" + # Misc dot: str = "" palette: str = "" @@ -170,16 +174,24 @@ radar: str = "" emoji: str = "" keyboard: str = "" terminal: str = "" +timer_off: str = "" +timer_on: str = "" +spy: str = "" +# Dice +dice_1: str = "" +dice_2: str = "" +dice_3: str = "" +dice_4: str = "" +dice_5: str = "" +dice_6: str = "" -exceptions: list[str] = ["font_family", "font_weight", "span"] - +exceptions: list[str] = ['font_family', 'font_weight', 'span'] def apply_span() -> None: global_dict = globals() for key in global_dict: - if key not in exceptions and not key.startswith("__"): + if key not in exceptions and not key.startswith('__'): global_dict[key] = f"{span}{global_dict[key]}" - apply_span() diff --git a/utils/custom_image.py b/utils/image.py similarity index 99% rename from utils/custom_image.py rename to utils/image.py index 1aae7f6..8096a6a 100644 --- a/utils/custom_image.py +++ b/utils/image.py @@ -2,9 +2,8 @@ import math from typing import cast import cairo -from gi.repository import Gtk - from fabric.widgets.image import Image +from gi.repository import Gtk class CustomImage(Image): diff --git a/utils/occlusion.py b/utils/occlusion.py index bf06350..6f49e46 100644 --- a/utils/occlusion.py +++ b/utils/occlusion.py @@ -3,62 +3,59 @@ import json import config.data as data - def get_current_workspace(): """ Get the current workspace ID using hyprctl. """ try: result = subprocess.run( - ["hyprctl", "activeworkspace"], capture_output=True, text=True + ["hyprctl", "activeworkspace"], + capture_output=True, + text=True ) # Assume the output similar to: "ID " # Extracting the number from the output parts = result.stdout.split() for i, part in enumerate(parts): if part == "ID" and i + 1 < len(parts): - return int(parts[i + 1]) + return int(parts[i+1]) except Exception as e: print(f"Error getting current workspace: {e}") return -1 - def get_screen_dimensions(): """ Get screen dimensions from hyprctl. - + Returns: tuple: (width, height) of the monitor containing the current workspace """ try: # Get current workspace workspace_id = get_current_workspace() - + # Get monitor information result = subprocess.run( - ["hyprctl", "-j", "monitors"], capture_output=True, text=True + ["hyprctl", "-j", "monitors"], + capture_output=True, + text=True ) monitors = json.loads(result.stdout) - + # Find the monitor containing our workspace for monitor in monitors: if monitor.get("activeWorkspace", {}).get("id") == workspace_id: - return monitor.get("width", data.CURRENT_WIDTH), monitor.get( - "height", data.CURRENT_HEIGHT - ) - + return monitor.get("width", data.CURRENT_WIDTH), monitor.get("height", data.CURRENT_HEIGHT) + # Fallback to first monitor if monitors: - return monitors[0].get("width", data.CURRENT_WIDTH), monitors[0].get( - "height", data.CURRENT_HEIGHT - ) + return monitors[0].get("width", data.CURRENT_WIDTH), monitors[0].get("height", data.CURRENT_HEIGHT) except Exception as e: print(f"Error getting screen dimensions: {e}") - + # Default fallback values return data.CURRENT_WIDTH, data.CURRENT_HEIGHT - def check_occlusion(occlusion_region, workspace=None): """ Check if a region is occupied by any window on a given workspace. @@ -75,14 +72,14 @@ def check_occlusion(occlusion_region, workspace=None): """ if workspace is None: workspace = get_current_workspace() - + # Handle simplified side-based format if isinstance(occlusion_region, tuple) and len(occlusion_region) == 2: side, size = occlusion_region if isinstance(side, str): # Convert side-based format to coordinates screen_width, screen_height = get_screen_dimensions() - + if side.lower() == "bottom": occlusion_region = (0, screen_height - size, screen_width, size) elif side.lower() == "top": @@ -91,7 +88,7 @@ def check_occlusion(occlusion_region, workspace=None): occlusion_region = (0, 0, size, screen_height) elif side.lower() == "right": occlusion_region = (screen_width - size, 0, size, screen_height) - + # Ensure occlusion_region is in the correct format (x, y, width, height) if not isinstance(occlusion_region, tuple) or len(occlusion_region) != 4: print(f"Invalid occlusion region format: {occlusion_region}") @@ -99,7 +96,9 @@ def check_occlusion(occlusion_region, workspace=None): try: result = subprocess.run( - ["hyprctl", "-j", "clients"], capture_output=True, text=True + ["hyprctl", "-j", "clients"], + capture_output=True, + text=True ) clients = json.loads(result.stdout) except Exception as e: @@ -132,10 +131,7 @@ def check_occlusion(occlusion_region, workspace=None): win_x2, win_y2 = x + width, y + height # Check for intersection between the window and occlusion region - if not ( - win_x2 <= occ_x or win_x1 >= occ_x2 or win_y2 <= occ_y or win_y1 >= occ_y2 - ): + if not (win_x2 <= occ_x or win_x1 >= occ_x2 or win_y2 <= occ_y or win_y1 >= occ_y2): return True # Occlusion region is occupied return False # No window overlaps the occlusion region - diff --git a/utils/rounded_image.py b/utils/rounded_image.py new file mode 100644 index 0000000..1974e16 --- /dev/null +++ b/utils/rounded_image.py @@ -0,0 +1,37 @@ +import math +import cairo +from typing import cast +from fabric.widgets.image import Image +from gi.repository import Gtk + + +class CustomImage(Image): + def do_render_rectangle( + self, cr: cairo.Context, width: int, height: int, radius: int = 0 + ): + cr.move_to(radius, 0) + cr.line_to(width - radius, 0) + cr.arc(width - radius, radius, radius, -(math.pi / 2), 0) + cr.line_to(width, height - radius) + cr.arc(width - radius, height - radius, radius, 0, (math.pi / 2)) + cr.line_to(radius, height) + cr.arc(radius, height - radius, radius, (math.pi / 2), math.pi) + cr.line_to(0, radius) + cr.arc(radius, radius, radius, math.pi, (3 * (math.pi / 2))) + cr.close_path() + + def do_draw(self, cr: cairo.Context): + context = self.get_style_context() + width, height = self.get_allocated_width(), self.get_allocated_height() + cr.save() + + self.do_render_rectangle( + cr, + width, + height, + cast(int, context.get_property("border-radius", Gtk.StateFlags.NORMAL)), + ) + cr.clip() + Image.do_draw(self, cr) + + cr.restore() diff --git a/utils/wayland.py b/utils/wayland.py new file mode 100644 index 0000000..0dcbe51 --- /dev/null +++ b/utils/wayland.py @@ -0,0 +1,359 @@ +from gi.repository import Gdk, GObject, Gtk +import re +from collections.abc import Iterable +from enum import Enum +from typing import Literal, cast + +import cairo +import gi +from fabric.core.service import Property +from fabric.utils.helpers import extract_css_values, get_enum_member +from fabric.widgets.window import Window +from loguru import logger + +gi.require_version("Gtk", "3.0") + +try: + gi.require_version("GtkLayerShell", "0.1") + from gi.repository import GtkLayerShell +except: + raise ImportError( + "looks like we don't have gtk-layer-shell installed, make sure to install it first (as well as using wayland)" + ) + + +class WaylandWindowExclusivity(Enum): + NONE = 1 + NORMAL = 2 + AUTO = 3 + + +class Layer(GObject.GEnum): + BACKGROUND = 0 + BOTTOM = 1 + TOP = 2 + OVERLAY = 3 + ENTRY_NUMBER = 4 + + +class KeyboardMode(GObject.GEnum): + NONE = 0 + EXCLUSIVE = 1 + ON_DEMAND = 2 + ENTRY_NUMBER = 3 + + +class Edge(GObject.GEnum): + LEFT = 0 + RIGHT = 1 + TOP = 2 + BOTTOM = 3 + ENTRY_NUMBER = 4 + + +class WaylandWindow(Window): + @Property( + Layer, + flags="read-write", + default_value=Layer.TOP, + ) + def layer(self) -> Layer: # type: ignore + return self._layer # type: ignore + + @layer.setter + def layer( + self, + value: Literal["background", "bottom", "top", "overlay"] | Layer, + ) -> None: + self._layer = get_enum_member(Layer, value, default=Layer.TOP) + return GtkLayerShell.set_layer(self, self._layer) + + @Property(int, "read-write") + def monitor(self) -> int: + if not (monitor := cast(Gdk.Monitor, GtkLayerShell.get_monitor(self))): + return -1 + display = monitor.get_display() or Gdk.Display.get_default() + for i in range(0, display.get_n_monitors()): + if display.get_monitor(i) is monitor: + return i + return -1 + + @monitor.setter + def monitor(self, monitor: int | Gdk.Monitor) -> bool: + if isinstance(monitor, int): + display = Gdk.Display().get_default() + monitor = display.get_monitor(monitor) + return ( + (GtkLayerShell.set_monitor(self, monitor), True)[1] + if monitor is not None + else False + ) + + @Property(WaylandWindowExclusivity, "read-write") + def exclusivity(self) -> WaylandWindowExclusivity: + return self._exclusivity + + @exclusivity.setter + def exclusivity( + self, value: Literal["none", "normal", "auto"] | WaylandWindowExclusivity + ) -> None: + value = get_enum_member( + WaylandWindowExclusivity, value, default=WaylandWindowExclusivity.NONE + ) + self._exclusivity = value + match value: + case WaylandWindowExclusivity.NORMAL: + return GtkLayerShell.set_exclusive_zone(self, True) + case WaylandWindowExclusivity.AUTO: + return GtkLayerShell.auto_exclusive_zone_enable(self) + case _: + return GtkLayerShell.set_exclusive_zone(self, False) + + @Property(bool, "read-write", default_value=False) + def pass_through(self) -> bool: + return self._pass_through + + @pass_through.setter + def pass_through(self, pass_through: bool = False): + self._pass_through = pass_through + region = cairo.Region() if pass_through is True else None + self.input_shape_combine_region(region) + del region + return + + @Property( + KeyboardMode, + "read-write", + default_value=KeyboardMode.NONE, + ) + def keyboard_mode(self) -> KeyboardMode: + return self._keyboard_mode + + @keyboard_mode.setter + def keyboard_mode( + self, + value: Literal[ + "none", + "exclusive", + "on-demand", + "entry-number", + ] + | KeyboardMode, + ): + self._keyboard_mode = get_enum_member( + KeyboardMode, value, default=KeyboardMode.NONE + ) + return GtkLayerShell.set_keyboard_mode(self, self._keyboard_mode) + + @Property(tuple[Edge, ...], "read-write") + def anchor(self): + return tuple( + x + for x in [ + Edge.TOP, + Edge.RIGHT, + Edge.BOTTOM, + Edge.LEFT, + ] + if GtkLayerShell.get_anchor(self, x) + ) + + @anchor.setter + def anchor(self, value: str | Iterable[Edge]) -> None: + self._anchor = value + if isinstance(value, (list, tuple)) and all( + isinstance(edge, Edge) for edge in value + ): + for edge in [ + Edge.TOP, + Edge.RIGHT, + Edge.BOTTOM, + Edge.LEFT, + ]: + if edge not in value: + GtkLayerShell.set_anchor(self, edge, False) + GtkLayerShell.set_anchor(self, edge, True) + return + elif isinstance(value, str): + for edge, anchored in WaylandWindow.extract_edges_from_string( + value + ).items(): + GtkLayerShell.set_anchor(self, edge, anchored) + + return + + @Property(tuple[int, ...], flags="read-write") + def margin(self) -> tuple[int, ...]: + return tuple( + GtkLayerShell.get_margin(self, x) + for x in [ + Edge.TOP, + Edge.RIGHT, + Edge.BOTTOM, + Edge.LEFT, + ] + ) + + @margin.setter + def margin(self, value: str | Iterable[int]) -> None: + for edge, mrgv in WaylandWindow.extract_margin(value).items(): + GtkLayerShell.set_margin(self, edge, mrgv) + return + + @Property(object, "read-write") + def keyboard_mode(self): + kb_mode = GtkLayerShell.get_keyboard_mode(self) + if GtkLayerShell.get_keyboard_interactivity(self): + kb_mode = KeyboardMode.EXCLUSIVE + return kb_mode + + @keyboard_mode.setter + def keyboard_mode( + self, + value: Literal["none", "exclusive", "on-demand"] | KeyboardMode, + ): + return GtkLayerShell.set_keyboard_mode( + self, + get_enum_member( + KeyboardMode, + value, + default=KeyboardMode.NONE, + ), + ) + + def __init__( + self, + layer: Literal["background", "bottom", "top", "overlay"] | Layer = Layer.TOP, + anchor: str = "", + margin: str | Iterable[int] = "0px 0px 0px 0px", + exclusivity: Literal["auto", "normal", "none"] + | WaylandWindowExclusivity = WaylandWindowExclusivity.NONE, + keyboard_mode: Literal["none", "exclusive", "on-demand"] + | KeyboardMode = KeyboardMode.NONE, + pass_through: bool = False, + monitor: int | Gdk.Monitor | None = None, + title: str = "fabric", + type: Literal["top-level", "popup"] | Gtk.WindowType = Gtk.WindowType.TOPLEVEL, + child: Gtk.Widget | None = None, + name: str | None = None, + visible: bool = True, + all_visible: bool = False, + style: str | None = None, + style_classes: Iterable[str] | str | None = None, + tooltip_text: str | None = None, + tooltip_markup: str | None = None, + h_align: Literal["fill", "start", "end", "center", "baseline"] + | Gtk.Align + | None = None, + v_align: Literal["fill", "start", "end", "center", "baseline"] + | Gtk.Align + | None = None, + h_expand: bool = False, + v_expand: bool = False, + size: Iterable[int] | int | None = None, + **kwargs, + ): + Window.__init__( + self, + title=title, + type=type, + child=child, + name=name, + visible=False, + all_visible=False, + style=style, + style_classes=style_classes, + tooltip_text=tooltip_text, + tooltip_markup=tooltip_markup, + h_align=h_align, + v_align=v_align, + h_expand=h_expand, + v_expand=v_expand, + size=size, + **kwargs, + ) + self._layer = Layer.ENTRY_NUMBER + self._keyboard_mode = KeyboardMode.NONE + self._anchor = anchor + self._exclusivity = WaylandWindowExclusivity.NONE + self._pass_through = pass_through + + GtkLayerShell.init_for_window(self) + GtkLayerShell.set_namespace(self, title) + self.connect( + "notify::title", + lambda *_: GtkLayerShell.set_namespace(self, self.get_title()), + ) + if monitor is not None: + self.monitor = monitor + self.layer = layer + self.anchor = anchor + self.margin = margin + self.keyboard_mode = keyboard_mode + self.exclusivity = exclusivity + self.pass_through = pass_through + self.show_all() if all_visible is True else self.show() if visible is True else None + + def steal_input(self) -> None: + return GtkLayerShell.set_keyboard_interactivity(self, True) + + def return_input(self) -> None: + return GtkLayerShell.set_keyboard_interactivity(self, False) + + # custom overrides + def show(self) -> None: + super().show() + return self.do_handle_post_show_request() + + def show_all(self) -> None: + super().show_all() + return self.do_handle_post_show_request() + + def do_handle_post_show_request(self) -> None: + if not self.get_children(): + logger.warning( + "[WaylandWindow] showing an empty window is not recommended, some compositors might freak out." + ) + self.pass_through = self._pass_through + return + + @staticmethod + def extract_anchor_values(string: str) -> tuple[str, ...]: + """ + extracts the geometry values from a given geometry string. + + :param string: the string containing the geometry values. + :type string: str + :return: a list of unique directions extracted from the geometry string. + :rtype: list + """ + direction_map = {"l": "left", "t": "top", "r": "right", "b": "bottom"} + pattern = re.compile(r"\b(left|right|top|bottom)\b", re.IGNORECASE) + matches = pattern.findall(string) + return tuple(set(tuple(direction_map[match.lower()[0]] for match in matches))) + + @staticmethod + def extract_edges_from_string(string: str) -> dict["Edge", bool]: + anchor_values = WaylandWindow.extract_anchor_values(string.lower()) + return { + Edge.TOP: "top" in anchor_values, + Edge.RIGHT: "right" in anchor_values, + Edge.BOTTOM: "bottom" in anchor_values, + Edge.LEFT: "left" in anchor_values, + } + + @staticmethod + def extract_margin(input: str | Iterable[int]) -> dict["Edge", int]: + margins = ( + extract_css_values(input.lower()) + if isinstance(input, str) + else input + if isinstance(input, (tuple, list)) and len(input) == 4 + else (0, 0, 0, 0) + ) + return { + Edge.TOP: margins[0], + Edge.RIGHT: margins[1], + Edge.BOTTOM: margins[2], + Edge.LEFT: margins[3], + }