diff --git a/config/__init__.py b/config/__init__.py deleted file mode 100644 index f94e240..0000000 --- a/config/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .data import * - -# 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 diff --git a/config/assets/accounts.json b/config/assets/accounts.json deleted file mode 100644 index 9e26dfe..0000000 --- a/config/assets/accounts.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of 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.py b/config/config.py deleted file mode 100644 index 44b7424..0000000 --- a/config/config.py +++ /dev/null @@ -1,1292 +0,0 @@ -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 -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}") - - -def open_config(): - """ - Entry point for opening the configuration GUI using Fabric Application. - """ - load_bind_vars() - - 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 - 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) - - 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) - shutil.copy(src_idle, dest_idle) - show_idle_checkbox = False - print(f"Copied default hypridle config to {dest_idle}") - except Exception as e: - print(f"Error copying default hypridle config: {e}") - show_idle_checkbox = os.path.exists(src_idle) - - app = Application(f"{APP_NAME}-settings") - window = HyprConfGUI( - show_lock_checkbox=show_lock_checkbox, - show_idle_checkbox=show_idle_checkbox, - application=app, - on_destroy=lambda *_: app.quit(), - ) - app.add_window(window) - - window.show_all() - app.run() - - -if __name__ == "__main__": - open_config() diff --git a/config/data.py b/config/data.py deleted file mode 100644 index f3b3271..0000000 --- a/config/data.py +++ /dev/null @@ -1,120 +0,0 @@ -import os -import json -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}") - - -USERNAME = os.getlogin() -HOSTNAME = os.uname().nodename -HOME_DIR = os.path.expanduser("~") - -CONFIG_DIR = os.path.expanduser(f"~/{APP_NAME}") - -screen = Gdk.Screen.get_default() -CURRENT_WIDTH = screen.get_width() -CURRENT_HEIGHT = screen.get_height() - -WALLPAPERS_DIR_DEFAULT = get_relative_path("../assets/wallpaper") -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 - - # 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 = {} - 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 diff --git a/config/hypr/colors.conf b/config/hypr/colors.conf deleted file mode 100644 index e839fe7..0000000 --- a/config/hypr/colors.conf +++ /dev/null @@ -1,11 +0,0 @@ -$wallpaper = /home/sankalp/Pictures/wallpapers/background.png -$background = 0e1514 -$foreground = dde4e2 - -$primary = 81d5ca -$secondary = b1ccc7 -$tertiary = aec9e6 -$surface = 0e1514 -$surface_bright = 343a39 -$outline = 899390 -$error = ff5945 diff --git a/config/hypr/hypridle.conf b/config/hypr/hypridle.conf deleted file mode 100644 index 48bd7d7..0000000 --- a/config/hypr/hypridle.conf +++ /dev/null @@ -1,41 +0,0 @@ - -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. - 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. -} - -listener { - timeout = 150 # 2.5min. - on-timeout = brightnessctl -s set 10 # set monitor backlight to minimum, avoid 0 on OLED monitor. - 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 -} - -listener { - timeout = 330 # 5.5min - on-timeout = hyprctl dispatch dpms off # screen off when timeout has passed - on-resume = hyprctl dispatch dpms on # screen on when activity is detected after timeout has fired. -} - -listener { - timeout = 1800 # 30min - on-timeout = systemctl suspend # suspend pc -} diff --git a/config/hypr/hyprlock.conf b/config/hypr/hyprlock.conf deleted file mode 100644 index 80977af..0000000 --- a/config/hypr/hyprlock.conf +++ /dev/null @@ -1,150 +0,0 @@ - - -source = ~/Modus/config/hypr/colors.conf - -# 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 - } - - -# 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 - } -# - - diff --git a/config/hypr/modus.conf b/config/hypr/modus.conf deleted file mode 100644 index edd9ec3..0000000 --- a/config/hypr/modus.conf +++ /dev/null @@ -1,88 +0,0 @@ -# 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 - -$fabricSend = fabric-cli exec modus -$scriptsDir = /home/sankalp/Modus/config/scripts/ - -# 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 - -# Wallpapers directory: /home/sankalp/Pictures/wallpapers - -# Source color scheme -source = /home/sankalp/Modus/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 -} diff --git a/config/matugen/templates/hyprland-colors.conf b/config/matugen/templates/hyprland-colors.conf deleted file mode 100644 index a375c24..0000000 --- a/config/matugen/templates/hyprland-colors.conf +++ /dev/null @@ -1,11 +0,0 @@ -$wallpaper = {{image}} -$background = {{colors.background.default.hex_stripped}} -$foreground = {{colors.on_background.default.hex_stripped}} - -$primary = {{colors.primary.default.hex_stripped}} -$secondary = {{colors.secondary.default.hex_stripped}} -$tertiary = {{colors.tertiary.default.hex_stripped}} -$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}} diff --git a/config/matugen/templates/modus.css b/config/matugen/templates/modus.css deleted file mode 100644 index db192d7..0000000 --- a/config/matugen/templates/modus.css +++ /dev/null @@ -1,44 +0,0 @@ -:vars { - --foreground: {{colors.on_background.default.hex}}; - --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}}; - --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}}; - -} 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/scripts/capture.sh b/config/scripts/capture.sh deleted file mode 100755 index 8204108..0000000 --- a/config/scripts/capture.sh +++ /dev/null @@ -1,166 +0,0 @@ -#!/bin/bash - -# Print usage information -print_usage() { - cat <<"EOF" -Usage: screen-tools.sh [mode] [option] -Modes: - screenshot [option] Take a screenshot - Options: - p Print all screens - s Snip current screen - sf Snip current screen (frozen) - m Print focused monitor - [mockup] Optional: Add mockup effect to screenshot - - record Start/Stop screen recording - - ocr Perform OCR on selected area -EOF -} - -# OCR functionality -do_ocr() { - sleep 0.25 - - ocr_text=$(grimblast --freeze save area - | tesseract -l eng - - 2>/dev/null) - - # 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" - else - notify-send -a "Modus" "OCR Failed" "No text recognized or operation failed" - fi -} - -# 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" - - if pgrep -f "gpu-screen-recorder" >/dev/null; then - pkill -SIGINT -f "gpu-screen-recorder" - 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") - - if [ "$action" = "view" ] && [ -n "$last_video" ]; then - xdg-open "$last_video" - elif [ "$action" = "open" ]; then - xdg-open "$save_dir" - fi - exit 0 - fi - - local output_file="$save_dir/$(date +%Y-%m-%d-%H-%M-%S).mp4" - notify-send -a "Modus" "๐Ÿ”ด Recording started" - gpu-screen-recorder -w screen -q ultra -a default_output -ac opus -cr full -f 60 -o "$output_file" -} - -# Screenshot functionality -do_screenshot() { - sleep 0.25 - - if [ -z "$XDG_PICTURES_DIR" ]; then - XDG_PICTURES_DIR="$HOME/Pictures" - fi - - local save_dir="${3:-$XDG_PICTURES_DIR/Screenshots}" - local save_file=$(date +'%y%m%d_%Hh%Mm%Ss_screenshot.png') - local full_path="$save_dir/$save_file" - mkdir -p "$save_dir" - - 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" ;; - *) - echo "Invalid screenshot mode" - print_usage - exit 1 - ;; - 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 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 - - local action=$(notify-send -a "Modus" -i "$full_path" "Screenshot saved" "in $full_path" \ - -A "view=View" -A "edit=Edit" -A "open=Open Folder") - - case "$action" in - view) xdg-open "$full_path" ;; - edit) swappy -f "$full_path" ;; - open) xdg-open "$save_dir" ;; - esac - else - notify-send -a "Modus" "Screenshot Aborted" - fi -} - -# Handle command line arguments -if [ $# -eq 0 ]; then - print_usage - exit 1 -fi - -case "$1" in -screenshot) - if [ $# -lt 2 ]; then - echo "Error: Screenshot mode requires an option" - print_usage - exit 1 - fi - do_screenshot "$2" "$3" - ;; -record) - do_screenrecord - ;; -ocr) - do_ocr - ;; -*) - echo "Error: Invalid mode '$1'" - print_usage - exit 1 - ;; -esac diff --git a/config/scripts/gamemode.sh b/config/scripts/gamemode.sh deleted file mode 100755 index 3652ccd..0000000 --- a/config/scripts/gamemode.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env sh - -# Check if animations are disabled (game mode is active) -check_gamemode() { - HYPRGAMEMODE=$(hyprctl getoption animations:enabled | awk 'NR==1{print $2}') - if [ "$HYPRGAMEMODE" = 0 ]; then - echo "t" - return 0 - else - echo "f" - return 1 - fi -} - -# Toggle game mode state -toggle_gamemode() { - HYPRGAMEMODE=$(hyprctl getoption animations:enabled | awk 'NR==1{print $2}') - if [ "$HYPRGAMEMODE" = 1 ]; then - hyprctl --batch "\ - keyword animations:enabled 0;\ - keyword decoration:shadow:enabled 0;\ - keyword decoration:blur:enabled 0;\ - keyword general:gaps_in 0;\ - keyword general:gaps_out 0;\ - keyword general:border_size 1;\ - keyword decoration:rounding 0" - exit - fi - hyprctl reload -} - -# Main script logic -case "$1" in -check) - check_gamemode - ;; -*) - toggle_gamemode - ;; -esac diff --git a/config/scripts/hyprpicker.sh b/config/scripts/hyprpicker.sh deleted file mode 100755 index 703c7df..0000000 --- a/config/scripts/hyprpicker.sh +++ /dev/null @@ -1,75 +0,0 @@ -#!/bin/bash - -pick_rgb() { - - # Execute hyprpicker with RGB format and save the output to a variable - hyprpicker -a -n -f rgb && sleep 0.1 - - # Create a temporal 64x64 PNG file with the color in /tmp using convert - magick -size 64x64 xc:"rgb($(wl-paste))" /tmp/color.png - - # Send a notification using the file as an icon - notify-send "RGB color picked" "rgb($(wl-paste))" -i /tmp/color.png -a "Hyprpicker" - - # Remove the temporal file - rm /tmp/color.png - - # Exit - exit 0 - -} - -pick_hex() { - - # Execute hyprpicker and save the output to a variable - hyprpicker -a -n -f hex && sleep 0.1 - - # Create a temporal 64x64 PNG file with the color in /tmp using convert - magick -size 64x64 xc:"$(wl-paste)" /tmp/color.png - - # Send a notification using the file as an icon - notify-send "HEX color picked" "$(wl-paste)" -i /tmp/color.png -a "Hyprpicker" - - # Remove the temporal file - rm /tmp/color.png - - # Exit - exit 0 - -} - -pick_hsv() { - - # Copy the color to the clipboard - echo -n "$(hyprpicker -n -f hsv)" | wl-copy -n - - # Create a temporal 64x64 PNG file with the color in /tmp using convert - magick -size 64x64 xc:"hsv($(wl-paste))" /tmp/color.png - - # Send a notification using the file as an icon - notify-send "HSV color picked" "hsv($(wl-paste))" -i /tmp/color.png -a "Hyprpicker" - - # Remove the temporal file - rm /tmp/color.png - - # Exit - exit 0 - -} - -case "$1" in --rgb) - pick_rgb - ;; --hsv) - pick_hsv - ;; --hex) - pick_hex - ;; - -*) - echo "Usage: $0 [-rgb|-hex|-hsv]" - exit 1 - ;; -esac diff --git a/config/scripts/systemupdates.sh b/config/scripts/systemupdates.sh deleted file mode 100755 index a2677ff..0000000 --- a/config/scripts/systemupdates.sh +++ /dev/null @@ -1,182 +0,0 @@ -#!/bin/bash - -# Word of caution: Changing this may break the update module, be careful! - -# Function to check updates for Arch-based distros -check_arch_updates() { - official_updates=0 - aur_updates=0 - flatpak_updates=0 - tooltip="" - - # Get the number of official updates - official_updates=$(checkupdates 2>/dev/null | wc -l) - - # Determine the AUR helper - if command -v paru &>/dev/null; then - aur_helper="paru" - else - aur_helper="yay" - fi - - # Get the number of AUR updates - aur_updates=$($aur_helper -Qum 2>/dev/null | wc -l) - - # Check for Flatpak updates - if command -v flatpak &>/dev/null; then - flatpak_updates=$(flatpak remote-ls --updates | wc -l) - fi - - # Calculate total updates - total_updates=$((official_updates + aur_updates + flatpak_updates)) - - # Build the tooltip - tooltip="๓ฐฃ‡ Official: $official_updates\n๓ฐฎฏ AUR: $aur_updates\n๏’‡ Flatpak: $flatpak_updates" - - # Output JSON - echo "{\"total\":\"$total_updates\", \"tooltip\":\"$tooltip\"}" -} - -# Function to check updates for Ubuntu-based distros -check_ubuntu_updates() { - official_updates=0 - flatpak_updates=0 - tooltip="" - - # Get the number of official updates - official_updates=$(apt-get -s -o Debug::NoLocking=true upgrade | grep -c ^Inst) - - # Check for Flatpak updates - if command -v flatpak &>/dev/null; then - flatpak_updates=$(flatpak remote-ls --updates | wc -l) - fi - - # Calculate total updates - total_updates=$((official_updates + flatpak_updates)) - - # Build the tooltip - tooltip="๓ฐ•ˆ Official: $official_updates\n๏’‡ Flatpak: $flatpak_updates" - - # Output JSON - echo "{\"total\":\"$total_updates\", \"tooltip\":\"$tooltip\"}" -} - -# Function to check updates for Fedora-based distros -check_fedora_updates() { - official_updates=0 - flatpak_updates=0 - tooltip="" - - # Get the number of official updates - official_updates=$(dnf list updates -q | tail -n +2 | wc -l) - - # Check for Flatpak updates - if command -v flatpak &>/dev/null; then - flatpak_updates=$(flatpak remote-ls --updates | wc -l) - fi - - # Calculate total updates - total_updates=$((official_updates + flatpak_updates)) - - # Build the tooltip - tooltip="๓ฐฃ› Official: $official_updates\n๏’‡ Flatpak: $flatpak_updates" - - # Output JSON - echo "{\"total\":\"$total_updates\", \"tooltip\":\"$tooltip\"}" -} - -# Function to check updates for openSUSE-based distros -check_opensuse_updates() { - official_updates=0 - flatpak_updates=0 - tooltip="" - - # Get the number of official updates (skip header lines) - official_updates=$(zypper lu | tail -n +3 | wc -l) - - # Check for Flatpak updates - if command -v flatpak &>/dev/null; then - flatpak_updates=$(flatpak remote-ls --updates | wc -l) - fi - - # Calculate total updates - total_updates=$((official_updates + flatpak_updates)) - - # Build the tooltip - tooltip="๎ฝญ Official: $official_updates\n๏’‡ Flatpak: $flatpak_updates" - - # Output JSON - echo "{\"total\":\"$total_updates\", \"tooltip\":\"$tooltip\"}" -} - -# Function to update Arch-based distros -update_arch() { - kitty --title "System Update" sh -c " - fastfetch - yay -Syu - read -n 1 -p 'Press any key to continue...' - " -} - -# Function to update Ubuntu-based distros -update_ubuntu() { - kitty --title "System Update" sh -c " - neofetch - sudo apt update && sudo apt upgrade -y - read -n 1 -p 'Press any key to continue...' - " -} - -# Function to update Fedora-based distros -update_fedora() { - kitty --title "System Update" sh -c " - neofetch - sudo dnf upgrade -y - read -n 1 -p 'Press any key to continue...' - " -} - -# Function to update openSUSE-based distros -update_opensuse() { - kitty --title "System Update" sh -c " - neofetch - sudo zypper up -y - read -n 1 -p 'Press any key to continue...' - " -} - -# Main case handling for different distributions -case "$1" in --arch) - if [ -z "$2" ]; then - check_arch_updates - else - update_arch - fi - ;; --ubuntu) - if [ -z "$2" ]; then - check_ubuntu_updates - else - update_ubuntu - fi - ;; --fedora) - if [ -z "$2" ]; then - check_fedora_updates - else - update_fedora - fi - ;; --suse) - if [ -z "$2" ]; then - check_opensuse_updates - else - update_opensuse - fi - ;; -*) - echo "Usage: $0 [-arch|-ubuntu|-fedora|-suse] [up (optional)]" - exit 1 - ;; -esac diff --git a/main.py b/main.py deleted file mode 100644 index 7488f5d..0000000 --- a/main.py +++ /dev/null @@ -1,61 +0,0 @@ -import os - -import setproctitle -from fabric import Application -from fabric.utils import get_relative_path, monitor_file, exec_shell_command_async -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 - - -for log in [ - "fabric.hyprland.widgets", - "fabric.audio.service", - "fabric.bluetooth.service", -]: - 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 - - app = Application(f"{APP_NAME}", bar, launcher, osd, dock) - - # Monitor CSS files for changes - css_file = monitor_file(get_relative_path("styles")) - _ = css_file.connect("changed", lambda *_: set_css()) - - color_css_file = monitor_file(get_relative_path("./styles/colors.css")) - _ = color_css_file.connect("changed", lambda *_: set_css()) - - 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_css = set_css - - app.set_css() - - app.run() diff --git a/modules/bar/bar.py b/modules/bar/bar.py deleted file mode 100644 index 46a9d5f..0000000 --- a/modules/bar/bar.py +++ /dev/null @@ -1,312 +0,0 @@ -from fabric.hyprland.widgets import HyprlandLanguage, 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 utils.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 HyprlandLanguage().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 e24a3d1..0000000 --- a/modules/bar/components/workspace.py +++ /dev/null @@ -1,32 +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/dock.py b/modules/dock.py deleted file mode 100644 index 0821fbf..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 utils.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/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 ed83c49..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 utils.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 6d5bd7c..0000000 --- a/modules/notification_popup.py +++ /dev/null @@ -1,306 +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 utils.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 4f4521f..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 utils.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/services/__init__.py b/services/__init__.py deleted file mode 100644 index cdba666..0000000 --- a/services/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -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() diff --git a/services/brightness.py b/services/brightness.py deleted file mode 100644 index 49e7040..0000000 --- a/services/brightness.py +++ /dev/null @@ -1,115 +0,0 @@ -import os -import subprocess - -from fabric.core.service import Property, Service, Signal -from fabric.utils import exec_shell_command, monitor_file -from gi.repository import GLib -from loguru import logger # Import loguru 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, - ) - - # 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}") - - -# 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 - ) - - -class Brightness(Service): - # Service to manage screen brightness levels. - - @Signal - def screen(self, value: int) -> None: - """Signal emitted when screen brightness changes.""" - pass # 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) - - # 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())) - ), - ) - - # Log the initialization of the service - logger.info(f"Brightness service initialized for device: {screen_device}") - - def _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 -1 # Return -1 if file doesn't exist, indicating an error. - - @Property(int, "read-write") - def screen_brightness(self) -> int: - # Property to get or set the screen brightness. - 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 -1 # Return -1 if file doesn't exist, indicating error. - - @screen_brightness.setter - def screen_brightness(self, value: int): - # Setter for screen brightness property. - if not (0 <= value <= self.max_screen): - value = max(0, min(value, self.max_screen)) - - 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})") - 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}") diff --git a/services/network.py b/services/network.py deleted file mode 100644 index 04ebcae..0000000 --- a/services/network.py +++ /dev/null @@ -1,334 +0,0 @@ -from typing import Any, List, Literal - -import gi -from fabric.core.service import Property, Service, Signal -from fabric.utils import bulk_connect, exec_shell_command_async -from gi.repository import Gio -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") - - -class Wifi(Service): - """A service to manage wifi devices""" - - @Signal - def changed(self) -> None: ... - - @Signal - def enabled(self) -> bool: ... - - @Signal - def scanning(self, is_scanning: bool) -> 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 - super().__init__(**kwargs) - - self._client.connect( - "notify::wireless-enabled", - lambda *args: self.notifier("enabled", args), - ) - 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.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 - - self._ap_signal = self._ap.connect( - "notify::strength", lambda *args: self.ap_update() - ) # type: ignore - - def toggle_wifi(self): - self._client.wireless_set_enabled(not self._client.wireless_get_enabled()) - - # def set_active_ap(self, ap): - # self._device.access - - 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( - None, - lambda device, result: [ - device.request_scan_finish(result), - self.emit( - "scanning", False - ), # Emit signal that scanning has stopped - ], - ) - - 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""" - - @Signal - def changed(self) -> None: ... - - @Signal - def enabled(self) -> bool: ... - - @Property(int, "readable") - def speed(self) -> int: - return self._device.get_speed() - - @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", - ) - - @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 - - for names in ( - "active-connection", - "icon-name", - "internet", - "speed", - "state", - ): - self._device.connect(f"notify::{names}", lambda *_: self.notifier(names)) - - self._device.connect("notify::speed", lambda *_: print(_)) - - def notifier(self, names): - self.notify(names) - 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() 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 deleted file mode 100644 index f30081e..0000000 --- a/styles/colors.css +++ /dev/null @@ -1,44 +0,0 @@ -:vars { - --foreground: #f1dfdb; - --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; - --surface: #1a1110; - --surface-container: #271d1b; - --surface-bright: #423734; - --surface-variant: #534340; - --surface-container-low: #231918; - --on-surface: #f1dfdb; - --error: #ffb4ab; - --error-dim: #ff8678; - --on-error: #690005; - --error-container: #93000a; - --on-error-container: #ffdad6; - --outline: #a08c88; - --outline-variant: #534340; - --shadow: #000000; - --red: #ffb4a7; - --red-dim: #ff8974; - --green: #b7d085; - --green-dim: #a1c260; - --yellow: #dec56e; - --yellow-dim: #d5b444; - --blue: #cebdfe; - --blue-dim: #a98bfd; - --magenta: #fcb0d5; - --magenta-dim: #fa7fbb; - --cyan: #84d5c4; - --cyan-dim: #5ec8b2; - --white: #82d3e0; - -} 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 deleted file mode 100644 index c0a9860..0000000 --- a/styles/dock.css +++ /dev/null @@ -1,78 +0,0 @@ -#dock { - background-color: var(--surface); - padding: 8px; - margin: 8px 8px 7px 8px; - border-radius: 20px 20px 0 0; - transition: all 0.25s cubic-bezier(0.5, 0.25, 0, 1); -} - -#dock.vertical { - margin: 8px 79px 8px 8px; - border-radius: 20px 0 0 20px; -} - -#dock.vertical-left { - margin: 8px 39px 8px 0px; - border-radius: 0 20px 20px 0; -} - -#dock.vertical-right{ - margin: 8px 79px 8px 8px; - border-radius: 20px 0 0 20px; -} - -#dock-full { - transition: all 0.25s cubic-bezier(0.5, 0.25, 0, 1); - opacity: 1; -} - -#dock-full.occluded { - @apply dock_nmargin(); -} - -#dock-separator { - padding: 1px; - @apply dock_sep(); - border-radius: 16px; - background-color: var(--surface-bright); -} - -#dock-app-button { - padding: 4px; - border-radius: 12px; -} - -#dock-app-button:hover, -#dock-app-button:hover.instance { - background-color: var(--surface-bright); - border-top: 1px solid var(--primary); - padding-top: 3px; -} - -#dock-app-button:active, -#dock-app-button:active.instance { - background-color: var(--primary); -} - -#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; -} - -#dock-corner-left { - margin: 0 -8px 7px 0; -} - -#dock-corner-right { - margin: 0 0 7px -8px; -} - -#dock-corner-top { - margin: 0 79px -8px 0; -} - -#dock-corner-bottom { - margin: -8px 79px 0 0; -} 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 deleted file mode 100644 index 43abad3..0000000 --- a/utils/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .custom_image import * -from .icon_resolver import * -from .occlusion import * -from .animator import * diff --git a/utils/animator.py b/utils/animator.py deleted file mode 100644 index 1799ae6..0000000 --- a/utils/animator.py +++ /dev/null @@ -1,179 +0,0 @@ -from typing import cast - -from fabric import Property, Service, Signal -from gi.repository import GLib, Gtk - - -class Animator(Service): - @Signal - def finished(self) -> None: ... - - @Property(tuple[float, float, float, float], "read-write") - def bezier_curve(self) -> tuple[float, float, float, float]: - return self._bezier_curve - - @bezier_curve.setter - def bezier_curve(self, value: tuple[float, float, float, float]): - self._bezier_curve = value - return - - @Property(float, "read-write") - def value(self): - return self._value - - @value.setter - def value(self, value: float): - self._value = value - return - - @Property(float, "read-write") - def max_value(self): - return self._max_value - - @max_value.setter - def max_value(self, value: float): - self._max_value = value - return - - @Property(float, "read-write") - def min_value(self): - return self._min_value - - @min_value.setter - def min_value(self, value: float): - self._min_value = value - return - - @Property(bool, "read-write", default_value=False) - def playing(self): - return self._playing - - @playing.setter - def playing(self, value: bool): - self._playing = value - return - - @Property(bool, "read-write", default_value=False) - def repeat(self): - return self._repeat - - @repeat.setter - def repeat(self, value: bool): - self._repeat = value - return - - def __init__( - self, - bezier_curve: tuple[float, float, float, float], - duration: float, - min_value: float = 0.0, - max_value: float = 1.0, - repeat: bool = False, - tick_widget: Gtk.Widget | None = None, - **kwargs, - ): - super().__init__(**kwargs) - self._bezier_curve = (1, 0, 1, 1) - self._duration = 5 - self._value = 0.0 - self._min_value = 0.0 - self._max_value = 1.0 - self._repeat = False - - self.bezier_curve = bezier_curve - self.duration = duration - self.value = min_value - self.min_value = min_value - self.max_value = max_value - self.repeat = repeat - - self.playing = False - self._start_time = None - self._tick_handler = None - self._timeline_pos = 0 - self._tick_widget = tick_widget - - def do_get_time_now(self): - return GLib.get_monotonic_time() / 1_000_000 - - def do_lerp(self, start: float, end: float, time: float) -> float: - return start + (end - start) * time - - def do_interpolate_cubic_bezier(self, time: float) -> float: - y_points = (0, self.bezier_curve[1], self.bezier_curve[3], 1) - return ( - (1 - time) ** 3 * y_points[0] - + 3 * (1 - time) ** 2 * time * y_points[1] - + 3 * (1 - time) * time**2 * y_points[2] - + time**3 * y_points[3] - ) - - def do_ease(self, time: float) -> float: - return self.do_lerp( - self.min_value, self.max_value, self.do_interpolate_cubic_bezier(time) - ) - - def do_update_value(self, delta_time: float): - if not self.playing: - return - - elapsed_time = delta_time - cast(float, self._start_time) - - self._timeline_pos = min(1, elapsed_time / self.duration) - - self.value = self.do_ease(self._timeline_pos) - - if not self._timeline_pos >= 1: - return - - if not self.repeat: - self.value = self.max_value - self.finished() - self.pause() - return - - self._start_time = delta_time - self._timeline_pos = 0 - return - - def do_handle_tick(self, *_): - current_time = self.do_get_time_now() - self.do_update_value(current_time) - return True - - def do_remove_tick_handlers(self): - if self._tick_handler: - if self._tick_widget: - self._tick_widget.remove_tick_callback(self._tick_handler) - else: - GLib.source_remove(self._tick_handler) - self._tick_handler = None - return - - def play(self): - if self.playing: - return - - self._start_time = self.do_get_time_now() - - if not self._tick_handler: - if self._tick_widget: - self._tick_handler = self._tick_widget.add_tick_callback( - self.do_handle_tick - ) - else: - self._tick_handler = GLib.timeout_add(16, self.do_handle_tick) - - self.playing = True - return - - def pause(self): - self.playing = False - return self.do_remove_tick_handlers() - - def stop(self): - if not self._tick_handler: - self._timeline_pos = 0 - self.playing = False - return - return self.do_remove_tick_handlers() diff --git a/utils/custom_image.py b/utils/custom_image.py deleted file mode 100644 index 1aae7f6..0000000 --- a/utils/custom_image.py +++ /dev/null @@ -1,39 +0,0 @@ -import math -from typing import cast - -import cairo -from gi.repository import Gtk - -from fabric.widgets.image import Image - - -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/icon_resolver.py b/utils/icon_resolver.py deleted file mode 100644 index b8bedeb..0000000 --- a/utils/icon_resolver.py +++ /dev/null @@ -1,102 +0,0 @@ -import json -import os -import re - -import gi - -gi.require_version("Gtk", "3.0") -from gi.repository import GLib, Gtk -from loguru import logger - -import config.data as data - -ICON_CACHE_FILE = data.CACHE_DIR + "/icons.json" -if not os.path.exists(data.CACHE_DIR): - os.makedirs(data.CACHE_DIR) - - -class IconResolver: - def __init__(self, default_applicaiton_icon: str = "application-x-executable-symbolic"): - if os.path.exists(ICON_CACHE_FILE): - with open(ICON_CACHE_FILE) as f: - try: - self._icon_dict = json.load(f) - except json.JSONDecodeError: - logger.info("[ICONS] Cache file does not exist or is corrupted") - self._icon_dict = {} - else: - self._icon_dict = {} - - self.default_applicaiton_icon = default_applicaiton_icon - - def get_icon_name(self, app_id: str): - if app_id in self._icon_dict: - return self._icon_dict[app_id] - new_icon = self._compositor_find_icon(app_id) - logger.info( - f"[ICONS] found new icon: '{new_icon}' for app id: '{app_id}', storing..." - ) - self._store_new_icon(app_id, new_icon) - return new_icon - - def get_icon_pixbuf(self, app_id: str, size: int = 16): - icon_theme = Gtk.IconTheme.get_default() - icon_name = self.get_icon_name(app_id) - try: - # Try to load the resolved icon. - return icon_theme.load_icon(icon_name, size, Gtk.IconLookupFlags.FORCE_SIZE) - except GLib.Error as primary_error: - logger.warning( - f"Warning: Icon '{icon_name}' not found in theme. Error: {primary_error}" - ) - try: - # Fallback to the default application icon. - return icon_theme.load_icon( - self.default_applicaiton_icon, size, Gtk.IconLookupFlags.FORCE_SIZE - ) - except GLib.Error as fallback_error: - logger.error( - f"Error: Fallback icon '{self.default_applicaiton_icon}' also not found. Error: {fallback_error}" - ) - return None - - def _store_new_icon(self, app_id: str, icon: str): - self._icon_dict[app_id] = icon - with open(ICON_CACHE_FILE, "w") as f: - json.dump(self._icon_dict, f) - - def _get_icon_from_desktop_file(self, desktop_file_path: str): - # Retrieve the icon specified in the [Desktop Entry] section. - with open(desktop_file_path) as f: - for line in f.readlines(): - if "Icon=" in line: - return "".join(line[5:].split()) - return self.default_applicaiton_icon - - def _get_desktop_file(self, app_id: str) -> str | None: - data_dirs = GLib.get_system_data_dirs() - for data_dir in data_dirs: - data_dir = os.path.join(data_dir, "applications") - if os.path.exists(data_dir): - files = os.listdir(data_dir) - matching = [s for s in files if "".join(app_id.lower().split()) in s.lower()] - if matching: - return os.path.join(data_dir, matching[0]) - for word in list(filter(None, re.split(r"-|\.|_|\s", app_id))): - matching = [s for s in files if word.lower() in s.lower()] - if matching: - return os.path.join(data_dir, matching[0]) - return None - - def _compositor_find_icon(self, app_id: str): - icon_theme = Gtk.IconTheme.get_default() - if icon_theme.has_icon(app_id): - return app_id - if icon_theme.has_icon(app_id + "-desktop"): - return app_id + "-desktop" - desktop_file = self._get_desktop_file(app_id) - return ( - self._get_icon_from_desktop_file(desktop_file) - if desktop_file - else self.default_applicaiton_icon - ) \ No newline at end of file diff --git a/utils/icons.py b/utils/icons.py deleted file mode 100644 index ed61594..0000000 --- a/utils/icons.py +++ /dev/null @@ -1,185 +0,0 @@ -# Parameters -font_family: str = "tabler-icons" -font_weight: str = "normal" - -span: str = f"" - -# Panels -apps: str = "" -dashboard: str = "" -chat: str = "" -wallpapers: str = "" -windows: str = "" - -# Bar -colorpicker: str = "" -media: str = "" - -# Circles -temp: str = "" -disk: str = "" -battery: str = "" -memory: str = "流" -cpu: 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 = "" - -# 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 = "" -chevron_down: str = "" -chevron_left: str = "" -chevron_right: str = "" - -# Power -lock: str = "" -suspend: str = "" -logout: str = "" -reboot: str = "" -shutdown: str = "" - -# Power Manager -power_saving: str = "" -power_balanced: str = "勺" -power_performance: str = "" -charging: str = "" -discharging: str = "" -alert: str = "" -bat_charging: str = "" -bat_discharging: str = "" -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 = "﹖" - - -wifi_off: str = "" -bluetooth_off: str = "" -night_off: str = "" -dnd_off: str = "" - - -# Bluetooth -bluetooth_connected: str = "" -bluetooth_disconnected: str = "" - - -# Player -pause: str = "" -play: str = "" -stop: str = "" -skip_back: str = "" -skip_forward: str = "" -prev: str = "" -next: str = "" -shuffle: str = "" -repeat: str = "" -music: str = "" -rewind_backward_5: str = "謹" -rewind_forward_5: str = "難" - -# Volume -vol_off: str = "" -vol_mute: str = "" -vol_medium: str = "" -vol_high: str = "" - - -mic: str = "" -mic_muted: str = "" - - -# Overview -circle_plus: str = "" - -# Pins -copy_plus: str = "ﶮ" - -# Clipboard Manager -clipboard: str = "" -clip_text: str = "" - -# Confirm -accept: str = "" -cancel: str = "" -trash: str = "" -clear: str = "" - -# Config -config: str = "" - -# Icons -firefox: str = "" -chromium: str = "" -spotify: str = "ﺆ" -disc: str = "𐀾" -disc_off: str = "" - -# Misc -dot: str = "" -palette: str = "" -cloud_off: str = "" -loader: str = "" -radar: str = "" -emoji: str = "" -keyboard: str = "" -terminal: str = "" - - -exceptions: list[str] = ["font_family", "font_weight", "span"] - - -def apply_span() -> None: - global_dict = globals() - for key in global_dict: - if key not in exceptions and not key.startswith("__"): - global_dict[key] = f"{span}{global_dict[key]}" - - -apply_span() diff --git a/utils/occlusion.py b/utils/occlusion.py deleted file mode 100644 index bf06350..0000000 --- a/utils/occlusion.py +++ /dev/null @@ -1,141 +0,0 @@ -import subprocess -import json - -import config.data as data - - -def get_current_workspace(): - """ - Get the current workspace ID using hyprctl. - """ - try: - result = subprocess.run( - ["hyprctl", "activeworkspace"], capture_output=True, text=True - ) - # Assume the output similar to: "ID " - # Extracting the number from the output - parts = result.stdout.split() - for i, part in enumerate(parts): - if part == "ID" and i + 1 < len(parts): - return int(parts[i + 1]) - except Exception as e: - print(f"Error getting current workspace: {e}") - return -1 - - -def get_screen_dimensions(): - """ - Get screen dimensions from hyprctl. - - Returns: - tuple: (width, height) of the monitor containing the current workspace - """ - try: - # Get current workspace - workspace_id = get_current_workspace() - - # Get monitor information - result = subprocess.run( - ["hyprctl", "-j", "monitors"], capture_output=True, text=True - ) - monitors = json.loads(result.stdout) - - # Find the monitor containing our workspace - for monitor in monitors: - if monitor.get("activeWorkspace", {}).get("id") == workspace_id: - return monitor.get("width", data.CURRENT_WIDTH), monitor.get( - "height", data.CURRENT_HEIGHT - ) - - # Fallback to first monitor - if monitors: - return monitors[0].get("width", data.CURRENT_WIDTH), monitors[0].get( - "height", data.CURRENT_HEIGHT - ) - except Exception as e: - print(f"Error getting screen dimensions: {e}") - - # Default fallback values - return data.CURRENT_WIDTH, data.CURRENT_HEIGHT - - -def check_occlusion(occlusion_region, workspace=None): - """ - Check if a region is occupied by any window on a given workspace. - - Parameters: - occlusion_region: Can be one of: - - tuple (side, size): where side is "top", "bottom", "left", or "right" - and size is the pixel width of the region - - tuple (x, y, width, height): The full region coordinates (legacy format) - workspace (int, optional): The workspace ID to check. If None, the current workspace is used. - - Returns: - bool: True if any window overlaps with the occlusion region, False otherwise. - """ - if workspace is None: - workspace = get_current_workspace() - - # Handle simplified side-based format - if isinstance(occlusion_region, tuple) and len(occlusion_region) == 2: - side, size = occlusion_region - if isinstance(side, str): - # Convert side-based format to coordinates - screen_width, screen_height = get_screen_dimensions() - - if side.lower() == "bottom": - occlusion_region = (0, screen_height - size, screen_width, size) - elif side.lower() == "top": - occlusion_region = (0, 0, screen_width, size) - elif side.lower() == "left": - occlusion_region = (0, 0, size, screen_height) - elif side.lower() == "right": - occlusion_region = (screen_width - size, 0, size, screen_height) - - # Ensure occlusion_region is in the correct format (x, y, width, height) - if not isinstance(occlusion_region, tuple) or len(occlusion_region) != 4: - print(f"Invalid occlusion region format: {occlusion_region}") - return False - - try: - result = subprocess.run( - ["hyprctl", "-j", "clients"], capture_output=True, text=True - ) - clients = json.loads(result.stdout) - except Exception as e: - print(f"Error retrieving client windows: {e}") - return False - - occ_x, occ_y, occ_width, occ_height = occlusion_region - occ_x2 = occ_x + occ_width - occ_y2 = occ_y + occ_height - - for client in clients: - # Check if client is mapped - if not client.get("mapped", False): - continue - - # Ensure client has proper workspace information and matches the workspace - client_workspace = client.get("workspace", {}) - if client_workspace.get("id") != workspace: - continue - - # Ensure client has position and size info - position = client.get("at") - size = client.get("size") - if not position or not size: - continue - - x, y = position - width, height = size - win_x1, win_y1 = x, y - win_x2, win_y2 = x + width, y + height - - # Check for intersection between the window and occlusion region - if not ( - win_x2 <= occ_x or win_x1 >= occ_x2 or win_y2 <= occ_y or win_y1 >= occ_y2 - ): - return True # Occlusion region is occupied - - return False # No window overlaps the occlusion region - diff --git a/utils/wayland.py b/utils/wayland.py deleted file mode 100644 index 472dc40..0000000 --- a/utils/wayland.py +++ /dev/null @@ -1,359 +0,0 @@ -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 gi.repository import Gdk, GObject, Gtk -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], - }