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()