diff --git a/assets/gtk-3.0/bookmarks b/assets/gtk-3.0/bookmarks
deleted file mode 100644
index e69de29..0000000
diff --git a/assets/gtk-3.0/gtk.css b/assets/gtk-3.0/gtk.css
deleted file mode 100644
index 0eabae0..0000000
--- a/assets/gtk-3.0/gtk.css
+++ /dev/null
@@ -1,252 +0,0 @@
-@import "../../.cache/material/colors.css";
-/* */
-@define-color theme_fg_color #AEE5FA;
-@define-color theme_text_color #AEE5FA;
-@define-color theme_bg_color #1a1b26;
-@define-color theme_base_color #1a1b26;
-@define-color theme_selected_bg_color #AEE5FA;
-@define-color theme_selected_fg_color rgba(0, 0, 0, 0.87);
-@define-color insensitive_bg_color #1a1b26;
-@define-color insensitive_fg_color rgba(192, 202, 245, 0.5);
-@define-color insensitive_base_color #24283b;
-@define-color theme_unfocused_fg_color #AEE5FA;
-@define-color theme_unfocused_text_color #c0caf5;
-@define-color theme_unfocused_bg_color #1a1b26;
-@define-color theme_unfocused_base_color #1a1b26;
-@define-color theme_unfocused_selected_bg_color #a9b1d6;
-@define-color theme_unfocused_selected_fg_color rgba(0, 0, 0, 0.87);
-@define-color unfocused_insensitive_color rgba(192, 202, 245, 0.5);
-@define-color borders rgba(192, 202, 245, 0.12);
-@define-color unfocused_borders rgba(192, 202, 245, 0.12);
-@define-color warning_color #FDD633;
-@define-color error_color #BA1B1B;
-@define-color success_color #81C995;
-@define-color wm_title #AEE5FA;
-@define-color wm_unfocused_title rgba(192, 202, 245, 0.7);
-@define-color wm_highlight rgba(192, 202, 245, 0.1);
-@define-color wm_bg #1a1b26;
-@define-color wm_unfocused_bg #1a1b26;
-@define-color wm_button_close_icon #1a1b26;
-@define-color wm_button_close_hover_bg #a9b1d6;
-@define-color wm_button_close_active_bg #c7c7c7;
-@define-color content_view_bg #1a1b26;
-@define-color placeholder_text_color @onSurface;
-@define-color text_view_bg #1d1d1d;
-@define-color budgie_tasklist_indicator_color #90D1F6;
-@define-color budgie_tasklist_indicator_color_active #90D1F6;
-@define-color budgie_tasklist_indicator_color_active_window #999999;
-@define-color budgie_tasklist_indicator_color_attention #FDD633;
-@define-color STRAWBERRY_100 #FF9262;
-@define-color STRAWBERRY_300 #FF793E;
-@define-color STRAWBERRY_500 #F15D22;
-@define-color STRAWBERRY_700 #CF3B00;
-@define-color STRAWBERRY_900 #AC1800;
-@define-color ORANGE_100 #FFDB91;
-@define-color ORANGE_300 #FFCA40;
-@define-color ORANGE_500 #FAA41A;
-@define-color ORANGE_700 #DE8800;
-@define-color ORANGE_900 #C26C00;
-@define-color BANANA_100 #FFFFA8;
-@define-color BANANA_300 #FFFA7D;
-@define-color BANANA_500 #FFCE51;
-@define-color BANANA_700 #D1A023;
-@define-color BANANA_900 #A27100;
-@define-color LIME_100 #A2F3BE;
-@define-color LIME_300 #8ADBA6;
-@define-color LIME_500 #73C48F;
-@define-color LIME_700 #479863;
-@define-color LIME_900 #1C6D38;
-@define-color BLUEBERRY_100 #94A6FF;
-@define-color BLUEBERRY_300 #6A7CE0;
-@define-color BLUEBERRY_500 #3F51B5;
-@define-color BLUEBERRY_700 #213397;
-@define-color BLUEBERRY_900 #031579;
-@define-color GRAPE_100 #D25DE6;
-@define-color GRAPE_300 #B84ACB;
-@define-color GRAPE_500 #9C27B0;
-@define-color GRAPE_700 #830E97;
-@define-color GRAPE_900 #6A007E;
-@define-color COCOA_100 #9F9792;
-@define-color COCOA_300 #7B736E;
-@define-color COCOA_500 #574F4A;
-@define-color COCOA_700 #463E39;
-@define-color COCOA_900 #342C27;
-@define-color SILVER_100 #EEE;
-@define-color SILVER_300 #CCC;
-@define-color SILVER_500 #AAA;
-@define-color SILVER_700 #888;
-@define-color SILVER_900 #666;
-@define-color SLATE_100 #888;
-@define-color SLATE_300 #666;
-@define-color SLATE_500 #444;
-@define-color SLATE_700 #222;
-@define-color SLATE_900 #111;
-@define-color BLACK_100 #474341;
-@define-color BLACK_300 #403C3A;
-@define-color BLACK_500 #393634;
-@define-color BLACK_700 #33302F;
-@define-color BLACK_900 #2B2928;
-@define-color accent_bg_color @primary;
-@define-color accent_fg_color @onPrimary;
-@define-color accent_color @primary;
-@define-color destructive_bg_color @error;
-@define-color destructive_fg_color @onError;
-@define-color destructive_color @error;
-@define-color success_bg_color #C5E1A5;
-@define-color success_fg_color #33691E;
-@define-color warning_bg_color #FFE082;
-@define-color warning_fg_color #E65100;
-@define-color error_bg_color @error;
-@define-color error_fg_color @onError;
-@define-color window_bg_color @background;
-@define-color window_fg_color @foreground;
-@define-color view_bg_color @surface;
-@define-color view_fg_color @onSurface;
-@define-color headerbar_bg_color mix(@dialog_bg_color, @window_bg_color, 0.5);
-@define-color headerbar_fg_color @onSecondaryContainer;
-@define-color headerbar_border_color @secondaryContainer;
-@define-color headerbar_backdrop_color @headerbar_bg_color;
-@define-color headerbar_shade_color rgba(0, 0, 0, 0.09);
-@define-color card_bg_color @surfaceContainerLowest;
-@define-color card_fg_color @onSecondaryContainer;
-@define-color card_shade_color rgba(0, 0, 0, 0.09);
-@define-color dialog_bg_color @surfaceContainer;
-@define-color dialog_fg_color @onSecondaryContainer;
-@define-color popover_bg_color @surfaceContainer;
-@define-color popover_fg_color @onSecondaryContainer;
-@define-color thumbnail_bg_color #1a1b26;
-@define-color thumbnail_fg_color #AEE5FA;
-@define-color shade_color rgba(0, 0, 0, 0.36);
-@define-color scrollbar_outline_color rgba(0, 0, 0, 0.5);
-@define-color sidebar_bg_color @surface;
-@define-color sidebar_fg_color @onSurface;
-@define-color sidebar_border_color @sidebar_bg_color;
-@define-color sidebar_backdrop_color @sidebar_bg_color;
-
-.navigation-sidebar {
- background-color: @sidebar_bg_color;
-}
-
-.navigation-sidebar row {
- border-radius: 100px;
-}
-
-.navigation-sidebar label {
- color: @outline;
-}
-
-.navigation-sidebar row:selected {
- background-color: @secondaryContainer;
-}
-
-.navigation-sidebar row:selected label {
- color: @onSecondaryContainer;
- background-color: transparent;
-}
-
-.navigation-sidebar row:active {
- background-color: mix(@secondaryContainer, @onSecondaryContainer, 0.1);
-}
-
-.navigation-sidebar row:active label {
- color: @onSecondaryContainer;
- background-color: transparent;
-}
-
-tooltip {
- background-color: @surfaceContainer;
- color: @foreground;
- border-radius: 11px;
- border: 0px solid;
-}
-
-menu * {
- border-radius: 0px;
-}
-
-menu separator {
- color: @outline;
- margin-right: 0px;
- margin-left: 0px;
- margin-top: 8px;
- margin-bottom: 8px;
- padding: 0px;
-}
-
-menu {
- padding: 8px 0px 8px 0px;
- background-color: @surfaceContainer;
-}
-
-separator {
- color: @outline;
-}
-
-button, combobox, entry {
- border-radius: 100px;
- margin-left: 2px;
- margin-right: 2px;
- margin-bottom: 0px;
-}
-
-* {
- box-shadow: none;
- transition: all 0.2s ease-in-out;
-}
-
-
-switch {
- border: 2px solid;
- border-radius: 100px;
- border-color: @outline;
- background-color: @surfaceContainerHighest;
-}
-
-switch slider {
- border-radius: 100px;
- background-color: @outline;
-}
-
-switch:disabled {
- background-color: mix(@surfaceContainerHighest, transparent, 0.38);
- border-color: mix(@onSurface, transparent, 0.38);
-}
-
-switch:disabled slider {
- background-color: mix(@onSurface, transparent, 0.38);
-}
-
-switch:hover {
- background-color: mix(@surfaceContainerHighest, @onSurface, 0.08);
-}
-
-switch:hover slider {
- background-color: @onSurfaceVariant;
-}
-
-switch:checked {
- background-color: @primary;
- border-color: transparent;
-}
-
-switch:checked slider {
- background-color: @onPrimary;
- border-color: transparent;
-}
-
-switch:checked:disabled {
- background-color: mix(@onSurface, transparent, 0.38);
- border-color: transparent;
-}
-
-switch:checked:disabled slider {
- background-color: mix(@surface, transparent, 0.38);
-}
-
-switch:checked:hover {
- background-color: @primary;
-}
-
-switch:checked:hover slider {
- background-color: @primaryContainer;
-}
\ No newline at end of file
diff --git a/assets/gtk-3.0/settings.ini b/assets/gtk-3.0/settings.ini
deleted file mode 100644
index f752fbe..0000000
--- a/assets/gtk-3.0/settings.ini
+++ /dev/null
@@ -1,17 +0,0 @@
-[Settings]
-gtk-theme-name=adw-gtk3-dark
-gtk-icon-theme-name=Tela-nord-dark
-gtk-font-name=Product Sans 12
-gtk-cursor-theme-name=Bibata-Original-Classic
-gtk-cursor-theme-size=6
-gtk-toolbar-style=GTK_TOOLBAR_ICONS
-gtk-toolbar-icon-size=GTK_ICON_SIZE_LARGE_TOOLBAR
-gtk-button-images=0
-gtk-menu-images=0
-gtk-enable-event-sounds=1
-gtk-enable-input-feedback-sounds=0
-gtk-xft-antialias=1
-gtk-xft-hinting=1
-gtk-xft-hintstyle=hintslight
-gtk-xft-rgba=rgb
-gtk-application-prefer-dark-theme=1
diff --git a/assets/gtk-4.0/gtk.css b/assets/gtk-4.0/gtk.css
deleted file mode 100644
index ca2ec25..0000000
--- a/assets/gtk-4.0/gtk.css
+++ /dev/null
@@ -1,430 +0,0 @@
-@import "../../.cache/material/colors.css";
-/* */
-@define-color theme_fg_color #AEE5FA;
-@define-color theme_text_color #AEE5FA;
-@define-color theme_bg_color #1a1b26;
-@define-color theme_base_color #1a1b26;
-@define-color theme_selected_bg_color #AEE5FA;
-@define-color theme_selected_fg_color rgba(0, 0, 0, 0.87);
-@define-color insensitive_bg_color #1a1b26;
-@define-color insensitive_fg_color rgba(192, 202, 245, 0.5);
-@define-color insensitive_base_color #24283b;
-@define-color theme_unfocused_fg_color #AEE5FA;
-@define-color theme_unfocused_text_color #c0caf5;
-@define-color theme_unfocused_bg_color #1a1b26;
-@define-color theme_unfocused_base_color #1a1b26;
-@define-color theme_unfocused_selected_bg_color #a9b1d6;
-@define-color theme_unfocused_selected_fg_color rgba(0, 0, 0, 0.87);
-@define-color unfocused_insensitive_color rgba(192, 202, 245, 0.5);
-@define-color borders rgba(192, 202, 245, 0.12);
-@define-color unfocused_borders rgba(192, 202, 245, 0.12);
-@define-color warning_color #FDD633;
-@define-color error_color #BA1B1B;
-@define-color success_color #81C995;
-@define-color wm_title #AEE5FA;
-@define-color wm_unfocused_title rgba(192, 202, 245, 0.7);
-@define-color wm_highlight rgba(192, 202, 245, 0.1);
-@define-color wm_bg #1a1b26;
-@define-color wm_unfocused_bg #1a1b26;
-@define-color wm_button_close_icon #1a1b26;
-@define-color wm_button_close_hover_bg #a9b1d6;
-@define-color wm_button_close_active_bg #c7c7c7;
-@define-color content_view_bg #1a1b26;
-@define-color placeholder_text_color @onSurface;
-@define-color text_view_bg #1d1d1d;
-@define-color budgie_tasklist_indicator_color #90D1F6;
-@define-color budgie_tasklist_indicator_color_active #90D1F6;
-@define-color budgie_tasklist_indicator_color_active_window #999999;
-@define-color budgie_tasklist_indicator_color_attention #FDD633;
-@define-color STRAWBERRY_100 #FF9262;
-@define-color STRAWBERRY_300 #FF793E;
-@define-color STRAWBERRY_500 #F15D22;
-@define-color STRAWBERRY_700 #CF3B00;
-@define-color STRAWBERRY_900 #AC1800;
-@define-color ORANGE_100 #FFDB91;
-@define-color ORANGE_300 #FFCA40;
-@define-color ORANGE_500 #FAA41A;
-@define-color ORANGE_700 #DE8800;
-@define-color ORANGE_900 #C26C00;
-@define-color BANANA_100 #FFFFA8;
-@define-color BANANA_300 #FFFA7D;
-@define-color BANANA_500 #FFCE51;
-@define-color BANANA_700 #D1A023;
-@define-color BANANA_900 #A27100;
-@define-color LIME_100 #A2F3BE;
-@define-color LIME_300 #8ADBA6;
-@define-color LIME_500 #73C48F;
-@define-color LIME_700 #479863;
-@define-color LIME_900 #1C6D38;
-@define-color BLUEBERRY_100 #94A6FF;
-@define-color BLUEBERRY_300 #6A7CE0;
-@define-color BLUEBERRY_500 #3F51B5;
-@define-color BLUEBERRY_700 #213397;
-@define-color BLUEBERRY_900 #031579;
-@define-color GRAPE_100 #D25DE6;
-@define-color GRAPE_300 #B84ACB;
-@define-color GRAPE_500 #9C27B0;
-@define-color GRAPE_700 #830E97;
-@define-color GRAPE_900 #6A007E;
-@define-color COCOA_100 #9F9792;
-@define-color COCOA_300 #7B736E;
-@define-color COCOA_500 #574F4A;
-@define-color COCOA_700 #463E39;
-@define-color COCOA_900 #342C27;
-@define-color SILVER_100 #EEE;
-@define-color SILVER_300 #CCC;
-@define-color SILVER_500 #AAA;
-@define-color SILVER_700 #888;
-@define-color SILVER_900 #666;
-@define-color SLATE_100 #888;
-@define-color SLATE_300 #666;
-@define-color SLATE_500 #444;
-@define-color SLATE_700 #222;
-@define-color SLATE_900 #111;
-@define-color BLACK_100 #474341;
-@define-color BLACK_300 #403C3A;
-@define-color BLACK_500 #393634;
-@define-color BLACK_700 #33302F;
-@define-color BLACK_900 #2B2928;
-@define-color accent_bg_color @primary;
-@define-color accent_fg_color @onPrimary;
-@define-color accent_color @primary;
-@define-color destructive_bg_color @error;
-@define-color destructive_fg_color @onError;
-@define-color destructive_color @error;
-@define-color success_bg_color #C5E1A5;
-@define-color success_fg_color #33691E;
-@define-color warning_bg_color #FFE082;
-@define-color warning_fg_color #E65100;
-@define-color error_bg_color @error;
-@define-color error_fg_color @onError;
-@define-color window_bg_color @background;
-@define-color window_fg_color @foreground;
-@define-color view_bg_color @surface;
-@define-color view_fg_color @onSurface;
-@define-color headerbar_bg_color mix(@dialog_bg_color, @window_bg_color, 0.5);
-@define-color headerbar_fg_color @onSecondaryContainer;
-@define-color headerbar_border_color @secondaryContainer;
-@define-color headerbar_backdrop_color @headerbar_bg_color;
-@define-color headerbar_shade_color rgba(0, 0, 0, 0.09);
-@define-color card_bg_color @surfaceContainerLowest;
-@define-color card_fg_color @onSecondaryContainer;
-@define-color card_shade_color rgba(0, 0, 0, 0.09);
-@define-color dialog_bg_color @surfaceContainer;
-@define-color dialog_fg_color @onSecondaryContainer;
-@define-color popover_bg_color @surfaceContainer;
-@define-color popover_fg_color @onSecondaryContainer;
-@define-color thumbnail_bg_color #1a1b26;
-@define-color thumbnail_fg_color #AEE5FA;
-@define-color shade_color rgba(0, 0, 0, 0.36);
-@define-color scrollbar_outline_color rgba(0, 0, 0, 0.5);
-@define-color sidebar_bg_color @surface;
-@define-color sidebar_fg_color @onSurface;
-@define-color sidebar_border_color @sidebar_bg_color;
-@define-color sidebar_backdrop_color @sidebar_bg_color;
-
-.navigation-sidebar {
- background-color: @sidebar_bg_color;
-}
-
-.navigation-sidebar row {
- border-radius: 100px;
-}
-
-.navigation-sidebar label {
- color: @outline;
-}
-
-.navigation-sidebar row:selected {
- background-color: @secondaryContainer;
-}
-
-.navigation-sidebar row:selected label {
- color: @onSecondaryContainer;
- background-color: transparent;
-}
-
-.navigation-sidebar row:active {
- background-color: mix(@secondaryContainer, @onSecondaryContainer, 0.1);
-}
-
-.navigation-sidebar row:active label {
- color: @onSecondaryContainer;
- background-color: transparent;
-}
-
-tooltip {
- background-color: @surfaceContainer;
- color: @foreground;
- border-radius: 11px;
- border: 0px solid;
-}
-
-menu * {
- border-radius: 0px;
-}
-
-menu separator {
- color: @outline;
-}
-
-menu {
- padding: 8px 0px 8px 0px;
- background: @surfaceContainer;
-}
-
-separator {
- color: @outline;
- margin-right: 0px;
- margin-left: 0px;
- margin-top: 8px;
- margin-bottom: 8px;
- padding: 0px;
-}
-
-button, combobox, entry {
- border-radius: 100px;
- margin-left: 2px;
- margin-right: 2px;
- border: none;
-}
-
-* {
- box-shadow: none;
- transition: all 0.2s ease-in-out;
-}
-
-button:not(.flat).image-button {
- background-color: @secondaryContainer;
- color: @onSecondaryContainer;
- transition: all 0.1s ease-in-out;
- margin-top: 2px;
- margin-bottom: 2px;
-}
-
-button:not(.flat).image-button label, button:not(.flat).image-button .icon,
-button:not(.flat).image-button image {
- color: @onSecondaryContainer;
-}
-
-button:not(.flat).image-button:hover {
- background-color: mix(@secondaryContainer, @onSecondaryContainer, 0.05);
- box-shadow: 00px 3px 3px -2px rgba(0, 0, 0, 0.2);
-}
-
-button:not(.flat).image-button:active {
- background-color: mix(@secondaryContainer, @onSecondaryContainer, 0.1);
- box-shadow: none;
-}
-
-button:not(.flat).image-button:checked {
- background-color: mix(@secondaryContainer, @onSecondaryContainer, 0.1);
- box-shadow: none;
-}
-
-scale slider {
- background-color: @primary;
-}
-
-switch {
- border: 2px solid;
- border-radius: 100px;
- border-color: @outline;
- background-color: @surfaceContainerHighest;
-}
-
-switch slider {
- border-radius: 100px;
- background-color: @outline;
-}
-
-switch:disabled {
- background-color: @surfaceContainerHighest;
- border-color: @onSurface;
-}
-
-switch:disabled slider {
- background-color: @onSurface;
-}
-
-switch:hover {
- background-color: mix(@surfaceContainerHighest, @onSurface, 0.08);
-}
-
-switch:hover slider {
- background-color: @onSurfaceVariant;
-}
-
-switch:active {
- background-color: mix(@surfaceContainerHighest, @onSurface, 0.08);
-}
-
-switch:active slider {
- background-color: @onSurfaceVariant;
-}
-
-switch:checked {
- background-color: @primary;
- border-color: @primary;
-}
-
-switch:checked slider {
- background-color: @onPrimary;
-}
-
-switch:checked:disabled {
- background-color: @onSurface;
- border-color: @onSurface;
-}
-
-switch:checked:disabled slider {
- background-color: @surface;
-}
-
-switch:checked:hover {
- background-color: @primary;
- border-color: @primary;
-}
-
-switch:checked:hover slider {
- background-color: @primaryContainer;
-}
-
-switch:checked:active {
- background-color: @primary;
- border-color: @primary;
-}
-
-switch:checked:active slider {
- background-color: @primaryContainer;
- border-color: @primaryContainer;
-}
-
-radio {
- color: @onSurfaceVariant;
- background-color: transparent;
- border: 2px solid;
- border-color: @onSurfaceVariant;
- opacity: 1;
- padding: 1px;
-}
-
-radio:checked {
- color: @primary;
- border-color: @primary;
-}
-
-radio:indeterminate {
- color: @primary;
- border-color: @primary;
-}
-
-radio:disabled {
- color: @onSurface;
- border-color: @onSurface;
- background-color: transparent;
- opacity: 0.38;
-}
-
-radio:checked:disabled {
- color: @surface;
- border-color: @onSurface;
- background-color: @onSurface;
- opacity: 0.38;
-}
-
-radio:indeterminate:disabled {
- color: @surface;
- border-color: @onSurface;
- background-color: @onSurface;
- opacity: 0.38;
-}
-
-radio:hover {
- background-color: mix(@onSurface, transparent, 0.92);
-}
-
-radio:checked:hover {
- background-color: mix(@primary, transparent, 0.92);
-}
-
-radio:indeterminate:hover {
- background-color: mix(@primary, transparent, 0.92);
-}
-
-radio:active {
- background-color: mix(@onSurface, transparent, 0.9);
-}
-
-radio:checked:active {
- background-color: mix(@primary, transparent, 0.9);
-}
-
-radio:indeterminate:active {
- background-color: mix(@primary, transparent, 0.9);
-}
-
-check {
- color: @onSurface;
- background-color: transparent;
- border: 2px solid;
- border-color: @onSurfaceVariant;
- opacity: 1;
- padding: 1px;
-}
-
-check:checked {
- background-color: @primary;
- border-color: transparent;
- color: @onPrimary;
-}
-
-check:indeterminate {
- background-color: @primary;
- border-color: transparent;
- color: @onPrimary;
-}
-
-check:disabled {
- color: @onSurface;
- border-color: @onSurface;
- opacity: 0.38;
-}
-
-check:checked:disabled {
- background-color: @onSurface;
- border-color: transparent;
- color: @surface;
- opacity: 0.38;
-}
-
-check:indeterminate:disabled {
- background-color: @onSurface;
- border-color: transparent;
- color: @surface;
- opacity: 0.38;
-}
-
-check:hover {
- background-color: mix(@onSurface, transparent, 0.92);
-}
-
-check:checked:hover {
- background-color: @primary;
-}
-
-check:indeterminate:hover {
- background-color: @primary;
-}
-
-check:active {
- background-color: mix(@onSurface, transparent, 0.92);
-}
-
-check:checked:active {
- background-color: @primary;
-}
-
-check:indeterminate:active {
- background-color: @primary;
-}
diff --git a/assets/logo.svg b/assets/logo.svg
deleted file mode 100644
index 5d19dc1..0000000
--- a/assets/logo.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/assets/screenshots/bluetooth.png b/assets/screenshots/bluetooth.png
deleted file mode 100644
index 7033d48..0000000
Binary files a/assets/screenshots/bluetooth.png and /dev/null differ
diff --git a/assets/screenshots/cliphist.png b/assets/screenshots/cliphist.png
deleted file mode 100644
index 6e21db0..0000000
Binary files a/assets/screenshots/cliphist.png and /dev/null differ
diff --git a/assets/screenshots/emoji.png b/assets/screenshots/emoji.png
deleted file mode 100644
index 3202bf9..0000000
Binary files a/assets/screenshots/emoji.png and /dev/null differ
diff --git a/assets/screenshots/home.png b/assets/screenshots/home.png
deleted file mode 100644
index c7ba7c3..0000000
Binary files a/assets/screenshots/home.png and /dev/null differ
diff --git a/assets/screenshots/notification.png b/assets/screenshots/notification.png
deleted file mode 100644
index c8c9f30..0000000
Binary files a/assets/screenshots/notification.png and /dev/null differ
diff --git a/assets/screenshots/osd.png b/assets/screenshots/osd.png
deleted file mode 100644
index a02dec6..0000000
Binary files a/assets/screenshots/osd.png and /dev/null differ
diff --git a/assets/screenshots/powermenu.png b/assets/screenshots/powermenu.png
deleted file mode 100644
index 165fc0e..0000000
Binary files a/assets/screenshots/powermenu.png and /dev/null differ
diff --git a/assets/screenshots/shrunner.png b/assets/screenshots/shrunner.png
deleted file mode 100644
index 40f184b..0000000
Binary files a/assets/screenshots/shrunner.png and /dev/null differ
diff --git a/assets/screenshots/todo.png b/assets/screenshots/todo.png
deleted file mode 100644
index 0718afb..0000000
Binary files a/assets/screenshots/todo.png and /dev/null differ
diff --git a/assets/screenshots/wallpaper.png b/assets/screenshots/wallpaper.png
deleted file mode 100644
index 4e6789b..0000000
Binary files a/assets/screenshots/wallpaper.png and /dev/null differ
diff --git a/config/__init__.py b/config/__init__.py
new file mode 100644
index 0000000..f94e240
--- /dev/null
+++ b/config/__init__.py
@@ -0,0 +1,6 @@
+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/config.json b/config/assets/config.json
index a93e26d..2cdda50 100644
--- a/config/assets/config.json
+++ b/config/assets/config.json
@@ -1 +1,58 @@
-{"prefix_restart": "SHIFT ALT", "suffix_restart": "T", "prefix_bluetooth": "ALT", "suffix_bluetooth": "B", "prefix_emoji": "SUPER", "suffix_emoji": "E", "prefix_cliphist": "SUPER", "suffix_cliphist": "V", "prefix_todo": "SUPER", "suffix_todo": "T", "prefix_sh": "SUPER", "suffix_sh": "S", "prefix_walls": "SUPER", "suffix_walls": "W", "prefix_screenshotregion": "SUPER", "suffix_screenshotregion": "Z", "prefix_screenshot": "", "suffix_screenshot": "Print", "prefix_screencast": "SUPER", "suffix_screencast": "R", "prefix_launcher": "SUPER", "suffix_launcher": "D", "prefix_power": "SUPER", "suffix_power": "X", "prefix_wifi": "SUPER", "suffix_wifi": "N", "prefix_toggle": "SUPER", "suffix_toggle": "H", "prefix_randomwallpaper": "ALT SHIFT", "suffix_randomwallpaper": "W", "wallpapers_dir": "/home/sankalp/Pictures/wallpapers"}
\ No newline at end of file
+{
+ "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/modus.json b/config/assets/modus.json
index 12e8657..530c99d 100644
--- a/config/assets/modus.json
+++ b/config/assets/modus.json
@@ -1,24 +1,17 @@
{
- "commands": {
- ":wp": "launcher.open(\"wallpapers\")",
- ":em": "launcher.open(\"emoji\")",
- ":ch": "launcher.open(\"cliphist\")",
- ":td": "launcher.open(\"todo\")",
- ":bt": "launcher.open(\"bluetooth\")",
- ":sh": "launcher.open(\"sh\")",
- ":pw": "launcher.open(\"power\")",
- ":nw": "launcher.open(\"wifi\")",
- ":cl": "launcher.open(\"calendar\")"
- },
- "pinned_apps": [
- "kitty",
- "code",
- "materialgram",
- "vesktop",
- "spotify",
- "libreoffice",
- "wasistlos",
- "pavucontrol",
- "zen-browser"
- ]
-}
+ "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/assets/settings.json b/config/assets/settings.json
deleted file mode 100644
index d11f7c8..0000000
--- a/config/assets/settings.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "color-scheme": "dark",
- "custom-color": "none",
- "generation-scheme": "tonalSpot"
-}
\ No newline at end of file
diff --git a/config/config.py b/config/config.py
index c280ec1..a1a1bd1 100644
--- a/config/config.py
+++ b/config/config.py
@@ -1,58 +1,113 @@
import os
-import json
import shutil
-import gi
+import json
+import sys
+from pathlib import Path
+import random
import subprocess
-from concurrent.futures import ThreadPoolExecutor
+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
-# Constants
-SOURCE_STRING = """
-# Modus
-source = ~/Modus/config/hypr/modus.conf
+# 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
"""
-CONFIG_DIR = os.path.expanduser("~/Modus")
-WALLPAPERS_DIR_DEFAULT = os.path.expanduser("~/Modus/assets/wallpaper")
-
-# Default key binding values
-bind_vars = {
+DEFAULTS = {
"prefix_restart": "SHIFT ALT",
"suffix_restart": "T",
- "prefix_bluetooth": "ALT",
+ "prefix_bluetooth": "SHIFT ALT",
"suffix_bluetooth": "B",
"prefix_emoji": "SUPER",
"suffix_emoji": "E",
"prefix_cliphist": "SUPER",
"suffix_cliphist": "V",
- "prefix_todo": "SUPER",
- "suffix_todo": "T",
- "prefix_sh": "SUPER",
- "suffix_sh": "S",
+ "prefix_kanban": "SUPER",
+ "suffix_kanban": "T",
+ "prefix_toolbox": "SUPER",
+ "suffix_toolbox": "S",
"prefix_walls": "SUPER",
"suffix_walls": "W",
- "prefix_screenshotregion": "SUPER",
- "suffix_screenshotregion": "Z",
- "prefix_screenshot": "",
- "suffix_screenshot": "Print",
- "prefix_screencast": "SUPER",
- "suffix_screencast": "R",
"prefix_launcher": "SUPER",
"suffix_launcher": "D",
"prefix_power": "SUPER",
"suffix_power": "X",
- "prefix_wifi": "SUPER",
- "suffix_wifi": "N",
- "prefix_toggle": "SUPER",
- "suffix_toggle": "H",
- "prefix_randomwallpaper": "ALT SHIFT",
- "suffix_randomwallpaper": "W",
+ "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,
+ "gpu": False,
+ },
}
+bind_vars = DEFAULTS.copy()
+
def deep_update(target: dict, update: dict) -> dict:
"""
@@ -66,76 +121,147 @@ def deep_update(target: dict, update: dict) -> dict:
return target
-def parallel_subprocesses(commands):
+def ensure_matugen_config():
"""
- Run multiple subprocess commands in parallel.
+ Ensure that the matugen configuration file exists and is updated
+ with the expected settings.
"""
- with ThreadPoolExecutor() as executor:
- executor.map(subprocess.run, commands)
+ 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)
-def setup_colors():
- subprocess.run(
- [
- "python",
- "-O",
- os.path.expanduser("~/Modus/config/material-colors/generate.py"),
- "--color",
- "#0000FF",
+ # 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)
+ ]
-def setup_sddm():
- commands = [
- ["sudo", "mkdir", "-p", "/etc/sddm.conf.d"],
- [
- "sudo",
- "cp",
- os.path.expanduser("~/Modus/config/sddm/sddm.conf"),
- "/etc/sddm.conf.d/",
- ],
- [
- "sudo",
- "cp",
- os.path.expanduser("~/Modus/config/sddm/sddm.conf"),
- "/etc/",
- ],
- ["sudo", "chmod", "644", "/etc/sddm.conf.d/sddm.conf"],
- ["sudo", "chmod", "644", "/etc/sddm.conf"],
- ["sudo", "chmod", "-R", "755", "/usr/share/sddm/themes/corners/"],
- ]
- parallel_subprocesses(commands)
- subprocess.run([os.path.expanduser("~/Modus/config/sddm/scripts/wallpaper.sh")])
-
-
-def copy_wallpapers():
- dest_wallpaper_dir = os.path.expanduser("~/Pictures/wallpaper")
- src_wallpaper_dir = os.path.expanduser("~/Modus/assets/wallpaper")
- if not os.path.exists(dest_wallpaper_dir):
- shutil.copytree(src_wallpaper_dir, dest_wallpaper_dir, dirs_exist_ok=True)
- subprocess.run(
- [
- "python",
- os.path.expanduser("~/Modus/config/scripts/wallpaper.py"),
- "-I",
- os.path.join(dest_wallpaper_dir, "example-1.jpg"),
- ]
- )
+ 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("~/Modus/config/assets/config.json")
- try:
- with open(config_json, "r") as f:
- saved_vars = json.load(f)
- bind_vars.update(saved_vars)
- except FileNotFoundError:
- # Use default values if no saved config exists
- pass
+ 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:
@@ -143,55 +269,60 @@ def generate_hyprconf() -> str:
Generate the Hypr configuration string using the current bind_vars.
"""
home = os.path.expanduser("~")
- return f"""exec-once = python {home}/Modus/main.py
+ 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 modus
-$scriptsDir = $HOME/Modus/config/scripts/
-
-bind = {bind_vars["prefix_restart"]}, {bind_vars["suffix_restart"]}, exec, killall modus; python {home}/Modus/main.py # Reload Modus | Default: SUPER ALT + T
-bind = {bind_vars["prefix_bluetooth"]}, {bind_vars["suffix_bluetooth"]}, exec, $fabricSend 'launcher.open("bluetooth")' # Bluetooth | Default: ALT + B
-bind = {bind_vars["prefix_wifi"]}, {bind_vars["suffix_wifi"]}, exec, $fabricSend 'launcher.open("wifi")' # Wifi | Default: SUPER + N
-bind = {bind_vars["prefix_emoji"]}, {bind_vars["suffix_emoji"]}, exec, $fabricSend 'launcher.open("emoji")' # Emoji | Default: SUPER + E
-bind = {bind_vars["prefix_cliphist"]}, {bind_vars["suffix_cliphist"]}, exec, $fabricSend 'launcher.open("cliphist")' # Cliphist | Default: SUPER + V
-bind = {bind_vars["prefix_todo"]}, {bind_vars["suffix_todo"]}, exec, $fabricSend 'launcher.open("todo")' # Todo | Default: SUPER + T
-bind = {bind_vars["prefix_sh"]}, {bind_vars["suffix_sh"]}, exec, $fabricSend 'launcher.open("sh")' # Sh | Default: SUPER + S
-bind = {bind_vars["prefix_walls"]}, {bind_vars["suffix_walls"]}, exec, $fabricSend 'launcher.open("wallpapers")' # Wallpaper Selector | Default: SUPER + W
-bind ={bind_vars["prefix_randomwallpaper"]}, {bind_vars["suffix_randomwallpaper"]}, exec, python -O $scriptsDir/wallpaper.py -R # Random Wallpaper | Default: ALT SHIFT + W
-bind = {bind_vars["prefix_launcher"]}, {bind_vars["suffix_launcher"]}, exec, $fabricSend 'launcher.open("launcher")' # App Launcher | Default: SUPER + D
-bind = {bind_vars["prefix_power"]}, {bind_vars["suffix_power"]}, exec, $fabricSend 'launcher.open("power")' # Power Menu | Default: SUPER + X
-bind = {bind_vars["prefix_toggle"]}, {bind_vars["suffix_toggle"]}, exec, $fabricSend 'bar.toggle_hidden()' # Toggle Bar | Default: SUPER + H
-bind = {bind_vars["prefix_screenshotregion"]}, {bind_vars["suffix_screenshotregion"]}, exec, $fabricSend 'sc.screenshot()' # Screenshot Region | Default: SUPER + Z
-bind = {bind_vars["prefix_screenshot"]}, {bind_vars["suffix_screenshot"]}, exec, $fabricSend 'sc.screenshot(True)' # ScreenshotFullScreen | Default: Print
-bind = {bind_vars["prefix_screencast"]}, {bind_vars["suffix_screencast"]}, exec, $fabricSend 'sc.screencast_start()' # Screencast | Default: SUPER + R
+$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 = 0
+ border_size = 2
layout = dwindle
}}
decoration {{
blur {{
- enabled = false
+ enabled = yes
+ size = 5
+ passes = 3
+ new_optimizations = yes
+ contrast = 1
+ brightness = 1
}}
- rounding = 15
- active_opacity = 1
- inactive_opacity = 1
- fullscreen_opacity = 1
-
- dim_inactive = true
- dim_strength = 0.1
- dim_special = 0.8
+ rounding = 14
shadow {{
- enabled = false
+ enabled = true
+ range = 10
+ render_power = 2
+ color = rgba(0, 0, 0, 0.25)
}}
}}
@@ -211,8 +342,8 @@ animations {{
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 # use with .2s duration
- # Animation configs
+ 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%
@@ -224,9 +355,6 @@ animations {{
animation = fadeLayersIn, 1, 2, menu_decel
animation = fadeLayersOut, 1, 4.5, menu_accel
animation = workspaces, 1, 7, menu_decel, slide
- # animation = workspaces, 1, 2.5, softAcDecel, slide
- # animation = workspaces, 1, 7, menu_decel, slidefade 15%
- # animation = specialWorkspace, 1, 3, md3_decel, slidefadevert 15%
animation = specialWorkspace, 1, 3, md3_decel, slidevert
}}
"""
@@ -238,187 +366,928 @@ def backup_and_replace(src: str, dest: str, config_name: str):
"""
if os.path.exists(dest):
backup_path = dest + ".bak"
- if not os.path.exists(backup_path):
- shutil.copy(dest, backup_path)
- print(f"{config_name} config backed up to {backup_path}")
+ 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(Gtk.Window):
- def __init__(self, show_lock_checkbox: bool, show_idle_checkbox: bool):
- super().__init__(title="Configure Key Binds")
- self.set_border_width(20)
- self.set_default_size(500, 450)
+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.selected_face_icon = None
+ self.show_lock_checkbox = show_lock_checkbox
+ self.show_idle_checkbox = show_idle_checkbox
- # Main container
- main_vbox = Gtk.VBox(spacing=10)
- self.add(main_vbox)
+ # 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)
- # Create input entries for key bindings
self.entries = []
bindings = [
- ("Reload Modus", "prefix_restart", "suffix_restart"),
+ (f"Reload {APP_NAME_CAP}", "prefix_restart", "suffix_restart"),
("Bluetooth", "prefix_bluetooth", "suffix_bluetooth"),
- ("Wallpaper Selector", "prefix_walls", "suffix_walls"),
- ("Random Wallpaper", "prefix_randomwallpaper", "suffix_randomwallpaper"),
+ ("Notifications", "prefix_notifications", "suffix_notifications"),
+ ("Kanban", "prefix_kanban", "suffix_kanban"),
("App Launcher", "prefix_launcher", "suffix_launcher"),
- ("Power Menu", "prefix_power", "suffix_power"),
- ("Emoji Selector", "prefix_emoji", "suffix_emoji"),
+ ("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"),
- ("Screenshot", "prefix_screenshot", "suffix_screenshot"),
- ("ScreenRecorder", "prefix_screencast", "suffix_screencast"),
- ("Screenshot Region", "prefix_screenshotregion", "suffix_screenshotregion"),
- ("Sh runner", "prefix_sh", "suffix_sh"),
- ("Todo List", "prefix_todo", "suffix_todo"),
- ("Toggle Bar", "prefix_toggle", "suffix_toggle"),
+ ("Window Switcher", "prefix_window_switcher", "suffix_window_switcher"),
+ ("Power Menu", "prefix_power", "suffix_power"),
+ (
+ "Restart with inspector",
+ "prefix_restart_inspector",
+ "suffix_restart_inspector",
+ ),
]
- for label_text, prefix_key, suffix_key in bindings:
- entry_box = Gtk.HBox(spacing=10)
+ # 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
- label = Gtk.Label(label=label_text)
- entry_box.pack_start(label, False, False, 0)
+ # Action label
+ binding_label = Label(label=label_text, h_align="start")
+ keybind_grid.attach(binding_label, 0, row, 1, 1)
- prefix_entry = Gtk.Entry()
- prefix_entry.set_text(bind_vars[prefix_key])
- entry_box.pack_start(prefix_entry, True, True, 0)
+ # Prefix entry
+ prefix_entry = Entry(text=bind_vars[prefix_key])
+ keybind_grid.attach(prefix_entry, 1, row, 1, 1)
- plus_label = Gtk.Label(label=" + ")
- entry_box.pack_start(plus_label, False, False, 0)
+ # Plus separator
+ plus_label = Label(label="+", h_align="center")
+ keybind_grid.attach(plus_label, 2, row, 1, 1)
- suffix_entry = Gtk.Entry()
- suffix_entry.set_text(bind_vars[suffix_key])
- entry_box.pack_start(suffix_entry, True, True, 0)
+ # Suffix entry
+ suffix_entry = Entry(text=bind_vars[suffix_key])
+ keybind_grid.attach(suffix_entry, 3, row, 1, 1)
- main_vbox.pack_start(entry_box, False, False, 0)
self.entries.append((prefix_key, suffix_key, prefix_entry, suffix_entry))
- # Wallpaper directory chooser
- wall_box = Gtk.HBox(spacing=10)
- wall_label = Gtk.Label(label="Wallpapers Directory")
- wall_box.pack_start(wall_label, False, False, 0)
+ 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"])
- wall_box.pack_start(self.wall_dir_chooser, True, True, 0)
- main_vbox.pack_start(wall_box, False, False, 0)
+ self.wall_dir_chooser.set_size_request(180, -1)
+ wall_box.add(self.wall_dir_chooser)
- # Optional checkboxes for replacing configs
- if show_lock_checkbox:
- self.lock_checkbox = Gtk.CheckButton(label="Replace Hyprlock config")
- self.lock_checkbox.set_active(False)
- main_vbox.pack_start(self.lock_checkbox, False, False, 0)
- if show_idle_checkbox:
- self.idle_checkbox = Gtk.CheckButton(label="Replace Hypridle config")
- self.idle_checkbox.set_active(False)
- main_vbox.pack_start(self.idle_checkbox, False, False, 0)
+ # === LAYOUT OPTIONS SECTION ===
+ layout_header = Label(markup="Layout Options", h_align="start")
+ vbox.add(layout_header)
- # Accept and Cancel buttons
- button_box = Gtk.HBox(spacing=10)
- cancel_btn = Gtk.Button(label="Cancel")
- cancel_btn.connect("clicked", self.on_cancel)
- accept_btn = Gtk.Button(label="Accept")
- accept_btn.connect("clicked", self.on_accept)
- button_box.pack_end(accept_btn, False, False, 0)
- button_box.pack_end(cancel_btn, False, False, 0)
- main_vbox.pack_start(button_box, False, False, 0)
+ # 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",
+ "gpu": "GPU",
+ }
+
+ # 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.
+ Save the configuration and update the necessary files without closing the window.
"""
- # Update bind_vars from user inputs
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()
- # Update wallpaper directory
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()
- # Save the updated bind_vars to a JSON file
- config_json = os.path.expanduser("~/Modus/config/assets/config.json")
+ 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)
- with open(config_json, "w") as f:
- json.dump(bind_vars, f)
+ 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}")
- # Replace hyprlock config if requested
- if hasattr(self, "lock_checkbox") and self.lock_checkbox.get_active():
- src_lock = os.path.expanduser("~/Modus/config/hypr/hyprlock.conf")
+ 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")
- backup_and_replace(src_lock, dest_lock, "Hyprlock")
+ 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}")
- # Replace hypridle config if requested
- if hasattr(self, "idle_checkbox") and self.idle_checkbox.get_active():
- src_idle = os.path.expanduser("~/Modus/config/hypr/hypridle.conf")
+ 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")
- backup_and_replace(src_idle, dest_idle, "Hypridle")
+ 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}")
- # Append the source string to the Hyprland config if not present
hyprland_config_path = os.path.expanduser("~/.config/hypr/hyprland.conf")
- with open(hyprland_config_path, "r") as f:
- content = f.read()
- if SOURCE_STRING not in content:
- with open(hyprland_config_path, "a") as f:
- f.write(SOURCE_STRING)
+ 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()
- self.destroy()
- def on_cancel(self, widget):
- self.destroy()
+ # 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.
"""
- copy_wallpapers()
- setup_colors()
+ ensure_matugen_config()
- # Write the generated hypr configuration to file
- hypr_config_dir = os.path.expanduser("~/Modus/config/hypr/")
+ 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, "modus.conf")
- with open(hypr_conf_path, "w") as f:
- f.write(generate_hyprconf())
+ 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}")
- # Reload Hyprland configuration
- os.system("hyprctl reload")
+ 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.
+ Entry point for opening the configuration GUI using Fabric Application.
"""
load_bind_vars()
- # Check and copy hyprlock config if needed
dest_lock = os.path.expanduser("~/.config/hypr/hyprlock.conf")
- src_lock = os.path.expanduser("~/Modus/config/hypr/hyprlock.conf")
- os.makedirs(os.path.dirname(dest_lock), exist_ok=True)
+ src_lock = os.path.expanduser(f"~/{APP_NAME_CAP}/config/hypr/hyprlock.conf")
show_lock_checkbox = True
- if not os.path.exists(dest_lock):
- shutil.copy(src_lock, dest_lock)
- show_lock_checkbox = False
+ 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)
- # Check and copy hypridle config if needed
dest_idle = os.path.expanduser("~/.config/hypr/hypridle.conf")
- src_idle = os.path.expanduser("~/Modus/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):
- shutil.copy(src_idle, dest_idle)
- show_idle_checkbox = False
+ 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)
- # Create and run the GUI
- window = HyprConfGUI(show_lock_checkbox, show_idle_checkbox)
- window.connect("destroy", Gtk.main_quit)
window.show_all()
- Gtk.main()
+ app.run()
if __name__ == "__main__":
diff --git a/config/data.py b/config/data.py
new file mode 100644
index 0000000..956159f
--- /dev/null
+++ b/config/data.py
@@ -0,0 +1,120 @@
+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")
+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, "gpu": False},
+ )
+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,
+ "gpu": False,
+ }
+
+
+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
new file mode 100644
index 0000000..e839fe7
--- /dev/null
+++ b/config/hypr/colors.conf
@@ -0,0 +1,11 @@
+$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/hyprlock.conf b/config/hypr/hyprlock.conf
index 48abea1..80977af 100644
--- a/config/hypr/hyprlock.conf
+++ b/config/hypr/hyprlock.conf
@@ -1 +1,150 @@
-source = ~/.cache/material/hyprlock.conf
+
+
+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
index 2eee02b..edd9ec3 100644
--- a/config/hypr/modus.conf
+++ b/config/hypr/modus.conf
@@ -1,3 +1,4 @@
+# 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
@@ -5,48 +6,52 @@ 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/Modus/config/scripts/
-
-bind = SHIFT ALT, T, exec, killall modus; python /home/sankalp/Modus/main.py # Reload Modus | Default: SUPER ALT + T
-bind = ALT, B, exec, $fabricSend 'launcher.open("bluetooth")' # Bluetooth | Default: ALT + B
-bind = SUPER, N, exec, $fabricSend 'launcher.open("wifi")' # Wifi | Default: SUPER + N
-bind = SUPER, E, exec, $fabricSend 'launcher.open("emoji")' # Emoji | Default: SUPER + E
-bind = SUPER, V, exec, $fabricSend 'launcher.open("cliphist")' # Cliphist | Default: SUPER + V
-bind = SUPER, T, exec, $fabricSend 'launcher.open("todo")' # Todo | Default: SUPER + T
-bind = SUPER, S, exec, $fabricSend 'launcher.open("sh")' # Sh | Default: SUPER + S
-bind = SUPER, W, exec, $fabricSend 'launcher.open("wallpapers")' # Wallpaper Selector | Default: SUPER + W
-bind =ALT SHIFT, W, exec, python -O $scriptsDir/wallpaper.py -R # Random Wallpaper | Default: ALT SHIFT + W
-bind = SUPER, D, exec, $fabricSend 'launcher.open("launcher")' # App Launcher | Default: SUPER + D
-bind = SUPER, X, exec, $fabricSend 'launcher.open("power")' # Power Menu | Default: SUPER + X
-bind = SUPER, H, exec, $fabricSend 'bar.toggle_hidden()' # Toggle Bar | Default: SUPER + H
-bind = SUPER, Z, exec, $fabricSend 'sc.screenshot()' # Screenshot Region | Default: SUPER + Z
-bind = , Print, exec, $fabricSend 'sc.screenshot(True)' # ScreenshotFullScreen | Default: Print
-bind = SUPER, R, exec, $fabricSend 'sc.screencast_start()' # Screencast | Default: SUPER + R
+$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 = 0
+ border_size = 2
layout = dwindle
}
decoration {
blur {
- enabled = false
+ enabled = yes
+ size = 5
+ passes = 3
+ new_optimizations = yes
+ contrast = 1
+ brightness = 1
}
- rounding = 15
- active_opacity = 1
- inactive_opacity = 1
- fullscreen_opacity = 1
-
- dim_inactive = true
- dim_strength = 0.1
- dim_special = 0.8
+ rounding = 14
shadow {
- enabled = false
+ enabled = true
+ range = 10
+ render_power = 2
+ color = rgba(0, 0, 0, 0.25)
}
}
@@ -66,8 +71,8 @@ animations {
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 # use with .2s duration
- # Animation configs
+ 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%
@@ -79,8 +84,5 @@ animations {
animation = fadeLayersIn, 1, 2, menu_decel
animation = fadeLayersOut, 1, 4.5, menu_accel
animation = workspaces, 1, 7, menu_decel, slide
- # animation = workspaces, 1, 2.5, softAcDecel, slide
- # animation = workspaces, 1, 7, menu_decel, slidefade 15%
- # animation = specialWorkspace, 1, 3, md3_decel, slidefadevert 15%
animation = specialWorkspace, 1, 3, md3_decel, slidevert
}
diff --git a/config/material-colors/generate.py b/config/material-colors/generate.py
deleted file mode 100644
index 6fa3446..0000000
--- a/config/material-colors/generate.py
+++ /dev/null
@@ -1,447 +0,0 @@
-import os
-import random
-import subprocess
-import json
-from PIL import Image
-import argparse
-from materialyoucolor.quantize import QuantizeCelebi # type: ignore
-from materialyoucolor.score.score import Score # type: ignore
-from materialyoucolor.hct import Hct # type: ignore
-from materialyoucolor.dynamiccolor.material_dynamic_colors import MaterialDynamicColors # type: ignore # noqa
-from materialyoucolor.scheme.dynamic_scheme import DynamicScheme # type: ignore # noqa
-from materialyoucolor.scheme.scheme_tonal_spot import SchemeTonalSpot # type: ignore # noqa
-from materialyoucolor.scheme.scheme_expressive import SchemeExpressive # type: ignore # noqa
-from materialyoucolor.scheme.scheme_fruit_salad import SchemeFruitSalad # type: ignore # noqa
-from materialyoucolor.scheme.scheme_monochrome import SchemeMonochrome # type: ignore # noqa
-from materialyoucolor.scheme.scheme_rainbow import SchemeRainbow # type: ignore # noqa
-from materialyoucolor.scheme.scheme_vibrant import SchemeVibrant # type: ignore # noqa
-from materialyoucolor.scheme.scheme_neutral import SchemeNeutral # type: ignore # noqa
-from materialyoucolor.scheme.scheme_fidelity import SchemeFidelity # type: ignore # noqa
-from materialyoucolor.scheme.scheme_content import SchemeContent # type: ignore # noqa
-import hashlib
-import pickle
-import numpy as np
-import re
-
-schemes = {
- "tonalSpot": SchemeTonalSpot,
- "expressive": SchemeExpressive,
- "fruitSalad": SchemeFruitSalad,
- "monochrome": SchemeMonochrome,
- "rainbow": SchemeRainbow,
- "vibrant": SchemeVibrant,
- "neutral": SchemeNeutral,
- "fidelity": SchemeFidelity,
- "content": SchemeContent,
-}
-
-
-def join(*args):
- return os.path.join(*args)
-
-
-home = os.path.expanduser("~")
-script_dir = os.path.dirname(os.path.abspath(__file__))
-cache_path = f"{home}/.cache/material/"
-os.makedirs(cache_path, exist_ok=True)
-
-
-def rgb_to_hex(rgb):
- return "#{:02x}{:02x}{:02x}".format(*rgb[:3])
-
-
-def rgba_to_rgb(rgba):
- return f"{rgba[0]}, {rgba[1]}, {rgba[2]}"
-
-
-class Color:
- def __init__(self, scheme) -> None:
- self.scheme = scheme
-
- def __call__(self, name: str):
- color_name = getattr(MaterialDynamicColors, name)
- if hasattr(color_name, "get_hct"):
- return rgb_to_hex(color_name.get_hct(self.scheme).to_rgba())
- else:
- return "#000000"
-
-
-class ColorsCache:
- def __init__(
- self,
- colors: DynamicScheme | dict,
- wallpaper: str,
- original_color: int,
- contrast_level: int,
- scheme_name: str,
- ) -> None:
- self.colors: dict[str, str] = {}
- self.wallpaper = wallpaper
- self.original_color = original_color
- self.contrast_level = contrast_level
- self.scheme_name = scheme_name
- if isinstance(colors, DynamicScheme):
- for color in vars(MaterialDynamicColors).keys():
- color_name = getattr(MaterialDynamicColors, color)
- if hasattr(color_name, "get_hct"):
- self.colors[str(color)] = rgb_to_hex(
- color_name.get_hct(colors).to_rgba()
- )
- else:
- self.colors = colors
-
-
-def colors_dict(cache: ColorsCache):
- return {
- "wallpaper": cache.wallpaper,
- "colors": cache.colors,
- "original_color": cache.original_color,
- "contrast_level": cache.contrast_level,
- "scheme_name": cache.scheme_name,
- }
-
-
-def get_cache_object(object: dict | str):
- if isinstance(object, str):
- object = dict(json.loads(object))
- colors = object["colors"]
- wallpaper = object["wallpaper"]
- original_color = object["original_color"]
- contrast_level = object.get("contrast_level", 0)
- scheme_name = object.get("scheme_name", "tonalSpot")
- return ColorsCache(colors, wallpaper, original_color, contrast_level, scheme_name)
-
-
-def get_file_list(folder_path):
- file_list = []
- for file in os.listdir(folder_path):
- if os.path.isfile(join(folder_path, file)):
- file_list.append(join(folder_path, file))
- return file_list
-
-
-ready_templates = {
- "colors.css": "@define-color {name} {hex};\n",
- "colors.scss": "${name}: {hex};\n",
-}
-additional = {"onBackground": "foreground"}
-
-
-def generate_color_map(scheme):
- colors = {}
- for color_name in vars(MaterialDynamicColors).keys():
- color_obj = getattr(MaterialDynamicColors, color_name)
- if hasattr(color_obj, "get_hct"):
- colors[color_name] = rgb_to_hex(color_obj.get_hct(scheme).to_rgba())
- return colors
-
-
-class ColorFormatter:
- def __init__(self, scheme: DynamicScheme):
- self.color_map = generate_color_map(scheme)
-
- def apply_transformations(self, value, transformations):
- intermediate_transforms = [
- t for t in transformations if not (t.startswith("strip") or t == "rgb")
- ]
- final_transforms = [
- t for t in transformations if t.startswith("strip") or t == "rgb"
- ]
-
- for transform in intermediate_transforms:
- if transform.startswith("lighten"):
- percent = int(re.search(r"\d+", transform).group())
- value = self.adjust_brightness(value, percent)
- elif transform.startswith("darken"):
- percent = int(re.search(r"\d+", transform).group())
- value = self.adjust_brightness(value, -percent)
-
- for transform in final_transforms:
- if transform.startswith("strip"):
- value = value.lstrip("#")
- elif transform == "rgb":
- value = self.hex_to_rgb(value)
-
- return value
-
- def adjust_brightness(self, hex_color, factor):
- def min_max(v):
- return min(255, max(0, v))
-
- hex_color = hex_color.lstrip("#")
- r, g, b = (int(hex_color[i : i + 2], 16) for i in (0, 2, 4))
- r = min_max(int(r * (1 + factor / 100)))
- g = min_max(int(g * (1 + factor / 100)))
- b = min_max(int(b * (1 + factor / 100)))
- return f"#{r:02X}{g:02X}{b:02X}".lower()
-
- def hex_to_rgb(self, hex_color):
- hex_color = hex_color.lstrip("#")
- r, g, b = (int(hex_color[i : i + 2], 16) for i in (0, 2, 4))
- return f"{r},{g},{b}"
-
- def parse_transformations(self, transformations_str):
- transformations = re.findall(r"(\w+)(?:\((\d*)\))?", transformations_str)
- result = []
- for command, arg in transformations:
- if command:
- if arg:
- result.append(f"{command}({arg})")
- else:
- result.append(command)
- return result
-
- def format(self, text):
- pattern = r"(<(\w+)(?:\.(.+))?>)"
- matches = re.findall(pattern, text)
- result = []
- last_end = 0
-
- for match in matches:
- full_match, key, transformations_str = match
- if key in self.color_map:
- result.append(text[last_end : text.find(full_match, last_end)])
- value = self.color_map[key]
- if transformations_str:
- transformations = self.parse_transformations(transformations_str)
- value = self.apply_transformations(value, transformations)
- result.append(value)
- last_end = text.find(full_match, last_end) + len(full_match)
-
- result.append(text[last_end:])
- result = "".join(result)
- pattern = r"<\\\\([^>]+)>"
- result = re.sub(pattern, r"<\1>", result)
- return result
-
-
-def generate_templates(
- folder: str,
- output_folder: str,
- scheme: DynamicScheme,
- color_scheme: str,
- wallpaper: str = "",
-):
- if not os.path.exists(output_folder):
- os.makedirs(output_folder)
- if not os.path.exists(folder):
- os.makedirs(folder)
- file_list = get_file_list(folder)
-
- for file_path in file_list:
- with open(file_path) as f:
- template = f.read()
- formatter = ColorFormatter(scheme)
- template = formatter.format(template)
- template = (
- template.replace("", color_scheme)
- .replace("", output_folder)
- .replace("", wallpaper)
- )
- new_path = join(output_folder, os.path.basename(file_path))
- with open(new_path, "w") as f:
- f.write(template)
-
- for file in ready_templates:
- _template = ""
- for color in vars(MaterialDynamicColors).keys():
- color_name = getattr(MaterialDynamicColors, color)
- if hasattr(color_name, "get_hct"):
- rgba = color_name.get_hct(scheme).to_rgba()
- hex_color = rgb_to_hex(rgba)
- rgb_color = rgba_to_rgb(rgba)
- new_line = ready_templates[file].format(
- name=color, hex=hex_color, rgb=rgb_color
- )
- _template += new_line
- if color in additional:
- new_line = ready_templates[file].format(
- name=additional[color], hex=hex_color, rgb=rgb_color
- )
- _template += new_line
-
- new_path = join(output_folder, os.path.basename(file))
- with open(new_path, "w") as f:
- f.write(_template)
-
-
-def run_hooks(folder: str):
- if not os.path.exists(folder):
- os.makedirs(folder)
- file_list = get_file_list(folder)
- for file_path in file_list:
- file_name = os.path.basename(file_path)
- if file_name.endswith(".py"):
- subprocess.run(["python", "-O", file_path])
- elif file_name.endswith(".sh"):
- subprocess.run(["sh", file_path])
-
-
-def process_image(image_path, quality=2, num_colors=128):
- def get_cache_path(image_path):
- global cache_path
- _cache_path = join(cache_path, "cached_colors")
- hash_object = hashlib.md5(image_path.encode())
- cache_filename = hash_object.hexdigest() + ".pkl"
- os.makedirs(_cache_path, exist_ok=True)
- return join(_cache_path, cache_filename)
-
- def load_from_cache(cache_path):
- if os.path.exists(cache_path):
- with open(cache_path, "rb") as f:
- return pickle.load(f)
- return None
-
- def save_to_cache(cache_path, data):
- with open(cache_path, "wb") as f:
- pickle.dump(data, f)
-
- cache_file = get_cache_path(image_path)
- cached_result = load_from_cache(cache_file)
- if cached_result is not None:
- return cached_result
-
- image = Image.open(image_path).convert("RGB")
- image_data = np.array(image)
- pixel_array = image_data[::quality, ::quality].reshape(-1, 3)
- result = QuantizeCelebi(pixel_array, num_colors)
- color = Score.score(result)[0]
- save_to_cache(cache_file, color)
- return color
-
-
-def main(
- color_scheme: str,
- image_path: str,
- use_color: int | None = None,
- scheme_name: str | None = None,
- contrast_level: int = 0,
-):
- scheme_name = scheme_name or "tonalSpot"
- scheme = schemes[scheme_name]
- is_dark = True if color_scheme == "dark" else False
- if use_color is None:
- color = process_image(image_path, 4, 1024)
- else:
- color = use_color
-
- scheme = scheme(Hct.from_int(color), is_dark, contrast_level)
- # for color in vars(MaterialDynamicColors).keys():
- # color_name = getattr(MaterialDynamicColors, color)
- # if hasattr(color_name, "get_hct"):
- # print(color, rgb_to_hex(color_name.get_hct(scheme).to_rgba()))
-
- with open(join(cache_path, "colors.json"), "w") as f:
- cache_obj = ColorsCache(
- generate_color_map(scheme), image_path, color, contrast_level, scheme_name
- )
- json.dump(colors_dict(cache_obj), f, indent=2)
-
- generate_templates(
- f"{script_dir}/templates", cache_path, scheme, color_scheme, image_path
- )
-
- if is_dark:
- subprocess.run(["sh", join(script_dir, "gtk-material.sh")])
- else:
- subprocess.run(["sh", join(script_dir, "gtk-material.sh"), "--light"])
-
- run_hooks(join(script_dir, "hooks"))
-
-
-def _argparse() -> tuple[bool, bool, str, str, str, str]:
- parser = argparse.ArgumentParser()
- group = parser.add_mutually_exclusive_group(required=True)
- group.add_argument("-R", help="Restore previous colorscheme.", action="store_true")
- group.add_argument(
- "-w", help="Use last used wallpaper for color generation.", action="store_true"
- )
- group.add_argument("--image", type=str, help="Image for color scheme generation")
- group.add_argument(
- "--color", type=str, help="Main color for color scheme generation"
- )
- parser.add_argument(
- "--color-scheme",
- type=str,
- default="dark",
- choices=["dark", "light"],
- help="Color scheme variant",
- )
- parser.add_argument(
- "--scheme",
- type=str,
- default="",
- choices=list(schemes.keys()),
- help="Scheme for color generation",
- )
- args = parser.parse_args()
- restore_palette = args.R
- use_last_used_wallpaper = args.w
- image = args.image
- color_scheme = args.color_scheme
- use_color = args.color
- scheme = args.scheme
- return (
- restore_palette,
- use_last_used_wallpaper,
- image,
- color_scheme,
- use_color,
- scheme,
- )
-
-
-def _main():
- restore_palette, use_last_used_wallpaper, image, color_scheme, use_color, scheme = (
- _argparse()
- )
- if not scheme:
- scheme = None
- if use_color is not None:
- try:
- with open(join(cache_path, "colors.json")) as f:
- content = get_cache_object(f.read())
- main(
- color_scheme,
- image_path=content.wallpaper,
- use_color=int(use_color.lstrip("#"), 16),
- scheme_name=scheme,
- )
- except Exception:
- main(
- color_scheme,
- image_path="/dev/null",
- use_color=int(use_color.lstrip("#"), 16),
- scheme_name=scheme,
- )
- elif restore_palette:
- with open(join(cache_path, "colors.json")) as f:
- content = get_cache_object(f.read())
- main(
- color_scheme,
- image_path=content.wallpaper,
- use_color=content.original_color,
- scheme_name=scheme or content.scheme_name,
- contrast_level=content.contrast_level,
- )
- elif use_last_used_wallpaper:
- with open(join(cache_path, "colors.json")) as f:
- content = get_cache_object(f.read())
- main(
- color_scheme,
- image_path=content.wallpaper,
- scheme_name=scheme or content.scheme_name,
- contrast_level=content.contrast_level,
- )
- else:
- if os.path.isdir(image):
- files = [
- f for f in os.listdir(image) if f.endswith((".png", ".jpg", ".jpeg"))
- ]
- if files:
- image = join(image, random.choice(files))
- main(color_scheme, image_path=image, scheme_name=scheme)
-
-
-if __name__ == "__main__":
- _main()
diff --git a/config/material-colors/gtk-material.sh b/config/material-colors/gtk-material.sh
deleted file mode 100755
index a197435..0000000
--- a/config/material-colors/gtk-material.sh
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/bin/bash
-
-dark_icon="Tela-nord-dark"
-light_icon="Tela-nord-light"
-
-if [ "$1" == "--light" ]; then
- gsettings set org.gnome.desktop.interface gtk-theme adw-gtk3
- gsettings set org.gnome.desktop.interface icon-theme "$light_icon"
- gsettings set org.gnome.desktop.interface color-scheme 'prefer-light'
-else
- gsettings set org.gnome.desktop.interface gtk-theme adw-gtk3-dark
- gsettings set org.gnome.desktop.interface color-scheme 'prefer-dark'
- gsettings set org.gnome.desktop.interface icon-theme "$dark_icon"
-fi
diff --git a/config/material-colors/hooks/discord.sh b/config/material-colors/hooks/discord.sh
deleted file mode 100755
index 2ec3a3b..0000000
--- a/config/material-colors/hooks/discord.sh
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/bin/bash
-material_cache="$HOME"/.cache/material/
-
-sass $material_cache/material-discord.scss $material_cache/material-discord.css
diff --git a/config/material-colors/hooks/kitty.sh b/config/material-colors/hooks/kitty.sh
deleted file mode 100755
index d6c3dfd..0000000
--- a/config/material-colors/hooks/kitty.sh
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/bin/bash
-# Define the cache path where your colors-kitty.conf file is stored.
-CACHE_PATH="${HOME}/.cache/material"
-COLORS_FILE="${CACHE_PATH}/colors-kitty.conf"
-
-if [ -f "$COLORS_FILE" ]; then
- # Reload kitty colors using kitty's remote control.
- kitty @ set-colors --all "$COLORS_FILE"
- # Send the USR1 signal to kitty to complete the reload process.
- pkill -USR1 kitty
- echo "Kitty colors reloaded successfully."
-else
- echo "Colors file not found at $COLORS_FILE."
-fi
diff --git a/config/material-colors/hooks/sddm.sh b/config/material-colors/hooks/sddm.sh
deleted file mode 100755
index 1528b6f..0000000
--- a/config/material-colors/hooks/sddm.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/bash
-
-"$HOME"/dotfiles/sddm/scripts/wallpaper.sh 1>/dev/null
diff --git a/config/material-colors/hooks/telegram.sh b/config/material-colors/hooks/telegram.sh
deleted file mode 100755
index dc001d7..0000000
--- a/config/material-colors/hooks/telegram.sh
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/bin/bash
-cache_folder="$HOME"/.cache/material
-telegram_theme_cache="$HOME"/.cache/telegram-theme
-
-bg_color_file="$cache_folder/bg-color"
-bg_color=$(cat "$bg_color_file")
-magick -size 256x256 "xc:$bg_color" $telegram_theme_cache/tiled.png
-
-mkdir -p $telegram_theme_cache
-
-cp $cache_folder/colors.tdesktop-theme $telegram_theme_cache/colors.tdesktop-theme
-
-zip -q -j $telegram_theme_cache/pallete.tdesktop-theme $telegram_theme_cache/tiled.png $telegram_theme_cache/colors.tdesktop-theme
diff --git a/config/material-colors/templates/bg-color b/config/material-colors/templates/bg-color
deleted file mode 100644
index af5dbef..0000000
--- a/config/material-colors/templates/bg-color
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/config/material-colors/templates/colors-hyprland.conf b/config/material-colors/templates/colors-hyprland.conf
deleted file mode 100644
index 2766e30..0000000
--- a/config/material-colors/templates/colors-hyprland.conf
+++ /dev/null
@@ -1,5 +0,0 @@
-$background = rgb()
-$foreground = rgb()
-$border_color_active = rgb()
-$border_color_inactive = rgb()
-# <\\outline>
diff --git a/config/material-colors/templates/colors-kitty.conf b/config/material-colors/templates/colors-kitty.conf
deleted file mode 100644
index eeebd86..0000000
--- a/config/material-colors/templates/colors-kitty.conf
+++ /dev/null
@@ -1,27 +0,0 @@
-foreground
-background
-cursor
-
-color0
-color8
-
-color1
-color9
-
-color2
-color10
-
-color3
-color11
-
-color4
-color12
-
-color5
-color13
-
-color6
-color14
-
-color7
-color15
\ No newline at end of file
diff --git a/config/material-colors/templates/colors-sddm-style.conf b/config/material-colors/templates/colors-sddm-style.conf
deleted file mode 100644
index d9417aa..0000000
--- a/config/material-colors/templates/colors-sddm-style.conf
+++ /dev/null
@@ -1,129 +0,0 @@
-## Please see https://github.com/aczw/sddm-theme-corners/blob/main/CONFIG.md for
-## more information about these options. Happy configuring!
-
-[General]
-
-# BgSource: string. Path to an image, used as the wallpaper. Can be placed
-# in the `backgrounds/` folder for convenience (see below!)
-#
-# FontFamily: string. Name of the font family. You can find this with
-# something like `fontconfig`, for example.
-#
-# FontSize: number. This value is used for everything *except* the date/time.
-#
-# Padding: number. How far away things should be from the edge of your screen.
-#
-# Radius: number. Corner radius for UI. Set to 0 to disable rounded corners.
-#
-# Scale: number. Adjusts the size of UI elements. Can be anything, but I
-# would stick between 1 and 2 (e.g. 1.5).
-
-BgSource="backgrounds/CURRENTWALLPAPER"
-FontFamily="Atkinson Hyperlegible"
-FontSize=9
-Padding=50
-Radius=10
-Scale=1
-
-# UserPictureEnabled: boolean. By default the user picture is shown. Set to false
-# if you don't have/want a user picture, or you don't use the
-# user selection functionality.
-#
-# UserBorderWidth: number. Width of the border around your picture. Set to
-# 0 to remove.
-#
-# UserBorderColor: color. Border color around your picture. Useless if border
-# width is 0.
-#
-# UserColor: color. Background color of the default, blank avatar. Only
-# visible if you don't have your own picture.
-
-UserPictureEnabled=false
-UserBorderWidth=5
-UserBorderColor=""
-UserColor=""
-
-# InputColor: color. Background color of the input fields.
-#
-# InputTextColor: color. Color of text you enter in the fields.
-#
-# InputBorderWidth: number. Width of the border around the active field. Set
-# to 0 to remove.
-#
-# InputBorderColor: color. Border color of active field. Useless if border
-# width is 0.
-#
-# UserPlaceholderText: string. Placeholder text shown when user field is empty.
-#
-# PassPlaceholderText: string. Placeholder text shown when user field is empty.
-#
-# HidePassword: boolean. Whether to replace your entered password with dots.
-
-InputColor=""
-InputTextColor=""
-InputBorderWidth=4
-InputBorderColor=""
-UserPlaceholderText="user"
-PassPlaceholderText="password"
-HidePassword=true
-
-# LoginButtonTextColor: color. Text color on the login button.
-#
-# LoginButtonText: string. Text displayed on the button.
-#
-# LoginButtonColor: color. Background color of the button.
-
-LoginButtonTextColor=""
-LoginButtonText="Login"
-LoginButtonColor=""
-
-# PopupColor: color. Background color of popup window.
-#
-# PopupActiveColor: color. Color around the currently selected entry.
-#
-# PopupActiveTextColor: color. Text color of the currently selected entry.
-# Mainly provided for potential contrast issues.
-
-PopupColor=""
-PopupActiveColor=""
-PopupActiveTextColor=""
-
-# SessionButtonColor: color. Session button background color.
-#
-# SessionIconColor: color. Color of the icon inside the session button.
-#
-# PowerButtonColor: color. Power button background color.
-#
-# PowerIconColor: color. Color of the icon inside the power button.
-
-SessionButtonColor=""
-SessionIconColor=""
-PowerButtonColor=""
-PowerIconColor=""
-
-# DateTimeSpacing: number. Spacing between the date and time.
-#
-# Date/TimeColor: color. Date/time text color.
-#
-# Date/TimeSize: number. Font size for the date/time.
-#
-# Date/TimeIsBold: boolean. Whether date/time text should be bolded.
-#
-# Date/TimeOpacity: number. Date/time text opacity.
-#
-# Date/TimeFormat: string. Change the format of how the date and time is displayed.
-# Note that they use different formats.
-
-DateTimeSpacing=-20
-
-DateColor=""
-DateSize=36
-DateIsBold=false
-DateOpacity=1.0
-DateFormat="dddd, MMMM d"
-
-TimeColor=""
-TimeSize=48
-TimeIsBold=true
-TimeOpacity=1.0
-TimeFormat="hh:mm AP"
diff --git a/config/material-colors/templates/colors.tdesktop-theme b/config/material-colors/templates/colors.tdesktop-theme
deleted file mode 100644
index 7b4db51..0000000
--- a/config/material-colors/templates/colors.tdesktop-theme
+++ /dev/null
@@ -1,568 +0,0 @@
-// vim:ft=cfg
-
-name: pywal;
-
-// Color presets //
-
-secondary: ;
-onSecondary: ;
-outline: ;
-shadow: ;
-
-BACKGROUND: ;
-BACKGROUND_LIT_LIGHT: ;
-BACKGROUND_LIGHT: ;
-BACKGROUND_EXTRA_LIGHT: ;
-ON_BACKGROUND: ;
-ON_BACKGROUND_DARKER: ;
-
-ACCENT: ;
-ON_ACCENT: ;
-ACCENT_LIGHT: ;
-ON_ACCENT_LIGHT: ;
-ACCENT_DARK: ;
-ON_ACCENT_DARK: ;
-
-ERROR: ;
-ERROR_RIPPLE: #ff9180;
-ON_ERROR: ;
-
-SUCCES: #66bb6a;
-SUCCES_LIGHT: #81c784;
-SUCCES_DARK: #388e3c;
-
-COLOR_PINK: #e016a0; // Just for testing purposes :)
-
-// basic
-windowBg: BACKGROUND; // Window backgrounds
-windowFg: ON_BACKGROUND; // Main text / chat text
-windowBgOver: BACKGROUND_LIGHT; // Search field background + buttons color in context menu
-windowBgRipple: BACKGROUND_EXTRA_LIGHT; // Inactive chat in chats list (press & hold)
-windowFgOver: windowFg; // black: fallback for over text color
-windowSubTextFg: ON_BACKGROUND_DARKER; // Chats list message preview text
-windowSubTextFgOver: ON_BACKGROUND_DARKER; // Mouse over chats list message preview text
-windowBoldFg: ON_BACKGROUND; // Bold font (titles in Settings, Left menu options)
-windowBoldFgOver: ACCENT_LIGHT; // Mouse over chats list chat name text (+ Left menu options on hover)
-windowBgActive: ACCENT; // Active button background (+ checkboxes background)
-windowFgActive: ON_ACCENT; // Active selected chats list text / button text + checkboxes' "ticks" color
-windowActiveTextFg: ACCENT_DARK; // Online status, links, recording cancel, etc
-windowShadowFg: BACKGROUND; // Window border shadows
-windowShadowFgFallback: BACKGROUND; // Notification border
-
-shadowFg: #00000018; // Internal border shadows
-slideFadeOutBg: #0000003c; // Slide animation (chat to profile) fade out filling
-slideFadeOutShadowFg: windowShadowFg; // Slide animation (chat to profile) fade out right section shadow
-
-imageBg: BACKGROUND; // Image background fallback (when photo size is less than minimum allowed)
-imageBgTransparent: ON_BACKGROUND; // Image background when displaying an image with opacity where no opacity is needed
-
-//widgets
-activeButtonBg: ACCENT; // Currently selected button background
-activeButtonBgOver: ACCENT_LIGHT; // Mouse over currently selected button background
-activeButtonBgRipple: ACCENT; // Primary button background color (and active chat) - Press & hold
-activeButtonFg: ON_ACCENT; // Currently selected button text
-activeButtonFgOver: ON_ACCENT_LIGHT; // Mouse over currently selected button text
-activeButtonSecondaryFg: ON_BACKGROUND; // Counter color on primary button
-activeButtonSecondaryFgOver: ON_BACKGROUND; // Counter color on primary button (hover)
-activeLineFg: ACCENT_LIGHT; // Active input field line color (normal)
-activeLineFgError: ERROR; // Active input field line color (error)
-
-lightButtonBg: BACKGROUND; // Secondary button background color (default)
-lightButtonBgOver: BACKGROUND_LIGHT; // Secondary button background color (hover)
-lightButtonBgRipple: BACKGROUND_EXTRA_LIGHT; // Secondary button background color (press & hold)
-lightButtonFg: ON_BACKGROUND; // Secondary button text color
-lightButtonFgOver: lightButtonFg; // Secondary button text color (hover)
-
-attentionButtonFg: ERROR; // Default attention button text (like confirm button on log out)
-attentionButtonFgOver: ON_ERROR; // Default attention button text with mouse over
-attentionButtonBgOver: ERROR; // Default attention button background with mouse over
-attentionButtonBgRipple: ERROR_RIPPLE; // Default attention button ripple effect
-
-outlineButtonBg: BACKGROUND; // Buttons background in profiles (shared media)
-outlineButtonBgOver: lightButtonBgOver; // Buttons background in profiles (hover)
-outlineButtonOutlineFg: windowBgActive; // Default left outlined button left outline border
-outlineButtonBgRipple: lightButtonBgRipple; // Default left outlined button ripple effect
-
-menuBg: BACKGROUND; // Default popup menu background
-menuBgOver: windowBgOver; // Default popup menu item background with mouse over
-menuBgRipple: windowBgRipple; // Default popup menu item ripple effect
-menuIconFg: ON_BACKGROUND; // Icons color (left menu button, scroll down, paperclip..)
-menuIconFgOver: ACCENT_DARK; // Icons color on hover (left menu button, scroll down, paperclip..)
-menuSubmenuArrowFg: #373737; // Default popup menu submenu arrow icon (like in message field context menu in case of RTL system language)
-menuFgDisabled: #727272; // Default popup menu item disabled text (like unavailable items in message field context menu)
-menuSeparatorFg: BACKGROUND; // Default popup menu separator (like in message field context menu)
-
-//
-
-scrollBarBg: ACCENT; // Scrollbar (chats list) current position
-scrollBarBgOver: ACCENT_DARK; // Mouse over scrollbar (chats list) current position
-scrollBg: #0000001a; // Scrollbar (chats list) background
-scrollBgOver: #0000002c; // Mouse over scrollbar (chats list) background
-
-smallCloseIconFg: ACCENT; // Notification close button (default)
-smallCloseIconFgOver: ACCENT_LIGHT; // Notification close button (hover)
-
-radialFg: ON_BACKGROUND; // Default radial loader line (like in Media Viewer when loading a photo)
-radialBg: #00000056; // Default radial loader background (like in Media Viewer when loading a photo)
-
-placeholderFg: ON_BACKGROUND_DARKER; // Inactive input box placeholder text
-placeholderFgActive: ON_BACKGROUND_DARKER; // Active input box placeholder text
-inputBorderFg: ACCENT; // Default input field bottom border (like in code input field when you log in and field is not focused)
-filterInputBorderFg: ACCENT_DARK; // Default rounded input field border (like in chats list search field when field is focused)
-filterInputInactiveBg: windowBgOver; // Default rounded input field background (like in chats list search field when field is inactive)
-checkboxFg: #b3b3b3; // Default unchecked checkbox rounded rectangle (and also emoji category icons)
-
-sliderBgInactive: #e1eaef; // Inactive media player button (prev/next song)
-sliderBgActive: windowBgActive; // Active media player button (prev/next song)
-
-tooltipBg: #eef2f5; // Tooltip background (hover on message sent time)
-tooltipFg: #5d6c80; // Tooltip text color
-tooltipBorderFg: #c9d1db; // Tooltip border color
-
-
-// custom title bar for Windows and macOS
-titleBg: BACKGROUND; // Title bar color
-titleBgActive: titleBg; // Title bar color when Telegram window is focused
-titleButtonBg: titleBg; // Title bar min/max/close buttons background (Windows only)
-titleShadow: #00000003; // One pixel line shadow at the bottom of custom window title
-titleButtonFg: BACKGROUND; // Title bar buttons(min/max/close) (Windows only)
-titleButtonBgOver: BACKGROUND_LIGHT; // Title bar min/max background (hover) (Windows only)
-titleButtonFgOver: ON_BACKGROUND; // Title bar min/max icons (Windows only)
-titleButtonBgActive: titleButtonBg; // Custom window title minimize/maximize/restore button background when window is active (Windows only)
-titleButtonFgActive: titleButtonFg; // Custom window title minimize/maximize/restore button icon when window is active (Windows only)
-titleButtonBgActiveOver: titleButtonBgOver; // Custom window title minimize/maximize/restore button background with mouse over when window is active (Windows only)
-titleButtonFgActiveOver: titleButtonFgOver; // Custom window title minimize/maximize/restore button icon with mouse over when window is active (Windows only)
-titleButtonCloseBg: titleButtonBg; // Custom window title close button background when window is inactive (Windows only)
-titleButtonCloseFg: titleButtonFg; // Custom window title close button icon when window is inactive (Windows only)
-titleButtonCloseBgOver: ERROR; // Close button background (hover)
-titleButtonCloseFgOver: ON_ERROR; // Close button icon color (hover)
-titleButtonCloseBgActive: titleButtonCloseBg; // Custom window title close button background when window is active (Windows only)
-titleButtonCloseFgActive: titleButtonCloseFg; // Custom window title close button icon when window is active (Windows only)
-titleButtonCloseBgActiveOver: titleButtonCloseBgOver; // Custom window title close button background with mouse over when window is active (Windows only)
-titleButtonCloseFgActiveOver: titleButtonCloseFgOver; // Custom window title close button icon with mouse over when window is active (Windows only)
-titleFgActive: #3e3c3e; // Active title bar text (Mac)
-titleFg: #acacac; // Inactive title bar text (Mac)
-
-// tray icon
-trayCounterBg: ON_ERROR; // Unread messages counter background in tray (not muted, default red)
-trayCounterBgMute: BACKGROUND; // Unread messages counter background in tray (muted, default gray)
-trayCounterFg: ON_BACKGROUND; // Unread messages counter text (number)
-trayCounterBgMacInvert: ON_BACKGROUND; // Tray icon counter background when tray icon is pressed or when dark theme of macOS is used (macOS only)
-trayCounterFgMacInvert: ON_BACKGROUND; // Tray icon counter text when tray icon is pressed or when dark theme of macOS is used (macOS only)
-
-// layers
-layerBg: #0000007f; // Box and main menu background layer fade (for example, current chat tint when forwarding message)
-
-cancelIconFg: menuIconFg; // Default for settings close icon and box search cancel icon
-cancelIconFgOver: menuIconFgOver; // Default for settings close icon and box search cancel icon with mouse over
-
-// boxes
-boxBg: BACKGROUND; // Popup windows background color
-boxTextFg: ON_BACKGROUND; // Popup windows text color
-boxTextFgGood: SUCCES; // Popup windows "good" text (e.g. "Username available")
-boxTextFgError: ERROR; // Popup windows "bad" text (e.g. "Username already taken")
-boxTitleFg: ACCENT; // Popup windows titles color (sessions, contacts...)
-boxSearchBg: boxBg; // Box search field background (like in contacts box)
-// boxSearchCancelIconFg: cancelIconFg; // (not implemented yet) Search cancel X button icon (like in contacts box)
-boxSearchCancelIconFgOver: cancelIconFgOver; // (not implemented yet) Search cancel X button icon with mouse over
-
-boxTitleAdditionalFg: ACCENT_LIGHT; // Box title additional text (like in create group box when you see chosen members count)
-boxTitleCloseFg: cancelIconFg; // Settings close icon and box search cancel icon (like in contacts box)
-boxTitleCloseFgOver: cancelIconFgOver; // Settings close icon and box search cancel icon (like in contacts box) with mouse over
-
-membersAboutLimitFg: windowSubTextFgOver; // Text in channel members box about the limit (max 200 last members are shown)
-
-contactsBg: BACKGROUND; // Contacts (and some other) list background color
-contactsBgOver: windowBgOver; // Contacts (and some other) list mouse over background
-contactsNameFg: ON_BACKGROUND; // Contacts (and some other) box row name text
-contactsStatusFg: ON_BACKGROUND_DARKER; // Contacts (and some other) box row additional text (like last seen stamp)
-contactsStatusFgOver: windowSubTextFgOver; // Contacts (and some other) box row additional text (like last seen stamp) with mouse over
-contactsStatusFgOnline: ACCENT_LIGHT; // Contacts (and some other) box row active additional text (like online status)
-
-photoCropFadeBg: layerBg; // "Out-of-bounds" area in photo cropping (avatars etc...)
-photoCropPointFg: ON_BACKGROUND; // Photo crop "corners" (which you drag to crop photo)
-
-callArrowFg: boxTextFgGood; // Received phone call arrow (in calls list box)
-callArrowMissedFg: boxTextFgError; // Missed phone call arrow (in calls list box)
-
-// intro
-introBg: BACKGROUND; // Login screen background
-introTitleFg: windowBoldFg; // Login title text
-introDescriptionFg: ON_BACKGROUND_DARKER; // Login description text
-introErrorFg: ERROR; // Login error text (like when providing a wrong log in code)
-
-introCoverTopBg: BACKGROUND; // Gradient finish at the top of the login screen
-introCoverBottomBg: BACKGROUND; // Gradient start at the bottom of the login screen
-introCoverIconsFg: ACCENT_DARK; // Icons and clouds colors
-introCoverPlaneTrace: 69; // Plane trace
-introCoverPlaneInner: ON_ACCENT; // Plane color inside
-introCoverPlaneOuter: ACCENT; // Plane color outside
-introCoverPlaneTop: ON_BACKGROUND; // Main plane color
-
-dialogsMenuIconFg: menuIconFg; // Main menu and lock telegram icon
-dialogsMenuIconFgOver: menuIconFgOver; // Main menu and lock telegram icon with mouse over
-
-dialogsBg: BACKGROUND; // Chats list background color
-dialogsNameFg: ON_BACKGROUND; // Chat list name text
-dialogsChatIconFg: dialogsNameFg; // Chat list group or channel icon
-dialogsDateFg: ON_BACKGROUND_DARKER; // Chats list date text
-dialogsTextFg: ON_BACKGROUND_DARKER; // Chats list message preview text
-dialogsTextFgService: ACCENT_LIGHT; // Chats list message preview sender name
-dialogsDraftFg: ACCENT; // "Draft" word
-dialogsVerifiedIconBg: windowBgActive; // Verification icon background
-dialogsVerifiedIconFg: ON_ACCENT; // Verification icon check color
-dialogsSendingIconFg: #c1c1c1; // Chat list sending message icon (clock)
-dialogsSentIconFg: ACCENT; // Chat list sent message tick / double tick icon
-dialogsUnreadBg: ACCENT; // Chats list unread counter background
-dialogsUnreadBgMuted: ACCENT_DARK; // Chat list unread badge background for muted chat
-dialogsUnreadFg: ON_ACCENT; // Unread counter text color
-dialogsOnlineBadgeFg: ACCENT_LIGHT; // Chat list online status (for inactive item)
-
-dialogsBgOver: BACKGROUND_LIGHT; // Inactive chat color on hover
-dialogsNameFgOver: ACCENT_LIGHT; // Chat list name text with mouse over
-dialogsChatIconFgOver: dialogsNameFgOver; // Chat list group or channel icon with mouse over
-dialogsDateFgOver: windowSubTextFgOver; // Chat list date text with mouse over
-dialogsTextFgOver: windowSubTextFgOver; // Chat list message text with mouse over
-dialogsTextFgServiceOver: dialogsTextFgService; // Mouse over chats list message preview sender name
-dialogsDraftFgOver: dialogsDraftFg; // Chat list draft label with mouse over
-dialogsVerifiedIconBgOver: dialogsVerifiedIconBg; // Chat list verified icon background with mouse over
-dialogsVerifiedIconFgOver: dialogsVerifiedIconFg; // Chat list verified icon check with mouse over
-dialogsSendingIconFgOver: dialogsSendingIconFg; // Chat list sending message icon (clock) with mouse over
-dialogsSentIconFgOver: dialogsSentIconFg; // Chat list sent message tick / double tick icon with mouse over
-dialogsUnreadBgOver: dialogsUnreadBg; // Chat list unread badge background for NOT muted chat with mouse over
-dialogsUnreadBgMutedOver: dialogsUnreadBgMuted; // Chat list unread badge background for muted chat with mouse over
-dialogsUnreadFgOver: dialogsUnreadFg; // Chat list unread badge text with mouse over
-
-dialogsBgActive: ACCENT; // Active chat color (in chats list)
-dialogsNameFgActive: ON_ACCENT; // Active chat name text (in chats list)
-dialogsChatIconFgActive: dialogsNameFgActive; // Active chat group or channel icon (in chats list)
-dialogsDateFgActive: ON_ACCENT; // Active chat date color
-dialogsTextFgActive: ON_ACCENT; // Active chat message preview color
-dialogsTextFgServiceActive: dialogsTextFgActive;// Active chat service messages color
-dialogsDraftFgActive: ON_ACCENT_LIGHT; // "Draft" word when chat is selected
-dialogsVerifiedIconBgActive: dialogsTextFgActive; // Active chat verified icon background (in chats list)
-dialogsVerifiedIconFgActive: dialogsBgActive; // Active chat verified icon check (in chats list)
-dialogsSendingIconFgActive: ON_ACCENT_LIGHT; // Active chat sending message icon (clock) (in chats list)
-dialogsSentIconFgActive: dialogsTextFgActive; // Active chat sent message tick / double tick icon (in chats list)
-dialogsUnreadBgActive: dialogsTextFgActive; // Active chat unread badge background for NOT muted chat (in chats list)
-dialogsUnreadBgMutedActive: dialogsDraftFgActive; // Active chat unread badge background for muted chat (in chats list)
-dialogsUnreadFgActive: dialogsBgActive; // Active chat unread badge text (in chats list)
-
-dialogsRippleBg: windowBgRipple;
-dialogsRippleBgActive: activeButtonBgRipple;
-
-dialogsForwardBg: dialogsBgActive; // Forwarding panel background (when forwarding messages in the smallest window size)
-dialogsForwardFg: dialogsNameFgActive; // Forwarding panel text (when forwarding messages in the smallest window size)
-
-searchedBarBg: BACKGROUND; // Search results window background color
-searchedBarFg: ACCENT_LIGHT; // Search results text color
-
-// history
-topBarBg: BACKGROUND; // Chat header background
-
-emojiPanBg: BACKGROUND; // Sticker panel background
-emojiPanCategories: BACKGROUND; // Sticker panel bottom bar background
-emojiPanHeaderFg: ACCENT; // Sticker panel title text
-emojiPanHeaderBg: BACKGROUND; // Sticker panel top bar background
-stickerPanDeleteBg: #000000cc; // Delete X button background for custom sent stickers in stickers panel (legacy)
-stickerPanDeleteFg: ON_BACKGROUND; // Delete X button icon for custom sent stickers in stickers panel (legacy)
-stickerPreviewBg: #2f2f2fCC; // Sticker and GIF preview background (when you press and hold on a sticker)
-
-historyTextInFg: ON_BACKGROUND; // Incoming message text color
-historyTextInFgSelected: ON_ACCENT; // Selected incoming message text or text in a selected message
-historyTextOutFg: ON_BACKGROUND; // Outgoing message text color
-historyTextOutFgSelected: ON_ACCENT; // Outgoing message selected text or text in a selected message
-historyLinkInFg: ACCENT_LIGHT; // Incoming message link color
-historyLinkInFgSelected: historyLinkInFg; // Incoming message link in a selected text or message
-historyLinkOutFg: ACCENT_LIGHT; // Outgoing message link color
-historyLinkOutFgSelected: historyLinkOutFg; // Outgoing message link in a selected text or message
-historyFileNameInFg: historyTextInFg; // Incoming message filename text color
-historyFileNameInFgSelected: historyFileNameInFg; // Incoming media filename text in a selected message
-historyFileNameOutFg: historyTextOutFg; // Outgoing message filename text color
-historyFileNameOutFgSelected: historyFileNameOutFg; // Outgoing media filename text in a selected message
-historyOutIconFg: dialogsSentIconFg; // "Eye" and "Checks" color for outgoing messages (selected)
-historyOutIconFgSelected: ACCENT_DARK; // "Eye" and "Checks" color for outgoing messages (selected)
-historyIconFgInverted: ON_ACCENT; // "Checks" on stickers (outgoing)
-historySendingOutIconFg: ACCENT_LIGHT; // "Clock" icon while sending file (outgoing)
-historySendingInIconFg: ACCENT_LIGHT; // "Clock" icon while sending file (incoming)
-historySendingInvertedIconFg: ACCENT_LIGHT; // Media sending message icon (clock) (like in sent photo)
-historyCallArrowInFg: callArrowFg; // Received phone call arrow
-historyCallArrowInFgSelected: callArrowFg; // Received phone call arrow in a selected message
-historyCallArrowMissedInFg: callArrowMissedFg; // Missed phone call arrow
-historyCallArrowMissedInFgSelected: callArrowMissedFg; // Missed phone call arrow in a selected message
-historyCallArrowOutFg: historyCallArrowInFg; // Outgoing phone call arrow
-historyCallArrowOutFgSelected: historyCallArrowInFgSelected; // Outgoing phone call arrow
-
-historyUnreadBarBg: #424444; // Unread messages bar background color
-historyUnreadBarBorder: BACKGROUND; // Unread messages bar border color
-historyUnreadBarFg: #ededed; // Unread messages bar text color
-
-historyForwardChooseBg: #0000004c; // "Choose recipient" BACKGROUND color (forwarding)
-historyForwardChooseFg: BACKGROUND; // "Choose recipient" text color (forwarding)
-
-historyPeer1NameFg: #E53935; // Group chat sender name - Red
-historyPeer1NameFgSelected: historyPeer1NameFg; // Red group member name in a selected message
-historyPeer1UserpicBg: #EF9A9A; // Group chat userpic background - Red
-
-historyPeer2NameFg: #7CB342; // Sender name - Green
-historyPeer2NameFgSelected: historyPeer2NameFg; // Green group member name in a selected message
-historyPeer2UserpicBg: #C5E1A5; // Userpic background - Green
-
-historyPeer3NameFg: #FDD835; // Sender name - Yellow
-historyPeer3NameFgSelected: historyPeer3NameFg; // Yellow group member name in a selected message
-historyPeer3UserpicBg: #FFF59D; // Userpic background - Yellow
-
-historyPeer4NameFg: #C0CA33; // Sender name - Light green
-historyPeer4NameFgSelected: historyPeer4NameFg; // Blue group member name in a selected message
-historyPeer4UserpicBg: #81D4FA; // Userpic background - Blue
-
-historyPeer5NameFg: #7E57C2; // Sender name - Purple
-historyPeer5NameFgSelected: historyPeer5NameFg; // Purple group member name in a selected message
-historyPeer5UserpicBg: #B39DDB; // Userpic background - Light Purple
-
-historyPeer6NameFg: #D81B60; // Sender name - "Raspberry"
-historyPeer6NameFgSelected: historyPeer6NameFg; // "Raspberry" group member name in a selected message
-historyPeer6UserpicBg: #F48FB1; // Userpic background - Light "Raspberry"
-
-historyPeer7NameFg: #00ACC1; // Sender name - Cyan
-historyPeer7NameFgSelected: historyPeer7NameFg; // Cyan group member name in a selected message
-historyPeer7UserpicBg: #80DEEA; // Userpic background - Cyan
-
-historyPeer8NameFg: #FB8C00; // Sender name - Orange
-historyPeer8NameFgSelected: historyPeer8NameFg; // Orange group member name in a selected message
-historyPeer8UserpicBg: #FFE082; // Userpic background - Light Orange
-
-historyPeerUserpicFg: ON_BACKGROUND; // Default userpic initials
-
-// Some values are marked as (adjusted), it means they're adjusted by
-// hue and saturation of the average background color if user chooses
-// some other (not bundled to this color theme) background. If the
-// bundled background is used those colors are not adjusted in any way.
-historyScrollBarBg: ACCENT; // Scrollbar (history view) background (adjusted)
-historyScrollBarBgOver: ACCENT_LIGHT; // Scrollbar (history view) background (hover) (adjusted)
-historyScrollBg: BACKGROUND; // Scrollbar (history view) current position background (adjusted)
-historyScrollBgOver: BACKGROUND_LIGHT; // Scrollbar (history view) current position background (hover) (adjusted)
-
-msgInBg: BACKGROUND_LIGHT; // Incoming message background
-msgInBgSelected: ACCENT; // Selected incoming message background
-msgOutBg: BACKGROUND_EXTRA_LIGHT; // Outgoing message background
-msgOutBgSelected: ACCENT; // Selected outgoing message background
-msgSelectOverlay: 50; // Selected media (images...) overlay
-msgStickerOverlay: 50; // Selected sticker overlay
-msgInServiceFg: ACCENT; // Incoming reply quoted name
-msgInServiceFgSelected: ON_ACCENT; // Selected incoming reply quoted name
-msgOutServiceFg: ACCENT; // Outgoing reply quoted name
-msgOutServiceFgSelected: ON_ACCENT; // Selected outgoing reply quoted name
-msgInShadow: BACKGROUND_LIT_LIGHT; // Incoming message box shadow
-msgInShadowSelected: BACKGROUND_LIT_LIGHT; // Selected incoming message box shadow
-msgOutShadow: BACKGROUND_LIT_LIGHT; // Outgoing message box shadow
-msgOutShadowSelected: BACKGROUND_LIT_LIGHT; // Selected outgoing message box shadow
-msgInDateFg: ON_BACKGROUND_DARKER; // Incoming message date text
-msgInDateFgSelected: ON_ACCENT; // Incoming selected message date text
-msgOutDateFg: ON_BACKGROUND; // Outgoing message date text
-msgOutDateFgSelected: ON_ACCENT; // Outgoing selected message date text
-msgServiceFg: ON_ACCENT; // Service messages text color (join, left etc)
-msgServiceBg: ACCENT; // Service messages BACKGROUND color (join, left etc) (adjusted)
-msgServiceBgSelected: ACCENT; // Service message selected text background (like in a service message about group title being changed) (adjusted)
-msgInReplyBarColor: ACCENT; // Incoming msg: vertical bar next to reply
-msgInReplyBarSelColor: ACCENT; // Incoming msg: vertical bar next to reply (selected)
-msgOutReplyBarColor: ACCENT; // Outgoing msg: vertical bar next to reply
-msgOutReplyBarSelColor: ACCENT; // Outgoing msg: vertical bar next to reply (selected)
-msgImgReplyBarColor: msgServiceFg; // Sticker message reply outline
-msgInMonoFg: ACCENT_LIGHT; // Monowidth font / incoming msg
-msgOutMonoFg: ACCENT_LIGHT; // Monowidth font / outgoing msg
-msgInMonoFgSelected: msgInMonoFg; // Incoming message monospace text in a selected text or message
-msgOutMonoFgSelected: msgOutMonoFg; // Outgoing message monospace text in a selected text or message
-msgDateImgFg: msgServiceFg; // Media message time text (like time text in a sent photo)
-msgDateImgBg: ACCENT; // Media message time bubble background (like time bubble in a sent photo) or file with thumbnail download icon circle background
-msgDateImgBgOver: ON_ACCENT; // Media message download icon circle background with mouse over (like file with thumbnail download icon)
-msgDateImgBgSelected: ACCENT; // Selected media message time bubble background
-
-msgFileThumbLinkInFg: lightButtonFg; // Incoming media file message with thumbnail download / open with button text
-msgFileThumbLinkInFgSelected: lightButtonFgOver; // Incoming selected media file message with thumbnail download / open with button text
-msgFileThumbLinkOutFg: ACCENT; // Outgoing media file message with thumbnail download / open with button text
-msgFileThumbLinkOutFgSelected: ACCENT_LIGHT; // Outgoing selected media file message with thumbnail download / open with button text
-msgFileInBg: windowBgActive; // Incoming audio file download circle background
-msgFileInBgOver: ACCENT_LIGHT; // Incoming audio file download circle background with mouse over
-msgFileInBgSelected: ACCENT_LIGHT; // Incoming selected audio file download circle background
-msgFileOutBg: ACCENT; // Outgoing audio file download circle background
-msgFileOutBgOver: ACCENT_DARK; // Outgoing audio file download circle background with mouse over
-msgFileOutBgSelected: ACCENT; // Outgoing selected audio file download circle background
-
-msgFile1Bg: #72b1df; // Blue shared links / files without image square thumbnail
-msgFile1BgDark: #5c9ece; // Blue shared files without image download circle background
-msgFile1BgOver: #5294c4; // Blue shared files without image download circle background with mouse over
-msgFile1BgSelected: #50100d0; // Blue shared files without image download circle background if file is selected
-msgFile2Bg: #61b96e; // Green shared links / shared files without image square thumbnail
-msgFile2BgDark: #4da859; // Green shared files without image download circle background
-msgFile2BgOver: #44a050; // Green shared files without image download circle background with mouse over
-msgFile2BgSelected: #46a07e; // Green shared files without image download circle background if file is selected
-msgFile3Bg: #e47272; // Red shared links / shared files without image square thumbnail
-msgFile3BgDark: #cd5b5e; // Red shared files without image download circle background
-msgFile3BgOver: #c35154; // Red shared files without image download circle background with mouse over
-msgFile3BgSelected: #9f6a82; // Red shared files without image download circle background if file is selected
-msgFile4Bg: #efc274; // Yellow shared links / shared files without image square thumbnail
-msgFile4BgDark: #e6a561; // Yellow shared files without image download circle background
-msgFile4BgOver: #dc9c5a; // Yellow shared files without image download circle background with mouse over
-msgFile4BgSelected: #b19d84; // Yellow shared files without image download circle background if file is selected
-
-historyFileInIconFg: ON_ACCENT; // Incoming file without thumbnail (like audio file) download arrow icon
-historyFileInIconFgSelected: ON_ACCENT; // Incoming selected file without thumbnail (like audio file) download arrow icon
-historyFileInRadialFg: ON_ACCENT; // Incoming file without thumbnail (like audio file) radial download animation line
-historyFileInRadialFgSelected: ON_ACCENT; // Incoming selected file without thumbnail (like audio file) radial download animation line
-historyFileOutIconFg: ON_ACCENT; // Outgoing file without thumbnail (like audio file) download arrow icon
-historyFileOutIconFgSelected: ON_ACCENT; // Outgoing selected file without thumbnail (like audio file) download arrow icon
-historyFileOutRadialFg: ON_ACCENT; // Outgoing file without thumbnail (like audio file) radial download animation line
-historyFileOutRadialFgSelected: ON_ACCENT; // Outgoing selected file without thumbnail (like audio file) radial download animation line
-historyFileThumbIconFg: ON_ACCENT; // File with thumbnail (or photo / video) download arrow icon
-historyFileThumbIconFgSelected: ON_ACCENT; // Selected file with thumbnail (or photo / video) download arrow icon
-historyFileThumbRadialFg: ON_ACCENT; // File with thumbnail (or photo / video) radial download animation line
-historyFileThumbRadialFgSelected: ON_ACCENT; // Selected file with thumbnail (or photo / video) radial download animation line
-
-historyVideoMessageProgressFg: historyFileThumbIconFg; // Radial playback progress in round video messages
-msgWaveformInActive: windowBgActive; // Incoming Voice "heard" part
-msgWaveformInActiveSelected: ACCENT_LIGHT; // Selected incoming Voice "heard" part
-msgWaveformInInactive: #d4dee6; // Incoming Voice "unheard" part
-msgWaveformInInactiveSelected: ON_BACKGROUND; // Selected incoming Voice "unheard" part
-msgWaveformOutActive: windowBgActive; // Outgoing Voice "heard" part
-msgWaveformOutActiveSelected: ACCENT_LIGHT; // Selected outgoing Voice "heard" part
-msgWaveformOutInactive: ON_BACKGROUND; // Outgoing Voice "unheard" part
-msgWaveformOutInactiveSelected: ON_BACKGROUND; // Selected outgoing Voice "unheard" part
-
-msgBotKbOverBgAdd: ACCENT_DARK; // Inline button (hover)
-msgBotKbIconFg: ON_ACCENT; // Inline button icon color (URL Buttons, for example)
-msgBotKbRippleBg: #00000020; // Inline button (press & hold)
-
-mediaInFg: msgInDateFg; // Incoming media message status text (like in file that is being downloaded)
-mediaInFgSelected: msgInDateFgSelected; // Incoming selected media message status text (like in file that is being downloaded)
-mediaOutFg: msgOutDateFg; // Outgoing media message status text (like in file that is being downloaded)
-mediaOutFgSelected: msgOutDateFgSelected; // Outgoing selected media message status text (like in file that is being downloaded)
-
-youtubePlayIconBg: #ef5350ee; // Youtube play icon background (when a link to a youtube video with a webpage preview is sent)
-youtubePlayIconFg: #ffcdd2; // Youtube play icon arrow (when a link to a youtube video with a webpage preview is sent)
-videoPlayIconBg: #0000007f; // Other video play icon background (like when a link to a vimeo video with a webpage preview is sent)
-videoPlayIconFg: ON_BACKGROUND; // Other video play icon arrow (like when a link to a vimeo video with a webpage preview is sent)
-toastBg: #000000b2; // Toast notification background (like when you click on your t.me link when editing your username)
-toastFg: ON_BACKGROUND; // Toast notification text (like when you click on your t.me link when editing your username)
-
-reportSpamBg: emojiPanHeaderBg; // Report spam panel background (like a non contact user writes your for the first time)
-reportSpamFg: ON_BACKGROUND_DARKER; // Report spam panel text (when you send a report from that panel)
-
-historyToDownBg: BACKGROUND; // "Scroll to bottom" button background
-historyToDownBgOver: windowBgOver; // "Scroll to bottom" button background (hover)
-historyToDownBgRipple: windowBgRipple; // "Scroll to bottom" button background (press & hold)
-historyToDownFg: menuIconFg; // "Scroll to bottom" internal icon color
-historyToDownFgOver: menuIconFgOver; // "Scroll to bottom" internal icon color (hover)
-historyToDownShadow: #00000040; // "Scroll to bottom" button shadow
-
-historyComposeAreaBg: BACKGROUND; // Text input field background
-historyComposeAreaFg: ON_BACKGROUND; // Text color in "Type your message" field
-historyComposeAreaFgService: msgInDateFg; // Text color in "Type your message" field when replying to a media message
-historyComposeIconFg: menuIconFg; // Text input field: Paperclip, Smile, Bot command icons
-historyComposeIconFgOver: ACCENT_DARK; // Text input field: Paperclip, Smile, Bot command icons (hover)
-historySendIconFg: ACCENT_DARK; // Text input field: Send icon
-historySendIconFgOver: ACCENT; // Text input field: Send icon (hover)
-historyPinnedBg: historyComposeAreaBg; // Pinned message area background
-historyReplyBg: historyComposeAreaBg; // Reply / forward / edit message area background
-historyReplyIconFg: windowBgActive; // Reply / forward / edit message left icon
-historyReplyCancelFg: cancelIconFg; // Reply / forward / edit message cancel button
-historyReplyCancelFgOver: cancelIconFgOver; // Reply / forward / edit message cancel button with mouse over
-
-historyComposeButtonBg: historyComposeAreaBg; // Unblock / join channel / mute channel button background
-historyComposeButtonBgOver: windowBgOver; // Unblock / join channel / mute channel button background with mouse over
-historyComposeButtonBgRipple: windowBgRipple; // Unblock / join channel / mute channel button ripple effect
-
-// overview
-overviewCheckBg: #00000040; // Shared files / links checkbox background for not selected rows when some rows are selected
-overviewCheckFg: BACKGROUND; // Shared files / links checkbox icon for not selected rows when some rows are selected
-overviewCheckFgActive: BACKGROUND; // Shared files / links checkbox icon for selected rows
-overviewPhotoSelectOverlay: #40ace333; // Shared photos / videos / links fill for selected rows
-
-// profile
-profileStatusFgOver: COLOR_PINK; // Group members list in group profile user last seen text with mouse over
-profileVerifiedCheckBg: windowBgActive; // Profile verified check icon background
-profileVerifiedCheckFg: windowFgActive; // Profile verified check icon tick
-profileAdminStartFg: ACCENT_LIGHT; // Star color for group/channhel admins and moderators (look in members list)
-
-// settings
-notificationsBoxMonitorFg: ON_BACKGROUND_DARKER;// Custom notifications settings box monitor color
-notificationsBoxScreenBg: dialogsBgActive; // Custom notifications settings box monitor screen background
-
-notificationSampleUserpicFg: windowBgActive; // Custom notifications settings box small sample userpic placeholder
-notificationSampleCloseFg: #d7d7d7; // Custom notifications settings box small sample close button placeholder
-notificationSampleTextFg: #d7d7d7; // Custom notifications settings box small sample text placeholder
-notificationSampleNameFg: #939393; // Custom notifications settings box small sample name placeholder
-
-changePhoneSimcardFrom: notificationSampleTextFg; // Change phone number box left simcard icon
-changePhoneSimcardTo: notificationSampleNameFg; // Change phone number box right simcard and plane icons
-
-mainMenuBg: BACKGROUND; // Left menu background (lower part, not buttons)
-mainMenuCoverBg: dialogsBgActive; // Left menu background (upper part)
-mainMenuCoverFg: ON_BACKGROUND; // Main menu top cover text
-mainMenuCloudFg: activeButtonFg; // "Cloud" icon in main menu
-mainMenuCloudBg: activeButtonBgRipple; // "Cloud" icon background in main menu
-
-mediaPlayerBg: BACKGROUND; // Music player background
-mediaPlayerActiveFg: ACCENT; // Clickable Next song/Prev song button in music player + played part color
-mediaPlayerInactiveFg: #4f5151; // "Disabled" Next song/Prev song button in music player + not player part color
-mediaPlayerDisabledFg: ACCENT_DARK; // Music player audio loading progress (when you're playing an audio file and switch to the previous one which is not loaded yet)
-
-// mediaview
-mediaviewFileBg: BACKGROUND; // File rectangle background (when you view a png file in Media Viewer and go to a previous, not loaded yet, file)
-mediaviewFileNameFg: ON_BACKGROUND_DARKER; // File name in file rectangle
-mediaviewFileSizeFg: ON_BACKGROUND_DARKER; // File size text in file rectangle
-mediaviewFileRedCornerFg: #d55959; // Red file thumbnail placeholder corner in file rectangle (for a file without thumbnail, like .pdf)
-mediaviewFileYellowCornerFg: #e8a659; // Yellow file thumbnail placeholder corner in file rectangle (for a file without thumbnail, like .zip)
-mediaviewFileGreenCornerFg: #49a957; // Green file thumbnail placeholder corner in file rectangle (for a file without thumbnail, like .exe)
-mediaviewFileBlueCornerFg: #5100dcf; // Blue file thumbnail placeholder corner in file rectangle (for a file without thumbnail, like .dmg)
-mediaviewFileExtFg: activeButtonFg; // File extension text in file thumbnail placeholder in file rectangle
-
-mediaviewMenuBg: BACKGROUND; // Context menu in Media Viewer background
-mediaviewMenuBgOver: BACKGROUND_LIGHT; // Menu items background (hover)
-mediaviewMenuBgRipple: BACKGROUND_EXTRA_LIGHT; // Menu items background (press & hold)
-mediaviewMenuFg: ON_BACKGROUND; // Text color in media view menu
-
-mediaviewBg: #222222eb; // Media Viewer background
-mediaviewVideoBg: imageBg; // Media Viewer background when viewing a video in full screen
-mediaviewControlBg: #0000003c; // Controls background (like next photo / previous photo)
-mediaviewControlFg: ON_BACKGROUND; // Controls icon (like next photo / previous photo)
-mediaviewCaptionBg: #11111180; // Caption text background (when viewing photo with caption)
-mediaviewCaptionFg: mediaviewControlFg; // Caption text color
-mediaviewTextLinkFg: #91d9ff; // Links color in captions
-mediaviewSaveMsgBg: toastBg; // 'Something was saved to % folder' message background
-mediaviewSaveMsgFg: toastFg; // 'Something was saved to % folder' message text color
-
-mediaviewPlaybackActive: #c7c7c7; // Video playback progress already played part
-mediaviewPlaybackInactive: #252525; // Video playback progress upcoming (not played yet) part
-mediaviewPlaybackActiveOver: ON_BACKGROUND; // Video playback progress already played part with mouse over
-mediaviewPlaybackInactiveOver: #474747; // Video playback progress upcoming (not played yet) part with mouse over
-mediaviewPlaybackProgressFg: ON_BACKGROUND; // Video playback progress text
-mediaviewPlaybackIconFg: mediaviewPlaybackActive; // Video playback controls icon
-mediaviewPlaybackIconFgOver: mediaviewPlaybackActiveOver; // Video playback controls icon with mouse over
-mediaviewTransparentBg: ON_BACKGROUND; // Transparent filling part (when viewing a transparent .png file in Media Viewer)
-mediaviewTransparentFg: #cccccc; // Another transparent filling part
-
-// notification
-notificationBg: BACKGROUND; // Notification background
-
-// calls
-callBg: #26282cf2; // Phone call popup background
-callNameFg: #ffffff; // Phone call popup name text
-callFingerprintBg: #00000066; // Phone call popup emoji fingerprint background
-callStatusFg: #aaabac; // Phone call popup status text
-callIconFg: #ffffff; // Phone call popup answer, hangup and mute mic icon
-callAnswerBg: #64c15b; // Phone call popup answer button background
-callAnswerRipple: #52b149; // Phone call popup answer button ripple effect
-callAnswerBgOuter: #50eb4126; // Phone call popup answer button outer ripple effect
-callHangupBg: #d75a5a; // Phone call popup hangup button background
-callHangupRipple: #c04646; // Phone call popup hangup button ripple effect
-callCancelBg: #ffffff; // Phone call popup line busy cancel button background
-callCancelFg: #777777; // Phone call popup line busy cancel button icon
-callCancelRipple: #f1f1f1; // Phone call popup line busy cancel button ripple effect
-callMuteRipple: #ffffff12; // Phone call popup mute mic ripple effect
-
-callBarBg: dialogsBgActive; // Active phone call bar background
-callBarMuteRipple: dialogsRippleBgActive; // Active phone call bar mute and hangup button ripple effect
-callBarBgMuted: dialogsUnreadBgMuted; // Phone call bar with muted mic background
-callBarUnmuteRipple: shadowFg; // Phone call bar with muted mic mute and hangup button ripple effect
-callBarFg: dialogsNameFgActive; // Phone call bar text and icons
diff --git a/config/material-colors/templates/hyprlock.conf b/config/material-colors/templates/hyprlock.conf
deleted file mode 100644
index 12e97a0..0000000
--- a/config/material-colors/templates/hyprlock.conf
+++ /dev/null
@@ -1,145 +0,0 @@
-# Configuration
- # (general settings)
- general {
- grace = 2
- hide_cursor = true
- ignore_empty_input = true
- text_trim = true
- }
- # (background config)
- background {
- monitor =
- path = ~/.cache/current_wallpaper.png # only png supported for now
- color = rgba(, 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 = 225, 50
- outline_thickness = 0
- 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 = rgba(, 1.0)
- fade_on_empty = false
- fade_timeout = 1000
- font_family = Product Sans
- placeholder_text =
- hide_input = false
- rounding = 4
- check_color = rgba(0,0,0,0)
- fail_color = rgba(0,0,0,0)
- 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 = rgba(, 1.0)
- 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 = rgba(, 1.0)
- 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 = rgba(, 1.0)
- 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 = rgba(, 1.0)
- 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 = rgba(, 1.0)
- 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 = rgba(, 1.0)
- 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/material-colors/templates/material-discord.scss b/config/material-colors/templates/material-discord.scss
deleted file mode 100644
index f302ab7..0000000
--- a/config/material-colors/templates/material-discord.scss
+++ /dev/null
@@ -1,55 +0,0 @@
-@use "sass:color";
-@use "sass:math";
-
-@use "colors.scss" as *;
-@import url(https://capnkitten.github.io/BetterDiscord/Themes/Material-Discord/css/source.css);
-
-$color-scheme: ;
-
-$hue: math.round(color.channel($primary, "hue", $space: hsl));
-$saturation: math.round(color.channel($primary, "saturation", $space: hsl));
-$lightness: math.round(color.channel($primary, "lightness", $space: hsl));
-
-* {
- transition: color 0.2s ease-in-out;
- transition-property: background-color, color;
-}
-
-.theme-#{$color-scheme} {
- --main-color: #{$background};
- --main-alt: #{$surfaceContainerLow};
-
- --message-color: #{$surfaceContainerLowest};
- --message-color-hover: #{$surfaceContainer};
- --message-color-alt: #{$surfaceContainer};
- --attachment-color: #{$background};
-
- --accent-hue: #{$hue};
- --accent-saturation: #{$saturation};
- --accent-lightness: #{$lightness};
- --accent-text-color: #{$onPrimary};
-
- --main-textarea-color: #{$surfaceContainerHigh};
- --main-textarea-color-alt: #{$surfaceContainerHighest};
- --status-picker-color: #{$surfaceContainerHigh};
-
- --tooltip-color: #{$surfaceContainer};
- --popout-color: #{$surfaceContainer};
- --popout-color-alt: #{$surfaceContainerLowest};
- --popout-header-border: #{$onSurface};
- --separator-color: #{$outlineVariant};
- --sidebar-panel-color: #{$surfaceContainerLowest};
- --server-color: #{$surfaceContainerLow};
-}
-
-[class*="botTag_"] svg path {
- fill: #{$onPrimary};
-}
-
-[class*="repliedTextContent_"] {
- padding: 0px 5px 0px 5px !important;
-}
-
-[class*="userPopoutOuter_"] {
- border-radius: 16px !important;
-}
diff --git a/config/matugen/templates/hyprland-colors.conf b/config/matugen/templates/hyprland-colors.conf
new file mode 100644
index 0000000..a375c24
--- /dev/null
+++ b/config/matugen/templates/hyprland-colors.conf
@@ -0,0 +1,11 @@
+$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
new file mode 100644
index 0000000..db192d7
--- /dev/null
+++ b/config/matugen/templates/modus.css
@@ -0,0 +1,44 @@
+: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
new file mode 100644
index 0000000..aba4239
--- /dev/null
+++ b/config/matugen/templates/modus.scss
@@ -0,0 +1,29 @@
+@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
new file mode 100755
index 0000000..8204108
--- /dev/null
+++ b/config/scripts/capture.sh
@@ -0,0 +1,166 @@
+#!/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/dark-theme.sh b/config/scripts/dark-theme.sh
deleted file mode 100755
index a43afcf..0000000
--- a/config/scripts/dark-theme.sh
+++ /dev/null
@@ -1,63 +0,0 @@
-#!/bin/bash
-
-# Function to get the relative path based on the current script's location
-get_relative_path() {
- local path=$1
- local level=$2
- local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-
- for ((i = 1; i <= level; i++)); do
- script_dir="$(dirname "$script_dir")"
- done
-
- echo "$script_dir/$path"
-}
-
-# Assign relative paths to variables
-GENERATOR=$(get_relative_path "../material-colors/generate.py")
-SETTINGS_FILE=$(get_relative_path "../assets/settings.json")
-
-# Function to read a value from the JSON file
-get_json_value() {
- local key=$1
- jq -r --arg key "$key" '.[$key]' $SETTINGS_FILE
-}
-
-# Function to update a value in the JSON file
-update_json_value() {
- local key=$1
- local value=$2
- jq --arg key "$key" --arg value "$value" \
- '.[$key] = $value' $SETTINGS_FILE >"$SETTINGS_FILE.tmp" && mv "$SETTINGS_FILE.tmp" $SETTINGS_FILE
-}
-
-toggle() {
- local color_scheme=$(get_json_value "color-scheme")
- local generation_scheme=$(get_json_value "generation-scheme")
-
- if [ "$color_scheme" == "dark" ]; then
- python -O $GENERATOR -R --color-scheme light --scheme $generation_scheme
- update_json_value "color-scheme" "light"
- elif [ "$color_scheme" == "light" ]; then
- python -O $GENERATOR -R --color-scheme dark --scheme $generation_scheme
- update_json_value "color-scheme" "dark"
- else
- echo "Unknown color scheme: $color_scheme"
- fi
-}
-
-set() {
- local new_scheme=$1
- local generation_scheme=$(get_json_value "generation-scheme")
- python -O $GENERATOR -R --color-scheme $new_scheme --scheme $generation_scheme
- update_json_value "color-scheme" $new_scheme
-}
-
-# Process arguments
-if [[ "$1" == "--toggle" ]]; then
- toggle
-elif [[ "$1" == "--set" ]]; then
- set $2
-else
- echo "Usage: $0 --toggle | --set "
-fi
diff --git a/config/scripts/gamemode.sh b/config/scripts/gamemode.sh
new file mode 100755
index 0000000..3652ccd
--- /dev/null
+++ b/config/scripts/gamemode.sh
@@ -0,0 +1,40 @@
+#!/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
new file mode 100755
index 0000000..703c7df
--- /dev/null
+++ b/config/scripts/hyprpicker.sh
@@ -0,0 +1,75 @@
+#!/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
index e8044eb..a2677ff 100755
--- a/config/scripts/systemupdates.sh
+++ b/config/scripts/systemupdates.sh
@@ -1,160 +1,178 @@
#!/bin/bash
-# Word of caution: changing this can break update module, be vigilant
+# 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="" # Initialize tooltip variable
+ tooltip=""
- # Get the number of official updates using 'checkupdates'
+ # 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 using 'yay'
+ # 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 for official updates
- tooltip=" Official $official_updates\n AUR $aur_updates"
+ # Build the tooltip
+ tooltip=" Official: $official_updates\n AUR: $aur_updates\n Flatpak: $flatpak_updates"
- # Check if flatpak is installed
- if command -v flatpak &>/dev/null; then
- # Get the number of Flatpak updates if flatpak is installed
- flatpak_updates=$(flatpak remote-ls --updates | wc -l)
-
- # Append Flatpak updates to the tooltip
- tooltip="$tooltip\n Flatpak $flatpak_updates"
-
- # Recalculate total updates
- total_updates=$((official_updates + aur_updates + flatpak_updates))
- fi
-
- # Output the result as a JSON object
+ # 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=""
- # Check if flatpak is installed and get Flatpak updates if it is
+ # 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)
- # Always show Flatpak updates in the tooltip, even if there are none
- tooltip=" Official $official_updates\n Flatpak $flatpak_updates"
- else
- # If flatpak is not installed, only show official updates
- tooltip=" Official $official_updates"
fi
- # Get the number of official updates using 'apt-get'
- official_updates=$(apt-get -s -o Debug::NoLocking=true upgrade | grep -c ^Inst)
-
# Calculate total updates
total_updates=$((official_updates + flatpak_updates))
- # Output the result as a JSON object
+ # 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 using 'dnf'
- official_updates=$(dnf check-update -q | grep -v '^Loaded plugins' | grep -v '^No match for' | wc -l)
+ # 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))
- # Check if flatpak is installed and get Flatpak updates if it is
- if command -v flatpak &>/dev/null; then
- flatpak_updates=$(flatpak remote-ls --updates | wc -l)
- # Always show Flatpak updates in the tooltip, even if there are none
- tooltip=" Official $official_updates\n Flatpak $flatpak_updates"
- else
- # If flatpak is not installed, only show official updates
- tooltip=" Official $official_updates"
- fi
+ # Build the tooltip
+ tooltip=" Official: $official_updates\n Flatpak: $flatpak_updates"
- # Output the result as a JSON object
+ # 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=""
- # Check if flatpak is installed and get Flatpak updates if it is
+ # 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
- # Get the number of official updates using 'dnf'
- official_updates=$(zypper lu | wc -l)
-
# Calculate total updates
total_updates=$((official_updates + flatpak_updates))
- # Check if flatpak is installed and get Flatpak updates if it is
- if command -v flatpak &>/dev/null; then
- flatpak_updates=$(flatpak remote-ls --updates | wc -l)
- # Always show Flatpak updates in the tooltip, even if there are none
- tooltip=" Official $official_updates\n Flatpak $flatpak_updates"
- else
- # If flatpak is not installed, only show official updates
- tooltip=" Official $official_updates"
- fi
+ # Build the tooltip
+ tooltip=" Official: $official_updates\n Flatpak: $flatpak_updates"
- # Output the result as a JSON object
+ # Output JSON
echo "{\"total\":\"$total_updates\", \"tooltip\":\"$tooltip\"}"
}
+# Function to update Arch-based distros
update_arch() {
- command="
- fastfetch
- yay -Syu
- read -n 1 -p 'Press any key to continue...'
- "
- kitty --title systemupdate sh -c "${command}"
+ 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 # If second argument is null (not provided)
+ if [ -z "$2" ]; then
check_arch_updates
else
update_arch
fi
;;
-ubuntu)
- if [ -z "$2" ]; then # If second argument is null (not provided)
+ if [ -z "$2" ]; then
check_ubuntu_updates
else
update_ubuntu
fi
;;
-fedora)
- if [ -z "$2" ]; then # If second argument is null (not provided)
+ if [ -z "$2" ]; then
check_fedora_updates
else
update_fedora
fi
;;
-suse)
- if [ -z "$2" ]; then # If second argument is null (not provided)
+ if [ -z "$2" ]; then
check_opensuse_updates
else
- update_arch
+ update_opensuse
fi
;;
*)
diff --git a/config/scripts/wallpaper.py b/config/scripts/wallpaper.py
deleted file mode 100644
index 6651fb6..0000000
--- a/config/scripts/wallpaper.py
+++ /dev/null
@@ -1,271 +0,0 @@
-import os
-import sys
-import importlib.util
-import argparse
-import fcntl
-import subprocess
-import asyncio
-import random as _random
-import colorsys
-import json
-import inspect
-
-
-def get_relative_path(path: str, level: int = 1) -> str:
- prev_globals = inspect.stack()[level][0].f_globals
- file_var = (
- os.path.dirname(os.path.abspath(prev_globals["__file__"]))
- if "__file__" in prev_globals
- else os.getcwd()
- )
- return os.path.join(file_var, path)
-
-
-lock_file_path = "/tmp/wallpaper.lock"
-settings_file_path = get_relative_path("../assets/settings.json")
-
-
-def acquire_lock():
- global lock_file
- lock_file = open(lock_file_path, "w")
- try:
- fcntl.flock(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
- lock_file.write(str(os.getpid()))
- lock_file.flush()
- except IOError:
- print("Another instance of the script is already running.")
- sys.exit(1)
-
-
-def release_lock():
- fcntl.flock(lock_file, fcntl.LOCK_UN)
- lock_file.close()
- os.remove(lock_file_path)
-
-
-def hue_to_numeric_hex(hue):
- hue = hue / 360.0
- rgb = colorsys.hls_to_rgb(hue, 0.5, 1.0)
- hex_color_str = "#{:02x}{:02x}{:02x}".format(
- int(rgb[0] * 255), int(rgb[1] * 255), int(rgb[2] * 255)
- )
- numeric_hex_color = int(hex_color_str.lstrip("#"), 16)
- return numeric_hex_color
-
-
-def load_settings():
- default_settings = {
- "color-scheme": "dark",
- "custom-color": "none",
- "generation-scheme": "tonalSpot",
- }
-
- if not os.path.exists(settings_file_path):
- os.makedirs(os.path.dirname(settings_file_path), exist_ok=True)
- with open(settings_file_path, "w") as file:
- json.dump(default_settings, file, indent=4)
-
- with open(settings_file_path, "r") as file:
- return json.load(file)
-
-
-parser = argparse.ArgumentParser()
-group = parser.add_mutually_exclusive_group(required=True)
-group.add_argument("-I", "--image", type=str, help="Image")
-group.add_argument(
- "-P",
- "--prev",
- help="Use last used wallpaper for color generation",
- action="store_true",
-)
-group.add_argument(
- "-R", "--random", help="Random image from folder", action="store_true"
-)
-parser.add_argument("-n", "--notify", help="Send notifications", action="store_true")
-parser.add_argument(
- "--status", type=str, help="Status file", default="/tmp/wallpaper.status"
-)
-
-args = parser.parse_args()
-
-random = args.random
-prev = args.prev
-image = args.image
-notify = args.notify
-status = args.status
-
-HOME = os.path.expanduser("~")
-
-module_path = get_relative_path("../material-colors")
-sys.path.append(module_path)
-
-module_name = "generate"
-file_path = f"{module_path}/{module_name}.py"
-
-spec = importlib.util.spec_from_file_location(module_name, file_path)
-GENERATOR = importlib.util.module_from_spec(spec) # type: ignore
-spec.loader.exec_module(GENERATOR) # type: ignore
-
-cache_file = f"{HOME}/.cache/current_wallpaper"
-square = f"{HOME}/.cache/square_wallpaper.png"
-png = f"{HOME}/.cache/current_wallpaper.png"
-
-
-def current_state(state_str: str):
- with open(status, "w") as f:
- f.write(state_str)
-
-
-def send_notify(label: str, desc: str):
- if not notify:
- return
- subprocess.run(["notify-send", label, desc])
-
-
-def state(name: str | None, label: str | None, desc: str | None):
- if name is not None:
- current_state(name)
- if label is not None:
- send_notify(label, desc or "")
-
-
-def join(*args):
- return os.path.join(*args)
-
-
-async def main():
- state("init", None, None)
-
- settings = load_settings()
- color_scheme = settings["color-scheme"]
- custom_color = settings["custom-color"]
- generation_scheme = settings["generation-scheme"]
- swww_animation = "wipe"
- wallpaper_engine = "swww"
-
- new_wallpaper = f"{HOME}/Pictures/wallpapers/example-1.jpg"
- # new_wallpaper = get_relative_path("../assets/wallpapers/1.png")
-
- if random:
- files = [
- f
- for f in os.listdir(f"{HOME}/Pictures/wallpapers/")
- if f.endswith((".png", ".jpg", ".jpeg"))
- ]
- if files:
- new_wallpaper = join(f"{HOME}/Pictures/wallpapers", _random.choice(files))
- elif prev:
- try:
- with open(cache_file) as f:
- new_wallpaper = f.read().strip()
- except FileNotFoundError:
- pass
- elif image is not None:
- new_wallpaper = os.path.abspath(image)
-
- print(f":: Wallpaper {new_wallpaper}")
-
- with open(cache_file, "w") as f:
- f.write(new_wallpaper)
-
- with_image = f"with image {new_wallpaper}"
-
- # -----------------------------------------------------
- # Set the new wallpaper
- # -----------------------------------------------------
-
- transition_type = swww_animation
-
- state("changing", "Changing wallpaper...", with_image)
- print(":: Changing wallpaper...")
-
- if wallpaper_engine == "swww":
- print(":: Using swww")
-
- cursor_pos = subprocess.getoutput("hyprctl cursorpos").replace(", ", ",")
-
- subprocess.run(
- [
- "swww",
- "img",
- new_wallpaper,
- "--transition-bezier",
- ".43,1.19,1,.4",
- "--transition-fps",
- "60",
- "--transition-type",
- transition_type,
- "--transition-duration",
- "0.7",
- "--transition-pos",
- cursor_pos,
- ]
- )
-
- else:
- print(":: Wallpaper Engine disabled")
-
- # -----------------------------------------------------
- # Generate colors
- # -----------------------------------------------------
- state("colors", "Generating colors...", with_image)
- print(":: Generate colors")
- _starts_with = lambda x: str(x).startswith("-")
-
- GENERATOR.main(
- color_scheme,
- new_wallpaper,
- (
- hue_to_numeric_hex(int(custom_color.lstrip("-")))
- if _starts_with(custom_color)
- else (int(custom_color.lstrip("#"), 16) if custom_color != "none" else None)
- ),
- scheme_name=generation_scheme,
- )
-
- # Generate square and png versions
- await asyncio.gather(square_image(new_wallpaper), png_image(new_wallpaper))
-
- state("finish", "Wallpaper procedure complete!", with_image)
-
-
-async def png_image(wallpaper: str):
- if wallpaper.lower().endswith((".jpg", ".jpeg")):
- state(None, "Converting jpg to png...", f"with image {wallpaper}")
- print(":: Converting jpg to png...")
- cmd = f"magick {wallpaper}[0] {png}"
- else:
- cmd = f"cp -f {wallpaper} {png}"
-
- process = await asyncio.create_subprocess_shell(
- cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
- )
- stdout, stderr = await process.communicate()
-
- if process.returncode != 0:
- print(f":: Error while processing image: {stderr.decode()}")
- else:
- print(":: JPG successfully converted to PNG!")
-
-
-async def square_image(wallpaper):
- state(None, "Creating square version...", f"with image {wallpaper}")
- print(":: Creating square version")
- cmd = f"magick {wallpaper}[0] -gravity Center -extent 1:1 {square}"
- process = await asyncio.create_subprocess_shell(
- cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
- )
- stdout, stderr = await process.communicate()
-
- if process.returncode != 0:
- print(f":: Error while processing image: {stderr.decode()}")
- else:
- print(":: Square image created!")
-
-
-if __name__ == "__main__":
- acquire_lock()
- try:
- asyncio.run(main())
- finally:
- release_lock()
diff --git a/config/sddm/Main.qml b/config/sddm/Main.qml
deleted file mode 100644
index cb7b43b..0000000
--- a/config/sddm/Main.qml
+++ /dev/null
@@ -1,38 +0,0 @@
-import QtQuick 2.15
-import QtQuick.Controls 2.15
-import QtQuick.Window 2.15
-
-import "./components"
-
-Rectangle {
- id: root
-
- height: Screen.height
- width: Screen.width
-
- Image {
- anchors { fill: parent }
-
- source: config.BgSource
- fillMode: Image.PreserveAspectCrop
- clip: true
- }
-
- Item {
- anchors {
- fill: parent
- margins: config.Padding
- }
-
- DateTimePanel {
- anchors {
- top: parent.top
- right: parent.right
- }
- }
-
- LoginPanel {
- anchors { fill: parent }
- }
- }
-}
diff --git a/config/sddm/scripts/disable.sh b/config/sddm/scripts/disable.sh
deleted file mode 100755
index b95c874..0000000
--- a/config/sddm/scripts/disable.sh
+++ /dev/null
@@ -1,12 +0,0 @@
-#!/bin/bash
-figlet "Disable SDDM"
-if [ -f /etc/systemd/system/display-manager.service ]; then
- if gum confirm "Do you want to disable the current display manager?"; then
- sudo rm /etc/systemd/system/display-manager.service
- echo ":: Current display manager removed."
- echo ":: Please reboot your system."
- fi
-else
- echo ":: No Display Manager enabled."
-fi
-sleep 3
diff --git a/config/sddm/scripts/enable.sh b/config/sddm/scripts/enable.sh
deleted file mode 100755
index 6383ed6..0000000
--- a/config/sddm/scripts/enable.sh
+++ /dev/null
@@ -1,12 +0,0 @@
-#!/bin/bash
-figlet "Enable SDDM"
-if [ -f /etc/systemd/system/display-manager.service ]; then
- echo ":: Display Manager is already enabled."
-else
- if gum confirm "Do you want to enable SDDM as your display manager?"; then
- sudo systemctl enable sddm.service
- echo ":: Display manager SDDM has been enabled."
- echo ":: Please reboot your system!"
- fi
-fi
-sleep 3
diff --git a/config/sddm/scripts/wallpaper.sh b/config/sddm/scripts/wallpaper.sh
deleted file mode 100755
index f8a0b36..0000000
--- a/config/sddm/scripts/wallpaper.sh
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/bin/bash
-cache_file="$HOME/.cache/current_wallpaper"
-if [ ! -d /etc/sddm.conf.d/ ]; then
- sudo mkdir /etc/sddm.conf.d
- echo "Folder /etc/sddm.conf.d created."
-fi
-
-cp $HOME/dotfiles/sddm/sddm.conf /etc/sddm.conf.d/
-cp $HOME/dotfiles/sddm/sddm.conf /etc/
-if [ "$1" != "init" ]; then
- echo "File /etc/sddm.conf.d/sddm.conf updated."
-fi
-
-current_wallpaper=$(cat "$cache_file")
-extension="${current_wallpaper##*.}"
-
-cp $current_wallpaper /usr/share/sddm/themes/corners/backgrounds/current_wallpaper.$extension
-
-if [ "$1" != "init" ]; then
- echo "Current wallpaper copied into /usr/share/sddm/themes/corners/backgrounds/"
-fi
-
-new_wall=$(echo $current_wallpaper | sed "s|$HOME/wallpaper/||g")
-cp $HOME/.cache/material/colors-sddm-style.conf /usr/share/sddm/themes/corners/theme.conf
-sed -i 's/CURRENTWALLPAPER/'"current_wallpaper.$extension"'/' /usr/share/sddm/themes/corners/theme.conf
-
-if [ "$1" != "init" ]; then
- echo "File theme.conf updated in /usr/share/sddm/themes/corners/"
-
- echo "DONE! Please logout to test sddm."
-fi
diff --git a/config/sddm/sddm.conf b/config/sddm/sddm.conf
deleted file mode 100644
index e459a7a..0000000
--- a/config/sddm/sddm.conf
+++ /dev/null
@@ -1,138 +0,0 @@
-[Autologin]
-# Whether sddm should automatically log back into sessions when they exit
-Relogin=false
-
-# Name of session file for autologin session (if empty try last logged in)
-Session=
-
-# Username for autologin session
-User=
-
-[General]
-# Which display server should be used.
-# Valid values are: x11, x11-user, wayland. Wayland support is experimental
-DisplayServer=x11
-
-# Comma-separated list of environment variables to be set
-GreeterEnvironment=
-
-# Halt command
-HaltCommand=/usr/bin/systemctl poweroff
-
-# Input method module
-InputMethod=
-
-# Comma-separated list of Linux namespaces for user session to enter
-Namespaces=
-
-# Initial NumLock state. Can be on, off or none.
-# If property is set to none, numlock won't be changed
-# NOTE: Currently ignored if autologin is enabled.
-Numlock=none
-
-# Reboot command
-RebootCommand=/usr/bin/systemctl reboot
-
-
-[Theme]
-# Current theme name
-Current=corners
-
-# Cursor size used in the greeter
-CursorSize=6
-
-# Cursor theme used in the greeter
-CursorTheme=Bibata-Original-Classic
-
-# Number of users to use as threshold
-# above which avatars are disabled
-# unless explicitly enabled with EnableAvatars
-DisableAvatarsThreshold=7
-
-# Enable display of custom user avatars
-EnableAvatars=false
-
-# Global directory for user avatars
-# The files should be named .face.icon
-FacesDir=/usr/share/sddm/faces
-
-# Font used in the greeter
-Font=
-
-# Theme directory path
-# ThemeDir=/usr/share/sddm/themes
-ThemeDir=/usr/share/sddm/themes
-
-[Users]
-# Default $PATH for logged in users
-DefaultPath=/usr/local/sbin:/usr/local/bin:/usr/bin
-
-# Comma-separated list of shells.
-# Users with these shells as their default won't be listed
-HideShells=
-
-# Comma-separated list of users that should not be listed
-HideUsers=
-
-# Maximum user id for displayed users
-MaximumUid=60513
-
-# Minimum user id for displayed users
-MinimumUid=1000
-
-# Remember the session of the last successfully logged in user
-RememberLastSession=true
-
-# Remember the last successfully logged in user
-RememberLastUser=true
-
-# When logging in as the same user twice, restore the original session, rather than create a new one
-ReuseSession=true
-
-
-[Wayland]
-# Path of the Wayland compositor to execute when starting the greeter
-CompositorCommand=weston --shell=fullscreen-shell.so
-
-# Enable Qt's automatic high-DPI scaling
-EnableHiDPI=true
-
-# Path to a script to execute when starting the desktop session
-SessionCommand=/usr/share/sddm/scripts/wayland-session
-
-# Comma-separated list of directories containing available Wayland sessions
-SessionDir=/usr/local/share/wayland-sessions,/usr/share/wayland-sessions
-
-# Path to the user session log file
-SessionLogFile=.local/share/sddm/wayland-session.log
-
-
-[X11]
-# Path to a script to execute when starting the display server
-DisplayCommand=/usr/share/sddm/scripts/Xsetup
-
-# Path to a script to execute when stopping the display server
-DisplayStopCommand=/usr/share/sddm/scripts/Xstop
-
-# Enable Qt's automatic high-DPI scaling
-EnableHiDPI=true
-
-# Arguments passed to the X server invocation
-ServerArguments=-nolisten tcp
-
-# Path to X server binary
-ServerPath=/usr/bin/X
-
-# Path to a script to execute when starting the desktop session
-SessionCommand=/usr/share/sddm/scripts/Xsession
-
-# Comma-separated list of directories containing available X sessions
-SessionDir=/usr/local/share/xsessions,/usr/share/xsessions
-
-# Path to the user session log file
-SessionLogFile=.local/share/sddm/xorg-session.log
-
-# Path to Xephyr binary
-XephyrPath=/usr/bin/Xephyr
-
-
diff --git a/main.py b/main.py
deleted file mode 100644
index ad71f77..0000000
--- a/main.py
+++ /dev/null
@@ -1,78 +0,0 @@
-import os
-
-import setproctitle
-from fabric import Application
-from fabric.utils import get_relative_path, monitor_file
-from loguru import logger
-
-from modules.bar.bar import Bar, ScreenCorners
-from modules.launcher.launcher import Launcher
-from modules.dock import Dock
-from modules.notification_popup import NotificationPopup
-
-from modules.osd import OSD
-
-from services import sc
-from config.config import open_config
-
-for log in [
- "fabric.hyprland.widgets",
- "fabric.audio.service",
- "fabric.bluetooth.service",
-]:
- logger.disable(log)
-
-config_path = os.path.expanduser("~/Modus/config/assets/config.json")
-
-
-def update_main_css():
- colors_css_path = f"/home/{os.getlogin()}/.cache/material/colors.css"
- main_css_path = get_relative_path("styles/main.css")
-
- if os.path.exists(colors_css_path):
- with open(main_css_path, "r+") as css_file:
- content = css_file.read()
- updated_content = content.replace("@COLORS_CSS_PATH", colors_css_path)
- css_file.seek(0)
- css_file.write(updated_content)
- css_file.truncate()
- else:
- logger.warning(f"[Main] colors.css not found at {colors_css_path}")
-
-
-def apply_style(app: Application):
- logger.info("[Main] Applying CSS")
- app.set_stylesheet_from_file(get_relative_path("styles/main.css"))
-
-
-if __name__ == "__main__":
- update_main_css()
- if not os.path.isfile(config_path):
- open_config()
- sc = sc
- bar = Bar()
- dock = Dock()
- corners = ScreenCorners()
- osd = OSD()
- notif = NotificationPopup()
- launcher = Launcher()
-
- app = Application(
- "modus",
- bar,
- launcher,
- osd,
- dock,
- )
- setproctitle.setproctitle("modus")
-
- # Monitor CSS files for changes
- css_file = monitor_file(get_relative_path("styles"))
- _ = css_file.connect("changed", lambda *_: apply_style(app))
-
- color_css_file = monitor_file(f"/home/{os.getlogin()}/.cache/material/colors.css")
- _ = color_css_file.connect("changed", lambda *_: apply_style(app))
-
- # Apply the styles and run the application
- apply_style(app)
- app.run()
diff --git a/modules/bar/bar.py b/modules/bar/bar.py
index 278e228..61a2653 100644
--- a/modules/bar/bar.py
+++ b/modules/bar/bar.py
@@ -1,186 +1,289 @@
-from typing import Literal
-from fabric.hyprland.widgets import Language
+from fabric.hyprland.widgets import Language, get_hyprland_connection
+from fabric.hyprland.service import HyprlandEvent
from fabric.system_tray.widgets import SystemTray
-from fabric.utils import (
- FormattedString,
- bulk_replace,
- exec_shell_command_async,
- get_relative_path,
- bulk_connect,
-)
from fabric.widgets.shapes import Corner
from fabric.widgets.box import Box
from fabric.widgets.button import Button
from fabric.widgets.centerbox import CenterBox
from fabric.widgets.datetime import DateTime
from fabric.widgets.label import Label
-from fabric.widgets.revealer import Revealer
from fabric.widgets.wayland import WaylandWindow as Window
-from gi.repository import GLib
-from modules.bar.components import (
- Battery,
- Metrics,
- workspace,
- SystemIndicators,
- UpdatesWidget,
-)
-from services import sc
+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
-
-
-class StatusBarCorner(Box):
- def __init__(self, corner: Literal["top-right", "top-left"]):
- super().__init__(
- style="margin-bottom: 15px;",
- name="bar-corner",
- children=Corner(
- orientation=corner,
- size=15,
- ),
- )
-
-
-class Tray(Box):
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
- self.tray = Box(
- name="left-tray",
- children=[SystemTray(name="tray", icon_size=16, spacing=4)],
- )
- self.profile = Button(
- child=Label(name="tray-revealer", markup=icons.chevron_left)
- )
- self.revealer = Revealer(transition_type="slide-left", transition_duration=1000)
- self.revealer.add(self.tray)
-
- bulk_connect(
- self.profile,
- {
- "enter-notify-event": self._on_enter,
- "leave-notify-event": self._on_leave,
- "button-press-event": self.toggle_revealer,
- },
- )
-
- self.add(self.profile)
- self.add(self.revealer)
-
- def _on_enter(self, *args):
- self.profile.set_cursor("pointer")
-
- def _on_leave(self, *args):
- self.profile.set_cursor("default")
-
- def toggle_revealer(self, *args):
- new_state = not self.revealer.get_reveal_child()
- self.revealer.set_reveal_child(new_state)
- new_icon = icons.chevron_right if new_state else icons.chevron_left
- self.profile.get_child().set_markup(new_icon)
+import os
+import json
class Bar(Window):
- def __init__(self):
- self.workspaces = Button(child=workspace, name="workspaces")
- self.language = Language(
- formatter=FormattedString(
- "{replace_lang(language)}",
- replace_lang=lambda lang: bulk_replace(
- lang,
- (r".*Eng.*", r".*Nep.*"),
- ("en", "np"),
- regex=True,
- ),
- ),
- )
- self.bar_content = CenterBox(name="center-bar")
- self.recording_indicator = Button(
- name="recording-indicator",
- child=Label(name="recorder", markup=icons.record),
- visible=False,
- on_clicked=lambda *_: sc.screencast_stop(),
+ 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,
)
- sc.connect("recording", self.on_recording_status_change)
+ self.launcher = kwargs.get("launcher", None)
+ self.component_visibility = data.BAR_COMPONENTS_VISIBILITY
- self.date_time = DateTime(formatters=["%-I:%M %a %d %b"], name="datetime")
- self.battery = Battery()
- self.launcher = Button(
- name="logo",
- child=Label(name="logo-name", label=" "),
- on_clicked=lambda *_: GLib.spawn_command_line_async(
- "fabric-cli exec modus 'launcher.open(\"launcher\")'"
- ),
+ # Determine if we should use vertical layout for components
+ is_vertical_layout = data.VERTICAL
+
+ self.workspaces = Button(
+ child=workspace,
+ name="workspaces",
)
- self.button_config = Button(
- name="button-bar",
- on_clicked=lambda *_: exec_shell_command_async(
- f"python {get_relative_path('../../config/config.py')}"
- ),
- child=Label(name="button-bar-label", markup=icons.config),
- )
- self.stats = Metrics()
- self.updates = UpdatesWidget()
- self.tray = Tray()
- self.indicators = SystemIndicators()
- self.applets = Box(
- name="system-indicators",
+
+ self.connection = get_hyprland_connection()
+ self.tray = SystemTray(
+ name="tray",
spacing=4,
- orientation="h",
+ 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],
)
- self.bar_content.end_children = [
- StatusBarCorner("top-right"),
- Box(
- name="bar",
- spacing=4,
- children=[
- self.recording_indicator,
- self.tray,
- self.battery,
- self.applets,
- self.date_time,
- self.button_config,
- ],
- style_classes="end-container",
- ),
+ # 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.bar_content.start_children = [
- Box(
- name="bar",
- spacing=4,
- children=[self.launcher, self.workspaces, self.stats, self.updates],
- style_classes="start-container",
- ),
- StatusBarCorner("top-left"),
- ]
+ self.h_end_children = [self.tray]
- super().__init__(
- layer="top",
- anchor="left top right",
- exclusivity="auto",
- visible=True,
- child=self.bar_content,
+ 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.hidden = False
- def on_recording_status_change(self, _, status):
- self.recording_indicator.set_visible(status)
+ self.children = self.bar
- def toggle_hidden(self):
- self.hidden = not self.hidden
- if self.hidden:
- self.bar_content.add_style_class("hidden")
- self.bar_content.remove_style_class("visible")
- self.set_property("visible", False)
+ def apply_component_visibility(self):
+ """Apply saved visibility settings to all components"""
+ components = {
+ "metrics": self.metrics,
+ "indicators": self.applets,
+ "updates": self.updates,
+ "workspaces": self.workspaces,
+ "button_app": self.button_app,
+ # "button_tools": self.button_tools,
+ "language": self.language,
+ "date_time": self.date_time,
+ "tray": self.tray,
+ }
+
+ for component_name, widget in components.items():
+ if component_name in self.component_visibility:
+ widget.set_visible(self.component_visibility[component_name])
+
+ def toggle_component_visibility(self, component_name):
+ """Toggle visibility for a specific component"""
+ components = {
+ "metrics": self.metrics,
+ "indicators": self.applets,
+ "updates": self.updates,
+ "workspaces": self.workspaces,
+ "button_app": self.button_app,
+ # "button_tools": self.button_tools,
+ "language": self.language,
+ "date_time": self.date_time,
+ "tray": self.tray,
+ }
+
+ if component_name in components and component_name in self.component_visibility:
+ # Toggle the visibility state
+ self.component_visibility[component_name] = not self.component_visibility[
+ component_name
+ ]
+ # Apply the new state
+ components[component_name].set_visible(
+ self.component_visibility[component_name]
+ )
+
+ # Update the configuration
+ config_file = os.path.expanduser(
+ f"~/{data.APP_NAME}/config/assets/config.json"
+ )
+ try:
+ if os.path.exists(config_file):
+ with open(config_file, "r") as f:
+ config = json.load(f)
+
+ # Update the config with the new visibility state
+ config[f"bar_{component_name}_visible"] = self.component_visibility[
+ component_name
+ ]
+
+ # Write the updated config back to file
+ with open(config_file, "w") as f:
+ json.dump(config, f, indent=4)
+ except (json.JSONDecodeError, IOError) as e:
+ print(f"Error updating config file: {e}")
+ # Revert the visibility change if config update fails
+ self.component_visibility[
+ component_name
+ ] = not self.component_visibility[component_name]
+ components[component_name].set_visible(
+ self.component_visibility[component_name]
+ )
+
+ return self.component_visibility[component_name]
+
+ return None
+
+ def on_button_enter(self, widget, event):
+ window = widget.get_window()
+ if window:
+ window.set_cursor(Gdk.Cursor(Gdk.CursorType.HAND2))
+
+ def on_button_leave(self, widget, event):
+ window = widget.get_window()
+ if window:
+ window.set_cursor(None)
+
+ def search_apps(self):
+ self.launcher.open("launcher")
+
+ def calendar(self):
+ self.launcher.open("calendar")
+
+ def on_language_switch(self, _=None, event: HyprlandEvent = None):
+ lang = event.data[1] if event else Language().get_label()
+ self.language.set_tooltip_text(lang)
+ if not data.VERTICAL:
+ self.lang_label.set_label(lang[:2].lower())
else:
- self.bar_content.add_style_class("visible")
- self.bar_content.remove_style_class("hidden")
- self.set_property("visible", True)
+ self.lang_label.add_style_class("icon")
+ self.lang_label.set_markup(icons.keyboard)
-class ScreenCorners(Window):
+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",
@@ -191,30 +294,19 @@ class ScreenCorners(Window):
children=[
Box(
children=[
- self.make_corner("top-left"),
+ MyCorner("top-left"),
Box(h_expand=True),
- self.make_corner("top-right"),
+ MyCorner("top-right"),
]
),
Box(v_expand=True),
Box(
children=[
- self.make_corner("bottom-left"),
+ MyCorner("bottom-left"),
Box(h_expand=True),
- self.make_corner("bottom-right"),
+ MyCorner("bottom-right"),
]
),
],
),
)
-
- def make_corner(self, orientation) -> Box:
- return Box(
- h_expand=False,
- v_expand=False,
- name="bar-corner",
- children=Corner(
- orientation=orientation,
- size=15,
- ),
- )
diff --git a/modules/bar/components/__init__.py b/modules/bar/components/__init__.py
index 4cba446..161c235 100644
--- a/modules/bar/components/__init__.py
+++ b/modules/bar/components/__init__.py
@@ -1,5 +1,4 @@
-from .battery import *
-from .metric import *
-from .workspace import *
-from .system_indicators import *
-from .updates import *
+from .metrics import Metrics
+from .workspace import workspace
+from .system_indicators import SystemIndicators
+from .updates import UpdatesWidget
diff --git a/modules/bar/components/battery.py b/modules/bar/components/battery.py
deleted file mode 100644
index 5707911..0000000
--- a/modules/bar/components/battery.py
+++ /dev/null
@@ -1,237 +0,0 @@
-import subprocess
-import re
-from fabric.widgets.box import Box
-from fabric.widgets.eventbox import EventBox
-from fabric.widgets.button import Button
-from fabric.widgets.label import Label
-from fabric.widgets.circularprogressbar import CircularProgressBar
-from fabric.widgets.overlay import Overlay
-from fabric.widgets.revealer import Revealer
-from fabric.core.fabricator import Fabricator
-from fabric.utils.helpers import exec_shell_command_async
-from gi.repository import GLib
-import utils.icons as icons
-
-
-class Battery(Box):
- def __init__(self, **kwargs):
- super().__init__(
- name="battery",
- orientation="h",
- spacing=0,
- )
-
- self.bat_save = Button(
- name="battery-save",
- child=Label(name="battery-save-label", markup=icons.power_saving),
- on_clicked=lambda *_: self.set_power_mode("powersave"),
- )
- self.bat_balanced = Button(
- name="battery-balanced",
- child=Label(name="battery-balanced-label", markup=icons.power_balanced),
- on_clicked=lambda *_: self.set_power_mode("balanced"),
- )
- self.bat_perf = Button(
- name="battery-performance",
- child=Label(
- name="battery-performance-label", markup=icons.power_performance
- ),
- on_clicked=lambda *_: self.set_power_mode("performance"),
- )
-
- # Attach mouse enter/leave events to buttons as well
- for btn in [self.bat_save, self.bat_balanced, self.bat_perf]:
- btn.connect("enter-notify-event", self.on_mouse_enter)
- btn.connect("leave-notify-event", self.on_mouse_leave)
-
- # Group the mode buttons into a container.
- self.mode_switcher = Box(
- name="power-mode-switcher",
- orientation="h",
- spacing=4,
- children=[self.bat_save, self.bat_balanced, self.bat_perf],
- )
-
- self.bat_icon = Label(name="battery-icon", markup=icons.battery)
-
- self.bat_circle = CircularProgressBar(
- name="battery-circle",
- value=0,
- size=28,
- line_width=2,
- start_angle=150,
- end_angle=390,
- )
-
- self.bat_overlay = Overlay(
- name="battery-overlay",
- visible=False,
- child=self.bat_circle,
- overlays=[self.bat_icon],
- )
-
- self.bat_level = Label(
- name="battery-level",
- label="100%",
- )
-
- self.bat_revealer = Revealer(
- name="battery-revealer",
- transition_duration=250,
- transition_type="slide-left",
- child=self.bat_level,
- child_revealed=False,
- )
-
- # Create an inner container to hold the battery elements.
- inner_container = Box(orientation="h", spacing=3)
- inner_container.add(self.bat_overlay)
- inner_container.add(self.bat_revealer)
- inner_container.add(self.mode_switcher)
-
- # Wrap the inner container in an EventBox.
- self.event_box = EventBox(
- events=["enter-notify-event", "leave-notify-event"],
- name="battery-eventbox",
- )
- self.event_box.connect("enter-notify-event", self.on_mouse_enter)
- self.event_box.connect("leave-notify-event", self.on_mouse_leave)
- self.event_box.add(inner_container)
-
- # Add the EventBox to the Battery widget.
- self.add(self.event_box)
-
- self.current_mode = None
- # Initialize hide_timer for delayed hiding.
- self.hide_timer = None
- # Initialize counter to track hover status across all related widgets.
- self.hover_counter = 0
-
- # Initialize Fabricator for battery polling every 5 seconds.
- self.batt_fabricator = Fabricator(
- lambda *args, **kwargs: self.poll_battery(),
- interval=1000,
- stream=False,
- default_value=0,
- )
- self.batt_fabricator.changed.connect(self.update_battery)
-
- # Run update_battery immediately at startup.
- GLib.idle_add(self.update_battery, None, self.poll_battery())
- self.set_power_mode("balanced")
-
- def on_mouse_enter(self, widget, event):
- """Reveal battery level on hover."""
- self.hover_counter += 1
- if self.hide_timer is not None:
- GLib.source_remove(self.hide_timer)
- self.hide_timer = None
- self.bat_revealer.set_reveal_child(True)
- return False
-
- def on_mouse_leave(self, widget, event):
- """Schedule hiding the battery level after a 0.5s delay only if not hovering any element."""
- 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):
- self.bat_revealer.set_reveal_child(False)
- self.hide_timer = None
- return False
-
- def poll_battery(self):
- """
- Polls the battery status by running the "acpi -b" command.
- If no battery information is found, returns (0, None).
- Otherwise, returns a tuple: (battery percentage as a float between 0 and 1, battery status string)
- """
- try:
- output = subprocess.check_output(["acpi", "-b"]).decode("utf-8").strip()
- if "Battery" not in output:
- return (0, None)
- match_percent = re.search(r"(\d+)%", output)
- match_status = re.search(r"Battery \d+: (\w+)", output)
- if match_percent:
- percent = int(match_percent.group(1))
- status = match_status.group(1) if match_status else None
- return (percent / 100.0, status)
- except Exception:
- pass
- return (0, None)
-
- def update_battery(self, sender, battery_data):
- """
- Updates the battery widget...
- """
- value, status = battery_data
- if value == 0:
- self.bat_overlay.set_visible(False)
- self.bat_revealer.set_visible(False)
- else:
- self.bat_overlay.set_visible(True)
- self.bat_revealer.set_visible(True)
- self.bat_circle.set_value(value)
-
- percentage = int(value * 100)
- self.bat_level.set_label(f"{percentage}%")
-
- # Actualizar el icono de la batería basado en el nivel y el estado de carga.
- if percentage <= 15:
- self.bat_icon.set_markup(icons.alert)
- 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")
- if (
- status == "Discharging"
- ): # Muestra discharging en cualquier nivel, incluso al 100%.
- self.bat_icon.set_markup(icons.discharging)
- elif percentage == 100:
- self.bat_icon.set_markup(icons.battery)
- elif status == "Charging":
- self.bat_icon.set_markup(icons.charging)
- else:
- self.bat_icon.set_markup(icons.battery)
-
- def set_power_mode(self, mode):
- """
- Switches power mode by running the corresponding auto-cpufreq command.
- mode: one of 'powersave', 'balanced', or 'performance'
- """
- commands = {
- "powersave": "sudo auto-cpufreq --force powersave",
- "balanced": "sudo auto-cpufreq --force reset",
- "performance": "sudo auto-cpufreq --force performance",
- }
- if mode in commands:
- try:
- exec_shell_command_async(commands[mode])
- self.current_mode = mode
- self.update_button_styles()
- except Exception as err:
- # Optionally, handle errors or display a notification.
- print(f"Error setting power mode: {err}")
-
- def update_button_styles(self):
- """
- Optionally updates button styles to reflect the current mode.
- Adjust the styling method based on your toolkit's capabilities.
- """
- if self.current_mode == "powersave":
- self.bat_save.add_style_class("active")
- self.bat_balanced.remove_style_class("active")
- self.bat_perf.remove_style_class("active")
- elif self.current_mode == "balanced":
- self.bat_save.remove_style_class("active")
- self.bat_balanced.add_style_class("active")
- self.bat_perf.remove_style_class("active")
- elif self.current_mode == "performance":
- self.bat_save.remove_style_class("active")
- self.bat_balanced.remove_style_class("active")
- self.bat_perf.add_style_class("active")
diff --git a/modules/bar/components/metric.py b/modules/bar/components/metric.py
deleted file mode 100644
index d0e026a..0000000
--- a/modules/bar/components/metric.py
+++ /dev/null
@@ -1,83 +0,0 @@
-import psutil
-from gi.repository import GLib
-
-from fabric.widgets.label import Label
-from fabric.widgets.box import Box
-from fabric.widgets.overlay import Overlay
-from fabric.widgets.circularprogressbar import CircularProgressBar
-import utils.icons as icons
-
-
-class Metrics(Box):
- ICONS = {
- "CPU": icons.cpu,
- "RAM": icons.memory,
- "Swap": icons.swap,
- "Disk": icons.disk,
- "Temp": icons.temp,
- }
-
- def __init__(self, **kwargs):
- super().__init__(
- name="metrics",
- spacing=8,
- h_align="center",
- v_align="fill",
- visible=True,
- all_visible=True,
- )
-
- self.progress_bars = {
- system: CircularProgressBar(
- name="metric-circle",
- line_style="round",
- line_width=2,
- size=28,
- start_angle=150,
- end_angle=390,
- )
- for system in self.ICONS
- }
-
- for system, icon_name in self.ICONS.items():
- overlay = Overlay(
- child=self.progress_bars[system],
- overlays=[Label(name="metric-icon", markup=icon_name)],
- )
- self.add(overlay)
-
- GLib.timeout_add_seconds(1, self._update_system_info)
-
- def _update_system_info(self):
- ram_usage = psutil.virtual_memory().percent
- swap_usage = psutil.swap_memory().percent
- disk_usage = psutil.disk_usage("/").percent
- cpu_usage = psutil.cpu_percent(interval=0)
-
- temp = self._get_device_temperature()
- temp_usage = temp if temp is not None else 0
-
- usages = {
- "CPU": cpu_usage,
- "RAM": ram_usage,
- "Swap": swap_usage,
- "Disk": disk_usage,
- "Temp": temp_usage,
- }
-
- for system, usage in usages.items():
- self.progress_bars[system].value = usage / 100.0
- self.progress_bars[system].set_tooltip_text(f"{system} {usage:.1f}%")
-
- return True
-
- @staticmethod
- def _get_device_temperature():
- try:
- temps = psutil.sensors_temperatures()
- for key in ("coretemp", "cpu_thermal"):
- if key in temps and temps[key]:
- return round(temps[key][0].current, 1)
- except Exception:
- pass
- return None
diff --git a/modules/bar/components/metrics.py b/modules/bar/components/metrics.py
new file mode 100644
index 0000000..52f9c64
--- /dev/null
+++ b/modules/bar/components/metrics.py
@@ -0,0 +1,406 @@
+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.gpu = []
+ 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]
+ info = self.get_gpu_info()
+ self.gpu = [int(v["gpu_util"][:-1]) for v in info]
+
+ 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)
+
+ def get_gpu_info(self):
+ try:
+ return json.loads(subprocess.check_output(["nvtop", "-s"]))
+ except:
+ return []
+
+
+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, "gpu": False, "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 []
+ )
+ gpu_info = shared_provider.get_gpu_info()
+ gpus = (
+ [
+ SingularMetric(
+ f"gpu",
+ f"GPU ({v['device_name']})" if len(gpu_info) != 1 else "GPU",
+ icons.gpu,
+ )
+ for v in gpu_info
+ ]
+ if visible.get("gpu", True)
+ else []
+ )
+
+ self.cpu = (
+ SingularMetric("cpu", "CPU", icons.cpu)
+ if visible.get("cpu", True)
+ else None
+ )
+ self.ram = (
+ SingularMetric("ram", "RAM", icons.memory)
+ if visible.get("ram", True)
+ else None
+ )
+ self.swap = (
+ SingularMetric("swap", "SWAP", icons.swap)
+ if visible.get("swap", True)
+ else None
+ )
+ self.bat = (
+ SingularMetric("bat", "BATTERY", icons.battery)
+ if visible.get("bat", True)
+ else None
+ )
+ self.disk = disks
+ self.gpu = gpus
+
+ # 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)
+ for gpu in self.gpu:
+ self.add(Box(name="metrics-sep"))
+ self.add(gpu.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)
+ for gpu in self.gpu:
+ gpu.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)
+ for gpu in self.gpu:
+ gpu.revealer.set_reveal_child(False)
+ self.hide_timer = None
+ return False
+
+ def update_metrics(self):
+ cpu, mem, swap, disk = shared_provider.get_metrics()
+ gpu = shared_provider.gpu
+ 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)
+ for i, gpu_metric in enumerate(self.gpu):
+ if i < len(gpu):
+ value_text = self._format_percentage(int(gpu[i]))
+ gpu_metric.circle.set_value(gpu[i] / 100.0)
+ gpu_metric.level.set_label(value_text)
+ gpu_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)
+ if self.gpu: tooltip_metrics.extend(self.gpu)
+ self.set_tooltip_markup((" - " if not data.VERTICAL else "\n").join([v.markup() for v in tooltip_metrics]))
+
+ return True
+
+ 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
index 0f49299..7d9b453 100644
--- a/modules/bar/components/system_indicators.py
+++ b/modules/bar/components/system_indicators.py
@@ -2,14 +2,22 @@ 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 NetworkClient, audio
+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):
- super().__init__(orientation="h", spacing=2, **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")
@@ -17,6 +25,11 @@ class SystemIndicators(Box):
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,
@@ -25,18 +38,25 @@ class SystemIndicators(Box):
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 = NetworkClient()
+ 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(
@@ -51,84 +71,188 @@ class SystemIndicators(Box):
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, *_):
- """Updates the idle and night mode status icons."""
- self.idle_label.set_markup(
- icons.coffee
- if subprocess.run(
- ["pgrep", "-x", "wlinhibit"], capture_output=True
- ).returncode
+ # Update idle status
+ has_idle = (
+ subprocess.run(["pgrep", "-x", "wlinhibit"], capture_output=True).returncode
== 0
- else ""
)
+ self.idle_label.set_visible(has_idle)
+ self.idle_label.set_markup(icons.coffee if has_idle else "")
- self.night_label.set_markup(
- icons.night
- if subprocess.run(
+ # Update night mode status
+ has_night = (
+ subprocess.run(
["pgrep", "-x", "hyprsunset"], capture_output=True
).returncode
== 0
- else ""
)
+ self.night_label.set_visible(has_night)
+ self.night_label.set_markup(icons.night if has_night else "")
def update_volume_status(self, *_):
- """Updates the volume icon based on speaker status."""
stream = self.audio_service.speaker
- if stream:
- volume_level = stream.volume
- self.volume_icon_button.set_markup(
- icons.vol_off
- if stream.muted
- else (
- icons.vol_mute
- if volume_level < 1
- else (icons.vol_medium if volume_level >= 1 else icons.vol_high)
- )
- )
+ 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, *_):
- """Updates the microphone icon based on mute status."""
mic = self.audio_service.microphone
- self.microphone_icon.set_markup(icons.mic_off if mic and mic.muted else "")
- self.microphone_icon.set_visible(bool(mic))
+ 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, *_):
- """Updates the Bluetooth icon based on enabled status."""
- self.bluetooth_icon.set_markup(
- icons.bluetooth if self.bluetooth_client.enabled else icons.bluetooth_off
- )
+ 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.set_tooltip_text(self.network_client.wifi_device.ssid)
+
+ self.wifi_icon.set_tooltip_text(
+ wifi_device.ssid if wifi_device.ssid else "No WiFi Connection"
+ )
if wifi_device.enabled:
- wifi_strength = wifi_device.strength
+ strength = wifi_device.strength
- if wifi_strength == -1: # No active connection
- icon_label = icons.wifi_off
+ 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 = {
- 80: icons.wifi,
- 60: icons.good_signal,
- 40: icons.moderate_signal,
- 20: icons.weak_signal,
- 0: icons.no_signal,
- }.get(min(80, 20 * round(wifi_strength / 20)), icons.wifi_off)
+ 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
+ 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
index a285479..f43add2 100644
--- a/modules/bar/components/updates.py
+++ b/modules/bar/components/updates.py
@@ -5,9 +5,9 @@ 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 loguru import logger
from gi.repository import Gtk
import utils.icons as icons
+import config.data as data
class UpdatesWidget(Button):
@@ -17,8 +17,15 @@ class UpdatesWidget(Button):
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="button-bar", **kwargs)
+ super().__init__(
+ name="update",
+ orientation="h" if not is_vertical_layout else "v",
+ **kwargs,
+ )
self.script_file = get_relative_path("../../../config/scripts/systemupdates.sh")
@@ -31,45 +38,49 @@ class UpdatesWidget(Button):
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,
)
- # Ensure the stack resizes to the current visible child
+ # Configure stack transitions
self.stack.set_homogeneous(False)
self.stack.set_transition_type(Gtk.StackTransitionType.CROSSFADE)
- # Connect the button press event
+ # Connect click handler
self.connect("button-press-event", self.on_button_press)
- # Set up a repeater to call the update method at specified intervals
+ # Check for updates every minute
invoke_repeater(60000, self.update, initial_call=True)
- def update_values(self, value: str):
- logger.info(f"[Updates] Received update JSON: {value}")
-
+ def update_values(self, value: str) -> bool:
+ """Update the widget state based on update check results"""
try:
value = json.loads(value)
except json.JSONDecodeError:
- logger.error("[Updates] Failed to parse update JSON")
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)
@@ -80,8 +91,10 @@ class UpdatesWidget(Button):
self.set_tooltip_text(value.get("tooltip", ""))
return True
- def on_button_press(self, _, event):
- if event.button == 1:
+ 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,
@@ -89,12 +102,11 @@ class UpdatesWidget(Button):
self.update()
return True
else:
+ # Other clicks just refresh
self.update()
- def update(self):
- logger.info("[Updates] Checking for updates...")
-
- # Execute the update script asynchronously and update values
+ def update(self) -> bool:
+ """Check for system updates"""
exec_shell_command_async(
f"{self.script_file} -arch",
lambda output: self.update_values(output),
diff --git a/modules/bar/components/workspace.py b/modules/bar/components/workspace.py
index f13372d..072364b 100644
--- a/modules/bar/components/workspace.py
+++ b/modules/bar/components/workspace.py
@@ -2,18 +2,29 @@ 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"]
-workspace = Label(label=f"Workspace {activeWorkspace}")
+# 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])
- workspace.set_label(f"Workspace {activeWorkspace}")
-
+ 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
index 593b2c6..fb89eb7 100644
--- a/modules/dock.py
+++ b/modules/dock.py
@@ -1,49 +1,257 @@
import json
-import threading
-from gi.repository import GLib
-from utils import IconResolver, read_config
+from gi.repository import GLib, Gtk, Gdk
+import cairo
+from utils.icon_resolver import IconResolver
+from utils.occlusion import check_occlusion
from fabric.widgets.box import Box
from fabric.widgets.button import Button
from fabric.widgets.image import Image
from fabric.widgets.eventbox import EventBox
from fabric.widgets.wayland import WaylandWindow as Window
from fabric.hyprland.widgets import get_hyprland_connection
-from fabric.utils import exec_shell_command, idle_add, remove_handler
+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",
+ 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,
)
- self.config = read_config()
+ # 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")
+ 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])
- self.hover = EventBox()
- self.hover.set_size_request(-1, 1)
- self.hover.connect("enter-notify-event", self._on_hover_enter)
- self.hover.connect("leave-notify-event", self._on_hover_leave)
- self.view.connect("enter-notify-event", self._on_hover_enter)
- self.view.connect("leave-notify-event", self._on_hover_leave)
- self.main_box = Box(orientation="v", children=[self.wrapper, self.hover])
+
+ # 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
- ) # Delay to ensure Hyprland loads clients
+ GLib.timeout_add(500, self.check_hide)
else:
self.conn.connect("event::ready", self.update_dock)
self.conn.connect("event::ready", self.check_hide)
@@ -52,41 +260,381 @@ class Dock(Window):
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."""
+ """Show or hide the dock immediately"""
if show:
if self.is_hidden:
self.is_hidden = False
- self.wrapper.add_style_class("show-dock")
- self.wrapper.remove_style_class("hide-dock")
+ 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.wrapper.add_style_class("hide-dock")
- self.wrapper.remove_style_class("show-dock")
+ self.dock_full.add_style_class("hide-dock")
+ self.dock_full.remove_style_class("show-dock")
def delay_hide(self):
- """Schedule hiding the dock after a short delay."""
+ """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):
- """Show the dock immediately in an empty workspace; otherwise, hide if needed."""
+ """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]
@@ -99,87 +647,229 @@ class Dock(Window):
self.toggle_dock(show=True)
def update_dock(self, *args):
- """Refresh the dock icons based on running and pinned apps."""
+ """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()
- running = {}
+
+ # Create a mapping of window class to instances
+ running_windows = {}
for c in clients:
- key = c["initialClass"].lower()
- running.setdefault(key, []).append(c)
- pinned_buttons = [
- self.create_button(app, running.get(app.lower(), [])) for app in self.pinned
- ]
- open_buttons = [
- self.create_button(
- c["initialClass"], running.get(c["initialClass"].lower(), [])
+ # 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', '')})"
)
- for group in running.values()
- for c in group
- if c["initialClass"].lower() not in self.pinned
- ]
+
+ # 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", v_expand=True, name="dock-separator")]
+ 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 create_button(self, app, instances):
- """Create a button for an app with an icon and an optional indicator."""
- icon_img = self.icon.get_icon_pixbuf(
- app.lower(), 30
- ) or self.icon.get_icon_pixbuf("image-missing", 30)
- items = [Image(pixbuf=icon_img)]
- if instances:
- items.append(
- Image(
- pixbuf=self.icon.get_icon_pixbuf("filename-dot-amarok", 11),
- name="dock-app-indicator",
- )
- )
- return Button(
- child=Box(
- name="dock-icon",
- orientation="v",
- h_align="center",
- children=items,
- ),
- on_clicked=lambda *a: self.handle_app(app, instances),
- tooltip_text=instances[0]["title"] if instances else app,
- name="dock-app-button",
- )
-
- def handle_app(self, app, instances):
- if not instances:
- threading.Thread(
- target=exec_shell_command, args=(f"nohup {app}",), daemon=True
- ).start()
- else:
- focused = self.get_focused()
- idx = next(
- (i for i, inst in enumerate(instances) if inst["address"] == focused),
- -1,
- )
- next_inst = instances[(idx + 1) % len(instances)]
- exec_shell_command(
- f"hyprctl dispatch focuswindow address:{next_inst['address']}"
- )
-
def 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()
@@ -188,9 +878,256 @@ class Dock(Window):
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
index 8ea1e96..da2ef07 100644
--- a/modules/launcher/components/__init__.py
+++ b/modules/launcher/components/__init__.py
@@ -5,8 +5,12 @@ from .dashboard import *
from .emoji import *
from .power import *
from .sh import *
-from .todo 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
index fb9ce05..83380de 100644
--- a/modules/launcher/components/applauncher.py
+++ b/modules/launcher/components/applauncher.py
@@ -1,101 +1,143 @@
+import operator
from collections.abc import Iterator
-from fabric.utils import DesktopApp, get_desktop_applications, idle_add, remove_handler
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.label import Label
-from fabric.widgets.scrolledwindow import ScrolledWindow
from fabric.widgets.image import Image
-from gi.repository import Gdk, GLib
+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 math
import re
+import math
+import numpy as np
import subprocess
-import webbrowser
-from functools import partial
-from utils import read_config
+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
+ name="app-launcher",
+ visible=False,
+ all_visible=False,
+ **kwargs,
)
- self.scrolled_window = None
self.launcher = kwargs["launcher"]
- self.selected_index = -1
- self._arranger_handler = 0
- self._all_apps = get_desktop_applications()
- self.config = read_config()
+ self.selected_index = -1 # Track the selected item index
- self.calc_history_path = os.path.expanduser("~/.cache/modus/calc.json")
- self.calc_history = self._load_calc_history()
+ 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._on_notify_text,
- on_activate=self._on_activate,
- on_key_press_event=self.on_search_entry_key_press,
+ 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", orientation="h", children=[self.search_entry]
+ 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],
+ children=[
+ self.header_box,
+ self.scrolled_window,
+ ],
)
+ self.resize_viewport()
+
self.add(self.launcher_box)
self.show_all()
- def _load_calc_history(self):
- if os.path.exists(self.calc_history_path):
- with open(self.calc_history_path, "r") as f:
- return json.load(f)
- return []
-
- def _on_notify_text(self, entry, *args):
- self.arrange_viewport(entry.get_text())
-
- def _on_activate(self, entry, *args):
- self.on_search_entry_activate(entry.get_text())
-
- def close_launcher(self, *_):
+ def close_launcher(self):
self.viewport.children = []
- self.selected_index = -1
+ self.selected_index = -1 # Reset selection
self.launcher.close()
- self.hide_scrolled_window()
def open_launcher(self):
self._all_apps = get_desktop_applications()
- self.arrange_viewport("")
+ 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(":"):
- self.hide_scrolled_window()
- self.launcher.dashboard.hide()
- return
-
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 self._arranger_handler:
- remove_handler(self._arranger_handler)
- self._arranger_handler = 0
- self.viewport.children = []
- self.selected_index = -1
-
if query.strip():
self.launcher.dashboard.hide()
self.show_scrolled_window()
@@ -104,171 +146,485 @@ class AppLauncher(Box):
self.hide_scrolled_window()
return False
- filtered_apps = [
- app
- for app in self._all_apps
- if query.casefold()
- in (
- (app.display_name or "")
- + " "
- + app.name
- + " "
- + (app.generic_name or "")
- ).casefold()
- ]
filtered_apps_iter = iter(
- sorted(filtered_apps, key=lambda app: (app.display_name or "").casefold())
+ 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(
- partial(self._arranger_callback, query=query), filtered_apps_iter, pin=True
+ lambda apps_iter: self.add_next_application(apps_iter)
+ or self.handle_arrange_complete(should_resize, query),
+ filtered_apps_iter,
+ pin=True,
)
- def _arranger_callback(self, apps_iter: Iterator[DesktopApp], query: str):
- if self.add_next_application(apps_iter):
- return True
- return self.handle_arrange_complete(query)
-
- def show_scrolled_window(self):
- if self.scrolled_window is None:
- self.scrolled_window = ScrolledWindow(
- name="scrolled-window",
- style_classes="applauncher-scrolled-window",
- h_scrollbar_policy="never",
- child=self.viewport,
- )
- self.launcher_box.add(self.scrolled_window)
-
- self.scrolled_window.show_all()
- GLib.idle_add(self.update_scrolled_window_size)
-
- def hide_scrolled_window(self):
- if self.scrolled_window:
- self.scrolled_window.hide()
-
- def handle_arrange_complete(self, query):
- self.update_scrolled_window_size()
- if query.strip() and self.viewport.get_children():
+ 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]):
- app = next(apps_iter, None)
- if app is None:
+ if not (app := next(apps_iter, None)):
return False
self.viewport.add(self.bake_application_slot(app))
return True
- def bake_application_slot(self, app: DesktopApp) -> Button:
- return Button(
- name="launcher-app",
+ 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(pixbuf=app.get_icon_pixbuf(size=32), h_align="start"),
+ 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=partial(self._on_app_clicked, app),
+ on_clicked=lambda *_: (app.launch(), self.close_launcher()),
+ **kwargs,
)
-
- def _on_app_clicked(self, app, *args):
- app.launch()
- self.close_launcher()
+ return button
def update_selection(self, new_index: int):
- children = self.viewport.get_children()
- if self.selected_index >= 0 < len(children):
- children[self.selected_index].get_style_context().remove_class("selected")
-
- if 0 <= new_index < len(children):
- children[new_index].get_style_context().add_class("selected")
+ # 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 update_scrolled_window_size(self):
- if self.scrolled_window is None:
- return
+ 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
- children = self.viewport.get_children()
- item_count = len(children)
+ y = alloc.y
+ height = alloc.height
+ page_size = adj.get_page_size()
+ current_value = adj.get_value()
- if item_count == 0:
- self.scrolled_window.set_size_request(-1, 0)
- return
+ # Calculate visible boundaries
+ visible_top = current_value
+ visible_bottom = current_value + page_size
- first_child = children[0]
- first_child_height = first_child.get_allocation().height
+ 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
- if first_child_height == 0:
- GLib.idle_add(self.update_scrolled_window_size)
- return
-
- spacing = self.viewport.props.spacing
- total_height = (item_count * first_child_height) + ((item_count - 1) * spacing)
-
- max_height = 400
- self.scrolled_window.set_size_request(-1, min(total_height, max_height))
+ GLib.idle_add(scroll)
def on_search_entry_activate(self, text):
- if text.startswith(":i "):
- webbrowser.open(f"https://www.google.com/search?q={text[3:].strip()}")
- self.close_launcher()
- return
+ # This method is now only used for non-Enter key activations
+ commands = self.config.get("commands", {})
- if text.startswith("="):
+ # Check if text is a mathematical expression
+ if text and (text[0].isdigit()):
self.evaluate_calculator_expression(text)
return
- children = self.viewport.get_children()
- if children:
- (children[self.selected_index or 0]).clicked()
+ 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):
- if event.keyval in (Gdk.KEY_Down, Gdk.KEY_Up):
- self.move_selection(1 if event.keyval == Gdk.KEY_Down else -1)
- return True
- if event.keyval == Gdk.KEY_Escape:
- self.close_launcher()
- return True
- return False
+ 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
- new_index = max(0, min(self.selected_index + delta, len(children) - 1))
+ # 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 evaluate_calculator_expression(self, text: str):
- expr = text.lstrip("=").replace("^", "**").replace("×", "*")
- expr = re.sub(r"(\d+)!", r"math.factorial(\1)", expr)
- result = eval(expr, {"__builtins__": None, "math": math})
- self.calc_history.insert(0, f"{text} => {result}")
- self.save_calc_history()
- self.update_calculator_viewport()
-
- def update_calculator_viewport(self):
- self.viewport.children = [
- Button(
- child=Label(label=item),
- on_clicked=partial(self.copy_text_to_clipboard, item),
- )
- for item in self.calc_history
- ]
- self.update_scrolled_window_size()
-
def save_calc_history(self):
with open(self.calc_history_path, "w") as f:
json.dump(self.calc_history, f)
- def copy_text_to_clipboard(self, text: str, *args):
- subprocess.run(
- ["wl-copy"], input=text.split("=>", 1)[-1].strip().encode(), check=True
- )
+ 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
index 69bf2a8..c3f6930 100644
--- a/modules/launcher/components/bluetooth.py
+++ b/modules/launcher/components/bluetooth.py
@@ -30,7 +30,7 @@ class BluetoothDeviceSlot(CenterBox):
Box(
spacing=8,
children=[
- Image(icon_name=f"{device.icon_name}-symbolic", size=32),
+ Image(icon_name=device.icon_name + "-symbolic", size=32),
Label(label=device.name),
self.connection_label,
],
@@ -61,7 +61,7 @@ class BluetoothConnections(Box):
def __init__(self, **kwargs):
super().__init__(
name="bluetooth",
- spacing=8,
+ spacing=4,
orientation="vertical",
**kwargs,
)
@@ -77,63 +77,54 @@ class BluetoothConnections(Box):
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=Label(name="bluetooth-scan-icon", markup=icons.scan_stop),
+ child=self.scan_label,
+ tooltip_text="Scan for Bluetooth devices",
on_clicked=lambda *_: self.client.toggle_scan(),
)
- self.toggle_button = Button(
- name="bluetooth-toggle",
- child=Label(name="bluetooth-toggle-icon", markup=icons.bluetooth_off),
- on_clicked=lambda *_: self.client.toggle_power(),
+ 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.scan_button.get_child().set_markup(
- icons.scan_stop if self.client.scanning else icons.scan
- ),
- )
+ 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.scan_button,
+ start_children=self.back_button,
center_children=Label(name="bluetooth-text", label="Bluetooth Devices"),
- end_children=self.toggle_button,
+ end_children=self.scan_button,
),
- Label(name="bluetooth-section", label="Paired"),
ScrolledWindow(
- name="bluetooth-paired",
+ name="bluetooth-devices",
min_content_size=(-1, -1),
- child=self.paired_box,
- v_expand=True,
- ),
- Label(name="bluetooth-section", label="Available"),
- ScrolledWindow(
- name="bluetooth-available",
- min_content_size=(-1, -1),
- child=self.available_box,
+ child=content_box,
v_expand=True,
),
]
- # to run notify closures thus display the status
- # without having to wait until an actual change
+ # 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.toggle_button.get_child().set_markup(icons.bluetooth)
self.bt_status_text.set_label("Enabled")
for i in [
self.bt_status_button,
@@ -146,7 +137,6 @@ class BluetoothConnections(Box):
i.remove_style_class("disabled")
self.bt_icon.set_markup(icons.bluetooth)
else:
- self.toggle_button.get_child().set_markup(icons.bluetooth_off)
self.bt_status_text.set_label("Disabled")
for i in [
self.bt_status_button,
@@ -165,6 +155,16 @@ class BluetoothConnections(Box):
slot = BluetoothDeviceSlot(device)
if device.paired:
- self.paired_box.add(slot)
+ 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.available_box.add(slot)
+ 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
index 76bd139..263e572 100644
--- a/modules/launcher/components/calendar.py
+++ b/modules/launcher/components/calendar.py
@@ -1,11 +1,13 @@
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
-import utils.icons as icons
class Calendar(Gtk.Box):
@@ -17,36 +19,47 @@ class Calendar(Gtk.Box):
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
- self.month_views = {}
- self.header = Gtk.Box(spacing=4, name="header")
- self.pack_start(self.header, False, False, 0)
+ # 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.header.pack_start(self.prev_month_button, False, False, 0)
self.month_label = Gtk.Label(name="month-label")
- self.header.pack_start(self.month_label, True, True, 0)
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.pack_start(self.next_month_button, False, False, 0)
+
+ 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(500)
+ self.stack.set_transition_duration(250)
self.pack_start(self.stack, True, True, 0)
self.update_header()
@@ -54,14 +67,15 @@ class Calendar(Gtk.Box):
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")
@@ -70,14 +84,15 @@ class Calendar(Gtk.Box):
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
@@ -86,24 +101,25 @@ class Calendar(Gtk.Box):
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 = [
- key
- for key in self.month_views
- if abs(month_index(key) - current_index) > self.cache_threshold
- ]
-
+ 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)
@@ -167,6 +183,7 @@ class Calendar(Gtk.Box):
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):
@@ -197,3 +214,4 @@ class Calendar(Gtk.Box):
self.current_year = now.year
self.update_calendar()
return True
+
diff --git a/modules/launcher/components/cliphist.py b/modules/launcher/components/cliphist.py
index 69f4c00..5747747 100644
--- a/modules/launcher/components/cliphist.py
+++ b/modules/launcher/components/cliphist.py
@@ -1,201 +1,504 @@
-import os
-import subprocess
-from typing import List
-from fabric import Fabricator
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.label import Label
+from fabric.widgets.image import Image
from fabric.widgets.scrolledwindow import ScrolledWindow
-from gi.repository import GLib
+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 Cliphist(Box):
+class ClipHistory(Box):
def __init__(self, **kwargs):
super().__init__(
- name="cliphist-launcher",
- all_visible=False,
+ name="clip-history",
visible=False,
+ all_visible=False,
**kwargs,
)
- self._arranger_handler = 0
- self.cliphist_manager = CliphistManager(self)
- self.viewport = None
- self.launcher = kwargs["launcher"]
+ # 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,
- on_activate=self._on_search_activate,
+ 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",
+ name="header_box",
spacing=10,
orientation="h",
children=[
- self.search_entry,
Button(
name="clear-button",
- child=Label(name="cliphist-clear-button", markup=icons.clear),
- tooltip_text="Clear Clipboard History",
- on_clicked=self.handle_clear_history,
+ child=Label(name="clear-label", markup=icons.trash),
+ on_clicked=lambda *_: self.clear_history(),
),
+ self.search_entry,
],
)
- self.launcher_box = Box(
- name="cliphist-launcher-box",
+ self.history_box = Box(
+ name="launcher-box",
spacing=10,
- orientation="v",
h_expand=True,
- children=[self.header_box],
+ orientation="v",
+ children=[
+ self.header_box,
+ self.scrolled_window,
+ ],
)
- self.add(self.launcher_box)
-
- def _on_search_activate(self, entry, *args):
- self.handle_search(entry.get_text())
-
- def open_launcher(self):
- if not self.viewport:
- self.viewport = Box(name="viewport", spacing=4, orientation="v")
- self.scrolled_window = ScrolledWindow(
- name="scrolled-window",
- spacing=10,
- min_content_size=(-1, -1),
- h_scrollbar_policy="never",
- child=self.viewport,
- )
- self.launcher_box.add(self.scrolled_window)
+ self.add(self.history_box)
+ self.show_all()
+ def close(self):
+ """Close the clipboard history panel"""
self.viewport.children = []
- self.cliphist_manager.update_cliphist_history()
-
- self.viewport.show()
- self.search_entry.grab_focus()
-
- def handle_search(self, text: str):
- self.cliphist_manager.arrange_viewport(query=text)
-
- def close_launcher(self):
+ self.selected_index = -1 # Reset selection
self.launcher.close()
- def handle_clear_history(self, *_):
- GLib.spawn_command_line_async("cliphist wipe")
- self.cliphist_manager.update_cliphist_history()
+ 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)
-
-class CliphistManager:
- def __init__(self, launcher):
- self.launcher = launcher
- self.cliphist_history = []
- self.wl_paste_watcher = Fabricator(
- poll_from="wl-paste --watch echo", stream=True, interval=-1
- )
- self.wl_paste_watcher.connect("changed", self._on_wl_paste_changed)
-
- def _on_wl_paste_changed(self, *args):
- self.update_cliphist_history()
-
- def get_clip_history(self) -> List[dict]:
+ 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
)
- return self._parse_clip_history(result.stdout)
+ lines = result.stdout.strip().split("\n")
+ new_items = []
+ for line in lines:
+ if not line or " List[dict]:
- items = []
- for line in output.splitlines():
- if line.strip():
- parts = line.strip().split(maxsplit=1)
- if len(parts) == 2:
- items.append({"id": parts[0], "content": parts[1]})
- return items
+ def _update_items(self, new_items):
+ """Update the items list from main thread"""
+ self.clipboard_items = new_items
+ self.display_clipboard_items()
- def copy_clip(self, clip_id: str):
- try:
- decode_result = subprocess.run(
- ["cliphist", "decode", clip_id], capture_output=True, check=True
- )
- subprocess.run(["wl-copy"], input=decode_result.stdout, check=True)
- self.launcher.close_launcher()
- except subprocess.CalledProcessError as e:
- print(f"Error copying clip {clip_id}: {e}")
+ def display_clipboard_items(self, filter_text=""):
+ """Display clipboard items in the viewport"""
+ remove_handler(self._arranger_handler) if self._arranger_handler else None
+ self.viewport.children = []
+ self.selected_index = -1 # Reset selection
- def save_image_file(self, clip_id: str) -> str:
- output_file = f"/tmp/cliphist-{clip_id}.png"
- os.makedirs("/tmp", exist_ok=True)
- try:
- decode_result = subprocess.run(
- ["cliphist", "decode", clip_id], capture_output=True, check=True
- )
- with open(output_file, "wb") as f:
- f.write(decode_result.stdout)
- return output_file
- except (subprocess.CalledProcessError, IOError) as e:
- print(f"Error saving image file for clip {clip_id}: {e}")
- return ""
+ # Filter items if search text is provided
+ filtered_items = []
+ for item in self.clipboard_items:
+ # Extract just the content part (after the first tab)
+ content = item.split("\t", 1)[1] if "\t" in item else item
+ if filter_text.lower() in content.lower():
+ filtered_items.append(item)
- def update_cliphist_history(self):
- self.cliphist_history = self.get_clip_history()
- self.arrange_viewport()
-
- def query_clips(self, query: str = "") -> List[dict]:
- if not query.strip():
- return self.cliphist_history
- return [
- clip
- for clip in self.cliphist_history
- if query.lower() in clip["content"].lower()
- ]
-
- def _on_clip_clicked(self, clip_id: str, *args):
- self.copy_clip(clip_id)
-
- def bake_clip_slot(self, item: dict) -> Button:
- if "[[ binary data" in item["content"].lower():
- return self._create_image_button(item)
- return self._create_text_button(item)
-
- def _create_image_button(self, item: dict) -> Button:
- output_file = self.save_image_file(item["id"])
- if output_file and os.path.exists(output_file):
- button = Button(
- h_align="start",
- name="clip-img-item",
- on_clicked=lambda _, clip_id=item["id"]: self.copy_clip(clip_id),
- )
- button.set_style(
- f"background-image: url('{output_file}'); background-size: cover; background-position: center;"
- )
- return button
- return None
-
- def _create_text_button(self, item: dict) -> Button:
- return Button(
- child=Label(
- label=item["content"][:35]
- + ("..." if len(item["content"]) > 35 else ""),
- h_expand=True,
+ # Show message if no items are found
+ if not filtered_items:
+ # Create a container box to better center the message
+ container = Box(
+ name="no-clip-container",
+ orientation="v",
+ h_align="center",
v_align="center",
- h_align="start",
- ),
- on_clicked=lambda _, clip_id=item["id"]: self.copy_clip(clip_id),
- name="clip-item",
+ h_expand=True,
+ v_expand=True,
+ )
+
+ # Show a message if no clipboard items
+ label = Label(
+ name="no-clip",
+ markup=icons.clipboard,
+ h_align="center",
+ v_align="center",
+ )
+
+ container.add(label)
+ self.viewport.add(container)
+ return
+
+ # Display items in batches to prevent UI freeze
+ self._display_items_batch(filtered_items, 0, 10)
+
+ def _display_items_batch(self, items, start, batch_size):
+ """Display items in batches to keep UI responsive"""
+ end = min(start + batch_size, len(items))
+
+ for i in range(start, end):
+ item = items[i]
+ self.viewport.add(self.create_clipboard_item(item))
+
+ # Schedule next batch if there are more items
+ if end < len(items):
+ GLib.idle_add(self._display_items_batch, items, end, batch_size)
+ else:
+ # Auto-select first item if we have filter text
+ if self.search_entry.get_text() and self.viewport.get_children():
+ self.update_selection(0)
+
+ def create_clipboard_item(self, item):
+ """Create a button for a clipboard item"""
+ # Extract ID and content
+ parts = item.split("\t", 1)
+ item_id = parts[0] if len(parts) > 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),
)
- def arrange_viewport(self, query: str = ""):
- if not self.launcher.viewport:
+ # 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
index 2dbe112..49b8ad8 100644
--- a/modules/launcher/components/dashboard.py
+++ b/modules/launcher/components/dashboard.py
@@ -1,12 +1,13 @@
import subprocess
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.utils.helpers import exec_shell_command_async, exec_shell_command
+from fabric.utils.helpers import exec_shell_command_async
import gi
-from gi.repository import Gtk, Gdk, GLib # Added Gdk import
-from fabric.utils import get_relative_path
+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
@@ -31,9 +32,14 @@ def add_hover_cursor(widget):
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=icons.wifi,
+ markup=None,
)
self.network_label = Label(
name="network-label",
@@ -43,7 +49,6 @@ class NetworkButton(Box):
self.network_label_box = Box(children=[self.network_label, Box(h_expand=True)])
self.network_ssid = Label(
name="network-ssid",
- label="Hello_World!!",
justification="left",
)
self.network_ssid_box = Box(children=[self.network_ssid, Box(h_expand=True)])
@@ -64,6 +69,9 @@ class NetworkButton(Box):
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
@@ -88,6 +96,148 @@ class NetworkButton(Box):
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):
@@ -205,7 +355,16 @@ class NightModeButton(Button):
self.night_mode_status,
self.night_mode_icon,
]
- self.check_hyprsunset()
+ # 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):
"""
@@ -225,16 +384,21 @@ class NightModeButton(Button):
for widget in self.widgets:
widget.remove_style_class("disabled")
- def check_hyprsunset(self, *args):
- """
- Update the button state based on whether hyprsunset is running.
- """
+ 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")
- except subprocess.CalledProcessError:
+ else:
self.night_mode_status.set_label("Disabled")
for widget in self.widgets:
widget.add_style_class("disabled")
@@ -289,117 +453,128 @@ class CaffeineButton(Button):
self.caffeine_status,
self.caffeine_icon,
]
- self.check_wlinhibit()
+ # **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 running, kill it and mark as 'Disabled' (add 'disabled' class).
- - If not running, start it and mark as 'Enabled' (remove 'disabled' class).
- """
- try:
- subprocess.check_output(["pgrep", "wlinhibit"])
+ """Toggle the 'wlinhibit' process."""
+ if self.is_wlinhibit_running():
exec_shell_command_async("pkill wlinhibit")
- self.caffeine_status.set_label("Disabled")
- for i in self.widgets:
- i.add_style_class("disabled")
- except subprocess.CalledProcessError:
+ else:
exec_shell_command_async("wlinhibit")
- self.caffeine_status.set_label("Enabled")
- for i in self.widgets:
- i.remove_style_class("disabled")
- def check_wlinhibit(self, *args):
+ 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"])
- self.caffeine_status.set_label("Enabled")
- for i in self.widgets:
- i.remove_style_class("disabled")
+ 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 i in self.widgets:
- i.add_style_class("disabled")
+ for widget in self.widgets:
+ widget.add_style_class("disabled")
-class DarkModeButton(Button):
+class PowerProfileButton(Button):
def __init__(self):
- self.dark_mode_icon = Label(
- name="dark-mode-icon",
- markup=icons.darkmode,
+ self.power_profile_service = PowerProfiles.get_default()
+
+ self.power_profile_icon = Label(
+ name="power-profile-icon",
+ markup=icons.power_balanced, # Default icon
)
- self.dark_mode_label = Label(
- name="dark-mode-label", label="Dark Mode", justification="left"
+ self.power_profile_label = Label(
+ name="power-profile-label",
+ label="Power Profile",
+ justification="left",
)
-
- self.dark_mode_label_box = Box(
- children=[self.dark_mode_label, Box(h_expand=True)]
+ self.power_profile_label_box = Box(
+ children=[self.power_profile_label, Box(h_expand=True)]
)
- self.dark_mode_status = Label(
- name="dark-mode-status", label="Enabled", justification="left"
+ self.power_profile_status = Label(
+ name="power-profile-status",
+ label="Balanced",
+ justification="left",
)
- self.dark_mode_status_box = Box(
- children=[self.dark_mode_status, Box(h_expand=True)]
+ self.power_profile_status_box = Box(
+ children=[self.power_profile_status, Box(h_expand=True)]
)
- self.dark_mode_text = Box(
- name="dark-mode-text",
+ self.power_profile_text = Box(
+ name="power-profile-text",
orientation="v",
h_align="start",
v_align="center",
- children=[self.dark_mode_label_box, self.dark_mode_status_box],
+ children=[self.power_profile_label_box, self.power_profile_status_box],
)
- self.dark_mode_box = Box(
+ self.power_profile_box = Box(
h_align="start",
v_align="center",
spacing=10,
- children=[self.dark_mode_icon, self.dark_mode_text],
+ children=[self.power_profile_icon, self.power_profile_text],
)
super().__init__(
- name="dark-mode-button",
+ name="power-profile-button",
h_expand=True,
- child=self.dark_mode_box,
- on_clicked=self.toggle_darkmode,
+ child=self.power_profile_box,
+ on_clicked=self.toggle_power_profile,
)
add_hover_cursor(self)
self.widgets = [
self,
- self.dark_mode_label,
- self.dark_mode_status,
- self.dark_mode_icon,
+ self.power_profile_label,
+ self.power_profile_status,
+ self.power_profile_icon,
]
- self.check_darkmode()
- def toggle_darkmode(self, *_):
- current_state = self.check_darkmode()
- new_mode = "dark" if not current_state else "light"
- command = f"bash {get_relative_path('../../../config/scripts/dark-theme.sh')} --set {new_mode}"
- GLib.spawn_command_line_async(command)
+ # 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())
- self.dark_mode_status.set_label("Enabled" if new_mode == "dark" else "Disabled")
+ 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)
- # Apply or remove 'disabled' class
- if new_mode == "light":
+ 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")
- def check_darkmode(self):
- result = exec_shell_command(
- "gsettings get org.gnome.desktop.interface color-scheme"
- )
- is_dark_mode = result.strip().replace("'", "") == "prefer-dark"
-
- if not is_dark_mode:
- for widget in self.widgets:
- widget.add_style_class("disabled")
- else:
- for widget in self.widgets:
- widget.remove_style_class("disabled")
-
- return is_dark_mode
class Dashboard(Gtk.Grid):
@@ -418,12 +593,12 @@ class Dashboard(Gtk.Grid):
self.bluetooth_button = BluetoothButton(self.launcher)
self.night_mode_button = NightModeButton()
self.caffeine_button = CaffeineButton()
- self.dark_mode_button = DarkModeButton()
+ self.power_profile_button = PowerProfileButton()
- # Attach buttons into the grid (one row, four columns)
+ # 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.dark_mode_button, 2, 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)
diff --git a/modules/launcher/components/emoji.py b/modules/launcher/components/emoji.py
index 731c108..b69355e 100644
--- a/modules/launcher/components/emoji.py
+++ b/modules/launcher/components/emoji.py
@@ -1,133 +1,330 @@
-import subprocess
-from typing import Generator, List
-
-import ijson
-from fabric.utils import remove_handler, get_relative_path
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.label import Label
-from fabric.widgets.scrolledwindow import ScrolledWindow
+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 Emoji(Box):
+class EmojiPicker(Box):
def __init__(self, **kwargs):
super().__init__(
- name="emoji-launcher",
- all_visible=False,
+ name="emoji",
visible=False,
+ all_visible=False,
**kwargs,
)
self.launcher = kwargs["launcher"]
- self._arranger_handler: int = 0
- self.emoji_manager = EmojiManager(self)
- self.viewport = None
+ 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.handle_search_input(entry.get_text()),
- on_activate=lambda entry, *_: self.handle_search_input(entry.get_text()),
+ 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",
+ 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.launcher_box = Box(
+ self.picker_box = Box(
+ name="picker-box",
spacing=10,
- orientation="v",
h_expand=True,
- children=[self.header_box],
+ orientation="v",
+ children=[
+ self.header_box,
+ self.stack,
+ ],
)
- self.add(self.launcher_box)
+ self.resize_viewport()
- def open_launcher(self):
- if not self.viewport:
- self.viewport = Box(name="viewport", spacing=4, orientation="v")
- self.scrolled_window = ScrolledWindow(
- name="scrolled-window",
- spacing=10,
- h_scrollbar_policy="never",
- v_scrollbar_policy="never",
- child=self.viewport,
- )
- self.launcher_box.add(self.scrolled_window)
+ self.add(self.picker_box)
+ self.show_all()
- self.viewport.children = []
- self.emoji_manager.arrange_viewport()
+ 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 {}
- self.viewport.show()
- self.search_entry.grab_focus()
+ 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_launcher(self):
+ def close_picker(self):
+ self.stack.children = []
+ self.selected_index = -1
self.launcher.close()
- def handle_search_input(self, text: str):
- self.emoji_manager.arrange_viewport(text)
-
-
-class EmojiManager:
- def __init__(self, launcher):
- self.launcher = launcher
- self.emoji_file_path = get_relative_path("../../../config/assets/emoji.json")
-
- def load_emojis(self) -> Generator[tuple, None, None]:
- try:
- with open(self.emoji_file_path, "r") as file:
- for emoji_str, item in ijson.kvitems(file, ""):
- yield emoji_str, item["name"], item["slug"], item["group"]
- except (ijson.JSONError, KeyError, OSError) as e:
- print(f"Error loading emojis: {e}")
- return
-
- def query_emojis(self, query: str) -> List[tuple]:
- query = query.lower()
- return [
- (emoji_str, name, slug, group)
- for emoji_str, name, slug, group in self.load_emojis()
- if query in name.lower() or query in slug.lower() or query in group.lower()
- ][:48]
-
- def copy_emoji(self, emoji: tuple):
- subprocess.run(["wl-copy"], input=emoji[0].encode(), check=True)
- self.launcher.close_launcher()
-
- def bake_emoji_slot(self, emoji: tuple, **kwargs) -> Button:
- return Button(
- name="emoji-item",
- child=Label(label=emoji[0], h_align="center"),
- tooltip_text=emoji[1],
- on_clicked=lambda *_: self.copy_emoji(emoji),
- **kwargs,
- )
+ 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 = ""):
- if not self.launcher.viewport:
+ 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
- (
- remove_handler(self.launcher._arranger_handler)
- if self.launcher._arranger_handler
- else None
- )
- self.launcher.viewport.children = []
+ 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
- filtered_emojis = self.query_emojis(query)
+ 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
- row = Box(name="emoji-row", spacing=10, orientation="h")
- for emoji in filtered_emojis:
- row.add(self.bake_emoji_slot(emoji))
- if len(row.children) >= 12:
- self.launcher.viewport.add(row)
- row = Box(name="emoji-row", spacing=10, orientation="h")
+ 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
- if row.children:
- self.launcher.viewport.add(row)
+ 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
new file mode 100644
index 0000000..8c2bf8a
--- /dev/null
+++ b/modules/launcher/components/kanban.py
@@ -0,0 +1,365 @@
+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
new file mode 100644
index 0000000..86ade48
--- /dev/null
+++ b/modules/launcher/components/notification_center.py
@@ -0,0 +1,409 @@
+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/old_cliphist.py b/modules/launcher/components/old_cliphist.py
new file mode 100644
index 0000000..2b65ae4
--- /dev/null
+++ b/modules/launcher/components/old_cliphist.py
@@ -0,0 +1,203 @@
+import os
+import subprocess
+from typing import List
+from fabric import Fabricator
+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
+from gi.repository import GLib
+import utils.icons as icons
+
+
+class Cliphist(Box):
+ def __init__(self, **kwargs):
+ super().__init__(
+ name="cliphist-launcher",
+ all_visible=False,
+ visible=False,
+ **kwargs,
+ )
+ self._arranger_handler = 0
+ self.cliphist_manager = CliphistManager(self)
+ self.viewport = None
+ self.launcher = kwargs["launcher"]
+
+ self.search_entry = Entry(
+ name="search-entry",
+ placeholder="Search Clipboard History...",
+ h_expand=True,
+ on_activate=self._on_search_activate,
+ )
+ self.search_entry.props.xalign = 0.5
+
+ self.header_box = Box(
+ name="header-box",
+ spacing=10,
+ orientation="h",
+ children=[
+ self.search_entry,
+ Button(
+ name="clear-button",
+ child=Label(name="clear-label", markup=icons.clear),
+ tooltip_text="Clear Clipboard History",
+ on_clicked=self.handle_clear_history,
+ ),
+ ],
+ )
+
+ self.launcher_box = Box(
+ name="cliphist-launcher-box",
+ spacing=10,
+ orientation="v",
+ h_expand=True,
+ children=[self.header_box],
+ )
+
+ self.add(self.launcher_box)
+
+ def _on_search_activate(self, entry, *args):
+ self.handle_search(entry.get_text())
+
+ def open_cliphist(self):
+ if not self.viewport:
+ self.viewport = Box(name="viewport", spacing=4, orientation="v")
+ self.scrolled_window = ScrolledWindow(
+ name="scrolled-window",
+ spacing=10,
+ min_content_size=(-1, -1),
+ h_scrollbar_policy="never",
+ child=self.viewport,
+ )
+ self.launcher_box.add(self.scrolled_window)
+
+ self.viewport.children = []
+ self.cliphist_manager.update_cliphist_history()
+
+ self.viewport.show()
+ self.search_entry.grab_focus()
+
+ def handle_search(self, text: str):
+ self.cliphist_manager.arrange_viewport(query=text)
+
+ def close_launcher(self):
+ self.launcher.close()
+
+ def handle_clear_history(self, *_):
+ GLib.spawn_command_line_async("cliphist wipe")
+ self.cliphist_manager.update_cliphist_history()
+
+
+class CliphistManager:
+ def __init__(self, launcher):
+ self.launcher = launcher
+ self.cliphist_history = []
+ self.wl_paste_watcher = Fabricator(
+ poll_from="wl-paste --watch echo", stream=True, interval=-1
+ )
+ self.wl_paste_watcher.connect("changed", self._on_wl_paste_changed)
+
+ def _on_wl_paste_changed(self, *args):
+ self.update_cliphist_history()
+
+ def get_clip_history(self) -> List[dict]:
+ try:
+ result = subprocess.run(
+ ["cliphist", "list"], capture_output=True, text=True, check=True
+ )
+ return self._parse_clip_history(result.stdout)
+ except subprocess.CalledProcessError as e:
+ print(f"Error fetching cliphist history: {e}")
+ return []
+
+ def _parse_clip_history(self, output: str) -> List[dict]:
+ items = []
+ for line in output.splitlines():
+ if line.strip():
+ parts = line.strip().split(maxsplit=1)
+ if len(parts) == 2:
+ items.append({"id": parts[0], "content": parts[1]})
+ return items
+
+ def copy_clip(self, clip_id: str):
+ try:
+ decode_result = subprocess.run(
+ ["cliphist", "decode", clip_id], capture_output=True, check=True
+ )
+ subprocess.run(["wl-copy"], input=decode_result.stdout, check=True)
+ self.launcher.close_launcher()
+ except subprocess.CalledProcessError as e:
+ print(f"Error copying clip {clip_id}: {e}")
+
+ def save_image_file(self, clip_id: str) -> str:
+ output_file = f"/tmp/cliphist-{clip_id}.png"
+ os.makedirs("/tmp", exist_ok=True)
+ try:
+ decode_result = subprocess.run(
+ ["cliphist", "decode", clip_id], capture_output=True, check=True
+ )
+ with open(output_file, "wb") as f:
+ f.write(decode_result.stdout)
+ return output_file
+ except (subprocess.CalledProcessError, IOError) as e:
+ print(f"Error saving image file for clip {clip_id}: {e}")
+ return ""
+
+ def update_cliphist_history(self):
+ self.cliphist_history = self.get_clip_history()
+ self.arrange_viewport()
+
+ def query_clips(self, query: str = "") -> List[dict]:
+ if not query.strip():
+ return self.cliphist_history
+ return [
+ clip
+ for clip in self.cliphist_history
+ if query.lower() in clip["content"].lower()
+ ]
+
+ def _on_clip_clicked(self, clip_id: str, *args):
+ self.copy_clip(clip_id)
+
+ def bake_clip_slot(self, item: dict) -> Button:
+ if "[[ binary data" in item["content"].lower():
+ return self._create_image_button(item)
+ return self._create_text_button(item)
+
+ def _create_image_button(self, item: dict) -> Button:
+ output_file = self.save_image_file(item["id"])
+ if output_file and os.path.exists(output_file):
+ button = Button(
+ h_align="start",
+ name="clip-item-img",
+ on_clicked=lambda _, clip_id=item["id"]: self.copy_clip(clip_id),
+ )
+ button.set_style(
+ f"background-image: url('{output_file}'); background-size: cover; background-position: center;"
+ )
+ return button
+ return None
+
+ def _create_text_button(self, item: dict) -> Button:
+ return Button(
+ child=Label(
+ label=item["content"][:50]
+ + ("..." if len(item["content"]) > 50 else ""),
+ h_expand=True,
+ v_align="center",
+ h_align="start",
+ ),
+ on_clicked=lambda _, clip_id=item["id"]: self.copy_clip(clip_id),
+ name="slot-button",
+ )
+
+ def arrange_viewport(self, query: str = ""):
+ if not self.launcher.viewport:
+ return
+ self.launcher.viewport.children = []
+ filtered_clips = self.query_clips(query)
+ for clip in filtered_clips:
+ button = self.bake_clip_slot(clip)
+ if button:
+ self.launcher.viewport.add(button)
diff --git a/modules/launcher/components/sh.py b/modules/launcher/components/sh.py
index 194b75b..6fef79a 100644
--- a/modules/launcher/components/sh.py
+++ b/modules/launcher/components/sh.py
@@ -6,12 +6,9 @@ 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
-
-CACHE_DIR = os.getenv(
- "XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache", "modus")
-)
-BINS = os.path.join(CACHE_DIR, "binaries")
+BINS = f"{data.CACHE_DIR}/binaries"
class Sh(Box):
@@ -29,10 +26,12 @@ class Sh(Box):
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,
@@ -49,7 +48,7 @@ class Sh(Box):
self.add(self.launcher_box)
- def open_launcher(self):
+ def open_sh(self):
if not self.viewport:
self.viewport = Box(name="viewport", spacing=10, orientation="v")
self.scrolled_window = ScrolledWindow(
@@ -112,7 +111,7 @@ class ShellCommandManager:
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))[:16]
+ return list(dict.fromkeys(filtered_bins))[:8]
def display_results(self, viewport, results: List[str]):
for command in results:
@@ -123,7 +122,7 @@ class ShellCommandManager:
h_align="start",
),
on_clicked=lambda _, cmd=command: self.execute_command(cmd),
- name="sh-item",
+ name="app-slot-button",
)
viewport.add(button)
diff --git a/modules/launcher/components/tmux.py b/modules/launcher/components/tmux.py
new file mode 100644
index 0000000..583e477
--- /dev/null
+++ b/modules/launcher/components/tmux.py
@@ -0,0 +1,540 @@
+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/todo.py b/modules/launcher/components/todo.py
deleted file mode 100644
index b34bc94..0000000
--- a/modules/launcher/components/todo.py
+++ /dev/null
@@ -1,170 +0,0 @@
-import json
-import os
-
-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 utils.icons as icons
-
-
-class TodoManager(Box):
- def __init__(self, **kwargs):
- super().__init__(
- name="todo-launcher",
- all_visible=False,
- visible=False,
- **kwargs,
- )
-
- self._arranger_handler = 0
- self.todo_manager = TodoListManager(self)
- self.viewport = None
-
- self.todo_entry = Entry(
- name="todo-entry",
- h_expand=True,
- on_activate=lambda entry, *_: self.handle_add_todo(entry.get_text()),
- )
-
- self.header_box = Box(
- name="header-box",
- spacing=10,
- orientation="h",
- children=[
- self.todo_entry,
- Button(
- name="add-button",
- child=Label(name="todo-add-button", markup=icons.add),
- tooltip_text="Add Todo",
- on_clicked=lambda *_: self.handle_add_todo(
- self.todo_entry.get_text()
- ),
- ),
- ],
- )
-
- self.launcher_box = Box(
- name="todo-launcher-box",
- spacing=10,
- orientation="v",
- h_expand=True,
- children=[self.header_box],
- )
-
- self.add(self.launcher_box)
-
- def open_launcher(self):
- if not self.viewport:
- self.viewport = Box(name="viewport", spacing=4, orientation="v")
- self.scrolled_window = ScrolledWindow(
- name="scrolled-window",
- spacing=10,
- min_content_size=(-1, -1),
- h_scrollbar_policy="never",
- child=self.viewport,
- )
- self.launcher_box.add(self.scrolled_window)
-
- self.viewport.children = []
- self.todo_manager.arrange_viewport()
-
- self.viewport.show()
- self.todo_entry.grab_focus()
-
- def handle_add_todo(self, text: str):
- if text.strip():
- self.todo_manager.add_todo(text)
- self.todo_entry.set_text("")
-
-
-class TodoListManager:
- def __init__(self, launcher):
- self.launcher = launcher
- self.todos = []
- self.todo_file = os.path.expanduser("~/.local/state/modus/todos.json")
- self.load_todos()
-
- def load_todos(self):
- os.makedirs(os.path.dirname(self.todo_file), exist_ok=True)
- try:
- if os.path.exists(self.todo_file):
- with open(self.todo_file, "r") as f:
- self.todos = json.load(f)
- except Exception as e:
- print(f"Error loading todos: {e}")
- self.todos = []
-
- def save_todos(self):
- try:
- with open(self.todo_file, "w") as f:
- json.dump(self.todos, f)
- except Exception as e:
- print(f"Error saving todos: {e}")
-
- def add_todo(self, content: str):
- todo = {"content": content, "completed": False, "id": len(self.todos)}
- self.todos.append(todo)
- self.save_todos()
- self.arrange_viewport()
-
- def toggle_todo(self, todo_id: int):
- for todo in self.todos:
- if todo["id"] == todo_id:
- todo["completed"] = not todo["completed"]
- break
- self.save_todos()
- self.arrange_viewport()
-
- def delete_todo(self, todo_id: int):
- self.todos = [todo for todo in self.todos if todo["id"] != todo_id]
- self.save_todos()
- self.arrange_viewport()
-
- def bake_todo_slot(self, todo: dict) -> Box:
- todo_box = Box(
- name="todo-item",
- spacing=10,
- orientation="h",
- h_expand=True,
- )
-
- checkbox = Button(
- name="checkbox",
- child=Label(
- name="todo-checkbox",
- markup=icons.check_box if todo["completed"] else icons.box,
- ),
- on_clicked=lambda _, todo_id=todo["id"]: self.toggle_todo(todo_id),
- )
-
- label = Label(
- label=todo["content"],
- h_expand=True,
- v_align="center",
- h_align="start",
- name="todo-label-completed" if todo["completed"] else "todo-label",
- )
-
- delete_button = Button(
- name="delete-button",
- child=Label(name="todo-delete-button", markup=icons.trash),
- on_clicked=lambda _, todo_id=todo["id"]: self.delete_todo(todo_id),
- )
-
- todo_box.add(checkbox)
- todo_box.add(label)
- todo_box.add(delete_button)
-
- return todo_box
-
- def arrange_viewport(self):
- if not self.launcher.viewport:
- return
-
- self.launcher.viewport.children = []
-
- for todo in self.todos:
- todo_slot = self.bake_todo_slot(todo)
- self.launcher.viewport.add(todo_slot)
diff --git a/modules/launcher/components/tools.py b/modules/launcher/components/tools.py
new file mode 100644
index 0000000..d04c39d
--- /dev/null
+++ b/modules/launcher/components/tools.py
@@ -0,0 +1,370 @@
+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
index b3eb956..d8d2197 100644
--- a/modules/launcher/components/wallpapers.py
+++ b/modules/launcher/components/wallpapers.py
@@ -1,33 +1,21 @@
-import hashlib, json, os, colorsys
-from PIL import Image
+import os
+import hashlib
+from gi.repository import GdkPixbuf, Gtk, GLib, Gio, Gdk
from fabric.widgets.box import Box
from fabric.widgets.centerbox import CenterBox
from fabric.widgets.entry import Entry
-from fabric.widgets.label import Label
from fabric.widgets.scrolledwindow import ScrolledWindow
-from gi.repository import GdkPixbuf, GLib, Gtk, Gio, Gdk
-from fabric.utils import get_relative_path, exec_shell_command_async
-from concurrent.futures import ThreadPoolExecutor, wait
-from utils import WALLPAPERS_DIR
+from fabric.widgets.label import Label
+from fabric.utils.helpers import exec_shell_command_async
+from PIL import Image
+import concurrent.futures
+from concurrent.futures import ThreadPoolExecutor
import utils.icons as icons
+import config.data as data
class WallpaperSelector(Box):
- CACHE_DIR = os.path.expanduser("~/.cache/modus/wallpapers")
- SETTINGS_FILE = get_relative_path("../../../config/assets/settings.json")
- CURRENT_WALLPAPER_FILE = os.path.expanduser("~/.cache/current_wallpaper")
- SCHEMES = {
- "TonalSpot": "tonalSpot",
- "Expressive": "expressive",
- "FruitSalad": "fruitSalad",
- "Monochrome": "monochrome",
- "Rainbow": "rainbow",
- "Vibrant": "vibrant",
- "Neutral": "neutral",
- "Fidelity": "fidelity",
- "Content": "content",
- }
- ITEM_WIDTH = 108
+ CACHE_DIR = f"{data.CACHE_DIR}/thumbnails"
def __init__(self, **kwargs):
super().__init__(
@@ -38,33 +26,44 @@ class WallpaperSelector(Box):
v_expand=False,
**kwargs,
)
- self.launcher = kwargs["launcher"]
os.makedirs(self.CACHE_DIR, exist_ok=True)
- self.files = [f for f in os.listdir(WALLPAPERS_DIR) if self._is_image(f)]
- self.thumbnails, self.thumbnail_queue = [], []
- self.executor = ThreadPoolExecutor(max_workers=4)
- self.selected_index, self.current_wallpaper = -1, None
- self._init_widgets()
- self._start_thumbnail_thread()
- self.setup_file_monitor()
- if os.path.exists(self.CURRENT_WALLPAPER_FILE):
- gfile = Gio.File.new_for_path(self.CURRENT_WALLPAPER_FILE)
- self.current_wp_monitor = gfile.monitor_file(
- Gio.FileMonitorFlags.NONE, None
- )
- self.current_wp_monitor.connect(
- "changed", self.on_current_wallpaper_changed
- )
- self.show_all()
- self.search_entry.grab_focus()
- def _init_widgets(self):
+ 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 possible 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(self.ITEM_WIDTH)
+ self.viewport.set_item_width(0)
self.viewport.connect("item-activated", self.on_wallpaper_selected)
+
self.scrolled_window = ScrolledWindow(
name="scrolled-window",
spacing=10,
@@ -72,117 +71,122 @@ class WallpaperSelector(Box):
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 display_name, scheme_id in sorted(self.SCHEMES.items()):
- self.scheme_dropdown.append(scheme_id, display_name)
- self.scheme_dropdown.set_active_id("tonalSpot")
+ 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)
- self.materialyoucolor_switcher = Gtk.Switch(name="materialyoucolor-switcher")
- self.materialyoucolor_switcher.set_vexpand(False)
- self.materialyoucolor_switcher.set_hexpand(False)
- self.materialyoucolor_switcher.set_valign(Gtk.Align.CENTER)
- self.materialyoucolor_switcher.set_halign(Gtk.Align.CENTER)
- self.materialyoucolor_switcher.set_active(True)
+
+ # 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(True)
+
self.mat_icon = Label(name="mat-label", markup=icons.palette)
- self.dropdown_box = Box(name="dropdown-box", children=[self.scheme_dropdown])
- self.custom_color_entry = Entry(
- name="entry-custom-color",
- h_expand=True,
- notify_text=lambda entry, *_: self.on_custom_color_submitted(entry),
- on_key_press_event=self.on_custom_color_key_press,
- )
+
+ # Add the switcher to the header_box's start_children
self.header_box = CenterBox(
name="header-box",
- spacing=10,
+ spacing=8,
orientation="h",
- start_children=[self.materialyoucolor_switcher, self.mat_icon],
- center_children=[
- self.search_entry,
- Label(label="Custom Color:"),
- self.custom_color_entry,
- ],
- end_children=[self.dropdown_box],
+ start_children=[self.matugen_switcher, self.mat_icon],
+ center_children=[self.search_entry],
+ end_children=[self.scheme_dropdown],
)
+
self.add(self.header_box)
self.add(self.scrolled_window)
+ self._start_thumbnail_thread()
+ 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(WALLPAPERS_DIR)
+ 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_current_wallpaper_changed(self, monitor, file, other_file, event_type):
- if event_type in (Gio.FileMonitorEvent.CHANGED, Gio.FileMonitorEvent.CREATED):
- try:
- with open(self.CURRENT_WALLPAPER_FILE, "r") as f:
- new_wp = f.read().strip()
- if new_wp and new_wp != self.current_wallpaper:
- self.current_wallpaper = new_wp
- if self.custom_color_entry.get_text().strip().lower() in [
- "",
- "none",
- ]:
- scheme = self.scheme_dropdown.get_active_id()
- color_generator = get_relative_path(
- "../../../config/material-colors/generate.py"
- )
- command = f'python -O {color_generator} --scheme "{scheme}" --image "{self.current_wallpaper}"'
- self._run_command(command)
- except Exception as e:
- print(f"Error updating current wallpaper: {e}")
-
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)
- self._delete_cache(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())
- return
- if event_type in (
- Gio.FileMonitorEvent.CREATED,
- Gio.FileMonitorEvent.CHANGED,
- ) and self._is_image(file_name):
- if (
- event_type == Gio.FileMonitorEvent.CREATED
- and file_name not in self.files
- ):
- self.files.append(file_name)
- self.files.sort()
- elif event_type == Gio.FileMonitorEvent.CHANGED and file_name in self.files:
- self._delete_cache(file_name)
- self.executor.submit(self._process_file, file_name)
-
- def _delete_cache(self, file_name: str):
- cache_path = self._get_cache_path(file_name)
- if os.path.exists(cache_path):
- try:
- os.remove(cache_path)
- except Exception:
- pass
+ 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()
- for pixbuf, file_name in sorted(
- [
- (thumb, name)
- for thumb, name in self.thumbnails
- if query.casefold() in name.casefold()
- ],
- key=lambda x: x[1].lower(),
- ):
+ 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 not query.strip():
+ # 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:
@@ -191,39 +195,40 @@ class WallpaperSelector(Box):
def on_wallpaper_selected(self, iconview, path):
model = iconview.get_model()
file_name = model[path][1]
- full_path = os.path.join(WALLPAPERS_DIR, file_name)
+ full_path = os.path.join(data.WALLPAPERS_DIR, file_name)
selected_scheme = self.scheme_dropdown.get_active_id()
- wallpaper_script = get_relative_path("../../../config/scripts/wallpaper.py")
- if self.materialyoucolor_switcher.get_active():
- command = f"python -O {wallpaper_script} -I {full_path}"
- GLib.spawn_command_line_async(command)
- self.update_scheme(selected_scheme)
+ 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:
- cmd = f"swww img {full_path} -t outer --transition-duration 1.5 --transition-step 255 --transition-fps 60 -f Nearest"
- exec_shell_command_async(cmd)
+ # 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):
- scheme_list = [
- scheme_id for _, scheme_id in sorted(self.SCHEMES.items())
- ]
- try:
- current_index = scheme_list.index(
- self.scheme_dropdown.get_active_id()
- )
- except ValueError:
- current_index = 0
+ 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(scheme_list)
+ (current_index - 1) % len(schemes_list)
if event.keyval == Gdk.KEY_Up
- else (current_index + 1) % len(scheme_list)
+ 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
@@ -239,6 +244,7 @@ class WallpaperSelector(Box):
total_items = len(model)
if total_items == 0:
return
+
if self.selected_index == -1:
new_index = (
0 if keyval in (Gdk.KEY_Down, Gdk.KEY_Right) else total_items - 1
@@ -246,39 +252,45 @@ class WallpaperSelector(Box):
else:
current_index = self.selected_index
allocation = self.viewport.get_allocation()
- columns = max(1, allocation.width // self.ITEM_WIDTH)
- new_index = current_index + (
- 1
- if keyval == Gdk.KEY_Right
- else -1
- if keyval == Gdk.KEY_Left
- else columns
- if keyval == Gdk.KEY_Down
- else -columns
- )
- new_index = max(0, min(new_index, total_items - 1))
+ item_width = 108 # Approximate item width including margins
+ columns = max(1, allocation.width // item_width)
+ if keyval == Gdk.KEY_Right:
+ new_index = current_index + 1
+ elif keyval == Gdk.KEY_Left:
+ new_index = current_index - 1
+ elif keyval == Gdk.KEY_Down:
+ new_index = current_index + columns
+ elif keyval == Gdk.KEY_Up:
+ new_index = current_index - columns
+ if new_index < 0:
+ new_index = 0
+ if new_index >= total_items:
+ new_index = total_items - 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)
+ 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):
- GLib.Thread.new("thumbnail-loader", self._preload_thumbnails, None)
+ 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
]
- wait(futures)
+ concurrent.futures.wait(futures)
GLib.idle_add(self._process_batch)
def _process_file(self, file_name):
- full_path = os.path.join(WALLPAPERS_DIR, 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:
@@ -287,10 +299,13 @@ class WallpaperSelector(Box):
side = min(width, height)
left = (width - side) // 2
top = (height - side) // 2
- img_cropped = img.crop((left, top, left + side, top + side))
+ 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:
+ 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)
@@ -313,110 +328,13 @@ class WallpaperSelector(Box):
file_hash = hashlib.md5(file_name.encode("utf-8")).hexdigest()
return os.path.join(self.CACHE_DIR, f"{file_hash}.png")
- def on_scheme_changed(self, combo):
- scheme_id = combo.get_active_id()
- display_name = next(
- name for name, id in self.SCHEMES.items() if id == scheme_id
- )
- self.update_scheme(scheme_id)
- color_generator = get_relative_path(
- "../../../config/material-colors/generate.py"
- )
- command = f'python -O {color_generator} -R --scheme "{scheme_id}"'
- self._run_command(
- command,
- success_message=f"Applied color scheme: {display_name}",
- failure_message="Failed to apply color scheme:",
- )
-
- def update_scheme(self, scheme: str):
- self._update_settings_field("generation-scheme", scheme)
-
- def on_custom_color_submitted(self, entry: Entry):
- color = entry.get_text().strip()
- if not color:
- self.update_custom_color("")
- return
- if self.is_valid_hex_color(color):
- color_value = color
- elif self.is_valid_hue(color):
- color_value = self.hue_to_hex(float(color))
- else:
- return
- self.update_custom_color(color_value)
-
- def on_custom_color_key_press(self, widget, event):
- if event.keyval == Gdk.KEY_Return:
- self.on_custom_color_submitted(widget)
- return True
- return False
-
- def _fetch_current_wallpaper(self):
- if not self.current_wallpaper or not os.path.exists(self.current_wallpaper):
- if os.path.exists(self.CURRENT_WALLPAPER_FILE):
- try:
- with open(self.CURRENT_WALLPAPER_FILE, "r") as f:
- self.current_wallpaper = f.read().strip()
- except Exception as e:
- print(f"Error reading current wallpaper: {e}")
- return self.current_wallpaper
-
- def update_custom_color(self, color: str):
- color = color if color and color.lower() != "none" else "none"
- self._update_settings_field("custom-color", color)
- color_generator = get_relative_path(
- "../../../config/material-colors/generate.py"
- )
- if color == "none":
- scheme = self.scheme_dropdown.get_active_id()
- current_wp = self._fetch_current_wallpaper()
- command = f'python -O {color_generator} --scheme "{scheme}" --image "{current_wp}"'
- else:
- command = f'python -O {color_generator} --color "{color}"'
- self._run_command(command)
-
@staticmethod
def _is_image(file_name: str) -> bool:
return file_name.lower().endswith(
(".png", ".jpg", ".jpeg", ".bmp", ".gif", ".webp")
)
- @staticmethod
- def is_valid_hex_color(value: str) -> bool:
- return (
- value.startswith("#")
- and len(value) == 7
- and all(c in "0123456789ABCDEFabcdef" for c in value[1:])
- )
-
- @staticmethod
- def is_valid_hue(value: str) -> bool:
- try:
- return 0 <= float(value) <= 360
- except ValueError:
- return False
-
- def hue_to_hex(self, hue: float) -> str:
- r, g, b = colorsys.hls_to_rgb(hue / 360.0, 0.5, 1.0)
- return f"#{int(r * 255):02x}{int(g * 255):02x}{int(b * 255):02x}"
-
- def _update_settings_field(self, field: str, value):
- try:
- with open(self.SETTINGS_FILE, "r") as f:
- settings = json.load(f)
- settings[field] = value
- with open(self.SETTINGS_FILE, "w") as f:
- json.dump(settings, f, indent=2)
- except Exception:
- pass
-
- def _run_command(
- self, command: str, success_message: str = None, failure_message: str = None
- ):
- try:
- GLib.spawn_command_line_async(command)
- if success_message:
- print(success_message)
- except Exception:
- if failure_message:
- print(failure_message)
+ def on_search_entry_focus_out(self, widget, event):
+ if self.get_mapped():
+ widget.grab_focus()
+ return False
diff --git a/modules/launcher/components/wifi.py b/modules/launcher/components/wifi.py
index 657d56c..94e52d6 100644
--- a/modules/launcher/components/wifi.py
+++ b/modules/launcher/components/wifi.py
@@ -4,12 +4,12 @@ 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 NetworkClient
+from services import network_client
import utils.icons as icons
class WifiNetworkSlot(CenterBox):
- def __init__(self, network, client: NetworkClient, **kwargs):
+ def __init__(self, network, client: network_client, **kwargs):
super().__init__(name="wifi-device", **kwargs)
self.network = network
self.client = client
@@ -81,7 +81,7 @@ class WifiManager(Box):
**kwargs,
)
- self.client = NetworkClient()
+ 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)
diff --git a/modules/launcher/components/window_switcher.py b/modules/launcher/components/window_switcher.py
new file mode 100644
index 0000000..c2ed906
--- /dev/null
+++ b/modules/launcher/components/window_switcher.py
@@ -0,0 +1,312 @@
+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
index ca99e66..4704e75 100644
--- a/modules/launcher/launcher.py
+++ b/modules/launcher/launcher.py
@@ -5,15 +5,19 @@ from fabric.widgets.box import Box
from modules.launcher.components import (
AppLauncher,
BluetoothConnections,
- Cliphist,
- Emoji,
+ EmojiPicker,
PowerMenu,
Sh,
- TodoManager,
WallpaperSelector,
WifiManager,
Calendar,
Dashboard,
+ Toolbox,
+ Kanban,
+ TmuxManager,
+ WindowSwitcher,
+ NotificationCenter,
+ ClipHistory,
)
@@ -22,6 +26,8 @@ class Launcher(Window):
super().__init__(
layer="top",
anchor="center",
+ keyboard_mode="none",
+ exclusivity="normal",
visible=False,
all_visible=False,
**kwargs,
@@ -30,13 +36,17 @@ class Launcher(Window):
self.dashboard = Dashboard(launcher=self)
self.wallpapers = WallpaperSelector(launcher=self)
self.power = PowerMenu(launcher=self)
- self.emoji = Emoji(launcher=self)
- self.cliphist = Cliphist(launcher=self)
- self.todo = TodoManager()
+ 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(
@@ -59,11 +69,15 @@ class Launcher(Window):
self.power,
self.emoji,
self.cliphist,
- self.todo,
self.bluetooth,
self.sh,
self.wifi,
self.calendar,
+ self.tools,
+ self.kanban,
+ self.tmux,
+ self.window_switcher,
+ self.notification_center,
],
)
@@ -77,10 +91,7 @@ class Launcher(Window):
self.add(self.launcher_box)
self.show_all()
self.hide()
- self.add_keybinding("Escape", self._on_escape)
-
- def _on_escape(self, *args):
- return self.close()
+ self.add_keybinding("Escape", lambda *_: self.close())
def close(self):
self.set_keyboard_mode("none")
@@ -92,11 +103,15 @@ class Launcher(Window):
self.power,
self.emoji,
self.cliphist,
- self.todo,
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()
@@ -107,32 +122,39 @@ class Launcher(Window):
"power",
"emoji",
"cliphist",
- "todo",
"bluetooth",
"sh",
"wifi",
"calendar",
+ "tools",
+ "kanban",
+ "tmux",
+ "window-switcher",
+ "notification-center",
]:
self.stack.remove_style_class(style)
return True
def open(self, widget):
- self.set_keyboard_mode("exclusive")
- self.show()
-
widgets = {
"launcher": self.launcher,
"wallpapers": self.wallpapers,
"power": self.power,
"emoji": self.emoji,
"cliphist": self.cliphist,
- "todo": self.todo,
"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()
@@ -156,22 +178,24 @@ class Launcher(Window):
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_launcher()
+ self.emoji.open_picker()
self.emoji.search_entry.set_text("")
self.emoji.search_entry.grab_focus()
elif widget == "cliphist":
- self.cliphist.open_launcher()
- self.cliphist.search_entry.set_text("")
- self.cliphist.search_entry.grab_focus()
-
- elif widget == "todo":
- self.todo.open_launcher()
- self.todo.todo_entry.set_text("")
- self.todo.todo_entry.grab_focus()
+ self.cliphist.open()
elif widget == "sh":
- self.sh.open_launcher()
- self.sh.search_entry.set_text("")
- self.sh.search_entry.grab_focus()
+ 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
index 35982ff..82dcb5d 100644
--- a/modules/notification_popup.py
+++ b/modules/notification_popup.py
@@ -4,7 +4,6 @@ from fabric.notifications import (
Notification,
NotificationAction,
NotificationCloseReason,
- Notifications,
)
from loguru import logger
from fabric.widgets.box import Box
@@ -16,6 +15,7 @@ from fabric.widgets.revealer import Revealer
from fabric.widgets.wayland import WaylandWindow as Window
from utils import CustomImage
import utils.icons as icons
+from services import notification_service
class ActionButton(Button):
@@ -267,7 +267,8 @@ class NotificationRevealer(Revealer):
class NotificationPopup(Window):
def __init__(self):
- self._server = Notifications()
+ self._server = notification_service
+ self.cache_notification_service = notification_service
self.notifications = Box(
v_expand=True,
h_expand=True,
@@ -287,6 +288,15 @@ class NotificationPopup(Window):
)
def on_new_notification(self, fabric_notif, id):
- new_box = NotificationRevealer(fabric_notif.get_notification_from_id(id))
- self.notifications.add(new_box)
- new_box.set_reveal_child(True)
+ 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
index d941046..6d79a60 100644
--- a/modules/osd.py
+++ b/modules/osd.py
@@ -1,111 +1,183 @@
-from fabric.widgets.wayland import WaylandWindow as Window
-from fabric.widgets.label import Label
+import time
+from typing import ClassVar
+
+from fabric.audio import Audio
from fabric.widgets.box import Box
-from gi.repository import GLib
-from fabric.widgets.centerbox import CenterBox
-from services import brightness, audio
+from fabric.widgets.scale import Scale, ScaleMark
+from fabric.widgets.wayland import WaylandWindow as Window
+from fabric.utils import invoke_repeater
+from gi.repository import GObject
+from services.brightness import Brightness
+from fabric.widgets.label import Label
+from utils import Animator
-def create_progress_bar(percentage, width=150):
- container = Box(orientation="h", name="progress-container")
- progress = Box(
- name="progress-fill", style=f"min-width: {percentage * width / 100}px;"
- )
- background = Box(name="progress-background", style=f"min-width: {width}px;")
- background.children = [progress]
- container.children = [background]
- return container
+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()
-def update_progress_bar(progress_bar, percentage, width=150):
- progress_fill = progress_bar.children[0].children[0]
- progress_fill.set_style(f"min-width: {percentage * width / 100}px;")
+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)))
-def create_labeled_progress(label_text, value_text, percentage):
- label = Label(name="osd-label", markup=label_text)
- value = Label(name="osd-value", markup=value_text)
- header = CenterBox(orientation="h", start_children=[label], end_children=[value])
- progress = create_progress_bar(percentage)
- return Box(
- name="control-section", orientation="v", spacing=5, children=[header, progress]
- )
+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
-def get_brightness():
- return (
- round((brightness.screen_brightness / brightness.max_screen) * 100)
- if brightness.screen_brightness
- else 0
- )
+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()
-def get_volume():
- return round(audio.speaker.volume) if audio.speaker else 0
+ 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):
+ 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__(
- name="osd-menu",
layer="overlay",
anchor="top",
- margin="10px 0 0 0",
- keyboard_mode="on-demand",
+ child=self.controls_box,
visible=False,
- style_classes="osd-panel",
+ pass_through=True,
+ keyboard_mode="on-demand",
+ **kwargs,
)
- self.last_volume = get_volume()
- self.last_brightness = get_brightness()
- self.volume_container = create_labeled_progress(
- "Speaker", str(self.last_volume), self.last_volume
+
+ 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.brightness_container = create_labeled_progress(
- "Brightness", str(self.last_brightness), self.last_brightness
- )
- self.children = Box(
- orientation="h",
- spacing=10,
- children=[self.brightness_container, self.volume_container],
- )
- self.hide_timeout_id = None
- self._connect_signals()
- GLib.timeout_add(100, self._check_changes)
+ self.audio_container.connect("volume-changed", self.show_controls)
- def _connect_signals(self):
- audio.connect("notify::speaker", self._update_volume)
- brightness.connect("screen", self._update_brightness)
+ invoke_repeater(100, self.check_inactivity, initial_call=True)
- def _update_volume(self, *_):
- volume = get_volume()
- self.volume_container.children[0].end_children[0].set_markup(str(volume))
- update_progress_bar(self.volume_container.children[1], volume)
- self._reset_timeout()
+ def show_controls(self, *_):
+ self.set_visible(True)
+ self.reset_inactivity_timer()
- def _update_brightness(self, *_):
- brightness_level = get_brightness()
- self.brightness_container.children[0].end_children[0].set_markup(
- str(brightness_level)
- )
- update_progress_bar(self.brightness_container.children[1], brightness_level)
- self._reset_timeout()
+ def start_hide_timer(self):
+ self.set_visible(False)
- def _reset_timeout(self):
- if self.hide_timeout_id:
- GLib.source_remove(self.hide_timeout_id)
- self.hide_timeout_id = GLib.timeout_add(1500, self._hide_osd)
+ def reset_inactivity_timer(self):
+ self.last_activity_time = time.time()
- def _hide_osd(self):
- self.hide()
- self.hide_timeout_id = None
- return False
-
- def _check_changes(self):
- volume = get_volume()
- brightness_level = get_brightness()
- if volume != self.last_volume or brightness_level != self.last_brightness:
- self.last_volume, self.last_brightness = volume, brightness_level
- self.show_all()
- self._update_volume()
- self._update_brightness()
+ 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
index 3d7d12d..cdba666 100644
--- a/services/__init__.py
+++ b/services/__init__.py
@@ -1,9 +1,10 @@
from fabric.audio import Audio
from .brightness import Brightness
-from .screen_record import ScreenRecorder
-from .network import *
-
+from .network import NetworkClient
+from .notification import *
+from .powerprofile import *
+network_client = NetworkClient()
brightness = Brightness()
audio = Audio()
-sc = ScreenRecorder()
+notification_service = CustomNotifications()
diff --git a/services/notification.py b/services/notification.py
new file mode 100644
index 0000000..4da918b
--- /dev/null
+++ b/services/notification.py
@@ -0,0 +1,226 @@
+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
new file mode 100644
index 0000000..cccf03e
--- /dev/null
+++ b/services/powerprofile.py
@@ -0,0 +1,88 @@
+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/styles/bar.css b/styles/bar.css
index 5cca825..6f4993c 100644
--- a/styles/bar.css
+++ b/styles/bar.css
@@ -1,254 +1,107 @@
#bar {
- padding: 0px 7px 0px 7px;
+ 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 */
}
-#bar.start-container {
+#start-container,
+#center-container,
+#end-container {
padding-top: 2px;
padding-bottom: 2px;
- border-bottom-right-radius: 15px;
}
-#bar.end-container {
- padding-top: 2px;
- padding-bottom: 2px;
- border-bottom-left-radius: 15px;
-}
-
-#center-bar {
- background-color: transparent;
-}
-
-#bar,
#bar-corner * {
- background-color: lighter(@background);
+ background-color: var(--surface);
}
-#bar.hidden {
- opacity: 0;
- transition: all 0.5s cubic-bezier(0.5, 0.25, 0, 1);
-}
-
-#bar.visible {
- opacity: 1;
- transition: all 0.5s cubic-bezier(0.5, 0.25, 0, 1);
-}
-
-/*
- * ── button bar ────────────────────────────────────────────────────────
-*/
-
-#button-bar {
- background-color: @surfaceContainer;
- margin-top: 5px;
- margin-bottom: 5px;
- padding: 0px 1px 0px 1px;
- border-radius: 8px;
- min-width: 24px;
- min-height: 24px;
- transition: all 0.1s ease;
-}
-
-#button-bar-label {
- color: @primary;
- font-size: 20px;
- border-radius: 8px;
- transition: all 0.1s ease;
-}
-
-#button-bar:hover #button-bar-label {
- background-color: @surfaceBright;
- box-shadow: 0px 3px 3px -2px rgba(0, 0, 0, 0.2);
-}
-
-#button-bar:active #button-bar-label {
- color: @shadow;
- background-color: @primary;
-}
-
-/*
- * ── workspace ─────────────────────────────────────────────────────────
-*/
-
+/**
+ * ── Workspace ─────────────────────────────────────────────────────────
+ */
#workspaces {
font-style: normal;
- background: @surfaceContainer;
- margin-top: 5px;
- margin-bottom: 5px;
- padding: 0px 8px 0px;
+ background-color: var(--surface-container);
+ padding: 4px;
+ margin-top: 1px;
+ margin-bottom: 1px;
border-radius: 8px;
- min-width: 24px;
- min-height: 24px;
}
-/*
- * ── update ────────────────────────────────────────────────────────────
-*/
+/**
+ * ── 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: @primary;
- font-size: 22px;
+ color: var(--primary);
+ font-size: 20px;
padding: 1px;
border-radius: 11px;
transition: all 0.1s ease;
}
#updated-icon {
- color: @primary;
+ color: var(--primary);
font-size: 20px;
padding: 4px;
- border-radius: 99px;
+ border-radius: 8px;
transition: all 0.1s ease;
}
#update-label {
- color: @primary;
+ color: var(--primary);
font-size: 10px;
font-weight: bold;
padding: 0px;
margin-right: 4px;
- border-radius: 11px;
+ border-radius: 8px;
transition: all 0.1s ease;
}
-/*
+/**
* ── Clock ─────────────────────────────────────────────────────────────
-*/
-
+ */
#datetime {
- background-color: @surfaceContainer;
- margin-top: 5px;
- margin-bottom: 5px;
- padding: 0px 8px 0px;
+ background-color: var(--surface-container);
+ margin-top: 1px;
+ margin-bottom: 1px;
+ padding: 4px;
border-radius: 8px;
- min-width: 24px;
- min-height: 24px;
}
-/*
- * ── battery ───────────────────────────────────────────────────────────
-*/
-
-#battery {
- background-color: @surfaceContainer;
- margin-top: 5px;
- margin-bottom: 5px;
- padding: 0px 8px 0px;
- border-radius: 8px;
- min-width: 24px;
- min-height: 24px;
-}
-
-#battery-circle {
- color: @surfaceBright;
- border: 3px solid @primary;
-}
-
-#battery-circle.alert {
- border: 2px solid @error;
-}
-
-#battery-icon {
- font-size: 20px;
-}
-
-#battery-icon.alert {
- color: @onError;
-}
-
-#battery-save,
-#battery-balanced,
-#battery-performance {
- min-width: 30px;
- min-height: 28px;
- margin: 2px 0px;
- border-radius: 100px;
-}
-
-#battery-save-label,
-#battery-balanced-label,
-#battery-performance-label {
- color: @outline;
- font-size: 20px;
-}
-
-#battery-save:hover,
-#battery-balanced:hover,
-#battery-performance:hover {
- background-color: @surfaceBright;
-}
-
-#battery-save:hover #battery-save-label,
-#battery-balanced:hover #battery-balanced-label,
-#battery-performance:hover #battery-performance-label {
- color: @primary;
-}
-
-#battery-save.active,
-#battery-balanced.active,
-#battery-performance.active {
- background-color: @primary;
-}
-
-#battery-save.active #battery-save-label,
-#battery-balanced.active #battery-balanced-label,
-#battery-performance.active #battery-performance-label {
- color: @shadow;
-}
-
-#battery-level {
- color: @primary;
- font-weight: bold;
- margin: 0 4px;
- font-size: 11px;
-}
-
-/*
- * ── metric ────────────────────────────────────────────────────────
-*/
-
-#metrics {
- background-color: @surfaceContainer;
- margin-top: 5px;
- margin-bottom: 5px;
- padding: 0px 8px 0px;
- border-radius: 8px;
- min-width: 24px;
- min-height: 24px;
-}
-
-#metric-icon {
- font-size: 20px;
-}
-
-#metric-circle {
- color: @surfaceBright;
- border: 3px solid @primary;
-}
-
-/*
- * ── tray ──────────────────────────────────────────────────────────────
-*/
-
-#tray-revealer {
- background-color: @surfaceContainer;
- margin-top: 5px;
- margin-bottom: 5px;
- padding: 0px 2px 0px;
- border-radius: 8px;
- min-width: 24px;
- min-height: 24px;
- font-size: 20px;
+#datetime:hover {
+ background-color: var(--surface-bright);
}
+/**
+ * ── Tray ──────────────────────────────────────────────────────────────
+ */
#tray {
- background-color: @surfaceContainer;
- margin-top: 5px;
- margin-left: 4px;
- margin-bottom: 5px;
- padding: 0px 8px 0px;
+ background-color: var(--surface-container);
+ margin-top: 1px;
+ margin-bottom: 1px;
+ padding: 4px;
border-radius: 8px;
- min-width: 24px;
- min-height: 24px;
}
#tray > button {
@@ -257,9 +110,9 @@
}
menu {
- border: solid 1px @outlineVariant;
+ border: solid 1px var(--outline-variant);
border-radius: 16px;
- background-color: @surface;
+ background-color: var(--surface);
padding: 6px;
}
@@ -269,101 +122,93 @@ menu > menuitem {
}
menu > menuitem:hover {
- background-color: @surfaceBright;
+ background-color: var(--surface-bright);
}
menu > menuitem:hover > label {
- color: @onSurface;
+ color: var(--on-surface);
}
-/*
- * ── logo ──────────────────────────────────────────────────────────────
-*/
-#logo {
- background-color: @surfaceContainer;
- margin-top: 5px;
- margin-bottom: 5px;
- padding: 0px 8px 0px;
+/**
+ * ── Logo ──────────────────────────────────────────────────────────────
+ */
+#button-bar {
+ background-color: var(--surface-container);
+ margin-top: 1px;
+ padding: 4px;
border-radius: 8px;
- min-width: 24px;
- min-height: 24px;
+ margin-bottom: 1px;
+ transition: all 0.1s ease;
}
-#logo-name {
- color: darker(@primary);
-}
-
-#logo:hover {
- background-color: @surfaceBright;
- box-shadow: 0px 3px 3px -2px rgba(0, 0, 0, 0.2);
-}
-
-/*
- * ── status indicator ───────────────────────────────────────────────────────────
-*/
-
-#system-indicators {
- background-color: @surfaceContainer;
- margin-top: 5px;
- margin-bottom: 5px;
- padding: 0px 8px 0px;
+#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;
- min-width: 24px;
- min-height: 24px;
}
#system-indicator-icon {
font-size: 20px;
}
-/*
- * ── taskbar ───────────────────────────────────────────────────────────
-*/
-
-#taskbar {
- border-radius: 100px;
- color: @onSurface;
- background: @surfaceContainer;
- padding-left: 4px;
- padding-right: 4px;
+#system-notif-label {
+ font-size: 18px;
}
-#taskbar > button.activated {
- border-radius: 100px;
- background: @surfaceVariant;
-}
-
-#taskbar > button:hover {
- background: @surfaceBright;
-}
-
-#taskbar > button {
- padding: 0px 4px;
- margin: 4px 0px;
- border-radius: 100px;
- border: 0px;
-}
-
-#taskbar > button:last-child {
- margin-right: 0px;
-}
-
-/*
- * ── recording indicator ───────────────────────────────────────────────
-*/
-
-#recorder {
- color: @error;
- margin-top: 5px;
- margin-bottom: 5px;
- padding: 0px 6px 0px;
+/**
+ * ── Metric ────────────────────────────────────────────────────────────
+ */
+#metrics {
+ background-color: var(--surface-container);
border-radius: 8px;
- font-size: 20px;
- animation: error 2s infinite;
}
-@keyframes error {
- to {
- background-color: @onError;
- }
+#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
new file mode 100644
index 0000000..11d719c
--- /dev/null
+++ b/styles/colors.css
@@ -0,0 +1,44 @@
+:vars {
+ --foreground: #dde4e2;
+ --background: #0e1514;
+ --cursor: #dde4e2;
+ --primary: #81d5ca;
+ --primary-container: #00504a;
+ --on-primary: #003732;
+ --on-primary-container: #9df2e6;
+ --secondary: #b1ccc7;
+ --on-secondary: #1c3531;
+ --secondary-container: #324b48;
+ --tertiary: #aec9e6;
+ --tertiary-container: #2e4961;
+ --on-tertiary: #163349;
+ --on-tertiary-container: #cde5ff;
+ --surface: #0e1514;
+ --surface-container: #1a2120;
+ --surface-bright: #343a39;
+ --surface-variant: #3f4947;
+ --surface-container-low: #161d1c;
+ --on-surface: #dde4e2;
+ --error: #ffb4ab;
+ --error-dim: #ff8678;
+ --on-error: #690005;
+ --error-container: #93000a;
+ --on-error-container: #ffdad6;
+ --outline: #899390;
+ --outline-variant: #3f4947;
+ --shadow: #000000;
+ --red: #ffb595;
+ --red-dim: #ff9162;
+ --green: #95d5a7;
+ --green-dim: #70c789;
+ --yellow: #b8cf84;
+ --yellow-dim: #a3c15f;
+ --blue: #afc6ff;
+ --blue-dim: #7ca2ff;
+ --magenta: #e4b7f3;
+ --magenta-dim: #d48bec;
+ --cyan: #80d5cf;
+ --cyan-dim: #5ac8c1;
+ --white: #82d3e0;
+
+}
diff --git a/styles/dashboard.css b/styles/dashboard.css
index 8792c72..6d99fc2 100644
--- a/styles/dashboard.css
+++ b/styles/dashboard.css
@@ -5,6 +5,7 @@
#network-button,
#bluetooth-button,
#night-mode-button,
+#power-profile-button,
#dark-mode-button,
#caffeine-button {
border-radius: 20px;
@@ -18,28 +19,30 @@
#night-mode-button,
#dark-mode-button,
+#power-profile-button,
#caffeine-button {
- background-color: @primary;
+ background-color: var(--primary);
padding: 0 10px;
}
#night-mode-button.disabled,
#dark-mode-button.disabled,
+#power-profile-button.disabled,
#caffeine-button.disabled {
- background-color: @surface;
+ background-color: var(--surface);
}
#network-status-button,
#network-menu-button,
#bluetooth-status-button,
#bluetooth-menu-button {
- background-color: @primary;
+ background-color: var(--primary);
padding: 0 8px;
}
#network-menu-button,
#bluetooth-menu-button {
- border-left: 1px solid @outline;
+ border-left: 1px solid var(--outline);
min-width: 24px;
}
@@ -49,9 +52,10 @@
#bluetooth-menu-button:hover,
#bluetooth-button:hover,
#night-mode-button:hover,
+#power-profile-button:hover,
#dark-mode-button:hover,
#caffeine-button:hover {
- background-color: @foreground;
+ background-color: var(--foreground);
}
/* Estados Disabled */
@@ -59,12 +63,12 @@
#network-menu-button.disabled,
#bluetooth-status-button.disabled,
#bluetooth-menu-button.disabled {
- background-color: @surface;
+ background-color: var(--surface);
}
#network-menu-button.disabled,
#bluetooth-menu-button.disabled {
- border-left: 1px solid @surfaceBright;
+ border-left: 1px solid var(--surface-bright);
}
#network-status-button.disabled:hover,
@@ -72,9 +76,10 @@
#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: @surfaceBright;
+ background-color: var(--surface-bright);
}
#network-status-button,
@@ -91,36 +96,39 @@
#bluetooth-icon,
#night-mode-icon,
#dark-mode-icon,
+#power-profile-icon,
#caffeine-icon {
- color: @shadow;
+ 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: @outline;
+ color: var(--outline);
}
#network-menu-label,
#bluetooth-menu-label {
- color: @shadow;
+ color: var(--shadow);
font-size: 16px;
}
#network-menu-label.disabled,
#bluetooth-menu-label.disabled {
- color: @outline;
+ color: var(--outline);
}
#network-label,
#bluetooth-label,
#night-mode-label,
+#power-profile-label,
#caffeine-label,
#dark-mode-label {
- color: @shadow;
+ color: var(--shadow);
font-size: 14px;
font-weight: bold;
margin-bottom: -4px;
@@ -129,24 +137,27 @@
#network-label.disabled,
#bluetooth-label.disabled,
#night-mode-label.disabled,
+#power-profile-label.disabled,
#dark-mode-label.disabled,
#caffeine-label.disabled {
- color: @outline;
+ color: var(--outline);
}
#network-ssid,
#bluetooth-status,
#night-mode-status,
+#power-profile-status,
#dark-mode-status,
#caffeine-status {
- color: @surfaceBright;
+ 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: @outline;
+ color: var(--outline);
}
diff --git a/styles/dock.css b/styles/dock.css
new file mode 100644
index 0000000..c0a9860
--- /dev/null
+++ b/styles/dock.css
@@ -0,0 +1,78 @@
+#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
index 7c30a14..9728485 100644
--- a/styles/launcher.css
+++ b/styles/launcher.css
@@ -7,8 +7,8 @@
#launcher {
margin: 18px;
- border: 1px solid @outlineVariant;
- background-color: lighter(@background);
+ 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);
@@ -19,7 +19,7 @@
border-radius: 11px;
padding: 10px;
margin: 12px;
- box-shadow: inset 0 0 0 1px @outlineVariant;
+ box-shadow: inset 0 0 0 1px var(--outline-variant);
}
#launcher-content.wallpapers {
@@ -28,11 +28,6 @@
min-width: 620px;
}
-.cliphist {
- min-height: 464px;
- min-width: 300px;
-}
-
.applauncher-scrolled-window {
transition: min-height 0.2s ease;
}
@@ -45,9 +40,8 @@
min-height: 464px;
}
-#launcher-content.todo {
- min-width: 460px;
- min-height: 300px;
+#launcher-content.tools {
+ min-width: 308px;
}
#launcher-content.calendar {
@@ -59,20 +53,24 @@
min-width: 485px;
}
-#app-launcher,
-#wallpapers,
-#todo,
-#bluetooth,
-#sh,
-#cliphist,
-#emoji,
-#power {
- transition: all 0.25s cubic-bezier(0.5, 0.25, 0, 1);
+#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: lighter(@background);
+ background-color: var(--surface);
padding: 4px;
margin-left: 6px;
}
@@ -81,85 +79,161 @@
border-radius: 8px;
min-width: 10px;
min-height: 48px;
- background-color: @primary;
+ 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;
+}
-#launcher-app {
+#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;
}
-#launcher-app:hover image,
-#launcher-app:active image,
-#launcher-app:selected image,
-#launcher-app.selected image,
-#launcher-app:focus image {
- -gtk-icon-shadow: 2px 2px @primary;
+@keyframes loadSlot {
+ 0% {
+ opacity: 0;
+ }
+
+ 100% {
+ opacity: 1;
+ }
}
-#launcher-app:hover,
-#launcher-app:active,
-#launcher-app.selected,
-#launcher-app:selected,
-#launcher-app:focus {
- text-shadow: 2pt 2pt 2pt @primary;
+#slot-button:hover,
+#slot-button:focus,
+#slot-button:selected,
+#slot-button.selected {
+ background-color: var(--surface-bright);
+ padding-left: 24px;
}
-#launcher-app-icon {
+#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: 300px;
-}
-
-#entry-custom-color {
- min-width: 10px;
+ min-width: 512px;
}
#wallpaper-icons:selected {
- background-color: @surfaceBright;
- border-radius: 8px;
+ background-color: var(--surface-bright);
+ border-radius: 0px;
}
#wallpaper-icons:hover {
- background-color: @surfaceBright;
- border-radius: 8px;
-}
-
-#dropdown-box {
- background-color: lighter(@background);
- box-shadow: inset 0 0 0 1px @outlineVariant;
- border-radius: 11px;
- padding: 10px;
- margin: 12px;
+ background-color: var(--surface-bright);
+ border-radius: 0px;
}
#scheme-dropdown {
+ background-color: var(--surface-container);
+ padding: 8px;
+ border-radius: 11px;
+ margin-top: 12px;
+ margin-bottom: 12px;
+}
+
+#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);
}
/* Customize the overall switch appearance */
-#materialyoucolor-switcher {
+#matugen-switcher {
min-width: 40px;
min-height: 20px;
- background-color: @surface;
+ background-color: var(--surface-variant);
border-radius: 15px;
padding: 2px;
transition: background-color 0.3s ease;
}
/* Style the switch's slider */
-#materialyoucolor-switcher slider {
+#matugen-switcher slider {
background-color: var(--primary);
border-radius: 16px;
min-width: 20px;
@@ -170,13 +244,17 @@
}
/* When the switch is active (checked) */
-#materialyoucolor-switcher:checked {
- background-color: @primary;
+#matugen-switcher:checked {
+ background-color: var(--primary);
}
/* Optional: additional styling for the slider when active */
-#materialyoucolor-switcher:checked slider {
- background-color: @shadow;
+#matugen-switcher:checked slider {
+ background-color: var(--surface);
+}
+
+#matugen-switcher:checked image {
+ color: var(--surface);
}
#mat-label {
@@ -188,21 +266,107 @@
* ── emoji ─────────────────────────────────────────────────────────────
*/
-#launcher button#emoji-item {
- border-radius: 11px;
- background-color: transparent;
- background-image: none;
- box-shadow: none;
- padding: 0px;
+#emoji #search-entry {
+ font-weight: bold;
+ background-color: var(--surface);
+ color: var(--foreground);
+ border-radius: 16px;
+ padding: 10px;
}
-#launcher button#emoji-item label {
+#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;
}
-#launcher button#emoji-item:hover,
-#launcher button#emoji-item:focus {
- text-shadow: 2px 2px 2px @primary;
+#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;
}
/*
@@ -213,141 +377,37 @@
border-radius: 20px;
min-width: 52px;
min-height: 52px;
- padding: 20px;
+ padding: 5px;
}
#power-menu-button label {
- font-size: 49px;
- color: @foreground;
+ font-size: 35px;
+ color: var(--foreground);
}
#power-menu-button:hover,
#power-menu-button:focus {
- background-color: @surfaceBright;
+ background-color: var(--surface-bright);
}
#power-menu-button:hover label,
#power-menu-button:focus label {
- color: @primary;
+ color: var(--primary);
}
#power-menu-button:active {
- background-color: @primary;
+ background-color: var(--primary);
}
#power-menu-button:active label {
- color: @shadow;
-}
-
-/*
- * ── cliphist ──────────────────────────────────────────────────────────
-*/
-
-button#clip-img-item {
- background-size: cover;
- background-position: center;
- background-repeat: no-repeat;
- border-radius: 8px;
- min-width: 70px;
- min-height: 70px;
-}
-
-button#clip-img-item:hover,
-button#clip-img-item:focus {
- box-shadow: 2px 2px 2px @primary;
-}
-
-button#clip-item {
- border-radius: 11px;
- background-color: transparent;
- background-image: none;
- box-shadow: none;
- padding: 6px;
-}
-
-button#clip-item {
- font-size: 30px;
-}
-
-button#clip-item:hover,
-button#clip-item:focus {
- text-shadow: 2px 2px 2px @primary;
-}
-
-#clear-button {
- margin: 11px;
- margin-left: 0px;
- min-width: 44px;
- min-height: 44px;
- border-radius: 100%;
-}
-
-#clear-button:hover {
- background-color: mix(@onSurface, @surface, 0.92);
-}
-
-#cliphist-clear-button {
- font-size: 24px;
-}
-
-/*
- * ── todo ──────────────────────────────────────────────────────────────
-*/
-#todo-launcher {
- padding: 10px;
- border-radius: 8px;
-}
-
-#todo-item {
- padding: 8px;
- border-radius: 4px;
- background-color: alpha(@background, 0.3);
-}
-
-#todo-label {
- font-size: 16px;
-}
-
-#todo-label-completed {
- color: alpha(@outlineVariant, 1);
- text-decoration: line-through;
-}
-
-#add-button {
- margin: 11px;
- margin-left: 0px;
- margin-right: 0px;
- min-width: 44px;
- min-height: 44px;
- border-radius: 100%;
-}
-
-#delete-button,
-#checkbox {
- margin-left: 0px;
- margin-right: 0px;
- min-width: 44px;
- min-height: 44px;
- border-radius: 100%;
-}
-
-#add-button:hover,
-#delete-button:hover,
-#checkbox:hover {
- background-color: mix(@onSurface, @surface, 0.92);
-}
-
-#todo-add-button,
-#todo-checkbox,
-#todo-delete-button {
- font-size: 24px;
+ color: var(--shadow);
}
/*
* ── sh runner ─────────────────────────────────────────────────────────
*/
-#launcher button#sh-item {
+#sh-item {
border-radius: 11px;
background-color: transparent;
background-image: none;
@@ -355,13 +415,13 @@ button#clip-item:focus {
padding: 0px;
}
-#launcher button#sh-item {
+#sh-item {
font-size: 24px;
}
-#launcher button#sh-item:hover,
-#launcher button#sh-item:focus {
- text-shadow: 2px 2px 2px @primary;
+#sh-item:hover,
+#sh-item:focus {
+ text-shadow: 2px 2px 2px var(--primary);
}
/*
@@ -369,35 +429,33 @@ button#clip-item:focus {
*/
#bluetooth-header {
- background-color: @surfaceContainer;
+ border: 2px solid var(--shadow);
padding: 4px;
- border-radius: 20px;
+ background-color: var(--surface-container);
+ border-radius: 12px;
}
#bluetooth-device {
- background-color: @secondaryContainer;
- border-radius: 16px;
+ border: 2px solid var(--shadow);
+ background-color: var(--surface-container);
+ border-radius: 12px;
padding: 4px;
- margin-bottom: 4px;
-}
-
-#bluetooth-scan-button,
-#bluetooth-toggle-button {
- font-size: 24px;
}
#bluetooth-scan,
-#bluetooth-toggle {
- background-color: @secondaryContainer;
- margin-left: 0px;
- min-width: 44px;
- min-height: 44px;
- border-radius: 100%;
+#bluetooth-back {
+ background-color: var(--surface);
+ border-radius: 8px;
+ padding: 4px;
+}
+
+#bluetooth-back-label {
+ font-size: 20px;
}
#bluetooth-scan:hover,
-#bluetooth-toggle:hover {
- background-color: @surfaceBright;
+#bluetooth-back:hover {
+ background-color: var(--surface-bright);
}
#bluetooth-scan label,
@@ -410,8 +468,10 @@ button#clip-item:focus {
font-weight: bold;
}
-#bluetooth-text {
- color: @primary;
+#bluetooth-section {
+ background-color: var(--surface-container);
+ border-radius: 12px;
+ padding: 8px;
}
#bluetooth-connection {
@@ -419,20 +479,54 @@ button#clip-item:focus {
}
#bluetooth-connect {
- background-color: lighter(@background);
- border-radius: 16px;
- padding: 8px 16px;
+ background-color: var(--surface);
+ border-radius: 8px;
+ padding: 8px;
}
#bluetooth-connect:hover {
- background-color: @surfaceBright;
+ background-color: var(--surface-bright);
}
#bluetooth-paired,
#bluetooth-available {
- background-color: @surfaceContainer;
- padding: 4px;
+ 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);
+ }
}
/*
@@ -440,13 +534,13 @@ button#clip-item:focus {
*/
#wifi-header {
- background-color: @surfaceContainer;
+ background-color: var(--surface-container);
padding: 4px;
border-radius: 20px;
}
#wifi-device {
- background-color: @secondaryContainer;
+ background-color: var(--secondary-container);
border-radius: 16px;
padding: 4px;
margin-bottom: 4px;
@@ -458,7 +552,7 @@ button#clip-item:focus {
#wifi-scan,
#wifi-toggle {
- background-color: @secondaryContainer;
+ background-color: var(--surface-container);
margin-left: 0px;
min-width: 44px;
min-height: 44px;
@@ -467,7 +561,7 @@ button#clip-item:focus {
#wifi-scan:hover,
#wifi-toggle:hover {
- background-color: @surfaceBright;
+ background-color: var(--surface-bright);
}
#wifi-scan label,
@@ -481,7 +575,7 @@ button#clip-item:focus {
}
#wifi-text {
- color: @primary;
+ color: var(--primary);
}
#wifi-connection {
@@ -489,17 +583,17 @@ button#clip-item:focus {
}
#wifi-connect {
- background-color: lighter(@background);
+ background-color: var(--surface);
border-radius: 16px;
padding: 8px 16px;
}
#wifi-connect:hover {
- background-color: @surfaceBright;
+ background-color: var(--surface-bright);
}
#wifi-available {
- background-color: @surfaceContainer;
+ /*background-color: var(--surface-container);*/
padding: 4px;
border-radius: 20px;
}
@@ -509,11 +603,12 @@ button#clip-item:focus {
*/
#header {
- border-radius: 16px;
+ border-radius: 12px;
padding: 4px;
}
#weekday-row {
+ background-color: var(--surface);
border-radius: 8px;
padding: 4px;
}
@@ -526,16 +621,16 @@ button#clip-item:focus {
#day-label,
#day-empty {
- min-width: 42px;
- min-height: 36px;
+ min-width: 38px;
+ min-height: 22px;
}
#day-empty {
- color: @surfaceBright;
+ color: var(--surface-bright);
}
#weekday-label {
- color: @primary;
+ color: var(--primary);
}
#calendar-grid {
@@ -544,47 +639,189 @@ button#clip-item:focus {
}
#day-label.current-day {
- background-color: @primary;
- color: @onPrimary;
- min-height: 36px;
- min-width: 42px;
- border-radius: 99px;
+ 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: @surfaceBright;
+ background-color: var(--surface-bright);
}
#prev-month-button:active,
#next-month-button:active {
- background-color: @primary;
+ background-color: var(--primary);
}
#month-button-label {
- color: @primary;
+ color: var(--primary);
font-size: 20px;
}
#prev-month-button:active #month-button-label,
#next-month-button:active #month-button-label {
- color: @surface;
-}
-
-#day-box {
- padding: 5px;
- border-radius: 5px;
- min-width: 30px;
- min-height: 30px;
+ color: var(--shadow);
}
#prev-month-day,
#next-month-day {
- color: mix(@onSurfaceVariant, transparent, 0.6);
+ 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
index fff38b2..79e35a8 100644
--- a/styles/main.css
+++ b/styles/main.css
@@ -1,26 +1,27 @@
-@import url(../../.cache/material/colors.css);
+@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: @foreground;
- font-size: 16px;
- font-family: "Chakra Petch";
+ color: var(--foreground);
+ font-size: 15px;
+ font-family: "Monaspace Radon";
}
/*
* ── tooltip ───────────────────────────────────────────────────────────
*/
-
tooltip {
border: solid 1px;
- border-radius: 10px;
- border-color: @surface;
- background-color: @shadow;
+ border-radius: 16px;
+ border-color: var(--outline);
+ background-color: var(--surface);
animation: tooltipShow 0.25s cubic-bezier(0.5, 0.25, 0, 1);
}
@@ -39,38 +40,12 @@ tooltip > * {
border-radius: 10px;
}
-/*
- * ── dock ──────────────────────────────────────────────────────────────
-*/
-#dock-icon {
- padding: 0px 2px 0px 2px;
+#corner {
+ background-color: var(--surface);
+ border-radius: 0;
}
-#dock {
- padding: 10px;
- background-color: alpha(@shadow, 0.5);
- border: 0.5px solid alpha(@outlineVariant, 0.4);
- border-radius: 12px;
- margin-bottom: 2px;
-}
-
-#dock-separator {
- border-right: 1px solid alpha(@foreground, 0.5);
-}
-
-#dock-app-button {
- padding: 2px;
- margin-left: 2px;
- margin-right: 2px;
- border-radius: 12px;
-}
-
-.show-dock {
- opacity: 1;
- margin: 0;
-}
-
-.hide-dock {
- opacity: 1;
- margin: -80px;
+#corner-container {
+ min-width: 20px;
+ min-height: 20px;
}
diff --git a/styles/notif_center.css b/styles/notif_center.css
new file mode 100644
index 0000000..85f0f1d
--- /dev/null
+++ b/styles/notif_center.css
@@ -0,0 +1,28 @@
+#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
index 7b8ea88..dddf9bf 100644
--- a/styles/notification.css
+++ b/styles/notification.css
@@ -3,26 +3,27 @@
}
#notification-action-buttons button {
- background-color: @surfaceContainer;
+ background-color: var(--surface);
padding: 8px;
border-radius: 20px;
+ transition: background-color 0.1s ease;
}
#notification-action-buttons button:hover {
- background-color: @surfaceBright;
+ background-color: var(--surface-bright);
}
#notification-action-buttons button:active {
- background-color: @primary;
+ background-color: var(--primary);
}
#notification-action-buttons button:active #button-label {
- color: @shadow;
+ color: var(--shadow);
}
#notification-image image {
border-radius: 16px;
- color: @onSurface;
+ color: var(--on-surface);
}
#notification-summary,
@@ -32,11 +33,11 @@
#notification-summary {
font-weight: bold;
- color: @primary;
+ color: var(--primary);
}
#notification-app-name {
- color: @outline;
+ color: var(--outline);
}
#action-button {
@@ -46,9 +47,10 @@
#notification-box {
border-radius: 20px;
padding: 16px;
- background-color: @surfaceContainerLow;
+ background-color: var(--surface-container-low);
margin: 4px;
min-width: 336px;
+ transition: background-color 0.1s ease;
}
#boxed-notification-revealer {
@@ -56,25 +58,26 @@
}
#notif-close-button {
- background-color: @surface;
+ 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: @surfaceBright;
+ background-color: var(--surface-bright);
}
#notif-close-button:active {
- background-color: @error;
+ background-color: var(--red-dim);
}
#notif-close-label {
- color: @error;
+ color: var(--red-dim);
font-size: 20px;
}
#notif-close-button:active #notif-close-label {
- color: @shadow;
+ color: var(--shadow);
}
diff --git a/styles/osd.css b/styles/osd.css
index b1f4818..017865d 100644
--- a/styles/osd.css
+++ b/styles/osd.css
@@ -1,44 +1,47 @@
-.osd-panel {
- background-color: transparent;
+#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;
- font-weight: bold;
-}
-
-#osd-value {
- font-weight: bold;
- font-size: 25px;
-}
-
-#progress-container {
- border-radius: 6px;
- min-height: 12px;
- min-width: 150px;
-}
-
-#progress-fill {
- background-color: @primary;
- border-radius: 6px;
- min-height: 8px;
- transition: min-width 0.3s ease-in-out;
-}
-
-#progress-background {
- background-color: @surfaceContainer;
- border-radius: 6px;
- min-height: 8px;
- min-width: 150px;
-}
-
-#control-section {
- padding: 10px;
- border-radius: 10px;
- background-color: lighter(@background);
-}
-
-.osd-hidden {
- opacity: 0;
- transition: opacity 0.3s ease-in-out;
}
diff --git a/utils/__init__.py b/utils/__init__.py
index 587fe40..43abad3 100644
--- a/utils/__init__.py
+++ b/utils/__init__.py
@@ -1,25 +1,4 @@
from .custom_image import *
from .icon_resolver import *
-import json
-import os
-from fabric.utils import get_relative_path
-
-
-def read_config():
- config_path = get_relative_path("../config/assets/modus.json")
-
- with open(config_path, "r") as file:
- # Load JSON data into a Python dictionary
- data = json.load(file)
- return data
-
-
-default_wallpapers_dir = get_relative_path("../assets/wallpaper")
-json_config_path = get_relative_path("../config/assets/config.json")
-
-if os.path.exists(json_config_path):
- with open(json_config_path, "r") as f:
- config = json.load(f)
- WALLPAPERS_DIR = config.get("wallpapers_dir", default_wallpapers_dir)
-else:
- WALLPAPERS_DIR = default_wallpapers_dir
+from .occlusion import *
+from .animator import *
diff --git a/utils/animator.py b/utils/animator.py
new file mode 100644
index 0000000..1799ae6
--- /dev/null
+++ b/utils/animator.py
@@ -0,0 +1,179 @@
+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/icon_resolver.py b/utils/icon_resolver.py
index cd814c7..b8bedeb 100644
--- a/utils/icon_resolver.py
+++ b/utils/icon_resolver.py
@@ -8,31 +8,22 @@ gi.require_version("Gtk", "3.0")
from gi.repository import GLib, Gtk
from loguru import logger
+import config.data as data
-# TODO WIP
-# Idea: nearest string matching algorithm
-# if already exists: retrieve from json
-# if found: stor in json: app_id -> icon_name
-# if not found: store in json -> misisng-icon
-
-
-CACHE_DIR = str(GLib.get_user_cache_dir()) + "/modus"
-ICON_CACHE_FILE = CACHE_DIR + "/icons.json"
-if not os.path.exists(CACHE_DIR):
- os.makedirs(CACHE_DIR)
+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"
- ):
+ def __init__(self, default_applicaiton_icon: str = "application-x-executable-symbolic"):
if os.path.exists(ICON_CACHE_FILE):
- f = open(ICON_CACHE_FILE)
- try:
- self._icon_dict = json.load(f)
- except json.JSONDecodeError:
- logger.info("[ICONS] Cache file does not exist or is corrupted")
- f.close()
+ 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 = {}
@@ -49,20 +40,33 @@ class IconResolver:
return new_icon
def get_icon_pixbuf(self, app_id: str, size: int = 16):
- return Gtk.IconTheme.get_default().load_icon(
- self.get_icon_name(app_id),
- size,
- Gtk.IconLookupFlags.FORCE_SIZE,
- )
+ 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)
- f.close()
def _get_icon_from_desktop_file(self, desktop_file_path: str):
- # TODO: get icon in the [Desktop Entry] section only
+ # 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:
@@ -72,32 +76,27 @@ class IconResolver:
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 = data_dir + "/applications/"
+ data_dir = os.path.join(data_dir, "applications")
if os.path.exists(data_dir):
- # Do name resolving here
-
files = os.listdir(data_dir)
- matching = [
- s for s in files if "".join(app_id.lower().split()) in s.lower()
- ]
+ matching = [s for s in files if "".join(app_id.lower().split()) in s.lower()]
if matching:
- return data_dir + matching[0]
-
+ 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 data_dir + matching[0]
-
+ return os.path.join(data_dir, matching[0])
return None
def _compositor_find_icon(self, app_id: str):
- if Gtk.IconTheme.get_default().has_icon(app_id):
+ icon_theme = Gtk.IconTheme.get_default()
+ if icon_theme.has_icon(app_id):
return app_id
- if Gtk.IconTheme.get_default().has_icon(app_id + "-desktop"):
+ 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
index 79c2407..ed61594 100644
--- a/utils/icons.py
+++ b/utils/icons.py
@@ -5,7 +5,7 @@ font_weight: str = "normal"
span: str = f""
# Panels
-apps: str = ""
+apps: str = ""
dashboard: str = ""
chat: str = ""
wallpapers: str = ""
@@ -23,6 +23,23 @@ 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 = ""
@@ -55,22 +72,38 @@ power_balanced: str = "勺"
power_performance: str = ""
charging: str = ""
discharging: str = ""
-alert: 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 = "﹖"
-world: str = ""
+
wifi_off: str = ""
bluetooth_off: str = ""
night_off: str = ""
dnd_off: str = ""
-mic_off: str = ""
+
# Bluetooth
bluetooth_connected: str = ""
@@ -97,19 +130,20 @@ vol_mute: str = ""
vol_medium: str = ""
vol_high: str = ""
-# Network
-lan: str = ""
-moderate_signal: str = ""
-good_signal: str = ""
-no_signal: str = ""
-weak_signal: str = ""
+
+mic: str = ""
+mic_muted: str = ""
+
# Overview
circle_plus: str = ""
# Pins
copy_plus: str = "ﶮ"
-paperclip: str = ""
+
+# Clipboard Manager
+clipboard: str = ""
+clip_text: str = ""
# Confirm
accept: str = ""
@@ -130,6 +164,13 @@ 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"]
diff --git a/utils/occlusion.py b/utils/occlusion.py
new file mode 100644
index 0000000..bf06350
--- /dev/null
+++ b/utils/occlusion.py
@@ -0,0 +1,141 @@
+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
+