diff --git a/hypr/hyprland.conf b/hypr/hyprland.conf
index 3aed6a4..037da93 100644
--- a/hypr/hyprland.conf
+++ b/hypr/hyprland.conf
@@ -27,38 +27,41 @@ $launchpad = nwggrid
# Cursor
env = XCURSOR_SIZE,24
-env = XCURSOR_THEME,Adwaita
+env = XCURSOR_THEME,macOS
+env = HYPRCURSOR_SIZE,24
+env = HYPRCURSOR_THEME,macOS
# Nvidia
env = LIBVA_DRIVER_NAME,nvidia
env = XDG_SESSION_TYPE,wayland
env = GBM_BACKEND,nvidia-drm
env = __GLX_VENDOR_LIBRARY_NAME,nvidia
-env = NVD_BACKEND,direct
+# env = NVD_BACKEND,direct
# Electron
-env = ELECTRON_OZONE_PLATFORM_HINT,auto
+# env = ELECTRON_OZONE_PLATFORM_HINT,auto
# Firefox
-env = MOZ_DISABLE_RDD_SANDBOX,1
-env = EGL_PLATFORM,wayland
-env = MOZ_ENABLE_WAYLAND,1
+# env = MOZ_DISABLE_RDD_SANDBOX,1
+# env = EGL_PLATFORM,wayland
+# env = MOZ_ENABLE_WAYLAND,1
# For qt apps
-env = QT_QPA_PLATFORM,wayland
-env = QT_QPA_PLATFORMTHEME,qt5ct
+# env = QT_QPA_PLATFORM,wayland
+# env = QT_QPA_PLATFORMTHEME,qt5ct
env = GTK_THEME,Material
env = XDG_CURRENT_DESKTOP,Hyprland
env = XDG_SESSION_DESKTOP,Hyprland
+env = bitdepth,10
+
#-----------------------------------------------------------------------------------------------------------
# EXEC
#-----------------------------------------------------------------------------------------------------------
exec-once = ignis init
-exec-once=/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1
exec-once = dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP
exec-once = telegram-desktop -startintray
exec-once = nm-applet
@@ -70,14 +73,16 @@ exec-once = wl-paste --type text --watch cliphist store
exec-once = wl-paste --type image --watch cliphist store
exec-once = fcitx5
-exec-once = hyprswitch init --show-title &
+exec-once = hyprswitch init --show-title --size-factor 5.5 --workspaces-per-row 5 &
#-----------------------------------------------------------------------------------------------------------
# INPUT
#-----------------------------------------------------------------------------------------------------------
cursor {
- no_hardware_cursors = true
+ no_hardware_cursors = false
+ use_cpu_buffer = true
+ warp_on_change_workspace = true
}
input {
@@ -122,15 +127,15 @@ decoration {
blur {
enabled = true
- size = 12
+ size = 12 # 12
passes = 4
new_optimizations = true
}
shadow {
- enabled = true
- range = 30
- render_power = 4
+ enabled = false # true
+ range = 30 # 30
+ render_power = 4 # 4
color = rgb(000000)
}
# old shadow settings
@@ -145,13 +150,16 @@ decoration {
#-----------------------------------------------------------------------------------------------------------
animations {
+ # animation = NAME, ONOFF, SPEED, CURVE [, STYLE]
enabled = yes
+ # bezier = quart, 0.25, 1, 0.5, 1
bezier = quart, 0.25, 1, 0.5, 1
- animation = windows, 1, 6, quart, slide
+ animation = windows, 1, 4, quart, slide
+ # animation = windows, 1, 4, quart, gnomed # hyprland 0.48.0 introduces gnomed
animation = border, 1, 6, quart
- animation = borderangle, 1, 6, quart
+ # animation = borderangle, 1, 6, quart
animation = fade, 1, 6, quart
animation = workspaces, 1, 6, quart
}
@@ -165,6 +173,13 @@ dwindle {
preserve_split = yes
}
+#-----------------------------------------------------------------------------------------------------------
+# MASTER
+#-----------------------------------------------------------------------------------------------------------
+
+master {
+ new_status = master
+}
#-----------------------------------------------------------------------------------------------------------
# MISC
@@ -182,24 +197,33 @@ render {
explicit_sync = true
}
+#-----------------------------------------------------------------------------------------------------------
+# EXPERIMENTAL
+#-----------------------------------------------------------------------------------------------------------
+
+# experimental {
+# hdr = false
+# }
+
#----------------------b-------------------------------------------------------------------------------------
# WINDOW RULES
#-----------------------------------------------------------------------------------------------------------
-windowrule = float,^(pavucontrol)$
-windowrule = pin,^(pavucontrol)$
-windowrule = size 900 500,^(pavucontrol)$
+# windowrule = float,^(pavucontrol)$
+# windowrule = pin,^(pavucontrol)$
+# windowrule = size 900 500,^(pavucontrol)$
#windowrule = float,^(kitty)$
#windowrule = size 640 400,^(kitty)$
windowrule = float,class:^(Material Settings)$
-windowrulev2 = opacity 0.0 override, class:^(xwaylandvideobridge)$
-windowrulev2 = noanim, class:^(xwaylandvideobridge)$
-windowrulev2 = noinitialfocus, class:^(xwaylandvideobridge)$
-windowrulev2 = maxsize 1 1, class:^(xwaylandvideobridge)$
-windowrulev2 = noblur, class:^(xwaylandvideobridge)$
+# windowrulev2 = opacity 0.0 override, class:^(xwaylandvideobridge)$
+# windowrulev2 = noanim, class:^(xwaylandvideobridge)$
+# windowrulev2 = noinitialfocus, class:^(xwaylandvideobridge)$
+# windowrulev2 = maxsize 1 1, class:^(xwaylandvideobridge)$
+# windowrulev2 = noblur, class:^(xwaylandvideobridge)$
+# windowrulev2 = nofocus, class:^(xwaylandvideobridge)$
layerrule = blur,^(ignis_BAR.*)$
layerrule = noanim,^(ignis_NOTIFICATION_POPUP.*)$
@@ -308,6 +332,8 @@ bind = ,XF86AudioPrev, exec, playerctl previous
bind = CONTROL SHIFT, ESCAPE, exec, missioncenter
# hyprswitch
-bind = $mod, $key, exec, hyprswitch gui --mod-key $mod --key $key --close mod-key-release --reverse-key=mod=$reverse --sort-recent && hyprswitch dispatch
-bind = $mod $reverse, $key, exec, hyprswitch gui --mod-key $mod --key $key --close mod-key-release --reverse-key=mod=$reverse --sort-recent && hyprswitch dispatch -r
+bind = $mod, $key, exec, hyprswitch gui --mod-key $mod --key $key --close mod-key-release --reverse-key=key=$reverse && hyprswitch dispatch
+bind = $mod $reverse, $key, exec, hyprswitch gui --mod-key $mod --key $key --close mod-key-release --reverse-key=key=$reverse && hyprswitch dispatch -r
+
+windowrulev2 = suppressevent maximize, class:.* # You'll probably like this.
diff --git a/hypr/hyprland.conf.bak b/hypr/hyprland.conf.bak
new file mode 100644
index 0000000..2c0eb7b
--- /dev/null
+++ b/hypr/hyprland.conf.bak
@@ -0,0 +1,325 @@
+#-----------------------------------------------------------------------------------------------------------
+# IMPORTING FILE WITH BORDER COLOR
+#-----------------------------------------------------------------------------------------------------------
+
+source=~/.cache/ignis/material/dark_colors-hyprland.conf
+source = ~/.config/hypr/monitors.conf
+
+# Monitor primary
+workspace = name:1, monitor:DP-3
+exec-once = hyprctl dispatch workspace 1
+
+# Set programs that you use
+$terminal = kitty
+$fileManager = nautilus
+#$menu = tofi-drun --config .config/tofi/config | xargs hyprctl dispatch exec -- # wofi --show drun
+$browser = zen-browser
+#$shareclip = kdeconnect-cli --device=347E8D05994A40EEBFCFFBCE700FDA6B --send-clipboard
+
+$key = tab
+$mod = alt
+$reverse = grave
+$launchpad = nwggrid
+
+#-----------------------------------------------------------------------------------------------------------
+# ENVIRONMENT
+#-----------------------------------------------------------------------------------------------------------
+
+# Cursor
+env = XCURSOR_SIZE,24
+# env = XCURSOR_THEME,Adwaita
+
+# Nvidia
+env = LIBVA_DRIVER_NAME,nvidia
+env = XDG_SESSION_TYPE,wayland
+env = GBM_BACKEND,nvidia-drm
+env = __GLX_VENDOR_LIBRARY_NAME,nvidia
+env = NVD_BACKEND,direct
+
+# Electron
+env = ELECTRON_OZONE_PLATFORM_HINT,auto
+
+# Firefox
+env = MOZ_DISABLE_RDD_SANDBOX,1
+env = EGL_PLATFORM,wayland
+env = MOZ_ENABLE_WAYLAND,1
+
+# For qt apps
+env = QT_QPA_PLATFORM,wayland
+env = QT_QPA_PLATFORMTHEME,qt5ct
+
+env = GTK_THEME,Material
+
+env = XDG_CURRENT_DESKTOP,Hyprland
+env = XDG_SESSION_DESKTOP,Hyprland
+
+#-----------------------------------------------------------------------------------------------------------
+# EXEC
+#-----------------------------------------------------------------------------------------------------------
+
+exec-once = ignis init
+exec-once=/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1
+exec-once = dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP
+exec-once = telegram-desktop -startintray
+exec-once = nm-applet
+exec-once = blueman-applet
+exec-once = /usr/lib/polkit-kde-authentication-agent-1
+# exec-once = swww-daemon
+# exec-once = swww img /home/timo/.config/hypr/wallpaper/wallpaper.jpg
+exec-once = wl-paste --type text --watch cliphist store
+exec-once = wl-paste --type image --watch cliphist store
+exec-once = fcitx5
+
+exec-once = hyprswitch init --show-title &
+
+#-----------------------------------------------------------------------------------------------------------
+# INPUT
+#-----------------------------------------------------------------------------------------------------------
+
+cursor {
+ no_hardware_cursors = false
+ use_cpu_buffer = true
+ warp_on_change_workspace = true
+}
+
+input {
+ kb_layout = de
+ kb_variant =
+ kb_model =
+ kb_options = grp:win_space_toggle
+ kb_rules =
+ accel_profile = flat
+
+ follow_mouse = 1
+
+ touchpad {
+ natural_scroll = no
+ }
+
+ #scroll_factor = 2
+
+ sensitivity = 0
+}
+
+#-----------------------------------------------------------------------------------------------------------
+# GENERAL
+#-----------------------------------------------------------------------------------------------------------
+
+general {
+ gaps_in = 5
+ gaps_out = 20
+ border_size = 2
+ col.active_border = $primary
+ col.inactive_border = rgb(000000)
+ resize_on_border=true
+ layout = dwindle
+}
+
+#-----------------------------------------------------------------------------------------------------------
+# DECORATION
+#-----------------------------------------------------------------------------------------------------------
+
+decoration {
+ rounding = 15
+
+ blur {
+ enabled = true
+ size = 12 # 12
+ passes = 4
+ new_optimizations = true
+ }
+
+ shadow {
+ enabled = false # true
+ range = 30 # 30
+ render_power = 4 # 4
+ color = rgb(000000)
+ }
+ # old shadow settings
+ # drop_shadow = yes
+ # shadow_range = 30
+ # shadow_render_power = 4
+ # col.shadow = rgb(000000)
+}
+
+#-----------------------------------------------------------------------------------------------------------
+# ANIMATIONS
+#-----------------------------------------------------------------------------------------------------------
+
+animations {
+ enabled = yes
+
+ # bezier = quart, 0.25, 1, 0.5, 1
+ bezier = quart, 0.25, 1, 0.5, 1
+
+ animation = windows, 1, 6, quart, slide
+ animation = border, 1, 6, quart
+ # animation = borderangle, 1, 6, quart
+ animation = fade, 1, 6, quart
+ animation = workspaces, 1, 6, quart
+}
+
+#-----------------------------------------------------------------------------------------------------------
+# WINDOW LAYOUTS
+#-----------------------------------------------------------------------------------------------------------
+
+dwindle {
+ pseudotile = yes
+ preserve_split = yes
+}
+
+
+#-----------------------------------------------------------------------------------------------------------
+# MISC
+#-----------------------------------------------------------------------------------------------------------
+
+misc {
+ disable_hyprland_logo=true
+}
+
+#-----------------------------------------------------------------------------------------------------------
+# RENDER
+#-----------------------------------------------------------------------------------------------------------
+
+render {
+ explicit_sync = true
+}
+
+#-----------------------------------------------------------------------------------------------------------
+# EXPERIMENTAL
+#-----------------------------------------------------------------------------------------------------------
+
+experimental {
+ hdr = false
+}
+
+#----------------------b-------------------------------------------------------------------------------------
+# WINDOW RULES
+#-----------------------------------------------------------------------------------------------------------
+
+windowrule = float,^(pavucontrol)$
+windowrule = pin,^(pavucontrol)$
+windowrule = size 900 500,^(pavucontrol)$
+
+#windowrule = float,^(kitty)$
+#windowrule = size 640 400,^(kitty)$
+
+windowrule = float,class:^(Material Settings)$
+
+windowrulev2 = opacity 0.0 override, class:^(xwaylandvideobridge)$
+windowrulev2 = noanim, class:^(xwaylandvideobridge)$
+windowrulev2 = noinitialfocus, class:^(xwaylandvideobridge)$
+windowrulev2 = maxsize 1 1, class:^(xwaylandvideobridge)$
+windowrulev2 = noblur, class:^(xwaylandvideobridge)$
+windowrulev2 = nofocus, class:^(xwaylandvideobridge)$
+
+layerrule = blur,^(ignis_BAR.*)$
+layerrule = noanim,^(ignis_NOTIFICATION_POPUP.*)$
+layerrule = noanim,^(ignis_CONTROL_CENTER.*)$
+
+#-----------------------------------------------------------------------------------------------------------
+# BINDS
+#-----------------------------------------------------------------------------------------------------------
+
+# Key modifier
+$mainMod = SUPER
+
+# Binds to control windows
+bind = $mainMod, Q, killactive,
+bind = $mainMod SHIFT, M, exit, # force quit Hyprland
+bind = $mainMod, T, togglefloating,
+bind = $mainMod, P, pseudo, # dwindle
+bind = $mainMod, J, togglesplit, # dwindle
+bind = ,F11, fullscreen, 0
+bind = $mainMod, C, centerwindow
+bind = $mainMod, D, pin
+
+# Used to switch windows left, right, ... with another windows, also move to another screen
+bind = $mainMod SHIFT, left, movewindow, l
+bind = $mainMod SHIFT, right, movewindow, r
+bind = $mainMod SHIFT, up, movewindow, u
+bind = $mainMod SHIFT, down, movewindow, d
+
+# Rsize but not working after reboot
+# bind = $mainMod CTRL, left, resizeactive, -50 0
+# bind = $mainMod CTRL, up, resizeactive, 0 50
+# bind = $mainMod CTRL, down, resizeactive, 0 -50
+# bind = $mainMod CTRL, right, resizeactive, 50 0
+
+# ignis
+bind = $mainMod, SPACE, exec, ignis toggle ignis_LAUNCHER
+bind = $mainMod, M, exec, ignis toggle ignis_POWERMENU
+bind = ALT, F4, exec, ignis toggle ignis_POWERMENU
+bind = $mainMod, R, exec, ~/.config/ignis/scripts/recording.py start
+bind = $mainMod SHIFT, R, exec, ~/.config/ignis/scripts/recording.py continue
+bind = $mainMod SHIFT, T, exec, ~/.config/ignis/scripts/recording.py stop
+bind = $mainMod SHIFT, P, exec, ~/.config/ignis/scripts/recording.py pause
+
+# Launch apps
+bind = $mainMod, Return, exec, $terminal
+bind = $mainMod, L, exec, hyprlock
+bind = $mainMod, E, exec, $fileManager
+bind = $mainMod SHIFT, S, exec, grimblast --notify --freeze copysave area
+bind = $mainMod, S, exec, grimblast --notify --freeze copysave output
+bind = ,PRINT, exec, grimblast --notify --freeze copysave output
+bind = $mainMod, B, exec, $browser
+bind = $mainMod SHIFT, SPACE, exec, $launchpad
+
+# Focus control
+bind = $mainMod, left, movefocus, l
+bind = $mainMod, right, movefocus, r
+bind = $mainMod, up, movefocus, u
+bind = $mainMod, down, movefocus, d
+
+# Workspace switching
+bind = $mainMod, 1, workspace, 1
+bind = $mainMod, 2, workspace, 2
+bind = $mainMod, 3, workspace, 3
+bind = $mainMod, 4, workspace, 4
+bind = $mainMod, 5, workspace, 5
+bind = $mainMod, 6, workspace, 6
+bind = $mainMod, 7, workspace, 7
+bind = $mainMod, 8, workspace, 8
+bind = $mainMod, 9, workspace, 9
+bind = $mainMod, 0, workspace, 10
+
+# Move active window to workspace
+bind = $mainMod SHIFT, 1, movetoworkspace, 1
+bind = $mainMod SHIFT, 2, movetoworkspace, 2
+bind = $mainMod SHIFT, 3, movetoworkspace, 3
+bind = $mainMod SHIFT, 4, movetoworkspace, 4
+bind = $mainMod SHIFT, 5, movetoworkspace, 5
+bind = $mainMod SHIFT, 6, movetoworkspace, 6
+bind = $mainMod SHIFT, 7, movetoworkspace, 7
+bind = $mainMod SHIFT, 8, movetoworkspace, 8
+bind = $mainMod SHIFT, 9, movetoworkspace, 9
+bind = $mainMod SHIFT, 0, movetoworkspace, 10
+
+# Switch between existing workspaces by scrolling the mouse
+bind = $mainMod, mouse_down, workspace, e+1
+bind = $mainMod, mouse_up, workspace, e-1
+
+# Move and resize window
+bindm = $mainMod, mouse:272, movewindow
+bindm = $mainMod, mouse:273, resizewindow
+
+# Media binds
+# bind = ,xf86audiomute, exec, pactl -- set-sink-mute @DEFAULT_SINK@ toggle
+bind = ,XF86AudioRaiseVolume, exec, pamixer -i 5 && ignis open ignis_OSD
+bind = ,XF86AudioLowerVolume, exec, pamixer -d 5 && ignis open ignis_OSD
+bind = ,XF86AudioMute, exec, pamixer -t && ignis open ignis_OSD
+bind = ,xf86monbrightnessdown, exec, blight -d backlight/amdgpu_bl1 set -25.5
+bind = ,xf86monbrightnessup, exec, blight -d backlight/amdgpu_bl1 set +25.5
+bind = ,XF86AudioPlay, exec, playerctl play-pause
+bind = ,XF86AudioPause, exec, playerctl play-pause
+bind = ,XF86AudioNext, exec, playerctl next
+bind = ,XF86AudioPrev, exec, playerctl previous
+
+
+# taskmgr
+bind = CONTROL SHIFT, ESCAPE, exec, missioncenter
+
+# hyprswitch
+bind = $mod, $key, exec, hyprswitch gui --mod-key $mod --key $key --close mod-key-release --reverse-key=mod=$reverse --sort-recent && hyprswitch dispatch
+bind = $mod $reverse, $key, exec, hyprswitch gui --mod-key $mod --key $key --close mod-key-release --reverse-key=mod=$reverse --sort-recent && hyprswitch dispatch -r
+
diff --git a/hypr/mocha.conf.bak b/hypr/mocha.conf.bak
new file mode 100644
index 0000000..8ccb56a
--- /dev/null
+++ b/hypr/mocha.conf.bak
@@ -0,0 +1,78 @@
+
+$rosewater = rgb(f5e0dc)
+$rosewaterAlpha = f5e0dc
+
+$flamingo = rgb(f2cdcd)
+$flamingoAlpha = f2cdcd
+
+$pink = rgb(f5c2e7)
+$pinkAlpha = f5c2e7
+
+$mauve = rgb(cba6f7)
+$mauveAlpha = cba6f7
+
+$red = rgb(f38ba8)
+$redAlpha = f38ba8
+
+$maroon = rgb(eba0ac)
+$maroonAlpha = eba0ac
+
+$peach = rgb(fab387)
+$peachAlpha = fab387
+
+$yellow = rgb(f9e2af)
+$yellowAlpha = f9e2af
+
+$green = rgb(a6e3a1)
+$greenAlpha = a6e3a1
+
+$teal = rgb(94e2d5)
+$tealAlpha = 94e2d5
+
+$sky = rgb(89dceb)
+$skyAlpha = 89dceb
+
+$sapphire = rgb(74c7ec)
+$sapphireAlpha = 74c7ec
+
+$blue = rgb(89b4fa)
+$blueAlpha = 89b4fa
+
+$lavender = rgb(b4befe)
+$lavenderAlpha = b4befe
+
+$text = rgb(cdd6f4)
+$textAlpha = cdd6f4
+
+$subtext1 = rgb(bac2de)
+$subtext1Alpha = bac2de
+
+$subtext0 = rgb(a6adc8)
+$subtext0Alpha = a6adc8
+
+$overlay2 = rgb(9399b2)
+$overlay2Alpha = 9399b2
+
+$overlay1 = rgb(7f849c)
+$overlay1Alpha = 7f849c
+
+$overlay0 = rgb(6c7086)
+$overlay0Alpha = 6c7086
+
+$surface2 = rgb(585b70)
+$surface2Alpha = 585b70
+
+$surface1 = rgb(45475a)
+$surface1Alpha = 45475a
+
+$surface0 = rgb(313244)
+$surface0Alpha = 313244
+
+$base = rgb(1e1e2e)
+$baseAlpha = 1e1e2e
+
+$mantle = rgb(181825)
+$mantleAlpha = 181825
+
+$crust = rgb(11111b)
+$crustAlpha = 11111b
diff --git a/hypr/monitors.conf b/hypr/monitors.conf
index ea9a5df..9b21c08 100644
--- a/hypr/monitors.conf
+++ b/hypr/monitors.conf
@@ -1,5 +1,5 @@
-# Generated by nwg-displays on 2025-01-26 at 00:56:09. Do not edit manually.
+monitor=HDMI-A-1,3840x2160@60.0,-2048x0,1.0,mirror,DP-3,bitdepth,10
+monitor=DP-2,2560x1080@60.0,644x-1080,1.0,mirror,10
+monitor=DP-3,3840x2160@60.0,0x0,1.0,bitdepth,8 # bitdepth 8 for screensharing
-monitor=HDMI-A-1,3840x2160@60.0,0x1080,1.0,mirror,DP-3,bitdepth,10
-monitor=DP-2,2560x1080@60.0,4484x0,1.0
-monitor=DP-3,3840x2160@60.0,3840x1080,1.0,bitdepth,10
+env = XWAYLAND_PRIMARY_OUTPUT,DP-3
diff --git a/ignis/__pycache__/config.cpython-313.pyc b/ignis/__pycache__/config.cpython-313.pyc
index 8bc7e8e..d61f5bf 100644
Binary files a/ignis/__pycache__/config.cpython-313.pyc and b/ignis/__pycache__/config.cpython-313.pyc differ
diff --git a/ignis/__pycache__/user_options.cpython-313.pyc b/ignis/__pycache__/user_options.cpython-313.pyc
new file mode 100644
index 0000000..2d047c6
Binary files /dev/null and b/ignis/__pycache__/user_options.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/__init__.py b/ignis/backup_04.03.2025/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/ignis/backup_04.03.2025/__pycache__/check_version.cpython-312.pyc b/ignis/backup_04.03.2025/__pycache__/check_version.cpython-312.pyc
new file mode 100644
index 0000000..a7bbbcd
Binary files /dev/null and b/ignis/backup_04.03.2025/__pycache__/check_version.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/__pycache__/check_version.cpython-313.pyc b/ignis/backup_04.03.2025/__pycache__/check_version.cpython-313.pyc
new file mode 100644
index 0000000..2cd7164
Binary files /dev/null and b/ignis/backup_04.03.2025/__pycache__/check_version.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/__pycache__/config.cpython-312.pyc b/ignis/backup_04.03.2025/__pycache__/config.cpython-312.pyc
new file mode 100644
index 0000000..b91d3c3
Binary files /dev/null and b/ignis/backup_04.03.2025/__pycache__/config.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/__pycache__/config.cpython-313.pyc b/ignis/backup_04.03.2025/__pycache__/config.cpython-313.pyc
new file mode 100644
index 0000000..8bc7e8e
Binary files /dev/null and b/ignis/backup_04.03.2025/__pycache__/config.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/__pycache__/options.cpython-312.pyc b/ignis/backup_04.03.2025/__pycache__/options.cpython-312.pyc
new file mode 100644
index 0000000..d6bfdcd
Binary files /dev/null and b/ignis/backup_04.03.2025/__pycache__/options.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/__pycache__/options.cpython-313.pyc b/ignis/backup_04.03.2025/__pycache__/options.cpython-313.pyc
new file mode 100644
index 0000000..c6fc568
Binary files /dev/null and b/ignis/backup_04.03.2025/__pycache__/options.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/check_version.py b/ignis/backup_04.03.2025/check_version.py
new file mode 100644
index 0000000..e68e18b
--- /dev/null
+++ b/ignis/backup_04.03.2025/check_version.py
@@ -0,0 +1,23 @@
+import os
+from ignis.utils import Utils
+
+EXPECT_VERSION = ["0", "2"]
+
+CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
+
+SKIP_VER_CHECK = os.getenv("SKIP_VER_CHECK")
+BYPASS_VER_MESSAGE = "To bypass this check set SKIP_VER_CHECK=1 env var."
+
+def check_version() -> None:
+ if SKIP_VER_CHECK == "1":
+ return
+
+ VERSION = Utils.get_ignis_version().replace("dev0", "").split(".")
+
+ if int(VERSION[0]) < int(EXPECT_VERSION[0]) or int(VERSION[1]) < int(
+ EXPECT_VERSION[1]
+ ):
+ print(
+ f"ERROR: My dotfiles requires at least Ignis v{'.'.join(EXPECT_VERSION)}, current version: v{'.'.join(VERSION)}. {BYPASS_VER_MESSAGE}"
+ )
+ exit(1)
diff --git a/ignis/backup_04.03.2025/config.py b/ignis/backup_04.03.2025/config.py
new file mode 100644
index 0000000..af1361e
--- /dev/null
+++ b/ignis/backup_04.03.2025/config.py
@@ -0,0 +1,32 @@
+from check_version import check_version
+
+check_version()
+
+from ignis.utils import Utils # noqa: E402
+from ignis.app import IgnisApp # noqa: E402
+from modules.control_center import control_center # noqa: E402
+from modules.bar import bar # noqa: E402
+from modules.notification_popup import notification_popup # noqa: E402
+from modules.osd import OSD # noqa: E402
+from modules.powermenu import powermenu # noqa: E402
+from modules.launcher import launcher # noqa: E402
+
+Utils.exec_sh("gsettings set org.gnome.desktop.interface gtk-theme Material")
+Utils.exec_sh("gsettings set org.gnome.desktop.interface icon-theme Papirus")
+Utils.exec_sh(
+ 'gsettings set org.gnome.desktop.interface font-name "JetBrains Mono Regular 11"'
+)
+Utils.exec_sh("hyprctl reload")
+app = IgnisApp.get_default()
+app.apply_css(Utils.get_current_dir() + "/style.scss")
+
+
+control_center()
+for monitor in range(Utils.get_n_monitors()):
+ bar(monitor)
+launcher()
+for monitor in range(Utils.get_n_monitors()):
+ notification_popup(monitor)
+
+powermenu()
+OSD()
diff --git a/ignis/backup_04.03.2025/misc/media-art-fallback.png b/ignis/backup_04.03.2025/misc/media-art-fallback.png
new file mode 100644
index 0000000..f76719b
Binary files /dev/null and b/ignis/backup_04.03.2025/misc/media-art-fallback.png differ
diff --git a/ignis/backup_04.03.2025/modules/__init__.py b/ignis/backup_04.03.2025/modules/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/ignis/backup_04.03.2025/modules/__pycache__/__init__.cpython-312.pyc b/ignis/backup_04.03.2025/modules/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..2a9d474
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/__pycache__/__init__.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/__pycache__/__init__.cpython-313.pyc b/ignis/backup_04.03.2025/modules/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..6b67587
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/bar/__init__.py b/ignis/backup_04.03.2025/modules/bar/__init__.py
new file mode 100644
index 0000000..a167d8f
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/bar/__init__.py
@@ -0,0 +1,25 @@
+from ignis.widgets import Widget
+from .workspaces import workspaces
+from .kb_layout import kb_layout
+from .clock import clock
+from .pinned_apps import pinned_apps
+from .tray import tray
+from .battery import battery_widget
+# from .wallpaper_engine import wallpaper_engine_widget
+
+def bar(monitor: int) -> Widget.Window:
+ return Widget.Window(
+ anchor=["left", "top", "right"],
+ exclusivity="exclusive",
+ monitor=monitor,
+ namespace=f"ignis_BAR_{monitor}",
+ layer="top",
+ kb_mode="none",
+ child=Widget.CenterBox(
+ css_classes=["bar-widget"],
+ start_widget=Widget.Box(child=[workspaces()]),
+ center_widget=Widget.Box(child=[pinned_apps()]),
+ end_widget=Widget.Box(child=[tray(), kb_layout(), battery_widget(), clock(monitor)]),
+ ), # wallpaper_engine_widget(),
+ css_classes=["unset"],
+ )
diff --git a/ignis/backup_04.03.2025/modules/bar/__pycache__/__init__.cpython-312.pyc b/ignis/backup_04.03.2025/modules/bar/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..d750549
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/bar/__pycache__/__init__.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/bar/__pycache__/__init__.cpython-313.pyc b/ignis/backup_04.03.2025/modules/bar/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..a18b492
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/bar/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/bar/__pycache__/battery.cpython-312.pyc b/ignis/backup_04.03.2025/modules/bar/__pycache__/battery.cpython-312.pyc
new file mode 100644
index 0000000..0160378
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/bar/__pycache__/battery.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/bar/__pycache__/battery.cpython-313.pyc b/ignis/backup_04.03.2025/modules/bar/__pycache__/battery.cpython-313.pyc
new file mode 100644
index 0000000..2d45190
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/bar/__pycache__/battery.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/bar/__pycache__/clock.cpython-312.pyc b/ignis/backup_04.03.2025/modules/bar/__pycache__/clock.cpython-312.pyc
new file mode 100644
index 0000000..178a246
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/bar/__pycache__/clock.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/bar/__pycache__/clock.cpython-313.pyc b/ignis/backup_04.03.2025/modules/bar/__pycache__/clock.cpython-313.pyc
new file mode 100644
index 0000000..84ebb0a
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/bar/__pycache__/clock.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/bar/__pycache__/indicator.cpython-312.pyc b/ignis/backup_04.03.2025/modules/bar/__pycache__/indicator.cpython-312.pyc
new file mode 100644
index 0000000..59fd0dc
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/bar/__pycache__/indicator.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/bar/__pycache__/indicator.cpython-313.pyc b/ignis/backup_04.03.2025/modules/bar/__pycache__/indicator.cpython-313.pyc
new file mode 100644
index 0000000..c2078cc
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/bar/__pycache__/indicator.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/bar/__pycache__/kb_layout.cpython-312.pyc b/ignis/backup_04.03.2025/modules/bar/__pycache__/kb_layout.cpython-312.pyc
new file mode 100644
index 0000000..893e83d
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/bar/__pycache__/kb_layout.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/bar/__pycache__/kb_layout.cpython-313.pyc b/ignis/backup_04.03.2025/modules/bar/__pycache__/kb_layout.cpython-313.pyc
new file mode 100644
index 0000000..9ce3d39
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/bar/__pycache__/kb_layout.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/bar/__pycache__/pinned_apps.cpython-312.pyc b/ignis/backup_04.03.2025/modules/bar/__pycache__/pinned_apps.cpython-312.pyc
new file mode 100644
index 0000000..642f8f7
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/bar/__pycache__/pinned_apps.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/bar/__pycache__/pinned_apps.cpython-313.pyc b/ignis/backup_04.03.2025/modules/bar/__pycache__/pinned_apps.cpython-313.pyc
new file mode 100644
index 0000000..85a4de7
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/bar/__pycache__/pinned_apps.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/bar/__pycache__/tray.cpython-312.pyc b/ignis/backup_04.03.2025/modules/bar/__pycache__/tray.cpython-312.pyc
new file mode 100644
index 0000000..74aea45
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/bar/__pycache__/tray.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/bar/__pycache__/tray.cpython-313.pyc b/ignis/backup_04.03.2025/modules/bar/__pycache__/tray.cpython-313.pyc
new file mode 100644
index 0000000..304e04d
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/bar/__pycache__/tray.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/bar/__pycache__/wallpaper_engine.cpython-313.pyc b/ignis/backup_04.03.2025/modules/bar/__pycache__/wallpaper_engine.cpython-313.pyc
new file mode 100644
index 0000000..8692a99
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/bar/__pycache__/wallpaper_engine.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/bar/__pycache__/workspaces.cpython-312.pyc b/ignis/backup_04.03.2025/modules/bar/__pycache__/workspaces.cpython-312.pyc
new file mode 100644
index 0000000..a2c99fc
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/bar/__pycache__/workspaces.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/bar/__pycache__/workspaces.cpython-313.pyc b/ignis/backup_04.03.2025/modules/bar/__pycache__/workspaces.cpython-313.pyc
new file mode 100644
index 0000000..648f107
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/bar/__pycache__/workspaces.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/bar/battery.py b/ignis/backup_04.03.2025/modules/bar/battery.py
new file mode 100644
index 0000000..e72d1bd
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/bar/battery.py
@@ -0,0 +1,35 @@
+from ignis.widgets import Widget
+from ignis.services.upower import UPowerService, UPowerDevice
+
+upower = UPowerService.get_default()
+
+
+def battery_item(device: UPowerDevice) -> Widget.Box:
+ return Widget.Box(
+ css_classes=["battery-item"],
+ setup=lambda self: device.connect("removed", lambda x: self.unparent()),
+ child=[
+ Widget.Icon(
+ icon_name=device.bind("icon_name"), css_classes=["battery-icon"]
+ ),
+ Widget.Label(
+ label=device.bind("percent", lambda x: f"{int(x)}%"),
+ css_classes=["battery-percent"],
+ ),
+ Widget.Scale(
+ min=0,
+ max=100,
+ value=device.bind("percent"),
+ sensitive=False,
+ css_classes=["battery-scale"],
+ ),
+ ],
+ )
+
+
+def battery_widget() -> Widget.Box:
+ return Widget.Box(
+ setup=lambda self: upower.connect(
+ "battery-added", lambda x, device: self.append(battery_item(device))
+ ),
+ )
diff --git a/ignis/backup_04.03.2025/modules/bar/clock.py b/ignis/backup_04.03.2025/modules/bar/clock.py
new file mode 100644
index 0000000..a176ba9
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/bar/clock.py
@@ -0,0 +1,41 @@
+import datetime
+from ignis.widgets import Widget
+from ignis.app import IgnisApp
+from ignis.utils import Utils
+from ignis.variable import Variable
+from .indicator import status_icons
+
+app = IgnisApp.get_default()
+
+current_time = Variable(
+ value=Utils.Poll(1000, lambda x: datetime.datetime.now().strftime("%H:%M:%S")).bind(
+ "output"
+ )
+)
+
+
+def clock(monitor):
+ window: Widget.Window = app.get_window("ignis_CONTROL_CENTER") # type: ignore
+
+ def on_click(x):
+ if window.monitor == monitor:
+ window.visible = not window.visible
+ else:
+ window.set_monitor(monitor)
+ window.visible = True
+
+ return Widget.Button(
+ child=Widget.Box(
+ child=[
+ status_icons(),
+ Widget.Label(
+ label=current_time.bind("value"),
+ ),
+ ]
+ ),
+ css_classes=window.bind(
+ "visible",
+ lambda value: ["clock", "unset", "active"] if value else ["clock", "unset"],
+ ),
+ on_click=on_click,
+ )
diff --git a/ignis/backup_04.03.2025/modules/bar/indicator.py b/ignis/backup_04.03.2025/modules/bar/indicator.py
new file mode 100644
index 0000000..b2d718d
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/bar/indicator.py
@@ -0,0 +1,92 @@
+from ignis.widgets import Widget
+from ignis.services.network import NetworkService
+from ignis.services.notifications import NotificationService
+from ignis.services.recorder import RecorderService
+from ignis.services.audio import AudioService
+
+network = NetworkService.get_default()
+notifications = NotificationService.get_default()
+recorder = RecorderService.get_default()
+audio = AudioService.get_default()
+
+
+def indicator_icon(**kwargs):
+ return Widget.Icon(style="margin-right: 0.5rem;", css_classes=["unset"], **kwargs)
+
+
+def wifi_icon():
+ def check_visible(*args) -> bool:
+ if len(network.wifi.devices) > 0:
+ if network.ethernet.is_connected:
+ if network.wifi.is_connected:
+ return True
+ else:
+ return False
+ else:
+ return True
+ else:
+ return False
+
+ icon = indicator_icon(image=network.wifi.bind("icon-name"))
+ icon.visible = network.wifi.bind("devices", check_visible)
+ icon.visible = network.ethernet.bind("is_connected", check_visible)
+ icon.visible = network.wifi.bind("is_connected", check_visible)
+ return icon
+
+
+def ethernet_icon():
+ def check_visible(*args) -> bool:
+ if len(network.ethernet.devices) > 0:
+ if network.wifi.is_connected:
+ if network.ethernet.is_connected:
+ return True
+ else:
+ return False
+ else:
+ return True
+ else:
+ return False
+
+ icon = indicator_icon(image=network.ethernet.bind("icon_name"))
+ icon.visible = network.ethernet.bind("devices", check_visible)
+ icon.visible = network.wifi.bind("is_connected", check_visible)
+ icon.visible = network.ethernet.bind("is_connected", check_visible)
+ return icon
+
+
+def dnd_icon():
+ return indicator_icon(
+ image="notification-disabled-symbolic",
+ visible=notifications.bind("dnd"),
+ )
+
+
+def recorder_icon():
+ def check_state(icon: Widget.Icon) -> None:
+ if recorder.is_paused:
+ icon.remove_css_class("active")
+ else:
+ icon.add_css_class("active")
+
+ icon = indicator_icon(
+ image="media-record-symbolic",
+ visible=recorder.bind("active"),
+ )
+
+ icon.add_css_class("record-indicator")
+
+ recorder.connect("notify::is-paused", lambda x, y: check_state(icon))
+
+ return icon
+
+
+def volume_icon():
+ return indicator_icon(
+ image=audio.speaker.bind("icon_name"),
+ )
+
+
+def status_icons():
+ return Widget.Box(
+ child=[recorder_icon(), wifi_icon(), ethernet_icon(), volume_icon(), dnd_icon()]
+ )
diff --git a/ignis/backup_04.03.2025/modules/bar/kb_layout.py b/ignis/backup_04.03.2025/modules/bar/kb_layout.py
new file mode 100644
index 0000000..b3a07a3
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/bar/kb_layout.py
@@ -0,0 +1,21 @@
+from ignis.widgets import Widget
+from ignis.exceptions import HyprlandIPCNotFoundError
+from ignis.services.hyprland import HyprlandService
+
+try:
+ hyprland = HyprlandService.get_default()
+
+ def kb_layout():
+ return Widget.Button(
+ css_classes=["kb-layout", "unset"],
+ on_click=lambda x: hyprland.switch_kb_layout(),
+ child=Widget.Label(
+ label=hyprland.bind(
+ "kb_layout", transform=lambda value: value[:2].lower()
+ )
+ ),
+ )
+except HyprlandIPCNotFoundError:
+
+ def kb_layout():
+ return
diff --git a/ignis/backup_04.03.2025/modules/bar/pinned_apps.py b/ignis/backup_04.03.2025/modules/bar/pinned_apps.py
new file mode 100644
index 0000000..bcd492c
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/bar/pinned_apps.py
@@ -0,0 +1,51 @@
+from ignis.widgets import Widget
+from ignis.app import IgnisApp
+from ignis.services.applications import ApplicationsService
+
+applications = ApplicationsService.get_default()
+app = IgnisApp.get_default()
+
+
+class AppItem(Widget.Button):
+ def __init__(self, app):
+ menu = Widget.PopoverMenu(
+ items=[
+ Widget.MenuItem(label="Launch", on_activate=lambda x: app.launch()),
+ Widget.Separator(),
+ ]
+ + [
+ Widget.MenuItem(
+ label=i.name, on_activate=lambda x, action=i: action.launch()
+ )
+ for i in app.actions
+ ]
+ + [
+ Widget.Separator(),
+ Widget.MenuItem(label="Unpin", on_activate=lambda x: app.unpin()),
+ ]
+ )
+
+ super().__init__(
+ child=Widget.Box(child=[Widget.Icon(image=app.icon, pixel_size=32), menu]),
+ on_click=lambda x: app.launch(),
+ on_right_click=lambda x: menu.popup(),
+ css_classes=["pinned-app", "unset"],
+ )
+
+
+def launcher_button():
+ return Widget.Button(
+ child=Widget.Icon(image="start-here-symbolic", pixel_size=32),
+ on_click=lambda x: app.toggle_window("ignis_LAUNCHER"),
+ css_classes=["pinned-app", "unset"],
+ )
+
+
+def pinned_apps():
+ return Widget.Box(
+ child=applications.bind(
+ "pinned",
+ transform=lambda value: [AppItem(app) for app in value]
+ + [launcher_button()],
+ )
+ )
diff --git a/ignis/backup_04.03.2025/modules/bar/tray.py b/ignis/backup_04.03.2025/modules/bar/tray.py
new file mode 100644
index 0000000..fdbf77c
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/bar/tray.py
@@ -0,0 +1,37 @@
+from ignis.widgets import Widget
+from ignis.services.system_tray import SystemTrayService
+
+system_tray = SystemTrayService.get_default()
+
+
+class TrayItem(Widget.Button):
+ def __init__(self, item):
+ if item.menu:
+ menu = item.menu.copy()
+ else:
+ menu = None
+
+ super().__init__(
+ child=Widget.Box(
+ child=[
+ Widget.Icon(image=item.bind("icon"), pixel_size=24),
+ menu,
+ ]
+ ),
+ tooltip_text=item.bind("tooltip"),
+ on_click=lambda x: item.activate(),
+ on_right_click=lambda x: menu.popup() if menu else None,
+ css_classes=["tray-item", "unset"],
+ )
+
+ item.connect("removed", lambda x: self.unparent())
+
+
+def tray():
+ return Widget.Box(
+ css_classes=["tray"],
+ setup=lambda self: system_tray.connect(
+ "added", lambda x, item: self.append(TrayItem(item))
+ ),
+ spacing=10,
+ )
diff --git a/ignis/backup_04.03.2025/modules/bar/wallpaper_engine.py b/ignis/backup_04.03.2025/modules/bar/wallpaper_engine.py
new file mode 100644
index 0000000..c545e47
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/bar/wallpaper_engine.py
@@ -0,0 +1,25 @@
+from ignis.widgets import Widget
+from ignis.exceptions import HyprlandIPCNotFoundError
+from ignis.services.hyprland import HyprlandService
+import os
+
+def toggle_engine():
+ os.system("cd /home/timo/.config/hypr/linux-wallpaperengine && ./linux-wallpaperengine 2657260174 --screen-root DP-3")
+
+try:
+ hyprland = HyprlandService.get_default()
+
+ def wallpaper_engine_widget():
+ return Widget.Button(
+ css_classes=["kb-layout", "unset"],
+ on_click=lambda x: os.system("cd /home/timo/.config/hypr/linux-wallpaperengine && ./linux-wallpaperengine 2657260174 --screen-root DP-3"),
+ child=Widget.Label(
+ label=hyprland.bind(
+ "kb_layout", transform=lambda value: value[:2].lower()
+ )
+ ),
+ )
+except HyprlandIPCNotFoundError:
+
+ def kb_layout():
+ return
diff --git a/ignis/backup_04.03.2025/modules/bar/workspaces.py b/ignis/backup_04.03.2025/modules/bar/workspaces.py
new file mode 100644
index 0000000..0cf7298
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/bar/workspaces.py
@@ -0,0 +1,46 @@
+from ignis.widgets import Widget
+from ignis.exceptions import HyprlandIPCNotFoundError
+from ignis.services.hyprland import HyprlandService
+
+
+class WorkspaceButton(Widget.Button):
+ def __init__(self, workspace: dict) -> None:
+ super().__init__(
+ css_classes=["workspace", "unset"],
+ on_click=lambda x, id=workspace["id"]: hyprland.switch_to_workspace(id),
+ halign="start",
+ valign="center",
+ )
+ if workspace["id"] == hyprland.active_workspace["id"]:
+ self.add_css_class("active")
+
+
+try:
+ hyprland = HyprlandService.get_default()
+
+ def scroll_workspaces(direction: str) -> None:
+ current = hyprland.active_workspace["id"]
+ if direction == "up":
+ target = current - 1
+ hyprland.switch_to_workspace(target)
+ else:
+ target = current + 1
+ if target == 11:
+ return
+ hyprland.switch_to_workspace(target)
+
+ def workspaces():
+ return Widget.EventBox(
+ on_scroll_up=lambda x: scroll_workspaces("up"),
+ on_scroll_down=lambda x: scroll_workspaces("down"),
+ css_classes=["workspaces"],
+ child=hyprland.bind(
+ "workspaces",
+ transform=lambda value: [WorkspaceButton(i) for i in value],
+ ),
+ )
+
+except HyprlandIPCNotFoundError:
+
+ def workspaces():
+ return
diff --git a/ignis/backup_04.03.2025/modules/control_center/__init__.py b/ignis/backup_04.03.2025/modules/control_center/__init__.py
new file mode 100644
index 0000000..285bb00
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/control_center/__init__.py
@@ -0,0 +1,62 @@
+from ignis.widgets import Widget
+from ignis.app import IgnisApp
+from .volume import volume_control
+from .quick_settings import quick_settings
+from .user import user
+from .media import media
+from .notification_center import notification_center
+from .brightness import brightness_slider
+
+app = IgnisApp.get_default()
+
+
+def control_center_widget() -> Widget.Box:
+ return Widget.Box(
+ vertical=True,
+ css_classes=["control-center"],
+ child=[
+ Widget.Box(
+ vertical=True,
+ css_classes=["control-center-widget"],
+ child=[
+ quick_settings(),
+ volume_control(),
+ brightness_slider(),
+ user(),
+ media(),
+ ],
+ ),
+ notification_center(),
+ ],
+ )
+
+
+def control_center() -> Widget.RevealerWindow:
+ revealer = Widget.Revealer(
+ transition_type="slide_left",
+ child=control_center_widget(),
+ transition_duration=300,
+ reveal_child=True,
+ )
+ box = Widget.Box(
+ child=[
+ Widget.Button(
+ vexpand=True,
+ hexpand=True,
+ css_classes=["unset"],
+ on_click=lambda x: app.close_window("ignis_CONTROL_CENTER"),
+ ),
+ revealer,
+ ],
+ )
+ return Widget.RevealerWindow(
+ visible=False,
+ popup=True,
+ kb_mode="on_demand",
+ layer="top",
+ css_classes=["unset"],
+ anchor=["top", "right", "bottom", "left"],
+ namespace="ignis_CONTROL_CENTER",
+ child=box,
+ revealer=revealer,
+ )
diff --git a/ignis/backup_04.03.2025/modules/control_center/__pycache__/__init__.cpython-312.pyc b/ignis/backup_04.03.2025/modules/control_center/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..e21d8e4
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/control_center/__pycache__/__init__.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/control_center/__pycache__/__init__.cpython-313.pyc b/ignis/backup_04.03.2025/modules/control_center/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..a14a45b
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/control_center/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/control_center/__pycache__/brightness.cpython-312.pyc b/ignis/backup_04.03.2025/modules/control_center/__pycache__/brightness.cpython-312.pyc
new file mode 100644
index 0000000..b92a376
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/control_center/__pycache__/brightness.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/control_center/__pycache__/brightness.cpython-313.pyc b/ignis/backup_04.03.2025/modules/control_center/__pycache__/brightness.cpython-313.pyc
new file mode 100644
index 0000000..322b318
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/control_center/__pycache__/brightness.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/control_center/__pycache__/dark_mode.cpython-312.pyc b/ignis/backup_04.03.2025/modules/control_center/__pycache__/dark_mode.cpython-312.pyc
new file mode 100644
index 0000000..a9a7548
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/control_center/__pycache__/dark_mode.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/control_center/__pycache__/dark_mode.cpython-313.pyc b/ignis/backup_04.03.2025/modules/control_center/__pycache__/dark_mode.cpython-313.pyc
new file mode 100644
index 0000000..5acb0c4
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/control_center/__pycache__/dark_mode.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/control_center/__pycache__/dnd.cpython-312.pyc b/ignis/backup_04.03.2025/modules/control_center/__pycache__/dnd.cpython-312.pyc
new file mode 100644
index 0000000..017eabe
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/control_center/__pycache__/dnd.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/control_center/__pycache__/dnd.cpython-313.pyc b/ignis/backup_04.03.2025/modules/control_center/__pycache__/dnd.cpython-313.pyc
new file mode 100644
index 0000000..06e6554
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/control_center/__pycache__/dnd.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/control_center/__pycache__/ethernet.cpython-312.pyc b/ignis/backup_04.03.2025/modules/control_center/__pycache__/ethernet.cpython-312.pyc
new file mode 100644
index 0000000..e329fe4
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/control_center/__pycache__/ethernet.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/control_center/__pycache__/ethernet.cpython-313.pyc b/ignis/backup_04.03.2025/modules/control_center/__pycache__/ethernet.cpython-313.pyc
new file mode 100644
index 0000000..26829a4
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/control_center/__pycache__/ethernet.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/control_center/__pycache__/media.cpython-312.pyc b/ignis/backup_04.03.2025/modules/control_center/__pycache__/media.cpython-312.pyc
new file mode 100644
index 0000000..6ec2385
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/control_center/__pycache__/media.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/control_center/__pycache__/media.cpython-313.pyc b/ignis/backup_04.03.2025/modules/control_center/__pycache__/media.cpython-313.pyc
new file mode 100644
index 0000000..d829979
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/control_center/__pycache__/media.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/control_center/__pycache__/notification_center.cpython-312.pyc b/ignis/backup_04.03.2025/modules/control_center/__pycache__/notification_center.cpython-312.pyc
new file mode 100644
index 0000000..8c61d5b
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/control_center/__pycache__/notification_center.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/control_center/__pycache__/notification_center.cpython-313.pyc b/ignis/backup_04.03.2025/modules/control_center/__pycache__/notification_center.cpython-313.pyc
new file mode 100644
index 0000000..0e77675
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/control_center/__pycache__/notification_center.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/control_center/__pycache__/qs_button.cpython-312.pyc b/ignis/backup_04.03.2025/modules/control_center/__pycache__/qs_button.cpython-312.pyc
new file mode 100644
index 0000000..a8f2ef6
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/control_center/__pycache__/qs_button.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/control_center/__pycache__/qs_button.cpython-313.pyc b/ignis/backup_04.03.2025/modules/control_center/__pycache__/qs_button.cpython-313.pyc
new file mode 100644
index 0000000..5af9afd
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/control_center/__pycache__/qs_button.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/control_center/__pycache__/quick_settings.cpython-312.pyc b/ignis/backup_04.03.2025/modules/control_center/__pycache__/quick_settings.cpython-312.pyc
new file mode 100644
index 0000000..04dc3be
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/control_center/__pycache__/quick_settings.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/control_center/__pycache__/quick_settings.cpython-313.pyc b/ignis/backup_04.03.2025/modules/control_center/__pycache__/quick_settings.cpython-313.pyc
new file mode 100644
index 0000000..2419a45
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/control_center/__pycache__/quick_settings.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/control_center/__pycache__/record.cpython-312.pyc b/ignis/backup_04.03.2025/modules/control_center/__pycache__/record.cpython-312.pyc
new file mode 100644
index 0000000..af3c069
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/control_center/__pycache__/record.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/control_center/__pycache__/record.cpython-313.pyc b/ignis/backup_04.03.2025/modules/control_center/__pycache__/record.cpython-313.pyc
new file mode 100644
index 0000000..ce79176
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/control_center/__pycache__/record.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/control_center/__pycache__/user.cpython-312.pyc b/ignis/backup_04.03.2025/modules/control_center/__pycache__/user.cpython-312.pyc
new file mode 100644
index 0000000..43da95d
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/control_center/__pycache__/user.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/control_center/__pycache__/user.cpython-313.pyc b/ignis/backup_04.03.2025/modules/control_center/__pycache__/user.cpython-313.pyc
new file mode 100644
index 0000000..b076b1a
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/control_center/__pycache__/user.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/control_center/__pycache__/volume.cpython-312.pyc b/ignis/backup_04.03.2025/modules/control_center/__pycache__/volume.cpython-312.pyc
new file mode 100644
index 0000000..5c73dd4
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/control_center/__pycache__/volume.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/control_center/__pycache__/volume.cpython-313.pyc b/ignis/backup_04.03.2025/modules/control_center/__pycache__/volume.cpython-313.pyc
new file mode 100644
index 0000000..0956d7d
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/control_center/__pycache__/volume.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/control_center/__pycache__/vpn.cpython-312.pyc b/ignis/backup_04.03.2025/modules/control_center/__pycache__/vpn.cpython-312.pyc
new file mode 100644
index 0000000..7701426
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/control_center/__pycache__/vpn.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/control_center/__pycache__/vpn.cpython-313.pyc b/ignis/backup_04.03.2025/modules/control_center/__pycache__/vpn.cpython-313.pyc
new file mode 100644
index 0000000..66f09fc
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/control_center/__pycache__/vpn.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/control_center/__pycache__/wifi.cpython-312.pyc b/ignis/backup_04.03.2025/modules/control_center/__pycache__/wifi.cpython-312.pyc
new file mode 100644
index 0000000..16cd6c3
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/control_center/__pycache__/wifi.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/control_center/__pycache__/wifi.cpython-313.pyc b/ignis/backup_04.03.2025/modules/control_center/__pycache__/wifi.cpython-313.pyc
new file mode 100644
index 0000000..0ccf542
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/control_center/__pycache__/wifi.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/control_center/brightness.py b/ignis/backup_04.03.2025/modules/control_center/brightness.py
new file mode 100644
index 0000000..da513c9
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/control_center/brightness.py
@@ -0,0 +1,27 @@
+from ignis.services.backlight import BacklightService
+from ignis.widgets import Widget
+
+backlight = BacklightService.get_default()
+
+
+def brightness_slider() -> Widget.Scale:
+ return Widget.Box(
+ visible=backlight.bind("available"),
+ hexpand=True,
+ style="margin-top: 0.25rem;",
+ child=[
+ Widget.Icon(
+ image="display-brightness-symbolic",
+ css_classes=["material-slider-icon"],
+ pixel_size=18,
+ ),
+ Widget.Scale(
+ min=0,
+ max=backlight.max_brightness,
+ hexpand=True,
+ value=backlight.bind("brightness"),
+ css_classes=["material-slider"],
+ on_change=lambda x: backlight.set_brightness(x.value),
+ ),
+ ],
+ )
diff --git a/ignis/backup_04.03.2025/modules/control_center/dark_mode.py b/ignis/backup_04.03.2025/modules/control_center/dark_mode.py
new file mode 100644
index 0000000..eeaeca3
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/control_center/dark_mode.py
@@ -0,0 +1,14 @@
+from services.material import MaterialService
+from .qs_button import QSButton
+
+material = MaterialService.get_default()
+
+
+def dark_mode_button() -> QSButton:
+ return QSButton(
+ label="Dark",
+ icon_name="night-light-symbolic",
+ on_activate=lambda x: material.set_dark_mode(True),
+ on_deactivate=lambda x: material.set_dark_mode(False),
+ active=material.bind("dark-mode"),
+ )
diff --git a/ignis/backup_04.03.2025/modules/control_center/dnd.py b/ignis/backup_04.03.2025/modules/control_center/dnd.py
new file mode 100644
index 0000000..2a01638
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/control_center/dnd.py
@@ -0,0 +1,19 @@
+from .qs_button import QSButton
+from ignis.services.notifications import NotificationService
+
+notifications = NotificationService.get_default()
+
+
+def dnd_button() -> QSButton:
+ return QSButton(
+ label=notifications.bind("dnd", lambda value: "Silent" if value else "Noisy"),
+ icon_name=notifications.bind(
+ "dnd",
+ transform=lambda value: "notification-disabled-symbolic"
+ if value
+ else "notification-symbolic",
+ ),
+ on_activate=lambda x: notifications.set_dnd(not notifications.dnd),
+ on_deactivate=lambda x: notifications.set_dnd(not notifications.dnd),
+ active=notifications.bind("dnd"),
+ )
diff --git a/ignis/backup_04.03.2025/modules/control_center/ethernet.py b/ignis/backup_04.03.2025/modules/control_center/ethernet.py
new file mode 100644
index 0000000..2a4e33f
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/control_center/ethernet.py
@@ -0,0 +1,100 @@
+from typing import List
+from ignis.widgets import Widget
+from ignis.utils import Utils
+from .qs_button import QSButton
+from ignis.services.network import NetworkService, EthernetDevice
+
+network = NetworkService.get_default()
+
+
+class EthernetConnectionItem(Widget.Button):
+ def __init__(self, device: EthernetDevice):
+ super().__init__(
+ css_classes=["ethernet-connection-item", "unset"],
+ on_click=lambda x: device.disconnect_from()
+ if device.is_connected
+ else device.connect_to(),
+ child=Widget.Box(
+ child=[
+ Widget.Icon(image="network-wired-symbolic"),
+ Widget.Label(
+ label=device.name,
+ ellipsize="end",
+ max_width_chars=20,
+ halign="start",
+ css_classes=["ethernet-connection-label"],
+ ),
+ Widget.Button(
+ child=Widget.Label(
+ label=device.bind(
+ "is_connected",
+ lambda value: "Disconnect" if value else "Connect",
+ )
+ ),
+ css_classes=["ethernet-connection-item-connect-label", "unset"],
+ halign="end",
+ hexpand=True,
+ ),
+ ]
+ ),
+ )
+
+
+def ethernet_control() -> List[QSButton]:
+ networks_list = Widget.Revealer(
+ transition_duration=300,
+ transition_type="slide_down",
+ child=Widget.Box(
+ vertical=True,
+ css_classes=["control-center-menu"],
+ child=[
+ Widget.Box(
+ css_classes=["ethernet-header-box"],
+ child=[
+ Widget.Icon(icon_name="network-wired-symbolic", pixel_size=28),
+ Widget.Label(
+ label="Wired connections",
+ css_classes=["ethernet-header-label"],
+ ),
+ ],
+ ),
+ Widget.Box(
+ vertical=True,
+ child=network.ethernet.bind(
+ "devices",
+ lambda value: [EthernetConnectionItem(i) for i in value],
+ ),
+ ),
+ Widget.Separator(css_classes=["ethernet-network-list-separator"]),
+ Widget.Button(
+ css_classes=["ethernet-connection-item", "unset"],
+ style="margin-bottom: 0;",
+ on_click=lambda x: Utils.exec_sh_async("nm-connection-editor"),
+ child=Widget.Box(
+ child=[
+ Widget.Icon(image="preferences-system-symbolic"),
+ Widget.Label(
+ label="Network Settings",
+ halign="start",
+ css_classes=["ethernet-connection-label"],
+ ),
+ ]
+ ),
+ ),
+ ],
+ ),
+ )
+
+ if len(network.ethernet.devices) > 0:
+ return [
+ QSButton(
+ label="Wired",
+ icon_name="network-wired-symbolic",
+ on_activate=lambda x: networks_list.toggle(),
+ on_deactivate=lambda x: networks_list.toggle(),
+ content=networks_list,
+ active=network.ethernet.bind("is_connected"),
+ )
+ ]
+ else:
+ return []
diff --git a/ignis/backup_04.03.2025/modules/control_center/media.py b/ignis/backup_04.03.2025/modules/control_center/media.py
new file mode 100644
index 0000000..4099ab5
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/control_center/media.py
@@ -0,0 +1,202 @@
+import os
+import ignis
+from ignis.widgets import Widget
+from ignis.services.mpris import MprisService, MprisPlayer
+from ignis.utils import Utils
+from services.material import MaterialService
+from ignis.app import IgnisApp
+from ignis.exceptions import StylePathNotFoundError
+
+
+mpris = MprisService.get_default()
+app = IgnisApp.get_default()
+material = MaterialService.get_default()
+
+MEDIA_TEMPLATE = Utils.get_current_dir() + "/../../scss/media.scss"
+MEDIA_SCSS_CACHE_DIR = ignis.CACHE_DIR + "/media" # type: ignore
+MEDIA_ART_FALLBACK = Utils.get_current_dir() + "/../../misc/media-art-fallback.png"
+os.makedirs(MEDIA_SCSS_CACHE_DIR, exist_ok=True)
+
+
+PLAYER_ICONS = {"spotify": "", "firefox": "", "chrome": "", None: ""}
+
+
+class Player(Widget.Revealer):
+ def __init__(self, player: MprisPlayer) -> None:
+ self._player = player
+ self._colors_path = f"{MEDIA_SCSS_CACHE_DIR}/{self._player.desktop_entry}.scss"
+ player.connect("closed", lambda x: self.destroy())
+ player.connect("notify::art-url", lambda x, y: self.load_colors())
+ self.load_colors()
+
+ super().__init__(
+ transition_type="slide_down",
+ reveal_child=False,
+ css_classes=[self.get_css("media")],
+ child=Widget.Overlay(
+ child=Widget.Box(css_classes=[self.get_css("media-image")]),
+ overlays=[
+ Widget.Box(
+ hexpand=True,
+ vexpand=True,
+ css_classes=[self.get_css("media-image-gradient")],
+ ),
+ Widget.Label(
+ label=self.get_player_icon(),
+ halign="start",
+ valign="start",
+ css_classes=[self.get_css("media-player-icon")],
+ ),
+ Widget.Box(
+ vertical=True,
+ hexpand=True,
+ css_classes=[self.get_css("media-content")],
+ child=[
+ Widget.Box(
+ vexpand=True,
+ valign="center",
+ child=[
+ Widget.Box(
+ hexpand=True,
+ vertical=True,
+ child=[
+ Widget.Label(
+ ellipsize="end",
+ label=player.bind("title"),
+ max_width_chars=30,
+ halign="start",
+ css_classes=[
+ self.get_css("media-title")
+ ],
+ ),
+ Widget.Label(
+ label=player.bind("artist"),
+ max_width_chars=30,
+ ellipsize="end",
+ halign="start",
+ css_classes=[
+ self.get_css("media-artist")
+ ],
+ ),
+ ],
+ ),
+ Widget.Button(
+ child=Widget.Icon(
+ image=player.bind(
+ "playback_status",
+ lambda value: "media-playback-pause-symbolic"
+ if value == "Playing"
+ else "media-playback-start-symbolic",
+ ),
+ pixel_size=18,
+ ),
+ on_click=lambda x: player.play_pause(),
+ visible=player.bind("can_play"),
+ css_classes=player.bind(
+ "playback_status",
+ lambda value: [
+ self.get_css("media-playback-button"),
+ "playing",
+ ]
+ if value == "Playing"
+ else [
+ self.get_css("media-playback-button"),
+ "paused",
+ ],
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ Widget.Box(
+ vexpand=True,
+ valign="end",
+ style="padding: 1rem;",
+ child=[
+ Widget.Scale(
+ value=player.bind("position"),
+ max=player.bind("length"),
+ hexpand=True,
+ css_classes=[self.get_css("media-scale")],
+ on_change=lambda x: player.set_position(x.value),
+ visible=player.bind(
+ "position", lambda value: value != -1
+ ),
+ ),
+ Widget.Button(
+ child=Widget.Icon(
+ image="media-skip-backward-symbolic",
+ pixel_size=20,
+ ),
+ css_classes=[self.get_css("media-skip-button")],
+ on_click=lambda x: player.previous(),
+ visible=player.bind("can_go_previous"),
+ style="margin-left: 1rem;",
+ ),
+ Widget.Button(
+ child=Widget.Icon(
+ image="media-skip-forward-symbolic",
+ pixel_size=20,
+ ),
+ css_classes=[self.get_css("media-skip-button")],
+ on_click=lambda x: player.next(),
+ visible=player.bind("can_go_next"),
+ style="margin-left: 1rem;",
+ ),
+ ],
+ ),
+ ],
+ ),
+ )
+
+ def get_player_icon(self) -> str:
+ if self._player.desktop_entry == "firefox":
+ return PLAYER_ICONS["firefox"]
+ elif self._player.desktop_entry == "spotify":
+ return PLAYER_ICONS["spotify"]
+ elif "chromium" in self._player.track_id or "chrome" in self._player.track_id:
+ return PLAYER_ICONS["chrome"]
+ else:
+ return PLAYER_ICONS[None]
+
+ def destroy(self) -> None:
+ self.set_reveal_child(False)
+ Utils.Timeout(self.transition_duration, super().unparent)
+
+ def get_css(self, class_name: str) -> str:
+ return f"{class_name}-{self._player.desktop_entry}"
+
+ def load_colors(self) -> None:
+ if not self._player.art_url:
+ art_url = MEDIA_ART_FALLBACK
+ else:
+ art_url = self._player.art_url
+
+ try:
+ app.remove_css(self._colors_path)
+ except StylePathNotFoundError:
+ pass
+
+ colors = material.get_colors_from_img(art_url, True)
+ colors["art_url"] = art_url
+ colors["desktop_entry"] = self._player.desktop_entry
+ material.render_template(
+ colors, input_file=MEDIA_TEMPLATE, output_file=self._colors_path
+ )
+ app.apply_css(self._colors_path)
+
+
+def media() -> Widget.Box:
+ def add_player(box: Widget.Box, obj: MprisPlayer) -> None:
+ player = Player(obj)
+ box.append(player)
+ player.set_reveal_child(True)
+
+ return Widget.Box(
+ vertical=True,
+ setup=lambda self: mpris.connect(
+ "player_added", lambda x, player: add_player(self, player)
+ ),
+ css_classes=["rec-unset"],
+ )
diff --git a/ignis/backup_04.03.2025/modules/control_center/notification_center.py b/ignis/backup_04.03.2025/modules/control_center/notification_center.py
new file mode 100644
index 0000000..2cbc6ac
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/control_center/notification_center.py
@@ -0,0 +1,242 @@
+from ignis.widgets import Widget
+from ignis.services.notifications import Notification, NotificationService
+from ignis.utils import Utils
+from gi.repository import GLib # type: ignore
+
+notifications = NotificationService.get_default()
+
+
+class ScreenshotLayout(Widget.Box):
+ def __init__(self, notification: Notification) -> None:
+ super().__init__(
+ vertical=True,
+ hexpand=True,
+ child=[
+ Widget.Box(
+ child=[
+ Widget.Picture(
+ image=notification.icon,
+ content_fit="cover",
+ width=1920 // 7,
+ height=1080 // 7,
+ style="border-radius: 1rem; background-color: black;",
+ ),
+ Widget.Button(
+ child=Widget.Icon(
+ image="window-close-symbolic", pixel_size=20
+ ),
+ halign="end",
+ valign="start",
+ hexpand=True,
+ css_classes=["notification-close"],
+ on_click=lambda x: notification.close(),
+ ),
+ ],
+ ),
+ Widget.Label(
+ label="Screenshot saved",
+ css_classes=["notification-screenshot-label"],
+ ),
+ Widget.Box(
+ homogeneous=True,
+ style="margin-top: 0.75rem;",
+ spacing=10,
+ child=[
+ Widget.Button(
+ child=Widget.Label(label="Open"),
+ css_classes=["notification-action"],
+ on_click=lambda x: Utils.exec_sh_async(
+ f"xdg-open {notification.icon}"
+ ),
+ ),
+ Widget.Button(
+ child=Widget.Label(label="Close"),
+ css_classes=["notification-action"],
+ on_click=lambda x: notification.close(),
+ ),
+ ],
+ ),
+ ],
+ )
+
+
+class NormalLayout(Widget.Box):
+ def __init__(self, notification: Notification) -> None:
+ super().__init__(
+ vertical=True,
+ hexpand=True,
+ child=[
+ Widget.Box(
+ child=[
+ Widget.Icon(
+ image=notification.icon
+ if notification.icon
+ else "dialog-information-symbolic",
+ pixel_size=48,
+ halign="start",
+ valign="start",
+ ),
+ Widget.Box(
+ vertical=True,
+ style="margin-left: 0.75rem;",
+ child=[
+ Widget.Label(
+ ellipsize="end",
+ label=notification.summary,
+ halign="start",
+ visible=notification.summary != "",
+ css_classes=["notification-summary"],
+ ),
+ Widget.Label(
+ label=notification.body,
+ ellipsize="end",
+ halign="start",
+ css_classes=["notification-body"],
+ visible=notification.body != "",
+ ),
+ ],
+ ),
+ Widget.Button(
+ child=Widget.Icon(
+ image="window-close-symbolic", pixel_size=20
+ ),
+ halign="end",
+ valign="start",
+ hexpand=True,
+ css_classes=["notification-close"],
+ on_click=lambda x: notification.close(),
+ ),
+ ],
+ ),
+ Widget.Box(
+ child=[
+ Widget.Button(
+ child=Widget.Label(label=action.label),
+ on_click=lambda x, action=action: action.invoke(),
+ css_classes=["notification-action"],
+ )
+ for action in notification.actions
+ ],
+ homogeneous=True,
+ style="margin-top: 0.75rem;" if notification.actions else "",
+ spacing=10,
+ ),
+ ],
+ )
+
+
+class NotificationWidget(Widget.Box):
+ def __init__(self, notification: Notification) -> None:
+ layout: NormalLayout | ScreenshotLayout
+
+ if notification.app_name == "grimblast":
+ layout = ScreenshotLayout(notification)
+ else:
+ layout = NormalLayout(notification)
+
+ super().__init__(
+ css_classes=["notification"],
+ child=[layout],
+ )
+
+
+class Popup(Widget.Revealer):
+ def __init__(self, notification: Notification, **kwargs):
+ widget = NotificationWidget(notification)
+ super().__init__(child=widget, transition_type="slide_down", **kwargs)
+
+ notification.connect("closed", lambda x: self.destroy())
+
+ def destroy(self):
+ self.reveal_child = False
+ Utils.Timeout(self.transition_duration, self.unparent)
+
+
+def loading_notif_label() -> Widget.Label:
+ return Widget.Label(
+ label="Loading notifications...",
+ valign="center",
+ vexpand=True,
+ css_classes=["notification-center-info-label"],
+ )
+
+
+def no_notifications_label() -> Widget.Label:
+ return Widget.Label(
+ label="No notifications",
+ valign="center",
+ vexpand=True,
+ visible=notifications.bind("notifications", lambda value: len(value) == 0),
+ css_classes=["notification-center-info-label"],
+ )
+
+
+def on_notified(box: Widget.Box, notification: Notification) -> None:
+ notify = Popup(notification)
+ box.prepend(notify)
+ notify.reveal_child = True
+
+
+def load_notifications() -> list[Widget.Label | Popup]:
+ widgets = []
+ for i in notifications.notifications:
+ GLib.idle_add(lambda i=i: widgets.append(Popup(i, reveal_child=True)))
+ return widgets
+
+
+def notification_list() -> Widget.Box:
+ box = Widget.Box(
+ vertical=True,
+ child=[loading_notif_label()],
+ vexpand=True,
+ css_classes=["rec-unset"],
+ setup=lambda self: notifications.connect(
+ "notified",
+ lambda x, notification: on_notified(self, notification),
+ ),
+ )
+
+ Utils.ThreadTask(
+ load_notifications,
+ lambda result: box.set_child(result + [no_notifications_label()]),
+ ).run()
+
+ return box
+
+
+def notification_center() -> Widget.Box:
+ main_box = Widget.Box(
+ vertical=True,
+ vexpand=True,
+ css_classes=["notification-center"],
+ child=[
+ Widget.Box(
+ css_classes=["notification-center-header", "rec-unset"],
+ child=[
+ Widget.Label(
+ label=notifications.bind(
+ "notifications", lambda value: str(len(value))
+ ),
+ css_classes=["notification-count"],
+ ),
+ Widget.Label(
+ label="notifications",
+ css_classes=["notification-header-label"],
+ ),
+ Widget.Button(
+ child=Widget.Label(label="Clear all"),
+ halign="end",
+ hexpand=True,
+ on_click=lambda x: notifications.clear_all(),
+ css_classes=["notification-clear-all"],
+ ),
+ ],
+ ),
+ Widget.Scroll(
+ child=notification_list(),
+ vexpand=True,
+ ),
+ ],
+ )
+
+ return main_box
diff --git a/ignis/backup_04.03.2025/modules/control_center/qs_button.py b/ignis/backup_04.03.2025/modules/control_center/qs_button.py
new file mode 100644
index 0000000..381ddf5
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/control_center/qs_button.py
@@ -0,0 +1,64 @@
+from ignis.widgets import Widget
+from gi.repository import GObject # type: ignore
+from typing import Callable
+from ignis.gobject import Binding
+
+
+class QSButton(Widget.Button):
+ def __init__(
+ self,
+ label: str | Binding,
+ icon_name: str | Binding,
+ on_activate: Callable | None = None,
+ on_deactivate: Callable | None = None,
+ content: Widget.Revealer | None = None,
+ **kwargs,
+ ):
+ self.on_activate = on_activate
+ self.on_deactivate = on_deactivate
+ self._active = False
+ self._content = content
+ super().__init__(
+ child=Widget.Box(
+ child=[
+ Widget.Icon(image=icon_name),
+ Widget.Label(label=label, css_classes=["qs-button-label"]),
+ Widget.Arrow(
+ halign="end",
+ hexpand=True,
+ pixel_size=20,
+ rotated=content.bind("reveal_child"),
+ )
+ if content
+ else None,
+ ]
+ ),
+ on_click=self.__callback,
+ css_classes=["qs-button", "unset"],
+ hexpand=True,
+ **kwargs,
+ )
+
+ def __callback(self, *args) -> None:
+ if self.active:
+ if self.on_deactivate:
+ self.on_deactivate(self)
+ else:
+ if self.on_activate:
+ self.on_activate(self)
+
+ @GObject.Property
+ def active(self) -> bool:
+ return self._active
+
+ @active.setter
+ def active(self, value: bool) -> None:
+ self._active = value
+ if value:
+ self.add_css_class("active")
+ else:
+ self.remove_css_class("active")
+
+ @GObject.Property
+ def content(self) -> Widget.Revealer | None:
+ return self._content
diff --git a/ignis/backup_04.03.2025/modules/control_center/quick_settings.py b/ignis/backup_04.03.2025/modules/control_center/quick_settings.py
new file mode 100644
index 0000000..d2974dd
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/control_center/quick_settings.py
@@ -0,0 +1,64 @@
+from ignis.widgets import Widget
+from .wifi import wifi_control
+from .record import record_control
+from .dnd import dnd_button
+from .dark_mode import dark_mode_button
+from .ethernet import ethernet_control
+from .vpn import vpn_control
+from .qs_button import QSButton
+from ignis.services.network import NetworkService
+
+network = NetworkService.get_default()
+
+
+def add_button(main_box: Widget.Box, buttons: tuple[QSButton, ...], i: int) -> None:
+ row = Widget.Box(homogeneous=True)
+ if len(main_box.child) > 0:
+ row.style = "margin-top: 0.5rem;"
+
+ main_box.append(row)
+
+ button1 = buttons[i]
+ row.append(button1)
+
+ if button1.content:
+ main_box.append(button1.content)
+
+ if i + 1 < len(buttons):
+ button2 = buttons[i + 1]
+ button2.style = "margin-left: 0.5rem;"
+ row.append(button2)
+
+ if button2.content:
+ main_box.append(button2.content)
+
+
+def qs_fabric(main_box: Widget.Box, *buttons: QSButton) -> None:
+ for i in range(0, len(buttons), 2):
+ add_button(main_box, buttons, i)
+
+
+def qs_config(main_box: Widget.Box) -> None:
+ qs_fabric(
+ main_box,
+ *wifi_control(),
+ *ethernet_control(),
+ *vpn_control(),
+ dnd_button(),
+ dark_mode_button(),
+ record_control(),
+ )
+
+
+def update_box(main_box: Widget.Box):
+ main_box.child = []
+ qs_config(main_box)
+
+
+def quick_settings() -> Widget.Box:
+ main_box = Widget.Box(vertical=True, css_classes=["qs-main-box"])
+ network.wifi.connect("notify::devices", lambda x, y: update_box(main_box))
+ network.ethernet.connect("notify::devices", lambda x, y: update_box(main_box))
+ network.vpn.connect("notify::connections", lambda x, y: update_box(main_box))
+
+ return main_box
diff --git a/ignis/backup_04.03.2025/modules/control_center/record.py b/ignis/backup_04.03.2025/modules/control_center/record.py
new file mode 100644
index 0000000..b6226a4
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/control_center/record.py
@@ -0,0 +1,100 @@
+from ignis.widgets import Widget
+from .qs_button import QSButton
+from ignis.services.recorder import RecorderService
+
+recorder = RecorderService.get_default()
+
+
+def record_control() -> QSButton:
+ record_audio_switch = Widget.Switch(halign="end", hexpand=True, valign="center")
+ dropdown = Widget.DropDown(
+ items=["Internal audio", "Microphone", "Both sources"],
+ css_classes=["record-dropdown"],
+ )
+
+ def start_recording(record_menu: Widget.Revealer) -> None:
+ record_menu.set_reveal_child(False)
+ microphone = False
+ internal = False
+ if record_audio_switch.active:
+ if dropdown.selected == "Internal audio":
+ internal = True
+ elif dropdown.selected == "Microphone":
+ microphone = True
+ else:
+ internal = True
+ microphone = True
+
+ recorder.start_recording(
+ record_microphone=microphone, record_internal_audio=internal
+ )
+
+ record_menu = Widget.Revealer(
+ transition_duration=300,
+ transition_type="slide_down",
+ child=Widget.Box(
+ css_classes=["record-menu"],
+ vertical=True,
+ child=[
+ Widget.Icon(
+ image="media-record-symbolic",
+ pixel_size=36,
+ halign="center",
+ css_classes=["record-icon"],
+ ),
+ Widget.Label(
+ label="Start recording?",
+ halign="center",
+ style="font-size: 1.2rem;",
+ ),
+ Widget.Box(
+ style="margin-top: 0.5rem;",
+ child=[
+ Widget.Icon(
+ image="microphone-sensitivity-medium-symbolic",
+ pixel_size=20,
+ style="margin-right: 0.5rem;",
+ ),
+ Widget.Box(
+ vertical=True,
+ child=[
+ Widget.Label(
+ label="Record audio",
+ style="font-size: 1.1rem;",
+ halign="start",
+ ),
+ dropdown,
+ ],
+ ),
+ record_audio_switch,
+ ],
+ ),
+ Widget.Box(
+ style="margin-top: 1rem;",
+ child=[
+ Widget.Button(
+ child=Widget.Label(label="Cancel"),
+ css_classes=["record-cancel-button", "unset"],
+ on_click=lambda x: record_menu.set_reveal_child(False), # type: ignore
+ ),
+ Widget.Button(
+ child=Widget.Label(label="Start recording"),
+ halign="end",
+ hexpand=True,
+ css_classes=["record-start-button", "unset"],
+ on_click=lambda x: start_recording(record_menu), # type: ignore
+ ),
+ ],
+ ),
+ ],
+ ),
+ )
+
+ return QSButton(
+ label="Recording",
+ icon_name="media-record-symbolic",
+ on_activate=lambda x: record_menu.toggle(),
+ on_deactivate=lambda x: recorder.stop_recording(),
+ active=recorder.bind("active"),
+ content=record_menu,
+ )
diff --git a/ignis/backup_04.03.2025/modules/control_center/user.py b/ignis/backup_04.03.2025/modules/control_center/user.py
new file mode 100644
index 0000000..21ab734
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/control_center/user.py
@@ -0,0 +1,76 @@
+import os
+from ignis.widgets import Widget
+from ignis.utils import Utils
+from ignis.app import IgnisApp
+from ignis.services.fetch import FetchService
+from ..settings import settings_window
+from options import avatar_opt
+
+fetch = FetchService.get_default()
+app = IgnisApp.get_default()
+
+
+def format_uptime(value):
+ days, hours, minutes, seconds = value
+ if days:
+ return f"up {days:02}:{hours:02}:{minutes:02}"
+ else:
+ return f"up {hours:02}:{minutes:02}"
+
+
+def user_image() -> Widget.Picture:
+ return Widget.Picture(
+ image=avatar_opt.bind(
+ "value",
+ lambda value: "user-info" if not os.path.exists(value) else value,
+ ),
+ width=44,
+ height=44,
+ content_fit="cover",
+ style="border-radius: 10rem;",
+ )
+
+
+def username() -> Widget.Box:
+ return Widget.Box(
+ child=[
+ Widget.Label(
+ label=os.getenv("USER"), css_classes=["user-name"], halign="start"
+ ),
+ Widget.Label(
+ label=Utils.Poll(
+ timeout=60 * 1000, callback=lambda x: fetch.uptime
+ ).bind("output", lambda value: format_uptime(value)),
+ halign="start",
+ css_classes=["user-name-secondary"],
+ ),
+ ],
+ vertical=True,
+ css_classes=["user-name-box"],
+ )
+
+
+def settings_button() -> Widget.Button:
+ return Widget.Button(
+ child=Widget.Icon(image="emblem-system-symbolic", pixel_size=20),
+ halign="end",
+ hexpand=True,
+ css_classes=["user-settings", "unset"],
+ on_click=lambda x: settings_window(),
+ )
+
+
+def power_button() -> Widget.Button:
+ return Widget.Button(
+ child=Widget.Icon(image="system-shutdown-symbolic", pixel_size=20),
+ halign="end",
+ css_classes=["user-power", "unset"],
+ on_click=lambda x: app.toggle_window("ignis_POWERMENU"),
+ )
+
+
+def user() -> Widget.Box:
+ return Widget.Box(
+ child=[user_image(), username(), settings_button(), power_button()],
+ css_classes=["user"],
+ )
diff --git a/ignis/backup_04.03.2025/modules/control_center/volume.py b/ignis/backup_04.03.2025/modules/control_center/volume.py
new file mode 100644
index 0000000..bb771f5
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/control_center/volume.py
@@ -0,0 +1,161 @@
+from ignis.widgets import Widget
+from ignis.utils import Utils
+from ignis.services.audio import AudioService, Stream
+
+audio = AudioService.get_default()
+
+
+def volume_scale(stream: Stream) -> Widget.Scale:
+ return Widget.Scale(
+ css_classes=["material-slider"],
+ value=stream.bind("volume"),
+ step=5,
+ hexpand=True,
+ on_change=lambda x: stream.set_volume(x.value),
+ sensitive=stream.bind("is_muted", lambda value: not value),
+ )
+
+
+def device_entry(stream: Stream, _type: str) -> Widget.Button:
+ widget = Widget.Button(
+ child=Widget.Box(
+ child=[
+ Widget.Icon(image="audio-card-symbolic"),
+ Widget.Label(
+ label=stream.description,
+ ellipsize="end",
+ max_width_chars=30,
+ halign="start",
+ css_classes=["volume-entry-label"],
+ ),
+ Widget.Icon(
+ image="object-select-symbolic",
+ halign="end",
+ hexpand=True,
+ visible=stream.bind("is_default"),
+ ),
+ ]
+ ),
+ css_classes=["volume-entry", "unset"],
+ hexpand=True,
+ on_click=lambda x: setattr(audio, _type, stream),
+ )
+ stream.connect("removed", lambda x: widget.unparent())
+
+ return widget
+
+
+def device_list(
+ header_label: str, header_icon: str, _type: str, **kwargs
+) -> Widget.Revealer:
+ box = Widget.Box(
+ css_classes=["control-center-menu"],
+ vertical=True,
+ child=[
+ Widget.Box(
+ child=[
+ Widget.Icon(image=header_icon, pixel_size=24),
+ Widget.Label(
+ label=header_label,
+ halign="start",
+ css_classes=["volume-entry-list-header-label"],
+ ),
+ ],
+ css_classes=["volume-entry-list-header-box"],
+ ),
+ Widget.Box(
+ vertical=True,
+ setup=lambda self: audio.connect(
+ f"{_type}-added",
+ lambda x, stream: self.append(device_entry(stream, _type)),
+ ),
+ ),
+ Widget.Separator(css_classes=["volume-entry-list-separator"]),
+ Widget.Button(
+ child=Widget.Box(
+ child=[
+ Widget.Icon(image="preferences-system-symbolic"),
+ Widget.Label(
+ label="Sound Settings",
+ halign="start",
+ css_classes=["volume-entry-label"],
+ ),
+ ]
+ ),
+ css_classes=["volume-entry", "unset"],
+ style="margin-bottom: 0;",
+ on_click=lambda x: Utils.exec_sh_async("pavucontrol"),
+ ),
+ ],
+ **kwargs,
+ )
+
+ return Widget.Revealer(
+ child=box, transition_type="slide_down", transition_duration=300
+ )
+
+
+def volume_icon(stream: Stream) -> Widget.Button:
+ return Widget.Button(
+ child=Widget.Icon(
+ image=stream.bind("icon_name"),
+ pixel_size=18,
+ ),
+ css_classes=["material-slider-icon", "unset", "hover-surface"],
+ on_click=lambda x: stream.set_is_muted(not stream.is_muted),
+ )
+
+
+def device_list_arrow(device_list: Widget.Revealer) -> Widget.Button:
+ return Widget.ArrowButton(
+ arrow=Widget.Arrow(pixel_size=20),
+ css_classes=["material-slider-arrow", "hover-surface"],
+ on_click=lambda x: device_list.toggle(),
+ )
+
+
+def volume_control():
+ speakers_list = device_list(
+ header_label="Sound Output",
+ header_icon="audio-headphones-symbolic",
+ _type="speaker",
+ style="margin-bottom: 1rem;",
+ )
+
+ microphones_list = device_list(
+ header_label="Sound Input",
+ header_icon="microphone-sensitivity-high-symbolic",
+ _type="microphone",
+ )
+
+ speaker_icon = volume_icon(audio.speaker)
+ microphone_icon = volume_icon(audio.microphone)
+
+ speaker_scale = volume_scale(audio.speaker)
+ microphone_scale = volume_scale(audio.microphone)
+
+ speaker_arrow = device_list_arrow(speakers_list)
+ microphone_arrow = device_list_arrow(microphones_list)
+
+ speaker_control = Widget.Box(
+ vertical=True,
+ child=[
+ Widget.Box(child=[speaker_icon, speaker_scale, speaker_arrow]),
+ speakers_list,
+ ],
+ style="margin-top: 1rem;",
+ )
+
+ microphone_control = Widget.Box(
+ vertical=True,
+ child=[
+ Widget.Box(child=[microphone_icon, microphone_scale, microphone_arrow]),
+ microphones_list,
+ ],
+ style="margin-top: 0.25rem;",
+ )
+
+ return Widget.Box(
+ vertical=True,
+ child=[speaker_control, microphone_control],
+ )
diff --git a/ignis/backup_04.03.2025/modules/control_center/vpn.py b/ignis/backup_04.03.2025/modules/control_center/vpn.py
new file mode 100644
index 0000000..d2167a4
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/control_center/vpn.py
@@ -0,0 +1,120 @@
+from ignis.widgets import Widget
+from ignis.utils import Utils
+from .qs_button import QSButton
+from typing import List
+from ignis.services.network import NetworkService, VpnConnection
+
+
+network = NetworkService.get_default()
+
+
+class VpnNetworkItem(Widget.Button):
+ def __init__(self, conn: VpnConnection):
+
+ super().__init__(
+ css_classes=["vpn-connection-item", "unset"],
+ on_click=lambda x: conn.toggle_connection(),
+ child=Widget.Box(
+ child=[
+ Widget.Label(
+ label=conn.name,
+ ellipsize="end",
+ max_width_chars=20,
+ halign="start",
+ css_classes=["vpn-connection-label"],
+ ),
+ Widget.Button(
+ child=Widget.Label(
+ label=conn.bind(
+ "is_connected",
+ lambda value: "Disconnect" if value else "Connect",
+ )
+ ),
+ css_classes=["vpn-connection-item-connect-label", "unset"],
+ halign="end",
+ hexpand=True,
+ ),
+ ]
+ ),
+ )
+
+
+def vpn_qsbutton() -> QSButton:
+ networks_list = Widget.Revealer(
+ transition_duration=300,
+ transition_type="slide_down",
+ child=Widget.Box(
+ vertical=True,
+ css_classes=["control-center-menu"],
+ child=[
+ Widget.Box(
+ css_classes=["vpn-header-box"],
+ child=[
+ Widget.Icon(
+ icon_name="network-vpn-symbolic", pixel_size=28),
+ Widget.Label(
+ label="VPN connections",
+ css_classes=["vpn-header-label"],
+ ),
+ ],
+ ),
+ Widget.Box(
+ vertical=True,
+ child=network.vpn.bind(
+ "connections",
+ transform=lambda value: [
+ VpnNetworkItem(i) for i in value],
+ ),
+ ),
+ Widget.Separator(
+ css_classes=["vpn-connection-list-separator"]),
+ Widget.Button(
+ css_classes=["vpn-connection-item", "unset"],
+ on_click=lambda x: Utils.exec_sh_async(
+ "nm-connection-editor"),
+ style="margin-bottom: 0;",
+ child=Widget.Box(
+ child=[
+ Widget.Icon(image="preferences-system-symbolic"),
+ Widget.Label(
+ label="Network Manager",
+ halign="start",
+ css_classes=["vpn-connection-label"],
+ ),
+ ]
+ ),
+ ),
+ ],
+ ),
+ )
+
+ def get_label(id: str) -> str:
+ if id:
+ return id
+ else:
+ return "VPN"
+
+ def get_icon(icon_name: str) -> str:
+ if network.vpn.is_connected:
+ return icon_name
+ else:
+ return "network-vpn-symbolic"
+
+ def toggle_list(x) -> None:
+ networks_list.toggle()
+
+ return QSButton(
+ label=network.vpn.bind("active_vpn_id", get_label),
+ icon_name=network.vpn.bind("icon-name", get_icon),
+ on_activate=toggle_list,
+ on_deactivate=toggle_list,
+ active=network.vpn.bind("is-connected"),
+ content=networks_list,
+ )
+
+
+def vpn_control() -> List[QSButton]:
+ if len(network.vpn.connections) > 0:
+ return [vpn_qsbutton()]
+ else:
+ return []
diff --git a/ignis/backup_04.03.2025/modules/control_center/wifi.py b/ignis/backup_04.03.2025/modules/control_center/wifi.py
new file mode 100644
index 0000000..70f4314
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/control_center/wifi.py
@@ -0,0 +1,112 @@
+from ignis.widgets import Widget
+from ignis.utils import Utils
+from .qs_button import QSButton
+from ignis.services.network import NetworkService, WifiAccessPoint, WifiDevice
+from typing import List
+
+network = NetworkService.get_default()
+
+
+class WifiNetworkItem(Widget.Button):
+ def __init__(self, access_point: WifiAccessPoint):
+ super().__init__(
+ css_classes=["wifi-network-item", "unset"],
+ on_click=lambda x: access_point.connect_to_graphical(),
+ child=Widget.Box(
+ child=[
+ Widget.Icon(
+ image=access_point.bind(
+ "strength", transform=lambda value: access_point.icon_name
+ ),
+ ),
+ Widget.Label(
+ label=access_point.ssid,
+ halign="start",
+ css_classes=["wifi-network-label"],
+ ),
+ Widget.Icon(
+ image="object-select-symbolic",
+ halign="end",
+ hexpand=True,
+ visible=access_point.bind("is_connected"),
+ ),
+ ]
+ ),
+ )
+
+
+def wifi_qsbutton(device: WifiDevice) -> QSButton:
+ networks_list = Widget.Revealer(
+ transition_duration=300,
+ transition_type="slide_down",
+ child=Widget.Box(
+ vertical=True,
+ css_classes=["control-center-menu"],
+ child=[
+ Widget.Box(
+ child=[
+ Widget.Label(label="Wi-Fi", css_classes=["wifi-header-label"]),
+ Widget.Switch(
+ halign="end",
+ hexpand=True,
+ active=network.wifi.enabled,
+ on_change=lambda x, state: network.wifi.set_enabled(state),
+ ),
+ ],
+ css_classes=["toggle-box", "wifi-header-box"],
+ ),
+ Widget.Box(
+ vertical=True,
+ child=device.bind(
+ "access_points",
+ transform=lambda value: [WifiNetworkItem(i) for i in value],
+ ),
+ ),
+ Widget.Separator(css_classes=["wifi-network-list-separator"]),
+ Widget.Button(
+ css_classes=["wifi-network-item", "unset"],
+ on_click=lambda x: Utils.exec_sh_async("nm-connection-editor"),
+ style="margin-bottom: 0;",
+ child=Widget.Box(
+ child=[
+ Widget.Icon(image="preferences-system-symbolic"),
+ Widget.Label(
+ label="Network Settings",
+ halign="start",
+ css_classes=["wifi-network-label"],
+ ),
+ ]
+ ),
+ ),
+ ],
+ ),
+ )
+
+ def get_label(ssid: str) -> str:
+ if ssid:
+ return ssid
+ else:
+ return "Wi-Fi"
+
+ def get_icon(icon_name: str) -> str:
+ if device.ap.is_connected:
+ return icon_name
+ else:
+ return "network-wireless-symbolic"
+
+ def toggle_list(x) -> None:
+ device.scan()
+ networks_list.toggle()
+
+ return QSButton(
+ label=device.ap.bind("ssid", get_label),
+ icon_name=device.ap.bind("icon-name", get_icon),
+ on_activate=toggle_list,
+ on_deactivate=toggle_list,
+ active=network.wifi.bind("enabled"),
+ content=networks_list,
+ )
+
+
+def wifi_control() -> List[QSButton]:
+ return [wifi_qsbutton(dev) for dev in network.wifi.devices]
diff --git a/ignis/backup_04.03.2025/modules/launcher/__init__.py b/ignis/backup_04.03.2025/modules/launcher/__init__.py
new file mode 100644
index 0000000..48480a1
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/launcher/__init__.py
@@ -0,0 +1,212 @@
+import re
+from ignis.widgets import Widget
+from ignis.app import IgnisApp
+from ignis.services.applications import (
+ ApplicationsService,
+ Application,
+ ApplicationAction,
+)
+from ignis.utils import Utils
+from gi.repository import Gio # type: ignore
+
+app = IgnisApp.get_default()
+
+applications = ApplicationsService.get_default()
+
+
+def is_url(url: str) -> bool:
+ regex = re.compile(
+ r"^(?:http|ftp)s?://" # http:// or https://
+ r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|" # domain
+ r"localhost|" # localhost
+ r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|" # or ipv4
+ r"\[?[A-F0-9]*:[A-F0-9:]+\]?)" # or ipv6
+ r"(?::\d+)?" # optional port
+ r"(?:/?|[/?]\S+)$",
+ re.IGNORECASE,
+ )
+
+ return re.match(regex, url) is not None
+
+
+class LauncherAppItem(Widget.Button):
+ def __init__(self, application: Application) -> None:
+ self._application = application
+ super().__init__(
+ on_click=lambda x: self.launch(),
+ on_right_click=lambda x: self._menu.popup(),
+ css_classes=["launcher-app"],
+ child=Widget.Box(
+ child=[
+ Widget.Icon(image=application.icon, pixel_size=48),
+ Widget.Label(
+ label=application.name,
+ ellipsize="end",
+ max_width_chars=30,
+ css_classes=["launcher-app-label"],
+ ),
+ ]
+ ),
+ )
+ self.__sync_menu()
+ application.connect("notify::is-pinned", lambda x, y: self.__sync_menu())
+
+ def launch(self) -> None:
+ self._application.launch()
+ app.close_window("ignis_LAUNCHER")
+
+ def launch_action(self, action: ApplicationAction) -> None:
+ action.launch()
+ app.close_window("ignis_LAUNCHER")
+
+ def __sync_menu(self) -> None:
+ self._menu = Widget.PopoverMenu(
+ items=[
+ Widget.MenuItem(label="Launch", on_activate=lambda x: self.launch()),
+ Widget.Separator(),
+ ]
+ + [
+ Widget.MenuItem(
+ label=i.name,
+ on_activate=lambda x, action=i: self.launch_action(action),
+ )
+ for i in self._application.actions
+ ]
+ + [
+ Widget.Separator(),
+ Widget.MenuItem(
+ label="Pin", on_activate=lambda x: self._application.pin()
+ )
+ if not self._application.is_pinned
+ else Widget.MenuItem(
+ label="Unpin", on_activate=lambda x: self._application.unpin()
+ ),
+ ]
+ )
+ self.child.append(self._menu)
+
+
+class SearchWebButton(Widget.Button):
+ def __init__(self, query: str):
+ self._query = query
+ self._url = ""
+
+ browser_desktop_file = Utils.exec_sh(
+ "xdg-settings get default-web-browser"
+ ).stdout.replace("\n", "")
+
+ app_info = Gio.DesktopAppInfo.new(desktop_id=browser_desktop_file)
+
+ icon_name = "applications-internet-symbolic"
+ if app_info:
+ icon_string = app_info.get_string("Icon")
+ if icon_string:
+ icon_name = icon_string
+
+ if not query.startswith(("http://", "https://")) and "." in query:
+ query = "https://" + query
+
+ if is_url(query):
+ label = f"Visit {query}"
+ self._url = query
+ else:
+ label = "Search in Google"
+ self._url = f"https://www.google.com/search?q={query.replace(' ', '+')}"
+
+ super().__init__(
+ on_click=lambda x: self.launch(),
+ css_classes=["launcher-app"],
+ child=Widget.Box(
+ child=[
+ Widget.Icon(image=icon_name, pixel_size=48),
+ Widget.Label(
+ label=label,
+ css_classes=["launcher-app-label"],
+ ),
+ ]
+ ),
+ )
+
+ def launch(self) -> None:
+ Utils.exec_sh_async(f"xdg-open {self._url}")
+ app.close_window("ignis_LAUNCHER")
+
+
+def launcher() -> Widget.Window:
+ def search(entry: Widget.Entry, app_list: Widget.Box) -> None:
+ query = entry.text
+
+ if query == "":
+ entry.grab_focus()
+ app_list.visible = False
+ return
+
+ apps = applications.search(applications.apps, query)
+ if apps == []:
+ app_list.child = [SearchWebButton(query)]
+ else:
+ app_list.visible = True
+ app_list.child = [LauncherAppItem(i) for i in apps[:5]]
+
+ def on_open(window: Widget.Window, entry: Widget.Entry) -> None:
+ if not window.visible:
+ return
+
+ entry.text = ""
+ entry.grab_focus()
+
+ app_list = Widget.Box(vertical=True, visible=False, style="margin-top: 1rem;")
+ entry = Widget.Entry(
+ hexpand=True,
+ placeholder_text="Search",
+ css_classes=["launcher-search"],
+ on_change=lambda x: search(x, app_list),
+ on_accept=lambda x: app_list.child[0].launch()
+ if len(app_list.child) > 0
+ else None,
+ )
+
+ main_box = Widget.Box(
+ vertical=True,
+ valign="start",
+ halign="center",
+ css_classes=["launcher"],
+ child=[
+ Widget.Box(
+ css_classes=["launcher-search-box"],
+ child=[
+ Widget.Icon(
+ icon_name="system-search-symbolic",
+ pixel_size=24,
+ style="margin-right: 0.5rem;",
+ ),
+ entry,
+ ],
+ ),
+ app_list,
+ ],
+ )
+
+ return Widget.Window(
+ namespace="ignis_LAUNCHER",
+ visible=False,
+ popup=True,
+ kb_mode="on_demand",
+ # exclusivity="ignore",
+ css_classes=["unset"],
+ setup=lambda self: self.connect(
+ "notify::visible", lambda x, y: on_open(self, entry)
+ ),
+ anchor=["top", "right", "bottom", "left"],
+ child=Widget.Overlay(
+ child=Widget.Button(
+ vexpand=True,
+ hexpand=True,
+ can_focus=False,
+ css_classes=["unset"],
+ on_click=lambda x: app.close_window("ignis_LAUNCHER"),
+ style="background-color: rgba(0, 0, 0, 0.3);",
+ ),
+ overlays=[main_box],
+ ),
+ )
diff --git a/ignis/backup_04.03.2025/modules/launcher/__pycache__/__init__.cpython-312.pyc b/ignis/backup_04.03.2025/modules/launcher/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..5e84f1d
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/launcher/__pycache__/__init__.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/launcher/__pycache__/__init__.cpython-313.pyc b/ignis/backup_04.03.2025/modules/launcher/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..224d862
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/launcher/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/notification_popup/__init__.py b/ignis/backup_04.03.2025/modules/notification_popup/__init__.py
new file mode 100644
index 0000000..8df12b3
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/notification_popup/__init__.py
@@ -0,0 +1,95 @@
+from ignis.widgets import Widget
+from ignis.app import IgnisApp
+from ignis.utils import Utils
+from ignis.services.notifications import Notification, NotificationService
+from ..control_center.notification_center import NotificationWidget
+
+
+app = IgnisApp.get_default()
+
+notifications = NotificationService.get_default()
+
+
+class Popup(Widget.Box):
+ def __init__(self, notification: Notification):
+ widget = NotificationWidget(notification)
+ widget.css_classes = ["notification-popup"]
+ self._inner = Widget.Revealer(transition_type="slide_left", child=widget)
+ self._outer = Widget.Revealer(transition_type="slide_down", child=self._inner)
+ super().__init__(child=[self._outer], halign="end")
+
+ notification.connect("dismissed", lambda x: self.destroy())
+
+ def destroy(self):
+ def box_destroy():
+ box: Widget.Box = self.get_parent() # type: ignore
+ if not box:
+ return
+
+ self.unparent()
+ if len(notifications.popups) == 0:
+ window: Widget.Window = box.get_parent() # type: ignore
+ if not window:
+ return
+ window.visible = False
+ else:
+ change_window_input_region(box)
+
+ def outer_close():
+ self._outer.reveal_child = False
+ Utils.Timeout(self._outer.transition_duration, box_destroy)
+
+ self._inner.transition_type = "crossfade"
+ self._inner.reveal_child = False
+ Utils.Timeout(self._outer.transition_duration, outer_close)
+
+
+def on_notified(box: Widget.Box, notification: Notification, monitor: int) -> None:
+ window: Widget.Window = app.get_window("ignis_CONTROL_CENTER") # type: ignore
+ if window.visible and window.monitor == monitor:
+ return
+
+ app.open_window(f"ignis_NOTIFICATION_POPUP_{monitor}")
+ popup = Popup(notification)
+ box.prepend(popup)
+ popup._outer.reveal_child = True
+ Utils.Timeout(popup._outer.transition_duration, reveal_popup, box, popup)
+
+
+def reveal_popup(box: Widget.Box, popup: Popup) -> None:
+ popup._inner.set_reveal_child(True)
+ change_window_input_region(box)
+
+
+def change_window_input_region(box: Widget.Box) -> None:
+ def callback() -> None:
+ width = box.get_width()
+ height = box.get_height()
+ window: Widget.Window = box.get_parent() # type: ignore
+
+ window.input_width = width
+ window.input_height = height
+
+ Utils.Timeout(ms=50, target=callback)
+
+
+def notification_popup(monitor: int) -> Widget.Window:
+ notifications_box = Widget.Box(
+ vertical=True,
+ valign="start",
+ setup=lambda self: notifications.connect(
+ "new_popup",
+ lambda x, notification: on_notified(self, notification, monitor),
+ ),
+ )
+
+ return Widget.Window(
+ anchor=["right", "top", "bottom"],
+ monitor=monitor,
+ namespace=f"ignis_NOTIFICATION_POPUP_{monitor}",
+ layer="top",
+ child=notifications_box,
+ visible=False,
+ css_classes=["rec-unset"],
+ style="min-width: 29rem;",
+ )
diff --git a/ignis/backup_04.03.2025/modules/notification_popup/__pycache__/__init__.cpython-312.pyc b/ignis/backup_04.03.2025/modules/notification_popup/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..8645f05
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/notification_popup/__pycache__/__init__.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/notification_popup/__pycache__/__init__.cpython-313.pyc b/ignis/backup_04.03.2025/modules/notification_popup/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..0ef8d4c
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/notification_popup/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/osd/__init__.py b/ignis/backup_04.03.2025/modules/osd/__init__.py
new file mode 100644
index 0000000..a41e2d3
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/osd/__init__.py
@@ -0,0 +1,43 @@
+from ignis.widgets import Widget
+from ignis.utils import Utils
+from ignis.services.audio import AudioService
+
+audio = AudioService.get_default()
+
+
+class OSD(Widget.Window):
+ def __init__(self):
+ self._timer = None
+ super().__init__(
+ layer="overlay",
+ anchor=["bottom"],
+ namespace="ignis_OSD",
+ visible=False,
+ css_classes=["rec-unset"],
+ child=Widget.Box(
+ css_classes=["osd"],
+ child=[
+ Widget.Icon(
+ pixel_size=26,
+ style="margin-right: 0.5rem;",
+ image=audio.speaker.bind("icon_name"),
+ ),
+ Widget.Scale(
+ value=audio.speaker.bind("volume"),
+ css_classes=["material-slider"],
+ sensitive=False,
+ hexpand=True,
+ ),
+ ],
+ ),
+ )
+
+ def set_property(self, property_name, value):
+ if property_name == "visible":
+ if self._timer:
+ self._timer.cancel()
+ self._timer = None
+
+ self._timer = Utils.Timeout(3000, self.set_visible, False)
+
+ super().set_property(property_name, value)
diff --git a/ignis/backup_04.03.2025/modules/osd/__pycache__/__init__.cpython-312.pyc b/ignis/backup_04.03.2025/modules/osd/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..63d962d
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/osd/__pycache__/__init__.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/osd/__pycache__/__init__.cpython-313.pyc b/ignis/backup_04.03.2025/modules/osd/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..221b777
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/osd/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/powermenu/__init__.py b/ignis/backup_04.03.2025/modules/powermenu/__init__.py
new file mode 100644
index 0000000..cdc4825
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/powermenu/__init__.py
@@ -0,0 +1,98 @@
+from ignis.widgets import Widget
+from ignis.utils import Utils
+from ignis.app import IgnisApp
+from typing import Callable
+
+app = IgnisApp.get_default()
+
+
+class PowermenuButton(Widget.Box):
+ def __init__(self, label: str, icon_name: str, on_click: Callable) -> None:
+ super().__init__(
+ child=[
+ Widget.Button(
+ child=Widget.Icon(image=icon_name, pixel_size=36),
+ on_click=on_click,
+ css_classes=["powermenu-button", "unset"],
+ ),
+ Widget.Label(label=label, css_classes=["powermenu-button-label"]),
+ ],
+ vertical=True,
+ css_classes=["powermenu-button-box"],
+ )
+
+
+def poweroff(*args) -> None:
+ Utils.exec_sh_async("poweroff")
+
+
+def reboot(*args) -> None:
+ Utils.exec_sh_async("reboot")
+
+
+def suspend(*args) -> None:
+ app.close_window("ignis_POWERMENU")
+ Utils.exec_sh_async("systemctl suspend && hyprlock")
+
+
+def hypr_exit(*args) -> None:
+ Utils.exec_sh_async("hyprctl dispatch exit 0")
+
+
+def powermenu():
+ return Widget.Window(
+ popup=True,
+ kb_mode="on_demand",
+ namespace="ignis_POWERMENU",
+ exclusivity="ignore",
+ anchor=["left", "right", "top", "bottom"],
+ visible=False,
+ child=Widget.Overlay(
+ child=Widget.Button(
+ vexpand=True,
+ hexpand=True,
+ can_focus=False,
+ css_classes=["unset", "powermenu-overlay"],
+ on_click=lambda x: app.close_window("ignis_POWERMENU"),
+ ),
+ overlays=[
+ Widget.Box(
+ vertical=True,
+ valign="center",
+ halign="center",
+ css_classes=["powermenu"],
+ child=[
+ Widget.Box(
+ child=[
+ PowermenuButton(
+ label="Power off",
+ icon_name="system-shutdown-symbolic",
+ on_click=poweroff,
+ ),
+ PowermenuButton(
+ label="Reboot",
+ icon_name="system-reboot-symbolic",
+ on_click=reboot,
+ ),
+ ]
+ ),
+ Widget.Box(
+ child=[
+ PowermenuButton(
+ label="Suspend",
+ icon_name="night-light-symbolic",
+ on_click=suspend,
+ ),
+ PowermenuButton(
+ label="Sign out",
+ icon_name="system-log-out-symbolic",
+ on_click=hypr_exit,
+ ),
+ ]
+ ),
+ ],
+ )
+ ],
+ ),
+ css_classes=["unset"],
+ )
diff --git a/ignis/backup_04.03.2025/modules/powermenu/__pycache__/__init__.cpython-312.pyc b/ignis/backup_04.03.2025/modules/powermenu/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..45609b8
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/powermenu/__pycache__/__init__.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/powermenu/__pycache__/__init__.cpython-313.pyc b/ignis/backup_04.03.2025/modules/powermenu/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..b35c7be
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/powermenu/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/settings/__init__.py b/ignis/backup_04.03.2025/modules/settings/__init__.py
new file mode 100644
index 0000000..941ab0c
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/settings/__init__.py
@@ -0,0 +1,83 @@
+from ignis.widgets import Widget
+from ignis.gobject import IgnisGObject
+from gi.repository import GObject # type: ignore
+from .notifications import notifications_entry
+from .about import about_entry
+from .appearance import appearance_entry
+from .recorder import recorder_entry
+from .user import user_entry
+from .elements import SettingsPage
+from ignis.app import IgnisApp
+from ignis.exceptions import WindowNotFoundError
+from options import settings_last_page
+
+app = IgnisApp.get_default()
+
+
+class ActivePage(IgnisGObject):
+ def __init__(self, name: str | None, page: SettingsPage | None):
+ super().__init__()
+ self._name = name
+ self._page = page
+
+ @GObject.Property
+ def name(self) -> str | None:
+ return self._name
+
+ @name.setter
+ def name(self, value: str | None) -> None:
+ self._name = value
+
+ @GObject.Property
+ def page(self) -> SettingsPage | None:
+ return self._page
+
+ @page.setter
+ def page(self, value: SettingsPage | None) -> None:
+ self._page = value
+
+
+def settings_widget():
+ active_page = ActivePage(name="Settings", page=None)
+ content = Widget.Box(
+ hexpand=True,
+ vexpand=True,
+ child=active_page.bind("page", transform=lambda value: [value]),
+ )
+ listbox = Widget.ListBox(
+ rows=[
+ notifications_entry(active_page),
+ recorder_entry(active_page),
+ appearance_entry(active_page),
+ user_entry(active_page),
+ about_entry(active_page),
+ ],
+ )
+
+ listbox.select_row(listbox.rows[settings_last_page.value])
+
+ navigation_sidebar = Widget.Box(
+ vertical=True,
+ css_classes=["settings-sidebar"],
+ child=[
+ Widget.Label(
+ label="Settings", halign="start", css_classes=["settings-sidebar-label"]
+ ),
+ listbox,
+ ],
+ )
+
+ return Widget.Box(child=[navigation_sidebar, content])
+
+
+def settings_window():
+ try:
+ app.get_window("ignis_SETTINGS")
+ except WindowNotFoundError:
+ return Widget.RegularWindow(
+ default_width=900,
+ default_height=600,
+ resizable=False,
+ child=settings_widget(),
+ namespace="ignis_SETTINGS",
+ )
diff --git a/ignis/backup_04.03.2025/modules/settings/__pycache__/__init__.cpython-312.pyc b/ignis/backup_04.03.2025/modules/settings/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..65d08f5
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/settings/__pycache__/__init__.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/settings/__pycache__/__init__.cpython-313.pyc b/ignis/backup_04.03.2025/modules/settings/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..e53b964
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/settings/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/settings/__pycache__/about.cpython-312.pyc b/ignis/backup_04.03.2025/modules/settings/__pycache__/about.cpython-312.pyc
new file mode 100644
index 0000000..5c60a1c
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/settings/__pycache__/about.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/settings/__pycache__/about.cpython-313.pyc b/ignis/backup_04.03.2025/modules/settings/__pycache__/about.cpython-313.pyc
new file mode 100644
index 0000000..8f41b1d
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/settings/__pycache__/about.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/settings/__pycache__/appearance.cpython-312.pyc b/ignis/backup_04.03.2025/modules/settings/__pycache__/appearance.cpython-312.pyc
new file mode 100644
index 0000000..73628eb
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/settings/__pycache__/appearance.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/settings/__pycache__/appearance.cpython-313.pyc b/ignis/backup_04.03.2025/modules/settings/__pycache__/appearance.cpython-313.pyc
new file mode 100644
index 0000000..ee4eab4
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/settings/__pycache__/appearance.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/settings/__pycache__/notifications.cpython-312.pyc b/ignis/backup_04.03.2025/modules/settings/__pycache__/notifications.cpython-312.pyc
new file mode 100644
index 0000000..05de33c
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/settings/__pycache__/notifications.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/settings/__pycache__/notifications.cpython-313.pyc b/ignis/backup_04.03.2025/modules/settings/__pycache__/notifications.cpython-313.pyc
new file mode 100644
index 0000000..3395837
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/settings/__pycache__/notifications.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/settings/__pycache__/recorder.cpython-312.pyc b/ignis/backup_04.03.2025/modules/settings/__pycache__/recorder.cpython-312.pyc
new file mode 100644
index 0000000..339f312
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/settings/__pycache__/recorder.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/settings/__pycache__/recorder.cpython-313.pyc b/ignis/backup_04.03.2025/modules/settings/__pycache__/recorder.cpython-313.pyc
new file mode 100644
index 0000000..108da2c
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/settings/__pycache__/recorder.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/settings/__pycache__/user.cpython-312.pyc b/ignis/backup_04.03.2025/modules/settings/__pycache__/user.cpython-312.pyc
new file mode 100644
index 0000000..938eed2
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/settings/__pycache__/user.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/settings/__pycache__/user.cpython-313.pyc b/ignis/backup_04.03.2025/modules/settings/__pycache__/user.cpython-313.pyc
new file mode 100644
index 0000000..a0d562d
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/settings/__pycache__/user.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/settings/about.py b/ignis/backup_04.03.2025/modules/settings/about.py
new file mode 100644
index 0000000..7fc529e
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/settings/about.py
@@ -0,0 +1,44 @@
+from .elements import SettingsPage, SettingsRow, SettingsEntry
+from ignis.utils import Utils
+from ignis.widgets import Widget
+from services.material import MaterialService
+from ignis.services.fetch import FetchService
+
+fetch = FetchService.get_default()
+material = MaterialService.get_default()
+
+
+def about_entry(active_page):
+ about_page = SettingsPage(
+ name="About",
+ groups=[
+ Widget.Box(
+ child=[
+ Widget.Picture(
+ image=material.bind(
+ "dark_mode",
+ transform=lambda value: fetch.os_logo_text_dark
+ if value
+ else fetch.os_logo_text,
+ ),
+ width=300,
+ height=100,
+ )
+ ],
+ halign="center",
+ width_request=300,
+ height_request=100,
+ ),
+ SettingsRow(label="OS", sublabel=fetch.os_name),
+ SettingsRow(label="Ignis version", sublabel=Utils.get_ignis_version()),
+ SettingsRow(label="Session type", sublabel=fetch.session_type),
+ SettingsRow(label="Wayland compositor", sublabel=fetch.current_desktop),
+ SettingsRow(label="Kernel", sublabel=fetch.kernel),
+ ],
+ )
+ return SettingsEntry(
+ label="About",
+ icon="help-about-symbolic",
+ active_page=active_page,
+ page=about_page,
+ )
diff --git a/ignis/backup_04.03.2025/modules/settings/appearance.py b/ignis/backup_04.03.2025/modules/settings/appearance.py
new file mode 100644
index 0000000..492eddd
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/settings/appearance.py
@@ -0,0 +1,64 @@
+import os
+from services.material import MaterialService
+from .elements import SwitchRow, SettingsPage, SettingsGroup, FileRow, SettingsEntry
+from ignis.widgets import Widget
+from ignis.services.wallpaper import WallpaperService
+
+wallpaper = WallpaperService.get_default()
+material = MaterialService.get_default()
+
+
+def appearance_entry(active_page):
+ appearance_page = SettingsPage(
+ name="Appearance",
+ groups=[
+ SettingsGroup(
+ name=None,
+ rows=[
+ Widget.ListBoxRow(
+ child=Widget.Picture(
+ image=wallpaper.bind("wallpaper"),
+ width=1920 // 4,
+ height=1080 // 4,
+ halign="center",
+ style="border-radius: 1rem;",
+ content_fit="cover",
+ ),
+ selectable=False,
+ activatable=False,
+ ),
+ SwitchRow(
+ label="Dark mode",
+ active=material.bind("dark_mode"),
+ on_change=lambda x, state: material.set_dark_mode(state),
+ style="margin-top: 1rem;",
+ ),
+ FileRow(
+ label="Wallpaper path",
+ button_label=os.path.basename(wallpaper.wallpaper)
+ if wallpaper.wallpaper
+ else None,
+ dialog=Widget.FileDialog(
+ on_file_set=lambda x, file: material.generate_colors(
+ file.get_path()
+ ),
+ initial_path=wallpaper.bind("wallpaper"),
+ filters=[
+ Widget.FileFilter(
+ mime_types=["image/jpeg", "image/png"],
+ default=True,
+ name="Images JPEG/PNG",
+ )
+ ],
+ ),
+ ),
+ ],
+ )
+ ],
+ )
+ return SettingsEntry(
+ label="Appearance",
+ icon="preferences-desktop-wallpaper-symbolic",
+ active_page=active_page,
+ page=appearance_page,
+ )
diff --git a/ignis/backup_04.03.2025/modules/settings/elements/__init__.py b/ignis/backup_04.03.2025/modules/settings/elements/__init__.py
new file mode 100644
index 0000000..d3795f0
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/settings/elements/__init__.py
@@ -0,0 +1,20 @@
+from .row import SettingsRow
+from .page import SettingsPage
+from .group import SettingsGroup
+from .switchrow import SwitchRow
+from .filerow import FileRow
+from .spinrow import SpinRow
+from .entryrow import EntryRow
+from .entry import SettingsEntry
+
+__all__ = [
+ "SettingsRow",
+ "SettingsPage",
+ "SettingsGroup",
+ "SettingsRow",
+ "SwitchRow",
+ "FileRow",
+ "SpinRow",
+ "EntryRow",
+ "SettingsEntry",
+]
diff --git a/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/__init__.cpython-312.pyc b/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..cbba8ba
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/__init__.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/__init__.cpython-313.pyc b/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..6956187
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/entry.cpython-312.pyc b/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/entry.cpython-312.pyc
new file mode 100644
index 0000000..4554234
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/entry.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/entry.cpython-313.pyc b/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/entry.cpython-313.pyc
new file mode 100644
index 0000000..1b5760b
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/entry.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/entryrow.cpython-312.pyc b/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/entryrow.cpython-312.pyc
new file mode 100644
index 0000000..4de8cab
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/entryrow.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/entryrow.cpython-313.pyc b/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/entryrow.cpython-313.pyc
new file mode 100644
index 0000000..78890a4
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/entryrow.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/filerow.cpython-312.pyc b/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/filerow.cpython-312.pyc
new file mode 100644
index 0000000..b21a852
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/filerow.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/filerow.cpython-313.pyc b/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/filerow.cpython-313.pyc
new file mode 100644
index 0000000..4967147
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/filerow.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/group.cpython-312.pyc b/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/group.cpython-312.pyc
new file mode 100644
index 0000000..68e1735
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/group.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/group.cpython-313.pyc b/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/group.cpython-313.pyc
new file mode 100644
index 0000000..445e1b4
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/group.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/page.cpython-312.pyc b/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/page.cpython-312.pyc
new file mode 100644
index 0000000..88b81ec
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/page.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/page.cpython-313.pyc b/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/page.cpython-313.pyc
new file mode 100644
index 0000000..50a42a9
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/page.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/row.cpython-312.pyc b/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/row.cpython-312.pyc
new file mode 100644
index 0000000..26ec3dd
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/row.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/row.cpython-313.pyc b/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/row.cpython-313.pyc
new file mode 100644
index 0000000..88d372f
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/row.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/spinrow.cpython-312.pyc b/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/spinrow.cpython-312.pyc
new file mode 100644
index 0000000..db1d8df
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/spinrow.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/spinrow.cpython-313.pyc b/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/spinrow.cpython-313.pyc
new file mode 100644
index 0000000..50d357e
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/spinrow.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/switchrow.cpython-312.pyc b/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/switchrow.cpython-312.pyc
new file mode 100644
index 0000000..59c3f7b
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/switchrow.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/switchrow.cpython-313.pyc b/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/switchrow.cpython-313.pyc
new file mode 100644
index 0000000..33d79f8
Binary files /dev/null and b/ignis/backup_04.03.2025/modules/settings/elements/__pycache__/switchrow.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/modules/settings/elements/entry.py b/ignis/backup_04.03.2025/modules/settings/elements/entry.py
new file mode 100644
index 0000000..2df6941
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/settings/elements/entry.py
@@ -0,0 +1,31 @@
+from ignis.widgets import Widget
+from .page import SettingsPage
+from options import settings_last_page
+
+
+class SettingsEntry(Widget.ListBoxRow):
+ def __init__(
+ self,
+ icon: str,
+ label: str,
+ active_page,
+ page: SettingsPage,
+ **kwargs,
+ ):
+ def callback(x):
+ active_page.page = page
+ active_page.name = label
+ if self in self.parent.rows:
+ settings_last_page.set_value(self.parent.rows.index(self))
+
+ super().__init__(
+ child=Widget.Box(
+ child=[
+ Widget.Icon(image=icon, pixel_size=20),
+ Widget.Label(label=label, style="margin-left: 0.75rem;"),
+ ],
+ ),
+ css_classes=["settings-sidebar-entry"],
+ on_activate=callback,
+ **kwargs,
+ )
diff --git a/ignis/backup_04.03.2025/modules/settings/elements/entryrow.py b/ignis/backup_04.03.2025/modules/settings/elements/entryrow.py
new file mode 100644
index 0000000..3349c21
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/settings/elements/entryrow.py
@@ -0,0 +1,24 @@
+from ignis.widgets import Widget
+from .row import SettingsRow
+from typing import Callable
+from ignis.gobject import Binding
+
+
+class EntryRow(SettingsRow):
+ def __init__(
+ self,
+ text: str | Binding | None = None,
+ on_change: Callable | None = None,
+ width: int | None = None,
+ **kwargs,
+ ):
+ super().__init__(**kwargs)
+ self._entry = Widget.Entry(
+ on_change=on_change,
+ text=text,
+ halign="end",
+ valign="center",
+ width_request=width,
+ hexpand=True,
+ )
+ self.child.append(self._entry)
diff --git a/ignis/backup_04.03.2025/modules/settings/elements/filerow.py b/ignis/backup_04.03.2025/modules/settings/elements/filerow.py
new file mode 100644
index 0000000..e948f5b
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/settings/elements/filerow.py
@@ -0,0 +1,23 @@
+from ignis.widgets import Widget
+from .row import SettingsRow
+from ignis.gobject import Binding
+
+
+class FileRow(SettingsRow):
+ def __init__(
+ self,
+ dialog: Widget.FileDialog,
+ button_label: str | Binding | None = None,
+ **kwargs,
+ ):
+ super().__init__(**kwargs)
+ self._button = Widget.FileChooserButton(
+ dialog=dialog,
+ label=Widget.Label(
+ label=button_label, ellipsize="start", max_width_chars=20
+ ),
+ hexpand=True,
+ halign="end",
+ )
+
+ self.child.append(self._button)
diff --git a/ignis/backup_04.03.2025/modules/settings/elements/group.py b/ignis/backup_04.03.2025/modules/settings/elements/group.py
new file mode 100644
index 0000000..d869761
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/settings/elements/group.py
@@ -0,0 +1,24 @@
+from typing import List
+from ignis.widgets import Widget
+from .row import SettingsRow
+from ignis.base_widget import BaseWidget
+
+
+class SettingsGroup(Widget.Box):
+ def __init__(
+ self, name: str | None, rows: List[SettingsRow | BaseWidget] = [], **kwargs
+ ):
+ super().__init__(
+ vertical=True,
+ css_classes=["settings-group"],
+ child=[
+ Widget.Label(
+ label=name,
+ css_classes=["settings-group-name"],
+ halign="start",
+ visible=True if name else False,
+ ),
+ Widget.ListBox(rows=[*rows]),
+ ],
+ **kwargs,
+ )
diff --git a/ignis/backup_04.03.2025/modules/settings/elements/page.py b/ignis/backup_04.03.2025/modules/settings/elements/page.py
new file mode 100644
index 0000000..64bd961
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/settings/elements/page.py
@@ -0,0 +1,24 @@
+from ignis.widgets import Widget
+from typing import List
+from .group import SettingsGroup
+from ignis.base_widget import BaseWidget
+
+
+class SettingsPage(Widget.Scroll):
+ def __init__(self, name: str, groups: List[SettingsGroup | BaseWidget] = []):
+ super().__init__(
+ hexpand=True,
+ vexpand=True,
+ child=Widget.Box(
+ vertical=True,
+ hexpand=True,
+ vexpand=True,
+ css_classes=["settings-page"],
+ child=[
+ Widget.Label(
+ label=name, css_classes=["settings-page-name"], halign="start"
+ ),
+ *groups,
+ ],
+ ),
+ )
diff --git a/ignis/backup_04.03.2025/modules/settings/elements/row.py b/ignis/backup_04.03.2025/modules/settings/elements/row.py
new file mode 100644
index 0000000..16f5950
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/settings/elements/row.py
@@ -0,0 +1,34 @@
+from ignis.widgets import Widget
+
+
+class SettingsRow(Widget.ListBoxRow):
+ def __init__(self, label: str | None = None, sublabel: str | None = None, **kwargs):
+ super().__init__(
+ css_classes=["settings-row"],
+ child=Widget.Box(
+ child=[
+ Widget.Box(
+ vertical=True,
+ child=[
+ Widget.Label(
+ label=label,
+ css_classes=["settings-row-label"],
+ halign="start",
+ vexpand=True,
+ wrap=True,
+ visible=True if label else False,
+ ),
+ Widget.Label(
+ label=sublabel,
+ css_classes=["settings-row-sublabel"],
+ halign="start",
+ vexpand=True,
+ wrap=True,
+ visible=True if sublabel else False,
+ ),
+ ],
+ )
+ ]
+ ),
+ **kwargs,
+ )
diff --git a/ignis/backup_04.03.2025/modules/settings/elements/spinrow.py b/ignis/backup_04.03.2025/modules/settings/elements/spinrow.py
new file mode 100644
index 0000000..3d3f09e
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/settings/elements/spinrow.py
@@ -0,0 +1,30 @@
+from ignis.widgets import Widget
+from .row import SettingsRow
+from typing import Callable
+from ignis.gobject import Binding
+
+
+class SpinRow(SettingsRow):
+ def __init__(
+ self,
+ value: int | Binding = 0,
+ on_change: Callable | None = None,
+ min: int = 0,
+ max: int = 100,
+ step: int = 1,
+ width: int = 0,
+ **kwargs,
+ ):
+ super().__init__(**kwargs)
+ self._spin_button = Widget.SpinButton(
+ value=value,
+ on_change=on_change,
+ min=min,
+ max=max,
+ halign="end",
+ valign="center",
+ width_request=width,
+ hexpand=True,
+ step=step,
+ )
+ self.child.append(self._spin_button)
diff --git a/ignis/backup_04.03.2025/modules/settings/elements/switchrow.py b/ignis/backup_04.03.2025/modules/settings/elements/switchrow.py
new file mode 100644
index 0000000..b204e9c
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/settings/elements/switchrow.py
@@ -0,0 +1,25 @@
+from ignis.widgets import Widget
+from .row import SettingsRow
+from typing import Callable
+from ignis.gobject import Binding
+
+
+class SwitchRow(SettingsRow):
+ def __init__(
+ self,
+ active: bool | Binding = False,
+ on_change: Callable | None = None,
+ **kwargs,
+ ):
+ super().__init__(**kwargs)
+ self._switch = Widget.Switch(
+ active=active,
+ on_change=on_change,
+ halign="end",
+ valign="center",
+ hexpand=True,
+ )
+ self.on_activate = lambda x: self._switch.emit(
+ "activate"
+ ) # if set "active" property animation will not work
+ self.child.append(self._switch)
diff --git a/ignis/backup_04.03.2025/modules/settings/notifications.py b/ignis/backup_04.03.2025/modules/settings/notifications.py
new file mode 100644
index 0000000..df21602
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/settings/notifications.py
@@ -0,0 +1,48 @@
+from .elements import SwitchRow, SettingsPage, SettingsGroup, SpinRow, SettingsEntry
+from ignis.services.notifications import NotificationService
+
+notifications = NotificationService.get_default()
+
+
+def notifications_entry(active_page):
+ notifications_page = SettingsPage(
+ name="Notifications",
+ groups=[
+ SettingsGroup(
+ name="General",
+ rows=[
+ SwitchRow(
+ label="Do not disturb",
+ active=notifications.bind("dnd"),
+ on_change=lambda x, state: notifications.set_dnd(state),
+ ),
+ SpinRow(
+ label="Maximum popups count",
+ sublabel="The first popup will automatically dismiss",
+ value=notifications.bind("max_popups_count"),
+ min=1,
+ on_change=lambda x, value: notifications.set_max_popups_count(
+ value
+ ),
+ ),
+ SpinRow(
+ label="Popup timeout",
+ sublabel="Timeout before popup will be dismissed, in milliseconds.",
+ max=100000,
+ step=100,
+ value=notifications.bind("popup_timeout"),
+ on_change=lambda x, value: notifications.set_popup_timeout(
+ value
+ ),
+ ),
+ ],
+ )
+ ],
+ )
+
+ return SettingsEntry(
+ label="Notifications",
+ icon="notification-symbolic",
+ active_page=active_page,
+ page=notifications_page,
+ )
diff --git a/ignis/backup_04.03.2025/modules/settings/recorder.py b/ignis/backup_04.03.2025/modules/settings/recorder.py
new file mode 100644
index 0000000..f0b0fbd
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/settings/recorder.py
@@ -0,0 +1,58 @@
+from ignis.services.recorder import RecorderService
+from .elements import (
+ SpinRow,
+ SettingsPage,
+ SettingsGroup,
+ EntryRow,
+ FileRow,
+ SettingsEntry,
+)
+from ignis.widgets import Widget
+
+recorder = RecorderService.get_default()
+
+
+def recorder_entry(active_page):
+ recorder_page = SettingsPage(
+ name="Recorder",
+ groups=[
+ SettingsGroup(
+ name="General",
+ rows=[
+ SpinRow(
+ label="Recording bitrate",
+ sublabel="Affects the recording quality",
+ value=recorder.bind("bitrate"),
+ max=640000,
+ width=150,
+ on_change=lambda x, value: recorder.set_bitrate(int(value)),
+ step=1000,
+ ),
+ FileRow(
+ label="Recording path",
+ button_label=recorder.bind("default_file_location"),
+ dialog=Widget.FileDialog(
+ on_file_set=lambda x,
+ file: recorder.set_default_file_location(file.get_path()),
+ select_folder=True,
+ initial_path=recorder.default_file_location,
+ ),
+ ),
+ EntryRow(
+ label="Recording filename",
+ sublabel="Support time formatting",
+ text=recorder.bind("default_filename"),
+ on_change=lambda x: recorder.set_default_filename(x.text),
+ width=200,
+ ),
+ ],
+ )
+ ],
+ )
+
+ return SettingsEntry(
+ label="Recorder",
+ icon="media-record-symbolic",
+ active_page=active_page,
+ page=recorder_page,
+ )
diff --git a/ignis/backup_04.03.2025/modules/settings/user.py b/ignis/backup_04.03.2025/modules/settings/user.py
new file mode 100644
index 0000000..c62e3f0
--- /dev/null
+++ b/ignis/backup_04.03.2025/modules/settings/user.py
@@ -0,0 +1,56 @@
+import os
+from .elements import SettingsGroup, SettingsPage, SettingsEntry, FileRow
+from ignis.widgets import Widget
+from options import avatar_opt
+
+
+def user_entry(active_page):
+ page = SettingsPage(
+ name="User",
+ groups=[
+ Widget.Box(
+ halign="start",
+ style="margin-left: 2rem;",
+ child=[
+ Widget.Picture(
+ image=avatar_opt.bind(
+ "value",
+ lambda value: "user-info"
+ if not os.path.exists(value)
+ else value,
+ ),
+ width=96,
+ height=96,
+ style="border-radius: 10rem;",
+ ),
+ Widget.Label(
+ label=os.getenv("USER"), css_classes=["settings-user-name"]
+ ),
+ ],
+ ),
+ SettingsGroup(
+ style="margin-top: 2rem;",
+ valign="start",
+ name="General",
+ vexpand=True,
+ rows=[
+ FileRow(
+ label="Avatar",
+ dialog=Widget.FileDialog(
+ initial_path=avatar_opt.bind("value"),
+ on_file_set=lambda x, gfile: avatar_opt.set_value(
+ gfile.get_path()
+ ),
+ ),
+ )
+ ],
+ ),
+ ],
+ )
+
+ return SettingsEntry(
+ label="User",
+ icon="user-available-symbolic",
+ active_page=active_page,
+ page=page,
+ )
diff --git a/ignis/backup_04.03.2025/options.py b/ignis/backup_04.03.2025/options.py
new file mode 100644
index 0000000..5bfa182
--- /dev/null
+++ b/ignis/backup_04.03.2025/options.py
@@ -0,0 +1,18 @@
+import os
+from ignis.services.options import OptionsService
+
+options = OptionsService.get_default()
+
+user_opt_group = options.create_group("user", exists_ok=True)
+avatar_opt = user_opt_group.create_option(
+ "avatar",
+ default=f"/var/lib/AccountsService/icons/{os.getenv('USER')}",
+ exists_ok=True,
+)
+
+settings_opt_group = options.create_group("settings", exists_ok=True)
+settings_last_page = settings_opt_group.create_option(
+ "last_page",
+ default=0,
+ exists_ok=True,
+)
diff --git a/ignis/backup_04.03.2025/scripts/recording.py b/ignis/backup_04.03.2025/scripts/recording.py
new file mode 100644
index 0000000..7bf75c6
--- /dev/null
+++ b/ignis/backup_04.03.2025/scripts/recording.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python3
+
+import sys
+from ignis.client import IgnisClient
+
+CODE_TEMPLATE = """
+from ignis.services.recorder import RecorderService
+
+recorder = RecorderService.get_default()
+recorder.{}_recording()
+"""
+
+
+def run_code(_type: str) -> None:
+ client = IgnisClient()
+ client.run_python(CODE_TEMPLATE.format(_type))
+
+
+if len(sys.argv) < 2:
+ sys.exit(1)
+
+elif sys.argv[1] == "start":
+ run_code("start")
+
+elif sys.argv[1] == "stop":
+ run_code("stop")
+
+elif sys.argv[1] == "pause":
+ run_code("pause")
+
+elif sys.argv[1] == "continue":
+ run_code("continue")
diff --git a/ignis/backup_04.03.2025/scss/bar.scss b/ignis/backup_04.03.2025/scss/bar.scss
new file mode 100644
index 0000000..9738518
--- /dev/null
+++ b/ignis/backup_04.03.2025/scss/bar.scss
@@ -0,0 +1,133 @@
+.bar-widget {
+ padding: 0.4rem 1rem;
+ margin: 0;
+ border-radius: 0;
+ transition: 0.3s;
+ background-color: rgba($surface, 0.3);
+}
+
+.clock {
+ @include hover($surface);
+ color: $onSurface;
+ background-color: $surface;
+ font-size: 1.5rem;
+ border-radius: 1rem;
+ padding: 0 0.75rem;
+ margin: 0 0.5rem;
+ margin-right: 0;
+ transition: 0.3s;
+
+ &.active {
+ background-color: $primary;
+ color: $surface;
+
+ .record-indicator {
+ color: $surface;
+ }
+
+ @include hover($primary)
+ }
+}
+
+.kb-layout {
+ border-radius: 0.5rem;
+ padding: 0 0.25rem;
+ margin: 0 0.25rem;
+ transition: 0.3s;
+ font-size: 1.3rem;
+}
+
+.kb-layout:hover {
+ background-color: rgba(white, 0.2);
+}
+
+.pinned-app {
+ margin: 0 0.25rem;
+ transition: 0.3s;
+ padding: 0 0.1rem;
+ border-radius: 0.25rem;
+}
+
+.pinned-app:hover {
+ background-color: rgba(white, 0.2);
+}
+
+
+.tray {
+ padding: 0 0.25rem;
+ margin: 0 0.25rem;
+}
+
+.workspaces {
+ border-radius: 1rem;
+ padding: 0.5rem;
+ background-color: $surface;
+ margin: 0 0.5rem;
+ margin-left: 0;
+}
+
+.workspace {
+ @include hover($primaryContainer);
+ border-radius: 1rem;
+ margin: 0 0.25rem;
+ min-width: 0.7rem;
+ min-height: 0.7rem;
+ transition: 0.3s;
+
+ &.active {
+ @include hover($primary);
+ min-width: 2rem;
+ }
+}
+
+@keyframes recorder-animation {
+ 0% { opacity: 0; }
+ 50% { opacity: 1; }
+ 90% { opacity: 1; }
+ 100% { opacity: 0; }
+}
+
+.record-indicator {
+ color: $error;
+}
+
+.record-indicator.active {
+ animation: recorder-animation 2s infinite;
+}
+
+.battery-item {
+ margin: 0 0.25rem;
+}
+
+.battery-scale {
+ min-width: 5rem;
+ all: unset;
+
+ * {
+ all: unset;
+ }
+}
+
+.battery-scale trough,
+.battery-scale trough highlight {
+ background-color: $surfaceContainerHighest;
+ border-radius: 0.5rem;
+ min-height: 1.5rem;
+ min-width: 6rem;
+}
+
+
+.battery-scale trough highlight {
+ background-color: $primary;
+ min-width: 0rem;
+}
+
+.battery-icon {
+ color: $onSurface;
+ margin-right: 0.3rem;
+}
+
+.battery-percent {
+ color: $onSurface;
+ margin-right: 0.3rem;
+}
\ No newline at end of file
diff --git a/ignis/backup_04.03.2025/scss/control_center.scss b/ignis/backup_04.03.2025/scss/control_center.scss
new file mode 100644
index 0000000..c1c955f
--- /dev/null
+++ b/ignis/backup_04.03.2025/scss/control_center.scss
@@ -0,0 +1,166 @@
+.control-center {
+ @include window;
+ min-width: 28rem;
+ padding: 0;
+}
+
+.control-center-widget {
+ padding: 1rem;
+ margin-bottom: 1rem;
+}
+
+.qs-main-box {
+ margin-top: 0.5rem;
+}
+
+.user {
+ margin-top: 1rem;
+}
+
+.qs-button {
+ @include hover($surfaceContainerHigh);
+ color: $onSurface;
+ min-height: 2.4rem;
+ border-radius: 1.2rem;
+ transition: 0.3s;
+ padding: 0.3rem 0.8rem;
+
+ &.active {
+ @include hover($primary);
+ color: $surface;
+ }
+
+}
+
+.qs-button-label {
+ margin-left: 0.5rem;
+}
+
+.record-menu {
+ background-color: $surfaceContainerHigh;
+ border-radius: 1rem;
+ padding: 1rem;
+ margin-top: 1rem;
+}
+
+.record-icon {
+ color: $error;
+}
+
+.record-dropdown {
+ button {
+ all: unset;
+ color: $onSurfaceVariant;
+ font-size: 0.9rem;
+ }
+}
+
+.record-cancel-button,
+.record-start-button {
+ color: $surface;
+ border-radius: 1rem;
+ padding: 0.25rem 0.75rem;
+ font-size: 0.95rem;
+ transition: 0.3s;
+}
+
+.record-cancel-button {
+ @include hover($surfaceContainerHigh);
+ color: $onSurface;
+ border: 1px solid $primary;
+}
+
+.record-start-button {
+ @include hover($primary);
+}
+
+.volume-entry {
+ @include hover($surfaceContainerHigh);
+ transition: 0.3s;
+ border-radius: 1rem;
+ padding: 0.5rem 0.5rem;
+}
+
+.volume-entry-list-header-label {
+ font-size: 1.35rem;
+ margin-left: 0.5rem;
+}
+
+.volume-entry-list-header-box {
+ margin-bottom: 0.75rem;
+}
+
+.user-name-box {
+ margin-left: 0.5rem;
+}
+
+.user-name {
+ font-size: 1.2rem;
+}
+
+.user-name-secondary {
+ color: $onSurfaceVariant;
+ font-size: 0.9rem;
+}
+
+.user-settings {
+ @include hover($surfaceContainerHighest);
+ color: $onSurface;
+ border-radius: 2rem;
+ min-width: 2.35rem;
+ margin: 0.4rem 0rem;
+ margin-right: 0.5rem;
+ transition: 0.3s;
+}
+
+.user-power {
+ @include hover($primary);
+ color: $surface;
+ border-radius: 1.5rem;
+ min-width: 2.35rem;
+ margin: 0.4rem 0rem;
+ transition: 0.3s;
+}
+
+.ethernet-header-box,
+.wifi-header-box,
+.vpn-header-box {
+ margin-bottom: 0.5rem;
+}
+
+.ethernet-header-label,
+.vpn-header-label {
+ font-size: 1.35rem;
+ margin-left: 0.75rem;
+}
+
+.wifi-header-label {
+ font-size: 1.35rem;
+}
+
+.ethernet-connection-item,
+.wifi-network-item,
+.vpn-connection-item {
+ @include hover($surfaceContainerHigh);
+ transition: 0.3s;
+ border-radius: 1rem;
+ padding: 0.5rem 0.5rem;
+}
+
+
+.ethernet-connection-label,
+.wifi-network-label,
+.vpn-connection-label {
+ margin-left: 0.5rem;
+}
+
+.ethernet-connection-item-connect-label,
+.vpn-connection-item-connect-label {
+ color: $onSurfaceVariant;
+ font-size: 1rem;
+}
+
+.volume-entry-label {
+ margin-left: 0.5rem;
+ margin-right: 1rem;
+}
diff --git a/ignis/backup_04.03.2025/scss/launcher.scss b/ignis/backup_04.03.2025/scss/launcher.scss
new file mode 100644
index 0000000..5331b36
--- /dev/null
+++ b/ignis/backup_04.03.2025/scss/launcher.scss
@@ -0,0 +1,38 @@
+.launcher {
+ @include window;
+ margin-top: 20rem;
+}
+
+.launcher-search {
+ all: unset;
+}
+
+.launcher-search-box {
+ padding: 0.75rem 1rem;
+ background-color: $surfaceContainer;
+ border-radius: 1rem;
+ min-width: 30rem;
+}
+
+.launcher-scroll {
+ min-height: 30rem;
+}
+
+.launcher-app {
+ all: unset;
+ @include hover($surface);
+ border-radius: 1rem;
+ padding: 0.5rem 0.25rem;
+ border: 2px solid $surface;
+ margin-top: 0.25rem;
+ transition: 0.3s;
+
+ &:focus {
+ border: 2px solid $primary;
+ }
+}
+
+.launcher-app-label {
+ font-size: 1.1rem;
+ margin-left: 1rem;
+}
\ No newline at end of file
diff --git a/ignis/backup_04.03.2025/scss/lib.scss b/ignis/backup_04.03.2025/scss/lib.scss
new file mode 100644
index 0000000..0ce0072
--- /dev/null
+++ b/ignis/backup_04.03.2025/scss/lib.scss
@@ -0,0 +1,75 @@
+.rec-unset {
+ * {
+ all: unset;
+ }
+}
+
+.unset {
+ all: unset;
+}
+
+tooltip {
+ all: unset;
+ background-color: $surface;
+ padding: 0.5rem 1rem;
+ border-radius: 1rem;
+}
+
+.toggle-box {
+ padding: 0.75rem 1rem;
+ border-radius: 1rem;
+ background-color: $primaryContainer;
+}
+
+.material-slider {
+ all: unset;
+
+ * {
+ all: unset;
+ }
+}
+
+.material-slider trough,
+.material-slider trough highlight {
+ background-color: $secondaryContainer;
+ border-radius: 1rem;
+ min-height: 1rem;
+}
+
+
+.material-slider trough highlight {
+ background-color: $primary;
+}
+
+.material-slider-icon {
+ padding: 0.25rem;
+ min-width: 1.5rem;
+ min-height: 1.5rem;
+ border-radius: 1rem;
+ margin-right: 0.5rem;
+}
+
+.material-slider-arrow {
+ all: unset;
+ padding: 0.25rem;
+ min-width: 1.5rem;
+ min-height: 1.5rem;
+ border-radius: 1rem;
+ margin-left: 0.5rem;
+}
+
+.hover-surface {
+ @include hover($surface);
+ transition: 0.3s;
+}
+
+.control-center-menu {
+ margin-top: 1rem;
+ background-color: $surfaceContainerHigh;
+ border-radius: 1rem;
+ padding: 1rem;
+
+ separator {
+ margin: 0.5rem 0;
+ }
+}
diff --git a/ignis/backup_04.03.2025/scss/media.scss b/ignis/backup_04.03.2025/scss/media.scss
new file mode 100644
index 0000000..22598b1
--- /dev/null
+++ b/ignis/backup_04.03.2025/scss/media.scss
@@ -0,0 +1,84 @@
+// WARNING: Do not import this file in style.scss
+
+.media-{{ desktop_entry }} {
+ margin-top: 1rem;
+}
+
+
+.media-image-{{ desktop_entry }} {
+ background-image: url('file:///{{ art_url }}');
+ background-repeat: no-repeat;
+ background-size: cover;
+ background-position: center;
+ min-height: 11rem;
+ border-radius: 1.5rem;
+}
+
+.media-image-gradient-{{ desktop_entry }} {
+ border-radius: 1.3rem;
+ background: linear-gradient(90deg, rgba({{ onPrimary }}, 0.9) 0%, rgba({{ onPrimary }}, 0.4) 50%, rgba({{ onPrimary }}, 0.9) 100%);
+}
+
+.media-scale-{{ desktop_entry }} trough,
+.media-scale-{{ desktop_entry }} trough highlight {
+ background-color: rgba({{ onSurface }}, 0.2);
+ border-radius: 1rem;
+ min-height: 0.2rem;
+}
+
+
+.media-scale-{{ desktop_entry }} trough highlight {
+ background-color: {{ onSurface }};
+}
+
+.media-scale-{{ desktop_entry }} slider {
+ background-color: {{ onSurface }};
+ padding: 0.5rem 0.1rem;
+ margin: -0.5rem -0.1rem;
+}
+
+.media-title-{{ desktop_entry }} {
+ color: {{ onSurface }};
+}
+
+.media-artist-{{ desktop_entry }} {
+ color: {{ onSurfaceVariant }};
+ font-weight: normal;
+}
+
+
+.media-content-{{ desktop_entry }} {
+ padding: 1rem;
+}
+
+.media-playback-button-{{ desktop_entry }} {
+ background-color: {{ primary }};
+ color: {{ onPrimary }};
+ border-radius: 4rem;
+ min-width: 2.7rem;
+ min-height: 2.7rem;
+ transition: 0.3s;
+
+ &:hover {
+ background-color: lighten({{ primary }}, 5%);
+ }
+
+ &.playing {
+ border-radius: 1rem;
+ }
+}
+
+.media-player-icon-{{ desktop_entry }} {
+ color: {{ primary }};
+ font-size: 1.6rem;
+ padding: 1rem;
+ font-family: "JetBrains Mono Nerd Font";
+}
+
+.media-skip-button-{{ desktop_entry }} {
+ transition: 0.3s;
+ color: {{ onSurface }};
+ &:hover {
+ color: {{ primary }};
+ }
+}
\ No newline at end of file
diff --git a/ignis/backup_04.03.2025/scss/mixins/hover.scss b/ignis/backup_04.03.2025/scss/mixins/hover.scss
new file mode 100644
index 0000000..7dca32c
--- /dev/null
+++ b/ignis/backup_04.03.2025/scss/mixins/hover.scss
@@ -0,0 +1,12 @@
+@mixin hover($bg) {
+ background-color: $bg;
+
+ &:hover {
+ @if $darkmode == true {
+ background-color: lighten($bg, 5%);
+ }
+ @else {
+ background-color: darken($bg, 5%);
+ }
+ }
+}
diff --git a/ignis/backup_04.03.2025/scss/mixins/window.scss b/ignis/backup_04.03.2025/scss/mixins/window.scss
new file mode 100644
index 0000000..e73845a
--- /dev/null
+++ b/ignis/backup_04.03.2025/scss/mixins/window.scss
@@ -0,0 +1,8 @@
+@mixin window {
+ background-color: $surface;
+ border-radius: 1rem;
+ padding: 1rem;
+ color: $onSurface;
+ box-shadow: 0 0 4px 2px $shadow;
+ margin: 1rem;
+}
\ No newline at end of file
diff --git a/ignis/backup_04.03.2025/scss/notification_center.scss b/ignis/backup_04.03.2025/scss/notification_center.scss
new file mode 100644
index 0000000..21d0854
--- /dev/null
+++ b/ignis/backup_04.03.2025/scss/notification_center.scss
@@ -0,0 +1,66 @@
+.notification-center {
+ background-color: $surfaceContainerLow;
+ border-radius: 1.5rem 1.5rem 1rem 1rem;
+ padding: 1rem;
+}
+
+.notification-center-header {
+ margin-bottom: 1rem;
+}
+
+.notification {
+ background-color: $surfaceContainerHigh;
+ border-radius: 1rem;
+ padding: 1rem;
+ margin-top: 1rem;
+}
+
+.notification-count {
+ background-color: $tertiary;
+ color: $onTertiary;
+ min-width: 2rem;
+ font-size: 1.5rem;
+ border-radius: 2rem;
+}
+
+.notification-header-label {
+ font-size: 1.1rem;
+ margin-left: 0.5rem;
+}
+
+.notification-close {
+ @include hover($surfaceContainerHigh);
+ color: $onSurface;
+ border-radius: 1rem;
+ padding: 0.25rem;
+ margin-left: 0.5rem;
+ transition: 0.3s;
+}
+
+.notification-clear-all {
+ @include hover($surfaceContainerHigh);
+ transition: 0.3s;
+ border-radius: 1rem;
+ padding: 0.25rem 1rem;
+}
+
+.notification-action {
+ color: $primary;
+ @include hover($surfaceContainerHighest);
+ border-radius: 0.5rem;
+ padding: 0.25rem 0.25rem;
+ transition: 0.3s;
+}
+
+.notification-screenshot-label {
+ margin-top: 0.5rem;
+ font-size: 1.1rem;
+}
+
+.notification-center-info-label {
+ color: $onSurfaceVariant;
+}
+
+.notification-body {
+ color: $onSurfaceVariant;
+}
\ No newline at end of file
diff --git a/ignis/backup_04.03.2025/scss/notification_popup.scss b/ignis/backup_04.03.2025/scss/notification_popup.scss
new file mode 100644
index 0000000..f4c9034
--- /dev/null
+++ b/ignis/backup_04.03.2025/scss/notification_popup.scss
@@ -0,0 +1,12 @@
+.notification-popup {
+ @include window;
+ min-width: 25rem;
+
+ & .notification-close {
+ @include hover($surface);
+ }
+
+ & .notification-action {
+ @include hover($surfaceContainer);
+ }
+}
diff --git a/ignis/backup_04.03.2025/scss/osd.scss b/ignis/backup_04.03.2025/scss/osd.scss
new file mode 100644
index 0000000..25f1aea
--- /dev/null
+++ b/ignis/backup_04.03.2025/scss/osd.scss
@@ -0,0 +1,8 @@
+.osd {
+ @include window;
+ min-width: 15rem;
+ margin-bottom: 5rem;
+ * {
+ all: unset
+ }
+}
\ No newline at end of file
diff --git a/ignis/backup_04.03.2025/scss/powermenu.scss b/ignis/backup_04.03.2025/scss/powermenu.scss
new file mode 100644
index 0000000..8011a64
--- /dev/null
+++ b/ignis/backup_04.03.2025/scss/powermenu.scss
@@ -0,0 +1,28 @@
+.powermenu {
+ @include window;
+}
+
+.powermenu-button {
+ @include hover($surfaceContainer);
+ transition: 0.3s;
+ border-radius: 10rem;
+ min-width: 8rem;
+ min-height: 8rem;
+
+ &:focus {
+ @include hover($primary);
+ color: $surface;
+ }
+}
+
+.powermenu-button-label {
+ margin-top: 0.5rem;
+}
+
+.powermenu-button-box {
+ margin: 0.5rem;
+}
+
+.powermenu-overlay {
+ background-color: rgba(0, 0, 0, 0.5);
+}
diff --git a/ignis/backup_04.03.2025/scss/settings.scss b/ignis/backup_04.03.2025/scss/settings.scss
new file mode 100644
index 0000000..31bcc08
--- /dev/null
+++ b/ignis/backup_04.03.2025/scss/settings.scss
@@ -0,0 +1,71 @@
+.settings-sidebar {
+ background-color: $surfaceContainer;
+ min-width: 300px;
+}
+
+.settings-sidebar-label {
+ font-size: 2rem;
+ margin-left: 1rem;
+ margin-bottom: 0.5rem;
+ margin-top: 2rem;
+}
+
+.settings-sidebar-entry {
+ all: unset;
+ background-color: $surfaceContainer;
+ padding: 1rem 2rem;
+ padding-left: 0.75rem;
+ font-size: 1.2rem;
+
+ &:selected {
+ @if $darkmode == true {
+ background-color: lighten($surfaceContainer, 10%);
+ }
+ @else {
+ background-color: darken($surfaceContainer, 10%);
+ }
+ }
+}
+
+
+.settings-page-name {
+ font-size: 2rem;
+ margin-left: 2.5rem;
+ margin-bottom: 2rem;
+ margin-top: 2rem;
+}
+
+.settings-group-name {
+ margin-left: 2.5rem;
+ color: $primary;
+}
+
+.settings-row {
+ all: unset;
+ padding: 0.7rem 2.5rem;
+ transition: 0.2s;
+
+ &:active {
+ @if $darkmode == true {
+ background-color: rgba(255, 255, 255, 0.05);
+ }
+ @else {
+ background-color: rgba(0, 0, 0, 0.05);
+ }
+ }
+}
+
+.settings-row-label {
+ font-size: 1.2rem;
+}
+
+.settings-row-sublabel {
+ font-size: 0.9rem;
+ font-weight: normal;
+ color: $onSurfaceVariant;
+}
+
+.settings-user-name {
+ font-size: 2.3rem;
+ margin-left: 1.5rem;
+}
\ No newline at end of file
diff --git a/ignis/backup_04.03.2025/services/__init__.py b/ignis/backup_04.03.2025/services/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/ignis/backup_04.03.2025/services/__pycache__/__init__.cpython-312.pyc b/ignis/backup_04.03.2025/services/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..cbdecc1
Binary files /dev/null and b/ignis/backup_04.03.2025/services/__pycache__/__init__.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/services/__pycache__/__init__.cpython-313.pyc b/ignis/backup_04.03.2025/services/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..df76d3b
Binary files /dev/null and b/ignis/backup_04.03.2025/services/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/services/material/__init__.py b/ignis/backup_04.03.2025/services/material/__init__.py
new file mode 100644
index 0000000..fd9caa6
--- /dev/null
+++ b/ignis/backup_04.03.2025/services/material/__init__.py
@@ -0,0 +1,3 @@
+from .service import MaterialService
+
+__all__ = ["MaterialService"]
diff --git a/ignis/backup_04.03.2025/services/material/__pycache__/__init__.cpython-312.pyc b/ignis/backup_04.03.2025/services/material/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..40de3b4
Binary files /dev/null and b/ignis/backup_04.03.2025/services/material/__pycache__/__init__.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/services/material/__pycache__/__init__.cpython-313.pyc b/ignis/backup_04.03.2025/services/material/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..b342171
Binary files /dev/null and b/ignis/backup_04.03.2025/services/material/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/services/material/__pycache__/constants.cpython-312.pyc b/ignis/backup_04.03.2025/services/material/__pycache__/constants.cpython-312.pyc
new file mode 100644
index 0000000..3526e9c
Binary files /dev/null and b/ignis/backup_04.03.2025/services/material/__pycache__/constants.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/services/material/__pycache__/constants.cpython-313.pyc b/ignis/backup_04.03.2025/services/material/__pycache__/constants.cpython-313.pyc
new file mode 100644
index 0000000..ec24c5c
Binary files /dev/null and b/ignis/backup_04.03.2025/services/material/__pycache__/constants.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/services/material/__pycache__/service.cpython-312.pyc b/ignis/backup_04.03.2025/services/material/__pycache__/service.cpython-312.pyc
new file mode 100644
index 0000000..5b48bc2
Binary files /dev/null and b/ignis/backup_04.03.2025/services/material/__pycache__/service.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/services/material/__pycache__/service.cpython-313.pyc b/ignis/backup_04.03.2025/services/material/__pycache__/service.cpython-313.pyc
new file mode 100644
index 0000000..25083d1
Binary files /dev/null and b/ignis/backup_04.03.2025/services/material/__pycache__/service.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/services/material/__pycache__/util.cpython-312.pyc b/ignis/backup_04.03.2025/services/material/__pycache__/util.cpython-312.pyc
new file mode 100644
index 0000000..65c5468
Binary files /dev/null and b/ignis/backup_04.03.2025/services/material/__pycache__/util.cpython-312.pyc differ
diff --git a/ignis/backup_04.03.2025/services/material/__pycache__/util.cpython-313.pyc b/ignis/backup_04.03.2025/services/material/__pycache__/util.cpython-313.pyc
new file mode 100644
index 0000000..bbf01ff
Binary files /dev/null and b/ignis/backup_04.03.2025/services/material/__pycache__/util.cpython-313.pyc differ
diff --git a/ignis/backup_04.03.2025/services/material/constants.py b/ignis/backup_04.03.2025/services/material/constants.py
new file mode 100644
index 0000000..9a5d44a
--- /dev/null
+++ b/ignis/backup_04.03.2025/services/material/constants.py
@@ -0,0 +1,11 @@
+import os
+import ignis
+from ignis.utils import Utils
+
+
+MATERIAL_CACHE_DIR = f"{ignis.CACHE_DIR}/material" # type: ignore
+
+TEMPLATES = Utils.get_current_dir() + "/templates"
+SAMPLE_WALL = Utils.get_current_dir() + "/sample_wall.png"
+
+os.makedirs(MATERIAL_CACHE_DIR, exist_ok=True)
diff --git a/ignis/backup_04.03.2025/services/material/sample_wall.png b/ignis/backup_04.03.2025/services/material/sample_wall.png
new file mode 100644
index 0000000..8d6dc01
Binary files /dev/null and b/ignis/backup_04.03.2025/services/material/sample_wall.png differ
diff --git a/ignis/backup_04.03.2025/services/material/service.py b/ignis/backup_04.03.2025/services/material/service.py
new file mode 100644
index 0000000..5399aab
--- /dev/null
+++ b/ignis/backup_04.03.2025/services/material/service.py
@@ -0,0 +1,144 @@
+#!/usr/bin/python
+import os
+
+from jinja2 import Template
+from PIL import Image
+from materialyoucolor.quantize import QuantizeCelebi
+from materialyoucolor.hct import Hct
+from materialyoucolor.scheme.scheme_tonal_spot import SchemeTonalSpot
+from materialyoucolor.dynamiccolor.material_dynamic_colors import MaterialDynamicColors
+from materialyoucolor.score.score import Score
+
+from ignis.utils import Utils
+from ignis.app import IgnisApp
+from ignis.base_service import BaseService
+from ignis.services.wallpaper import CACHE_WALLPAPER_PATH
+from gi.repository import GObject # type: ignore
+from ignis.services.wallpaper import WallpaperService
+from ignis.services.options import OptionsService
+
+from .constants import MATERIAL_CACHE_DIR, TEMPLATES, SAMPLE_WALL
+from .util import rgba_to_hex, calculate_optimal_size
+
+app = IgnisApp.get_default()
+
+
+class MaterialService(BaseService):
+ def __init__(self):
+ super().__init__()
+
+ self._wallpaper = WallpaperService.get_default()
+
+ options = OptionsService.get_default()
+ opt_group = options.create_group(name="material", exists_ok=True)
+ self._dark_mode_opt = opt_group.create_option(
+ name="dark_mode", default=True, exists_ok=True
+ )
+ self._colors_opt = opt_group.create_option(
+ name="colors", default={}, exists_ok=True
+ )
+
+ if not os.path.exists(CACHE_WALLPAPER_PATH):
+ self.__on_colors_not_found()
+ if self.colors == {}:
+ self.__on_colors_not_found()
+
+ @GObject.Property
+ def dark_mode(self) -> bool:
+ return self._dark_mode_opt.value
+
+ @dark_mode.setter
+ def dark_mode(self, value: bool) -> None:
+ self._dark_mode_opt.value = value
+ self.generate_colors(CACHE_WALLPAPER_PATH)
+
+ @GObject.Property
+ def colors(self) -> dict:
+ return self._colors_opt.value
+
+ def __on_colors_not_found(self) -> None:
+ self._wallpaper.set_wallpaper(SAMPLE_WALL)
+ self.generate_colors(SAMPLE_WALL)
+ Utils.exec_sh_async("hyprctl reload")
+
+ def get_colors_from_img(self, path: str, dark_mode: bool) -> dict[str, str]:
+ image = Image.open(path)
+ wsize, hsize = image.size
+ wsize_new, hsize_new = calculate_optimal_size(wsize, hsize, 128)
+ if wsize_new < wsize or hsize_new < hsize:
+ image = image.resize((wsize_new, hsize_new), Image.Resampling.BICUBIC) # type: ignore
+
+ pixel_len = image.width * image.height
+ image_data = image.getdata()
+ pixel_array = [image_data[_] for _ in range(0, pixel_len, 1)]
+
+ colors = QuantizeCelebi(pixel_array, 128)
+ argb = Score.score(colors)[0]
+
+ hct = Hct.from_int(argb)
+ scheme = SchemeTonalSpot(hct, dark_mode, 0.0)
+
+ material_colors = {}
+ 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()
+ material_colors[color] = rgba_to_hex(rgba)
+
+ return material_colors
+
+ def generate_colors(self, path: str) -> None:
+ colors = self.get_colors_from_img(path, self.dark_mode)
+ dark_colors = self.get_colors_from_img(path, True)
+ self._colors_opt.value = colors
+ self.__render_templates(colors, dark_colors)
+ self.__setup(path)
+
+ def __render_templates(self, colors: dict, dark_colors: dict) -> None:
+ for template in os.listdir(TEMPLATES):
+ self.render_template(
+ colors=colors,
+ dark_mode=self.dark_mode,
+ input_file=f"{TEMPLATES}/{template}",
+ output_file=f"{MATERIAL_CACHE_DIR}/{template}",
+ )
+
+ for template in os.listdir(TEMPLATES):
+ self.render_template(
+ colors=dark_colors,
+ dark_mode=True,
+ input_file=f"{TEMPLATES}/{template}",
+ output_file=f"{MATERIAL_CACHE_DIR}/dark_{template}",
+ )
+
+ def render_template(
+ self,
+ colors: dict,
+ input_file: str,
+ output_file: str,
+ dark_mode: bool | None = None,
+ ) -> None:
+ if dark_mode is None:
+ colors["dark_mode"] = str(self.dark_mode).lower()
+ else:
+ colors["dark_mode"] = str(dark_mode).lower()
+ with open(input_file) as file:
+ template_rendered = Template(file.read()).render(colors)
+
+ with open(output_file, "w") as file:
+ file.write(template_rendered)
+
+ def __reload_gtk_theme(self) -> None:
+ THEME_CMD = "gsettings set org.gnome.desktop.interface gtk-theme {}"
+ COLOR_SCHEME_CMD = "gsettings set org.gnome.desktop.interface color-scheme {}"
+ Utils.exec_sh_async(THEME_CMD.format("Adwaita"))
+ Utils.exec_sh_async(THEME_CMD.format("Material"))
+ Utils.exec_sh_async(COLOR_SCHEME_CMD.format("default"))
+ Utils.exec_sh_async(COLOR_SCHEME_CMD.format("prefer-dark"))
+ Utils.exec_sh_async(COLOR_SCHEME_CMD.format("default"))
+
+ def __setup(self, image_path: str) -> None:
+ Utils.exec_sh_async("pkill -SIGUSR1 kitty")
+ self._wallpaper.set_wallpaper(image_path)
+ app.reload_css()
+ self.__reload_gtk_theme()
diff --git a/ignis/backup_04.03.2025/services/material/templates/colors-hyprland.conf b/ignis/backup_04.03.2025/services/material/templates/colors-hyprland.conf
new file mode 100644
index 0000000..1ae3635
--- /dev/null
+++ b/ignis/backup_04.03.2025/services/material/templates/colors-hyprland.conf
@@ -0,0 +1,7 @@
+$primary = rgb({{ primary | replace("#", "") }})
+$surface = rgb({{ surface | replace("#", "") }})
+$onSurface = rgb({{ onSurface | replace("#", "") }})
+
+$primaryHex = #{{ primary }}
+$surfaceHex = #{{ surface }}
+$onSurfaceHex = #{{ onSurface }}
\ No newline at end of file
diff --git a/ignis/backup_04.03.2025/services/material/templates/colors-kitty.conf b/ignis/backup_04.03.2025/services/material/templates/colors-kitty.conf
new file mode 100644
index 0000000..a28b4cc
--- /dev/null
+++ b/ignis/backup_04.03.2025/services/material/templates/colors-kitty.conf
@@ -0,0 +1,27 @@
+foreground {{ onSurface }}
+background {{ surface }}
+cursor {{ onSurface }}
+
+color0 {{ surface }}
+color8 {{ surface }}
+
+color1 {{ error }}
+color9 {{ error }}
+
+color2 {{ inversePrimary }}
+color10 {{ inversePrimary }}
+
+color3 {{ error }}
+color11 {{ error }}
+
+color4 {{ primaryContainer }}
+color12 {{ primaryContainer }}
+
+color5 {{ onSecondaryContainer }}
+color13 {{ onSecondaryContainer }}
+
+color6 {{ primary }}
+color14 {{ primary }}
+
+color7 {{ onSurface }}
+color15 {{ onSurface }}
\ No newline at end of file
diff --git a/ignis/backup_04.03.2025/services/material/templates/colors.scss b/ignis/backup_04.03.2025/services/material/templates/colors.scss
new file mode 100644
index 0000000..d6c1f68
--- /dev/null
+++ b/ignis/backup_04.03.2025/services/material/templates/colors.scss
@@ -0,0 +1,55 @@
+$primary_paletteKeyColor: {{ primary_paletteKeyColor }};
+$secondary_paletteKeyColor: {{ secondary_paletteKeyColor }};
+$tertiary_paletteKeyColor: {{ tertiary_paletteKeyColor }};
+$neutral_paletteKeyColor: {{ neutral_paletteKeyColor }};
+$neutral_variant_paletteKeyColor: {{ neutral_variant_paletteKeyColor }};
+$background: {{ background }};
+$onBackground: {{ onBackground }};
+$surface: {{ surface }};
+$surfaceDim: {{ surfaceDim }};
+$surfaceBright: {{ surfaceBright }};
+$surfaceContainerLowest: {{ surfaceContainerLowest }};
+$surfaceContainerLow: {{ surfaceContainerLow }};
+$surfaceContainer: {{ surfaceContainer }};
+$surfaceContainerHigh: {{ surfaceContainerHigh }};
+$surfaceContainerHighest: {{ surfaceContainerHighest }};
+$onSurface: {{ onSurface }};
+$surfaceVariant: {{ surfaceVariant }};
+$onSurfaceVariant: {{ onSurfaceVariant }};
+$inverseSurface: {{ inverseSurface }};
+$inverseOnSurface: {{ inverseOnSurface }};
+$outline: {{ outline }};
+$outlineVariant: {{ outlineVariant }};
+$shadow: {{ shadow }};
+$scrim: {{ scrim }};
+$surfaceTint: {{ surfaceTint }};
+$primary: {{ primary }};
+$onPrimary: {{ onPrimary }};
+$primaryContainer: {{ primaryContainer }};
+$onPrimaryContainer: {{ onPrimaryContainer }};
+$inversePrimary: {{ inversePrimary }};
+$secondary: {{ secondary }};
+$onSecondary: {{ onSecondary }};
+$secondaryContainer: {{ secondaryContainer }};
+$onSecondaryContainer: {{ onSecondaryContainer }};
+$tertiary: {{ tertiary }};
+$onTertiary: {{ onTertiary }};
+$tertiaryContainer: {{ tertiaryContainer }};
+$onTertiaryContainer: {{ onTertiaryContainer }};
+$error: {{ error }};
+$onError: {{ onError }};
+$errorContainer: {{ errorContainer }};
+$onErrorContainer: {{ onErrorContainer }};
+$primaryFixed: {{ primaryFixed }};
+$primaryFixedDim: {{ primaryFixedDim }};
+$onPrimaryFixed: {{ onPrimaryFixed }};
+$onPrimaryFixedVariant: {{ onPrimaryFixedVariant }};
+$secondaryFixed: {{ secondaryFixed }};
+$secondaryFixedDim: {{ secondaryFixedDim }};
+$onSecondaryFixed: {{ onSecondaryFixed }};
+$onSecondaryFixedVariant: {{ onSecondaryFixedVariant }};
+$tertiaryFixed: {{ tertiaryFixed }};
+$tertiaryFixedDim: {{ tertiaryFixedDim }};
+$onTertiaryFixed: {{ onTertiaryFixed }};
+$onTertiaryFixedVariant: {{ onTertiaryFixedVariant }};
+$darkmode: {{ dark_mode }};
\ No newline at end of file
diff --git a/ignis/backup_04.03.2025/services/material/templates/gtk.css b/ignis/backup_04.03.2025/services/material/templates/gtk.css
new file mode 100644
index 0000000..688ae16
--- /dev/null
+++ b/ignis/backup_04.03.2025/services/material/templates/gtk.css
@@ -0,0 +1,50 @@
+@define-color accent_color {{ primary }};
+@define-color accent_bg_color {{ primary }};
+@define-color accent_fg_color {{ surface }};
+@define-color destructive_color {{ errorContainer }};
+@define-color destructive_bg_color {{ errorContainer }};
+@define-color destructive_fg_color {{ onSurface }};
+@define-color success_color {{ tertiary }};
+@define-color success_bg_color {{ tertiary }};
+@define-color success_fg_color {{ onSurface }};
+@define-color warning_color {{ secondary }};
+@define-color warning_bg_color {{ secondary }};
+@define-color warning_fg_color {{ onSurface }};
+@define-color error_color {{ errorContainer }};
+@define-color error_bg_color {{ errorContainer }};
+@define-color error_fg_color {{ onSurface }};
+@define-color window_bg_color {{ surface }};
+@define-color window_fg_color {{ onSurface }};
+@define-color view_bg_color {{ surface }};
+@define-color view_fg_color {{ onSurface }};
+@define-color headerbar_bg_color {{ surface }};
+@define-color headerbar_fg_color {{ onSurface }};
+@define-color headerbar_border_color {{ outline }};
+@define-color headerbar_backdrop_color {{ surface }};
+@define-color headerbar_shade_color {{ outline }};
+@define-color sidebar_bg_color {{ surfaceContainer }};
+@define-color sidebar_fg_color {{ onSurface }};
+@define-color sidebar_backdrop_color {{ surfaceContainer }};
+@define-color card_bg_color {{ surfaceContainer }};
+@define-color card_fg_color {{ onSurface }};
+@define-color card_shade_color rgba(0, 0, 0, 0.07);
+@define-color thumbnail_bg_color {{ surface }};
+@define-color thumbnail_fg_color {{ onSurface }};
+@define-color dialog_bg_color {{ surface }};
+@define-color dialog_fg_color {{ onSurface }};
+@define-color popover_bg_color {{ surfaceContainer }};
+@define-color popover_fg_color {{ onSurface }};
+@define-color shade_color rgba(0, 0, 0, 0.36);
+@define-color scrollbar_outline_color {{ outline }};
+
+.navigation-sidebar {
+ background-color: {{ surfaceContainer }};
+}
+
+switch:checked {
+ background-color: {{ primary }};
+}
+
+switch:checked slider {
+ background-color: {{ onPrimary }};
+}
\ No newline at end of file
diff --git a/ignis/backup_04.03.2025/services/material/templates/swaylock b/ignis/backup_04.03.2025/services/material/templates/swaylock
new file mode 100644
index 0000000..451229a
--- /dev/null
+++ b/ignis/backup_04.03.2025/services/material/templates/swaylock
@@ -0,0 +1,51 @@
+# ---------------------SETTINGS---------------------------#
+daemonize
+show-failed-attempts
+clock
+screenshot
+ignore-empty-password
+indicator
+font=JetBrainsMono
+indicator-radius=200
+indicator-thickness=20
+
+
+#----------------------EFFECTS----------------------------#
+effect-blur=9x5
+effect-vignette=0.5:0.5
+
+#--------------------INPUT IGNORE-------------------------#
+grace-no-mouse
+grace-no-touch
+
+
+#--------------------DATE & TIME--------------------------#
+datestr=%a,%e %B
+timestr=%H:%M
+fade-in=0.2
+
+
+#----------------------COLORS-----------------------------#
+bs-hl-color={{ error }}
+inside-color=#FFFFFF00
+ring-color=#FFFFFF00
+line-color=#FFFFFF00
+separator-color=#FFFFFF00
+key-hl-color={{ primaryFixedDim }}
+text-color={{ primaryFixedDim }}
+text-caps-lock-color={{ tertiary }}
+
+ring-clear-color=#FFFFFF00
+text-clear-color={{ onTertiary }}
+inside-clear-color={{ tertiary }}
+line-clear-color=#FFFFFF00
+
+ring-ver-color=#FFFFFF00
+text-ver-color={{ onSecondary }}
+inside-ver-color={{ secondary }}
+line-ver-color=#FFFFFF00
+
+ring-wrong-color=#FFFFFF00
+text-wrong-color={{ onError }}
+inside-wrong-color={{ error }}
+line-wrong-color=#FFFFFF00
\ No newline at end of file
diff --git a/ignis/backup_04.03.2025/services/material/util.py b/ignis/backup_04.03.2025/services/material/util.py
new file mode 100644
index 0000000..dce5ec7
--- /dev/null
+++ b/ignis/backup_04.03.2025/services/material/util.py
@@ -0,0 +1,18 @@
+import math
+
+
+def rgba_to_hex(rgba: list) -> str:
+ return "#{:02x}{:02x}{:02x}".format(*rgba)
+
+
+def calculate_optimal_size(width: int, height: int, bitmap_size: int) -> tuple:
+ image_area = width * height
+ bitmap_area = bitmap_size**2
+ scale = math.sqrt(bitmap_area / image_area) if image_area > bitmap_area else 1
+ new_width = round(width * scale)
+ new_height = round(height * scale)
+ if new_width == 0:
+ new_width = 1
+ if new_height == 0:
+ new_height = 1
+ return new_width, new_height
diff --git a/ignis/backup_04.03.2025/style.scss b/ignis/backup_04.03.2025/style.scss
new file mode 100644
index 0000000..9092992
--- /dev/null
+++ b/ignis/backup_04.03.2025/style.scss
@@ -0,0 +1,20 @@
+* {
+ font-family: JetBrainsMono;
+ font-weight: bold;
+}
+@import "../../.cache/ignis/material/colors.scss";
+
+@import "scss/mixins/window.scss";
+@import "scss/mixins/hover.scss";
+
+@import "scss/lib.scss";
+
+
+@import "./scss/bar.scss";
+@import "./scss/control_center.scss";
+@import "./scss/osd.scss";
+@import "./scss/notification_popup.scss";
+@import "./scss/launcher.scss";
+@import "./scss/settings.scss";
+@import "./scss/powermenu.scss";
+@import "./scss/notification_center.scss";
\ No newline at end of file
diff --git a/ignis/backup_30.03.2025/__init__.py b/ignis/backup_30.03.2025/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/ignis/backup_30.03.2025/__pycache__/config.cpython-313.pyc b/ignis/backup_30.03.2025/__pycache__/config.cpython-313.pyc
new file mode 100644
index 0000000..027aef0
Binary files /dev/null and b/ignis/backup_30.03.2025/__pycache__/config.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/__pycache__/user_options.cpython-313.pyc b/ignis/backup_30.03.2025/__pycache__/user_options.cpython-313.pyc
new file mode 100644
index 0000000..60b980d
Binary files /dev/null and b/ignis/backup_30.03.2025/__pycache__/user_options.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/config.py b/ignis/backup_30.03.2025/config.py
new file mode 100644
index 0000000..826bf50
--- /dev/null
+++ b/ignis/backup_30.03.2025/config.py
@@ -0,0 +1,40 @@
+from ignis.utils import Utils
+from ignis.app import IgnisApp
+from ignis.services.wallpaper import WallpaperService
+from modules import (
+ Bar,
+ ControlCenter,
+ Launcher,
+ NotificationPopup,
+ OSD,
+ Powermenu,
+ Settings,
+)
+
+app = IgnisApp.get_default()
+WallpaperService.get_default()
+
+app.add_icons(f"{Utils.get_current_dir()}/icons")
+app.apply_css(Utils.get_current_dir() + "/style.scss")
+
+Utils.exec_sh("gsettings set org.gnome.desktop.interface gtk-theme Material")
+Utils.exec_sh("gsettings set org.gnome.desktop.interface icon-theme Papirus")
+Utils.exec_sh(
+ 'gsettings set org.gnome.desktop.interface font-name "JetBrains Mono Regular 11"'
+)
+Utils.exec_sh("hyprctl reload")
+
+
+ControlCenter()
+
+for monitor in range(Utils.get_n_monitors()):
+ Bar(monitor)
+
+for monitor in range(Utils.get_n_monitors()):
+ NotificationPopup(monitor)
+
+Launcher()
+Powermenu()
+OSD()
+
+Settings()
diff --git a/ignis/backup_30.03.2025/dependencies.txt b/ignis/backup_30.03.2025/dependencies.txt
new file mode 100644
index 0000000..8024023
--- /dev/null
+++ b/ignis/backup_30.03.2025/dependencies.txt
@@ -0,0 +1,43 @@
+hyprland
+xdg-desktop-portal-hyprland
+xorg-xwayland
+qt5-wayland
+qt6-wayland
+qt5ct
+qt6ct
+libva
+linux-headers
+pipewire
+pipewire-alsa
+pipewire-pulse
+pipewire-jack
+pavucontrol
+wireplumber
+ignis-git
+dart-sass
+nm-connection-editor
+polkit-gnome
+hyprlock
+pamixer
+grimblast-git
+kitty
+thunar
+thunar-archive-plugin
+file-roller
+xdg-user-dirs
+python-requests
+python-jinja
+python-materialyoucolor
+python-pillow
+gst-plugin-pipewire
+gst-plugins-good
+gst-plugins-ugly
+gst-plugins-base
+networkmanager
+ttf-jetbrains-mono
+ttf-jetbrains-mono-nerd
+ttf-nerd-fonts-symbols
+papirus-icon-theme
+upower
+wl-clipboard
+gnome-bluetooth-3.0
\ No newline at end of file
diff --git a/ignis/backup_30.03.2025/icons/hicolor/scalable/actions/chrome-symbolic.svg b/ignis/backup_30.03.2025/icons/hicolor/scalable/actions/chrome-symbolic.svg
new file mode 100644
index 0000000..4b97625
--- /dev/null
+++ b/ignis/backup_30.03.2025/icons/hicolor/scalable/actions/chrome-symbolic.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ignis/backup_30.03.2025/icons/hicolor/scalable/actions/firefox-browser-symbolic.svg b/ignis/backup_30.03.2025/icons/hicolor/scalable/actions/firefox-browser-symbolic.svg
new file mode 100644
index 0000000..4b552d3
--- /dev/null
+++ b/ignis/backup_30.03.2025/icons/hicolor/scalable/actions/firefox-browser-symbolic.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ignis/backup_30.03.2025/icons/hicolor/scalable/actions/spotify-symbolic.svg b/ignis/backup_30.03.2025/icons/hicolor/scalable/actions/spotify-symbolic.svg
new file mode 100644
index 0000000..d5d67f7
--- /dev/null
+++ b/ignis/backup_30.03.2025/icons/hicolor/scalable/actions/spotify-symbolic.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ignis/backup_30.03.2025/misc/media-art-fallback.png b/ignis/backup_30.03.2025/misc/media-art-fallback.png
new file mode 100644
index 0000000..f76719b
Binary files /dev/null and b/ignis/backup_30.03.2025/misc/media-art-fallback.png differ
diff --git a/ignis/backup_30.03.2025/modules/__init__.py b/ignis/backup_30.03.2025/modules/__init__.py
new file mode 100644
index 0000000..2d194b4
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/__init__.py
@@ -0,0 +1,17 @@
+from .bar import Bar
+from .control_center import ControlCenter
+from .launcher import Launcher
+from .notification_popup import NotificationPopup
+from .osd import OSD
+from .powermenu import Powermenu
+from .settings import Settings
+
+__all__ = [
+ "Bar",
+ "ControlCenter",
+ "Launcher",
+ "NotificationPopup",
+ "OSD",
+ "Powermenu",
+ "Settings",
+]
diff --git a/ignis/backup_30.03.2025/modules/__pycache__/__init__.cpython-313.pyc b/ignis/backup_30.03.2025/modules/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..80bb49c
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/bar/__init__.py b/ignis/backup_30.03.2025/modules/bar/__init__.py
new file mode 100644
index 0000000..53b82f9
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/bar/__init__.py
@@ -0,0 +1,3 @@
+from .bar import Bar
+
+__all__ = ["Bar"]
diff --git a/ignis/backup_30.03.2025/modules/bar/__pycache__/__init__.cpython-313.pyc b/ignis/backup_30.03.2025/modules/bar/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..953188f
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/bar/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/bar/__pycache__/bar.cpython-313.pyc b/ignis/backup_30.03.2025/modules/bar/__pycache__/bar.cpython-313.pyc
new file mode 100644
index 0000000..a3126bf
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/bar/__pycache__/bar.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/bar/__pycache__/indicator_icon.cpython-313.pyc b/ignis/backup_30.03.2025/modules/bar/__pycache__/indicator_icon.cpython-313.pyc
new file mode 100644
index 0000000..8906e80
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/bar/__pycache__/indicator_icon.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/bar/bar.py b/ignis/backup_30.03.2025/modules/bar/bar.py
new file mode 100644
index 0000000..ebaf016
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/bar/bar.py
@@ -0,0 +1,25 @@
+from ignis.widgets import Widget
+from .widgets import StatusPill, Tray, KeyboardLayout, Battery, Apps, Workspaces
+
+
+class Bar(Widget.Window):
+ __gtype_name__ = "Bar"
+
+ def __init__(self, monitor: int):
+ super().__init__(
+ anchor=["left", "top", "right"],
+ exclusivity="exclusive",
+ monitor=monitor,
+ namespace=f"ignis_BAR_{monitor}",
+ layer="top",
+ kb_mode="none",
+ child=Widget.CenterBox(
+ css_classes=["bar-widget"],
+ start_widget=Widget.Box(child=[Workspaces()]),
+ center_widget=Widget.Box(child=[Apps()]),
+ end_widget=Widget.Box(
+ child=[Tray(), KeyboardLayout(), Battery(), StatusPill(monitor)]
+ ),
+ ),
+ css_classes=["unset"],
+ )
diff --git a/ignis/backup_30.03.2025/modules/bar/indicator_icon.py b/ignis/backup_30.03.2025/modules/bar/indicator_icon.py
new file mode 100644
index 0000000..6106f22
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/bar/indicator_icon.py
@@ -0,0 +1,31 @@
+from ignis.widgets import Widget
+from ignis.services.network import Ethernet, Wifi
+
+
+class IndicatorIcon(Widget.Icon):
+ def __init__(self, css_classes: list[str] = [], **kwargs):
+ super().__init__(
+ style="margin-right: 0.5rem;", css_classes=["unset"] + css_classes, **kwargs
+ )
+
+
+class NetworkIndicatorIcon(IndicatorIcon):
+ def __init__(
+ self, device_type: Ethernet | Wifi, other_device_type: Wifi | Ethernet
+ ):
+ self._device_type = device_type
+ self._other_device_type = other_device_type
+
+ super().__init__(icon_name=device_type.bind("icon-name"))
+
+ for binding in (
+ device_type.bind("devices", self.__check_visibility),
+ other_device_type.bind("is_connected", self.__check_visibility),
+ device_type.bind("is_connected", self.__check_visibility),
+ ):
+ self.visible = binding # type: ignore
+
+ def __check_visibility(self, *args) -> bool:
+ return len(self._device_type.devices) > 0 and (
+ not self._other_device_type.is_connected or self._device_type.is_connected
+ )
diff --git a/ignis/backup_30.03.2025/modules/bar/widgets/__init__.py b/ignis/backup_30.03.2025/modules/bar/widgets/__init__.py
new file mode 100644
index 0000000..96d6383
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/bar/widgets/__init__.py
@@ -0,0 +1,8 @@
+from .pill import StatusPill
+from .tray import Tray
+from .kb_layout import KeyboardLayout
+from .battery import Battery
+from .apps import Apps
+from .workspaces import Workspaces
+
+__all__ = ["StatusPill", "Tray", "KeyboardLayout", "Battery", "Apps", "Workspaces"]
diff --git a/ignis/backup_30.03.2025/modules/bar/widgets/__pycache__/__init__.cpython-313.pyc b/ignis/backup_30.03.2025/modules/bar/widgets/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..1ec4e3d
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/bar/widgets/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/bar/widgets/__pycache__/apps.cpython-313.pyc b/ignis/backup_30.03.2025/modules/bar/widgets/__pycache__/apps.cpython-313.pyc
new file mode 100644
index 0000000..fec106a
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/bar/widgets/__pycache__/apps.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/bar/widgets/__pycache__/battery.cpython-313.pyc b/ignis/backup_30.03.2025/modules/bar/widgets/__pycache__/battery.cpython-313.pyc
new file mode 100644
index 0000000..9401e2e
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/bar/widgets/__pycache__/battery.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/bar/widgets/__pycache__/kb_layout.cpython-313.pyc b/ignis/backup_30.03.2025/modules/bar/widgets/__pycache__/kb_layout.cpython-313.pyc
new file mode 100644
index 0000000..d9eee22
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/bar/widgets/__pycache__/kb_layout.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/bar/widgets/__pycache__/pill.cpython-313.pyc b/ignis/backup_30.03.2025/modules/bar/widgets/__pycache__/pill.cpython-313.pyc
new file mode 100644
index 0000000..ad296c9
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/bar/widgets/__pycache__/pill.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/bar/widgets/__pycache__/tray.cpython-313.pyc b/ignis/backup_30.03.2025/modules/bar/widgets/__pycache__/tray.cpython-313.pyc
new file mode 100644
index 0000000..2f1022c
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/bar/widgets/__pycache__/tray.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/bar/widgets/__pycache__/workspaces.cpython-313.pyc b/ignis/backup_30.03.2025/modules/bar/widgets/__pycache__/workspaces.cpython-313.pyc
new file mode 100644
index 0000000..3523e33
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/bar/widgets/__pycache__/workspaces.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/bar/widgets/apps.py b/ignis/backup_30.03.2025/modules/bar/widgets/apps.py
new file mode 100644
index 0000000..f939f94
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/bar/widgets/apps.py
@@ -0,0 +1,52 @@
+from ignis.widgets import Widget
+from ignis.app import IgnisApp
+from ignis.services.applications import ApplicationsService, Application
+
+applications = ApplicationsService.get_default()
+app = IgnisApp.get_default()
+
+TERMINAL_FORMAT = "kitty %command%"
+
+
+class AppItem(Widget.Button):
+ def __init__(self, app: Application):
+ menu = Widget.PopoverMenu(
+ items=[
+ Widget.MenuItem(label="Launch", on_activate=lambda x: app.launch()),
+ Widget.Separator(),
+ ]
+ + [
+ Widget.MenuItem(
+ label=i.name, on_activate=lambda x, action=i: action.launch()
+ )
+ for i in app.actions
+ ]
+ + [
+ Widget.Separator(),
+ Widget.MenuItem(label="Unpin", on_activate=lambda x: app.unpin()),
+ ]
+ )
+
+ super().__init__(
+ child=Widget.Box(child=[Widget.Icon(image=app.icon, pixel_size=32), menu]),
+ on_click=lambda x: app.launch(terminal_format=TERMINAL_FORMAT),
+ on_right_click=lambda x: menu.popup(),
+ css_classes=["pinned-app", "unset"],
+ )
+
+
+class Apps(Widget.Box):
+ def __init__(self):
+ super().__init__(
+ child=applications.bind(
+ "pinned",
+ transform=lambda value: [AppItem(app) for app in value]
+ + [
+ Widget.Button(
+ child=Widget.Icon(image="start-here-symbolic", pixel_size=32),
+ on_click=lambda x: app.toggle_window("ignis_LAUNCHER"),
+ css_classes=["pinned-app", "unset"],
+ )
+ ],
+ )
+ )
diff --git a/ignis/backup_30.03.2025/modules/bar/widgets/battery.py b/ignis/backup_30.03.2025/modules/bar/widgets/battery.py
new file mode 100644
index 0000000..658c7f6
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/bar/widgets/battery.py
@@ -0,0 +1,37 @@
+from ignis.widgets import Widget
+from ignis.services.upower import UPowerService, UPowerDevice
+
+upower = UPowerService.get_default()
+
+
+class BatteryItem(Widget.Box):
+ def __init__(self, device: UPowerDevice):
+ super().__init__(
+ css_classes=["battery-item"],
+ setup=lambda self: device.connect("removed", lambda x: self.unparent()),
+ child=[
+ Widget.Icon(
+ icon_name=device.bind("icon_name"), css_classes=["battery-icon"]
+ ),
+ Widget.Label(
+ label=device.bind("percent", lambda x: f"{int(x)}%"),
+ css_classes=["battery-percent"],
+ ),
+ Widget.Scale(
+ min=0,
+ max=100,
+ value=device.bind("percent"),
+ sensitive=False,
+ css_classes=["battery-scale"],
+ ),
+ ],
+ )
+
+
+class Battery(Widget.Box):
+ def __init__(self):
+ super().__init__(
+ setup=lambda self: upower.connect(
+ "battery-added", lambda x, device: self.append(BatteryItem(device))
+ ),
+ )
diff --git a/ignis/backup_30.03.2025/modules/bar/widgets/kb_layout.py b/ignis/backup_30.03.2025/modules/bar/widgets/kb_layout.py
new file mode 100644
index 0000000..cb451f1
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/bar/widgets/kb_layout.py
@@ -0,0 +1,18 @@
+from ignis.widgets import Widget
+from ignis.services.hyprland import HyprlandService
+
+hyprland = HyprlandService.get_default()
+
+
+class KeyboardLayout(Widget.Button):
+ def __init__(self):
+ super().__init__(
+ css_classes=["kb-layout", "unset"],
+ on_click=lambda x: hyprland.main_keyboard.switch_layout("1"),
+ visible=hyprland.is_available,
+ child=Widget.Label(
+ label=hyprland.main_keyboard.bind(
+ "active_keymap", transform=lambda value: value[:2].lower()
+ )
+ ),
+ )
diff --git a/ignis/backup_30.03.2025/modules/bar/widgets/pill.py b/ignis/backup_30.03.2025/modules/bar/widgets/pill.py
new file mode 100644
index 0000000..9ff4d5d
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/bar/widgets/pill.py
@@ -0,0 +1,111 @@
+import datetime
+from ignis.widgets import Widget
+from ignis.app import IgnisApp
+from ignis.utils import Utils
+from ignis.variable import Variable
+from ignis.services.network import NetworkService
+from ignis.services.notifications import NotificationService
+from ignis.services.recorder import RecorderService
+from ignis.services.audio import AudioService
+from ..indicator_icon import IndicatorIcon, NetworkIndicatorIcon
+from ignis.options import options
+
+network = NetworkService.get_default()
+notifications = NotificationService.get_default()
+recorder = RecorderService.get_default()
+audio = AudioService.get_default()
+
+app = IgnisApp.get_default()
+
+current_time = Variable(
+ value=Utils.Poll(1000, lambda x: datetime.datetime.now().strftime("%H:%M:%S")).bind(
+ "output"
+ )
+)
+
+
+class WifiIcon(NetworkIndicatorIcon):
+ def __init__(self):
+ super().__init__(device_type=network.wifi, other_device_type=network.ethernet)
+
+
+class EthernetIcon(NetworkIndicatorIcon):
+ def __init__(self):
+ super().__init__(device_type=network.ethernet, other_device_type=network.wifi)
+
+
+class VpnIcon(IndicatorIcon):
+ def __init__(self):
+ super().__init__(
+ image=network.vpn.bind("icon_name"),
+ visible=network.vpn.bind("is_connected"),
+ )
+
+
+class DNDIcon(IndicatorIcon):
+ def __init__(self):
+ super().__init__(
+ image="notification-disabled-symbolic",
+ visible=options.notifications.bind("dnd"),
+ )
+
+
+class RecorderIcon(IndicatorIcon):
+ def __init__(self):
+ super().__init__(
+ image="media-record-symbolic",
+ css_classes=["record-indicator"],
+ setup=lambda self: recorder.connect(
+ "notify::is-paused", self.__update_css_class
+ ),
+ visible=recorder.bind("active"),
+ )
+
+ def __update_css_class(self, *args) -> None:
+ if recorder.is_paused:
+ self.remove_css_class("active")
+ else:
+ self.add_css_class("active")
+
+
+class VolumeIcon(IndicatorIcon):
+ def __init__(self):
+ super().__init__(
+ image=audio.speaker.bind("icon_name"),
+ )
+
+
+class StatusPill(Widget.Button):
+ def __init__(self, monitor: int):
+ self._monitor = monitor
+ self._window: Widget.Window = app.get_window("ignis_CONTROL_CENTER") # type: ignore
+
+ super().__init__(
+ child=Widget.Box(
+ child=[
+ RecorderIcon(),
+ WifiIcon(),
+ EthernetIcon(),
+ VpnIcon(),
+ VolumeIcon(),
+ DNDIcon(),
+ Widget.Label(
+ label=current_time.bind("value"),
+ ),
+ ]
+ ),
+ css_classes=self._window.bind(
+ "visible",
+ lambda value: ["clock", "unset", "active"]
+ if value
+ else ["clock", "unset"],
+ ),
+ on_click=self.__on_click,
+ )
+
+ def __on_click(self, x) -> None:
+ if self._window.monitor == self._monitor:
+ self._window.visible = not self._window.visible
+ else:
+ self._window.set_monitor(self._monitor)
+ self._window.visible = True
diff --git a/ignis/backup_30.03.2025/modules/bar/widgets/tray.py b/ignis/backup_30.03.2025/modules/bar/widgets/tray.py
new file mode 100644
index 0000000..3ef6106
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/bar/widgets/tray.py
@@ -0,0 +1,41 @@
+from ignis.widgets import Widget
+from ignis.services.system_tray import SystemTrayService, SystemTrayItem
+
+system_tray = SystemTrayService.get_default()
+
+
+class TrayItem(Widget.Button):
+ __gtype_name__ = "TrayItem"
+
+ def __init__(self, item: SystemTrayItem):
+ if item.menu:
+ menu = item.menu.copy()
+ else:
+ menu = None
+
+ super().__init__(
+ child=Widget.Box(
+ child=[
+ Widget.Icon(image=item.bind("icon"), pixel_size=24),
+ menu,
+ ]
+ ),
+ tooltip_text=item.bind("tooltip"),
+ on_click=lambda x: item.activate(),
+ setup=lambda self: item.connect("removed", lambda x: self.unparent()),
+ on_right_click=lambda x: menu.popup() if menu else None,
+ css_classes=["tray-item", "unset"],
+ )
+
+
+class Tray(Widget.Box):
+ __gtype_name__ = "Tray"
+
+ def __init__(self):
+ super().__init__(
+ css_classes=["tray"],
+ setup=lambda self: system_tray.connect(
+ "added", lambda x, item: self.append(TrayItem(item))
+ ),
+ spacing=10,
+ )
diff --git a/ignis/backup_30.03.2025/modules/bar/widgets/workspaces.py b/ignis/backup_30.03.2025/modules/bar/widgets/workspaces.py
new file mode 100644
index 0000000..4cb479e
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/bar/widgets/workspaces.py
@@ -0,0 +1,49 @@
+from ignis.widgets import Widget
+from ignis.services.hyprland import HyprlandService, HyprlandWorkspace
+
+hyprland = HyprlandService.get_default()
+
+
+class WorkspaceButton(Widget.Button):
+ def __init__(self, workspace: HyprlandWorkspace) -> None:
+ super().__init__(
+ css_classes=["workspace", "unset"],
+ on_click=lambda x: workspace.switch_to(),
+ halign="start",
+ valign="center",
+ )
+ if workspace.id == hyprland.active_workspace.id:
+ self.add_css_class("active")
+
+
+def scroll_workspaces(direction: str) -> None:
+ current = hyprland.active_workspace.id
+ if direction == "up":
+ target = current - 1
+ hyprland.switch_to_workspace(target)
+ else:
+ target = current + 1
+ if target == 11:
+ return
+ hyprland.switch_to_workspace(target)
+
+
+class Workspaces(Widget.Box):
+ def __init__(self):
+ if hyprland.is_available:
+ child = [
+ Widget.EventBox(
+ on_scroll_up=lambda x: scroll_workspaces("up"),
+ on_scroll_down=lambda x: scroll_workspaces("down"),
+ css_classes=["workspaces"],
+ child=hyprland.bind_many(
+ ["workspaces", "active_workspace"],
+ transform=lambda workspaces, *_: [
+ WorkspaceButton(i) for i in workspaces
+ ],
+ ),
+ )
+ ]
+ else:
+ child = []
+ super().__init__(child=child)
diff --git a/ignis/backup_30.03.2025/modules/control_center/__init__.py b/ignis/backup_30.03.2025/modules/control_center/__init__.py
new file mode 100644
index 0000000..15bd497
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/control_center/__init__.py
@@ -0,0 +1,3 @@
+from .control_center import ControlCenter
+
+__all__ = ["ControlCenter"]
diff --git a/ignis/backup_30.03.2025/modules/control_center/__pycache__/__init__.cpython-313.pyc b/ignis/backup_30.03.2025/modules/control_center/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..1a1eec1
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/control_center/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/control_center/__pycache__/control_center.cpython-313.pyc b/ignis/backup_30.03.2025/modules/control_center/__pycache__/control_center.cpython-313.pyc
new file mode 100644
index 0000000..932a235
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/control_center/__pycache__/control_center.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/control_center/__pycache__/menu.cpython-313.pyc b/ignis/backup_30.03.2025/modules/control_center/__pycache__/menu.cpython-313.pyc
new file mode 100644
index 0000000..16b9412
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/control_center/__pycache__/menu.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/control_center/__pycache__/qs_button.cpython-313.pyc b/ignis/backup_30.03.2025/modules/control_center/__pycache__/qs_button.cpython-313.pyc
new file mode 100644
index 0000000..015418e
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/control_center/__pycache__/qs_button.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/control_center/control_center.py b/ignis/backup_30.03.2025/modules/control_center/control_center.py
new file mode 100644
index 0000000..0915bed
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/control_center/control_center.py
@@ -0,0 +1,66 @@
+from ignis.widgets import Widget
+from ignis.app import IgnisApp
+from .widgets import (
+ QuickSettings,
+ Brightness,
+ VolumeSlider,
+ User,
+ Media,
+ NotificationCenter,
+)
+from .menu import opened_menu
+
+app = IgnisApp.get_default()
+
+
+class ControlCenter(Widget.RevealerWindow):
+ def __init__(self):
+ revealer = Widget.Revealer(
+ transition_type="slide_left",
+ child=Widget.Box(
+ vertical=True,
+ css_classes=["control-center"],
+ child=[
+ Widget.Box(
+ vertical=True,
+ css_classes=["control-center-widget"],
+ child=[
+ QuickSettings(),
+ VolumeSlider("speaker"),
+ VolumeSlider("microphone"),
+ Brightness(),
+ User(),
+ Media(),
+ ],
+ ),
+ NotificationCenter(),
+ ],
+ ),
+ transition_duration=300,
+ reveal_child=True,
+ )
+
+ super().__init__(
+ visible=False,
+ popup=True,
+ kb_mode="on_demand",
+ layer="top",
+ css_classes=["unset"],
+ anchor=["top", "right", "bottom", "left"],
+ namespace="ignis_CONTROL_CENTER",
+ child=Widget.Box(
+ child=[
+ Widget.Button(
+ vexpand=True,
+ hexpand=True,
+ css_classes=["unset"],
+ on_click=lambda x: app.close_window("ignis_CONTROL_CENTER"),
+ ),
+ revealer,
+ ],
+ ),
+ setup=lambda self: self.connect(
+ "notify::visible", lambda x, y: opened_menu.set_value("")
+ ),
+ revealer=revealer,
+ )
diff --git a/ignis/backup_30.03.2025/modules/control_center/menu.py b/ignis/backup_30.03.2025/modules/control_center/menu.py
new file mode 100644
index 0000000..6e71480
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/control_center/menu.py
@@ -0,0 +1,34 @@
+from gi.repository import GObject # type: ignore
+from ignis.widgets import Widget
+from ignis.variable import Variable
+from ignis.base_widget import BaseWidget
+
+opened_menu = Variable()
+
+
+class Menu(Widget.Revealer):
+ def __init__(self, name: str, child: list[BaseWidget], **kwargs):
+ self._name = name
+ self._box = Widget.Box(
+ vertical=True,
+ css_classes=["control-center-menu"],
+ child=child,
+ )
+
+ super().__init__(
+ transition_type="slide_down",
+ transition_duration=300,
+ reveal_child=opened_menu.bind("value", lambda value: value == self._name),
+ child=self._box,
+ **kwargs,
+ )
+
+ def toggle(self) -> None:
+ if self.reveal_child:
+ opened_menu.value = ""
+ else:
+ opened_menu.value = self._name
+
+ @GObject.Property
+ def box(self) -> Widget.Box:
+ return self._box
diff --git a/ignis/backup_30.03.2025/modules/control_center/qs_button.py b/ignis/backup_30.03.2025/modules/control_center/qs_button.py
new file mode 100644
index 0000000..e049c53
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/control_center/qs_button.py
@@ -0,0 +1,65 @@
+from ignis.widgets import Widget
+from gi.repository import GObject # type: ignore
+from typing import Callable
+from ignis.gobject import Binding
+from .menu import Menu
+
+
+class QSButton(Widget.Button):
+ def __init__(
+ self,
+ label: str | Binding,
+ icon_name: str | Binding,
+ on_activate: Callable | None = None,
+ on_deactivate: Callable | None = None,
+ menu: Menu | None = None,
+ **kwargs,
+ ):
+ self.on_activate = on_activate
+ self.on_deactivate = on_deactivate
+ self._active = False
+ self._menu = menu
+ super().__init__(
+ child=Widget.Box(
+ child=[
+ Widget.Icon(image=icon_name),
+ Widget.Label(label=label, css_classes=["qs-button-label"]),
+ Widget.Arrow(
+ halign="end",
+ hexpand=True,
+ pixel_size=20,
+ rotated=menu.bind("reveal_child"),
+ )
+ if menu
+ else None,
+ ]
+ ),
+ on_click=self.__callback,
+ css_classes=["qs-button", "unset"],
+ hexpand=True,
+ **kwargs,
+ )
+
+ def __callback(self, *args) -> None:
+ if self.active:
+ if self.on_deactivate:
+ self.on_deactivate(self)
+ else:
+ if self.on_activate:
+ self.on_activate(self)
+
+ @GObject.Property
+ def active(self) -> bool:
+ return self._active
+
+ @active.setter
+ def active(self, value: bool) -> None:
+ self._active = value
+ if value:
+ self.add_css_class("active")
+ else:
+ self.remove_css_class("active")
+
+ @GObject.Property
+ def menu(self) -> Menu | None:
+ return self._menu
diff --git a/ignis/backup_30.03.2025/modules/control_center/widgets/__init__.py b/ignis/backup_30.03.2025/modules/control_center/widgets/__init__.py
new file mode 100644
index 0000000..399ce63
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/control_center/widgets/__init__.py
@@ -0,0 +1,15 @@
+from .quick_settings import QuickSettings
+from .brightness import Brightness
+from .volume import VolumeSlider
+from .user import User
+from .media import Media
+from .notification_center import NotificationCenter
+
+__all__ = [
+ "QuickSettings",
+ "Brightness",
+ "VolumeSlider",
+ "User",
+ "Media",
+ "NotificationCenter",
+]
diff --git a/ignis/backup_30.03.2025/modules/control_center/widgets/__pycache__/__init__.cpython-313.pyc b/ignis/backup_30.03.2025/modules/control_center/widgets/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..4dda8e6
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/control_center/widgets/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/control_center/widgets/__pycache__/brightness.cpython-313.pyc b/ignis/backup_30.03.2025/modules/control_center/widgets/__pycache__/brightness.cpython-313.pyc
new file mode 100644
index 0000000..55de99a
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/control_center/widgets/__pycache__/brightness.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/control_center/widgets/__pycache__/media.cpython-313.pyc b/ignis/backup_30.03.2025/modules/control_center/widgets/__pycache__/media.cpython-313.pyc
new file mode 100644
index 0000000..f8ddd29
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/control_center/widgets/__pycache__/media.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/control_center/widgets/__pycache__/notification_center.cpython-313.pyc b/ignis/backup_30.03.2025/modules/control_center/widgets/__pycache__/notification_center.cpython-313.pyc
new file mode 100644
index 0000000..8621067
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/control_center/widgets/__pycache__/notification_center.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/control_center/widgets/__pycache__/user.cpython-313.pyc b/ignis/backup_30.03.2025/modules/control_center/widgets/__pycache__/user.cpython-313.pyc
new file mode 100644
index 0000000..62047f3
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/control_center/widgets/__pycache__/user.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/control_center/widgets/__pycache__/volume.cpython-313.pyc b/ignis/backup_30.03.2025/modules/control_center/widgets/__pycache__/volume.cpython-313.pyc
new file mode 100644
index 0000000..6d3bc56
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/control_center/widgets/__pycache__/volume.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/control_center/widgets/brightness.py b/ignis/backup_30.03.2025/modules/control_center/widgets/brightness.py
new file mode 100644
index 0000000..6d4aef2
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/control_center/widgets/brightness.py
@@ -0,0 +1,28 @@
+from ignis.services.backlight import BacklightService
+from ignis.widgets import Widget
+
+backlight = BacklightService.get_default()
+
+
+class Brightness(Widget.Box):
+ def __init__(self):
+ super().__init__(
+ visible=backlight.bind("available"),
+ hexpand=True,
+ style="margin-top: 0.25rem;",
+ child=[
+ Widget.Icon(
+ image="display-brightness-symbolic",
+ css_classes=["material-slider-icon"],
+ pixel_size=18,
+ ),
+ Widget.Scale(
+ min=0,
+ max=backlight.max_brightness,
+ hexpand=True,
+ value=backlight.bind("brightness"),
+ css_classes=["material-slider"],
+ on_change=lambda x: backlight.set_brightness(x.value),
+ ),
+ ],
+ )
diff --git a/ignis/backup_30.03.2025/modules/control_center/widgets/media.py b/ignis/backup_30.03.2025/modules/control_center/widgets/media.py
new file mode 100644
index 0000000..1491321
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/control_center/widgets/media.py
@@ -0,0 +1,210 @@
+import os
+import ignis
+from ignis.widgets import Widget
+from ignis.services.mpris import MprisService, MprisPlayer
+from ignis.utils import Utils
+from services.material import MaterialService
+from ignis.app import IgnisApp
+from ignis.exceptions import StylePathNotFoundError
+
+
+mpris = MprisService.get_default()
+app = IgnisApp.get_default()
+material = MaterialService.get_default()
+
+MEDIA_TEMPLATE = Utils.get_current_dir() + "/media.scss"
+MEDIA_SCSS_CACHE_DIR = ignis.CACHE_DIR + "/media" # type: ignore
+MEDIA_ART_FALLBACK = Utils.get_current_dir() + "/../../../misc/media-art-fallback.png"
+os.makedirs(MEDIA_SCSS_CACHE_DIR, exist_ok=True)
+
+
+PLAYER_ICONS = {
+ "spotify": "spotify-symbolic",
+ "firefox": "firefox-browser-symbolic",
+ "chrome": "chrome-symbolic",
+ None: "folder-music-symbolic",
+}
+
+
+class Player(Widget.Revealer):
+ def __init__(self, player: MprisPlayer) -> None:
+ self._player = player
+ self._colors_path = f"{MEDIA_SCSS_CACHE_DIR}/{self._player.desktop_entry}.scss"
+ player.connect("closed", lambda x: self.destroy())
+ player.connect("notify::art-url", lambda x, y: self.load_colors())
+ self.load_colors()
+
+ super().__init__(
+ transition_type="slide_down",
+ reveal_child=False,
+ css_classes=[self.get_css("media")],
+ child=Widget.Overlay(
+ child=Widget.Box(css_classes=[self.get_css("media-image")]),
+ overlays=[
+ Widget.Box(
+ hexpand=True,
+ vexpand=True,
+ css_classes=[self.get_css("media-image-gradient")],
+ ),
+ Widget.Icon(
+ icon_name=self.get_player_icon(),
+ pixel_size=22,
+ halign="start",
+ valign="start",
+ css_classes=[self.get_css("media-player-icon")],
+ ),
+ Widget.Box(
+ vertical=True,
+ hexpand=True,
+ css_classes=[self.get_css("media-content")],
+ child=[
+ Widget.Box(
+ vexpand=True,
+ valign="center",
+ child=[
+ Widget.Box(
+ hexpand=True,
+ vertical=True,
+ child=[
+ Widget.Label(
+ ellipsize="end",
+ label=player.bind("title"),
+ max_width_chars=30,
+ halign="start",
+ css_classes=[
+ self.get_css("media-title")
+ ],
+ ),
+ Widget.Label(
+ label=player.bind("artist"),
+ max_width_chars=30,
+ ellipsize="end",
+ halign="start",
+ css_classes=[
+ self.get_css("media-artist")
+ ],
+ ),
+ ],
+ ),
+ Widget.Button(
+ child=Widget.Icon(
+ image=player.bind(
+ "playback_status",
+ lambda value: "media-playback-pause-symbolic"
+ if value == "Playing"
+ else "media-playback-start-symbolic",
+ ),
+ pixel_size=18,
+ ),
+ on_click=lambda x: player.play_pause(),
+ visible=player.bind("can_play"),
+ css_classes=player.bind(
+ "playback_status",
+ lambda value: [
+ self.get_css("media-playback-button"),
+ "playing",
+ ]
+ if value == "Playing"
+ else [
+ self.get_css("media-playback-button"),
+ "paused",
+ ],
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ Widget.Box(
+ vexpand=True,
+ valign="end",
+ style="padding: 1rem;",
+ child=[
+ Widget.Scale(
+ value=player.bind("position"),
+ max=player.bind("length"),
+ hexpand=True,
+ css_classes=[self.get_css("media-scale")],
+ on_change=lambda x: player.set_position(x.value),
+ visible=player.bind(
+ "position", lambda value: value != -1
+ ),
+ ),
+ Widget.Button(
+ child=Widget.Icon(
+ image="media-skip-backward-symbolic",
+ pixel_size=20,
+ ),
+ css_classes=[self.get_css("media-skip-button")],
+ on_click=lambda x: player.previous(),
+ visible=player.bind("can_go_previous"),
+ style="margin-left: 1rem;",
+ ),
+ Widget.Button(
+ child=Widget.Icon(
+ image="media-skip-forward-symbolic",
+ pixel_size=20,
+ ),
+ css_classes=[self.get_css("media-skip-button")],
+ on_click=lambda x: player.next(),
+ visible=player.bind("can_go_next"),
+ style="margin-left: 1rem;",
+ ),
+ ],
+ ),
+ ],
+ ),
+ )
+
+ def get_player_icon(self) -> str:
+ if self._player.desktop_entry == "firefox":
+ return PLAYER_ICONS["firefox"]
+ elif self._player.desktop_entry == "spotify":
+ return PLAYER_ICONS["spotify"]
+ elif self._player.track_id is not None:
+ if "chromium" in self._player.track_id or "chrome" in self._player.track_id:
+ return PLAYER_ICONS["chrome"]
+
+ return PLAYER_ICONS[None]
+
+ def destroy(self) -> None:
+ self.set_reveal_child(False)
+ Utils.Timeout(self.transition_duration, super().unparent)
+
+ def get_css(self, class_name: str) -> str:
+ return f"{class_name}-{self._player.desktop_entry}"
+
+ def load_colors(self) -> None:
+ if not self._player.art_url:
+ art_url = MEDIA_ART_FALLBACK
+ else:
+ art_url = self._player.art_url
+
+ try:
+ app.remove_css(self._colors_path)
+ except StylePathNotFoundError:
+ pass
+
+ colors = material.get_colors_from_img(art_url, True)
+ colors["art_url"] = art_url
+ colors["desktop_entry"] = self._player.desktop_entry
+ material.render_template(
+ colors, input_file=MEDIA_TEMPLATE, output_file=self._colors_path
+ )
+ app.apply_css(self._colors_path)
+
+
+class Media(Widget.Box):
+ def __init__(self):
+ super().__init__(
+ vertical=True,
+ setup=lambda self: mpris.connect(
+ "player_added", lambda x, player: self.__add_player(player)
+ ),
+ css_classes=["rec-unset"],
+ )
+
+ def __add_player(self, obj: MprisPlayer) -> None:
+ player = Player(obj)
+ self.append(player)
+ player.set_reveal_child(True)
diff --git a/ignis/backup_30.03.2025/modules/control_center/widgets/media.scss b/ignis/backup_30.03.2025/modules/control_center/widgets/media.scss
new file mode 100644
index 0000000..f4a10ae
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/control_center/widgets/media.scss
@@ -0,0 +1,84 @@
+// JINJA2 TEMPLATE
+// WARNING: Do not import this file in style.scss
+
+.media-{{ desktop_entry }} {
+ margin-top: 1rem;
+}
+
+
+.media-image-{{ desktop_entry }} {
+ background-image: url('file:///{{ art_url }}');
+ background-repeat: no-repeat;
+ background-size: cover;
+ background-position: center;
+ min-height: 11rem;
+ border-radius: 1.5rem;
+}
+
+.media-image-gradient-{{ desktop_entry }} {
+ border-radius: 1.3rem;
+ background: linear-gradient(90deg, rgba({{ onPrimary }}, 0.9) 0%, rgba({{ onPrimary }}, 0.4) 50%, rgba({{ onPrimary }}, 0.9) 100%);
+}
+
+.media-scale-{{ desktop_entry }} trough,
+.media-scale-{{ desktop_entry }} trough highlight {
+ background-color: rgba({{ onSurface }}, 0.2);
+ border-radius: 1rem;
+ min-height: 0.2rem;
+}
+
+
+.media-scale-{{ desktop_entry }} trough highlight {
+ background-color: {{ onSurface }};
+}
+
+.media-scale-{{ desktop_entry }} slider {
+ background-color: {{ onSurface }};
+ padding: 0.5rem 0.1rem;
+ margin: -0.5rem -0.1rem;
+}
+
+.media-title-{{ desktop_entry }} {
+ color: {{ onSurface }};
+}
+
+.media-artist-{{ desktop_entry }} {
+ color: {{ onSurfaceVariant }};
+ font-weight: normal;
+}
+
+
+.media-content-{{ desktop_entry }} {
+ padding: 1rem;
+}
+
+.media-playback-button-{{ desktop_entry }} {
+ background-color: {{ primary }};
+ color: {{ onPrimary }};
+ border-radius: 4rem;
+ min-width: 2.7rem;
+ min-height: 2.7rem;
+ transition: 0.3s;
+
+ &:hover {
+ background-color: lighten({{ primary }}, 5%);
+ }
+
+ &.playing {
+ border-radius: 1rem;
+ }
+}
+
+.media-player-icon-{{ desktop_entry }} {
+ color: {{ primary }};
+ padding: 1rem;
+ padding-top: 1.2rem;
+}
+
+.media-skip-button-{{ desktop_entry }} {
+ transition: 0.3s;
+ color: {{ onSurface }};
+ &:hover {
+ color: {{ primary }};
+ }
+}
\ No newline at end of file
diff --git a/ignis/backup_30.03.2025/modules/control_center/widgets/notification_center.py b/ignis/backup_30.03.2025/modules/control_center/widgets/notification_center.py
new file mode 100644
index 0000000..962ef6c
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/control_center/widgets/notification_center.py
@@ -0,0 +1,109 @@
+from ignis.widgets import Widget
+from ignis.services.notifications import Notification, NotificationService
+from ignis.utils import Utils
+from gi.repository import GLib # type: ignore
+from ...shared_widgets import NotificationWidget
+
+notifications = NotificationService.get_default()
+
+
+class Popup(Widget.Revealer):
+ def __init__(self, notification: Notification, **kwargs):
+ widget = NotificationWidget(notification)
+ super().__init__(child=widget, transition_type="slide_down", **kwargs)
+
+ notification.connect("closed", lambda x: self.destroy())
+
+ def destroy(self):
+ self.reveal_child = False
+ Utils.Timeout(self.transition_duration, self.unparent)
+
+
+class NotificationList(Widget.Box):
+ __gtype_name__ = "NotificationList"
+
+ def __init__(self):
+ loading_notifications_label = Widget.Label(
+ label="Loading notifications...",
+ valign="center",
+ vexpand=True,
+ css_classes=["notification-center-info-label"],
+ )
+
+ super().__init__(
+ vertical=True,
+ child=[loading_notifications_label],
+ vexpand=True,
+ css_classes=["rec-unset"],
+ setup=lambda self: notifications.connect(
+ "notified",
+ lambda x, notification: self.__on_notified(notification),
+ ),
+ )
+
+ Utils.ThreadTask(
+ self.__load_notifications,
+ lambda result: self.set_child(result),
+ ).run()
+
+ def __on_notified(self, notification: Notification) -> None:
+ notify = Popup(notification)
+ self.prepend(notify)
+ notify.reveal_child = True
+
+ def __load_notifications(self) -> list[Widget.Label | Popup]:
+ widgets: list[Widget.Label | Popup] = []
+ for i in notifications.notifications:
+ GLib.idle_add(lambda i=i: widgets.append(Popup(i, reveal_child=True)))
+
+ widgets.append(
+ Widget.Label(
+ label="No notifications",
+ valign="center",
+ vexpand=True,
+ visible=notifications.bind(
+ "notifications", lambda value: len(value) == 0
+ ),
+ css_classes=["notification-center-info-label"],
+ )
+ )
+ return widgets
+
+
+class NotificationCenter(Widget.Box):
+ __gtype_name__ = "NotificationCenter"
+
+ def __init__(self):
+ super().__init__(
+ vertical=True,
+ vexpand=True,
+ css_classes=["notification-center"],
+ child=[
+ Widget.Box(
+ css_classes=["notification-center-header", "rec-unset"],
+ child=[
+ Widget.Label(
+ label=notifications.bind(
+ "notifications", lambda value: str(len(value))
+ ),
+ css_classes=["notification-count"],
+ ),
+ Widget.Label(
+ label="notifications",
+ css_classes=["notification-header-label"],
+ ),
+ Widget.Button(
+ child=Widget.Label(label="Clear all"),
+ halign="end",
+ hexpand=True,
+ on_click=lambda x: notifications.clear_all(),
+ css_classes=["notification-clear-all"],
+ ),
+ ],
+ ),
+ Widget.Scroll(
+ child=NotificationList(),
+ vexpand=True,
+ ),
+ ],
+ )
diff --git a/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/__init__.py b/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/__init__.py
new file mode 100644
index 0000000..9ad69f9
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/__init__.py
@@ -0,0 +1,3 @@
+from .quick_settings import QuickSettings
+
+__all__ = ["QuickSettings"]
diff --git a/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/__pycache__/__init__.cpython-313.pyc b/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..e5af60d
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/__pycache__/bluetooth.cpython-313.pyc b/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/__pycache__/bluetooth.cpython-313.pyc
new file mode 100644
index 0000000..c9c1f63
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/__pycache__/bluetooth.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/__pycache__/dark_mode.cpython-313.pyc b/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/__pycache__/dark_mode.cpython-313.pyc
new file mode 100644
index 0000000..8c93aab
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/__pycache__/dark_mode.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/__pycache__/dnd.cpython-313.pyc b/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/__pycache__/dnd.cpython-313.pyc
new file mode 100644
index 0000000..3aa18ee
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/__pycache__/dnd.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/__pycache__/ethernet.cpython-313.pyc b/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/__pycache__/ethernet.cpython-313.pyc
new file mode 100644
index 0000000..f7c076a
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/__pycache__/ethernet.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/__pycache__/quick_settings.cpython-313.pyc b/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/__pycache__/quick_settings.cpython-313.pyc
new file mode 100644
index 0000000..715938c
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/__pycache__/quick_settings.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/__pycache__/record.cpython-313.pyc b/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/__pycache__/record.cpython-313.pyc
new file mode 100644
index 0000000..7eadb24
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/__pycache__/record.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/__pycache__/vpn.cpython-313.pyc b/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/__pycache__/vpn.cpython-313.pyc
new file mode 100644
index 0000000..cfba3ad
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/__pycache__/vpn.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/__pycache__/wifi.cpython-313.pyc b/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/__pycache__/wifi.cpython-313.pyc
new file mode 100644
index 0000000..3ceb542
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/__pycache__/wifi.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/bluetooth.py b/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/bluetooth.py
new file mode 100644
index 0000000..15f1a32
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/bluetooth.py
@@ -0,0 +1,87 @@
+from ignis.widgets import Widget
+from ...qs_button import QSButton
+from ...menu import Menu
+from ....shared_widgets import ToggleBox
+from ignis.services.bluetooth import BluetoothService, BluetoothDevice
+
+bluetooth = BluetoothService.get_default()
+
+
+class BluetoothDeviceItem(Widget.Button):
+ def __init__(self, device: BluetoothDevice):
+ super().__init__(
+ css_classes=["network-item", "unset"],
+ on_click=lambda x: device.disconnect_from()
+ if device.connected
+ else device.connect_to(),
+ child=Widget.Box(
+ child=[
+ Widget.Icon(
+ image=device.bind("icon_name"),
+ ),
+ Widget.Label(
+ label=device.alias,
+ halign="start",
+ css_classes=["wifi-network-label"],
+ ),
+ Widget.Icon(
+ image="object-select-symbolic",
+ halign="end",
+ hexpand=True,
+ visible=device.bind("connected"),
+ ),
+ ]
+ ),
+ )
+
+
+class BluetoothMenu(Menu):
+ def __init__(self):
+ super().__init__(
+ name="bluetooth",
+ child=[
+ ToggleBox(
+ label="Bluetooth",
+ active=bluetooth.powered,
+ on_change=lambda x, state: bluetooth.set_powered(state),
+ css_classes=["network-header-box"],
+ ),
+ Widget.Box(
+ vertical=True,
+ child=bluetooth.bind(
+ "devices",
+ transform=lambda value: [BluetoothDeviceItem(i) for i in value],
+ ),
+ ),
+ ],
+ )
+
+
+class BluetoothButton(QSButton):
+ def __init__(self):
+ menu = BluetoothMenu()
+
+ def get_label(devices: list[BluetoothDevice]) -> str:
+ if len(devices) == 0:
+ return "Bluetooth"
+ elif len(devices) == 1:
+ return devices[0].alias
+ else:
+ return f"{len(devices)} pairs"
+
+ def toggle_menu(x) -> None:
+ bluetooth.set_setup_mode(True)
+ menu.toggle()
+
+ super().__init__(
+ label=bluetooth.bind("connected_devices", get_label),
+ icon_name="bluetooth-active-symbolic",
+ on_activate=toggle_menu,
+ on_deactivate=toggle_menu,
+ active=bluetooth.bind("powered"),
+ menu=menu,
+ )
+
+
+def bluetooth_control() -> list[QSButton]:
+ return [] if bluetooth.state == "absent" else [BluetoothButton()]
diff --git a/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/dark_mode.py b/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/dark_mode.py
new file mode 100644
index 0000000..e7fe3f3
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/dark_mode.py
@@ -0,0 +1,15 @@
+from ...qs_button import QSButton
+from user_options import user_options
+
+
+class DarkModeButton(QSButton):
+ __gtype_name__ = "DarkModeButton"
+
+ def __init__(self):
+ super().__init__(
+ label="Dark",
+ icon_name="night-light-symbolic",
+ on_activate=lambda x: user_options.material.set_dark_mode(True),
+ on_deactivate=lambda x: user_options.material.set_dark_mode(False),
+ active=user_options.material.bind("dark_mode"),
+ )
diff --git a/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/dnd.py b/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/dnd.py
new file mode 100644
index 0000000..1544f3e
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/dnd.py
@@ -0,0 +1,28 @@
+from ...qs_button import QSButton
+from ignis.services.notifications import NotificationService
+from ignis.options import options
+
+notifications = NotificationService.get_default()
+
+
+class DNDButton(QSButton):
+ __gtype_name__ = "DNDButton"
+
+ def __init__(self):
+ super().__init__(
+ label=options.notifications.bind(
+ "dnd", lambda value: "Silent" if value else "Noisy"
+ ),
+ icon_name=options.notifications.bind(
+ "dnd",
+ transform=lambda value: "notification-disabled-symbolic"
+ if value
+ else "notification-symbolic",
+ ),
+ on_activate=lambda x: self.__activate(True),
+ on_deactivate=lambda x: self.__activate(False),
+ active=options.notifications.bind("dnd"),
+ )
+
+ def __activate(self, state: bool) -> None:
+ options.notifications.dnd = state
diff --git a/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/ethernet.py b/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/ethernet.py
new file mode 100644
index 0000000..9ec00bc
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/ethernet.py
@@ -0,0 +1,102 @@
+import asyncio
+from ignis.widgets import Widget
+from ignis.utils import Utils
+from ...qs_button import QSButton
+from ...menu import Menu
+from ignis.services.network import NetworkService, EthernetDevice
+
+network = NetworkService.get_default()
+
+
+class EthernetConnectionItem(Widget.Button):
+ def __init__(self, device: EthernetDevice):
+ super().__init__(
+ css_classes=["network-item", "unset"],
+ on_click=lambda x: asyncio.create_task(device.disconnect_from())
+ if device.is_connected
+ else asyncio.create_task(device.connect_to()),
+ child=Widget.Box(
+ child=[
+ Widget.Icon(image="network-wired-symbolic"),
+ Widget.Label(
+ label=device.name,
+ ellipsize="end",
+ max_width_chars=20,
+ halign="start",
+ ),
+ Widget.Button(
+ child=Widget.Label(
+ label=device.bind(
+ "is_connected",
+ lambda value: "Disconnect" if value else "Connect",
+ )
+ ),
+ css_classes=["connect-label", "unset"],
+ halign="end",
+ hexpand=True,
+ ),
+ ]
+ ),
+ )
+
+
+class EthernetMenu(Menu):
+ def __init__(self):
+ super().__init__(
+ name="ethernet",
+ child=[
+ Widget.Box(
+ css_classes=["network-header-box"],
+ child=[
+ Widget.Icon(icon_name="network-wired-symbolic", pixel_size=28),
+ Widget.Label(
+ label="Wired connections",
+ css_classes=["network-header-label"],
+ ),
+ ],
+ ),
+ Widget.Box(
+ vertical=True,
+ child=network.ethernet.bind(
+ "devices",
+ lambda value: [EthernetConnectionItem(i) for i in value],
+ ),
+ ),
+ Widget.Separator(),
+ Widget.Button(
+ css_classes=["network-item", "unset"],
+ style="margin-bottom: 0;",
+ on_click=lambda x: asyncio.create_task(Utils.exec_sh_async("nm-connection-editor")),
+ child=Widget.Box(
+ child=[
+ Widget.Icon(image="preferences-system-symbolic"),
+ Widget.Label(
+ label="Network Settings",
+ halign="start",
+ ),
+ ]
+ ),
+ ),
+ ],
+ )
+
+
+class EthernetButton(QSButton):
+ def __init__(self):
+ menu = EthernetMenu()
+
+ super().__init__(
+ label="Wired",
+ icon_name="network-wired-symbolic",
+ on_activate=lambda x: menu.toggle(),
+ on_deactivate=lambda x: menu.toggle(),
+ menu=menu,
+ active=network.ethernet.bind("is_connected"),
+ )
+
+
+def ethernet_control() -> list[QSButton]:
+ if len(network.ethernet.devices) > 0:
+ return [EthernetButton()]
+ else:
+ return []
diff --git a/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/quick_settings.py b/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/quick_settings.py
new file mode 100644
index 0000000..cb8ec89
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/quick_settings.py
@@ -0,0 +1,68 @@
+from ignis.widgets import Widget
+from .wifi import wifi_control
+from .record import RecordButton
+from .dnd import DNDButton
+from .dark_mode import DarkModeButton
+from .ethernet import ethernet_control
+from .vpn import vpn_control
+from .bluetooth import bluetooth_control
+from ...qs_button import QSButton
+from ignis.services.network import NetworkService
+
+network = NetworkService.get_default()
+
+
+class QuickSettings(Widget.Box):
+ def __init__(self):
+ super().__init__(vertical=True, css_classes=["qs-main-box"])
+ network.wifi.connect("notify::devices", lambda x, y: self.__refresh())
+ network.ethernet.connect("notify::devices", lambda x, y: self.__refresh())
+ network.vpn.connect("notify::connections", lambda x, y: self.__refresh())
+
+ self.__refresh()
+
+ def __refresh(self) -> None:
+ self.child = []
+ self.__configure()
+
+ def __configure(self) -> None:
+ self.__qs_fabric(
+ *wifi_control(),
+ *ethernet_control(),
+ *vpn_control(),
+ *bluetooth_control(),
+ DNDButton(),
+ DarkModeButton(),
+ RecordButton(),
+ )
+
+ def __qs_fabric(self, *buttons: QSButton) -> None:
+ for i in range(0, len(buttons), 2):
+ self.__add_row(buttons, i)
+
+ def __add_row(self, buttons: tuple[QSButton, ...], i: int) -> None:
+ row = Widget.Box(homogeneous=True)
+ if len(self.child) > 0:
+ row.style = "margin-top: 0.5rem;"
+
+ self.append(row)
+
+ button1 = buttons[i]
+
+ self.__add_button(row, button1, buttons, i)
+
+ if i + 1 < len(buttons):
+ button2 = buttons[i + 1]
+ button2.style = "margin-left: 0.5rem;"
+ self.__add_button(row, button2, buttons, i)
+
+ def __add_button(
+ self, row: Widget.Box, button: QSButton, buttons: tuple[QSButton, ...], i: int
+ ) -> None:
+ row.append(button)
+
+ if button.menu:
+ self.append(button.menu)
+
+ if i == len(buttons) - 1 or i == len(buttons) - 2:
+ button.menu.box.add_css_class("control-center-menu-last-row")
diff --git a/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/record.py b/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/record.py
new file mode 100644
index 0000000..2f6772d
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/record.py
@@ -0,0 +1,102 @@
+from ignis.widgets import Widget
+from ...qs_button import QSButton
+from ...menu import Menu
+from ignis.services.recorder import RecorderService
+
+recorder = RecorderService.get_default()
+
+
+class RecordMenu(Menu):
+ def __init__(self):
+ self._audio_switch = Widget.Switch(halign="end", hexpand=True, valign="center")
+ self._dropdown = Widget.DropDown(
+ items=["Internal audio", "Microphone", "Both sources"],
+ css_classes=["record-dropdown"],
+ )
+
+ super().__init__(
+ name="recording",
+ child=[
+ Widget.Icon(
+ image="media-record-symbolic",
+ pixel_size=36,
+ halign="center",
+ css_classes=["record-icon"],
+ ),
+ Widget.Label(
+ label="Start recording?",
+ halign="center",
+ style="font-size: 1.2rem;",
+ ),
+ Widget.Box(
+ style="margin-top: 0.5rem;",
+ child=[
+ Widget.Icon(
+ image="microphone-sensitivity-medium-symbolic",
+ pixel_size=20,
+ style="margin-right: 0.5rem;",
+ ),
+ Widget.Box(
+ vertical=True,
+ child=[
+ Widget.Label(
+ label="Record audio",
+ style="font-size: 1.1rem;",
+ halign="start",
+ ),
+ self._dropdown,
+ ],
+ ),
+ self._audio_switch,
+ ],
+ ),
+ Widget.Box(
+ style="margin-top: 1rem;",
+ child=[
+ Widget.Button(
+ child=Widget.Label(label="Cancel"),
+ css_classes=["record-cancel-button", "unset"],
+ on_click=lambda x: self.set_reveal_child(False), # type: ignore
+ ),
+ Widget.Button(
+ child=Widget.Label(label="Start recording"),
+ halign="end",
+ hexpand=True,
+ css_classes=["record-start-button", "unset"],
+ on_click=lambda x: self.__start_recording(), # type: ignore
+ ),
+ ],
+ ),
+ ],
+ )
+
+ def __start_recording(self) -> None:
+ self.set_reveal_child(False)
+ microphone = False
+ internal = False
+ if self._audio_switch.active:
+ if self._dropdown.selected == "Internal audio":
+ internal = True
+ elif self._dropdown.selected == "Microphone":
+ microphone = True
+ else:
+ internal = True
+ microphone = True
+
+ recorder.start_recording(
+ record_microphone=microphone, record_internal_audio=internal
+ )
+
+
+class RecordButton(QSButton):
+ def __init__(self):
+ record_menu = RecordMenu()
+
+ super().__init__(
+ label="Recording",
+ icon_name="media-record-symbolic",
+ on_activate=lambda x: record_menu.toggle(),
+ on_deactivate=lambda x: recorder.stop_recording(),
+ active=recorder.bind("active"),
+ menu=record_menu,
+ )
diff --git a/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/vpn.py b/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/vpn.py
new file mode 100644
index 0000000..b45cf73
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/vpn.py
@@ -0,0 +1,112 @@
+import asyncio
+from ignis.widgets import Widget
+from ignis.utils import Utils
+from ...qs_button import QSButton
+from ...menu import Menu
+from ignis.services.network import NetworkService, VpnConnection
+
+
+network = NetworkService.get_default()
+
+
+class VpnNetworkItem(Widget.Button):
+ def __init__(self, conn: VpnConnection):
+ super().__init__(
+ css_classes=["network-item", "unset"],
+ on_click=lambda x: asyncio.create_task(conn.toggle_connection()),
+ child=Widget.Box(
+ child=[
+ Widget.Label(
+ label=conn.name,
+ ellipsize="end",
+ max_width_chars=20,
+ halign="start",
+ ),
+ Widget.Button(
+ child=Widget.Label(
+ label=conn.bind(
+ "is_connected",
+ lambda value: "Disconnect" if value else "Connect",
+ )
+ ),
+ css_classes=["connect-label", "unset"],
+ halign="end",
+ hexpand=True,
+ ),
+ ]
+ ),
+ )
+
+
+class VpnMenu(Menu):
+ def __init__(self):
+ super().__init__(
+ name="vpn",
+ child=[
+ Widget.Box(
+ css_classes=["network-header-box"],
+ child=[
+ Widget.Icon(icon_name="network-vpn-symbolic", pixel_size=28),
+ Widget.Label(
+ label="VPN connections",
+ css_classes=["network-header-label"],
+ ),
+ ],
+ ),
+ Widget.Box(
+ vertical=True,
+ child=network.vpn.bind(
+ "connections",
+ transform=lambda value: [VpnNetworkItem(i) for i in value],
+ ),
+ ),
+ Widget.Separator(),
+ Widget.Button(
+ css_classes=["network-item", "unset"],
+ on_click=lambda x: asyncio.create_task(Utils.exec_sh_async("nm-connection-editor")),
+ style="margin-bottom: 0;",
+ child=Widget.Box(
+ child=[
+ Widget.Icon(image="preferences-system-symbolic"),
+ Widget.Label(
+ label="Network Manager",
+ halign="start",
+ ),
+ ]
+ ),
+ ),
+ ],
+ )
+
+
+class VpnButton(QSButton):
+ def __init__(self):
+ menu = VpnMenu()
+
+ def get_label(id: str) -> str:
+ if id:
+ return id
+ else:
+ return "VPN"
+
+ def get_icon(icon_name: str) -> str:
+ if network.vpn.is_connected:
+ return icon_name
+ else:
+ return "network-vpn-symbolic"
+
+ super().__init__(
+ label=network.vpn.bind("active_vpn_id", get_label),
+ icon_name=network.vpn.bind("icon-name", get_icon),
+ on_activate=lambda x: menu.toggle(),
+ on_deactivate=lambda x: menu.toggle(),
+ active=network.vpn.bind("is-connected"),
+ menu=menu,
+ )
+
+
+def vpn_control() -> list[QSButton]:
+ if len(network.vpn.connections) > 0:
+ return [VpnButton()]
+ else:
+ return []
diff --git a/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/wifi.py b/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/wifi.py
new file mode 100644
index 0000000..75be064
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/control_center/widgets/quick_settings/wifi.py
@@ -0,0 +1,107 @@
+import asyncio
+from ignis.widgets import Widget
+from ignis.utils import Utils
+from ...qs_button import QSButton
+from ...menu import Menu
+from ....shared_widgets import ToggleBox
+from ignis.services.network import NetworkService, WifiAccessPoint, WifiDevice
+
+network = NetworkService.get_default()
+
+
+class WifiNetworkItem(Widget.Button):
+ def __init__(self, access_point: WifiAccessPoint):
+ super().__init__(
+ css_classes=["network-item", "unset"],
+ on_click=lambda x: asyncio.create_task(access_point.connect_to_graphical()),
+ child=Widget.Box(
+ child=[
+ Widget.Icon(
+ image=access_point.bind(
+ "strength", transform=lambda value: access_point.icon_name
+ ),
+ ),
+ Widget.Label(
+ label=access_point.ssid,
+ halign="start",
+ ),
+ Widget.Icon(
+ image="object-select-symbolic",
+ halign="end",
+ hexpand=True,
+ visible=access_point.bind("is_connected"),
+ ),
+ ]
+ ),
+ )
+
+
+class WifiMenu(Menu):
+ def __init__(self, device: WifiDevice):
+ super().__init__(
+ name="wifi",
+ child=[
+ ToggleBox(
+ label="Wi-Fi",
+ active=network.wifi.enabled,
+ on_change=lambda x, state: network.wifi.set_enabled(state),
+ css_classes=["network-header-box"],
+ ),
+ Widget.Box(
+ vertical=True,
+ child=device.bind(
+ "access_points",
+ transform=lambda value: [WifiNetworkItem(i) for i in value],
+ ),
+ ),
+ Widget.Separator(),
+ Widget.Button(
+ css_classes=["network-item", "unset"],
+ on_click=lambda x: asyncio.create_task(Utils.exec_sh_async("nm-connection-editor")),
+ style="margin-bottom: 0;",
+ child=Widget.Box(
+ child=[
+ Widget.Icon(image="preferences-system-symbolic"),
+ Widget.Label(
+ label="Network Settings",
+ halign="start",
+ ),
+ ]
+ ),
+ ),
+ ],
+ )
+
+
+class WifiButton(QSButton):
+ def __init__(self, device: WifiDevice):
+ menu = WifiMenu(device)
+
+ def get_label(ssid: str) -> str:
+ if ssid:
+ return ssid
+ else:
+ return "Wi-Fi"
+
+ def get_icon(icon_name: str) -> str:
+ if device.ap.is_connected:
+ return icon_name
+ else:
+ return "network-wireless-symbolic"
+
+ def toggle_list(x) -> None:
+ asyncio.create_task(device.scan())
+ menu.toggle()
+
+ super().__init__(
+ label=device.ap.bind("ssid", get_label),
+ icon_name=device.ap.bind("icon-name", get_icon),
+ on_activate=toggle_list,
+ on_deactivate=toggle_list,
+ active=network.wifi.bind("enabled"),
+ menu=menu,
+ )
+
+
+def wifi_control() -> list[QSButton]:
+ return [WifiButton(dev) for dev in network.wifi.devices]
diff --git a/ignis/backup_30.03.2025/modules/control_center/widgets/user.py b/ignis/backup_30.03.2025/modules/control_center/widgets/user.py
new file mode 100644
index 0000000..03d09b0
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/control_center/widgets/user.py
@@ -0,0 +1,71 @@
+import os
+from ignis.widgets import Widget
+from ignis.utils import Utils
+from ignis.app import IgnisApp
+from ignis.services.fetch import FetchService
+from user_options import user_options
+
+fetch = FetchService.get_default()
+app = IgnisApp.get_default()
+
+
+def format_uptime(value: tuple[int, int, int, int]) -> str:
+ days, hours, minutes, seconds = value
+ if days:
+ return f"up {days:02}:{hours:02}:{minutes:02}"
+ else:
+ return f"up {hours:02}:{minutes:02}"
+
+
+class User(Widget.Box):
+ def __init__(self):
+ user_image = Widget.Picture(
+ image=user_options.user.bind(
+ "avatar",
+ lambda value: "user-info" if not os.path.exists(value) else value,
+ ),
+ width=44,
+ height=44,
+ content_fit="cover",
+ style="border-radius: 10rem;",
+ )
+
+ username = Widget.Box(
+ child=[
+ Widget.Label(
+ label=os.getenv("USER"), css_classes=["user-name"], halign="start"
+ ),
+ Widget.Label(
+ label=Utils.Poll(
+ timeout=60 * 1000, callback=lambda x: fetch.uptime
+ ).bind("output", lambda value: format_uptime(value)),
+ halign="start",
+ css_classes=["user-name-secondary"],
+ ),
+ ],
+ vertical=True,
+ css_classes=["user-name-box"],
+ )
+
+ settings_button = Widget.Button(
+ child=Widget.Icon(image="emblem-system-symbolic", pixel_size=20),
+ halign="end",
+ hexpand=True,
+ css_classes=["user-settings", "unset"],
+ on_click=lambda x: self.__on_settings_button_click(),
+ )
+
+ power_button = Widget.Button(
+ child=Widget.Icon(image="system-shutdown-symbolic", pixel_size=20),
+ halign="end",
+ css_classes=["user-power", "unset"],
+ on_click=lambda x: app.toggle_window("ignis_POWERMENU"),
+ )
+ super().__init__(
+ child=[user_image, username, settings_button, power_button],
+ css_classes=["user"],
+ )
+
+ def __on_settings_button_click(self) -> None:
+ window = app.get_window("ignis_SETTINGS")
+ window.visible = not window.visible # type: ignore
diff --git a/ignis/backup_30.03.2025/modules/control_center/widgets/volume.py b/ignis/backup_30.03.2025/modules/control_center/widgets/volume.py
new file mode 100644
index 0000000..fa75a07
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/control_center/widgets/volume.py
@@ -0,0 +1,128 @@
+import asyncio
+from ignis.widgets import Widget
+from ignis.utils import Utils
+from ignis.services.audio import AudioService, Stream
+from typing import Literal
+from ..menu import Menu
+from ...shared_widgets import MaterialVolumeSlider
+
+audio = AudioService.get_default()
+
+AUDIO_TYPES = {
+ "speaker": {"menu_icon": "audio-headphones-symbolic", "menu_label": "Sound Output"},
+ "microphone": {
+ "menu_icon": "microphone-sensitivity-high-symbolic",
+ "menu_label": "Sound Input",
+ },
+}
+
+
+class DeviceItem(Widget.Button):
+ def __init__(self, stream: Stream, _type: Literal["speaker", "microphone"]):
+ super().__init__(
+ child=Widget.Box(
+ child=[
+ Widget.Icon(image="audio-card-symbolic"),
+ Widget.Label(
+ label=stream.description,
+ ellipsize="end",
+ max_width_chars=30,
+ halign="start",
+ css_classes=["volume-entry-label"],
+ ),
+ Widget.Icon(
+ image="object-select-symbolic",
+ halign="end",
+ hexpand=True,
+ visible=stream.bind("is_default"),
+ ),
+ ]
+ ),
+ css_classes=["volume-entry", "unset"],
+ hexpand=True,
+ setup=lambda self: stream.connect("removed", lambda x: self.unparent()),
+ on_click=lambda x: setattr(audio, _type, stream),
+ )
+
+
+class DeviceMenu(Menu):
+ def __init__(self, _type: Literal["speaker", "microphone"], style: str = ""):
+ data = AUDIO_TYPES[_type]
+
+ super().__init__(
+ name=f"volume-{_type}",
+ child=[
+ Widget.Box(
+ child=[
+ Widget.Icon(image=data["menu_icon"], pixel_size=24),
+ Widget.Label(
+ label=data["menu_label"],
+ halign="start",
+ css_classes=["volume-entry-list-header-label"],
+ ),
+ ],
+ css_classes=["volume-entry-list-header-box"],
+ ),
+ Widget.Box(
+ vertical=True,
+ setup=lambda self: audio.connect(
+ f"{_type}-added",
+ lambda x, stream: self.append(DeviceItem(stream, _type)),
+ ),
+ ),
+ Widget.Separator(css_classes=["volume-entry-list-separator"]),
+ Widget.Button(
+ child=Widget.Box(
+ child=[
+ Widget.Icon(image="preferences-system-symbolic"),
+ Widget.Label(
+ label="Sound Settings",
+ halign="start",
+ css_classes=["volume-entry-label"],
+ ),
+ ]
+ ),
+ css_classes=["volume-entry", "unset"],
+ style="margin-bottom: 0;",
+ on_click=lambda x: asyncio.create_task(Utils.exec_sh_async("pavucontrol")),
+ ),
+ ],
+ )
+
+ self.box.add_css_class(f"volume-menubox-{_type}")
+
+
+class VolumeSlider(Widget.Box):
+ def __init__(self, _type: Literal["speaker", "microphone"]):
+ stream = getattr(audio, _type)
+
+ icon = Widget.Button(
+ child=Widget.Icon(
+ image=stream.bind("icon_name"),
+ pixel_size=18,
+ ),
+ css_classes=["material-slider-icon", "unset", "hover-surface"],
+ on_click=lambda x: stream.set_is_muted(not stream.is_muted),
+ )
+
+ device_menu = DeviceMenu(_type=_type)
+
+ scale = MaterialVolumeSlider(
+ stream=stream,
+ on_change=lambda x: stream.set_volume(x.value),
+ sensitive=stream.bind("is_muted", lambda value: not value),
+ )
+
+ arrow = Widget.Button(
+ child=Widget.Arrow(pixel_size=20, rotated=device_menu.bind("reveal_child")),
+ css_classes=["material-slider-arrow", "hover-surface"],
+ on_click=lambda x: device_menu.toggle(),
+ )
+ super().__init__(
+ vertical=True,
+ child=[
+ Widget.Box(child=[icon, scale, arrow]),
+ device_menu,
+ ],
+ css_classes=[f"volume-mainbox-{_type}"],
+ )
diff --git a/ignis/backup_30.03.2025/modules/launcher/__init__.py b/ignis/backup_30.03.2025/modules/launcher/__init__.py
new file mode 100644
index 0000000..97c6326
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/launcher/__init__.py
@@ -0,0 +1,3 @@
+from .launcher import Launcher
+
+__all__ = ["Launcher"]
diff --git a/ignis/backup_30.03.2025/modules/launcher/__pycache__/__init__.cpython-313.pyc b/ignis/backup_30.03.2025/modules/launcher/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..fb19724
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/launcher/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/launcher/__pycache__/launcher.cpython-313.pyc b/ignis/backup_30.03.2025/modules/launcher/__pycache__/launcher.cpython-313.pyc
new file mode 100644
index 0000000..b049dff
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/launcher/__pycache__/launcher.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/launcher/launcher.py b/ignis/backup_30.03.2025/modules/launcher/launcher.py
new file mode 100644
index 0000000..c8824a7
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/launcher/launcher.py
@@ -0,0 +1,217 @@
+import re
+import asyncio
+from ignis.widgets import Widget
+from ignis.app import IgnisApp
+from ignis.services.applications import (
+ ApplicationsService,
+ Application,
+ ApplicationAction,
+)
+from ignis.utils import Utils
+from gi.repository import Gio # type: ignore
+
+app = IgnisApp.get_default()
+
+applications = ApplicationsService.get_default()
+
+TERMINAL_FORMAT = "kitty %command%"
+
+
+def is_url(url: str) -> bool:
+ regex = re.compile(
+ r"^(?:http|ftp)s?://" # http:// or https://
+ r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|" # domain
+ r"localhost|" # localhost
+ r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|" # or ipv4
+ r"\[?[A-F0-9]*:[A-F0-9:]+\]?)" # or ipv6
+ r"(?::\d+)?" # optional port
+ r"(?:/?|[/?]\S+)$",
+ re.IGNORECASE,
+ )
+
+ return re.match(regex, url) is not None
+
+
+class LauncherAppItem(Widget.Button):
+ def __init__(self, application: Application) -> None:
+ self._application = application
+ super().__init__(
+ on_click=lambda x: self.launch(),
+ on_right_click=lambda x: self._menu.popup(),
+ css_classes=["launcher-app"],
+ child=Widget.Box(
+ child=[
+ Widget.Icon(image=application.icon, pixel_size=48),
+ Widget.Label(
+ label=application.name,
+ ellipsize="end",
+ max_width_chars=30,
+ css_classes=["launcher-app-label"],
+ ),
+ ]
+ ),
+ )
+ self.__sync_menu()
+ application.connect("notify::is-pinned", lambda x, y: self.__sync_menu())
+
+ def launch(self) -> None:
+ self._application.launch(terminal_format=TERMINAL_FORMAT)
+ app.close_window("ignis_LAUNCHER")
+
+ def launch_action(self, action: ApplicationAction) -> None:
+ action.launch()
+ app.close_window("ignis_LAUNCHER")
+
+ def __sync_menu(self) -> None:
+ self._menu = Widget.PopoverMenu(
+ items=[
+ Widget.MenuItem(label="Launch", on_activate=lambda x: self.launch()),
+ Widget.Separator(),
+ ]
+ + [
+ Widget.MenuItem(
+ label=i.name,
+ on_activate=lambda x, action=i: self.launch_action(action),
+ )
+ for i in self._application.actions
+ ]
+ + [
+ Widget.Separator(),
+ Widget.MenuItem(
+ label="Pin", on_activate=lambda x: self._application.pin()
+ )
+ if not self._application.is_pinned
+ else Widget.MenuItem(
+ label="Unpin", on_activate=lambda x: self._application.unpin()
+ ),
+ ]
+ )
+ self.child.append(self._menu)
+
+
+class SearchWebButton(Widget.Button):
+ def __init__(self, query: str):
+ self._query = query
+ self._url = ""
+
+ browser_desktop_file = Utils.exec_sh(
+ "xdg-settings get default-web-browser"
+ ).stdout.replace("\n", "")
+
+ app_info = Gio.DesktopAppInfo.new(desktop_id=browser_desktop_file)
+
+ icon_name = "applications-internet-symbolic"
+ if app_info:
+ icon_string = app_info.get_string("Icon")
+ if icon_string:
+ icon_name = icon_string
+
+ if not query.startswith(("http://", "https://")) and "." in query:
+ query = "https://" + query
+
+ if is_url(query):
+ label = f"Visit {query}"
+ self._url = query
+ else:
+ label = "Search in Google"
+ self._url = f"https://www.google.com/search?q={query.replace(' ', '+')}"
+
+ super().__init__(
+ on_click=lambda x: self.launch(),
+ css_classes=["launcher-app"],
+ child=Widget.Box(
+ child=[
+ Widget.Icon(image=icon_name, pixel_size=48),
+ Widget.Label(
+ label=label,
+ css_classes=["launcher-app-label"],
+ ),
+ ]
+ ),
+ )
+
+ def launch(self) -> None:
+ asyncio.create_task(Utils.exec_sh_async(f"xdg-open {self._url}"))
+ app.close_window("ignis_LAUNCHER")
+
+
+class Launcher(Widget.Window):
+ def __init__(self):
+ self._app_list = Widget.Box(
+ vertical=True, visible=False, style="margin-top: 1rem;"
+ )
+ self._entry = Widget.Entry(
+ hexpand=True,
+ placeholder_text="Search",
+ css_classes=["launcher-search"],
+ on_change=self.__search,
+ on_accept=self.__on_accept,
+ )
+
+ main_box = Widget.Box(
+ vertical=True,
+ valign="start",
+ halign="center",
+ css_classes=["launcher"],
+ child=[
+ Widget.Box(
+ css_classes=["launcher-search-box"],
+ child=[
+ Widget.Icon(
+ icon_name="system-search-symbolic",
+ pixel_size=24,
+ style="margin-right: 0.5rem;",
+ ),
+ self._entry,
+ ],
+ ),
+ self._app_list,
+ ],
+ )
+
+ super().__init__(
+ namespace="ignis_LAUNCHER",
+ visible=False,
+ popup=True,
+ kb_mode="on_demand",
+ css_classes=["unset"],
+ setup=lambda self: self.connect("notify::visible", self.__on_open),
+ anchor=["top", "right", "bottom", "left"],
+ child=Widget.Overlay(
+ child=Widget.Button(
+ vexpand=True,
+ hexpand=True,
+ can_focus=False,
+ css_classes=["unset"],
+ on_click=lambda x: app.close_window("ignis_LAUNCHER"),
+ style="background-color: rgba(0, 0, 0, 0.3);",
+ ),
+ overlays=[main_box],
+ ),
+ )
+
+ def __on_open(self, *args) -> None:
+ if not self.visible:
+ return
+
+ self._entry.text = ""
+ self._entry.grab_focus()
+
+ def __on_accept(self, *args) -> None:
+ if len(self._app_list.child) > 0:
+ self._app_list.child[0].launch()
+
+ def __search(self, *args) -> None:
+ query = self._entry.text
+
+ if query == "":
+ self._entry.grab_focus()
+ self._app_list.visible = False
+ return
+
+ apps = applications.search(applications.apps, query)
+ if apps == []:
+ self._app_list.child = [SearchWebButton(query)]
+ else:
+ self._app_list.visible = True
+ self._app_list.child = [LauncherAppItem(i) for i in apps[:5]]
diff --git a/ignis/backup_30.03.2025/modules/notification_popup/__init__.py b/ignis/backup_30.03.2025/modules/notification_popup/__init__.py
new file mode 100644
index 0000000..94c4309
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/notification_popup/__init__.py
@@ -0,0 +1,3 @@
+from .notification_popup import NotificationPopup
+
+__all__ = ["NotificationPopup"]
diff --git a/ignis/backup_30.03.2025/modules/notification_popup/__pycache__/__init__.cpython-313.pyc b/ignis/backup_30.03.2025/modules/notification_popup/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..14ec6e6
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/notification_popup/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/notification_popup/__pycache__/notification_popup.cpython-313.pyc b/ignis/backup_30.03.2025/modules/notification_popup/__pycache__/notification_popup.cpython-313.pyc
new file mode 100644
index 0000000..7672d7f
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/notification_popup/__pycache__/notification_popup.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/notification_popup/notification_popup.py b/ignis/backup_30.03.2025/modules/notification_popup/notification_popup.py
new file mode 100644
index 0000000..4bd1a28
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/notification_popup/notification_popup.py
@@ -0,0 +1,79 @@
+from ignis.widgets import Widget
+from ignis.app import IgnisApp
+from ignis.utils import Utils
+from ignis.services.notifications import Notification, NotificationService
+from ..shared_widgets import NotificationWidget
+
+
+app = IgnisApp.get_default()
+
+notifications = NotificationService.get_default()
+
+
+class Popup(Widget.Box):
+ def __init__(
+ self, box: "PopupBox", window: "NotificationPopup", notification: Notification
+ ):
+ self._box = box
+ self._window = window
+
+ widget = NotificationWidget(notification)
+ widget.css_classes = ["notification-popup"]
+ self._inner = Widget.Revealer(transition_type="slide_left", child=widget)
+ self._outer = Widget.Revealer(transition_type="slide_down", child=self._inner)
+ super().__init__(child=[self._outer], halign="end")
+
+ notification.connect("dismissed", lambda x: self.destroy())
+
+ def destroy(self):
+ def box_destroy():
+ self.unparent()
+ if len(notifications.popups) == 0:
+ self._window.visible = False
+
+ def outer_close():
+ self._outer.reveal_child = False
+ Utils.Timeout(self._outer.transition_duration, box_destroy)
+
+ self._inner.transition_type = "crossfade"
+ self._inner.reveal_child = False
+ Utils.Timeout(self._outer.transition_duration, outer_close)
+
+
+class PopupBox(Widget.Box):
+ def __init__(self, window: "NotificationPopup", monitor: int):
+ self._window = window
+ self._monitor = monitor
+
+ super().__init__(
+ vertical=True,
+ valign="start",
+ setup=lambda self: notifications.connect(
+ "new_popup",
+ lambda x, notification: self.__on_notified(notification),
+ ),
+ )
+
+ def __on_notified(self, notification: Notification) -> None:
+ self._window.visible = True
+ popup = Popup(box=self, window=self._window, notification=notification)
+ self.prepend(popup)
+ popup._outer.reveal_child = True
+ Utils.Timeout(
+ popup._outer.transition_duration, popup._inner.set_reveal_child, True
+ )
+
+
+class NotificationPopup(Widget.Window):
+ def __init__(self, monitor: int):
+ super().__init__(
+ anchor=["right", "top", "bottom"],
+ monitor=monitor,
+ namespace=f"ignis_NOTIFICATION_POPUP_{monitor}",
+ layer="top",
+ child=PopupBox(window=self, monitor=monitor),
+ visible=False,
+ dynamic_input_region=True,
+ css_classes=["rec-unset"],
+ style="min-width: 29rem;",
+ )
diff --git a/ignis/backup_30.03.2025/modules/osd/__init__.py b/ignis/backup_30.03.2025/modules/osd/__init__.py
new file mode 100644
index 0000000..7568b7d
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/osd/__init__.py
@@ -0,0 +1,3 @@
+from .osd import OSD
+
+__all__ = ["OSD"]
diff --git a/ignis/backup_30.03.2025/modules/osd/__pycache__/__init__.cpython-313.pyc b/ignis/backup_30.03.2025/modules/osd/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..4f39525
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/osd/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/osd/__pycache__/osd.cpython-313.pyc b/ignis/backup_30.03.2025/modules/osd/__pycache__/osd.cpython-313.pyc
new file mode 100644
index 0000000..56bb4e8
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/osd/__pycache__/osd.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/osd/osd.py b/ignis/backup_30.03.2025/modules/osd/osd.py
new file mode 100644
index 0000000..ced81ac
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/osd/osd.py
@@ -0,0 +1,38 @@
+from ignis.widgets import Widget
+from ignis.utils import Utils
+from ignis.services.audio import AudioService
+from ..shared_widgets import MaterialVolumeSlider
+
+audio = AudioService.get_default()
+
+
+class OSD(Widget.Window):
+ def __init__(self):
+ super().__init__(
+ layer="overlay",
+ anchor=["bottom"],
+ namespace="ignis_OSD",
+ visible=False,
+ css_classes=["rec-unset"],
+ child=Widget.Box(
+ css_classes=["osd"],
+ child=[
+ Widget.Icon(
+ pixel_size=26,
+ style="margin-right: 0.5rem;",
+ image=audio.speaker.bind("icon_name"),
+ ),
+ MaterialVolumeSlider(stream=audio.speaker, sensitive=False),
+ ],
+ ),
+ )
+
+ def set_property(self, property_name, value):
+ if property_name == "visible":
+ self.__update_visible()
+
+ super().set_property(property_name, value)
+
+ @Utils.debounce(3000)
+ def __update_visible(self) -> None:
+ super().set_property("visible", False)
diff --git a/ignis/backup_30.03.2025/modules/powermenu/__init__.py b/ignis/backup_30.03.2025/modules/powermenu/__init__.py
new file mode 100644
index 0000000..73fd886
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/powermenu/__init__.py
@@ -0,0 +1,3 @@
+from .powermenu import Powermenu
+
+__all__ = ["Powermenu"]
diff --git a/ignis/backup_30.03.2025/modules/powermenu/__pycache__/__init__.cpython-313.pyc b/ignis/backup_30.03.2025/modules/powermenu/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..0e43930
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/powermenu/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/powermenu/__pycache__/powermenu.cpython-313.pyc b/ignis/backup_30.03.2025/modules/powermenu/__pycache__/powermenu.cpython-313.pyc
new file mode 100644
index 0000000..10e5549
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/powermenu/__pycache__/powermenu.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/powermenu/powermenu.py b/ignis/backup_30.03.2025/modules/powermenu/powermenu.py
new file mode 100644
index 0000000..76dde62
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/powermenu/powermenu.py
@@ -0,0 +1,106 @@
+import asyncio
+from ignis.widgets import Widget
+from ignis.utils import Utils
+from ignis.app import IgnisApp
+from typing import Callable
+
+app = IgnisApp.get_default()
+
+def create_exec_task(cmd: str) -> None:
+ asyncio.create_task(Utils.exec_sh_async(cmd))
+
+class PowermenuButton(Widget.Box):
+ def __init__(self, label: str, icon_name: str, on_click: Callable) -> None:
+ super().__init__(
+ child=[
+ Widget.Button(
+ child=Widget.Icon(image=icon_name, pixel_size=36),
+ on_click=on_click,
+ css_classes=["powermenu-button", "unset"],
+ ),
+ Widget.Label(label=label, css_classes=["powermenu-button-label"]),
+ ],
+ vertical=True,
+ css_classes=["powermenu-button-box"],
+ )
+
+
+class PowerOffButton(PowermenuButton):
+ def __init__(self):
+ super().__init__(
+ label="Power off",
+ icon_name="system-shutdown-symbolic",
+ on_click=lambda *args: create_exec_task("poweroff"),
+ )
+
+
+class RebootButton(PowermenuButton):
+ def __init__(self):
+ super().__init__(
+ label="Reboot",
+ icon_name="system-reboot-symbolic",
+ on_click=lambda *args: create_exec_task("reboot"),
+ )
+
+
+class SuspendButton(PowermenuButton):
+ def __init__(self):
+ super().__init__(
+ label="Suspend", icon_name="night-light-symbolic", on_click=self.__invoke
+ )
+
+ def __invoke(self, *args) -> None:
+ app.close_window("ignis_POWERMENU")
+ create_exec_task("systemctl suspend && hyprlock")
+
+
+class HyprlandExitButton(PowermenuButton):
+ def __init__(self):
+ super().__init__(
+ label="Sign out",
+ icon_name="system-log-out-symbolic",
+ on_click=lambda *args: create_exec_task("hyprctl dispatch exit 0"),
+ )
+
+
+class Powermenu(Widget.Window):
+ def __init__(self):
+ main_box = Widget.Box(
+ vertical=True,
+ valign="center",
+ halign="center",
+ css_classes=["powermenu"],
+ child=[
+ Widget.Box(
+ child=[
+ PowerOffButton(),
+ RebootButton(),
+ ]
+ ),
+ Widget.Box(
+ child=[
+ SuspendButton(),
+ HyprlandExitButton(),
+ ]
+ ),
+ ],
+ )
+ super().__init__(
+ popup=True,
+ kb_mode="on_demand",
+ namespace="ignis_POWERMENU",
+ exclusivity="ignore",
+ anchor=["left", "right", "top", "bottom"],
+ visible=False,
+ child=Widget.Overlay(
+ child=Widget.Button(
+ vexpand=True,
+ hexpand=True,
+ can_focus=False,
+ css_classes=["unset", "powermenu-overlay"],
+ on_click=lambda x: app.close_window("ignis_POWERMENU"),
+ ),
+ overlays=[main_box],
+ ),
+ css_classes=["unset"],
+ )
diff --git a/ignis/backup_30.03.2025/modules/settings/__init__.py b/ignis/backup_30.03.2025/modules/settings/__init__.py
new file mode 100644
index 0000000..76bd427
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/settings/__init__.py
@@ -0,0 +1,5 @@
+from .settings import Settings
+
+__all__ = [
+ "Settings",
+]
diff --git a/ignis/backup_30.03.2025/modules/settings/__pycache__/__init__.cpython-313.pyc b/ignis/backup_30.03.2025/modules/settings/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..1e68cf7
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/settings/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/settings/__pycache__/active_page.cpython-313.pyc b/ignis/backup_30.03.2025/modules/settings/__pycache__/active_page.cpython-313.pyc
new file mode 100644
index 0000000..dc327ab
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/settings/__pycache__/active_page.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/settings/__pycache__/settings.cpython-313.pyc b/ignis/backup_30.03.2025/modules/settings/__pycache__/settings.cpython-313.pyc
new file mode 100644
index 0000000..06efde3
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/settings/__pycache__/settings.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/settings/active_page.py b/ignis/backup_30.03.2025/modules/settings/active_page.py
new file mode 100644
index 0000000..d1af2e7
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/settings/active_page.py
@@ -0,0 +1,5 @@
+from ignis.variable import Variable
+from .elements import SettingsPage
+
+fallback_page = SettingsPage(name="Settings")
+active_page = Variable(value=fallback_page)
diff --git a/ignis/backup_30.03.2025/modules/settings/elements/__init__.py b/ignis/backup_30.03.2025/modules/settings/elements/__init__.py
new file mode 100644
index 0000000..d3795f0
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/settings/elements/__init__.py
@@ -0,0 +1,20 @@
+from .row import SettingsRow
+from .page import SettingsPage
+from .group import SettingsGroup
+from .switchrow import SwitchRow
+from .filerow import FileRow
+from .spinrow import SpinRow
+from .entryrow import EntryRow
+from .entry import SettingsEntry
+
+__all__ = [
+ "SettingsRow",
+ "SettingsPage",
+ "SettingsGroup",
+ "SettingsRow",
+ "SwitchRow",
+ "FileRow",
+ "SpinRow",
+ "EntryRow",
+ "SettingsEntry",
+]
diff --git a/ignis/backup_30.03.2025/modules/settings/elements/__pycache__/__init__.cpython-313.pyc b/ignis/backup_30.03.2025/modules/settings/elements/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..bc84294
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/settings/elements/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/settings/elements/__pycache__/entry.cpython-313.pyc b/ignis/backup_30.03.2025/modules/settings/elements/__pycache__/entry.cpython-313.pyc
new file mode 100644
index 0000000..0ac49d7
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/settings/elements/__pycache__/entry.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/settings/elements/__pycache__/entryrow.cpython-313.pyc b/ignis/backup_30.03.2025/modules/settings/elements/__pycache__/entryrow.cpython-313.pyc
new file mode 100644
index 0000000..76cbbd3
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/settings/elements/__pycache__/entryrow.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/settings/elements/__pycache__/filerow.cpython-313.pyc b/ignis/backup_30.03.2025/modules/settings/elements/__pycache__/filerow.cpython-313.pyc
new file mode 100644
index 0000000..f99aa67
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/settings/elements/__pycache__/filerow.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/settings/elements/__pycache__/group.cpython-313.pyc b/ignis/backup_30.03.2025/modules/settings/elements/__pycache__/group.cpython-313.pyc
new file mode 100644
index 0000000..37d1a80
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/settings/elements/__pycache__/group.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/settings/elements/__pycache__/page.cpython-313.pyc b/ignis/backup_30.03.2025/modules/settings/elements/__pycache__/page.cpython-313.pyc
new file mode 100644
index 0000000..5f51d14
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/settings/elements/__pycache__/page.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/settings/elements/__pycache__/row.cpython-313.pyc b/ignis/backup_30.03.2025/modules/settings/elements/__pycache__/row.cpython-313.pyc
new file mode 100644
index 0000000..c2c1ce6
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/settings/elements/__pycache__/row.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/settings/elements/__pycache__/spinrow.cpython-313.pyc b/ignis/backup_30.03.2025/modules/settings/elements/__pycache__/spinrow.cpython-313.pyc
new file mode 100644
index 0000000..8d3146b
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/settings/elements/__pycache__/spinrow.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/settings/elements/__pycache__/switchrow.cpython-313.pyc b/ignis/backup_30.03.2025/modules/settings/elements/__pycache__/switchrow.cpython-313.pyc
new file mode 100644
index 0000000..6aa01a4
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/settings/elements/__pycache__/switchrow.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/settings/elements/entry.py b/ignis/backup_30.03.2025/modules/settings/elements/entry.py
new file mode 100644
index 0000000..d6fdb4e
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/settings/elements/entry.py
@@ -0,0 +1,25 @@
+from ignis.widgets import Widget
+from .page import SettingsPage
+
+
+class SettingsEntry(Widget.ListBoxRow):
+ def __init__(
+ self,
+ icon: str,
+ label: str,
+ page: SettingsPage,
+ **kwargs,
+ ):
+ from ..active_page import active_page # avoid a circular import
+
+ super().__init__(
+ child=Widget.Box(
+ child=[
+ Widget.Icon(image=icon, pixel_size=20),
+ Widget.Label(label=label, style="margin-left: 0.75rem;"),
+ ],
+ ),
+ css_classes=["settings-sidebar-entry"],
+ on_activate=lambda x: active_page.set_value(page),
+ **kwargs,
+ )
diff --git a/ignis/backup_30.03.2025/modules/settings/elements/entryrow.py b/ignis/backup_30.03.2025/modules/settings/elements/entryrow.py
new file mode 100644
index 0000000..3349c21
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/settings/elements/entryrow.py
@@ -0,0 +1,24 @@
+from ignis.widgets import Widget
+from .row import SettingsRow
+from typing import Callable
+from ignis.gobject import Binding
+
+
+class EntryRow(SettingsRow):
+ def __init__(
+ self,
+ text: str | Binding | None = None,
+ on_change: Callable | None = None,
+ width: int | None = None,
+ **kwargs,
+ ):
+ super().__init__(**kwargs)
+ self._entry = Widget.Entry(
+ on_change=on_change,
+ text=text,
+ halign="end",
+ valign="center",
+ width_request=width,
+ hexpand=True,
+ )
+ self.child.append(self._entry)
diff --git a/ignis/backup_30.03.2025/modules/settings/elements/filerow.py b/ignis/backup_30.03.2025/modules/settings/elements/filerow.py
new file mode 100644
index 0000000..e948f5b
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/settings/elements/filerow.py
@@ -0,0 +1,23 @@
+from ignis.widgets import Widget
+from .row import SettingsRow
+from ignis.gobject import Binding
+
+
+class FileRow(SettingsRow):
+ def __init__(
+ self,
+ dialog: Widget.FileDialog,
+ button_label: str | Binding | None = None,
+ **kwargs,
+ ):
+ super().__init__(**kwargs)
+ self._button = Widget.FileChooserButton(
+ dialog=dialog,
+ label=Widget.Label(
+ label=button_label, ellipsize="start", max_width_chars=20
+ ),
+ hexpand=True,
+ halign="end",
+ )
+
+ self.child.append(self._button)
diff --git a/ignis/backup_30.03.2025/modules/settings/elements/group.py b/ignis/backup_30.03.2025/modules/settings/elements/group.py
new file mode 100644
index 0000000..8db3615
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/settings/elements/group.py
@@ -0,0 +1,23 @@
+from ignis.widgets import Widget
+from .row import SettingsRow
+from ignis.base_widget import BaseWidget
+
+
+class SettingsGroup(Widget.Box):
+ def __init__(
+ self, name: str | None, rows: list[SettingsRow | BaseWidget] = [], **kwargs
+ ):
+ super().__init__(
+ vertical=True,
+ css_classes=["settings-group"],
+ child=[
+ Widget.Label(
+ label=name,
+ css_classes=["settings-group-name"],
+ halign="start",
+ visible=True if name else False,
+ ),
+ Widget.ListBox(rows=[*rows]),
+ ],
+ **kwargs,
+ )
diff --git a/ignis/backup_30.03.2025/modules/settings/elements/page.py b/ignis/backup_30.03.2025/modules/settings/elements/page.py
new file mode 100644
index 0000000..8c6d7a6
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/settings/elements/page.py
@@ -0,0 +1,23 @@
+from ignis.widgets import Widget
+from .group import SettingsGroup
+from ignis.base_widget import BaseWidget
+
+
+class SettingsPage(Widget.Scroll):
+ def __init__(self, name: str, groups: list[SettingsGroup | BaseWidget] = []):
+ super().__init__(
+ hexpand=True,
+ vexpand=True,
+ child=Widget.Box(
+ vertical=True,
+ hexpand=True,
+ vexpand=True,
+ css_classes=["settings-page"],
+ child=[
+ Widget.Label(
+ label=name, css_classes=["settings-page-name"], halign="start"
+ ),
+ *groups,
+ ],
+ ),
+ )
diff --git a/ignis/backup_30.03.2025/modules/settings/elements/row.py b/ignis/backup_30.03.2025/modules/settings/elements/row.py
new file mode 100644
index 0000000..fd68be6
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/settings/elements/row.py
@@ -0,0 +1,39 @@
+from ignis.widgets import Widget
+
+
+class SettingsRow(Widget.ListBoxRow):
+ def __init__(
+ self,
+ label: str | None = None,
+ sublabel: str | None = None,
+ **kwargs,
+ ):
+ super().__init__(
+ css_classes=["settings-row"],
+ child=Widget.Box(
+ child=[
+ Widget.Box(
+ vertical=True,
+ child=[
+ Widget.Label(
+ label=label,
+ css_classes=["settings-row-label"],
+ halign="start",
+ vexpand=True,
+ wrap=True,
+ visible=True if label else False,
+ ),
+ Widget.Label(
+ label=sublabel,
+ css_classes=["settings-row-sublabel"],
+ halign="start",
+ vexpand=True,
+ wrap=True,
+ visible=True if sublabel else False,
+ ),
+ ],
+ )
+ ]
+ ),
+ **kwargs,
+ )
diff --git a/ignis/backup_30.03.2025/modules/settings/elements/spinrow.py b/ignis/backup_30.03.2025/modules/settings/elements/spinrow.py
new file mode 100644
index 0000000..3d3f09e
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/settings/elements/spinrow.py
@@ -0,0 +1,30 @@
+from ignis.widgets import Widget
+from .row import SettingsRow
+from typing import Callable
+from ignis.gobject import Binding
+
+
+class SpinRow(SettingsRow):
+ def __init__(
+ self,
+ value: int | Binding = 0,
+ on_change: Callable | None = None,
+ min: int = 0,
+ max: int = 100,
+ step: int = 1,
+ width: int = 0,
+ **kwargs,
+ ):
+ super().__init__(**kwargs)
+ self._spin_button = Widget.SpinButton(
+ value=value,
+ on_change=on_change,
+ min=min,
+ max=max,
+ halign="end",
+ valign="center",
+ width_request=width,
+ hexpand=True,
+ step=step,
+ )
+ self.child.append(self._spin_button)
diff --git a/ignis/backup_30.03.2025/modules/settings/elements/switchrow.py b/ignis/backup_30.03.2025/modules/settings/elements/switchrow.py
new file mode 100644
index 0000000..b204e9c
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/settings/elements/switchrow.py
@@ -0,0 +1,25 @@
+from ignis.widgets import Widget
+from .row import SettingsRow
+from typing import Callable
+from ignis.gobject import Binding
+
+
+class SwitchRow(SettingsRow):
+ def __init__(
+ self,
+ active: bool | Binding = False,
+ on_change: Callable | None = None,
+ **kwargs,
+ ):
+ super().__init__(**kwargs)
+ self._switch = Widget.Switch(
+ active=active,
+ on_change=on_change,
+ halign="end",
+ valign="center",
+ hexpand=True,
+ )
+ self.on_activate = lambda x: self._switch.emit(
+ "activate"
+ ) # if set "active" property animation will not work
+ self.child.append(self._switch)
diff --git a/ignis/backup_30.03.2025/modules/settings/pages/__init__.py b/ignis/backup_30.03.2025/modules/settings/pages/__init__.py
new file mode 100644
index 0000000..e1f9d71
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/settings/pages/__init__.py
@@ -0,0 +1,13 @@
+from .about import AboutEntry
+from .appearance import AppearanceEntry
+from .notifications import NotificationsEntry
+from .recorder import RecorderEntry
+from .user import UserEntry
+
+__all__ = [
+ "AboutEntry",
+ "AppearanceEntry",
+ "NotificationsEntry",
+ "RecorderEntry",
+ "UserEntry",
+]
diff --git a/ignis/backup_30.03.2025/modules/settings/pages/__pycache__/__init__.cpython-313.pyc b/ignis/backup_30.03.2025/modules/settings/pages/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..e95d511
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/settings/pages/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/settings/pages/__pycache__/about.cpython-313.pyc b/ignis/backup_30.03.2025/modules/settings/pages/__pycache__/about.cpython-313.pyc
new file mode 100644
index 0000000..17b47cd
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/settings/pages/__pycache__/about.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/settings/pages/__pycache__/appearance.cpython-313.pyc b/ignis/backup_30.03.2025/modules/settings/pages/__pycache__/appearance.cpython-313.pyc
new file mode 100644
index 0000000..99cbde3
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/settings/pages/__pycache__/appearance.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/settings/pages/__pycache__/notifications.cpython-313.pyc b/ignis/backup_30.03.2025/modules/settings/pages/__pycache__/notifications.cpython-313.pyc
new file mode 100644
index 0000000..9f20f5f
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/settings/pages/__pycache__/notifications.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/settings/pages/__pycache__/recorder.cpython-313.pyc b/ignis/backup_30.03.2025/modules/settings/pages/__pycache__/recorder.cpython-313.pyc
new file mode 100644
index 0000000..66f73dc
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/settings/pages/__pycache__/recorder.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/settings/pages/__pycache__/user.cpython-313.pyc b/ignis/backup_30.03.2025/modules/settings/pages/__pycache__/user.cpython-313.pyc
new file mode 100644
index 0000000..8c57a32
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/settings/pages/__pycache__/user.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/settings/pages/about.py b/ignis/backup_30.03.2025/modules/settings/pages/about.py
new file mode 100644
index 0000000..95e37a9
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/settings/pages/about.py
@@ -0,0 +1,52 @@
+from ..elements import SettingsPage, SettingsRow, SettingsEntry, SettingsGroup
+from ignis.utils import Utils
+from ignis.widgets import Widget
+from ignis.services.fetch import FetchService
+from user_options import user_options
+
+fetch = FetchService.get_default()
+
+
+class AboutEntry(SettingsEntry):
+ def __init__(self):
+ page = SettingsPage(
+ name="About",
+ groups=[
+ Widget.Box(
+ child=[
+ Widget.Picture(
+ image=user_options.material.bind(
+ "dark_mode",
+ transform=lambda value: fetch.os_logo_text_dark
+ if value
+ else fetch.os_logo_text,
+ ),
+ width=300,
+ height=100,
+ )
+ ],
+ halign="center",
+ width_request=300,
+ height_request=100,
+ ),
+ SettingsGroup(
+ name="Info",
+ rows=[
+ SettingsRow(label="OS", sublabel=fetch.os_name),
+ SettingsRow(
+ label="Ignis version", sublabel=Utils.get_ignis_version()
+ ),
+ SettingsRow(label="Session type", sublabel=fetch.session_type),
+ SettingsRow(
+ label="Wayland compositor", sublabel=fetch.current_desktop
+ ),
+ SettingsRow(label="Kernel", sublabel=fetch.kernel),
+ ],
+ ),
+ ],
+ )
+ super().__init__(
+ label="About",
+ icon="help-about-symbolic",
+ page=page,
+ )
diff --git a/ignis/backup_30.03.2025/modules/settings/pages/appearance.py b/ignis/backup_30.03.2025/modules/settings/pages/appearance.py
new file mode 100644
index 0000000..df228f3
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/settings/pages/appearance.py
@@ -0,0 +1,67 @@
+import os
+from services.material import MaterialService
+from ..elements import SwitchRow, SettingsPage, SettingsGroup, FileRow, SettingsEntry
+from ignis.widgets import Widget
+from user_options import user_options
+from ignis.options import options
+
+material = MaterialService.get_default()
+
+
+class AppearanceEntry(SettingsEntry):
+ def __init__(self):
+ page = SettingsPage(
+ name="Appearance",
+ groups=[
+ SettingsGroup(
+ name=None,
+ rows=[
+ Widget.ListBoxRow(
+ child=Widget.Picture(
+ image=options.wallpaper.bind("wallpaper_path"),
+ width=1920 // 4,
+ height=1080 // 4,
+ halign="center",
+ style="border-radius: 1rem;",
+ content_fit="cover",
+ ),
+ selectable=False,
+ activatable=False,
+ ),
+ SwitchRow(
+ label="Dark mode",
+ active=user_options.material.bind("dark_mode"),
+ on_change=lambda x,
+ state: user_options.material.set_dark_mode(state),
+ style="margin-top: 1rem;",
+ ),
+ FileRow(
+ label="Wallpaper path",
+ button_label=os.path.basename(
+ options.wallpaper.wallpaper_path
+ )
+ if options.wallpaper.wallpaper_path
+ else None,
+ dialog=Widget.FileDialog(
+ on_file_set=lambda x, file: material.generate_colors(
+ file.get_path()
+ ),
+ initial_path=options.wallpaper.bind("wallpaper_path"),
+ filters=[
+ Widget.FileFilter(
+ mime_types=["image/jpeg", "image/png"],
+ default=True,
+ name="Images JPEG/PNG",
+ )
+ ],
+ ),
+ ),
+ ],
+ )
+ ],
+ )
+ super().__init__(
+ label="Appearance",
+ icon="preferences-desktop-wallpaper-symbolic",
+ page=page,
+ )
diff --git a/ignis/backup_30.03.2025/modules/settings/pages/notifications.py b/ignis/backup_30.03.2025/modules/settings/pages/notifications.py
new file mode 100644
index 0000000..cbd0f11
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/settings/pages/notifications.py
@@ -0,0 +1,45 @@
+from ..elements import SwitchRow, SettingsPage, SettingsGroup, SpinRow, SettingsEntry
+from ignis.options import options
+
+
+class NotificationsEntry(SettingsEntry):
+ def __init__(self):
+ page = SettingsPage(
+ name="Notifications",
+ groups=[
+ SettingsGroup(
+ name="General",
+ rows=[
+ SwitchRow(
+ label="Do not disturb",
+ active=options.notifications.bind("dnd"),
+ on_change=lambda x, state: options.notifications.set_dnd(
+ state
+ ),
+ ),
+ SpinRow(
+ label="Maximum popups count",
+ sublabel="The first popup will automatically dismiss",
+ value=options.notifications.bind("max_popups_count"),
+ min=1,
+ on_change=lambda x,
+ value: options.notifications.set_max_popups_count(value),
+ ),
+ SpinRow(
+ label="Popup timeout",
+ sublabel="Timeout before popup will be dismissed, in milliseconds.",
+ max=100000,
+ step=100,
+ value=options.notifications.bind("popup_timeout"),
+ on_change=lambda x,
+ value: options.notifications.set_popup_timeout(value),
+ ),
+ ],
+ )
+ ],
+ )
+ super().__init__(
+ label="Notifications",
+ icon="notification-symbolic",
+ page=page,
+ )
diff --git a/ignis/backup_30.03.2025/modules/settings/pages/recorder.py b/ignis/backup_30.03.2025/modules/settings/pages/recorder.py
new file mode 100644
index 0000000..1ca59f4
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/settings/pages/recorder.py
@@ -0,0 +1,61 @@
+from ..elements import (
+ SpinRow,
+ SettingsPage,
+ SettingsGroup,
+ EntryRow,
+ FileRow,
+ SettingsEntry,
+)
+from ignis.widgets import Widget
+from ignis.options import options
+
+
+class RecorderEntry(SettingsEntry):
+ def __init__(self):
+ page = SettingsPage(
+ name="Recorder",
+ groups=[
+ SettingsGroup(
+ name="General",
+ rows=[
+ SpinRow(
+ label="Recording bitrate",
+ sublabel="Affects the recording quality",
+ value=options.recorder.bind("bitrate"),
+ max=640000,
+ width=150,
+ on_change=lambda x, value: options.recorder.set_bitrate(
+ int(value)
+ ),
+ step=1000,
+ ),
+ FileRow(
+ label="Recording path",
+ button_label=options.recorder.bind("default_file_location"),
+ dialog=Widget.FileDialog(
+ on_file_set=lambda x,
+ file: options.recorder.set_default_file_location(
+ file.get_path()
+ ),
+ select_folder=True,
+ initial_path=options.recorder.default_file_location,
+ ),
+ ),
+ EntryRow(
+ label="Recording filename",
+ sublabel="Support time formatting",
+ text=options.recorder.bind("default_filename"),
+ on_change=lambda x: options.recorder.set_default_filename(
+ x.text
+ ),
+ width=200,
+ ),
+ ],
+ )
+ ],
+ )
+ super().__init__(
+ label="Recorder",
+ icon="media-record-symbolic",
+ page=page,
+ )
diff --git a/ignis/backup_30.03.2025/modules/settings/pages/user.py b/ignis/backup_30.03.2025/modules/settings/pages/user.py
new file mode 100644
index 0000000..49b84d3
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/settings/pages/user.py
@@ -0,0 +1,54 @@
+import os
+from ..elements import SettingsGroup, SettingsPage, SettingsEntry, FileRow
+from ignis.widgets import Widget
+from user_options import user_options
+
+
+class UserEntry(SettingsEntry):
+ def __init__(self):
+ page = SettingsPage(
+ name="User",
+ groups=[
+ Widget.Box(
+ halign="start",
+ style="margin-left: 2rem;",
+ child=[
+ Widget.Picture(
+ image=user_options.user.bind(
+ "avatar",
+ lambda value: "user-info"
+ if not os.path.exists(value)
+ else value,
+ ),
+ width=96,
+ height=96,
+ style="border-radius: 10rem;",
+ ),
+ Widget.Label(
+ label=os.getenv("USER"), css_classes=["settings-user-name"]
+ ),
+ ],
+ ),
+ SettingsGroup(
+ style="margin-top: 2rem;",
+ valign="start",
+ name="General",
+ vexpand=True,
+ rows=[
+ FileRow(
+ label="Avatar",
+ dialog=Widget.FileDialog(
+ initial_path=user_options.user.bind("avatar"),
+ on_file_set=lambda x,
+ gfile: user_options.user.set_avatar(gfile.get_path()),
+ ),
+ )
+ ],
+ ),
+ ],
+ )
+ super().__init__(
+ label="User",
+ icon="user-available-symbolic",
+ page=page,
+ )
diff --git a/ignis/backup_30.03.2025/modules/settings/settings.py b/ignis/backup_30.03.2025/modules/settings/settings.py
new file mode 100644
index 0000000..2dd6db7
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/settings/settings.py
@@ -0,0 +1,71 @@
+from ignis.widgets import Widget
+from ignis.app import IgnisApp
+from user_options import user_options
+from .active_page import active_page
+from .pages import (
+ AboutEntry,
+ AppearanceEntry,
+ NotificationsEntry,
+ RecorderEntry,
+ UserEntry,
+)
+
+app = IgnisApp.get_default()
+
+
+class Settings(Widget.RegularWindow):
+ def __init__(self) -> None:
+ content = Widget.Box(
+ hexpand=True,
+ vexpand=True,
+ child=active_page.bind("value", transform=lambda value: [value]),
+ )
+ self._listbox = Widget.ListBox()
+
+ navigation_sidebar = Widget.Box(
+ vertical=True,
+ css_classes=["settings-sidebar"],
+ child=[
+ Widget.Label(
+ label="Settings",
+ halign="start",
+ css_classes=["settings-sidebar-label"],
+ ),
+ self._listbox,
+ ],
+ )
+
+ super().__init__(
+ default_width=900,
+ default_height=600,
+ resizable=False,
+ hide_on_close=True,
+ visible=False,
+ child=Widget.Box(child=[navigation_sidebar, content]),
+ namespace="ignis_SETTINGS",
+ )
+
+ self.connect("notify::visible", self.__on_open)
+
+ def __on_open(self, *args) -> None:
+ if self.visible is False:
+ return
+
+ if len(self._listbox.rows) != 0:
+ return
+
+ rows = [
+ NotificationsEntry(),
+ RecorderEntry(),
+ AppearanceEntry(),
+ UserEntry(),
+ AboutEntry(),
+ ]
+
+ self._listbox.rows = rows
+ self._listbox.select_row(rows[user_options.settings.last_page])
+
+ self._listbox.connect("row-activated", self.__update_last_page)
+
+ def __update_last_page(self, x, row) -> None:
+ user_options.settings.last_page = self._listbox.rows.index(row)
diff --git a/ignis/backup_30.03.2025/modules/shared_widgets/__init__.py b/ignis/backup_30.03.2025/modules/shared_widgets/__init__.py
new file mode 100644
index 0000000..ec224cb
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/shared_widgets/__init__.py
@@ -0,0 +1,5 @@
+from .toggle_box import ToggleBox
+from .notification import NotificationWidget
+from .volume_slider import MaterialVolumeSlider
+
+__all__ = ["ToggleBox", "NotificationWidget", "MaterialVolumeSlider"]
diff --git a/ignis/backup_30.03.2025/modules/shared_widgets/__pycache__/__init__.cpython-313.pyc b/ignis/backup_30.03.2025/modules/shared_widgets/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..da42942
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/shared_widgets/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/shared_widgets/__pycache__/notification.cpython-313.pyc b/ignis/backup_30.03.2025/modules/shared_widgets/__pycache__/notification.cpython-313.pyc
new file mode 100644
index 0000000..663cf1a
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/shared_widgets/__pycache__/notification.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/shared_widgets/__pycache__/toggle_box.cpython-313.pyc b/ignis/backup_30.03.2025/modules/shared_widgets/__pycache__/toggle_box.cpython-313.pyc
new file mode 100644
index 0000000..ee5f559
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/shared_widgets/__pycache__/toggle_box.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/shared_widgets/__pycache__/volume_slider.cpython-313.pyc b/ignis/backup_30.03.2025/modules/shared_widgets/__pycache__/volume_slider.cpython-313.pyc
new file mode 100644
index 0000000..aa35a3f
Binary files /dev/null and b/ignis/backup_30.03.2025/modules/shared_widgets/__pycache__/volume_slider.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/modules/shared_widgets/notification.py b/ignis/backup_30.03.2025/modules/shared_widgets/notification.py
new file mode 100644
index 0000000..ad505a3
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/shared_widgets/notification.py
@@ -0,0 +1,138 @@
+import asyncio
+from ignis.widgets import Widget
+from ignis.services.notifications import Notification
+from ignis.utils import Utils
+
+
+class ScreenshotLayout(Widget.Box):
+ def __init__(self, notification: Notification) -> None:
+ super().__init__(
+ vertical=True,
+ hexpand=True,
+ child=[
+ Widget.Box(
+ child=[
+ Widget.Picture(
+ image=notification.icon,
+ content_fit="cover",
+ width=1920 // 7,
+ height=1080 // 7,
+ style="border-radius: 1rem; background-color: black;",
+ ),
+ Widget.Button(
+ child=Widget.Icon(
+ image="window-close-symbolic", pixel_size=20
+ ),
+ halign="end",
+ valign="start",
+ hexpand=True,
+ css_classes=["notification-close"],
+ on_click=lambda x: notification.close(),
+ ),
+ ],
+ ),
+ Widget.Label(
+ label="Screenshot saved",
+ css_classes=["notification-screenshot-label"],
+ ),
+ Widget.Box(
+ homogeneous=True,
+ style="margin-top: 0.75rem;",
+ spacing=10,
+ child=[
+ Widget.Button(
+ child=Widget.Label(label="Open"),
+ css_classes=["notification-action"],
+ on_click=lambda x: asyncio.create_task(
+ Utils.exec_sh_async(f"xdg-open {notification.icon}")
+ ),
+ ),
+ Widget.Button(
+ child=Widget.Label(label="Close"),
+ css_classes=["notification-action"],
+ on_click=lambda x: notification.close(),
+ ),
+ ],
+ ),
+ ],
+ )
+
+
+class NormalLayout(Widget.Box):
+ def __init__(self, notification: Notification) -> None:
+ super().__init__(
+ vertical=True,
+ hexpand=True,
+ child=[
+ Widget.Box(
+ child=[
+ Widget.Icon(
+ image=notification.icon
+ if notification.icon
+ else "dialog-information-symbolic",
+ pixel_size=48,
+ halign="start",
+ valign="start",
+ ),
+ Widget.Box(
+ vertical=True,
+ style="margin-left: 0.75rem;",
+ child=[
+ Widget.Label(
+ ellipsize="end",
+ label=notification.summary,
+ halign="start",
+ visible=notification.summary != "",
+ css_classes=["notification-summary"],
+ ),
+ Widget.Label(
+ label=notification.body,
+ ellipsize="end",
+ halign="start",
+ css_classes=["notification-body"],
+ visible=notification.body != "",
+ ),
+ ],
+ ),
+ Widget.Button(
+ child=Widget.Icon(
+ image="window-close-symbolic", pixel_size=20
+ ),
+ halign="end",
+ valign="start",
+ hexpand=True,
+ css_classes=["notification-close"],
+ on_click=lambda x: notification.close(),
+ ),
+ ],
+ ),
+ Widget.Box(
+ child=[
+ Widget.Button(
+ child=Widget.Label(label=action.label),
+ on_click=lambda x, action=action: action.invoke(),
+ css_classes=["notification-action"],
+ )
+ for action in notification.actions
+ ],
+ homogeneous=True,
+ style="margin-top: 0.75rem;" if notification.actions else "",
+ spacing=10,
+ ),
+ ],
+ )
+
+
+class NotificationWidget(Widget.Box):
+ def __init__(self, notification: Notification) -> None:
+ layout: NormalLayout | ScreenshotLayout
+
+ if notification.app_name == "grimblast":
+ layout = ScreenshotLayout(notification)
+ else:
+ layout = NormalLayout(notification)
+
+ super().__init__(
+ css_classes=["notification"],
+ child=[layout],
+ )
diff --git a/ignis/backup_30.03.2025/modules/shared_widgets/toggle_box.py b/ignis/backup_30.03.2025/modules/shared_widgets/toggle_box.py
new file mode 100644
index 0000000..2d94c28
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/shared_widgets/toggle_box.py
@@ -0,0 +1,25 @@
+from ignis.widgets import Widget
+from typing import Callable
+
+
+class ToggleBox(Widget.Box):
+ def __init__(
+ self,
+ label: str,
+ active: bool,
+ on_change: Callable,
+ css_classes: list[str] = [],
+ **kwargs,
+ ):
+ super().__init__(
+ child=[
+ Widget.Label(label=label),
+ Widget.Switch(
+ halign="end",
+ hexpand=True,
+ active=active,
+ on_change=on_change,
+ ),
+ ],
+ css_classes=["toggle-box"] + css_classes,
+ )
diff --git a/ignis/backup_30.03.2025/modules/shared_widgets/volume_slider.py b/ignis/backup_30.03.2025/modules/shared_widgets/volume_slider.py
new file mode 100644
index 0000000..68a9a0e
--- /dev/null
+++ b/ignis/backup_30.03.2025/modules/shared_widgets/volume_slider.py
@@ -0,0 +1,18 @@
+from ignis.widgets import Widget
+from ignis.services.audio import AudioService, Stream
+
+audio = AudioService.get_default()
+
+
+class MaterialVolumeSlider(Widget.Scale):
+ def __init__(self, stream: Stream, **kwargs):
+ super().__init__(
+ value=stream.bind_many(
+ ["volume", "is_muted"],
+ lambda volume, is_muted: 0 if is_muted or volume is None else volume,
+ ),
+ css_classes=["material-slider"],
+ hexpand=True,
+ step=5,
+ **kwargs,
+ )
diff --git a/ignis/backup_30.03.2025/nvidia_deps.txt b/ignis/backup_30.03.2025/nvidia_deps.txt
new file mode 100644
index 0000000..704505f
--- /dev/null
+++ b/ignis/backup_30.03.2025/nvidia_deps.txt
@@ -0,0 +1 @@
+libva-nvidia-driver
\ No newline at end of file
diff --git a/ignis/backup_30.03.2025/scripts/recording.py b/ignis/backup_30.03.2025/scripts/recording.py
new file mode 100644
index 0000000..7bf75c6
--- /dev/null
+++ b/ignis/backup_30.03.2025/scripts/recording.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python3
+
+import sys
+from ignis.client import IgnisClient
+
+CODE_TEMPLATE = """
+from ignis.services.recorder import RecorderService
+
+recorder = RecorderService.get_default()
+recorder.{}_recording()
+"""
+
+
+def run_code(_type: str) -> None:
+ client = IgnisClient()
+ client.run_python(CODE_TEMPLATE.format(_type))
+
+
+if len(sys.argv) < 2:
+ sys.exit(1)
+
+elif sys.argv[1] == "start":
+ run_code("start")
+
+elif sys.argv[1] == "stop":
+ run_code("stop")
+
+elif sys.argv[1] == "pause":
+ run_code("pause")
+
+elif sys.argv[1] == "continue":
+ run_code("continue")
diff --git a/ignis/backup_30.03.2025/scss/bar.scss b/ignis/backup_30.03.2025/scss/bar.scss
new file mode 100644
index 0000000..3d73c1e
--- /dev/null
+++ b/ignis/backup_30.03.2025/scss/bar.scss
@@ -0,0 +1,142 @@
+.bar-widget {
+ padding: 0.4rem 1rem;
+ margin: 0;
+ border-radius: 0;
+ transition: 0.3s;
+ background-color: rgba($surface, 0.3);
+}
+
+.clock {
+ @include hover($surface);
+ color: $onSurface;
+ background-color: $surface;
+ font-size: 1.5rem;
+ border-radius: 1rem;
+ padding: 0 0.75rem;
+ margin: 0 0.5rem;
+ margin-right: 0;
+ transition: 0.3s;
+
+ &.active {
+ background-color: $primary;
+ color: $surface;
+
+ .record-indicator {
+ color: $surface;
+ }
+
+ @include hover($primary)
+ }
+}
+
+.kb-layout {
+ border-radius: 0.5rem;
+ padding: 0 0.25rem;
+ margin: 0 0.25rem;
+ transition: 0.3s;
+ font-size: 1.3rem;
+}
+
+.kb-layout:hover {
+ background-color: rgba(white, 0.2);
+}
+
+.pinned-app {
+ margin: 0 0.25rem;
+ transition: 0.3s;
+ padding: 0 0.1rem;
+ border-radius: 0.25rem;
+}
+
+.pinned-app:hover {
+ background-color: rgba(white, 0.2);
+}
+
+
+.tray {
+ padding: 0 0.25rem;
+ margin: 0 0.25rem;
+}
+
+.workspaces {
+ border-radius: 1rem;
+ padding: 0.5rem;
+ background-color: $surface;
+ margin: 0 0.5rem;
+ margin-left: 0;
+}
+
+.workspace {
+ @include hover($primaryContainer);
+ border-radius: 1rem;
+ margin: 0 0.25rem;
+ min-width: 0.7rem;
+ min-height: 0.7rem;
+ transition: 0.3s;
+
+ &.active {
+ @include hover($primary);
+ min-width: 2rem;
+ }
+}
+
+@keyframes recorder-animation {
+ 0% {
+ opacity: 0;
+ }
+
+ 50% {
+ opacity: 1;
+ }
+
+ 90% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0;
+ }
+}
+
+.record-indicator {
+ color: $error;
+}
+
+.record-indicator.active {
+ animation: recorder-animation 2s infinite;
+}
+
+.battery-item {
+ margin: 0 0.25rem;
+}
+
+.battery-scale {
+ min-width: 5rem;
+ all: unset;
+
+ * {
+ all: unset;
+ }
+}
+
+.battery-scale trough,
+.battery-scale trough highlight {
+ background: $surface;
+ border-radius: 1rem;
+ min-height: 1.3rem;
+ min-width: 5rem;
+}
+
+.battery-scale trough highlight {
+ background: $primary;
+}
+
+.battery-icon {
+ color: $onSurface;
+ margin-right: 0.3rem;
+}
+
+.battery-percent {
+ color: $onSurface;
+ margin-right: 0.3rem;
+}
\ No newline at end of file
diff --git a/ignis/backup_30.03.2025/scss/control_center.scss b/ignis/backup_30.03.2025/scss/control_center.scss
new file mode 100644
index 0000000..0d1fd5f
--- /dev/null
+++ b/ignis/backup_30.03.2025/scss/control_center.scss
@@ -0,0 +1,174 @@
+.control-center {
+ @include window;
+ min-width: 28rem;
+ padding: 0;
+
+ .control-center-widget {
+ padding: 1rem;
+ margin-bottom: 1rem;
+ }
+
+ .qs-main-box {
+ margin-top: 0.5rem;
+ }
+
+ .user {
+ margin-top: 1rem;
+ }
+
+ .qs-button {
+ @include hover($surfaceContainerHigh);
+ color: $onSurface;
+ min-height: 2.4rem;
+ border-radius: 1.2rem;
+ transition: 0.3s;
+ padding: 0.3rem 0.8rem;
+
+ &.active {
+ @include hover($primary);
+ color: $surface;
+ }
+
+ }
+
+ .qs-button-label {
+ margin-left: 0.5rem;
+ }
+
+ .control-center-menu {
+ background-color: $surfaceContainerHigh;
+ border-radius: 1rem;
+ padding: 1rem;
+ margin-top: 1rem;
+ margin-bottom: 0.5rem;
+
+ separator {
+ margin: 0.5rem 0;
+ }
+
+ }
+ .control-center-menu-last-row {
+ margin-bottom: 0;
+ }
+
+ .record-icon {
+ color: $error;
+ }
+
+ .record-dropdown {
+ button {
+ all: unset;
+ color: $onSurfaceVariant;
+ font-size: 0.9rem;
+ }
+ }
+
+ .record-cancel-button,
+ .record-start-button {
+ color: $surface;
+ border-radius: 1rem;
+ padding: 0.25rem 0.75rem;
+ font-size: 0.95rem;
+ transition: 0.3s;
+ }
+
+ .record-cancel-button {
+ @include hover($surfaceContainerHigh);
+ color: $onSurface;
+ border: 1px solid $primary;
+ }
+
+ .record-start-button {
+ @include hover($primary);
+ }
+
+ .volume-entry {
+ @include hover($surfaceContainerHigh);
+ transition: 0.3s;
+ border-radius: 1rem;
+ padding: 0.5rem 0.5rem;
+ }
+
+ .volume-entry-list-header-label {
+ font-size: 1.35rem;
+ margin-left: 0.5rem;
+ }
+
+ .volume-entry-list-header-box {
+ margin-bottom: 0.75rem;
+ }
+
+ .user-name-box {
+ margin-left: 0.5rem;
+ }
+
+ .user-name {
+ font-size: 1.2rem;
+ }
+
+ .user-name-secondary {
+ color: $onSurfaceVariant;
+ font-size: 0.9rem;
+ }
+
+ .user-settings {
+ @include hover($surfaceContainerHighest);
+ color: $onSurface;
+ border-radius: 2rem;
+ min-width: 2.35rem;
+ margin: 0.4rem 0rem;
+ margin-right: 0.5rem;
+ transition: 0.3s;
+ }
+
+ .user-power {
+ @include hover($primary);
+ color: $surface;
+ border-radius: 1.5rem;
+ min-width: 2.35rem;
+ margin: 0.4rem 0rem;
+ transition: 0.3s;
+ }
+
+ .network-header-box {
+ margin-bottom: 0.5rem;
+ }
+
+ .network-header-label {
+ font-size: 1.35rem;
+ margin-left: 0.75rem;
+ }
+
+ .network-item {
+ @include hover($surfaceContainerHigh);
+ transition: 0.3s;
+ border-radius: 1rem;
+ padding: 0.5rem 0.5rem;
+
+ label {
+ margin-left: 0.5rem;
+ }
+
+ .connect-label {
+ font-size: 1rem;
+ color: $onSurfaceVariant;
+ }
+ }
+
+ .volume-entry-label {
+ margin-left: 0.5rem;
+ margin-right: 1rem;
+ }
+
+ .volume-mainbox-speaker {
+ margin-top: 1rem;
+ }
+
+ .volume-mainbox-microphone {
+ margin-top: 0.25rem;
+ }
+
+ .volume-menubox-speaker {
+ margin-bottom: 0.75rem;
+ }
+}
diff --git a/ignis/backup_30.03.2025/scss/launcher.scss b/ignis/backup_30.03.2025/scss/launcher.scss
new file mode 100644
index 0000000..5331b36
--- /dev/null
+++ b/ignis/backup_30.03.2025/scss/launcher.scss
@@ -0,0 +1,38 @@
+.launcher {
+ @include window;
+ margin-top: 20rem;
+}
+
+.launcher-search {
+ all: unset;
+}
+
+.launcher-search-box {
+ padding: 0.75rem 1rem;
+ background-color: $surfaceContainer;
+ border-radius: 1rem;
+ min-width: 30rem;
+}
+
+.launcher-scroll {
+ min-height: 30rem;
+}
+
+.launcher-app {
+ all: unset;
+ @include hover($surface);
+ border-radius: 1rem;
+ padding: 0.5rem 0.25rem;
+ border: 2px solid $surface;
+ margin-top: 0.25rem;
+ transition: 0.3s;
+
+ &:focus {
+ border: 2px solid $primary;
+ }
+}
+
+.launcher-app-label {
+ font-size: 1.1rem;
+ margin-left: 1rem;
+}
\ No newline at end of file
diff --git a/ignis/backup_30.03.2025/scss/lib.scss b/ignis/backup_30.03.2025/scss/lib.scss
new file mode 100644
index 0000000..9bc3d5c
--- /dev/null
+++ b/ignis/backup_30.03.2025/scss/lib.scss
@@ -0,0 +1,65 @@
+.rec-unset {
+ * {
+ all: unset;
+ }
+}
+
+.unset {
+ all: unset;
+}
+
+tooltip {
+ all: unset;
+ background-color: $surface;
+ padding: 0.5rem 1rem;
+ border-radius: 1rem;
+}
+
+.toggle-box {
+ padding: 0.75rem 1rem;
+ border-radius: 1rem;
+ font-size: 1.35rem;
+ background-color: $primaryContainer;
+}
+
+.material-slider {
+ all: unset;
+
+ * {
+ all: unset;
+ }
+}
+
+.material-slider trough,
+.material-slider trough highlight {
+ background-color: $secondaryContainer;
+ border-radius: 1rem;
+ min-height: 1rem;
+}
+
+
+.material-slider trough highlight {
+ background-color: $primary;
+}
+
+.material-slider-icon {
+ padding: 0.25rem;
+ min-width: 1.5rem;
+ min-height: 1.5rem;
+ border-radius: 1rem;
+ margin-right: 0.5rem;
+}
+
+.material-slider-arrow {
+ all: unset;
+ padding: 0.25rem;
+ min-width: 1.5rem;
+ min-height: 1.5rem;
+ border-radius: 1rem;
+ margin-left: 0.5rem;
+}
+
+.hover-surface {
+ @include hover($surface);
+ transition: 0.3s;
+}
diff --git a/ignis/backup_30.03.2025/scss/mixins/hover.scss b/ignis/backup_30.03.2025/scss/mixins/hover.scss
new file mode 100644
index 0000000..7dca32c
--- /dev/null
+++ b/ignis/backup_30.03.2025/scss/mixins/hover.scss
@@ -0,0 +1,12 @@
+@mixin hover($bg) {
+ background-color: $bg;
+
+ &:hover {
+ @if $darkmode == true {
+ background-color: lighten($bg, 5%);
+ }
+ @else {
+ background-color: darken($bg, 5%);
+ }
+ }
+}
diff --git a/ignis/backup_30.03.2025/scss/mixins/window.scss b/ignis/backup_30.03.2025/scss/mixins/window.scss
new file mode 100644
index 0000000..e73845a
--- /dev/null
+++ b/ignis/backup_30.03.2025/scss/mixins/window.scss
@@ -0,0 +1,8 @@
+@mixin window {
+ background-color: $surface;
+ border-radius: 1rem;
+ padding: 1rem;
+ color: $onSurface;
+ box-shadow: 0 0 4px 2px $shadow;
+ margin: 1rem;
+}
\ No newline at end of file
diff --git a/ignis/backup_30.03.2025/scss/notification_center.scss b/ignis/backup_30.03.2025/scss/notification_center.scss
new file mode 100644
index 0000000..21d0854
--- /dev/null
+++ b/ignis/backup_30.03.2025/scss/notification_center.scss
@@ -0,0 +1,66 @@
+.notification-center {
+ background-color: $surfaceContainerLow;
+ border-radius: 1.5rem 1.5rem 1rem 1rem;
+ padding: 1rem;
+}
+
+.notification-center-header {
+ margin-bottom: 1rem;
+}
+
+.notification {
+ background-color: $surfaceContainerHigh;
+ border-radius: 1rem;
+ padding: 1rem;
+ margin-top: 1rem;
+}
+
+.notification-count {
+ background-color: $tertiary;
+ color: $onTertiary;
+ min-width: 2rem;
+ font-size: 1.5rem;
+ border-radius: 2rem;
+}
+
+.notification-header-label {
+ font-size: 1.1rem;
+ margin-left: 0.5rem;
+}
+
+.notification-close {
+ @include hover($surfaceContainerHigh);
+ color: $onSurface;
+ border-radius: 1rem;
+ padding: 0.25rem;
+ margin-left: 0.5rem;
+ transition: 0.3s;
+}
+
+.notification-clear-all {
+ @include hover($surfaceContainerHigh);
+ transition: 0.3s;
+ border-radius: 1rem;
+ padding: 0.25rem 1rem;
+}
+
+.notification-action {
+ color: $primary;
+ @include hover($surfaceContainerHighest);
+ border-radius: 0.5rem;
+ padding: 0.25rem 0.25rem;
+ transition: 0.3s;
+}
+
+.notification-screenshot-label {
+ margin-top: 0.5rem;
+ font-size: 1.1rem;
+}
+
+.notification-center-info-label {
+ color: $onSurfaceVariant;
+}
+
+.notification-body {
+ color: $onSurfaceVariant;
+}
\ No newline at end of file
diff --git a/ignis/backup_30.03.2025/scss/notification_popup.scss b/ignis/backup_30.03.2025/scss/notification_popup.scss
new file mode 100644
index 0000000..f4c9034
--- /dev/null
+++ b/ignis/backup_30.03.2025/scss/notification_popup.scss
@@ -0,0 +1,12 @@
+.notification-popup {
+ @include window;
+ min-width: 25rem;
+
+ & .notification-close {
+ @include hover($surface);
+ }
+
+ & .notification-action {
+ @include hover($surfaceContainer);
+ }
+}
diff --git a/ignis/backup_30.03.2025/scss/osd.scss b/ignis/backup_30.03.2025/scss/osd.scss
new file mode 100644
index 0000000..25f1aea
--- /dev/null
+++ b/ignis/backup_30.03.2025/scss/osd.scss
@@ -0,0 +1,8 @@
+.osd {
+ @include window;
+ min-width: 15rem;
+ margin-bottom: 5rem;
+ * {
+ all: unset
+ }
+}
\ No newline at end of file
diff --git a/ignis/backup_30.03.2025/scss/powermenu.scss b/ignis/backup_30.03.2025/scss/powermenu.scss
new file mode 100644
index 0000000..8011a64
--- /dev/null
+++ b/ignis/backup_30.03.2025/scss/powermenu.scss
@@ -0,0 +1,28 @@
+.powermenu {
+ @include window;
+}
+
+.powermenu-button {
+ @include hover($surfaceContainer);
+ transition: 0.3s;
+ border-radius: 10rem;
+ min-width: 8rem;
+ min-height: 8rem;
+
+ &:focus {
+ @include hover($primary);
+ color: $surface;
+ }
+}
+
+.powermenu-button-label {
+ margin-top: 0.5rem;
+}
+
+.powermenu-button-box {
+ margin: 0.5rem;
+}
+
+.powermenu-overlay {
+ background-color: rgba(0, 0, 0, 0.5);
+}
diff --git a/ignis/backup_30.03.2025/scss/settings.scss b/ignis/backup_30.03.2025/scss/settings.scss
new file mode 100644
index 0000000..31bcc08
--- /dev/null
+++ b/ignis/backup_30.03.2025/scss/settings.scss
@@ -0,0 +1,71 @@
+.settings-sidebar {
+ background-color: $surfaceContainer;
+ min-width: 300px;
+}
+
+.settings-sidebar-label {
+ font-size: 2rem;
+ margin-left: 1rem;
+ margin-bottom: 0.5rem;
+ margin-top: 2rem;
+}
+
+.settings-sidebar-entry {
+ all: unset;
+ background-color: $surfaceContainer;
+ padding: 1rem 2rem;
+ padding-left: 0.75rem;
+ font-size: 1.2rem;
+
+ &:selected {
+ @if $darkmode == true {
+ background-color: lighten($surfaceContainer, 10%);
+ }
+ @else {
+ background-color: darken($surfaceContainer, 10%);
+ }
+ }
+}
+
+
+.settings-page-name {
+ font-size: 2rem;
+ margin-left: 2.5rem;
+ margin-bottom: 2rem;
+ margin-top: 2rem;
+}
+
+.settings-group-name {
+ margin-left: 2.5rem;
+ color: $primary;
+}
+
+.settings-row {
+ all: unset;
+ padding: 0.7rem 2.5rem;
+ transition: 0.2s;
+
+ &:active {
+ @if $darkmode == true {
+ background-color: rgba(255, 255, 255, 0.05);
+ }
+ @else {
+ background-color: rgba(0, 0, 0, 0.05);
+ }
+ }
+}
+
+.settings-row-label {
+ font-size: 1.2rem;
+}
+
+.settings-row-sublabel {
+ font-size: 0.9rem;
+ font-weight: normal;
+ color: $onSurfaceVariant;
+}
+
+.settings-user-name {
+ font-size: 2.3rem;
+ margin-left: 1.5rem;
+}
\ No newline at end of file
diff --git a/ignis/backup_30.03.2025/services/__init__.py b/ignis/backup_30.03.2025/services/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/ignis/backup_30.03.2025/services/__pycache__/__init__.cpython-313.pyc b/ignis/backup_30.03.2025/services/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..c03bdff
Binary files /dev/null and b/ignis/backup_30.03.2025/services/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/services/material/__init__.py b/ignis/backup_30.03.2025/services/material/__init__.py
new file mode 100644
index 0000000..fd9caa6
--- /dev/null
+++ b/ignis/backup_30.03.2025/services/material/__init__.py
@@ -0,0 +1,3 @@
+from .service import MaterialService
+
+__all__ = ["MaterialService"]
diff --git a/ignis/backup_30.03.2025/services/material/__pycache__/__init__.cpython-313.pyc b/ignis/backup_30.03.2025/services/material/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..348d851
Binary files /dev/null and b/ignis/backup_30.03.2025/services/material/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/services/material/__pycache__/constants.cpython-313.pyc b/ignis/backup_30.03.2025/services/material/__pycache__/constants.cpython-313.pyc
new file mode 100644
index 0000000..7390c6a
Binary files /dev/null and b/ignis/backup_30.03.2025/services/material/__pycache__/constants.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/services/material/__pycache__/service.cpython-313.pyc b/ignis/backup_30.03.2025/services/material/__pycache__/service.cpython-313.pyc
new file mode 100644
index 0000000..74dfd51
Binary files /dev/null and b/ignis/backup_30.03.2025/services/material/__pycache__/service.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/services/material/__pycache__/util.cpython-313.pyc b/ignis/backup_30.03.2025/services/material/__pycache__/util.cpython-313.pyc
new file mode 100644
index 0000000..0a3f86e
Binary files /dev/null and b/ignis/backup_30.03.2025/services/material/__pycache__/util.cpython-313.pyc differ
diff --git a/ignis/backup_30.03.2025/services/material/constants.py b/ignis/backup_30.03.2025/services/material/constants.py
new file mode 100644
index 0000000..9a5d44a
--- /dev/null
+++ b/ignis/backup_30.03.2025/services/material/constants.py
@@ -0,0 +1,11 @@
+import os
+import ignis
+from ignis.utils import Utils
+
+
+MATERIAL_CACHE_DIR = f"{ignis.CACHE_DIR}/material" # type: ignore
+
+TEMPLATES = Utils.get_current_dir() + "/templates"
+SAMPLE_WALL = Utils.get_current_dir() + "/sample_wall.png"
+
+os.makedirs(MATERIAL_CACHE_DIR, exist_ok=True)
diff --git a/ignis/backup_30.03.2025/services/material/sample_wall.png b/ignis/backup_30.03.2025/services/material/sample_wall.png
new file mode 100644
index 0000000..8d6dc01
Binary files /dev/null and b/ignis/backup_30.03.2025/services/material/sample_wall.png differ
diff --git a/ignis/backup_30.03.2025/services/material/service.py b/ignis/backup_30.03.2025/services/material/service.py
new file mode 100644
index 0000000..81f73fa
--- /dev/null
+++ b/ignis/backup_30.03.2025/services/material/service.py
@@ -0,0 +1,127 @@
+#!/usr/bin/python
+import os
+import asyncio
+from gi.repository import GLib # type: ignore
+from jinja2 import Template
+from PIL import Image
+from materialyoucolor.quantize import QuantizeCelebi
+from materialyoucolor.hct import Hct
+from materialyoucolor.scheme.scheme_tonal_spot import SchemeTonalSpot
+from materialyoucolor.dynamiccolor.material_dynamic_colors import MaterialDynamicColors
+from materialyoucolor.score.score import Score
+
+from ignis.utils import Utils
+from ignis.app import IgnisApp
+from ignis.base_service import BaseService
+from ignis.services.wallpaper import CACHE_WALLPAPER_PATH
+from ignis.options import options
+from user_options import user_options
+
+from .constants import MATERIAL_CACHE_DIR, TEMPLATES, SAMPLE_WALL
+from .util import rgba_to_hex, calculate_optimal_size
+
+app = IgnisApp.get_default()
+
+
+class MaterialService(BaseService):
+ def __init__(self):
+ super().__init__()
+
+ if not os.path.exists(CACHE_WALLPAPER_PATH):
+ self.__on_colors_not_found()
+ if user_options.material.colors == {}:
+ self.__on_colors_not_found()
+
+ user_options.material.connect_option(
+ "dark_mode", lambda: self.generate_colors(CACHE_WALLPAPER_PATH)
+ )
+
+ def __on_colors_not_found(self) -> None:
+ options.wallpaper.set_wallpaper_path(SAMPLE_WALL)
+ self.generate_colors(SAMPLE_WALL)
+ asyncio.create_task(Utils.exec_sh_async("hyprctl reload"))
+
+ def get_colors_from_img(self, path: str, dark_mode: bool) -> dict[str, str]:
+ image = Image.open(path)
+ wsize, hsize = image.size
+ wsize_new, hsize_new = calculate_optimal_size(wsize, hsize, 128)
+ if wsize_new < wsize or hsize_new < hsize:
+ image = image.resize((wsize_new, hsize_new), Image.Resampling.BICUBIC) # type: ignore
+
+ pixel_len = image.width * image.height
+ image_data = image.getdata()
+ pixel_array = [image_data[_] for _ in range(0, pixel_len, 1)]
+
+ colors = QuantizeCelebi(pixel_array, 128)
+ argb = Score.score(colors)[0]
+
+ hct = Hct.from_int(argb)
+ scheme = SchemeTonalSpot(hct, dark_mode, 0.0)
+
+ material_colors = {}
+ 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()
+ material_colors[color] = rgba_to_hex(rgba)
+
+ return material_colors
+
+ def generate_colors(self, path: str) -> None:
+ colors = self.get_colors_from_img(path, user_options.material.dark_mode)
+ dark_colors = self.get_colors_from_img(path, True)
+ user_options.material.colors = colors
+ self.__render_templates(colors, dark_colors)
+ asyncio.create_task(self.__setup(path))
+
+ def __render_templates(self, colors: dict, dark_colors: dict) -> None:
+ for template in os.listdir(TEMPLATES):
+ self.render_template(
+ colors=colors,
+ dark_mode=user_options.material.dark_mode,
+ input_file=f"{TEMPLATES}/{template}",
+ output_file=f"{MATERIAL_CACHE_DIR}/{template}",
+ )
+
+ for template in os.listdir(TEMPLATES):
+ self.render_template(
+ colors=dark_colors,
+ dark_mode=True,
+ input_file=f"{TEMPLATES}/{template}",
+ output_file=f"{MATERIAL_CACHE_DIR}/dark_{template}",
+ )
+
+ def render_template(
+ self,
+ colors: dict,
+ input_file: str,
+ output_file: str,
+ dark_mode: bool | None = None,
+ ) -> None:
+ if dark_mode is None:
+ colors["dark_mode"] = str(user_options.material.dark_mode).lower()
+ else:
+ colors["dark_mode"] = str(dark_mode).lower()
+ with open(input_file) as file:
+ template_rendered = Template(file.read()).render(colors)
+
+ with open(output_file, "w") as file:
+ file.write(template_rendered)
+
+ async def __reload_gtk_theme(self) -> None:
+ THEME_CMD = "gsettings set org.gnome.desktop.interface gtk-theme {}"
+ COLOR_SCHEME_CMD = "gsettings set org.gnome.desktop.interface color-scheme {}"
+ await Utils.exec_sh_async(THEME_CMD.format("Adwaita"))
+ await Utils.exec_sh_async(THEME_CMD.format("Material"))
+ await Utils.exec_sh_async(COLOR_SCHEME_CMD.format("default"))
+ await Utils.exec_sh_async(COLOR_SCHEME_CMD.format("prefer-dark"))
+ await Utils.exec_sh_async(COLOR_SCHEME_CMD.format("default"))
+
+ async def __setup(self, image_path: str) -> None:
+ try:
+ await Utils.exec_sh_async("pkill -SIGUSR1 kitty")
+ except GLib.Error:
+ ...
+ options.wallpaper.set_wallpaper_path(image_path)
+ app.reload_css()
+ await self.__reload_gtk_theme()
diff --git a/ignis/backup_30.03.2025/services/material/templates/colors-hyprland.conf b/ignis/backup_30.03.2025/services/material/templates/colors-hyprland.conf
new file mode 100644
index 0000000..1ae3635
--- /dev/null
+++ b/ignis/backup_30.03.2025/services/material/templates/colors-hyprland.conf
@@ -0,0 +1,7 @@
+$primary = rgb({{ primary | replace("#", "") }})
+$surface = rgb({{ surface | replace("#", "") }})
+$onSurface = rgb({{ onSurface | replace("#", "") }})
+
+$primaryHex = #{{ primary }}
+$surfaceHex = #{{ surface }}
+$onSurfaceHex = #{{ onSurface }}
\ No newline at end of file
diff --git a/ignis/backup_30.03.2025/services/material/templates/colors-kitty.conf b/ignis/backup_30.03.2025/services/material/templates/colors-kitty.conf
new file mode 100644
index 0000000..c3ed32e
--- /dev/null
+++ b/ignis/backup_30.03.2025/services/material/templates/colors-kitty.conf
@@ -0,0 +1,23 @@
+foreground {{ onBackground }}
+background {{ background }}
+cursor {{ primaryFixed }}
+
+# Base 8 Colors (0-7)
+color0 {{ surfaceContainerLowest }}
+color1 {{ error }}
+color2 {{ primary }}
+color3 {{ tertiary }}
+color4 {{ primaryContainer }}
+color5 {{ secondaryFixedDim }}
+color6 {{ tertiaryFixed }}
+color7 {{ onSurface }}
+
+# Bright (Bold) Variants (8–15)
+color8 {{ outlineVariant }}
+color9 {{ errorContainer }}
+color10 {{ primaryContainer }}
+color11 {{ tertiaryContainer }}
+color12 {{ secondaryContainer }}
+color13 {{ onSecondaryFixedVariant }}
+color14 {{ onTertiaryFixedVariant }}
+color15 {{ onSurfaceVariant }}
diff --git a/ignis/backup_30.03.2025/services/material/templates/colors.scss b/ignis/backup_30.03.2025/services/material/templates/colors.scss
new file mode 100644
index 0000000..d6c1f68
--- /dev/null
+++ b/ignis/backup_30.03.2025/services/material/templates/colors.scss
@@ -0,0 +1,55 @@
+$primary_paletteKeyColor: {{ primary_paletteKeyColor }};
+$secondary_paletteKeyColor: {{ secondary_paletteKeyColor }};
+$tertiary_paletteKeyColor: {{ tertiary_paletteKeyColor }};
+$neutral_paletteKeyColor: {{ neutral_paletteKeyColor }};
+$neutral_variant_paletteKeyColor: {{ neutral_variant_paletteKeyColor }};
+$background: {{ background }};
+$onBackground: {{ onBackground }};
+$surface: {{ surface }};
+$surfaceDim: {{ surfaceDim }};
+$surfaceBright: {{ surfaceBright }};
+$surfaceContainerLowest: {{ surfaceContainerLowest }};
+$surfaceContainerLow: {{ surfaceContainerLow }};
+$surfaceContainer: {{ surfaceContainer }};
+$surfaceContainerHigh: {{ surfaceContainerHigh }};
+$surfaceContainerHighest: {{ surfaceContainerHighest }};
+$onSurface: {{ onSurface }};
+$surfaceVariant: {{ surfaceVariant }};
+$onSurfaceVariant: {{ onSurfaceVariant }};
+$inverseSurface: {{ inverseSurface }};
+$inverseOnSurface: {{ inverseOnSurface }};
+$outline: {{ outline }};
+$outlineVariant: {{ outlineVariant }};
+$shadow: {{ shadow }};
+$scrim: {{ scrim }};
+$surfaceTint: {{ surfaceTint }};
+$primary: {{ primary }};
+$onPrimary: {{ onPrimary }};
+$primaryContainer: {{ primaryContainer }};
+$onPrimaryContainer: {{ onPrimaryContainer }};
+$inversePrimary: {{ inversePrimary }};
+$secondary: {{ secondary }};
+$onSecondary: {{ onSecondary }};
+$secondaryContainer: {{ secondaryContainer }};
+$onSecondaryContainer: {{ onSecondaryContainer }};
+$tertiary: {{ tertiary }};
+$onTertiary: {{ onTertiary }};
+$tertiaryContainer: {{ tertiaryContainer }};
+$onTertiaryContainer: {{ onTertiaryContainer }};
+$error: {{ error }};
+$onError: {{ onError }};
+$errorContainer: {{ errorContainer }};
+$onErrorContainer: {{ onErrorContainer }};
+$primaryFixed: {{ primaryFixed }};
+$primaryFixedDim: {{ primaryFixedDim }};
+$onPrimaryFixed: {{ onPrimaryFixed }};
+$onPrimaryFixedVariant: {{ onPrimaryFixedVariant }};
+$secondaryFixed: {{ secondaryFixed }};
+$secondaryFixedDim: {{ secondaryFixedDim }};
+$onSecondaryFixed: {{ onSecondaryFixed }};
+$onSecondaryFixedVariant: {{ onSecondaryFixedVariant }};
+$tertiaryFixed: {{ tertiaryFixed }};
+$tertiaryFixedDim: {{ tertiaryFixedDim }};
+$onTertiaryFixed: {{ onTertiaryFixed }};
+$onTertiaryFixedVariant: {{ onTertiaryFixedVariant }};
+$darkmode: {{ dark_mode }};
\ No newline at end of file
diff --git a/ignis/backup_30.03.2025/services/material/templates/gtk.css b/ignis/backup_30.03.2025/services/material/templates/gtk.css
new file mode 100644
index 0000000..688ae16
--- /dev/null
+++ b/ignis/backup_30.03.2025/services/material/templates/gtk.css
@@ -0,0 +1,50 @@
+@define-color accent_color {{ primary }};
+@define-color accent_bg_color {{ primary }};
+@define-color accent_fg_color {{ surface }};
+@define-color destructive_color {{ errorContainer }};
+@define-color destructive_bg_color {{ errorContainer }};
+@define-color destructive_fg_color {{ onSurface }};
+@define-color success_color {{ tertiary }};
+@define-color success_bg_color {{ tertiary }};
+@define-color success_fg_color {{ onSurface }};
+@define-color warning_color {{ secondary }};
+@define-color warning_bg_color {{ secondary }};
+@define-color warning_fg_color {{ onSurface }};
+@define-color error_color {{ errorContainer }};
+@define-color error_bg_color {{ errorContainer }};
+@define-color error_fg_color {{ onSurface }};
+@define-color window_bg_color {{ surface }};
+@define-color window_fg_color {{ onSurface }};
+@define-color view_bg_color {{ surface }};
+@define-color view_fg_color {{ onSurface }};
+@define-color headerbar_bg_color {{ surface }};
+@define-color headerbar_fg_color {{ onSurface }};
+@define-color headerbar_border_color {{ outline }};
+@define-color headerbar_backdrop_color {{ surface }};
+@define-color headerbar_shade_color {{ outline }};
+@define-color sidebar_bg_color {{ surfaceContainer }};
+@define-color sidebar_fg_color {{ onSurface }};
+@define-color sidebar_backdrop_color {{ surfaceContainer }};
+@define-color card_bg_color {{ surfaceContainer }};
+@define-color card_fg_color {{ onSurface }};
+@define-color card_shade_color rgba(0, 0, 0, 0.07);
+@define-color thumbnail_bg_color {{ surface }};
+@define-color thumbnail_fg_color {{ onSurface }};
+@define-color dialog_bg_color {{ surface }};
+@define-color dialog_fg_color {{ onSurface }};
+@define-color popover_bg_color {{ surfaceContainer }};
+@define-color popover_fg_color {{ onSurface }};
+@define-color shade_color rgba(0, 0, 0, 0.36);
+@define-color scrollbar_outline_color {{ outline }};
+
+.navigation-sidebar {
+ background-color: {{ surfaceContainer }};
+}
+
+switch:checked {
+ background-color: {{ primary }};
+}
+
+switch:checked slider {
+ background-color: {{ onPrimary }};
+}
\ No newline at end of file
diff --git a/ignis/backup_30.03.2025/services/material/templates/swaylock b/ignis/backup_30.03.2025/services/material/templates/swaylock
new file mode 100644
index 0000000..451229a
--- /dev/null
+++ b/ignis/backup_30.03.2025/services/material/templates/swaylock
@@ -0,0 +1,51 @@
+# ---------------------SETTINGS---------------------------#
+daemonize
+show-failed-attempts
+clock
+screenshot
+ignore-empty-password
+indicator
+font=JetBrainsMono
+indicator-radius=200
+indicator-thickness=20
+
+
+#----------------------EFFECTS----------------------------#
+effect-blur=9x5
+effect-vignette=0.5:0.5
+
+#--------------------INPUT IGNORE-------------------------#
+grace-no-mouse
+grace-no-touch
+
+
+#--------------------DATE & TIME--------------------------#
+datestr=%a,%e %B
+timestr=%H:%M
+fade-in=0.2
+
+
+#----------------------COLORS-----------------------------#
+bs-hl-color={{ error }}
+inside-color=#FFFFFF00
+ring-color=#FFFFFF00
+line-color=#FFFFFF00
+separator-color=#FFFFFF00
+key-hl-color={{ primaryFixedDim }}
+text-color={{ primaryFixedDim }}
+text-caps-lock-color={{ tertiary }}
+
+ring-clear-color=#FFFFFF00
+text-clear-color={{ onTertiary }}
+inside-clear-color={{ tertiary }}
+line-clear-color=#FFFFFF00
+
+ring-ver-color=#FFFFFF00
+text-ver-color={{ onSecondary }}
+inside-ver-color={{ secondary }}
+line-ver-color=#FFFFFF00
+
+ring-wrong-color=#FFFFFF00
+text-wrong-color={{ onError }}
+inside-wrong-color={{ error }}
+line-wrong-color=#FFFFFF00
\ No newline at end of file
diff --git a/ignis/backup_30.03.2025/services/material/util.py b/ignis/backup_30.03.2025/services/material/util.py
new file mode 100644
index 0000000..dce5ec7
--- /dev/null
+++ b/ignis/backup_30.03.2025/services/material/util.py
@@ -0,0 +1,18 @@
+import math
+
+
+def rgba_to_hex(rgba: list) -> str:
+ return "#{:02x}{:02x}{:02x}".format(*rgba)
+
+
+def calculate_optimal_size(width: int, height: int, bitmap_size: int) -> tuple:
+ image_area = width * height
+ bitmap_area = bitmap_size**2
+ scale = math.sqrt(bitmap_area / image_area) if image_area > bitmap_area else 1
+ new_width = round(width * scale)
+ new_height = round(height * scale)
+ if new_width == 0:
+ new_width = 1
+ if new_height == 0:
+ new_height = 1
+ return new_width, new_height
diff --git a/ignis/backup_30.03.2025/style.scss b/ignis/backup_30.03.2025/style.scss
new file mode 100644
index 0000000..9092992
--- /dev/null
+++ b/ignis/backup_30.03.2025/style.scss
@@ -0,0 +1,20 @@
+* {
+ font-family: JetBrainsMono;
+ font-weight: bold;
+}
+@import "../../.cache/ignis/material/colors.scss";
+
+@import "scss/mixins/window.scss";
+@import "scss/mixins/hover.scss";
+
+@import "scss/lib.scss";
+
+
+@import "./scss/bar.scss";
+@import "./scss/control_center.scss";
+@import "./scss/osd.scss";
+@import "./scss/notification_popup.scss";
+@import "./scss/launcher.scss";
+@import "./scss/settings.scss";
+@import "./scss/powermenu.scss";
+@import "./scss/notification_center.scss";
\ No newline at end of file
diff --git a/ignis/backup_30.03.2025/user_options.py b/ignis/backup_30.03.2025/user_options.py
new file mode 100644
index 0000000..506faaa
--- /dev/null
+++ b/ignis/backup_30.03.2025/user_options.py
@@ -0,0 +1,28 @@
+import os
+from ignis.options_manager import OptionsGroup, OptionsManager
+from ignis import CACHE_DIR # type: ignore
+
+
+class UserOptions(OptionsManager):
+ def __init__(self):
+ try:
+ super().__init__(file=f"{CACHE_DIR}/user_options.json")
+ except FileNotFoundError:
+ pass
+
+ class User(OptionsGroup):
+ avatar: str = f"/var/lib/AccountsService/icons/{os.getenv('USER')}"
+
+ class Settings(OptionsGroup):
+ last_page: int = 0
+
+ class Material(OptionsGroup):
+ dark_mode: bool = True
+ colors: dict[str, str] = {}
+
+ user = User()
+ settings = Settings()
+ material = Material()
+
+
+user_options = UserOptions()
diff --git a/ignis/config.py b/ignis/config.py
index af1361e..826bf50 100644
--- a/ignis/config.py
+++ b/ignis/config.py
@@ -1,15 +1,21 @@
-from check_version import check_version
+from ignis.utils import Utils
+from ignis.app import IgnisApp
+from ignis.services.wallpaper import WallpaperService
+from modules import (
+ Bar,
+ ControlCenter,
+ Launcher,
+ NotificationPopup,
+ OSD,
+ Powermenu,
+ Settings,
+)
-check_version()
+app = IgnisApp.get_default()
+WallpaperService.get_default()
-from ignis.utils import Utils # noqa: E402
-from ignis.app import IgnisApp # noqa: E402
-from modules.control_center import control_center # noqa: E402
-from modules.bar import bar # noqa: E402
-from modules.notification_popup import notification_popup # noqa: E402
-from modules.osd import OSD # noqa: E402
-from modules.powermenu import powermenu # noqa: E402
-from modules.launcher import launcher # noqa: E402
+app.add_icons(f"{Utils.get_current_dir()}/icons")
+app.apply_css(Utils.get_current_dir() + "/style.scss")
Utils.exec_sh("gsettings set org.gnome.desktop.interface gtk-theme Material")
Utils.exec_sh("gsettings set org.gnome.desktop.interface icon-theme Papirus")
@@ -17,16 +23,18 @@ Utils.exec_sh(
'gsettings set org.gnome.desktop.interface font-name "JetBrains Mono Regular 11"'
)
Utils.exec_sh("hyprctl reload")
-app = IgnisApp.get_default()
-app.apply_css(Utils.get_current_dir() + "/style.scss")
-control_center()
+ControlCenter()
+
for monitor in range(Utils.get_n_monitors()):
- bar(monitor)
-launcher()
-for monitor in range(Utils.get_n_monitors()):
- notification_popup(monitor)
+ Bar(monitor)
-powermenu()
+for monitor in range(Utils.get_n_monitors()):
+ NotificationPopup(monitor)
+
+Launcher()
+Powermenu()
OSD()
+
+Settings()
diff --git a/ignis/icons/hicolor/scalable/actions/chrome-symbolic.svg b/ignis/icons/hicolor/scalable/actions/chrome-symbolic.svg
new file mode 100644
index 0000000..4b97625
--- /dev/null
+++ b/ignis/icons/hicolor/scalable/actions/chrome-symbolic.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ignis/icons/hicolor/scalable/actions/firefox-browser-symbolic.svg b/ignis/icons/hicolor/scalable/actions/firefox-browser-symbolic.svg
new file mode 100644
index 0000000..4b552d3
--- /dev/null
+++ b/ignis/icons/hicolor/scalable/actions/firefox-browser-symbolic.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ignis/icons/hicolor/scalable/actions/spotify-symbolic.svg b/ignis/icons/hicolor/scalable/actions/spotify-symbolic.svg
new file mode 100644
index 0000000..d5d67f7
--- /dev/null
+++ b/ignis/icons/hicolor/scalable/actions/spotify-symbolic.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ignis/modules/__init__.py b/ignis/modules/__init__.py
index e69de29..2d194b4 100644
--- a/ignis/modules/__init__.py
+++ b/ignis/modules/__init__.py
@@ -0,0 +1,17 @@
+from .bar import Bar
+from .control_center import ControlCenter
+from .launcher import Launcher
+from .notification_popup import NotificationPopup
+from .osd import OSD
+from .powermenu import Powermenu
+from .settings import Settings
+
+__all__ = [
+ "Bar",
+ "ControlCenter",
+ "Launcher",
+ "NotificationPopup",
+ "OSD",
+ "Powermenu",
+ "Settings",
+]
diff --git a/ignis/modules/__pycache__/__init__.cpython-313.pyc b/ignis/modules/__pycache__/__init__.cpython-313.pyc
index 6b67587..8307df2 100644
Binary files a/ignis/modules/__pycache__/__init__.cpython-313.pyc and b/ignis/modules/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/modules/bar/__init__.py b/ignis/modules/bar/__init__.py
index a167d8f..53b82f9 100644
--- a/ignis/modules/bar/__init__.py
+++ b/ignis/modules/bar/__init__.py
@@ -1,25 +1,3 @@
-from ignis.widgets import Widget
-from .workspaces import workspaces
-from .kb_layout import kb_layout
-from .clock import clock
-from .pinned_apps import pinned_apps
-from .tray import tray
-from .battery import battery_widget
-# from .wallpaper_engine import wallpaper_engine_widget
+from .bar import Bar
-def bar(monitor: int) -> Widget.Window:
- return Widget.Window(
- anchor=["left", "top", "right"],
- exclusivity="exclusive",
- monitor=monitor,
- namespace=f"ignis_BAR_{monitor}",
- layer="top",
- kb_mode="none",
- child=Widget.CenterBox(
- css_classes=["bar-widget"],
- start_widget=Widget.Box(child=[workspaces()]),
- center_widget=Widget.Box(child=[pinned_apps()]),
- end_widget=Widget.Box(child=[tray(), kb_layout(), battery_widget(), clock(monitor)]),
- ), # wallpaper_engine_widget(),
- css_classes=["unset"],
- )
+__all__ = ["Bar"]
diff --git a/ignis/modules/bar/__pycache__/__init__.cpython-313.pyc b/ignis/modules/bar/__pycache__/__init__.cpython-313.pyc
index a18b492..7191f43 100644
Binary files a/ignis/modules/bar/__pycache__/__init__.cpython-313.pyc and b/ignis/modules/bar/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/modules/bar/__pycache__/bar.cpython-313.pyc b/ignis/modules/bar/__pycache__/bar.cpython-313.pyc
new file mode 100644
index 0000000..975f834
Binary files /dev/null and b/ignis/modules/bar/__pycache__/bar.cpython-313.pyc differ
diff --git a/ignis/modules/bar/__pycache__/indicator_icon.cpython-313.pyc b/ignis/modules/bar/__pycache__/indicator_icon.cpython-313.pyc
new file mode 100644
index 0000000..07f372e
Binary files /dev/null and b/ignis/modules/bar/__pycache__/indicator_icon.cpython-313.pyc differ
diff --git a/ignis/modules/bar/bar.py b/ignis/modules/bar/bar.py
new file mode 100644
index 0000000..ebaf016
--- /dev/null
+++ b/ignis/modules/bar/bar.py
@@ -0,0 +1,25 @@
+from ignis.widgets import Widget
+from .widgets import StatusPill, Tray, KeyboardLayout, Battery, Apps, Workspaces
+
+
+class Bar(Widget.Window):
+ __gtype_name__ = "Bar"
+
+ def __init__(self, monitor: int):
+ super().__init__(
+ anchor=["left", "top", "right"],
+ exclusivity="exclusive",
+ monitor=monitor,
+ namespace=f"ignis_BAR_{monitor}",
+ layer="top",
+ kb_mode="none",
+ child=Widget.CenterBox(
+ css_classes=["bar-widget"],
+ start_widget=Widget.Box(child=[Workspaces()]),
+ center_widget=Widget.Box(child=[Apps()]),
+ end_widget=Widget.Box(
+ child=[Tray(), KeyboardLayout(), Battery(), StatusPill(monitor)]
+ ),
+ ),
+ css_classes=["unset"],
+ )
diff --git a/ignis/modules/bar/indicator_icon.py b/ignis/modules/bar/indicator_icon.py
new file mode 100644
index 0000000..6106f22
--- /dev/null
+++ b/ignis/modules/bar/indicator_icon.py
@@ -0,0 +1,31 @@
+from ignis.widgets import Widget
+from ignis.services.network import Ethernet, Wifi
+
+
+class IndicatorIcon(Widget.Icon):
+ def __init__(self, css_classes: list[str] = [], **kwargs):
+ super().__init__(
+ style="margin-right: 0.5rem;", css_classes=["unset"] + css_classes, **kwargs
+ )
+
+
+class NetworkIndicatorIcon(IndicatorIcon):
+ def __init__(
+ self, device_type: Ethernet | Wifi, other_device_type: Wifi | Ethernet
+ ):
+ self._device_type = device_type
+ self._other_device_type = other_device_type
+
+ super().__init__(icon_name=device_type.bind("icon-name"))
+
+ for binding in (
+ device_type.bind("devices", self.__check_visibility),
+ other_device_type.bind("is_connected", self.__check_visibility),
+ device_type.bind("is_connected", self.__check_visibility),
+ ):
+ self.visible = binding # type: ignore
+
+ def __check_visibility(self, *args) -> bool:
+ return len(self._device_type.devices) > 0 and (
+ not self._other_device_type.is_connected or self._device_type.is_connected
+ )
diff --git a/ignis/modules/bar/widgets/__init__.py b/ignis/modules/bar/widgets/__init__.py
new file mode 100644
index 0000000..96d6383
--- /dev/null
+++ b/ignis/modules/bar/widgets/__init__.py
@@ -0,0 +1,8 @@
+from .pill import StatusPill
+from .tray import Tray
+from .kb_layout import KeyboardLayout
+from .battery import Battery
+from .apps import Apps
+from .workspaces import Workspaces
+
+__all__ = ["StatusPill", "Tray", "KeyboardLayout", "Battery", "Apps", "Workspaces"]
diff --git a/ignis/modules/bar/widgets/__pycache__/__init__.cpython-313.pyc b/ignis/modules/bar/widgets/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..d836d2f
Binary files /dev/null and b/ignis/modules/bar/widgets/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/modules/bar/widgets/__pycache__/apps.cpython-313.pyc b/ignis/modules/bar/widgets/__pycache__/apps.cpython-313.pyc
new file mode 100644
index 0000000..fbc8f3b
Binary files /dev/null and b/ignis/modules/bar/widgets/__pycache__/apps.cpython-313.pyc differ
diff --git a/ignis/modules/bar/widgets/__pycache__/battery.cpython-313.pyc b/ignis/modules/bar/widgets/__pycache__/battery.cpython-313.pyc
new file mode 100644
index 0000000..b058a9a
Binary files /dev/null and b/ignis/modules/bar/widgets/__pycache__/battery.cpython-313.pyc differ
diff --git a/ignis/modules/bar/widgets/__pycache__/kb_layout.cpython-313.pyc b/ignis/modules/bar/widgets/__pycache__/kb_layout.cpython-313.pyc
new file mode 100644
index 0000000..85ad0eb
Binary files /dev/null and b/ignis/modules/bar/widgets/__pycache__/kb_layout.cpython-313.pyc differ
diff --git a/ignis/modules/bar/widgets/__pycache__/pill.cpython-313.pyc b/ignis/modules/bar/widgets/__pycache__/pill.cpython-313.pyc
new file mode 100644
index 0000000..5ceddc1
Binary files /dev/null and b/ignis/modules/bar/widgets/__pycache__/pill.cpython-313.pyc differ
diff --git a/ignis/modules/bar/widgets/__pycache__/tray.cpython-313.pyc b/ignis/modules/bar/widgets/__pycache__/tray.cpython-313.pyc
new file mode 100644
index 0000000..db464cd
Binary files /dev/null and b/ignis/modules/bar/widgets/__pycache__/tray.cpython-313.pyc differ
diff --git a/ignis/modules/bar/widgets/__pycache__/workspaces.cpython-313.pyc b/ignis/modules/bar/widgets/__pycache__/workspaces.cpython-313.pyc
new file mode 100644
index 0000000..21e61e2
Binary files /dev/null and b/ignis/modules/bar/widgets/__pycache__/workspaces.cpython-313.pyc differ
diff --git a/ignis/modules/bar/widgets/apps.py b/ignis/modules/bar/widgets/apps.py
new file mode 100644
index 0000000..f939f94
--- /dev/null
+++ b/ignis/modules/bar/widgets/apps.py
@@ -0,0 +1,52 @@
+from ignis.widgets import Widget
+from ignis.app import IgnisApp
+from ignis.services.applications import ApplicationsService, Application
+
+applications = ApplicationsService.get_default()
+app = IgnisApp.get_default()
+
+TERMINAL_FORMAT = "kitty %command%"
+
+
+class AppItem(Widget.Button):
+ def __init__(self, app: Application):
+ menu = Widget.PopoverMenu(
+ items=[
+ Widget.MenuItem(label="Launch", on_activate=lambda x: app.launch()),
+ Widget.Separator(),
+ ]
+ + [
+ Widget.MenuItem(
+ label=i.name, on_activate=lambda x, action=i: action.launch()
+ )
+ for i in app.actions
+ ]
+ + [
+ Widget.Separator(),
+ Widget.MenuItem(label="Unpin", on_activate=lambda x: app.unpin()),
+ ]
+ )
+
+ super().__init__(
+ child=Widget.Box(child=[Widget.Icon(image=app.icon, pixel_size=32), menu]),
+ on_click=lambda x: app.launch(terminal_format=TERMINAL_FORMAT),
+ on_right_click=lambda x: menu.popup(),
+ css_classes=["pinned-app", "unset"],
+ )
+
+
+class Apps(Widget.Box):
+ def __init__(self):
+ super().__init__(
+ child=applications.bind(
+ "pinned",
+ transform=lambda value: [AppItem(app) for app in value]
+ + [
+ Widget.Button(
+ child=Widget.Icon(image="start-here-symbolic", pixel_size=32),
+ on_click=lambda x: app.toggle_window("ignis_LAUNCHER"),
+ css_classes=["pinned-app", "unset"],
+ )
+ ],
+ )
+ )
diff --git a/ignis/modules/bar/widgets/battery.py b/ignis/modules/bar/widgets/battery.py
new file mode 100644
index 0000000..658c7f6
--- /dev/null
+++ b/ignis/modules/bar/widgets/battery.py
@@ -0,0 +1,37 @@
+from ignis.widgets import Widget
+from ignis.services.upower import UPowerService, UPowerDevice
+
+upower = UPowerService.get_default()
+
+
+class BatteryItem(Widget.Box):
+ def __init__(self, device: UPowerDevice):
+ super().__init__(
+ css_classes=["battery-item"],
+ setup=lambda self: device.connect("removed", lambda x: self.unparent()),
+ child=[
+ Widget.Icon(
+ icon_name=device.bind("icon_name"), css_classes=["battery-icon"]
+ ),
+ Widget.Label(
+ label=device.bind("percent", lambda x: f"{int(x)}%"),
+ css_classes=["battery-percent"],
+ ),
+ Widget.Scale(
+ min=0,
+ max=100,
+ value=device.bind("percent"),
+ sensitive=False,
+ css_classes=["battery-scale"],
+ ),
+ ],
+ )
+
+
+class Battery(Widget.Box):
+ def __init__(self):
+ super().__init__(
+ setup=lambda self: upower.connect(
+ "battery-added", lambda x, device: self.append(BatteryItem(device))
+ ),
+ )
diff --git a/ignis/modules/bar/widgets/kb_layout.py b/ignis/modules/bar/widgets/kb_layout.py
new file mode 100644
index 0000000..cb451f1
--- /dev/null
+++ b/ignis/modules/bar/widgets/kb_layout.py
@@ -0,0 +1,18 @@
+from ignis.widgets import Widget
+from ignis.services.hyprland import HyprlandService
+
+hyprland = HyprlandService.get_default()
+
+
+class KeyboardLayout(Widget.Button):
+ def __init__(self):
+ super().__init__(
+ css_classes=["kb-layout", "unset"],
+ on_click=lambda x: hyprland.main_keyboard.switch_layout("1"),
+ visible=hyprland.is_available,
+ child=Widget.Label(
+ label=hyprland.main_keyboard.bind(
+ "active_keymap", transform=lambda value: value[:2].lower()
+ )
+ ),
+ )
diff --git a/ignis/modules/bar/widgets/pill.py b/ignis/modules/bar/widgets/pill.py
new file mode 100644
index 0000000..9ff4d5d
--- /dev/null
+++ b/ignis/modules/bar/widgets/pill.py
@@ -0,0 +1,111 @@
+import datetime
+from ignis.widgets import Widget
+from ignis.app import IgnisApp
+from ignis.utils import Utils
+from ignis.variable import Variable
+from ignis.services.network import NetworkService
+from ignis.services.notifications import NotificationService
+from ignis.services.recorder import RecorderService
+from ignis.services.audio import AudioService
+from ..indicator_icon import IndicatorIcon, NetworkIndicatorIcon
+from ignis.options import options
+
+network = NetworkService.get_default()
+notifications = NotificationService.get_default()
+recorder = RecorderService.get_default()
+audio = AudioService.get_default()
+
+app = IgnisApp.get_default()
+
+current_time = Variable(
+ value=Utils.Poll(1000, lambda x: datetime.datetime.now().strftime("%H:%M:%S")).bind(
+ "output"
+ )
+)
+
+
+class WifiIcon(NetworkIndicatorIcon):
+ def __init__(self):
+ super().__init__(device_type=network.wifi, other_device_type=network.ethernet)
+
+
+class EthernetIcon(NetworkIndicatorIcon):
+ def __init__(self):
+ super().__init__(device_type=network.ethernet, other_device_type=network.wifi)
+
+
+class VpnIcon(IndicatorIcon):
+ def __init__(self):
+ super().__init__(
+ image=network.vpn.bind("icon_name"),
+ visible=network.vpn.bind("is_connected"),
+ )
+
+
+class DNDIcon(IndicatorIcon):
+ def __init__(self):
+ super().__init__(
+ image="notification-disabled-symbolic",
+ visible=options.notifications.bind("dnd"),
+ )
+
+
+class RecorderIcon(IndicatorIcon):
+ def __init__(self):
+ super().__init__(
+ image="media-record-symbolic",
+ css_classes=["record-indicator"],
+ setup=lambda self: recorder.connect(
+ "notify::is-paused", self.__update_css_class
+ ),
+ visible=recorder.bind("active"),
+ )
+
+ def __update_css_class(self, *args) -> None:
+ if recorder.is_paused:
+ self.remove_css_class("active")
+ else:
+ self.add_css_class("active")
+
+
+class VolumeIcon(IndicatorIcon):
+ def __init__(self):
+ super().__init__(
+ image=audio.speaker.bind("icon_name"),
+ )
+
+
+class StatusPill(Widget.Button):
+ def __init__(self, monitor: int):
+ self._monitor = monitor
+ self._window: Widget.Window = app.get_window("ignis_CONTROL_CENTER") # type: ignore
+
+ super().__init__(
+ child=Widget.Box(
+ child=[
+ RecorderIcon(),
+ WifiIcon(),
+ EthernetIcon(),
+ VpnIcon(),
+ VolumeIcon(),
+ DNDIcon(),
+ Widget.Label(
+ label=current_time.bind("value"),
+ ),
+ ]
+ ),
+ css_classes=self._window.bind(
+ "visible",
+ lambda value: ["clock", "unset", "active"]
+ if value
+ else ["clock", "unset"],
+ ),
+ on_click=self.__on_click,
+ )
+
+ def __on_click(self, x) -> None:
+ if self._window.monitor == self._monitor:
+ self._window.visible = not self._window.visible
+ else:
+ self._window.set_monitor(self._monitor)
+ self._window.visible = True
diff --git a/ignis/modules/bar/widgets/tray.py b/ignis/modules/bar/widgets/tray.py
new file mode 100644
index 0000000..6b5572c
--- /dev/null
+++ b/ignis/modules/bar/widgets/tray.py
@@ -0,0 +1,42 @@
+import asyncio
+from ignis.widgets import Widget
+from ignis.services.system_tray import SystemTrayService, SystemTrayItem
+
+system_tray = SystemTrayService.get_default()
+
+
+class TrayItem(Widget.Button):
+ __gtype_name__ = "TrayItem"
+
+ def __init__(self, item: SystemTrayItem):
+ if item.menu:
+ menu = item.menu.copy()
+ else:
+ menu = None
+
+ super().__init__(
+ child=Widget.Box(
+ child=[
+ Widget.Icon(image=item.bind("icon"), pixel_size=24),
+ menu,
+ ]
+ ),
+ tooltip_text=item.bind("tooltip"),
+ on_click=lambda x: asyncio.create_task(item.activate_async()),
+ setup=lambda self: item.connect("removed", lambda x: self.unparent()),
+ on_right_click=lambda x: menu.popup() if menu else None,
+ css_classes=["tray-item", "unset"],
+ )
+
+
+class Tray(Widget.Box):
+ __gtype_name__ = "Tray"
+
+ def __init__(self):
+ super().__init__(
+ css_classes=["tray"],
+ setup=lambda self: system_tray.connect(
+ "added", lambda x, item: self.append(TrayItem(item))
+ ),
+ spacing=10,
+ )
diff --git a/ignis/modules/bar/widgets/workspaces.py b/ignis/modules/bar/widgets/workspaces.py
new file mode 100644
index 0000000..4cb479e
--- /dev/null
+++ b/ignis/modules/bar/widgets/workspaces.py
@@ -0,0 +1,49 @@
+from ignis.widgets import Widget
+from ignis.services.hyprland import HyprlandService, HyprlandWorkspace
+
+hyprland = HyprlandService.get_default()
+
+
+class WorkspaceButton(Widget.Button):
+ def __init__(self, workspace: HyprlandWorkspace) -> None:
+ super().__init__(
+ css_classes=["workspace", "unset"],
+ on_click=lambda x: workspace.switch_to(),
+ halign="start",
+ valign="center",
+ )
+ if workspace.id == hyprland.active_workspace.id:
+ self.add_css_class("active")
+
+
+def scroll_workspaces(direction: str) -> None:
+ current = hyprland.active_workspace.id
+ if direction == "up":
+ target = current - 1
+ hyprland.switch_to_workspace(target)
+ else:
+ target = current + 1
+ if target == 11:
+ return
+ hyprland.switch_to_workspace(target)
+
+
+class Workspaces(Widget.Box):
+ def __init__(self):
+ if hyprland.is_available:
+ child = [
+ Widget.EventBox(
+ on_scroll_up=lambda x: scroll_workspaces("up"),
+ on_scroll_down=lambda x: scroll_workspaces("down"),
+ css_classes=["workspaces"],
+ child=hyprland.bind_many(
+ ["workspaces", "active_workspace"],
+ transform=lambda workspaces, *_: [
+ WorkspaceButton(i) for i in workspaces
+ ],
+ ),
+ )
+ ]
+ else:
+ child = []
+ super().__init__(child=child)
diff --git a/ignis/modules/control_center/__init__.py b/ignis/modules/control_center/__init__.py
index 285bb00..15bd497 100644
--- a/ignis/modules/control_center/__init__.py
+++ b/ignis/modules/control_center/__init__.py
@@ -1,62 +1,3 @@
-from ignis.widgets import Widget
-from ignis.app import IgnisApp
-from .volume import volume_control
-from .quick_settings import quick_settings
-from .user import user
-from .media import media
-from .notification_center import notification_center
-from .brightness import brightness_slider
+from .control_center import ControlCenter
-app = IgnisApp.get_default()
-
-
-def control_center_widget() -> Widget.Box:
- return Widget.Box(
- vertical=True,
- css_classes=["control-center"],
- child=[
- Widget.Box(
- vertical=True,
- css_classes=["control-center-widget"],
- child=[
- quick_settings(),
- volume_control(),
- brightness_slider(),
- user(),
- media(),
- ],
- ),
- notification_center(),
- ],
- )
-
-
-def control_center() -> Widget.RevealerWindow:
- revealer = Widget.Revealer(
- transition_type="slide_left",
- child=control_center_widget(),
- transition_duration=300,
- reveal_child=True,
- )
- box = Widget.Box(
- child=[
- Widget.Button(
- vexpand=True,
- hexpand=True,
- css_classes=["unset"],
- on_click=lambda x: app.close_window("ignis_CONTROL_CENTER"),
- ),
- revealer,
- ],
- )
- return Widget.RevealerWindow(
- visible=False,
- popup=True,
- kb_mode="on_demand",
- layer="top",
- css_classes=["unset"],
- anchor=["top", "right", "bottom", "left"],
- namespace="ignis_CONTROL_CENTER",
- child=box,
- revealer=revealer,
- )
+__all__ = ["ControlCenter"]
diff --git a/ignis/modules/control_center/__pycache__/__init__.cpython-313.pyc b/ignis/modules/control_center/__pycache__/__init__.cpython-313.pyc
index a14a45b..78b2fe4 100644
Binary files a/ignis/modules/control_center/__pycache__/__init__.cpython-313.pyc and b/ignis/modules/control_center/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/modules/control_center/__pycache__/control_center.cpython-313.pyc b/ignis/modules/control_center/__pycache__/control_center.cpython-313.pyc
new file mode 100644
index 0000000..685c0d4
Binary files /dev/null and b/ignis/modules/control_center/__pycache__/control_center.cpython-313.pyc differ
diff --git a/ignis/modules/control_center/__pycache__/menu.cpython-313.pyc b/ignis/modules/control_center/__pycache__/menu.cpython-313.pyc
new file mode 100644
index 0000000..16c2143
Binary files /dev/null and b/ignis/modules/control_center/__pycache__/menu.cpython-313.pyc differ
diff --git a/ignis/modules/control_center/__pycache__/qs_button.cpython-313.pyc b/ignis/modules/control_center/__pycache__/qs_button.cpython-313.pyc
index 5af9afd..68669cb 100644
Binary files a/ignis/modules/control_center/__pycache__/qs_button.cpython-313.pyc and b/ignis/modules/control_center/__pycache__/qs_button.cpython-313.pyc differ
diff --git a/ignis/modules/control_center/control_center.py b/ignis/modules/control_center/control_center.py
new file mode 100644
index 0000000..0915bed
--- /dev/null
+++ b/ignis/modules/control_center/control_center.py
@@ -0,0 +1,66 @@
+from ignis.widgets import Widget
+from ignis.app import IgnisApp
+from .widgets import (
+ QuickSettings,
+ Brightness,
+ VolumeSlider,
+ User,
+ Media,
+ NotificationCenter,
+)
+from .menu import opened_menu
+
+app = IgnisApp.get_default()
+
+
+class ControlCenter(Widget.RevealerWindow):
+ def __init__(self):
+ revealer = Widget.Revealer(
+ transition_type="slide_left",
+ child=Widget.Box(
+ vertical=True,
+ css_classes=["control-center"],
+ child=[
+ Widget.Box(
+ vertical=True,
+ css_classes=["control-center-widget"],
+ child=[
+ QuickSettings(),
+ VolumeSlider("speaker"),
+ VolumeSlider("microphone"),
+ Brightness(),
+ User(),
+ Media(),
+ ],
+ ),
+ NotificationCenter(),
+ ],
+ ),
+ transition_duration=300,
+ reveal_child=True,
+ )
+
+ super().__init__(
+ visible=False,
+ popup=True,
+ kb_mode="on_demand",
+ layer="top",
+ css_classes=["unset"],
+ anchor=["top", "right", "bottom", "left"],
+ namespace="ignis_CONTROL_CENTER",
+ child=Widget.Box(
+ child=[
+ Widget.Button(
+ vexpand=True,
+ hexpand=True,
+ css_classes=["unset"],
+ on_click=lambda x: app.close_window("ignis_CONTROL_CENTER"),
+ ),
+ revealer,
+ ],
+ ),
+ setup=lambda self: self.connect(
+ "notify::visible", lambda x, y: opened_menu.set_value("")
+ ),
+ revealer=revealer,
+ )
diff --git a/ignis/modules/control_center/menu.py b/ignis/modules/control_center/menu.py
new file mode 100644
index 0000000..6e71480
--- /dev/null
+++ b/ignis/modules/control_center/menu.py
@@ -0,0 +1,34 @@
+from gi.repository import GObject # type: ignore
+from ignis.widgets import Widget
+from ignis.variable import Variable
+from ignis.base_widget import BaseWidget
+
+opened_menu = Variable()
+
+
+class Menu(Widget.Revealer):
+ def __init__(self, name: str, child: list[BaseWidget], **kwargs):
+ self._name = name
+ self._box = Widget.Box(
+ vertical=True,
+ css_classes=["control-center-menu"],
+ child=child,
+ )
+
+ super().__init__(
+ transition_type="slide_down",
+ transition_duration=300,
+ reveal_child=opened_menu.bind("value", lambda value: value == self._name),
+ child=self._box,
+ **kwargs,
+ )
+
+ def toggle(self) -> None:
+ if self.reveal_child:
+ opened_menu.value = ""
+ else:
+ opened_menu.value = self._name
+
+ @GObject.Property
+ def box(self) -> Widget.Box:
+ return self._box
diff --git a/ignis/modules/control_center/qs_button.py b/ignis/modules/control_center/qs_button.py
index 381ddf5..e049c53 100644
--- a/ignis/modules/control_center/qs_button.py
+++ b/ignis/modules/control_center/qs_button.py
@@ -2,6 +2,7 @@ from ignis.widgets import Widget
from gi.repository import GObject # type: ignore
from typing import Callable
from ignis.gobject import Binding
+from .menu import Menu
class QSButton(Widget.Button):
@@ -11,13 +12,13 @@ class QSButton(Widget.Button):
icon_name: str | Binding,
on_activate: Callable | None = None,
on_deactivate: Callable | None = None,
- content: Widget.Revealer | None = None,
+ menu: Menu | None = None,
**kwargs,
):
self.on_activate = on_activate
self.on_deactivate = on_deactivate
self._active = False
- self._content = content
+ self._menu = menu
super().__init__(
child=Widget.Box(
child=[
@@ -27,9 +28,9 @@ class QSButton(Widget.Button):
halign="end",
hexpand=True,
pixel_size=20,
- rotated=content.bind("reveal_child"),
+ rotated=menu.bind("reveal_child"),
)
- if content
+ if menu
else None,
]
),
@@ -60,5 +61,5 @@ class QSButton(Widget.Button):
self.remove_css_class("active")
@GObject.Property
- def content(self) -> Widget.Revealer | None:
- return self._content
+ def menu(self) -> Menu | None:
+ return self._menu
diff --git a/ignis/modules/control_center/widgets/__init__.py b/ignis/modules/control_center/widgets/__init__.py
new file mode 100644
index 0000000..399ce63
--- /dev/null
+++ b/ignis/modules/control_center/widgets/__init__.py
@@ -0,0 +1,15 @@
+from .quick_settings import QuickSettings
+from .brightness import Brightness
+from .volume import VolumeSlider
+from .user import User
+from .media import Media
+from .notification_center import NotificationCenter
+
+__all__ = [
+ "QuickSettings",
+ "Brightness",
+ "VolumeSlider",
+ "User",
+ "Media",
+ "NotificationCenter",
+]
diff --git a/ignis/modules/control_center/widgets/__pycache__/__init__.cpython-313.pyc b/ignis/modules/control_center/widgets/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..9b2e3ec
Binary files /dev/null and b/ignis/modules/control_center/widgets/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/modules/control_center/widgets/__pycache__/brightness.cpython-313.pyc b/ignis/modules/control_center/widgets/__pycache__/brightness.cpython-313.pyc
new file mode 100644
index 0000000..7573612
Binary files /dev/null and b/ignis/modules/control_center/widgets/__pycache__/brightness.cpython-313.pyc differ
diff --git a/ignis/modules/control_center/widgets/__pycache__/media.cpython-313.pyc b/ignis/modules/control_center/widgets/__pycache__/media.cpython-313.pyc
new file mode 100644
index 0000000..aa707be
Binary files /dev/null and b/ignis/modules/control_center/widgets/__pycache__/media.cpython-313.pyc differ
diff --git a/ignis/modules/control_center/widgets/__pycache__/notification_center.cpython-313.pyc b/ignis/modules/control_center/widgets/__pycache__/notification_center.cpython-313.pyc
new file mode 100644
index 0000000..207078e
Binary files /dev/null and b/ignis/modules/control_center/widgets/__pycache__/notification_center.cpython-313.pyc differ
diff --git a/ignis/modules/control_center/widgets/__pycache__/user.cpython-313.pyc b/ignis/modules/control_center/widgets/__pycache__/user.cpython-313.pyc
new file mode 100644
index 0000000..9c9505f
Binary files /dev/null and b/ignis/modules/control_center/widgets/__pycache__/user.cpython-313.pyc differ
diff --git a/ignis/modules/control_center/widgets/__pycache__/volume.cpython-313.pyc b/ignis/modules/control_center/widgets/__pycache__/volume.cpython-313.pyc
new file mode 100644
index 0000000..84427dd
Binary files /dev/null and b/ignis/modules/control_center/widgets/__pycache__/volume.cpython-313.pyc differ
diff --git a/ignis/modules/control_center/widgets/brightness.py b/ignis/modules/control_center/widgets/brightness.py
new file mode 100644
index 0000000..8f1bc3c
--- /dev/null
+++ b/ignis/modules/control_center/widgets/brightness.py
@@ -0,0 +1,29 @@
+import asyncio
+from ignis.services.backlight import BacklightService
+from ignis.widgets import Widget
+
+backlight = BacklightService.get_default()
+
+
+class Brightness(Widget.Box):
+ def __init__(self):
+ super().__init__(
+ visible=backlight.bind("available"),
+ hexpand=True,
+ style="margin-top: 0.25rem;",
+ child=[
+ Widget.Icon(
+ image="display-brightness-symbolic",
+ css_classes=["material-slider-icon"],
+ pixel_size=18,
+ ),
+ Widget.Scale(
+ min=0,
+ max=backlight.max_brightness,
+ hexpand=True,
+ value=backlight.bind("brightness"),
+ css_classes=["material-slider"],
+ on_change=lambda x: asyncio.create_task(backlight.set_brightness_async(x.value)),
+ ),
+ ],
+ )
diff --git a/ignis/modules/control_center/widgets/media.py b/ignis/modules/control_center/widgets/media.py
new file mode 100644
index 0000000..feab6bc
--- /dev/null
+++ b/ignis/modules/control_center/widgets/media.py
@@ -0,0 +1,211 @@
+import os
+import ignis
+import asyncio
+from ignis.widgets import Widget
+from ignis.services.mpris import MprisService, MprisPlayer
+from ignis.utils import Utils
+from services.material import MaterialService
+from ignis.app import IgnisApp
+from ignis.exceptions import StylePathNotFoundError
+
+
+mpris = MprisService.get_default()
+app = IgnisApp.get_default()
+material = MaterialService.get_default()
+
+MEDIA_TEMPLATE = Utils.get_current_dir() + "/media.scss"
+MEDIA_SCSS_CACHE_DIR = ignis.CACHE_DIR + "/media" # type: ignore
+MEDIA_ART_FALLBACK = Utils.get_current_dir() + "/../../../misc/media-art-fallback.png"
+os.makedirs(MEDIA_SCSS_CACHE_DIR, exist_ok=True)
+
+
+PLAYER_ICONS = {
+ "spotify": "spotify-symbolic",
+ "firefox": "firefox-browser-symbolic",
+ "chrome": "chrome-symbolic",
+ None: "folder-music-symbolic",
+}
+
+
+class Player(Widget.Revealer):
+ def __init__(self, player: MprisPlayer) -> None:
+ self._player = player
+ self._colors_path = f"{MEDIA_SCSS_CACHE_DIR}/{self._player.desktop_entry}.scss"
+ player.connect("closed", lambda x: self.destroy())
+ player.connect("notify::art-url", lambda x, y: self.load_colors())
+ self.load_colors()
+
+ super().__init__(
+ transition_type="slide_down",
+ reveal_child=False,
+ css_classes=[self.get_css("media")],
+ child=Widget.Overlay(
+ child=Widget.Box(css_classes=[self.get_css("media-image")]),
+ overlays=[
+ Widget.Box(
+ hexpand=True,
+ vexpand=True,
+ css_classes=[self.get_css("media-image-gradient")],
+ ),
+ Widget.Icon(
+ icon_name=self.get_player_icon(),
+ pixel_size=22,
+ halign="start",
+ valign="start",
+ css_classes=[self.get_css("media-player-icon")],
+ ),
+ Widget.Box(
+ vertical=True,
+ hexpand=True,
+ css_classes=[self.get_css("media-content")],
+ child=[
+ Widget.Box(
+ vexpand=True,
+ valign="center",
+ child=[
+ Widget.Box(
+ hexpand=True,
+ vertical=True,
+ child=[
+ Widget.Label(
+ ellipsize="end",
+ label=player.bind("title"),
+ max_width_chars=30,
+ halign="start",
+ css_classes=[
+ self.get_css("media-title")
+ ],
+ ),
+ Widget.Label(
+ label=player.bind("artist"),
+ max_width_chars=30,
+ ellipsize="end",
+ halign="start",
+ css_classes=[
+ self.get_css("media-artist")
+ ],
+ ),
+ ],
+ ),
+ Widget.Button(
+ child=Widget.Icon(
+ image=player.bind(
+ "playback_status",
+ lambda value: "media-playback-pause-symbolic"
+ if value == "Playing"
+ else "media-playback-start-symbolic",
+ ),
+ pixel_size=18,
+ ),
+ on_click=lambda x: asyncio.create_task(player.play_pause_async()),
+ visible=player.bind("can_play"),
+ css_classes=player.bind(
+ "playback_status",
+ lambda value: [
+ self.get_css("media-playback-button"),
+ "playing",
+ ]
+ if value == "Playing"
+ else [
+ self.get_css("media-playback-button"),
+ "paused",
+ ],
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ Widget.Box(
+ vexpand=True,
+ valign="end",
+ style="padding: 1rem;",
+ child=[
+ Widget.Scale(
+ value=player.bind("position"),
+ max=player.bind("length"),
+ hexpand=True,
+ css_classes=[self.get_css("media-scale")],
+ on_change=lambda x: asyncio.create_task(self._player.set_position_async(x.value)),
+ visible=player.bind(
+ "position", lambda value: value != -1
+ ),
+ ),
+ Widget.Button(
+ child=Widget.Icon(
+ image="media-skip-backward-symbolic",
+ pixel_size=20,
+ ),
+ css_classes=[self.get_css("media-skip-button")],
+ on_click=lambda x: asyncio.create_task(player.previous_async()),
+ visible=player.bind("can_go_previous"),
+ style="margin-left: 1rem;",
+ ),
+ Widget.Button(
+ child=Widget.Icon(
+ image="media-skip-forward-symbolic",
+ pixel_size=20,
+ ),
+ css_classes=[self.get_css("media-skip-button")],
+ on_click=lambda x: asyncio.create_task(player.next_async()),
+ visible=player.bind("can_go_next"),
+ style="margin-left: 1rem;",
+ ),
+ ],
+ ),
+ ],
+ ),
+ )
+
+ def get_player_icon(self) -> str:
+ if self._player.desktop_entry == "firefox":
+ return PLAYER_ICONS["firefox"]
+ elif self._player.desktop_entry == "spotify":
+ return PLAYER_ICONS["spotify"]
+ elif self._player.track_id is not None:
+ if "chromium" in self._player.track_id or "chrome" in self._player.track_id:
+ return PLAYER_ICONS["chrome"]
+
+ return PLAYER_ICONS[None]
+
+ def destroy(self) -> None:
+ self.set_reveal_child(False)
+ Utils.Timeout(self.transition_duration, super().unparent)
+
+ def get_css(self, class_name: str) -> str:
+ return f"{class_name}-{self._player.desktop_entry}"
+
+ def load_colors(self) -> None:
+ if not self._player.art_url:
+ art_url = MEDIA_ART_FALLBACK
+ else:
+ art_url = self._player.art_url
+
+ try:
+ app.remove_css(self._colors_path)
+ except StylePathNotFoundError:
+ pass
+
+ colors = material.get_colors_from_img(art_url, True)
+ colors["art_url"] = art_url
+ colors["desktop_entry"] = self._player.desktop_entry
+ material.render_template(
+ colors, input_file=MEDIA_TEMPLATE, output_file=self._colors_path
+ )
+ app.apply_css(self._colors_path)
+
+
+class Media(Widget.Box):
+ def __init__(self):
+ super().__init__(
+ vertical=True,
+ setup=lambda self: mpris.connect(
+ "player_added", lambda x, player: self.__add_player(player)
+ ),
+ css_classes=["rec-unset"],
+ )
+
+ def __add_player(self, obj: MprisPlayer) -> None:
+ player = Player(obj)
+ self.append(player)
+ player.set_reveal_child(True)
diff --git a/ignis/modules/control_center/widgets/media.scss b/ignis/modules/control_center/widgets/media.scss
new file mode 100644
index 0000000..f4a10ae
--- /dev/null
+++ b/ignis/modules/control_center/widgets/media.scss
@@ -0,0 +1,84 @@
+// JINJA2 TEMPLATE
+// WARNING: Do not import this file in style.scss
+
+.media-{{ desktop_entry }} {
+ margin-top: 1rem;
+}
+
+
+.media-image-{{ desktop_entry }} {
+ background-image: url('file:///{{ art_url }}');
+ background-repeat: no-repeat;
+ background-size: cover;
+ background-position: center;
+ min-height: 11rem;
+ border-radius: 1.5rem;
+}
+
+.media-image-gradient-{{ desktop_entry }} {
+ border-radius: 1.3rem;
+ background: linear-gradient(90deg, rgba({{ onPrimary }}, 0.9) 0%, rgba({{ onPrimary }}, 0.4) 50%, rgba({{ onPrimary }}, 0.9) 100%);
+}
+
+.media-scale-{{ desktop_entry }} trough,
+.media-scale-{{ desktop_entry }} trough highlight {
+ background-color: rgba({{ onSurface }}, 0.2);
+ border-radius: 1rem;
+ min-height: 0.2rem;
+}
+
+
+.media-scale-{{ desktop_entry }} trough highlight {
+ background-color: {{ onSurface }};
+}
+
+.media-scale-{{ desktop_entry }} slider {
+ background-color: {{ onSurface }};
+ padding: 0.5rem 0.1rem;
+ margin: -0.5rem -0.1rem;
+}
+
+.media-title-{{ desktop_entry }} {
+ color: {{ onSurface }};
+}
+
+.media-artist-{{ desktop_entry }} {
+ color: {{ onSurfaceVariant }};
+ font-weight: normal;
+}
+
+
+.media-content-{{ desktop_entry }} {
+ padding: 1rem;
+}
+
+.media-playback-button-{{ desktop_entry }} {
+ background-color: {{ primary }};
+ color: {{ onPrimary }};
+ border-radius: 4rem;
+ min-width: 2.7rem;
+ min-height: 2.7rem;
+ transition: 0.3s;
+
+ &:hover {
+ background-color: lighten({{ primary }}, 5%);
+ }
+
+ &.playing {
+ border-radius: 1rem;
+ }
+}
+
+.media-player-icon-{{ desktop_entry }} {
+ color: {{ primary }};
+ padding: 1rem;
+ padding-top: 1.2rem;
+}
+
+.media-skip-button-{{ desktop_entry }} {
+ transition: 0.3s;
+ color: {{ onSurface }};
+ &:hover {
+ color: {{ primary }};
+ }
+}
\ No newline at end of file
diff --git a/ignis/modules/control_center/widgets/notification_center.py b/ignis/modules/control_center/widgets/notification_center.py
new file mode 100644
index 0000000..962ef6c
--- /dev/null
+++ b/ignis/modules/control_center/widgets/notification_center.py
@@ -0,0 +1,109 @@
+from ignis.widgets import Widget
+from ignis.services.notifications import Notification, NotificationService
+from ignis.utils import Utils
+from gi.repository import GLib # type: ignore
+from ...shared_widgets import NotificationWidget
+
+notifications = NotificationService.get_default()
+
+
+class Popup(Widget.Revealer):
+ def __init__(self, notification: Notification, **kwargs):
+ widget = NotificationWidget(notification)
+ super().__init__(child=widget, transition_type="slide_down", **kwargs)
+
+ notification.connect("closed", lambda x: self.destroy())
+
+ def destroy(self):
+ self.reveal_child = False
+ Utils.Timeout(self.transition_duration, self.unparent)
+
+
+class NotificationList(Widget.Box):
+ __gtype_name__ = "NotificationList"
+
+ def __init__(self):
+ loading_notifications_label = Widget.Label(
+ label="Loading notifications...",
+ valign="center",
+ vexpand=True,
+ css_classes=["notification-center-info-label"],
+ )
+
+ super().__init__(
+ vertical=True,
+ child=[loading_notifications_label],
+ vexpand=True,
+ css_classes=["rec-unset"],
+ setup=lambda self: notifications.connect(
+ "notified",
+ lambda x, notification: self.__on_notified(notification),
+ ),
+ )
+
+ Utils.ThreadTask(
+ self.__load_notifications,
+ lambda result: self.set_child(result),
+ ).run()
+
+ def __on_notified(self, notification: Notification) -> None:
+ notify = Popup(notification)
+ self.prepend(notify)
+ notify.reveal_child = True
+
+ def __load_notifications(self) -> list[Widget.Label | Popup]:
+ widgets: list[Widget.Label | Popup] = []
+ for i in notifications.notifications:
+ GLib.idle_add(lambda i=i: widgets.append(Popup(i, reveal_child=True)))
+
+ widgets.append(
+ Widget.Label(
+ label="No notifications",
+ valign="center",
+ vexpand=True,
+ visible=notifications.bind(
+ "notifications", lambda value: len(value) == 0
+ ),
+ css_classes=["notification-center-info-label"],
+ )
+ )
+ return widgets
+
+
+class NotificationCenter(Widget.Box):
+ __gtype_name__ = "NotificationCenter"
+
+ def __init__(self):
+ super().__init__(
+ vertical=True,
+ vexpand=True,
+ css_classes=["notification-center"],
+ child=[
+ Widget.Box(
+ css_classes=["notification-center-header", "rec-unset"],
+ child=[
+ Widget.Label(
+ label=notifications.bind(
+ "notifications", lambda value: str(len(value))
+ ),
+ css_classes=["notification-count"],
+ ),
+ Widget.Label(
+ label="notifications",
+ css_classes=["notification-header-label"],
+ ),
+ Widget.Button(
+ child=Widget.Label(label="Clear all"),
+ halign="end",
+ hexpand=True,
+ on_click=lambda x: notifications.clear_all(),
+ css_classes=["notification-clear-all"],
+ ),
+ ],
+ ),
+ Widget.Scroll(
+ child=NotificationList(),
+ vexpand=True,
+ ),
+ ],
+ )
diff --git a/ignis/modules/control_center/widgets/quick_settings/__init__.py b/ignis/modules/control_center/widgets/quick_settings/__init__.py
new file mode 100644
index 0000000..9ad69f9
--- /dev/null
+++ b/ignis/modules/control_center/widgets/quick_settings/__init__.py
@@ -0,0 +1,3 @@
+from .quick_settings import QuickSettings
+
+__all__ = ["QuickSettings"]
diff --git a/ignis/modules/control_center/widgets/quick_settings/__pycache__/__init__.cpython-313.pyc b/ignis/modules/control_center/widgets/quick_settings/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..1d513af
Binary files /dev/null and b/ignis/modules/control_center/widgets/quick_settings/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/modules/control_center/widgets/quick_settings/__pycache__/bluetooth.cpython-313.pyc b/ignis/modules/control_center/widgets/quick_settings/__pycache__/bluetooth.cpython-313.pyc
new file mode 100644
index 0000000..c5dab00
Binary files /dev/null and b/ignis/modules/control_center/widgets/quick_settings/__pycache__/bluetooth.cpython-313.pyc differ
diff --git a/ignis/modules/control_center/widgets/quick_settings/__pycache__/dark_mode.cpython-313.pyc b/ignis/modules/control_center/widgets/quick_settings/__pycache__/dark_mode.cpython-313.pyc
new file mode 100644
index 0000000..32c5818
Binary files /dev/null and b/ignis/modules/control_center/widgets/quick_settings/__pycache__/dark_mode.cpython-313.pyc differ
diff --git a/ignis/modules/control_center/widgets/quick_settings/__pycache__/dnd.cpython-313.pyc b/ignis/modules/control_center/widgets/quick_settings/__pycache__/dnd.cpython-313.pyc
new file mode 100644
index 0000000..b308078
Binary files /dev/null and b/ignis/modules/control_center/widgets/quick_settings/__pycache__/dnd.cpython-313.pyc differ
diff --git a/ignis/modules/control_center/widgets/quick_settings/__pycache__/ethernet.cpython-313.pyc b/ignis/modules/control_center/widgets/quick_settings/__pycache__/ethernet.cpython-313.pyc
new file mode 100644
index 0000000..b1fbb7b
Binary files /dev/null and b/ignis/modules/control_center/widgets/quick_settings/__pycache__/ethernet.cpython-313.pyc differ
diff --git a/ignis/modules/control_center/widgets/quick_settings/__pycache__/quick_settings.cpython-313.pyc b/ignis/modules/control_center/widgets/quick_settings/__pycache__/quick_settings.cpython-313.pyc
new file mode 100644
index 0000000..c5376c6
Binary files /dev/null and b/ignis/modules/control_center/widgets/quick_settings/__pycache__/quick_settings.cpython-313.pyc differ
diff --git a/ignis/modules/control_center/widgets/quick_settings/__pycache__/record.cpython-313.pyc b/ignis/modules/control_center/widgets/quick_settings/__pycache__/record.cpython-313.pyc
new file mode 100644
index 0000000..f6b97ec
Binary files /dev/null and b/ignis/modules/control_center/widgets/quick_settings/__pycache__/record.cpython-313.pyc differ
diff --git a/ignis/modules/control_center/widgets/quick_settings/__pycache__/vpn.cpython-313.pyc b/ignis/modules/control_center/widgets/quick_settings/__pycache__/vpn.cpython-313.pyc
new file mode 100644
index 0000000..2aac668
Binary files /dev/null and b/ignis/modules/control_center/widgets/quick_settings/__pycache__/vpn.cpython-313.pyc differ
diff --git a/ignis/modules/control_center/widgets/quick_settings/__pycache__/wifi.cpython-313.pyc b/ignis/modules/control_center/widgets/quick_settings/__pycache__/wifi.cpython-313.pyc
new file mode 100644
index 0000000..ccb8c24
Binary files /dev/null and b/ignis/modules/control_center/widgets/quick_settings/__pycache__/wifi.cpython-313.pyc differ
diff --git a/ignis/modules/control_center/widgets/quick_settings/bluetooth.py b/ignis/modules/control_center/widgets/quick_settings/bluetooth.py
new file mode 100644
index 0000000..15f1a32
--- /dev/null
+++ b/ignis/modules/control_center/widgets/quick_settings/bluetooth.py
@@ -0,0 +1,87 @@
+from ignis.widgets import Widget
+from ...qs_button import QSButton
+from ...menu import Menu
+from ....shared_widgets import ToggleBox
+from ignis.services.bluetooth import BluetoothService, BluetoothDevice
+
+bluetooth = BluetoothService.get_default()
+
+
+class BluetoothDeviceItem(Widget.Button):
+ def __init__(self, device: BluetoothDevice):
+ super().__init__(
+ css_classes=["network-item", "unset"],
+ on_click=lambda x: device.disconnect_from()
+ if device.connected
+ else device.connect_to(),
+ child=Widget.Box(
+ child=[
+ Widget.Icon(
+ image=device.bind("icon_name"),
+ ),
+ Widget.Label(
+ label=device.alias,
+ halign="start",
+ css_classes=["wifi-network-label"],
+ ),
+ Widget.Icon(
+ image="object-select-symbolic",
+ halign="end",
+ hexpand=True,
+ visible=device.bind("connected"),
+ ),
+ ]
+ ),
+ )
+
+
+class BluetoothMenu(Menu):
+ def __init__(self):
+ super().__init__(
+ name="bluetooth",
+ child=[
+ ToggleBox(
+ label="Bluetooth",
+ active=bluetooth.powered,
+ on_change=lambda x, state: bluetooth.set_powered(state),
+ css_classes=["network-header-box"],
+ ),
+ Widget.Box(
+ vertical=True,
+ child=bluetooth.bind(
+ "devices",
+ transform=lambda value: [BluetoothDeviceItem(i) for i in value],
+ ),
+ ),
+ ],
+ )
+
+
+class BluetoothButton(QSButton):
+ def __init__(self):
+ menu = BluetoothMenu()
+
+ def get_label(devices: list[BluetoothDevice]) -> str:
+ if len(devices) == 0:
+ return "Bluetooth"
+ elif len(devices) == 1:
+ return devices[0].alias
+ else:
+ return f"{len(devices)} pairs"
+
+ def toggle_menu(x) -> None:
+ bluetooth.set_setup_mode(True)
+ menu.toggle()
+
+ super().__init__(
+ label=bluetooth.bind("connected_devices", get_label),
+ icon_name="bluetooth-active-symbolic",
+ on_activate=toggle_menu,
+ on_deactivate=toggle_menu,
+ active=bluetooth.bind("powered"),
+ menu=menu,
+ )
+
+
+def bluetooth_control() -> list[QSButton]:
+ return [] if bluetooth.state == "absent" else [BluetoothButton()]
diff --git a/ignis/modules/control_center/widgets/quick_settings/dark_mode.py b/ignis/modules/control_center/widgets/quick_settings/dark_mode.py
new file mode 100644
index 0000000..e7fe3f3
--- /dev/null
+++ b/ignis/modules/control_center/widgets/quick_settings/dark_mode.py
@@ -0,0 +1,15 @@
+from ...qs_button import QSButton
+from user_options import user_options
+
+
+class DarkModeButton(QSButton):
+ __gtype_name__ = "DarkModeButton"
+
+ def __init__(self):
+ super().__init__(
+ label="Dark",
+ icon_name="night-light-symbolic",
+ on_activate=lambda x: user_options.material.set_dark_mode(True),
+ on_deactivate=lambda x: user_options.material.set_dark_mode(False),
+ active=user_options.material.bind("dark_mode"),
+ )
diff --git a/ignis/modules/control_center/widgets/quick_settings/dnd.py b/ignis/modules/control_center/widgets/quick_settings/dnd.py
new file mode 100644
index 0000000..1544f3e
--- /dev/null
+++ b/ignis/modules/control_center/widgets/quick_settings/dnd.py
@@ -0,0 +1,28 @@
+from ...qs_button import QSButton
+from ignis.services.notifications import NotificationService
+from ignis.options import options
+
+notifications = NotificationService.get_default()
+
+
+class DNDButton(QSButton):
+ __gtype_name__ = "DNDButton"
+
+ def __init__(self):
+ super().__init__(
+ label=options.notifications.bind(
+ "dnd", lambda value: "Silent" if value else "Noisy"
+ ),
+ icon_name=options.notifications.bind(
+ "dnd",
+ transform=lambda value: "notification-disabled-symbolic"
+ if value
+ else "notification-symbolic",
+ ),
+ on_activate=lambda x: self.__activate(True),
+ on_deactivate=lambda x: self.__activate(False),
+ active=options.notifications.bind("dnd"),
+ )
+
+ def __activate(self, state: bool) -> None:
+ options.notifications.dnd = state
diff --git a/ignis/modules/control_center/widgets/quick_settings/ethernet.py b/ignis/modules/control_center/widgets/quick_settings/ethernet.py
new file mode 100644
index 0000000..9ec00bc
--- /dev/null
+++ b/ignis/modules/control_center/widgets/quick_settings/ethernet.py
@@ -0,0 +1,102 @@
+import asyncio
+from ignis.widgets import Widget
+from ignis.utils import Utils
+from ...qs_button import QSButton
+from ...menu import Menu
+from ignis.services.network import NetworkService, EthernetDevice
+
+network = NetworkService.get_default()
+
+
+class EthernetConnectionItem(Widget.Button):
+ def __init__(self, device: EthernetDevice):
+ super().__init__(
+ css_classes=["network-item", "unset"],
+ on_click=lambda x: asyncio.create_task(device.disconnect_from())
+ if device.is_connected
+ else asyncio.create_task(device.connect_to()),
+ child=Widget.Box(
+ child=[
+ Widget.Icon(image="network-wired-symbolic"),
+ Widget.Label(
+ label=device.name,
+ ellipsize="end",
+ max_width_chars=20,
+ halign="start",
+ ),
+ Widget.Button(
+ child=Widget.Label(
+ label=device.bind(
+ "is_connected",
+ lambda value: "Disconnect" if value else "Connect",
+ )
+ ),
+ css_classes=["connect-label", "unset"],
+ halign="end",
+ hexpand=True,
+ ),
+ ]
+ ),
+ )
+
+
+class EthernetMenu(Menu):
+ def __init__(self):
+ super().__init__(
+ name="ethernet",
+ child=[
+ Widget.Box(
+ css_classes=["network-header-box"],
+ child=[
+ Widget.Icon(icon_name="network-wired-symbolic", pixel_size=28),
+ Widget.Label(
+ label="Wired connections",
+ css_classes=["network-header-label"],
+ ),
+ ],
+ ),
+ Widget.Box(
+ vertical=True,
+ child=network.ethernet.bind(
+ "devices",
+ lambda value: [EthernetConnectionItem(i) for i in value],
+ ),
+ ),
+ Widget.Separator(),
+ Widget.Button(
+ css_classes=["network-item", "unset"],
+ style="margin-bottom: 0;",
+ on_click=lambda x: asyncio.create_task(Utils.exec_sh_async("nm-connection-editor")),
+ child=Widget.Box(
+ child=[
+ Widget.Icon(image="preferences-system-symbolic"),
+ Widget.Label(
+ label="Network Settings",
+ halign="start",
+ ),
+ ]
+ ),
+ ),
+ ],
+ )
+
+
+class EthernetButton(QSButton):
+ def __init__(self):
+ menu = EthernetMenu()
+
+ super().__init__(
+ label="Wired",
+ icon_name="network-wired-symbolic",
+ on_activate=lambda x: menu.toggle(),
+ on_deactivate=lambda x: menu.toggle(),
+ menu=menu,
+ active=network.ethernet.bind("is_connected"),
+ )
+
+
+def ethernet_control() -> list[QSButton]:
+ if len(network.ethernet.devices) > 0:
+ return [EthernetButton()]
+ else:
+ return []
diff --git a/ignis/modules/control_center/widgets/quick_settings/quick_settings.py b/ignis/modules/control_center/widgets/quick_settings/quick_settings.py
new file mode 100644
index 0000000..cb8ec89
--- /dev/null
+++ b/ignis/modules/control_center/widgets/quick_settings/quick_settings.py
@@ -0,0 +1,68 @@
+from ignis.widgets import Widget
+from .wifi import wifi_control
+from .record import RecordButton
+from .dnd import DNDButton
+from .dark_mode import DarkModeButton
+from .ethernet import ethernet_control
+from .vpn import vpn_control
+from .bluetooth import bluetooth_control
+from ...qs_button import QSButton
+from ignis.services.network import NetworkService
+
+network = NetworkService.get_default()
+
+
+class QuickSettings(Widget.Box):
+ def __init__(self):
+ super().__init__(vertical=True, css_classes=["qs-main-box"])
+ network.wifi.connect("notify::devices", lambda x, y: self.__refresh())
+ network.ethernet.connect("notify::devices", lambda x, y: self.__refresh())
+ network.vpn.connect("notify::connections", lambda x, y: self.__refresh())
+
+ self.__refresh()
+
+ def __refresh(self) -> None:
+ self.child = []
+ self.__configure()
+
+ def __configure(self) -> None:
+ self.__qs_fabric(
+ *wifi_control(),
+ *ethernet_control(),
+ *vpn_control(),
+ *bluetooth_control(),
+ DNDButton(),
+ DarkModeButton(),
+ RecordButton(),
+ )
+
+ def __qs_fabric(self, *buttons: QSButton) -> None:
+ for i in range(0, len(buttons), 2):
+ self.__add_row(buttons, i)
+
+ def __add_row(self, buttons: tuple[QSButton, ...], i: int) -> None:
+ row = Widget.Box(homogeneous=True)
+ if len(self.child) > 0:
+ row.style = "margin-top: 0.5rem;"
+
+ self.append(row)
+
+ button1 = buttons[i]
+
+ self.__add_button(row, button1, buttons, i)
+
+ if i + 1 < len(buttons):
+ button2 = buttons[i + 1]
+ button2.style = "margin-left: 0.5rem;"
+ self.__add_button(row, button2, buttons, i)
+
+ def __add_button(
+ self, row: Widget.Box, button: QSButton, buttons: tuple[QSButton, ...], i: int
+ ) -> None:
+ row.append(button)
+
+ if button.menu:
+ self.append(button.menu)
+
+ if i == len(buttons) - 1 or i == len(buttons) - 2:
+ button.menu.box.add_css_class("control-center-menu-last-row")
diff --git a/ignis/modules/control_center/widgets/quick_settings/record.py b/ignis/modules/control_center/widgets/quick_settings/record.py
new file mode 100644
index 0000000..2f6772d
--- /dev/null
+++ b/ignis/modules/control_center/widgets/quick_settings/record.py
@@ -0,0 +1,102 @@
+from ignis.widgets import Widget
+from ...qs_button import QSButton
+from ...menu import Menu
+from ignis.services.recorder import RecorderService
+
+recorder = RecorderService.get_default()
+
+
+class RecordMenu(Menu):
+ def __init__(self):
+ self._audio_switch = Widget.Switch(halign="end", hexpand=True, valign="center")
+ self._dropdown = Widget.DropDown(
+ items=["Internal audio", "Microphone", "Both sources"],
+ css_classes=["record-dropdown"],
+ )
+
+ super().__init__(
+ name="recording",
+ child=[
+ Widget.Icon(
+ image="media-record-symbolic",
+ pixel_size=36,
+ halign="center",
+ css_classes=["record-icon"],
+ ),
+ Widget.Label(
+ label="Start recording?",
+ halign="center",
+ style="font-size: 1.2rem;",
+ ),
+ Widget.Box(
+ style="margin-top: 0.5rem;",
+ child=[
+ Widget.Icon(
+ image="microphone-sensitivity-medium-symbolic",
+ pixel_size=20,
+ style="margin-right: 0.5rem;",
+ ),
+ Widget.Box(
+ vertical=True,
+ child=[
+ Widget.Label(
+ label="Record audio",
+ style="font-size: 1.1rem;",
+ halign="start",
+ ),
+ self._dropdown,
+ ],
+ ),
+ self._audio_switch,
+ ],
+ ),
+ Widget.Box(
+ style="margin-top: 1rem;",
+ child=[
+ Widget.Button(
+ child=Widget.Label(label="Cancel"),
+ css_classes=["record-cancel-button", "unset"],
+ on_click=lambda x: self.set_reveal_child(False), # type: ignore
+ ),
+ Widget.Button(
+ child=Widget.Label(label="Start recording"),
+ halign="end",
+ hexpand=True,
+ css_classes=["record-start-button", "unset"],
+ on_click=lambda x: self.__start_recording(), # type: ignore
+ ),
+ ],
+ ),
+ ],
+ )
+
+ def __start_recording(self) -> None:
+ self.set_reveal_child(False)
+ microphone = False
+ internal = False
+ if self._audio_switch.active:
+ if self._dropdown.selected == "Internal audio":
+ internal = True
+ elif self._dropdown.selected == "Microphone":
+ microphone = True
+ else:
+ internal = True
+ microphone = True
+
+ recorder.start_recording(
+ record_microphone=microphone, record_internal_audio=internal
+ )
+
+
+class RecordButton(QSButton):
+ def __init__(self):
+ record_menu = RecordMenu()
+
+ super().__init__(
+ label="Recording",
+ icon_name="media-record-symbolic",
+ on_activate=lambda x: record_menu.toggle(),
+ on_deactivate=lambda x: recorder.stop_recording(),
+ active=recorder.bind("active"),
+ menu=record_menu,
+ )
diff --git a/ignis/modules/control_center/widgets/quick_settings/vpn.py b/ignis/modules/control_center/widgets/quick_settings/vpn.py
new file mode 100644
index 0000000..b45cf73
--- /dev/null
+++ b/ignis/modules/control_center/widgets/quick_settings/vpn.py
@@ -0,0 +1,112 @@
+import asyncio
+from ignis.widgets import Widget
+from ignis.utils import Utils
+from ...qs_button import QSButton
+from ...menu import Menu
+from ignis.services.network import NetworkService, VpnConnection
+
+
+network = NetworkService.get_default()
+
+
+class VpnNetworkItem(Widget.Button):
+ def __init__(self, conn: VpnConnection):
+ super().__init__(
+ css_classes=["network-item", "unset"],
+ on_click=lambda x: asyncio.create_task(conn.toggle_connection()),
+ child=Widget.Box(
+ child=[
+ Widget.Label(
+ label=conn.name,
+ ellipsize="end",
+ max_width_chars=20,
+ halign="start",
+ ),
+ Widget.Button(
+ child=Widget.Label(
+ label=conn.bind(
+ "is_connected",
+ lambda value: "Disconnect" if value else "Connect",
+ )
+ ),
+ css_classes=["connect-label", "unset"],
+ halign="end",
+ hexpand=True,
+ ),
+ ]
+ ),
+ )
+
+
+class VpnMenu(Menu):
+ def __init__(self):
+ super().__init__(
+ name="vpn",
+ child=[
+ Widget.Box(
+ css_classes=["network-header-box"],
+ child=[
+ Widget.Icon(icon_name="network-vpn-symbolic", pixel_size=28),
+ Widget.Label(
+ label="VPN connections",
+ css_classes=["network-header-label"],
+ ),
+ ],
+ ),
+ Widget.Box(
+ vertical=True,
+ child=network.vpn.bind(
+ "connections",
+ transform=lambda value: [VpnNetworkItem(i) for i in value],
+ ),
+ ),
+ Widget.Separator(),
+ Widget.Button(
+ css_classes=["network-item", "unset"],
+ on_click=lambda x: asyncio.create_task(Utils.exec_sh_async("nm-connection-editor")),
+ style="margin-bottom: 0;",
+ child=Widget.Box(
+ child=[
+ Widget.Icon(image="preferences-system-symbolic"),
+ Widget.Label(
+ label="Network Manager",
+ halign="start",
+ ),
+ ]
+ ),
+ ),
+ ],
+ )
+
+
+class VpnButton(QSButton):
+ def __init__(self):
+ menu = VpnMenu()
+
+ def get_label(id: str) -> str:
+ if id:
+ return id
+ else:
+ return "VPN"
+
+ def get_icon(icon_name: str) -> str:
+ if network.vpn.is_connected:
+ return icon_name
+ else:
+ return "network-vpn-symbolic"
+
+ super().__init__(
+ label=network.vpn.bind("active_vpn_id", get_label),
+ icon_name=network.vpn.bind("icon-name", get_icon),
+ on_activate=lambda x: menu.toggle(),
+ on_deactivate=lambda x: menu.toggle(),
+ active=network.vpn.bind("is-connected"),
+ menu=menu,
+ )
+
+
+def vpn_control() -> list[QSButton]:
+ if len(network.vpn.connections) > 0:
+ return [VpnButton()]
+ else:
+ return []
diff --git a/ignis/modules/control_center/widgets/quick_settings/wifi.py b/ignis/modules/control_center/widgets/quick_settings/wifi.py
new file mode 100644
index 0000000..75be064
--- /dev/null
+++ b/ignis/modules/control_center/widgets/quick_settings/wifi.py
@@ -0,0 +1,107 @@
+import asyncio
+from ignis.widgets import Widget
+from ignis.utils import Utils
+from ...qs_button import QSButton
+from ...menu import Menu
+from ....shared_widgets import ToggleBox
+from ignis.services.network import NetworkService, WifiAccessPoint, WifiDevice
+
+network = NetworkService.get_default()
+
+
+class WifiNetworkItem(Widget.Button):
+ def __init__(self, access_point: WifiAccessPoint):
+ super().__init__(
+ css_classes=["network-item", "unset"],
+ on_click=lambda x: asyncio.create_task(access_point.connect_to_graphical()),
+ child=Widget.Box(
+ child=[
+ Widget.Icon(
+ image=access_point.bind(
+ "strength", transform=lambda value: access_point.icon_name
+ ),
+ ),
+ Widget.Label(
+ label=access_point.ssid,
+ halign="start",
+ ),
+ Widget.Icon(
+ image="object-select-symbolic",
+ halign="end",
+ hexpand=True,
+ visible=access_point.bind("is_connected"),
+ ),
+ ]
+ ),
+ )
+
+
+class WifiMenu(Menu):
+ def __init__(self, device: WifiDevice):
+ super().__init__(
+ name="wifi",
+ child=[
+ ToggleBox(
+ label="Wi-Fi",
+ active=network.wifi.enabled,
+ on_change=lambda x, state: network.wifi.set_enabled(state),
+ css_classes=["network-header-box"],
+ ),
+ Widget.Box(
+ vertical=True,
+ child=device.bind(
+ "access_points",
+ transform=lambda value: [WifiNetworkItem(i) for i in value],
+ ),
+ ),
+ Widget.Separator(),
+ Widget.Button(
+ css_classes=["network-item", "unset"],
+ on_click=lambda x: asyncio.create_task(Utils.exec_sh_async("nm-connection-editor")),
+ style="margin-bottom: 0;",
+ child=Widget.Box(
+ child=[
+ Widget.Icon(image="preferences-system-symbolic"),
+ Widget.Label(
+ label="Network Settings",
+ halign="start",
+ ),
+ ]
+ ),
+ ),
+ ],
+ )
+
+
+class WifiButton(QSButton):
+ def __init__(self, device: WifiDevice):
+ menu = WifiMenu(device)
+
+ def get_label(ssid: str) -> str:
+ if ssid:
+ return ssid
+ else:
+ return "Wi-Fi"
+
+ def get_icon(icon_name: str) -> str:
+ if device.ap.is_connected:
+ return icon_name
+ else:
+ return "network-wireless-symbolic"
+
+ def toggle_list(x) -> None:
+ asyncio.create_task(device.scan())
+ menu.toggle()
+
+ super().__init__(
+ label=device.ap.bind("ssid", get_label),
+ icon_name=device.ap.bind("icon-name", get_icon),
+ on_activate=toggle_list,
+ on_deactivate=toggle_list,
+ active=network.wifi.bind("enabled"),
+ menu=menu,
+ )
+
+
+def wifi_control() -> list[QSButton]:
+ return [WifiButton(dev) for dev in network.wifi.devices]
diff --git a/ignis/modules/control_center/widgets/user.py b/ignis/modules/control_center/widgets/user.py
new file mode 100644
index 0000000..03d09b0
--- /dev/null
+++ b/ignis/modules/control_center/widgets/user.py
@@ -0,0 +1,71 @@
+import os
+from ignis.widgets import Widget
+from ignis.utils import Utils
+from ignis.app import IgnisApp
+from ignis.services.fetch import FetchService
+from user_options import user_options
+
+fetch = FetchService.get_default()
+app = IgnisApp.get_default()
+
+
+def format_uptime(value: tuple[int, int, int, int]) -> str:
+ days, hours, minutes, seconds = value
+ if days:
+ return f"up {days:02}:{hours:02}:{minutes:02}"
+ else:
+ return f"up {hours:02}:{minutes:02}"
+
+
+class User(Widget.Box):
+ def __init__(self):
+ user_image = Widget.Picture(
+ image=user_options.user.bind(
+ "avatar",
+ lambda value: "user-info" if not os.path.exists(value) else value,
+ ),
+ width=44,
+ height=44,
+ content_fit="cover",
+ style="border-radius: 10rem;",
+ )
+
+ username = Widget.Box(
+ child=[
+ Widget.Label(
+ label=os.getenv("USER"), css_classes=["user-name"], halign="start"
+ ),
+ Widget.Label(
+ label=Utils.Poll(
+ timeout=60 * 1000, callback=lambda x: fetch.uptime
+ ).bind("output", lambda value: format_uptime(value)),
+ halign="start",
+ css_classes=["user-name-secondary"],
+ ),
+ ],
+ vertical=True,
+ css_classes=["user-name-box"],
+ )
+
+ settings_button = Widget.Button(
+ child=Widget.Icon(image="emblem-system-symbolic", pixel_size=20),
+ halign="end",
+ hexpand=True,
+ css_classes=["user-settings", "unset"],
+ on_click=lambda x: self.__on_settings_button_click(),
+ )
+
+ power_button = Widget.Button(
+ child=Widget.Icon(image="system-shutdown-symbolic", pixel_size=20),
+ halign="end",
+ css_classes=["user-power", "unset"],
+ on_click=lambda x: app.toggle_window("ignis_POWERMENU"),
+ )
+ super().__init__(
+ child=[user_image, username, settings_button, power_button],
+ css_classes=["user"],
+ )
+
+ def __on_settings_button_click(self) -> None:
+ window = app.get_window("ignis_SETTINGS")
+ window.visible = not window.visible # type: ignore
diff --git a/ignis/modules/control_center/widgets/volume.py b/ignis/modules/control_center/widgets/volume.py
new file mode 100644
index 0000000..fa75a07
--- /dev/null
+++ b/ignis/modules/control_center/widgets/volume.py
@@ -0,0 +1,128 @@
+import asyncio
+from ignis.widgets import Widget
+from ignis.utils import Utils
+from ignis.services.audio import AudioService, Stream
+from typing import Literal
+from ..menu import Menu
+from ...shared_widgets import MaterialVolumeSlider
+
+audio = AudioService.get_default()
+
+AUDIO_TYPES = {
+ "speaker": {"menu_icon": "audio-headphones-symbolic", "menu_label": "Sound Output"},
+ "microphone": {
+ "menu_icon": "microphone-sensitivity-high-symbolic",
+ "menu_label": "Sound Input",
+ },
+}
+
+
+class DeviceItem(Widget.Button):
+ def __init__(self, stream: Stream, _type: Literal["speaker", "microphone"]):
+ super().__init__(
+ child=Widget.Box(
+ child=[
+ Widget.Icon(image="audio-card-symbolic"),
+ Widget.Label(
+ label=stream.description,
+ ellipsize="end",
+ max_width_chars=30,
+ halign="start",
+ css_classes=["volume-entry-label"],
+ ),
+ Widget.Icon(
+ image="object-select-symbolic",
+ halign="end",
+ hexpand=True,
+ visible=stream.bind("is_default"),
+ ),
+ ]
+ ),
+ css_classes=["volume-entry", "unset"],
+ hexpand=True,
+ setup=lambda self: stream.connect("removed", lambda x: self.unparent()),
+ on_click=lambda x: setattr(audio, _type, stream),
+ )
+
+
+class DeviceMenu(Menu):
+ def __init__(self, _type: Literal["speaker", "microphone"], style: str = ""):
+ data = AUDIO_TYPES[_type]
+
+ super().__init__(
+ name=f"volume-{_type}",
+ child=[
+ Widget.Box(
+ child=[
+ Widget.Icon(image=data["menu_icon"], pixel_size=24),
+ Widget.Label(
+ label=data["menu_label"],
+ halign="start",
+ css_classes=["volume-entry-list-header-label"],
+ ),
+ ],
+ css_classes=["volume-entry-list-header-box"],
+ ),
+ Widget.Box(
+ vertical=True,
+ setup=lambda self: audio.connect(
+ f"{_type}-added",
+ lambda x, stream: self.append(DeviceItem(stream, _type)),
+ ),
+ ),
+ Widget.Separator(css_classes=["volume-entry-list-separator"]),
+ Widget.Button(
+ child=Widget.Box(
+ child=[
+ Widget.Icon(image="preferences-system-symbolic"),
+ Widget.Label(
+ label="Sound Settings",
+ halign="start",
+ css_classes=["volume-entry-label"],
+ ),
+ ]
+ ),
+ css_classes=["volume-entry", "unset"],
+ style="margin-bottom: 0;",
+ on_click=lambda x: asyncio.create_task(Utils.exec_sh_async("pavucontrol")),
+ ),
+ ],
+ )
+
+ self.box.add_css_class(f"volume-menubox-{_type}")
+
+
+class VolumeSlider(Widget.Box):
+ def __init__(self, _type: Literal["speaker", "microphone"]):
+ stream = getattr(audio, _type)
+
+ icon = Widget.Button(
+ child=Widget.Icon(
+ image=stream.bind("icon_name"),
+ pixel_size=18,
+ ),
+ css_classes=["material-slider-icon", "unset", "hover-surface"],
+ on_click=lambda x: stream.set_is_muted(not stream.is_muted),
+ )
+
+ device_menu = DeviceMenu(_type=_type)
+
+ scale = MaterialVolumeSlider(
+ stream=stream,
+ on_change=lambda x: stream.set_volume(x.value),
+ sensitive=stream.bind("is_muted", lambda value: not value),
+ )
+
+ arrow = Widget.Button(
+ child=Widget.Arrow(pixel_size=20, rotated=device_menu.bind("reveal_child")),
+ css_classes=["material-slider-arrow", "hover-surface"],
+ on_click=lambda x: device_menu.toggle(),
+ )
+ super().__init__(
+ vertical=True,
+ child=[
+ Widget.Box(child=[icon, scale, arrow]),
+ device_menu,
+ ],
+ css_classes=[f"volume-mainbox-{_type}"],
+ )
diff --git a/ignis/modules/launcher/__init__.py b/ignis/modules/launcher/__init__.py
index 48480a1..97c6326 100644
--- a/ignis/modules/launcher/__init__.py
+++ b/ignis/modules/launcher/__init__.py
@@ -1,212 +1,3 @@
-import re
-from ignis.widgets import Widget
-from ignis.app import IgnisApp
-from ignis.services.applications import (
- ApplicationsService,
- Application,
- ApplicationAction,
-)
-from ignis.utils import Utils
-from gi.repository import Gio # type: ignore
+from .launcher import Launcher
-app = IgnisApp.get_default()
-
-applications = ApplicationsService.get_default()
-
-
-def is_url(url: str) -> bool:
- regex = re.compile(
- r"^(?:http|ftp)s?://" # http:// or https://
- r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|" # domain
- r"localhost|" # localhost
- r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|" # or ipv4
- r"\[?[A-F0-9]*:[A-F0-9:]+\]?)" # or ipv6
- r"(?::\d+)?" # optional port
- r"(?:/?|[/?]\S+)$",
- re.IGNORECASE,
- )
-
- return re.match(regex, url) is not None
-
-
-class LauncherAppItem(Widget.Button):
- def __init__(self, application: Application) -> None:
- self._application = application
- super().__init__(
- on_click=lambda x: self.launch(),
- on_right_click=lambda x: self._menu.popup(),
- css_classes=["launcher-app"],
- child=Widget.Box(
- child=[
- Widget.Icon(image=application.icon, pixel_size=48),
- Widget.Label(
- label=application.name,
- ellipsize="end",
- max_width_chars=30,
- css_classes=["launcher-app-label"],
- ),
- ]
- ),
- )
- self.__sync_menu()
- application.connect("notify::is-pinned", lambda x, y: self.__sync_menu())
-
- def launch(self) -> None:
- self._application.launch()
- app.close_window("ignis_LAUNCHER")
-
- def launch_action(self, action: ApplicationAction) -> None:
- action.launch()
- app.close_window("ignis_LAUNCHER")
-
- def __sync_menu(self) -> None:
- self._menu = Widget.PopoverMenu(
- items=[
- Widget.MenuItem(label="Launch", on_activate=lambda x: self.launch()),
- Widget.Separator(),
- ]
- + [
- Widget.MenuItem(
- label=i.name,
- on_activate=lambda x, action=i: self.launch_action(action),
- )
- for i in self._application.actions
- ]
- + [
- Widget.Separator(),
- Widget.MenuItem(
- label="Pin", on_activate=lambda x: self._application.pin()
- )
- if not self._application.is_pinned
- else Widget.MenuItem(
- label="Unpin", on_activate=lambda x: self._application.unpin()
- ),
- ]
- )
- self.child.append(self._menu)
-
-
-class SearchWebButton(Widget.Button):
- def __init__(self, query: str):
- self._query = query
- self._url = ""
-
- browser_desktop_file = Utils.exec_sh(
- "xdg-settings get default-web-browser"
- ).stdout.replace("\n", "")
-
- app_info = Gio.DesktopAppInfo.new(desktop_id=browser_desktop_file)
-
- icon_name = "applications-internet-symbolic"
- if app_info:
- icon_string = app_info.get_string("Icon")
- if icon_string:
- icon_name = icon_string
-
- if not query.startswith(("http://", "https://")) and "." in query:
- query = "https://" + query
-
- if is_url(query):
- label = f"Visit {query}"
- self._url = query
- else:
- label = "Search in Google"
- self._url = f"https://www.google.com/search?q={query.replace(' ', '+')}"
-
- super().__init__(
- on_click=lambda x: self.launch(),
- css_classes=["launcher-app"],
- child=Widget.Box(
- child=[
- Widget.Icon(image=icon_name, pixel_size=48),
- Widget.Label(
- label=label,
- css_classes=["launcher-app-label"],
- ),
- ]
- ),
- )
-
- def launch(self) -> None:
- Utils.exec_sh_async(f"xdg-open {self._url}")
- app.close_window("ignis_LAUNCHER")
-
-
-def launcher() -> Widget.Window:
- def search(entry: Widget.Entry, app_list: Widget.Box) -> None:
- query = entry.text
-
- if query == "":
- entry.grab_focus()
- app_list.visible = False
- return
-
- apps = applications.search(applications.apps, query)
- if apps == []:
- app_list.child = [SearchWebButton(query)]
- else:
- app_list.visible = True
- app_list.child = [LauncherAppItem(i) for i in apps[:5]]
-
- def on_open(window: Widget.Window, entry: Widget.Entry) -> None:
- if not window.visible:
- return
-
- entry.text = ""
- entry.grab_focus()
-
- app_list = Widget.Box(vertical=True, visible=False, style="margin-top: 1rem;")
- entry = Widget.Entry(
- hexpand=True,
- placeholder_text="Search",
- css_classes=["launcher-search"],
- on_change=lambda x: search(x, app_list),
- on_accept=lambda x: app_list.child[0].launch()
- if len(app_list.child) > 0
- else None,
- )
-
- main_box = Widget.Box(
- vertical=True,
- valign="start",
- halign="center",
- css_classes=["launcher"],
- child=[
- Widget.Box(
- css_classes=["launcher-search-box"],
- child=[
- Widget.Icon(
- icon_name="system-search-symbolic",
- pixel_size=24,
- style="margin-right: 0.5rem;",
- ),
- entry,
- ],
- ),
- app_list,
- ],
- )
-
- return Widget.Window(
- namespace="ignis_LAUNCHER",
- visible=False,
- popup=True,
- kb_mode="on_demand",
- # exclusivity="ignore",
- css_classes=["unset"],
- setup=lambda self: self.connect(
- "notify::visible", lambda x, y: on_open(self, entry)
- ),
- anchor=["top", "right", "bottom", "left"],
- child=Widget.Overlay(
- child=Widget.Button(
- vexpand=True,
- hexpand=True,
- can_focus=False,
- css_classes=["unset"],
- on_click=lambda x: app.close_window("ignis_LAUNCHER"),
- style="background-color: rgba(0, 0, 0, 0.3);",
- ),
- overlays=[main_box],
- ),
- )
+__all__ = ["Launcher"]
diff --git a/ignis/modules/launcher/__pycache__/__init__.cpython-313.pyc b/ignis/modules/launcher/__pycache__/__init__.cpython-313.pyc
index 224d862..fc3a639 100644
Binary files a/ignis/modules/launcher/__pycache__/__init__.cpython-313.pyc and b/ignis/modules/launcher/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/modules/launcher/__pycache__/launcher.cpython-313.pyc b/ignis/modules/launcher/__pycache__/launcher.cpython-313.pyc
new file mode 100644
index 0000000..5db9b36
Binary files /dev/null and b/ignis/modules/launcher/__pycache__/launcher.cpython-313.pyc differ
diff --git a/ignis/modules/launcher/launcher.py b/ignis/modules/launcher/launcher.py
new file mode 100644
index 0000000..c8824a7
--- /dev/null
+++ b/ignis/modules/launcher/launcher.py
@@ -0,0 +1,217 @@
+import re
+import asyncio
+from ignis.widgets import Widget
+from ignis.app import IgnisApp
+from ignis.services.applications import (
+ ApplicationsService,
+ Application,
+ ApplicationAction,
+)
+from ignis.utils import Utils
+from gi.repository import Gio # type: ignore
+
+app = IgnisApp.get_default()
+
+applications = ApplicationsService.get_default()
+
+TERMINAL_FORMAT = "kitty %command%"
+
+
+def is_url(url: str) -> bool:
+ regex = re.compile(
+ r"^(?:http|ftp)s?://" # http:// or https://
+ r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|" # domain
+ r"localhost|" # localhost
+ r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|" # or ipv4
+ r"\[?[A-F0-9]*:[A-F0-9:]+\]?)" # or ipv6
+ r"(?::\d+)?" # optional port
+ r"(?:/?|[/?]\S+)$",
+ re.IGNORECASE,
+ )
+
+ return re.match(regex, url) is not None
+
+
+class LauncherAppItem(Widget.Button):
+ def __init__(self, application: Application) -> None:
+ self._application = application
+ super().__init__(
+ on_click=lambda x: self.launch(),
+ on_right_click=lambda x: self._menu.popup(),
+ css_classes=["launcher-app"],
+ child=Widget.Box(
+ child=[
+ Widget.Icon(image=application.icon, pixel_size=48),
+ Widget.Label(
+ label=application.name,
+ ellipsize="end",
+ max_width_chars=30,
+ css_classes=["launcher-app-label"],
+ ),
+ ]
+ ),
+ )
+ self.__sync_menu()
+ application.connect("notify::is-pinned", lambda x, y: self.__sync_menu())
+
+ def launch(self) -> None:
+ self._application.launch(terminal_format=TERMINAL_FORMAT)
+ app.close_window("ignis_LAUNCHER")
+
+ def launch_action(self, action: ApplicationAction) -> None:
+ action.launch()
+ app.close_window("ignis_LAUNCHER")
+
+ def __sync_menu(self) -> None:
+ self._menu = Widget.PopoverMenu(
+ items=[
+ Widget.MenuItem(label="Launch", on_activate=lambda x: self.launch()),
+ Widget.Separator(),
+ ]
+ + [
+ Widget.MenuItem(
+ label=i.name,
+ on_activate=lambda x, action=i: self.launch_action(action),
+ )
+ for i in self._application.actions
+ ]
+ + [
+ Widget.Separator(),
+ Widget.MenuItem(
+ label="Pin", on_activate=lambda x: self._application.pin()
+ )
+ if not self._application.is_pinned
+ else Widget.MenuItem(
+ label="Unpin", on_activate=lambda x: self._application.unpin()
+ ),
+ ]
+ )
+ self.child.append(self._menu)
+
+
+class SearchWebButton(Widget.Button):
+ def __init__(self, query: str):
+ self._query = query
+ self._url = ""
+
+ browser_desktop_file = Utils.exec_sh(
+ "xdg-settings get default-web-browser"
+ ).stdout.replace("\n", "")
+
+ app_info = Gio.DesktopAppInfo.new(desktop_id=browser_desktop_file)
+
+ icon_name = "applications-internet-symbolic"
+ if app_info:
+ icon_string = app_info.get_string("Icon")
+ if icon_string:
+ icon_name = icon_string
+
+ if not query.startswith(("http://", "https://")) and "." in query:
+ query = "https://" + query
+
+ if is_url(query):
+ label = f"Visit {query}"
+ self._url = query
+ else:
+ label = "Search in Google"
+ self._url = f"https://www.google.com/search?q={query.replace(' ', '+')}"
+
+ super().__init__(
+ on_click=lambda x: self.launch(),
+ css_classes=["launcher-app"],
+ child=Widget.Box(
+ child=[
+ Widget.Icon(image=icon_name, pixel_size=48),
+ Widget.Label(
+ label=label,
+ css_classes=["launcher-app-label"],
+ ),
+ ]
+ ),
+ )
+
+ def launch(self) -> None:
+ asyncio.create_task(Utils.exec_sh_async(f"xdg-open {self._url}"))
+ app.close_window("ignis_LAUNCHER")
+
+
+class Launcher(Widget.Window):
+ def __init__(self):
+ self._app_list = Widget.Box(
+ vertical=True, visible=False, style="margin-top: 1rem;"
+ )
+ self._entry = Widget.Entry(
+ hexpand=True,
+ placeholder_text="Search",
+ css_classes=["launcher-search"],
+ on_change=self.__search,
+ on_accept=self.__on_accept,
+ )
+
+ main_box = Widget.Box(
+ vertical=True,
+ valign="start",
+ halign="center",
+ css_classes=["launcher"],
+ child=[
+ Widget.Box(
+ css_classes=["launcher-search-box"],
+ child=[
+ Widget.Icon(
+ icon_name="system-search-symbolic",
+ pixel_size=24,
+ style="margin-right: 0.5rem;",
+ ),
+ self._entry,
+ ],
+ ),
+ self._app_list,
+ ],
+ )
+
+ super().__init__(
+ namespace="ignis_LAUNCHER",
+ visible=False,
+ popup=True,
+ kb_mode="on_demand",
+ css_classes=["unset"],
+ setup=lambda self: self.connect("notify::visible", self.__on_open),
+ anchor=["top", "right", "bottom", "left"],
+ child=Widget.Overlay(
+ child=Widget.Button(
+ vexpand=True,
+ hexpand=True,
+ can_focus=False,
+ css_classes=["unset"],
+ on_click=lambda x: app.close_window("ignis_LAUNCHER"),
+ style="background-color: rgba(0, 0, 0, 0.3);",
+ ),
+ overlays=[main_box],
+ ),
+ )
+
+ def __on_open(self, *args) -> None:
+ if not self.visible:
+ return
+
+ self._entry.text = ""
+ self._entry.grab_focus()
+
+ def __on_accept(self, *args) -> None:
+ if len(self._app_list.child) > 0:
+ self._app_list.child[0].launch()
+
+ def __search(self, *args) -> None:
+ query = self._entry.text
+
+ if query == "":
+ self._entry.grab_focus()
+ self._app_list.visible = False
+ return
+
+ apps = applications.search(applications.apps, query)
+ if apps == []:
+ self._app_list.child = [SearchWebButton(query)]
+ else:
+ self._app_list.visible = True
+ self._app_list.child = [LauncherAppItem(i) for i in apps[:5]]
diff --git a/ignis/modules/notification_popup/__init__.py b/ignis/modules/notification_popup/__init__.py
index 8df12b3..94c4309 100644
--- a/ignis/modules/notification_popup/__init__.py
+++ b/ignis/modules/notification_popup/__init__.py
@@ -1,95 +1,3 @@
-from ignis.widgets import Widget
-from ignis.app import IgnisApp
-from ignis.utils import Utils
-from ignis.services.notifications import Notification, NotificationService
-from ..control_center.notification_center import NotificationWidget
+from .notification_popup import NotificationPopup
-
-app = IgnisApp.get_default()
-
-notifications = NotificationService.get_default()
-
-
-class Popup(Widget.Box):
- def __init__(self, notification: Notification):
- widget = NotificationWidget(notification)
- widget.css_classes = ["notification-popup"]
- self._inner = Widget.Revealer(transition_type="slide_left", child=widget)
- self._outer = Widget.Revealer(transition_type="slide_down", child=self._inner)
- super().__init__(child=[self._outer], halign="end")
-
- notification.connect("dismissed", lambda x: self.destroy())
-
- def destroy(self):
- def box_destroy():
- box: Widget.Box = self.get_parent() # type: ignore
- if not box:
- return
-
- self.unparent()
- if len(notifications.popups) == 0:
- window: Widget.Window = box.get_parent() # type: ignore
- if not window:
- return
- window.visible = False
- else:
- change_window_input_region(box)
-
- def outer_close():
- self._outer.reveal_child = False
- Utils.Timeout(self._outer.transition_duration, box_destroy)
-
- self._inner.transition_type = "crossfade"
- self._inner.reveal_child = False
- Utils.Timeout(self._outer.transition_duration, outer_close)
-
-
-def on_notified(box: Widget.Box, notification: Notification, monitor: int) -> None:
- window: Widget.Window = app.get_window("ignis_CONTROL_CENTER") # type: ignore
- if window.visible and window.monitor == monitor:
- return
-
- app.open_window(f"ignis_NOTIFICATION_POPUP_{monitor}")
- popup = Popup(notification)
- box.prepend(popup)
- popup._outer.reveal_child = True
- Utils.Timeout(popup._outer.transition_duration, reveal_popup, box, popup)
-
-
-def reveal_popup(box: Widget.Box, popup: Popup) -> None:
- popup._inner.set_reveal_child(True)
- change_window_input_region(box)
-
-
-def change_window_input_region(box: Widget.Box) -> None:
- def callback() -> None:
- width = box.get_width()
- height = box.get_height()
- window: Widget.Window = box.get_parent() # type: ignore
-
- window.input_width = width
- window.input_height = height
-
- Utils.Timeout(ms=50, target=callback)
-
-
-def notification_popup(monitor: int) -> Widget.Window:
- notifications_box = Widget.Box(
- vertical=True,
- valign="start",
- setup=lambda self: notifications.connect(
- "new_popup",
- lambda x, notification: on_notified(self, notification, monitor),
- ),
- )
-
- return Widget.Window(
- anchor=["right", "top", "bottom"],
- monitor=monitor,
- namespace=f"ignis_NOTIFICATION_POPUP_{monitor}",
- layer="top",
- child=notifications_box,
- visible=False,
- css_classes=["rec-unset"],
- style="min-width: 29rem;",
- )
+__all__ = ["NotificationPopup"]
diff --git a/ignis/modules/notification_popup/__pycache__/__init__.cpython-313.pyc b/ignis/modules/notification_popup/__pycache__/__init__.cpython-313.pyc
index 0ef8d4c..95f0d6f 100644
Binary files a/ignis/modules/notification_popup/__pycache__/__init__.cpython-313.pyc and b/ignis/modules/notification_popup/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/modules/notification_popup/__pycache__/notification_popup.cpython-313.pyc b/ignis/modules/notification_popup/__pycache__/notification_popup.cpython-313.pyc
new file mode 100644
index 0000000..c89dc5a
Binary files /dev/null and b/ignis/modules/notification_popup/__pycache__/notification_popup.cpython-313.pyc differ
diff --git a/ignis/modules/notification_popup/notification_popup.py b/ignis/modules/notification_popup/notification_popup.py
new file mode 100644
index 0000000..4bd1a28
--- /dev/null
+++ b/ignis/modules/notification_popup/notification_popup.py
@@ -0,0 +1,79 @@
+from ignis.widgets import Widget
+from ignis.app import IgnisApp
+from ignis.utils import Utils
+from ignis.services.notifications import Notification, NotificationService
+from ..shared_widgets import NotificationWidget
+
+
+app = IgnisApp.get_default()
+
+notifications = NotificationService.get_default()
+
+
+class Popup(Widget.Box):
+ def __init__(
+ self, box: "PopupBox", window: "NotificationPopup", notification: Notification
+ ):
+ self._box = box
+ self._window = window
+
+ widget = NotificationWidget(notification)
+ widget.css_classes = ["notification-popup"]
+ self._inner = Widget.Revealer(transition_type="slide_left", child=widget)
+ self._outer = Widget.Revealer(transition_type="slide_down", child=self._inner)
+ super().__init__(child=[self._outer], halign="end")
+
+ notification.connect("dismissed", lambda x: self.destroy())
+
+ def destroy(self):
+ def box_destroy():
+ self.unparent()
+ if len(notifications.popups) == 0:
+ self._window.visible = False
+
+ def outer_close():
+ self._outer.reveal_child = False
+ Utils.Timeout(self._outer.transition_duration, box_destroy)
+
+ self._inner.transition_type = "crossfade"
+ self._inner.reveal_child = False
+ Utils.Timeout(self._outer.transition_duration, outer_close)
+
+
+class PopupBox(Widget.Box):
+ def __init__(self, window: "NotificationPopup", monitor: int):
+ self._window = window
+ self._monitor = monitor
+
+ super().__init__(
+ vertical=True,
+ valign="start",
+ setup=lambda self: notifications.connect(
+ "new_popup",
+ lambda x, notification: self.__on_notified(notification),
+ ),
+ )
+
+ def __on_notified(self, notification: Notification) -> None:
+ self._window.visible = True
+ popup = Popup(box=self, window=self._window, notification=notification)
+ self.prepend(popup)
+ popup._outer.reveal_child = True
+ Utils.Timeout(
+ popup._outer.transition_duration, popup._inner.set_reveal_child, True
+ )
+
+
+class NotificationPopup(Widget.Window):
+ def __init__(self, monitor: int):
+ super().__init__(
+ anchor=["right", "top", "bottom"],
+ monitor=monitor,
+ namespace=f"ignis_NOTIFICATION_POPUP_{monitor}",
+ layer="top",
+ child=PopupBox(window=self, monitor=monitor),
+ visible=False,
+ dynamic_input_region=True,
+ css_classes=["rec-unset"],
+ style="min-width: 29rem;",
+ )
diff --git a/ignis/modules/osd/__init__.py b/ignis/modules/osd/__init__.py
index a41e2d3..7568b7d 100644
--- a/ignis/modules/osd/__init__.py
+++ b/ignis/modules/osd/__init__.py
@@ -1,43 +1,3 @@
-from ignis.widgets import Widget
-from ignis.utils import Utils
-from ignis.services.audio import AudioService
+from .osd import OSD
-audio = AudioService.get_default()
-
-
-class OSD(Widget.Window):
- def __init__(self):
- self._timer = None
- super().__init__(
- layer="overlay",
- anchor=["bottom"],
- namespace="ignis_OSD",
- visible=False,
- css_classes=["rec-unset"],
- child=Widget.Box(
- css_classes=["osd"],
- child=[
- Widget.Icon(
- pixel_size=26,
- style="margin-right: 0.5rem;",
- image=audio.speaker.bind("icon_name"),
- ),
- Widget.Scale(
- value=audio.speaker.bind("volume"),
- css_classes=["material-slider"],
- sensitive=False,
- hexpand=True,
- ),
- ],
- ),
- )
-
- def set_property(self, property_name, value):
- if property_name == "visible":
- if self._timer:
- self._timer.cancel()
- self._timer = None
-
- self._timer = Utils.Timeout(3000, self.set_visible, False)
-
- super().set_property(property_name, value)
+__all__ = ["OSD"]
diff --git a/ignis/modules/osd/__pycache__/__init__.cpython-313.pyc b/ignis/modules/osd/__pycache__/__init__.cpython-313.pyc
index 221b777..951a337 100644
Binary files a/ignis/modules/osd/__pycache__/__init__.cpython-313.pyc and b/ignis/modules/osd/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/modules/osd/__pycache__/osd.cpython-313.pyc b/ignis/modules/osd/__pycache__/osd.cpython-313.pyc
new file mode 100644
index 0000000..a537978
Binary files /dev/null and b/ignis/modules/osd/__pycache__/osd.cpython-313.pyc differ
diff --git a/ignis/modules/osd/osd.py b/ignis/modules/osd/osd.py
new file mode 100644
index 0000000..ced81ac
--- /dev/null
+++ b/ignis/modules/osd/osd.py
@@ -0,0 +1,38 @@
+from ignis.widgets import Widget
+from ignis.utils import Utils
+from ignis.services.audio import AudioService
+from ..shared_widgets import MaterialVolumeSlider
+
+audio = AudioService.get_default()
+
+
+class OSD(Widget.Window):
+ def __init__(self):
+ super().__init__(
+ layer="overlay",
+ anchor=["bottom"],
+ namespace="ignis_OSD",
+ visible=False,
+ css_classes=["rec-unset"],
+ child=Widget.Box(
+ css_classes=["osd"],
+ child=[
+ Widget.Icon(
+ pixel_size=26,
+ style="margin-right: 0.5rem;",
+ image=audio.speaker.bind("icon_name"),
+ ),
+ MaterialVolumeSlider(stream=audio.speaker, sensitive=False),
+ ],
+ ),
+ )
+
+ def set_property(self, property_name, value):
+ if property_name == "visible":
+ self.__update_visible()
+
+ super().set_property(property_name, value)
+
+ @Utils.debounce(3000)
+ def __update_visible(self) -> None:
+ super().set_property("visible", False)
diff --git a/ignis/modules/powermenu/__init__.py b/ignis/modules/powermenu/__init__.py
index cdc4825..73fd886 100644
--- a/ignis/modules/powermenu/__init__.py
+++ b/ignis/modules/powermenu/__init__.py
@@ -1,98 +1,3 @@
-from ignis.widgets import Widget
-from ignis.utils import Utils
-from ignis.app import IgnisApp
-from typing import Callable
+from .powermenu import Powermenu
-app = IgnisApp.get_default()
-
-
-class PowermenuButton(Widget.Box):
- def __init__(self, label: str, icon_name: str, on_click: Callable) -> None:
- super().__init__(
- child=[
- Widget.Button(
- child=Widget.Icon(image=icon_name, pixel_size=36),
- on_click=on_click,
- css_classes=["powermenu-button", "unset"],
- ),
- Widget.Label(label=label, css_classes=["powermenu-button-label"]),
- ],
- vertical=True,
- css_classes=["powermenu-button-box"],
- )
-
-
-def poweroff(*args) -> None:
- Utils.exec_sh_async("poweroff")
-
-
-def reboot(*args) -> None:
- Utils.exec_sh_async("reboot")
-
-
-def suspend(*args) -> None:
- app.close_window("ignis_POWERMENU")
- Utils.exec_sh_async("systemctl suspend && hyprlock")
-
-
-def hypr_exit(*args) -> None:
- Utils.exec_sh_async("hyprctl dispatch exit 0")
-
-
-def powermenu():
- return Widget.Window(
- popup=True,
- kb_mode="on_demand",
- namespace="ignis_POWERMENU",
- exclusivity="ignore",
- anchor=["left", "right", "top", "bottom"],
- visible=False,
- child=Widget.Overlay(
- child=Widget.Button(
- vexpand=True,
- hexpand=True,
- can_focus=False,
- css_classes=["unset", "powermenu-overlay"],
- on_click=lambda x: app.close_window("ignis_POWERMENU"),
- ),
- overlays=[
- Widget.Box(
- vertical=True,
- valign="center",
- halign="center",
- css_classes=["powermenu"],
- child=[
- Widget.Box(
- child=[
- PowermenuButton(
- label="Power off",
- icon_name="system-shutdown-symbolic",
- on_click=poweroff,
- ),
- PowermenuButton(
- label="Reboot",
- icon_name="system-reboot-symbolic",
- on_click=reboot,
- ),
- ]
- ),
- Widget.Box(
- child=[
- PowermenuButton(
- label="Suspend",
- icon_name="night-light-symbolic",
- on_click=suspend,
- ),
- PowermenuButton(
- label="Sign out",
- icon_name="system-log-out-symbolic",
- on_click=hypr_exit,
- ),
- ]
- ),
- ],
- )
- ],
- ),
- css_classes=["unset"],
- )
+__all__ = ["Powermenu"]
diff --git a/ignis/modules/powermenu/__pycache__/__init__.cpython-313.pyc b/ignis/modules/powermenu/__pycache__/__init__.cpython-313.pyc
index b35c7be..69bd8d0 100644
Binary files a/ignis/modules/powermenu/__pycache__/__init__.cpython-313.pyc and b/ignis/modules/powermenu/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/modules/powermenu/__pycache__/powermenu.cpython-313.pyc b/ignis/modules/powermenu/__pycache__/powermenu.cpython-313.pyc
new file mode 100644
index 0000000..4fd7376
Binary files /dev/null and b/ignis/modules/powermenu/__pycache__/powermenu.cpython-313.pyc differ
diff --git a/ignis/modules/powermenu/powermenu.py b/ignis/modules/powermenu/powermenu.py
new file mode 100644
index 0000000..76dde62
--- /dev/null
+++ b/ignis/modules/powermenu/powermenu.py
@@ -0,0 +1,106 @@
+import asyncio
+from ignis.widgets import Widget
+from ignis.utils import Utils
+from ignis.app import IgnisApp
+from typing import Callable
+
+app = IgnisApp.get_default()
+
+def create_exec_task(cmd: str) -> None:
+ asyncio.create_task(Utils.exec_sh_async(cmd))
+
+class PowermenuButton(Widget.Box):
+ def __init__(self, label: str, icon_name: str, on_click: Callable) -> None:
+ super().__init__(
+ child=[
+ Widget.Button(
+ child=Widget.Icon(image=icon_name, pixel_size=36),
+ on_click=on_click,
+ css_classes=["powermenu-button", "unset"],
+ ),
+ Widget.Label(label=label, css_classes=["powermenu-button-label"]),
+ ],
+ vertical=True,
+ css_classes=["powermenu-button-box"],
+ )
+
+
+class PowerOffButton(PowermenuButton):
+ def __init__(self):
+ super().__init__(
+ label="Power off",
+ icon_name="system-shutdown-symbolic",
+ on_click=lambda *args: create_exec_task("poweroff"),
+ )
+
+
+class RebootButton(PowermenuButton):
+ def __init__(self):
+ super().__init__(
+ label="Reboot",
+ icon_name="system-reboot-symbolic",
+ on_click=lambda *args: create_exec_task("reboot"),
+ )
+
+
+class SuspendButton(PowermenuButton):
+ def __init__(self):
+ super().__init__(
+ label="Suspend", icon_name="night-light-symbolic", on_click=self.__invoke
+ )
+
+ def __invoke(self, *args) -> None:
+ app.close_window("ignis_POWERMENU")
+ create_exec_task("systemctl suspend && hyprlock")
+
+
+class HyprlandExitButton(PowermenuButton):
+ def __init__(self):
+ super().__init__(
+ label="Sign out",
+ icon_name="system-log-out-symbolic",
+ on_click=lambda *args: create_exec_task("hyprctl dispatch exit 0"),
+ )
+
+
+class Powermenu(Widget.Window):
+ def __init__(self):
+ main_box = Widget.Box(
+ vertical=True,
+ valign="center",
+ halign="center",
+ css_classes=["powermenu"],
+ child=[
+ Widget.Box(
+ child=[
+ PowerOffButton(),
+ RebootButton(),
+ ]
+ ),
+ Widget.Box(
+ child=[
+ SuspendButton(),
+ HyprlandExitButton(),
+ ]
+ ),
+ ],
+ )
+ super().__init__(
+ popup=True,
+ kb_mode="on_demand",
+ namespace="ignis_POWERMENU",
+ exclusivity="ignore",
+ anchor=["left", "right", "top", "bottom"],
+ visible=False,
+ child=Widget.Overlay(
+ child=Widget.Button(
+ vexpand=True,
+ hexpand=True,
+ can_focus=False,
+ css_classes=["unset", "powermenu-overlay"],
+ on_click=lambda x: app.close_window("ignis_POWERMENU"),
+ ),
+ overlays=[main_box],
+ ),
+ css_classes=["unset"],
+ )
diff --git a/ignis/modules/settings/__init__.py b/ignis/modules/settings/__init__.py
index 941ab0c..76bd427 100644
--- a/ignis/modules/settings/__init__.py
+++ b/ignis/modules/settings/__init__.py
@@ -1,83 +1,5 @@
-from ignis.widgets import Widget
-from ignis.gobject import IgnisGObject
-from gi.repository import GObject # type: ignore
-from .notifications import notifications_entry
-from .about import about_entry
-from .appearance import appearance_entry
-from .recorder import recorder_entry
-from .user import user_entry
-from .elements import SettingsPage
-from ignis.app import IgnisApp
-from ignis.exceptions import WindowNotFoundError
-from options import settings_last_page
+from .settings import Settings
-app = IgnisApp.get_default()
-
-
-class ActivePage(IgnisGObject):
- def __init__(self, name: str | None, page: SettingsPage | None):
- super().__init__()
- self._name = name
- self._page = page
-
- @GObject.Property
- def name(self) -> str | None:
- return self._name
-
- @name.setter
- def name(self, value: str | None) -> None:
- self._name = value
-
- @GObject.Property
- def page(self) -> SettingsPage | None:
- return self._page
-
- @page.setter
- def page(self, value: SettingsPage | None) -> None:
- self._page = value
-
-
-def settings_widget():
- active_page = ActivePage(name="Settings", page=None)
- content = Widget.Box(
- hexpand=True,
- vexpand=True,
- child=active_page.bind("page", transform=lambda value: [value]),
- )
- listbox = Widget.ListBox(
- rows=[
- notifications_entry(active_page),
- recorder_entry(active_page),
- appearance_entry(active_page),
- user_entry(active_page),
- about_entry(active_page),
- ],
- )
-
- listbox.select_row(listbox.rows[settings_last_page.value])
-
- navigation_sidebar = Widget.Box(
- vertical=True,
- css_classes=["settings-sidebar"],
- child=[
- Widget.Label(
- label="Settings", halign="start", css_classes=["settings-sidebar-label"]
- ),
- listbox,
- ],
- )
-
- return Widget.Box(child=[navigation_sidebar, content])
-
-
-def settings_window():
- try:
- app.get_window("ignis_SETTINGS")
- except WindowNotFoundError:
- return Widget.RegularWindow(
- default_width=900,
- default_height=600,
- resizable=False,
- child=settings_widget(),
- namespace="ignis_SETTINGS",
- )
+__all__ = [
+ "Settings",
+]
diff --git a/ignis/modules/settings/__pycache__/__init__.cpython-313.pyc b/ignis/modules/settings/__pycache__/__init__.cpython-313.pyc
index e53b964..c7dc202 100644
Binary files a/ignis/modules/settings/__pycache__/__init__.cpython-313.pyc and b/ignis/modules/settings/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/modules/settings/__pycache__/active_page.cpython-313.pyc b/ignis/modules/settings/__pycache__/active_page.cpython-313.pyc
new file mode 100644
index 0000000..c3d79e6
Binary files /dev/null and b/ignis/modules/settings/__pycache__/active_page.cpython-313.pyc differ
diff --git a/ignis/modules/settings/__pycache__/settings.cpython-313.pyc b/ignis/modules/settings/__pycache__/settings.cpython-313.pyc
new file mode 100644
index 0000000..7d4970e
Binary files /dev/null and b/ignis/modules/settings/__pycache__/settings.cpython-313.pyc differ
diff --git a/ignis/modules/settings/active_page.py b/ignis/modules/settings/active_page.py
new file mode 100644
index 0000000..d1af2e7
--- /dev/null
+++ b/ignis/modules/settings/active_page.py
@@ -0,0 +1,5 @@
+from ignis.variable import Variable
+from .elements import SettingsPage
+
+fallback_page = SettingsPage(name="Settings")
+active_page = Variable(value=fallback_page)
diff --git a/ignis/modules/settings/elements/__pycache__/__init__.cpython-313.pyc b/ignis/modules/settings/elements/__pycache__/__init__.cpython-313.pyc
index 6956187..25c1dcd 100644
Binary files a/ignis/modules/settings/elements/__pycache__/__init__.cpython-313.pyc and b/ignis/modules/settings/elements/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/modules/settings/elements/__pycache__/entry.cpython-313.pyc b/ignis/modules/settings/elements/__pycache__/entry.cpython-313.pyc
index 1b5760b..0d9b577 100644
Binary files a/ignis/modules/settings/elements/__pycache__/entry.cpython-313.pyc and b/ignis/modules/settings/elements/__pycache__/entry.cpython-313.pyc differ
diff --git a/ignis/modules/settings/elements/__pycache__/entryrow.cpython-313.pyc b/ignis/modules/settings/elements/__pycache__/entryrow.cpython-313.pyc
index 78890a4..2b5925d 100644
Binary files a/ignis/modules/settings/elements/__pycache__/entryrow.cpython-313.pyc and b/ignis/modules/settings/elements/__pycache__/entryrow.cpython-313.pyc differ
diff --git a/ignis/modules/settings/elements/__pycache__/filerow.cpython-313.pyc b/ignis/modules/settings/elements/__pycache__/filerow.cpython-313.pyc
index 4967147..26c15f9 100644
Binary files a/ignis/modules/settings/elements/__pycache__/filerow.cpython-313.pyc and b/ignis/modules/settings/elements/__pycache__/filerow.cpython-313.pyc differ
diff --git a/ignis/modules/settings/elements/__pycache__/group.cpython-313.pyc b/ignis/modules/settings/elements/__pycache__/group.cpython-313.pyc
index 445e1b4..f9a97e8 100644
Binary files a/ignis/modules/settings/elements/__pycache__/group.cpython-313.pyc and b/ignis/modules/settings/elements/__pycache__/group.cpython-313.pyc differ
diff --git a/ignis/modules/settings/elements/__pycache__/page.cpython-313.pyc b/ignis/modules/settings/elements/__pycache__/page.cpython-313.pyc
index 50a42a9..7162668 100644
Binary files a/ignis/modules/settings/elements/__pycache__/page.cpython-313.pyc and b/ignis/modules/settings/elements/__pycache__/page.cpython-313.pyc differ
diff --git a/ignis/modules/settings/elements/__pycache__/row.cpython-313.pyc b/ignis/modules/settings/elements/__pycache__/row.cpython-313.pyc
index 88d372f..14903d5 100644
Binary files a/ignis/modules/settings/elements/__pycache__/row.cpython-313.pyc and b/ignis/modules/settings/elements/__pycache__/row.cpython-313.pyc differ
diff --git a/ignis/modules/settings/elements/__pycache__/spinrow.cpython-313.pyc b/ignis/modules/settings/elements/__pycache__/spinrow.cpython-313.pyc
index 50d357e..998636d 100644
Binary files a/ignis/modules/settings/elements/__pycache__/spinrow.cpython-313.pyc and b/ignis/modules/settings/elements/__pycache__/spinrow.cpython-313.pyc differ
diff --git a/ignis/modules/settings/elements/__pycache__/switchrow.cpython-313.pyc b/ignis/modules/settings/elements/__pycache__/switchrow.cpython-313.pyc
index 33d79f8..d33c93e 100644
Binary files a/ignis/modules/settings/elements/__pycache__/switchrow.cpython-313.pyc and b/ignis/modules/settings/elements/__pycache__/switchrow.cpython-313.pyc differ
diff --git a/ignis/modules/settings/elements/entry.py b/ignis/modules/settings/elements/entry.py
index 2df6941..d6fdb4e 100644
--- a/ignis/modules/settings/elements/entry.py
+++ b/ignis/modules/settings/elements/entry.py
@@ -1,6 +1,5 @@
from ignis.widgets import Widget
from .page import SettingsPage
-from options import settings_last_page
class SettingsEntry(Widget.ListBoxRow):
@@ -8,15 +7,10 @@ class SettingsEntry(Widget.ListBoxRow):
self,
icon: str,
label: str,
- active_page,
page: SettingsPage,
**kwargs,
):
- def callback(x):
- active_page.page = page
- active_page.name = label
- if self in self.parent.rows:
- settings_last_page.set_value(self.parent.rows.index(self))
+ from ..active_page import active_page # avoid a circular import
super().__init__(
child=Widget.Box(
@@ -26,6 +20,6 @@ class SettingsEntry(Widget.ListBoxRow):
],
),
css_classes=["settings-sidebar-entry"],
- on_activate=callback,
+ on_activate=lambda x: active_page.set_value(page),
**kwargs,
)
diff --git a/ignis/modules/settings/elements/group.py b/ignis/modules/settings/elements/group.py
index d869761..8db3615 100644
--- a/ignis/modules/settings/elements/group.py
+++ b/ignis/modules/settings/elements/group.py
@@ -1,4 +1,3 @@
-from typing import List
from ignis.widgets import Widget
from .row import SettingsRow
from ignis.base_widget import BaseWidget
@@ -6,7 +5,7 @@ from ignis.base_widget import BaseWidget
class SettingsGroup(Widget.Box):
def __init__(
- self, name: str | None, rows: List[SettingsRow | BaseWidget] = [], **kwargs
+ self, name: str | None, rows: list[SettingsRow | BaseWidget] = [], **kwargs
):
super().__init__(
vertical=True,
diff --git a/ignis/modules/settings/elements/page.py b/ignis/modules/settings/elements/page.py
index 64bd961..8c6d7a6 100644
--- a/ignis/modules/settings/elements/page.py
+++ b/ignis/modules/settings/elements/page.py
@@ -1,11 +1,10 @@
from ignis.widgets import Widget
-from typing import List
from .group import SettingsGroup
from ignis.base_widget import BaseWidget
class SettingsPage(Widget.Scroll):
- def __init__(self, name: str, groups: List[SettingsGroup | BaseWidget] = []):
+ def __init__(self, name: str, groups: list[SettingsGroup | BaseWidget] = []):
super().__init__(
hexpand=True,
vexpand=True,
diff --git a/ignis/modules/settings/elements/row.py b/ignis/modules/settings/elements/row.py
index 16f5950..fd68be6 100644
--- a/ignis/modules/settings/elements/row.py
+++ b/ignis/modules/settings/elements/row.py
@@ -2,7 +2,12 @@ from ignis.widgets import Widget
class SettingsRow(Widget.ListBoxRow):
- def __init__(self, label: str | None = None, sublabel: str | None = None, **kwargs):
+ def __init__(
+ self,
+ label: str | None = None,
+ sublabel: str | None = None,
+ **kwargs,
+ ):
super().__init__(
css_classes=["settings-row"],
child=Widget.Box(
diff --git a/ignis/modules/settings/pages/__init__.py b/ignis/modules/settings/pages/__init__.py
new file mode 100644
index 0000000..e1f9d71
--- /dev/null
+++ b/ignis/modules/settings/pages/__init__.py
@@ -0,0 +1,13 @@
+from .about import AboutEntry
+from .appearance import AppearanceEntry
+from .notifications import NotificationsEntry
+from .recorder import RecorderEntry
+from .user import UserEntry
+
+__all__ = [
+ "AboutEntry",
+ "AppearanceEntry",
+ "NotificationsEntry",
+ "RecorderEntry",
+ "UserEntry",
+]
diff --git a/ignis/modules/settings/pages/__pycache__/__init__.cpython-313.pyc b/ignis/modules/settings/pages/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..43f9e08
Binary files /dev/null and b/ignis/modules/settings/pages/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/modules/settings/pages/__pycache__/about.cpython-313.pyc b/ignis/modules/settings/pages/__pycache__/about.cpython-313.pyc
new file mode 100644
index 0000000..3502e54
Binary files /dev/null and b/ignis/modules/settings/pages/__pycache__/about.cpython-313.pyc differ
diff --git a/ignis/modules/settings/pages/__pycache__/appearance.cpython-313.pyc b/ignis/modules/settings/pages/__pycache__/appearance.cpython-313.pyc
new file mode 100644
index 0000000..dd22157
Binary files /dev/null and b/ignis/modules/settings/pages/__pycache__/appearance.cpython-313.pyc differ
diff --git a/ignis/modules/settings/pages/__pycache__/notifications.cpython-313.pyc b/ignis/modules/settings/pages/__pycache__/notifications.cpython-313.pyc
new file mode 100644
index 0000000..d683edd
Binary files /dev/null and b/ignis/modules/settings/pages/__pycache__/notifications.cpython-313.pyc differ
diff --git a/ignis/modules/settings/pages/__pycache__/recorder.cpython-313.pyc b/ignis/modules/settings/pages/__pycache__/recorder.cpython-313.pyc
new file mode 100644
index 0000000..5131334
Binary files /dev/null and b/ignis/modules/settings/pages/__pycache__/recorder.cpython-313.pyc differ
diff --git a/ignis/modules/settings/pages/__pycache__/user.cpython-313.pyc b/ignis/modules/settings/pages/__pycache__/user.cpython-313.pyc
new file mode 100644
index 0000000..8785df8
Binary files /dev/null and b/ignis/modules/settings/pages/__pycache__/user.cpython-313.pyc differ
diff --git a/ignis/modules/settings/pages/about.py b/ignis/modules/settings/pages/about.py
new file mode 100644
index 0000000..95e37a9
--- /dev/null
+++ b/ignis/modules/settings/pages/about.py
@@ -0,0 +1,52 @@
+from ..elements import SettingsPage, SettingsRow, SettingsEntry, SettingsGroup
+from ignis.utils import Utils
+from ignis.widgets import Widget
+from ignis.services.fetch import FetchService
+from user_options import user_options
+
+fetch = FetchService.get_default()
+
+
+class AboutEntry(SettingsEntry):
+ def __init__(self):
+ page = SettingsPage(
+ name="About",
+ groups=[
+ Widget.Box(
+ child=[
+ Widget.Picture(
+ image=user_options.material.bind(
+ "dark_mode",
+ transform=lambda value: fetch.os_logo_text_dark
+ if value
+ else fetch.os_logo_text,
+ ),
+ width=300,
+ height=100,
+ )
+ ],
+ halign="center",
+ width_request=300,
+ height_request=100,
+ ),
+ SettingsGroup(
+ name="Info",
+ rows=[
+ SettingsRow(label="OS", sublabel=fetch.os_name),
+ SettingsRow(
+ label="Ignis version", sublabel=Utils.get_ignis_version()
+ ),
+ SettingsRow(label="Session type", sublabel=fetch.session_type),
+ SettingsRow(
+ label="Wayland compositor", sublabel=fetch.current_desktop
+ ),
+ SettingsRow(label="Kernel", sublabel=fetch.kernel),
+ ],
+ ),
+ ],
+ )
+ super().__init__(
+ label="About",
+ icon="help-about-symbolic",
+ page=page,
+ )
diff --git a/ignis/modules/settings/pages/appearance.py b/ignis/modules/settings/pages/appearance.py
new file mode 100644
index 0000000..df228f3
--- /dev/null
+++ b/ignis/modules/settings/pages/appearance.py
@@ -0,0 +1,67 @@
+import os
+from services.material import MaterialService
+from ..elements import SwitchRow, SettingsPage, SettingsGroup, FileRow, SettingsEntry
+from ignis.widgets import Widget
+from user_options import user_options
+from ignis.options import options
+
+material = MaterialService.get_default()
+
+
+class AppearanceEntry(SettingsEntry):
+ def __init__(self):
+ page = SettingsPage(
+ name="Appearance",
+ groups=[
+ SettingsGroup(
+ name=None,
+ rows=[
+ Widget.ListBoxRow(
+ child=Widget.Picture(
+ image=options.wallpaper.bind("wallpaper_path"),
+ width=1920 // 4,
+ height=1080 // 4,
+ halign="center",
+ style="border-radius: 1rem;",
+ content_fit="cover",
+ ),
+ selectable=False,
+ activatable=False,
+ ),
+ SwitchRow(
+ label="Dark mode",
+ active=user_options.material.bind("dark_mode"),
+ on_change=lambda x,
+ state: user_options.material.set_dark_mode(state),
+ style="margin-top: 1rem;",
+ ),
+ FileRow(
+ label="Wallpaper path",
+ button_label=os.path.basename(
+ options.wallpaper.wallpaper_path
+ )
+ if options.wallpaper.wallpaper_path
+ else None,
+ dialog=Widget.FileDialog(
+ on_file_set=lambda x, file: material.generate_colors(
+ file.get_path()
+ ),
+ initial_path=options.wallpaper.bind("wallpaper_path"),
+ filters=[
+ Widget.FileFilter(
+ mime_types=["image/jpeg", "image/png"],
+ default=True,
+ name="Images JPEG/PNG",
+ )
+ ],
+ ),
+ ),
+ ],
+ )
+ ],
+ )
+ super().__init__(
+ label="Appearance",
+ icon="preferences-desktop-wallpaper-symbolic",
+ page=page,
+ )
diff --git a/ignis/modules/settings/pages/notifications.py b/ignis/modules/settings/pages/notifications.py
new file mode 100644
index 0000000..cbd0f11
--- /dev/null
+++ b/ignis/modules/settings/pages/notifications.py
@@ -0,0 +1,45 @@
+from ..elements import SwitchRow, SettingsPage, SettingsGroup, SpinRow, SettingsEntry
+from ignis.options import options
+
+
+class NotificationsEntry(SettingsEntry):
+ def __init__(self):
+ page = SettingsPage(
+ name="Notifications",
+ groups=[
+ SettingsGroup(
+ name="General",
+ rows=[
+ SwitchRow(
+ label="Do not disturb",
+ active=options.notifications.bind("dnd"),
+ on_change=lambda x, state: options.notifications.set_dnd(
+ state
+ ),
+ ),
+ SpinRow(
+ label="Maximum popups count",
+ sublabel="The first popup will automatically dismiss",
+ value=options.notifications.bind("max_popups_count"),
+ min=1,
+ on_change=lambda x,
+ value: options.notifications.set_max_popups_count(value),
+ ),
+ SpinRow(
+ label="Popup timeout",
+ sublabel="Timeout before popup will be dismissed, in milliseconds.",
+ max=100000,
+ step=100,
+ value=options.notifications.bind("popup_timeout"),
+ on_change=lambda x,
+ value: options.notifications.set_popup_timeout(value),
+ ),
+ ],
+ )
+ ],
+ )
+ super().__init__(
+ label="Notifications",
+ icon="notification-symbolic",
+ page=page,
+ )
diff --git a/ignis/modules/settings/pages/recorder.py b/ignis/modules/settings/pages/recorder.py
new file mode 100644
index 0000000..1ca59f4
--- /dev/null
+++ b/ignis/modules/settings/pages/recorder.py
@@ -0,0 +1,61 @@
+from ..elements import (
+ SpinRow,
+ SettingsPage,
+ SettingsGroup,
+ EntryRow,
+ FileRow,
+ SettingsEntry,
+)
+from ignis.widgets import Widget
+from ignis.options import options
+
+
+class RecorderEntry(SettingsEntry):
+ def __init__(self):
+ page = SettingsPage(
+ name="Recorder",
+ groups=[
+ SettingsGroup(
+ name="General",
+ rows=[
+ SpinRow(
+ label="Recording bitrate",
+ sublabel="Affects the recording quality",
+ value=options.recorder.bind("bitrate"),
+ max=640000,
+ width=150,
+ on_change=lambda x, value: options.recorder.set_bitrate(
+ int(value)
+ ),
+ step=1000,
+ ),
+ FileRow(
+ label="Recording path",
+ button_label=options.recorder.bind("default_file_location"),
+ dialog=Widget.FileDialog(
+ on_file_set=lambda x,
+ file: options.recorder.set_default_file_location(
+ file.get_path()
+ ),
+ select_folder=True,
+ initial_path=options.recorder.default_file_location,
+ ),
+ ),
+ EntryRow(
+ label="Recording filename",
+ sublabel="Support time formatting",
+ text=options.recorder.bind("default_filename"),
+ on_change=lambda x: options.recorder.set_default_filename(
+ x.text
+ ),
+ width=200,
+ ),
+ ],
+ )
+ ],
+ )
+ super().__init__(
+ label="Recorder",
+ icon="media-record-symbolic",
+ page=page,
+ )
diff --git a/ignis/modules/settings/pages/user.py b/ignis/modules/settings/pages/user.py
new file mode 100644
index 0000000..49b84d3
--- /dev/null
+++ b/ignis/modules/settings/pages/user.py
@@ -0,0 +1,54 @@
+import os
+from ..elements import SettingsGroup, SettingsPage, SettingsEntry, FileRow
+from ignis.widgets import Widget
+from user_options import user_options
+
+
+class UserEntry(SettingsEntry):
+ def __init__(self):
+ page = SettingsPage(
+ name="User",
+ groups=[
+ Widget.Box(
+ halign="start",
+ style="margin-left: 2rem;",
+ child=[
+ Widget.Picture(
+ image=user_options.user.bind(
+ "avatar",
+ lambda value: "user-info"
+ if not os.path.exists(value)
+ else value,
+ ),
+ width=96,
+ height=96,
+ style="border-radius: 10rem;",
+ ),
+ Widget.Label(
+ label=os.getenv("USER"), css_classes=["settings-user-name"]
+ ),
+ ],
+ ),
+ SettingsGroup(
+ style="margin-top: 2rem;",
+ valign="start",
+ name="General",
+ vexpand=True,
+ rows=[
+ FileRow(
+ label="Avatar",
+ dialog=Widget.FileDialog(
+ initial_path=user_options.user.bind("avatar"),
+ on_file_set=lambda x,
+ gfile: user_options.user.set_avatar(gfile.get_path()),
+ ),
+ )
+ ],
+ ),
+ ],
+ )
+ super().__init__(
+ label="User",
+ icon="user-available-symbolic",
+ page=page,
+ )
diff --git a/ignis/modules/settings/settings.py b/ignis/modules/settings/settings.py
new file mode 100644
index 0000000..2dd6db7
--- /dev/null
+++ b/ignis/modules/settings/settings.py
@@ -0,0 +1,71 @@
+from ignis.widgets import Widget
+from ignis.app import IgnisApp
+from user_options import user_options
+from .active_page import active_page
+from .pages import (
+ AboutEntry,
+ AppearanceEntry,
+ NotificationsEntry,
+ RecorderEntry,
+ UserEntry,
+)
+
+app = IgnisApp.get_default()
+
+
+class Settings(Widget.RegularWindow):
+ def __init__(self) -> None:
+ content = Widget.Box(
+ hexpand=True,
+ vexpand=True,
+ child=active_page.bind("value", transform=lambda value: [value]),
+ )
+ self._listbox = Widget.ListBox()
+
+ navigation_sidebar = Widget.Box(
+ vertical=True,
+ css_classes=["settings-sidebar"],
+ child=[
+ Widget.Label(
+ label="Settings",
+ halign="start",
+ css_classes=["settings-sidebar-label"],
+ ),
+ self._listbox,
+ ],
+ )
+
+ super().__init__(
+ default_width=900,
+ default_height=600,
+ resizable=False,
+ hide_on_close=True,
+ visible=False,
+ child=Widget.Box(child=[navigation_sidebar, content]),
+ namespace="ignis_SETTINGS",
+ )
+
+ self.connect("notify::visible", self.__on_open)
+
+ def __on_open(self, *args) -> None:
+ if self.visible is False:
+ return
+
+ if len(self._listbox.rows) != 0:
+ return
+
+ rows = [
+ NotificationsEntry(),
+ RecorderEntry(),
+ AppearanceEntry(),
+ UserEntry(),
+ AboutEntry(),
+ ]
+
+ self._listbox.rows = rows
+ self._listbox.select_row(rows[user_options.settings.last_page])
+
+ self._listbox.connect("row-activated", self.__update_last_page)
+
+ def __update_last_page(self, x, row) -> None:
+ user_options.settings.last_page = self._listbox.rows.index(row)
diff --git a/ignis/modules/shared_widgets/__init__.py b/ignis/modules/shared_widgets/__init__.py
new file mode 100644
index 0000000..ec224cb
--- /dev/null
+++ b/ignis/modules/shared_widgets/__init__.py
@@ -0,0 +1,5 @@
+from .toggle_box import ToggleBox
+from .notification import NotificationWidget
+from .volume_slider import MaterialVolumeSlider
+
+__all__ = ["ToggleBox", "NotificationWidget", "MaterialVolumeSlider"]
diff --git a/ignis/modules/shared_widgets/__pycache__/__init__.cpython-313.pyc b/ignis/modules/shared_widgets/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..c2afef3
Binary files /dev/null and b/ignis/modules/shared_widgets/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/modules/shared_widgets/__pycache__/notification.cpython-313.pyc b/ignis/modules/shared_widgets/__pycache__/notification.cpython-313.pyc
new file mode 100644
index 0000000..4e4a96b
Binary files /dev/null and b/ignis/modules/shared_widgets/__pycache__/notification.cpython-313.pyc differ
diff --git a/ignis/modules/shared_widgets/__pycache__/toggle_box.cpython-313.pyc b/ignis/modules/shared_widgets/__pycache__/toggle_box.cpython-313.pyc
new file mode 100644
index 0000000..a5e71d6
Binary files /dev/null and b/ignis/modules/shared_widgets/__pycache__/toggle_box.cpython-313.pyc differ
diff --git a/ignis/modules/shared_widgets/__pycache__/volume_slider.cpython-313.pyc b/ignis/modules/shared_widgets/__pycache__/volume_slider.cpython-313.pyc
new file mode 100644
index 0000000..16bf5a9
Binary files /dev/null and b/ignis/modules/shared_widgets/__pycache__/volume_slider.cpython-313.pyc differ
diff --git a/ignis/modules/shared_widgets/notification.py b/ignis/modules/shared_widgets/notification.py
new file mode 100644
index 0000000..ad505a3
--- /dev/null
+++ b/ignis/modules/shared_widgets/notification.py
@@ -0,0 +1,138 @@
+import asyncio
+from ignis.widgets import Widget
+from ignis.services.notifications import Notification
+from ignis.utils import Utils
+
+
+class ScreenshotLayout(Widget.Box):
+ def __init__(self, notification: Notification) -> None:
+ super().__init__(
+ vertical=True,
+ hexpand=True,
+ child=[
+ Widget.Box(
+ child=[
+ Widget.Picture(
+ image=notification.icon,
+ content_fit="cover",
+ width=1920 // 7,
+ height=1080 // 7,
+ style="border-radius: 1rem; background-color: black;",
+ ),
+ Widget.Button(
+ child=Widget.Icon(
+ image="window-close-symbolic", pixel_size=20
+ ),
+ halign="end",
+ valign="start",
+ hexpand=True,
+ css_classes=["notification-close"],
+ on_click=lambda x: notification.close(),
+ ),
+ ],
+ ),
+ Widget.Label(
+ label="Screenshot saved",
+ css_classes=["notification-screenshot-label"],
+ ),
+ Widget.Box(
+ homogeneous=True,
+ style="margin-top: 0.75rem;",
+ spacing=10,
+ child=[
+ Widget.Button(
+ child=Widget.Label(label="Open"),
+ css_classes=["notification-action"],
+ on_click=lambda x: asyncio.create_task(
+ Utils.exec_sh_async(f"xdg-open {notification.icon}")
+ ),
+ ),
+ Widget.Button(
+ child=Widget.Label(label="Close"),
+ css_classes=["notification-action"],
+ on_click=lambda x: notification.close(),
+ ),
+ ],
+ ),
+ ],
+ )
+
+
+class NormalLayout(Widget.Box):
+ def __init__(self, notification: Notification) -> None:
+ super().__init__(
+ vertical=True,
+ hexpand=True,
+ child=[
+ Widget.Box(
+ child=[
+ Widget.Icon(
+ image=notification.icon
+ if notification.icon
+ else "dialog-information-symbolic",
+ pixel_size=48,
+ halign="start",
+ valign="start",
+ ),
+ Widget.Box(
+ vertical=True,
+ style="margin-left: 0.75rem;",
+ child=[
+ Widget.Label(
+ ellipsize="end",
+ label=notification.summary,
+ halign="start",
+ visible=notification.summary != "",
+ css_classes=["notification-summary"],
+ ),
+ Widget.Label(
+ label=notification.body,
+ ellipsize="end",
+ halign="start",
+ css_classes=["notification-body"],
+ visible=notification.body != "",
+ ),
+ ],
+ ),
+ Widget.Button(
+ child=Widget.Icon(
+ image="window-close-symbolic", pixel_size=20
+ ),
+ halign="end",
+ valign="start",
+ hexpand=True,
+ css_classes=["notification-close"],
+ on_click=lambda x: notification.close(),
+ ),
+ ],
+ ),
+ Widget.Box(
+ child=[
+ Widget.Button(
+ child=Widget.Label(label=action.label),
+ on_click=lambda x, action=action: action.invoke(),
+ css_classes=["notification-action"],
+ )
+ for action in notification.actions
+ ],
+ homogeneous=True,
+ style="margin-top: 0.75rem;" if notification.actions else "",
+ spacing=10,
+ ),
+ ],
+ )
+
+
+class NotificationWidget(Widget.Box):
+ def __init__(self, notification: Notification) -> None:
+ layout: NormalLayout | ScreenshotLayout
+
+ if notification.app_name == "grimblast":
+ layout = ScreenshotLayout(notification)
+ else:
+ layout = NormalLayout(notification)
+
+ super().__init__(
+ css_classes=["notification"],
+ child=[layout],
+ )
diff --git a/ignis/modules/shared_widgets/toggle_box.py b/ignis/modules/shared_widgets/toggle_box.py
new file mode 100644
index 0000000..2d94c28
--- /dev/null
+++ b/ignis/modules/shared_widgets/toggle_box.py
@@ -0,0 +1,25 @@
+from ignis.widgets import Widget
+from typing import Callable
+
+
+class ToggleBox(Widget.Box):
+ def __init__(
+ self,
+ label: str,
+ active: bool,
+ on_change: Callable,
+ css_classes: list[str] = [],
+ **kwargs,
+ ):
+ super().__init__(
+ child=[
+ Widget.Label(label=label),
+ Widget.Switch(
+ halign="end",
+ hexpand=True,
+ active=active,
+ on_change=on_change,
+ ),
+ ],
+ css_classes=["toggle-box"] + css_classes,
+ )
diff --git a/ignis/modules/shared_widgets/volume_slider.py b/ignis/modules/shared_widgets/volume_slider.py
new file mode 100644
index 0000000..68a9a0e
--- /dev/null
+++ b/ignis/modules/shared_widgets/volume_slider.py
@@ -0,0 +1,18 @@
+from ignis.widgets import Widget
+from ignis.services.audio import AudioService, Stream
+
+audio = AudioService.get_default()
+
+
+class MaterialVolumeSlider(Widget.Scale):
+ def __init__(self, stream: Stream, **kwargs):
+ super().__init__(
+ value=stream.bind_many(
+ ["volume", "is_muted"],
+ lambda volume, is_muted: 0 if is_muted or volume is None else volume,
+ ),
+ css_classes=["material-slider"],
+ hexpand=True,
+ step=5,
+ **kwargs,
+ )
diff --git a/ignis/scss/bar.scss b/ignis/scss/bar.scss
index 9738518..3d73c1e 100644
--- a/ignis/scss/bar.scss
+++ b/ignis/scss/bar.scss
@@ -81,10 +81,21 @@
}
@keyframes recorder-animation {
- 0% { opacity: 0; }
- 50% { opacity: 1; }
- 90% { opacity: 1; }
- 100% { opacity: 0; }
+ 0% {
+ opacity: 0;
+ }
+
+ 50% {
+ opacity: 1;
+ }
+
+ 90% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0;
+ }
}
.record-indicator {
@@ -110,16 +121,14 @@
.battery-scale trough,
.battery-scale trough highlight {
- background-color: $surfaceContainerHighest;
- border-radius: 0.5rem;
- min-height: 1.5rem;
- min-width: 6rem;
+ background: $surface;
+ border-radius: 1rem;
+ min-height: 1.3rem;
+ min-width: 5rem;
}
-
.battery-scale trough highlight {
- background-color: $primary;
- min-width: 0rem;
+ background: $primary;
}
.battery-icon {
diff --git a/ignis/scss/control_center.scss b/ignis/scss/control_center.scss
index c1c955f..0d1fd5f 100644
--- a/ignis/scss/control_center.scss
+++ b/ignis/scss/control_center.scss
@@ -2,165 +2,173 @@
@include window;
min-width: 28rem;
padding: 0;
-}
-.control-center-widget {
- padding: 1rem;
- margin-bottom: 1rem;
-}
-
-.qs-main-box {
- margin-top: 0.5rem;
-}
-
-.user {
- margin-top: 1rem;
-}
-
-.qs-button {
- @include hover($surfaceContainerHigh);
- color: $onSurface;
- min-height: 2.4rem;
- border-radius: 1.2rem;
- transition: 0.3s;
- padding: 0.3rem 0.8rem;
-
- &.active {
- @include hover($primary);
- color: $surface;
+ .control-center-widget {
+ padding: 1rem;
+ margin-bottom: 1rem;
}
+
+ .qs-main-box {
+ margin-top: 0.5rem;
+ }
+
+ .user {
+ margin-top: 1rem;
+ }
+
+ .qs-button {
+ @include hover($surfaceContainerHigh);
+ color: $onSurface;
+ min-height: 2.4rem;
+ border-radius: 1.2rem;
+ transition: 0.3s;
+ padding: 0.3rem 0.8rem;
+
+ &.active {
+ @include hover($primary);
+ color: $surface;
+ }
+
+ }
+
+ .qs-button-label {
+ margin-left: 0.5rem;
+ }
+
+ .control-center-menu {
+ background-color: $surfaceContainerHigh;
+ border-radius: 1rem;
+ padding: 1rem;
+ margin-top: 1rem;
+ margin-bottom: 0.5rem;
+
+ separator {
+ margin: 0.5rem 0;
+ }
-}
-
-.qs-button-label {
- margin-left: 0.5rem;
-}
-
-.record-menu {
- background-color: $surfaceContainerHigh;
- border-radius: 1rem;
- padding: 1rem;
- margin-top: 1rem;
-}
-
-.record-icon {
- color: $error;
-}
-
-.record-dropdown {
- button {
- all: unset;
+ }
+ .control-center-menu-last-row {
+ margin-bottom: 0;
+ }
+
+ .record-icon {
+ color: $error;
+ }
+
+ .record-dropdown {
+ button {
+ all: unset;
+ color: $onSurfaceVariant;
+ font-size: 0.9rem;
+ }
+ }
+
+ .record-cancel-button,
+ .record-start-button {
+ color: $surface;
+ border-radius: 1rem;
+ padding: 0.25rem 0.75rem;
+ font-size: 0.95rem;
+ transition: 0.3s;
+ }
+
+ .record-cancel-button {
+ @include hover($surfaceContainerHigh);
+ color: $onSurface;
+ border: 1px solid $primary;
+ }
+
+ .record-start-button {
+ @include hover($primary);
+ }
+
+ .volume-entry {
+ @include hover($surfaceContainerHigh);
+ transition: 0.3s;
+ border-radius: 1rem;
+ padding: 0.5rem 0.5rem;
+ }
+
+ .volume-entry-list-header-label {
+ font-size: 1.35rem;
+ margin-left: 0.5rem;
+ }
+
+ .volume-entry-list-header-box {
+ margin-bottom: 0.75rem;
+ }
+
+ .user-name-box {
+ margin-left: 0.5rem;
+ }
+
+ .user-name {
+ font-size: 1.2rem;
+ }
+
+ .user-name-secondary {
color: $onSurfaceVariant;
font-size: 0.9rem;
}
-}
+
+ .user-settings {
+ @include hover($surfaceContainerHighest);
+ color: $onSurface;
+ border-radius: 2rem;
+ min-width: 2.35rem;
+ margin: 0.4rem 0rem;
+ margin-right: 0.5rem;
+ transition: 0.3s;
+ }
+
+ .user-power {
+ @include hover($primary);
+ color: $surface;
+ border-radius: 1.5rem;
+ min-width: 2.35rem;
+ margin: 0.4rem 0rem;
+ transition: 0.3s;
+ }
+
+ .network-header-box {
+ margin-bottom: 0.5rem;
+ }
+
+ .network-header-label {
+ font-size: 1.35rem;
+ margin-left: 0.75rem;
+ }
+
+ .network-item {
+ @include hover($surfaceContainerHigh);
+ transition: 0.3s;
+ border-radius: 1rem;
+ padding: 0.5rem 0.5rem;
-.record-cancel-button,
-.record-start-button {
- color: $surface;
- border-radius: 1rem;
- padding: 0.25rem 0.75rem;
- font-size: 0.95rem;
- transition: 0.3s;
-}
+ label {
+ margin-left: 0.5rem;
+ }
-.record-cancel-button {
- @include hover($surfaceContainerHigh);
- color: $onSurface;
- border: 1px solid $primary;
-}
+ .connect-label {
+ font-size: 1rem;
+ color: $onSurfaceVariant;
+ }
+ }
+
+ .volume-entry-label {
+ margin-left: 0.5rem;
+ margin-right: 1rem;
+ }
-.record-start-button {
- @include hover($primary);
-}
+ .volume-mainbox-speaker {
+ margin-top: 1rem;
+ }
-.volume-entry {
- @include hover($surfaceContainerHigh);
- transition: 0.3s;
- border-radius: 1rem;
- padding: 0.5rem 0.5rem;
-}
+ .volume-mainbox-microphone {
+ margin-top: 0.25rem;
+ }
-.volume-entry-list-header-label {
- font-size: 1.35rem;
- margin-left: 0.5rem;
-}
-
-.volume-entry-list-header-box {
- margin-bottom: 0.75rem;
-}
-
-.user-name-box {
- margin-left: 0.5rem;
-}
-
-.user-name {
- font-size: 1.2rem;
-}
-
-.user-name-secondary {
- color: $onSurfaceVariant;
- font-size: 0.9rem;
-}
-
-.user-settings {
- @include hover($surfaceContainerHighest);
- color: $onSurface;
- border-radius: 2rem;
- min-width: 2.35rem;
- margin: 0.4rem 0rem;
- margin-right: 0.5rem;
- transition: 0.3s;
-}
-
-.user-power {
- @include hover($primary);
- color: $surface;
- border-radius: 1.5rem;
- min-width: 2.35rem;
- margin: 0.4rem 0rem;
- transition: 0.3s;
-}
-
-.ethernet-header-box,
-.wifi-header-box,
-.vpn-header-box {
- margin-bottom: 0.5rem;
-}
-
-.ethernet-header-label,
-.vpn-header-label {
- font-size: 1.35rem;
- margin-left: 0.75rem;
-}
-
-.wifi-header-label {
- font-size: 1.35rem;
-}
-
-.ethernet-connection-item,
-.wifi-network-item,
-.vpn-connection-item {
- @include hover($surfaceContainerHigh);
- transition: 0.3s;
- border-radius: 1rem;
- padding: 0.5rem 0.5rem;
-}
-
-
-.ethernet-connection-label,
-.wifi-network-label,
-.vpn-connection-label {
- margin-left: 0.5rem;
-}
-
-.ethernet-connection-item-connect-label,
-.vpn-connection-item-connect-label {
- color: $onSurfaceVariant;
- font-size: 1rem;
-}
-
-.volume-entry-label {
- margin-left: 0.5rem;
- margin-right: 1rem;
+ .volume-menubox-speaker {
+ margin-bottom: 0.75rem;
+ }
}
diff --git a/ignis/scss/lib.scss b/ignis/scss/lib.scss
index 0ce0072..9bc3d5c 100644
--- a/ignis/scss/lib.scss
+++ b/ignis/scss/lib.scss
@@ -18,6 +18,7 @@ tooltip {
.toggle-box {
padding: 0.75rem 1rem;
border-radius: 1rem;
+ font-size: 1.35rem;
background-color: $primaryContainer;
}
@@ -62,14 +63,3 @@ tooltip {
@include hover($surface);
transition: 0.3s;
}
-
-.control-center-menu {
- margin-top: 1rem;
- background-color: $surfaceContainerHigh;
- border-radius: 1rem;
- padding: 1rem;
-
- separator {
- margin: 0.5rem 0;
- }
-}
diff --git a/ignis/services/__pycache__/__init__.cpython-313.pyc b/ignis/services/__pycache__/__init__.cpython-313.pyc
index df76d3b..002b3f4 100644
Binary files a/ignis/services/__pycache__/__init__.cpython-313.pyc and b/ignis/services/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/services/material/__pycache__/__init__.cpython-313.pyc b/ignis/services/material/__pycache__/__init__.cpython-313.pyc
index b342171..1262766 100644
Binary files a/ignis/services/material/__pycache__/__init__.cpython-313.pyc and b/ignis/services/material/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ignis/services/material/__pycache__/constants.cpython-313.pyc b/ignis/services/material/__pycache__/constants.cpython-313.pyc
index ec24c5c..c767ea3 100644
Binary files a/ignis/services/material/__pycache__/constants.cpython-313.pyc and b/ignis/services/material/__pycache__/constants.cpython-313.pyc differ
diff --git a/ignis/services/material/__pycache__/service.cpython-313.pyc b/ignis/services/material/__pycache__/service.cpython-313.pyc
index 25083d1..1cae4ce 100644
Binary files a/ignis/services/material/__pycache__/service.cpython-313.pyc and b/ignis/services/material/__pycache__/service.cpython-313.pyc differ
diff --git a/ignis/services/material/__pycache__/util.cpython-313.pyc b/ignis/services/material/__pycache__/util.cpython-313.pyc
index bbf01ff..c4cb89e 100644
Binary files a/ignis/services/material/__pycache__/util.cpython-313.pyc and b/ignis/services/material/__pycache__/util.cpython-313.pyc differ
diff --git a/ignis/services/material/service.py b/ignis/services/material/service.py
index 5399aab..b13d168 100644
--- a/ignis/services/material/service.py
+++ b/ignis/services/material/service.py
@@ -1,6 +1,7 @@
#!/usr/bin/python
import os
-
+import asyncio
+from gi.repository import GLib # type: ignore
from jinja2 import Template
from PIL import Image
from materialyoucolor.quantize import QuantizeCelebi
@@ -12,10 +13,8 @@ from materialyoucolor.score.score import Score
from ignis.utils import Utils
from ignis.app import IgnisApp
from ignis.base_service import BaseService
-from ignis.services.wallpaper import CACHE_WALLPAPER_PATH
-from gi.repository import GObject # type: ignore
-from ignis.services.wallpaper import WallpaperService
-from ignis.services.options import OptionsService
+from ignis.options import options
+from user_options import user_options
from .constants import MATERIAL_CACHE_DIR, TEMPLATES, SAMPLE_WALL
from .util import rgba_to_hex, calculate_optimal_size
@@ -27,39 +26,19 @@ class MaterialService(BaseService):
def __init__(self):
super().__init__()
- self._wallpaper = WallpaperService.get_default()
-
- options = OptionsService.get_default()
- opt_group = options.create_group(name="material", exists_ok=True)
- self._dark_mode_opt = opt_group.create_option(
- name="dark_mode", default=True, exists_ok=True
- )
- self._colors_opt = opt_group.create_option(
- name="colors", default={}, exists_ok=True
- )
-
- if not os.path.exists(CACHE_WALLPAPER_PATH):
+ if not options.wallpaper.wallpaper_path:
self.__on_colors_not_found()
- if self.colors == {}:
+ if user_options.material.colors == {}:
self.__on_colors_not_found()
- @GObject.Property
- def dark_mode(self) -> bool:
- return self._dark_mode_opt.value
-
- @dark_mode.setter
- def dark_mode(self, value: bool) -> None:
- self._dark_mode_opt.value = value
- self.generate_colors(CACHE_WALLPAPER_PATH)
-
- @GObject.Property
- def colors(self) -> dict:
- return self._colors_opt.value
+ user_options.material.connect_option(
+ "dark_mode", lambda: self.generate_colors(options.wallpaper.wallpaper_path)
+ )
def __on_colors_not_found(self) -> None:
- self._wallpaper.set_wallpaper(SAMPLE_WALL)
+ options.wallpaper.set_wallpaper_path(SAMPLE_WALL)
self.generate_colors(SAMPLE_WALL)
- Utils.exec_sh_async("hyprctl reload")
+ asyncio.create_task(Utils.exec_sh_async("hyprctl reload"))
def get_colors_from_img(self, path: str, dark_mode: bool) -> dict[str, str]:
image = Image.open(path)
@@ -88,17 +67,17 @@ class MaterialService(BaseService):
return material_colors
def generate_colors(self, path: str) -> None:
- colors = self.get_colors_from_img(path, self.dark_mode)
+ colors = self.get_colors_from_img(path, user_options.material.dark_mode)
dark_colors = self.get_colors_from_img(path, True)
- self._colors_opt.value = colors
+ user_options.material.colors = colors
self.__render_templates(colors, dark_colors)
- self.__setup(path)
+ asyncio.create_task(self.__setup(path))
def __render_templates(self, colors: dict, dark_colors: dict) -> None:
for template in os.listdir(TEMPLATES):
self.render_template(
colors=colors,
- dark_mode=self.dark_mode,
+ dark_mode=user_options.material.dark_mode,
input_file=f"{TEMPLATES}/{template}",
output_file=f"{MATERIAL_CACHE_DIR}/{template}",
)
@@ -119,7 +98,7 @@ class MaterialService(BaseService):
dark_mode: bool | None = None,
) -> None:
if dark_mode is None:
- colors["dark_mode"] = str(self.dark_mode).lower()
+ colors["dark_mode"] = str(user_options.material.dark_mode).lower()
else:
colors["dark_mode"] = str(dark_mode).lower()
with open(input_file) as file:
@@ -128,17 +107,20 @@ class MaterialService(BaseService):
with open(output_file, "w") as file:
file.write(template_rendered)
- def __reload_gtk_theme(self) -> None:
+ async def __reload_gtk_theme(self) -> None:
THEME_CMD = "gsettings set org.gnome.desktop.interface gtk-theme {}"
COLOR_SCHEME_CMD = "gsettings set org.gnome.desktop.interface color-scheme {}"
- Utils.exec_sh_async(THEME_CMD.format("Adwaita"))
- Utils.exec_sh_async(THEME_CMD.format("Material"))
- Utils.exec_sh_async(COLOR_SCHEME_CMD.format("default"))
- Utils.exec_sh_async(COLOR_SCHEME_CMD.format("prefer-dark"))
- Utils.exec_sh_async(COLOR_SCHEME_CMD.format("default"))
+ await Utils.exec_sh_async(THEME_CMD.format("Adwaita"))
+ await Utils.exec_sh_async(THEME_CMD.format("Material"))
+ await Utils.exec_sh_async(COLOR_SCHEME_CMD.format("default"))
+ await Utils.exec_sh_async(COLOR_SCHEME_CMD.format("prefer-dark"))
+ await Utils.exec_sh_async(COLOR_SCHEME_CMD.format("default"))
- def __setup(self, image_path: str) -> None:
- Utils.exec_sh_async("pkill -SIGUSR1 kitty")
- self._wallpaper.set_wallpaper(image_path)
+ async def __setup(self, image_path: str) -> None:
+ try:
+ await Utils.exec_sh_async("pkill -SIGUSR1 kitty")
+ except GLib.Error:
+ ...
+ options.wallpaper.set_wallpaper_path(image_path)
app.reload_css()
- self.__reload_gtk_theme()
+ await self.__reload_gtk_theme()
diff --git a/ignis/services/material/templates/colors-kitty.conf b/ignis/services/material/templates/colors-kitty.conf
index a28b4cc..c3ed32e 100644
--- a/ignis/services/material/templates/colors-kitty.conf
+++ b/ignis/services/material/templates/colors-kitty.conf
@@ -1,27 +1,23 @@
-foreground {{ onSurface }}
-background {{ surface }}
-cursor {{ onSurface }}
-
-color0 {{ surface }}
-color8 {{ surface }}
+foreground {{ onBackground }}
+background {{ background }}
+cursor {{ primaryFixed }}
+# Base 8 Colors (0-7)
+color0 {{ surfaceContainerLowest }}
color1 {{ error }}
-color9 {{ error }}
-
-color2 {{ inversePrimary }}
-color10 {{ inversePrimary }}
-
-color3 {{ error }}
-color11 {{ error }}
-
+color2 {{ primary }}
+color3 {{ tertiary }}
color4 {{ primaryContainer }}
-color12 {{ primaryContainer }}
-
-color5 {{ onSecondaryContainer }}
-color13 {{ onSecondaryContainer }}
-
-color6 {{ primary }}
-color14 {{ primary }}
-
+color5 {{ secondaryFixedDim }}
+color6 {{ tertiaryFixed }}
color7 {{ onSurface }}
-color15 {{ onSurface }}
\ No newline at end of file
+
+# Bright (Bold) Variants (8–15)
+color8 {{ outlineVariant }}
+color9 {{ errorContainer }}
+color10 {{ primaryContainer }}
+color11 {{ tertiaryContainer }}
+color12 {{ secondaryContainer }}
+color13 {{ onSecondaryFixedVariant }}
+color14 {{ onTertiaryFixedVariant }}
+color15 {{ onSurfaceVariant }}
diff --git a/ignis/user_options.py b/ignis/user_options.py
new file mode 100644
index 0000000..4844a39
--- /dev/null
+++ b/ignis/user_options.py
@@ -0,0 +1,45 @@
+import os
+from ignis.options_manager import OptionsGroup, OptionsManager
+from ignis import DATA_DIR, CACHE_DIR # type: ignore
+
+USER_OPTIONS_FILE = f"{DATA_DIR}/user_options.json"
+OLD_USER_OPTIONS_FILE = f"{CACHE_DIR}/user_options.json"
+
+
+# FIXME: remove someday
+def _migrate_old_options_file() -> None:
+ with open(OLD_USER_OPTIONS_FILE) as f:
+ data = f.read()
+
+ with open(USER_OPTIONS_FILE, "w") as f:
+ f.write(data)
+
+
+class UserOptions(OptionsManager):
+ def __init__(self):
+ if not os.path.exists(USER_OPTIONS_FILE) and os.path.exists(
+ OLD_USER_OPTIONS_FILE
+ ):
+ _migrate_old_options_file()
+
+ try:
+ super().__init__(file=USER_OPTIONS_FILE)
+ except FileNotFoundError:
+ pass
+
+ class User(OptionsGroup):
+ avatar: str = f"/var/lib/AccountsService/icons/{os.getenv('USER')}"
+
+ class Settings(OptionsGroup):
+ last_page: int = 0
+
+ class Material(OptionsGroup):
+ dark_mode: bool = True
+ colors: dict[str, str] = {}
+
+ user = User()
+ settings = Settings()
+ material = Material()
+
+
+user_options = UserOptions()