├── .config ├── fastfetch │ └── config.jsonc ├── fish │ └── config.fish ├── hypr │ ├── custom │ │ ├── env.conf │ │ ├── exec.conf │ │ ├── general.conf │ │ ├── keybinds.conf │ │ ├── monitors.conf │ │ └── rules.conf │ ├── default │ │ ├── colors.conf │ │ ├── env.conf │ │ ├── exec.conf │ │ ├── general.conf │ │ ├── keybinds.conf │ │ ├── monitors.conf │ │ └── rules.conf │ ├── hyprland.conf │ ├── hyprlock.conf │ └── scripts │ │ └── hyprlock-time.sh └── kitty │ └── kitty.conf ├── .github └── ISSUE_TEMPLATE │ ├── 1-bug.yaml │ ├── 2-feature.yaml │ ├── 3-question.yaml │ └── 4-other.yaml ├── .gitignore ├── LICENSE ├── Material ├── gtk-3.0 │ ├── assets │ │ ├── bullet-symbolic.svg │ │ ├── bullet-symbolic.symbolic.png │ │ ├── bullet@2-symbolic.symbolic.png │ │ ├── check-symbolic.svg │ │ ├── check-symbolic.symbolic.png │ │ ├── check@2-symbolic.symbolic.png │ │ ├── dash-symbolic.svg │ │ ├── dash-symbolic.symbolic.png │ │ ├── dash@2-symbolic.symbolic.png │ │ ├── slider-horz-scale-has-marks-above-dark.png │ │ ├── slider-horz-scale-has-marks-above-dark@2.png │ │ ├── slider-horz-scale-has-marks-above-insensitive-dark.png │ │ ├── slider-horz-scale-has-marks-above-insensitive-dark@2.png │ │ ├── slider-horz-scale-has-marks-above-insensitive.png │ │ ├── slider-horz-scale-has-marks-above-insensitive@2.png │ │ ├── slider-horz-scale-has-marks-above.png │ │ ├── slider-horz-scale-has-marks-above@2.png │ │ ├── slider-horz-scale-has-marks-below-dark.png │ │ ├── slider-horz-scale-has-marks-below-dark@2.png │ │ ├── slider-horz-scale-has-marks-below-insensitive-dark.png │ │ ├── slider-horz-scale-has-marks-below-insensitive-dark@2.png │ │ ├── slider-horz-scale-has-marks-below-insensitive.png │ │ ├── slider-horz-scale-has-marks-below-insensitive@2.png │ │ ├── slider-horz-scale-has-marks-below.png │ │ ├── slider-horz-scale-has-marks-below@2.png │ │ ├── slider-vert-scale-has-marks-above-dark.png │ │ ├── slider-vert-scale-has-marks-above-dark@2.png │ │ ├── slider-vert-scale-has-marks-above-insensitive-dark.png │ │ ├── slider-vert-scale-has-marks-above-insensitive-dark@2.png │ │ ├── slider-vert-scale-has-marks-above-insensitive.png │ │ ├── slider-vert-scale-has-marks-above-insensitive@2.png │ │ ├── slider-vert-scale-has-marks-above.png │ │ ├── slider-vert-scale-has-marks-above@2.png │ │ ├── slider-vert-scale-has-marks-below-dark.png │ │ ├── slider-vert-scale-has-marks-below-dark@2.png │ │ ├── slider-vert-scale-has-marks-below-insensitive-dark.png │ │ ├── slider-vert-scale-has-marks-below-insensitive-dark@2.png │ │ ├── slider-vert-scale-has-marks-below-insensitive.png │ │ ├── slider-vert-scale-has-marks-below-insensitive@2.png │ │ ├── slider-vert-scale-has-marks-below.png │ │ ├── slider-vert-scale-has-marks-below@2.png │ │ ├── tab-border-dark.png │ │ ├── tab-border-dark@2.png │ │ ├── tab-border-light.png │ │ ├── tab-border-light@2.png │ │ ├── text-select-end-dark.png │ │ ├── text-select-end-dark@2.png │ │ ├── text-select-end.png │ │ ├── text-select-end@2.png │ │ ├── text-select-start-dark.png │ │ ├── text-select-start-dark@2.png │ │ ├── text-select-start.png │ │ └── text-select-start@2.png │ ├── gtk-dark.css │ ├── gtk.css │ ├── libadwaita-tweaks.css │ └── libadwaita.css ├── gtk-4.0 │ ├── gtk-dark.css │ └── gtk.css └── index.theme ├── README.md ├── assets ├── 1.png ├── 2.png ├── 3.png └── 4.png ├── dependencies.txt ├── ignis ├── __init__.py ├── config.py ├── icons │ └── hicolor │ │ └── scalable │ │ └── actions │ │ ├── chrome-symbolic.svg │ │ ├── firefox-browser-symbolic.svg │ │ └── spotify-symbolic.svg ├── misc │ └── media-art-fallback.png ├── modules │ ├── __init__.py │ ├── bar │ │ ├── __init__.py │ │ ├── bar.py │ │ ├── indicator_icon.py │ │ └── widgets │ │ │ ├── __init__.py │ │ │ ├── apps.py │ │ │ ├── battery.py │ │ │ ├── kb_layout.py │ │ │ ├── pill.py │ │ │ ├── tray.py │ │ │ └── workspaces.py │ ├── control_center │ │ ├── __init__.py │ │ ├── control_center.py │ │ ├── menu.py │ │ ├── qs_button.py │ │ └── widgets │ │ │ ├── __init__.py │ │ │ ├── brightness.py │ │ │ ├── media.py │ │ │ ├── media.scss │ │ │ ├── notification_center.py │ │ │ ├── quick_settings │ │ │ ├── __init__.py │ │ │ ├── bluetooth.py │ │ │ ├── dark_mode.py │ │ │ ├── dnd.py │ │ │ ├── ethernet.py │ │ │ ├── quick_settings.py │ │ │ ├── record.py │ │ │ ├── vpn.py │ │ │ └── wifi.py │ │ │ ├── user.py │ │ │ └── volume.py │ ├── launcher │ │ ├── __init__.py │ │ └── launcher.py │ ├── notification_popup │ │ ├── __init__.py │ │ └── notification_popup.py │ ├── osd │ │ ├── __init__.py │ │ └── osd.py │ ├── powermenu │ │ ├── __init__.py │ │ └── powermenu.py │ ├── settings │ │ ├── __init__.py │ │ ├── active_page.py │ │ ├── elements │ │ │ ├── __init__.py │ │ │ ├── entry.py │ │ │ ├── entryrow.py │ │ │ ├── filerow.py │ │ │ ├── group.py │ │ │ ├── page.py │ │ │ ├── row.py │ │ │ ├── spinrow.py │ │ │ └── switchrow.py │ │ ├── pages │ │ │ ├── __init__.py │ │ │ ├── about.py │ │ │ ├── appearance.py │ │ │ ├── notifications.py │ │ │ ├── recorder.py │ │ │ └── user.py │ │ └── settings.py │ └── shared_widgets │ │ ├── __init__.py │ │ ├── notification.py │ │ ├── toggle_box.py │ │ └── volume_slider.py ├── scripts │ └── recording.py ├── scss │ ├── bar.scss │ ├── control_center.scss │ ├── launcher.scss │ ├── lib.scss │ ├── mixins │ │ ├── hover.scss │ │ └── window.scss │ ├── notification_center.scss │ ├── notification_popup.scss │ ├── osd.scss │ ├── powermenu.scss │ └── settings.scss ├── services │ ├── __init__.py │ └── material │ │ ├── __init__.py │ │ ├── constants.py │ │ ├── sample_wall.png │ │ ├── service.py │ │ ├── templates │ │ ├── colors-hyprland.conf │ │ ├── colors-kitty.conf │ │ ├── colors.scss │ │ ├── gtk.css │ │ └── swaylock │ │ └── util.py ├── style.scss └── user_options.py ├── installation.md ├── keybindings.md ├── nvidia_deps.txt ├── pyproject.toml ├── requirements.txt └── ruff.toml /.config/fastfetch/config.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://github.com/fastfetch-cli/fastfetch/raw/dev/doc/json_schema.json", 3 | "logo": {"type": "small"}, 4 | "general": {"detectVersion": false}, 5 | "modules": [ 6 | "title", 7 | // "separator", 8 | {"type": "os", "format": "{name}"}, 9 | // "host", 10 | // "kernel", 11 | // "uptime", 12 | "packages", 13 | "shell", 14 | // "display", 15 | // "de", 16 | {"type": "wm", "format": "{pretty-name}"}, 17 | // "wmtheme", 18 | // "theme", 19 | // "icons", 20 | // "font", 21 | // "cursor", 22 | "terminal", 23 | // "terminalfont", 24 | // "cpu", 25 | // "gpu", 26 | "memory" 27 | // "swap", 28 | // "disk", 29 | // "localip", 30 | // "battery", 31 | // "poweradapter", 32 | // "locale", 33 | // "break", 34 | // "colors" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /.config/fish/config.fish: -------------------------------------------------------------------------------- 1 | if status is-interactive 2 | # Commands to run in interactive sessions can go here 3 | end 4 | 5 | set -g fish_greeting 6 | set --global fish_color_command blue -------------------------------------------------------------------------------- /.config/hypr/custom/env.conf: -------------------------------------------------------------------------------- 1 | # Define custom environment variables here 2 | # More info: https://wiki.hyprland.org/Configuring/Keywords/#setting-the-environment 3 | -------------------------------------------------------------------------------- /.config/hypr/custom/exec.conf: -------------------------------------------------------------------------------- 1 | # Define custom commands to execute here 2 | # More info: https://wiki.hyprland.org/Configuring/Keywords/#executing 3 | -------------------------------------------------------------------------------- /.config/hypr/custom/general.conf: -------------------------------------------------------------------------------- 1 | # Define custom general stuff here 2 | # More info: https://wiki.hyprland.org/Configuring/Variables/ 3 | -------------------------------------------------------------------------------- /.config/hypr/custom/keybinds.conf: -------------------------------------------------------------------------------- 1 | # Define custom keybinds here 2 | # More info: https://wiki.hyprland.org/Configuring/Binds/ 3 | -------------------------------------------------------------------------------- /.config/hypr/custom/monitors.conf: -------------------------------------------------------------------------------- 1 | # Configure monitors here 2 | # More info: https://wiki.hyprland.org/Configuring/Binds/ 3 | -------------------------------------------------------------------------------- /.config/hypr/custom/rules.conf: -------------------------------------------------------------------------------- 1 | # Define custom window and layer rules here 2 | # More info: https://wiki.hyprland.org/Configuring/Window-Rules/ 3 | -------------------------------------------------------------------------------- /.config/hypr/default/colors.conf: -------------------------------------------------------------------------------- 1 | # hyprlang noerror true 2 | source=~/.cache/ignis/material/dark_colors-hyprland.conf 3 | 4 | general { 5 | col.active_border = $primary 6 | col.inactive_border = rgb(000000) 7 | } 8 | # hyprlang noerror false -------------------------------------------------------------------------------- /.config/hypr/default/env.conf: -------------------------------------------------------------------------------- 1 | # Cursor 2 | env = XCURSOR_SIZE,24 3 | env = XCURSOR_THEME,Adwaita 4 | 5 | # Nvidia 6 | env = LIBVA_DRIVER_NAME,nvidia 7 | env = XDG_SESSION_TYPE,wayland 8 | env = GBM_BACKEND,nvidia-drm 9 | env = __GLX_VENDOR_LIBRARY_NAME,nvidia 10 | env = NVD_BACKEND,direct 11 | 12 | # Electron 13 | env = ELECTRON_OZONE_PLATFORM_HINT,auto 14 | 15 | # Firefox 16 | env = MOZ_DISABLE_RDD_SANDBOX,1 17 | env = EGL_PLATFORM,wayland 18 | env = MOZ_ENABLE_WAYLAND,1 19 | 20 | # For qt apps 21 | env = QT_QPA_PLATFORM,wayland 22 | env = QT_QPA_PLATFORMTHEME,qt5ct 23 | 24 | # Kinda hacky, but this is need to hot reload a GTK (4) theme 25 | env = GTK_THEME,Material -------------------------------------------------------------------------------- /.config/hypr/default/exec.conf: -------------------------------------------------------------------------------- 1 | exec-once = ignis init 2 | exec-once=/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1 3 | exec-once = dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP -------------------------------------------------------------------------------- /.config/hypr/default/general.conf: -------------------------------------------------------------------------------- 1 | cursor { 2 | no_hardware_cursors = true 3 | } 4 | 5 | input { 6 | kb_layout = 7 | kb_variant = 8 | kb_model = 9 | kb_options = grp:win_space_toggle 10 | kb_rules = 11 | accel_profile = flat 12 | 13 | follow_mouse = 1 14 | 15 | touchpad { 16 | natural_scroll = no 17 | } 18 | 19 | sensitivity = 0 20 | } 21 | 22 | general { 23 | gaps_in = 5 24 | gaps_out = 20 25 | border_size = 2 26 | resize_on_border=true 27 | layout = dwindle 28 | } 29 | 30 | decoration { 31 | rounding = 15 32 | 33 | blur { 34 | enabled = true 35 | size = 12 36 | passes = 4 37 | new_optimizations = true 38 | } 39 | 40 | shadow { 41 | enabled = true 42 | range = 30 43 | render_power = 4 44 | color = rgb(000000) 45 | } 46 | } 47 | 48 | animations { 49 | enabled = yes 50 | 51 | bezier = quart, 0.25, 1, 0.5, 1 52 | 53 | animation = windows, 1, 6, quart, slide 54 | animation = border, 1, 6, quart 55 | animation = borderangle, 1, 6, quart 56 | animation = fade, 1, 6, quart 57 | animation = workspaces, 1, 6, quart 58 | } 59 | 60 | dwindle { 61 | pseudotile = yes 62 | preserve_split = yes 63 | } 64 | 65 | 66 | misc { 67 | disable_hyprland_logo = true 68 | enable_anr_dialog = false 69 | } 70 | 71 | render { 72 | explicit_sync = true 73 | } -------------------------------------------------------------------------------- /.config/hypr/default/keybinds.conf: -------------------------------------------------------------------------------- 1 | # Key modifier 2 | $mainMod = SUPER 3 | 4 | # Binds to control windows 5 | bind = $mainMod, C, killactive, 6 | bind = $mainMod SHIFT, M, exit, # force quit Hyprland 7 | bind = $mainMod, V, togglefloating, 8 | bind = $mainMod, P, pseudo, # dwindle 9 | bind = $mainMod, J, togglesplit, # dwindle 10 | bind = ,F11, fullscreen, 0 11 | bind = $mainMod, G, centerwindow 12 | bind = $mainMod, D, pin 13 | 14 | # ignis 15 | bind = $mainMod, X, exec, ignis toggle-window ignis_LAUNCHER 16 | bind = $mainMod, M, exec, ignis toggle-window ignis_POWERMENU 17 | bind = ALT, F4, exec, ignis toggle-window ignis_POWERMENU 18 | bind = $mainMod, R, exec, ~/.config/ignis/scripts/recording.py start 19 | bind = $mainMod SHIFT, R, exec, ~/.config/ignis/scripts/recording.py continue 20 | bind = $mainMod, T, exec, ~/.config/ignis/scripts/recording.py stop 21 | bind = $mainMod SHIFT, T, exec, ~/.config/ignis/scripts/recording.py pause 22 | 23 | # Launch apps 24 | bind = $mainMod, Q, exec, kitty 25 | bind = $mainMod, L, exec, hyprlock 26 | bind = $mainMod, E, exec, thunar 27 | bind = $mainMod SHIFT, S, exec, GRIMBLAST_HIDE_CURSOR=0 grimblast --notify --freeze copysave area 28 | bind = $mainMod, S, exec, GRIMBLAST_HIDE_CURSOR=0 grimblast --notify --freeze copysave output 29 | bind = ,PRINT, exec, GRIMBLAST_HIDE_CURSOR=0 grimblast --notify --freeze copysave output 30 | 31 | # Focus control 32 | bind = $mainMod, left, movefocus, l 33 | bind = $mainMod, right, movefocus, r 34 | bind = $mainMod, up, movefocus, u 35 | bind = $mainMod, down, movefocus, d 36 | 37 | # Workspace switching 38 | bind = $mainMod, 1, workspace, 1 39 | bind = $mainMod, 2, workspace, 2 40 | bind = $mainMod, 3, workspace, 3 41 | bind = $mainMod, 4, workspace, 4 42 | bind = $mainMod, 5, workspace, 5 43 | bind = $mainMod, 6, workspace, 6 44 | bind = $mainMod, 7, workspace, 7 45 | bind = $mainMod, 8, workspace, 8 46 | bind = $mainMod, 9, workspace, 9 47 | bind = $mainMod, 0, workspace, 10 48 | 49 | # Move active window to workspace 50 | bind = $mainMod SHIFT, 1, movetoworkspace, 1 51 | bind = $mainMod SHIFT, 2, movetoworkspace, 2 52 | bind = $mainMod SHIFT, 3, movetoworkspace, 3 53 | bind = $mainMod SHIFT, 4, movetoworkspace, 4 54 | bind = $mainMod SHIFT, 5, movetoworkspace, 5 55 | bind = $mainMod SHIFT, 6, movetoworkspace, 6 56 | bind = $mainMod SHIFT, 7, movetoworkspace, 7 57 | bind = $mainMod SHIFT, 8, movetoworkspace, 8 58 | bind = $mainMod SHIFT, 9, movetoworkspace, 9 59 | bind = $mainMod SHIFT, 0, movetoworkspace, 10 60 | 61 | # Switch between existing workspaces by scrolling the mouse 62 | bind = $mainMod, mouse_down, workspace, e+1 63 | bind = $mainMod, mouse_up, workspace, e-1 64 | 65 | # Move and resize window 66 | bindm = $mainMod, mouse:272, movewindow 67 | bindm = $mainMod, mouse:273, resizewindow 68 | 69 | # Media binds 70 | bind = ,XF86AudioRaiseVolume, exec, pamixer -i 5 && ignis open-window ignis_OSD 71 | bind = ,XF86AudioLowerVolume, exec, pamixer -d 5 && ignis open-window ignis_OSD 72 | bind = ,XF86AudioMute, exec, pamixer -t && ignis open-window ignis_OSD -------------------------------------------------------------------------------- /.config/hypr/default/monitors.conf: -------------------------------------------------------------------------------- 1 | monitor = , preferred, auto, 1 -------------------------------------------------------------------------------- /.config/hypr/default/rules.conf: -------------------------------------------------------------------------------- 1 | windowrule = float, class:pavucontrol 2 | windowrule = pin, class:pavucontrol 3 | windowrule = size 900 500, class:pavucontrol 4 | 5 | windowrule = float, class:kitty 6 | windowrule = size 640 400, class:kitty 7 | 8 | windowrule = float,class:^(Material Settings)$ 9 | 10 | layerrule = blur,^(ignis_BAR.*)$ 11 | layerrule = noanim,^(ignis_NOTIFICATION_POPUP.*)$ 12 | layerrule = noanim,^(ignis_CONTROL_CENTER.*)$ -------------------------------------------------------------------------------- /.config/hypr/hyprland.conf: -------------------------------------------------------------------------------- 1 | # Add custom stuff to files in the `custom` directory 2 | 3 | # Defaults 4 | source=./default/colors.conf 5 | source=./default/monitors.conf 6 | source=./default/env.conf 7 | source=./default/exec.conf 8 | source=./default/general.conf 9 | source=./default/rules.conf 10 | source=./default/keybinds.conf 11 | 12 | # Custom 13 | source=./custom/monitors.conf 14 | source=./custom/env.conf 15 | source=./custom/exec.conf 16 | source=./custom/general.conf 17 | source=./custom/rules.conf 18 | source=./custom/keybinds.conf 19 | -------------------------------------------------------------------------------- /.config/hypr/hyprlock.conf: -------------------------------------------------------------------------------- 1 | source=~/.cache/ignis/material/dark_colors-hyprland.conf 2 | 3 | # BACKGROUND 4 | background { 5 | monitor = 6 | path = ~/.local/share/ignis/wallpaper 7 | blur_passes = 2 8 | contrast = 0.9 9 | brightness = 0.5 10 | vibrancy = 0.17 11 | vibrancy_darkness = 0 12 | } 13 | 14 | # GENERAL 15 | general { 16 | disable_loading_bar = true 17 | } 18 | 19 | # INPUT FIELD 20 | input-field { 21 | monitor = 22 | size = 300, 40 23 | outline_thickness = 2 24 | dots_size = 0.2 # Scale of input-field height, 0.2 - 0.8 25 | dots_spacing = 0.2 # Scale of dots' absolute size, 0.0 - 1.0 26 | dots_center = true 27 | outer_color = $surface 28 | inner_color = $surface 29 | font_color = $onSurface 30 | fade_on_empty = false 31 | placeholder_text = 32 | hide_input = false 33 | position = 0, 150 34 | halign = center 35 | valign = bottom 36 | } 37 | 38 | # Hour-Time 39 | label { 40 | monitor = 41 | text = cmd[update:1000] echo -e "$(date +"%H")" 42 | color = $primary 43 | font_family = JetBrainsMono Bold 44 | font_size = 180 45 | position = 0, 150 46 | halign = center 47 | valign = center 48 | } 49 | 50 | # Minute-Time 51 | label { 52 | monitor = 53 | text = cmd[update:1000] echo -e "$(date +"%M")" 54 | color = $onSurface 55 | font_family = JetBrainsMono Bold 56 | font_size = 180 57 | position = 0, -75 58 | halign = center 59 | valign = center 60 | } 61 | 62 | # Date 63 | label { 64 | monitor = 65 | text = cmd[update:1000] echo -e "$(date +"%a, %b %d")" 66 | color = $onSurface 67 | font_family = JetBrainsMono Bold 68 | position = 100, -100 69 | halign = left 70 | valign = top 71 | } 72 | 73 | # Date 74 | label { 75 | monitor = 76 | text = cmd[update:1000] primaryHex=$primaryHex bash ~/.config/hypr/scripts/hyprlock-time.sh 77 | color = $onSurface 78 | font_family = JetBrainsMono Bold 79 | position = 100, -130 80 | halign = left 81 | valign = top 82 | } -------------------------------------------------------------------------------- /.config/hypr/scripts/hyprlock-time.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | current_hour=$(date +"%H") 4 | user_string="$USER" 5 | 6 | if [ "$current_hour" -ge 5 ] && [ "$current_hour" -lt 12 ]; then 7 | echo "Good morning, $user_string" 8 | elif [ "$current_hour" -ge 12 ] && [ "$current_hour" -lt 18 ]; then 9 | echo "Good day, $user_string" 10 | elif [ "$current_hour" -ge 18 ] && [ "$current_hour" -lt 22 ]; then 11 | echo "Good evening, $user_string" 12 | else 13 | echo "Good night, $user_string" 14 | fi 15 | -------------------------------------------------------------------------------- /.config/kitty/kitty.conf: -------------------------------------------------------------------------------- 1 | include ~/.cache/ignis/material/dark_colors-kitty.conf 2 | font_size 12 3 | font_family JetBrainsMono 4 | window_margin_width 15 5 | remember_window_size no 6 | background_opacity 1 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-bug.yaml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Something is not working as expected 3 | labels: ["bug"] 4 | body: 5 | - type: textarea 6 | attributes: 7 | label: System Information 8 | description: | 9 | Paste the output of `ignis systeminfo` below. 10 | 11 | value: " 12 | ``` 13 | 14 | 15 | 16 | ```" 17 | validations: 18 | required: true 19 | 20 | - type: textarea 21 | attributes: 22 | label: Description 23 | description: "Provide a clear and concise description of the issue you encountered" 24 | validations: 25 | required: true 26 | 27 | - type: textarea 28 | attributes: 29 | label: How to reproduce 30 | description: "List the steps to reproduce the issue. Be as detailed as possible to help replicate the problem" 31 | validations: 32 | required: true 33 | 34 | - type: textarea 35 | attributes: 36 | label: Additional Information 37 | description: | 38 | Anything that can help (Logs, Images, Videos, Configs, etc.). Please avoid pasting large text directly. 39 | Logs can be found at `~/.ignis/ignis.log`. 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-feature.yaml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest a new feature or improvement 3 | labels: ["enhancement"] 4 | body: 5 | - type: textarea 6 | attributes: 7 | label: Feature Description 8 | description: "Clearly describe the feature or improvement you'd like to see" 9 | validations: 10 | required: true 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3-question.yaml: -------------------------------------------------------------------------------- 1 | name: Question 2 | description: I'd like to ask a question 3 | labels: ["question"] 4 | body: 5 | - type: textarea 6 | attributes: 7 | label: Question 8 | description: "Describe your question" 9 | validations: 10 | required: true 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/4-other.yaml: -------------------------------------------------------------------------------- 1 | name: Other 2 | description: Something else? if the options above do not suit 3 | body: 4 | - type: textarea 5 | attributes: 6 | label: Description 7 | description: "Provide a description of your issue" 8 | validations: 9 | required: true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | __pycache__ 3 | fishd.tmp* 4 | venv -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/bullet-symbolic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/bullet-symbolic.symbolic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/bullet-symbolic.symbolic.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/bullet@2-symbolic.symbolic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/bullet@2-symbolic.symbolic.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/check-symbolic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/check-symbolic.symbolic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/check-symbolic.symbolic.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/check@2-symbolic.symbolic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/check@2-symbolic.symbolic.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/dash-symbolic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/dash-symbolic.symbolic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/dash-symbolic.symbolic.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/dash@2-symbolic.symbolic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/dash@2-symbolic.symbolic.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/slider-horz-scale-has-marks-above-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/slider-horz-scale-has-marks-above-dark.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/slider-horz-scale-has-marks-above-dark@2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/slider-horz-scale-has-marks-above-dark@2.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/slider-horz-scale-has-marks-above-insensitive-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/slider-horz-scale-has-marks-above-insensitive-dark.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/slider-horz-scale-has-marks-above-insensitive-dark@2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/slider-horz-scale-has-marks-above-insensitive-dark@2.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/slider-horz-scale-has-marks-above-insensitive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/slider-horz-scale-has-marks-above-insensitive.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/slider-horz-scale-has-marks-above-insensitive@2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/slider-horz-scale-has-marks-above-insensitive@2.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/slider-horz-scale-has-marks-above.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/slider-horz-scale-has-marks-above.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/slider-horz-scale-has-marks-above@2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/slider-horz-scale-has-marks-above@2.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/slider-horz-scale-has-marks-below-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/slider-horz-scale-has-marks-below-dark.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/slider-horz-scale-has-marks-below-dark@2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/slider-horz-scale-has-marks-below-dark@2.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/slider-horz-scale-has-marks-below-insensitive-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/slider-horz-scale-has-marks-below-insensitive-dark.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/slider-horz-scale-has-marks-below-insensitive-dark@2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/slider-horz-scale-has-marks-below-insensitive-dark@2.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/slider-horz-scale-has-marks-below-insensitive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/slider-horz-scale-has-marks-below-insensitive.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/slider-horz-scale-has-marks-below-insensitive@2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/slider-horz-scale-has-marks-below-insensitive@2.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/slider-horz-scale-has-marks-below.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/slider-horz-scale-has-marks-below.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/slider-horz-scale-has-marks-below@2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/slider-horz-scale-has-marks-below@2.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/slider-vert-scale-has-marks-above-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/slider-vert-scale-has-marks-above-dark.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/slider-vert-scale-has-marks-above-dark@2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/slider-vert-scale-has-marks-above-dark@2.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/slider-vert-scale-has-marks-above-insensitive-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/slider-vert-scale-has-marks-above-insensitive-dark.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/slider-vert-scale-has-marks-above-insensitive-dark@2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/slider-vert-scale-has-marks-above-insensitive-dark@2.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/slider-vert-scale-has-marks-above-insensitive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/slider-vert-scale-has-marks-above-insensitive.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/slider-vert-scale-has-marks-above-insensitive@2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/slider-vert-scale-has-marks-above-insensitive@2.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/slider-vert-scale-has-marks-above.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/slider-vert-scale-has-marks-above.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/slider-vert-scale-has-marks-above@2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/slider-vert-scale-has-marks-above@2.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/slider-vert-scale-has-marks-below-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/slider-vert-scale-has-marks-below-dark.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/slider-vert-scale-has-marks-below-dark@2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/slider-vert-scale-has-marks-below-dark@2.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/slider-vert-scale-has-marks-below-insensitive-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/slider-vert-scale-has-marks-below-insensitive-dark.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/slider-vert-scale-has-marks-below-insensitive-dark@2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/slider-vert-scale-has-marks-below-insensitive-dark@2.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/slider-vert-scale-has-marks-below-insensitive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/slider-vert-scale-has-marks-below-insensitive.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/slider-vert-scale-has-marks-below-insensitive@2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/slider-vert-scale-has-marks-below-insensitive@2.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/slider-vert-scale-has-marks-below.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/slider-vert-scale-has-marks-below.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/slider-vert-scale-has-marks-below@2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/slider-vert-scale-has-marks-below@2.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/tab-border-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/tab-border-dark.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/tab-border-dark@2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/tab-border-dark@2.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/tab-border-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/tab-border-light.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/tab-border-light@2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/tab-border-light@2.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/text-select-end-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/text-select-end-dark.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/text-select-end-dark@2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/text-select-end-dark@2.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/text-select-end.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/text-select-end.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/text-select-end@2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/text-select-end@2.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/text-select-start-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/text-select-start-dark.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/text-select-start-dark@2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/text-select-start-dark@2.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/text-select-start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/text-select-start.png -------------------------------------------------------------------------------- /Material/gtk-3.0/assets/text-select-start@2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/Material/gtk-3.0/assets/text-select-start@2.png -------------------------------------------------------------------------------- /Material/gtk-3.0/gtk-dark.css: -------------------------------------------------------------------------------- 1 | @import url("gtk.css"); -------------------------------------------------------------------------------- /Material/gtk-3.0/libadwaita-tweaks.css: -------------------------------------------------------------------------------- 1 | /* 2 | This file will fix some legacy widget styles that aren't styled in libadwaita 3 | */ 4 | 5 | /* add a bg color to notebook headers */ 6 | notebook > header { 7 | background-color: @headerbar_bg_color; 8 | border-color: mix(currentColor,@window_bg_color,0.85); 9 | } 10 | -------------------------------------------------------------------------------- /Material/gtk-4.0/gtk-dark.css: -------------------------------------------------------------------------------- 1 | @import url("gtk.css"); -------------------------------------------------------------------------------- /Material/gtk-4.0/gtk.css: -------------------------------------------------------------------------------- 1 | /* GTK NAMED COLORS ---------------- use responsibly! */ 2 | @define-color accent_bg_color @blue_3; 3 | @define-color accent_fg_color white; 4 | @define-color accent_color @blue_4; 5 | @define-color destructive_bg_color @red_3; 6 | @define-color destructive_fg_color white; 7 | @define-color destructive_color @red_4; 8 | @define-color success_bg_color @green_4; 9 | @define-color success_fg_color white; 10 | @define-color success_color #1b8553; 11 | @define-color warning_bg_color @yellow_5; 12 | @define-color warning_fg_color rgba(0, 0, 0, 0.8); 13 | @define-color warning_color #9c6e03; 14 | @define-color error_bg_color @red_3; 15 | @define-color error_fg_color white; 16 | @define-color error_color @red_4; 17 | @define-color window_bg_color #fafafa; 18 | @define-color window_fg_color rgba(0, 0, 0, 0.8); 19 | @define-color view_bg_color #ffffff; 20 | @define-color view_fg_color rgba(0, 0, 0, 0.8); 21 | @define-color headerbar_bg_color #ffffff; 22 | @define-color headerbar_fg_color rgba(0, 0, 0, 0.8); 23 | @define-color headerbar_border_color rgba(0, 0, 0, 0.8); 24 | @define-color headerbar_backdrop_color @window_bg_color; 25 | @define-color headerbar_shade_color rgba(0, 0, 0, 0.12); 26 | @define-color headerbar_darker_shade_color rgba(0, 0, 0, 0.12); 27 | @define-color sidebar_bg_color #ebebeb; 28 | @define-color sidebar_fg_color rgba(0, 0, 0, 0.8); 29 | @define-color sidebar_backdrop_color #f2f2f2; 30 | @define-color sidebar_shade_color rgba(0, 0, 0, 0.07); 31 | @define-color sidebar_border_color rgba(0, 0, 0, 0.07); 32 | @define-color secondary_sidebar_bg_color #f3f3f3; 33 | @define-color secondary_sidebar_fg_color rgba(0, 0, 0, 0.8); 34 | @define-color secondary_sidebar_backdrop_color #f6f6f6; 35 | @define-color secondary_sidebar_shade_color rgba(0, 0, 0, 0.07); 36 | @define-color secondary_sidebar_border_color rgba(0, 0, 0, 0.07); 37 | @define-color card_bg_color #ffffff; 38 | @define-color card_fg_color rgba(0, 0, 0, 0.8); 39 | @define-color card_shade_color rgba(0, 0, 0, 0.07); 40 | @define-color dialog_bg_color #fafafa; 41 | @define-color dialog_fg_color rgba(0, 0, 0, 0.8); 42 | @define-color popover_bg_color #ffffff; 43 | @define-color popover_fg_color rgba(0, 0, 0, 0.8); 44 | @define-color popover_shade_color rgba(0, 0, 0, 0.07); 45 | @define-color thumbnail_bg_color #ffffff; 46 | @define-color thumbnail_fg_color rgba(0, 0, 0, 0.8); 47 | @define-color shade_color rgba(0, 0, 0, 0.07); 48 | @define-color scrollbar_outline_color white; 49 | @import '../gtk-3.0/libadwaita.css'; 50 | @import '../gtk-3.0/libadwaita-tweaks.css'; 51 | @import url("../../../../../.cache/ignis/material/gtk.css"); 52 | -------------------------------------------------------------------------------- /Material/index.theme: -------------------------------------------------------------------------------- 1 | [X-GNOME-Metatheme] 2 | Name=adw-gtk3 3 | Type=X-GNOME-Metatheme 4 | Comment=adw-gtk3 theme 5 | Encoding=UTF-8 6 | GtkTheme=adw-gtk3 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

linkfrg's Hyprland(and Ignis!) dotfiles

3 | 4 | 5 | 6 |
7 | 8 | 9 | ## Gallery 10 | 11 | 12 | 13 | 14 | 15 | 16 | ## Features 17 | 18 | - **Dynamic**: Autogenerated material colors based on your wallpaper 19 | - **Dark and light theme**: Just toggle button in control center 20 | - **Control center**: Quick access to everything you need 21 | - **Settings app**: GUI app to adjust Ignis options 22 | 23 | ## Installation 24 | 25 | See [Installation](./installation.md) 26 | 27 | ## Keybindings 28 | 29 | See [Keybindings](./keybindings.md) 30 | -------------------------------------------------------------------------------- /assets/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/assets/1.png -------------------------------------------------------------------------------- /assets/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/assets/2.png -------------------------------------------------------------------------------- /assets/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/assets/3.png -------------------------------------------------------------------------------- /assets/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/assets/4.png -------------------------------------------------------------------------------- /dependencies.txt: -------------------------------------------------------------------------------- 1 | hyprland 2 | xdg-desktop-portal-hyprland 3 | xorg-xwayland 4 | qt5-wayland 5 | qt6-wayland 6 | qt5ct 7 | qt6ct 8 | libva 9 | linux-headers 10 | pipewire 11 | pipewire-alsa 12 | pipewire-pulse 13 | pipewire-jack 14 | pavucontrol 15 | wireplumber 16 | ignis-git 17 | dart-sass 18 | nm-connection-editor 19 | polkit-gnome 20 | hyprlock 21 | pamixer 22 | grimblast-git 23 | kitty 24 | thunar 25 | thunar-archive-plugin 26 | file-roller 27 | xdg-user-dirs 28 | python-requests 29 | python-jinja 30 | python-materialyoucolor 31 | python-pillow 32 | gst-plugin-pipewire 33 | gst-plugins-good 34 | gst-plugins-ugly 35 | gst-plugins-base 36 | networkmanager 37 | ttf-jetbrains-mono 38 | ttf-jetbrains-mono-nerd 39 | ttf-nerd-fonts-symbols 40 | papirus-icon-theme 41 | upower 42 | wl-clipboard 43 | gnome-bluetooth-3.0 -------------------------------------------------------------------------------- /ignis/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/ignis/__init__.py -------------------------------------------------------------------------------- /ignis/config.py: -------------------------------------------------------------------------------- 1 | from ignis.utils import Utils 2 | from ignis.app import IgnisApp 3 | from ignis.services.wallpaper import WallpaperService 4 | from modules import ( 5 | Bar, 6 | ControlCenter, 7 | Launcher, 8 | NotificationPopup, 9 | OSD, 10 | Powermenu, 11 | Settings, 12 | ) 13 | 14 | app = IgnisApp.get_default() 15 | WallpaperService.get_default() 16 | 17 | app.add_icons(f"{Utils.get_current_dir()}/icons") 18 | app.apply_css(Utils.get_current_dir() + "/style.scss") 19 | 20 | Utils.exec_sh("gsettings set org.gnome.desktop.interface gtk-theme Material") 21 | Utils.exec_sh("gsettings set org.gnome.desktop.interface icon-theme Papirus") 22 | Utils.exec_sh( 23 | 'gsettings set org.gnome.desktop.interface font-name "JetBrains Mono Regular 11"' 24 | ) 25 | Utils.exec_sh("hyprctl reload") 26 | 27 | 28 | ControlCenter() 29 | 30 | for monitor in range(Utils.get_n_monitors()): 31 | Bar(monitor) 32 | 33 | for monitor in range(Utils.get_n_monitors()): 34 | NotificationPopup(monitor) 35 | 36 | Launcher() 37 | Powermenu() 38 | OSD() 39 | 40 | Settings() 41 | -------------------------------------------------------------------------------- /ignis/icons/hicolor/scalable/actions/chrome-symbolic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ignis/icons/hicolor/scalable/actions/firefox-browser-symbolic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ignis/icons/hicolor/scalable/actions/spotify-symbolic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ignis/misc/media-art-fallback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/ignis/misc/media-art-fallback.png -------------------------------------------------------------------------------- /ignis/modules/__init__.py: -------------------------------------------------------------------------------- 1 | from .bar import Bar 2 | from .control_center import ControlCenter 3 | from .launcher import Launcher 4 | from .notification_popup import NotificationPopup 5 | from .osd import OSD 6 | from .powermenu import Powermenu 7 | from .settings import Settings 8 | 9 | __all__ = [ 10 | "Bar", 11 | "ControlCenter", 12 | "Launcher", 13 | "NotificationPopup", 14 | "OSD", 15 | "Powermenu", 16 | "Settings", 17 | ] 18 | -------------------------------------------------------------------------------- /ignis/modules/bar/__init__.py: -------------------------------------------------------------------------------- 1 | from .bar import Bar 2 | 3 | __all__ = ["Bar"] 4 | -------------------------------------------------------------------------------- /ignis/modules/bar/bar.py: -------------------------------------------------------------------------------- 1 | from ignis.widgets import Widget 2 | from .widgets import StatusPill, Tray, KeyboardLayout, Battery, Apps, Workspaces 3 | 4 | 5 | class Bar(Widget.Window): 6 | __gtype_name__ = "Bar" 7 | 8 | def __init__(self, monitor: int): 9 | super().__init__( 10 | anchor=["left", "top", "right"], 11 | exclusivity="exclusive", 12 | monitor=monitor, 13 | namespace=f"ignis_BAR_{monitor}", 14 | layer="top", 15 | kb_mode="none", 16 | child=Widget.CenterBox( 17 | css_classes=["bar-widget"], 18 | start_widget=Widget.Box(child=[Workspaces()]), 19 | center_widget=Widget.Box(child=[Apps()]), 20 | end_widget=Widget.Box( 21 | child=[Tray(), KeyboardLayout(), Battery(), StatusPill(monitor)] 22 | ), 23 | ), 24 | css_classes=["unset"], 25 | ) 26 | -------------------------------------------------------------------------------- /ignis/modules/bar/indicator_icon.py: -------------------------------------------------------------------------------- 1 | from ignis.widgets import Widget 2 | from ignis.services.network import Ethernet, Wifi 3 | 4 | 5 | class IndicatorIcon(Widget.Icon): 6 | def __init__(self, css_classes: list[str] = [], **kwargs): 7 | super().__init__( 8 | style="margin-right: 0.5rem;", css_classes=["unset"] + css_classes, **kwargs 9 | ) 10 | 11 | 12 | class NetworkIndicatorIcon(IndicatorIcon): 13 | def __init__( 14 | self, device_type: Ethernet | Wifi, other_device_type: Wifi | Ethernet 15 | ): 16 | self._device_type = device_type 17 | self._other_device_type = other_device_type 18 | 19 | super().__init__(icon_name=device_type.bind("icon-name")) 20 | 21 | for binding in ( 22 | device_type.bind("devices", self.__check_visibility), 23 | other_device_type.bind("is_connected", self.__check_visibility), 24 | device_type.bind("is_connected", self.__check_visibility), 25 | ): 26 | self.visible = binding # type: ignore 27 | 28 | def __check_visibility(self, *args) -> bool: 29 | return len(self._device_type.devices) > 0 and ( 30 | not self._other_device_type.is_connected or self._device_type.is_connected 31 | ) 32 | -------------------------------------------------------------------------------- /ignis/modules/bar/widgets/__init__.py: -------------------------------------------------------------------------------- 1 | from .pill import StatusPill 2 | from .tray import Tray 3 | from .kb_layout import KeyboardLayout 4 | from .battery import Battery 5 | from .apps import Apps 6 | from .workspaces import Workspaces 7 | 8 | __all__ = ["StatusPill", "Tray", "KeyboardLayout", "Battery", "Apps", "Workspaces"] 9 | -------------------------------------------------------------------------------- /ignis/modules/bar/widgets/apps.py: -------------------------------------------------------------------------------- 1 | from ignis.widgets import Widget 2 | from ignis.app import IgnisApp 3 | from ignis.services.applications import ApplicationsService, Application 4 | from ignis.menu_model import IgnisMenuModel, IgnisMenuItem, IgnisMenuSeparator 5 | 6 | applications = ApplicationsService.get_default() 7 | app = IgnisApp.get_default() 8 | 9 | TERMINAL_FORMAT = "kitty %command%" 10 | 11 | 12 | class AppItem(Widget.Button): 13 | def __init__(self, app: Application): 14 | menu = Widget.PopoverMenu( 15 | model=IgnisMenuModel( 16 | IgnisMenuItem(label="Launch", on_activate=lambda x: app.launch()), 17 | IgnisMenuSeparator(), 18 | *( 19 | IgnisMenuItem( 20 | label=i.name, on_activate=lambda x, action=i: action.launch() 21 | ) 22 | for i in app.actions 23 | ), 24 | IgnisMenuSeparator(), 25 | IgnisMenuItem(label="Unpin", on_activate=lambda x: app.unpin()), 26 | ) 27 | ) 28 | 29 | super().__init__( 30 | child=Widget.Box(child=[Widget.Icon(image=app.icon, pixel_size=32), menu]), 31 | on_click=lambda x: app.launch(terminal_format=TERMINAL_FORMAT), 32 | on_right_click=lambda x: menu.popup(), 33 | css_classes=["pinned-app", "unset"], 34 | ) 35 | 36 | 37 | class Apps(Widget.Box): 38 | def __init__(self): 39 | super().__init__( 40 | child=applications.bind( 41 | "pinned", 42 | transform=lambda value: [AppItem(app) for app in value] 43 | + [ 44 | Widget.Button( 45 | child=Widget.Icon(image="start-here-symbolic", pixel_size=32), 46 | on_click=lambda x: app.toggle_window("ignis_LAUNCHER"), 47 | css_classes=["pinned-app", "unset"], 48 | ) 49 | ], 50 | ) 51 | ) 52 | -------------------------------------------------------------------------------- /ignis/modules/bar/widgets/battery.py: -------------------------------------------------------------------------------- 1 | from ignis.widgets import Widget 2 | from ignis.services.upower import UPowerService, UPowerDevice 3 | 4 | upower = UPowerService.get_default() 5 | 6 | 7 | class BatteryItem(Widget.Box): 8 | def __init__(self, device: UPowerDevice): 9 | super().__init__( 10 | css_classes=["battery-item"], 11 | setup=lambda self: device.connect("removed", lambda x: self.unparent()), 12 | child=[ 13 | Widget.Icon( 14 | icon_name=device.bind("icon_name"), css_classes=["battery-icon"] 15 | ), 16 | Widget.Label( 17 | label=device.bind("percent", lambda x: f"{int(x)}%"), 18 | css_classes=["battery-percent"], 19 | ), 20 | Widget.Scale( 21 | min=0, 22 | max=100, 23 | value=device.bind("percent"), 24 | sensitive=False, 25 | css_classes=["battery-scale"], 26 | ), 27 | ], 28 | ) 29 | 30 | 31 | class Battery(Widget.Box): 32 | def __init__(self): 33 | super().__init__( 34 | setup=lambda self: upower.connect( 35 | "battery-added", lambda x, device: self.append(BatteryItem(device)) 36 | ), 37 | ) 38 | -------------------------------------------------------------------------------- /ignis/modules/bar/widgets/kb_layout.py: -------------------------------------------------------------------------------- 1 | from ignis.widgets import Widget 2 | from ignis.services.hyprland import HyprlandService 3 | 4 | hyprland = HyprlandService.get_default() 5 | 6 | 7 | class KeyboardLayout(Widget.Button): 8 | def __init__(self): 9 | super().__init__( 10 | css_classes=["kb-layout", "unset"], 11 | on_click=lambda x: hyprland.main_keyboard.switch_layout("next"), 12 | visible=hyprland.is_available, 13 | child=Widget.Label( 14 | label=hyprland.main_keyboard.bind( 15 | "active_keymap", transform=lambda value: value[:2].lower() 16 | ) 17 | ), 18 | ) 19 | -------------------------------------------------------------------------------- /ignis/modules/bar/widgets/pill.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from ignis.widgets import Widget 3 | from ignis.app import IgnisApp 4 | from ignis.utils import Utils 5 | from ignis.variable import Variable 6 | from ignis.services.network import NetworkService 7 | from ignis.services.notifications import NotificationService 8 | from ignis.services.recorder import RecorderService 9 | from ignis.services.audio import AudioService 10 | from ..indicator_icon import IndicatorIcon, NetworkIndicatorIcon 11 | from ignis.options import options 12 | 13 | network = NetworkService.get_default() 14 | notifications = NotificationService.get_default() 15 | recorder = RecorderService.get_default() 16 | audio = AudioService.get_default() 17 | 18 | app = IgnisApp.get_default() 19 | 20 | current_time = Variable( 21 | value=Utils.Poll(1000, lambda x: datetime.datetime.now().strftime("%H:%M")).bind( 22 | "output" 23 | ) 24 | ) 25 | 26 | 27 | class WifiIcon(NetworkIndicatorIcon): 28 | def __init__(self): 29 | super().__init__(device_type=network.wifi, other_device_type=network.ethernet) 30 | 31 | 32 | class EthernetIcon(NetworkIndicatorIcon): 33 | def __init__(self): 34 | super().__init__(device_type=network.ethernet, other_device_type=network.wifi) 35 | 36 | 37 | class VpnIcon(IndicatorIcon): 38 | def __init__(self): 39 | super().__init__( 40 | image=network.vpn.bind("icon_name"), 41 | visible=network.vpn.bind("is_connected"), 42 | ) 43 | 44 | 45 | class DNDIcon(IndicatorIcon): 46 | def __init__(self): 47 | super().__init__( 48 | image="notification-disabled-symbolic", 49 | visible=options.notifications.bind("dnd"), 50 | ) 51 | 52 | 53 | class RecorderIcon(IndicatorIcon): 54 | def __init__(self): 55 | super().__init__( 56 | image="media-record-symbolic", 57 | css_classes=["record-indicator"], 58 | setup=lambda self: recorder.connect( 59 | "notify::is-paused", self.__update_css_class 60 | ), 61 | visible=recorder.bind("active"), 62 | ) 63 | 64 | def __update_css_class(self, *args) -> None: 65 | if recorder.is_paused: 66 | self.remove_css_class("active") 67 | else: 68 | self.add_css_class("active") 69 | 70 | 71 | class VolumeIcon(IndicatorIcon): 72 | def __init__(self): 73 | super().__init__( 74 | image=audio.speaker.bind("icon_name"), 75 | ) 76 | 77 | 78 | class StatusPill(Widget.Button): 79 | def __init__(self, monitor: int): 80 | self._monitor = monitor 81 | self._window: Widget.Window = app.get_window("ignis_CONTROL_CENTER") # type: ignore 82 | 83 | super().__init__( 84 | child=Widget.Box( 85 | child=[ 86 | RecorderIcon(), 87 | WifiIcon(), 88 | EthernetIcon(), 89 | VpnIcon(), 90 | VolumeIcon(), 91 | DNDIcon(), 92 | Widget.Label( 93 | label=current_time.bind("value"), 94 | ), 95 | ] 96 | ), 97 | css_classes=self._window.bind( 98 | "visible", 99 | lambda value: ["clock", "unset", "active"] 100 | if value 101 | else ["clock", "unset"], 102 | ), 103 | on_click=self.__on_click, 104 | ) 105 | 106 | def __on_click(self, x) -> None: 107 | if self._window.monitor == self._monitor: 108 | self._window.visible = not self._window.visible 109 | else: 110 | self._window.set_monitor(self._monitor) 111 | self._window.visible = True 112 | -------------------------------------------------------------------------------- /ignis/modules/bar/widgets/tray.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from ignis.widgets import Widget 3 | from ignis.services.system_tray import SystemTrayService, SystemTrayItem 4 | 5 | system_tray = SystemTrayService.get_default() 6 | 7 | 8 | class TrayItem(Widget.Button): 9 | __gtype_name__ = "TrayItem" 10 | 11 | def __init__(self, item: SystemTrayItem): 12 | if item.menu: 13 | menu = item.menu.copy() 14 | else: 15 | menu = None 16 | 17 | super().__init__( 18 | child=Widget.Box( 19 | child=[ 20 | Widget.Icon(image=item.bind("icon"), pixel_size=24), 21 | menu, 22 | ] 23 | ), 24 | tooltip_text=item.bind("tooltip"), 25 | on_click=lambda x: asyncio.create_task(item.activate_async()), 26 | setup=lambda self: item.connect("removed", lambda x: self.unparent()), 27 | on_right_click=lambda x: menu.popup() if menu else None, 28 | css_classes=["tray-item", "unset"], 29 | ) 30 | 31 | 32 | class Tray(Widget.Box): 33 | __gtype_name__ = "Tray" 34 | 35 | def __init__(self): 36 | super().__init__( 37 | css_classes=["tray"], 38 | setup=lambda self: system_tray.connect( 39 | "added", lambda x, item: self.append(TrayItem(item)) 40 | ), 41 | spacing=10, 42 | ) 43 | -------------------------------------------------------------------------------- /ignis/modules/bar/widgets/workspaces.py: -------------------------------------------------------------------------------- 1 | from ignis.widgets import Widget 2 | from ignis.services.hyprland import HyprlandService, HyprlandWorkspace 3 | 4 | hyprland = HyprlandService.get_default() 5 | 6 | 7 | class WorkspaceButton(Widget.Button): 8 | def __init__(self, workspace: HyprlandWorkspace) -> None: 9 | super().__init__( 10 | css_classes=["workspace", "unset"], 11 | on_click=lambda x: workspace.switch_to(), 12 | halign="start", 13 | valign="center", 14 | ) 15 | if workspace.id == hyprland.active_workspace.id: 16 | self.add_css_class("active") 17 | 18 | 19 | def scroll_workspaces(direction: str) -> None: 20 | current = hyprland.active_workspace.id 21 | if direction == "up": 22 | target = current - 1 23 | hyprland.switch_to_workspace(target) 24 | else: 25 | target = current + 1 26 | if target == 11: 27 | return 28 | hyprland.switch_to_workspace(target) 29 | 30 | 31 | class Workspaces(Widget.Box): 32 | def __init__(self): 33 | if hyprland.is_available: 34 | child = [ 35 | Widget.EventBox( 36 | on_scroll_up=lambda x: scroll_workspaces("up"), 37 | on_scroll_down=lambda x: scroll_workspaces("down"), 38 | css_classes=["workspaces"], 39 | child=hyprland.bind_many( 40 | ["workspaces", "active_workspace"], 41 | transform=lambda workspaces, *_: [ 42 | WorkspaceButton(i) for i in workspaces 43 | ], 44 | ), 45 | ) 46 | ] 47 | else: 48 | child = [] 49 | super().__init__(child=child) 50 | -------------------------------------------------------------------------------- /ignis/modules/control_center/__init__.py: -------------------------------------------------------------------------------- 1 | from .control_center import ControlCenter 2 | 3 | __all__ = ["ControlCenter"] 4 | -------------------------------------------------------------------------------- /ignis/modules/control_center/control_center.py: -------------------------------------------------------------------------------- 1 | from ignis.widgets import Widget 2 | from ignis.app import IgnisApp 3 | from .widgets import ( 4 | QuickSettings, 5 | Brightness, 6 | VolumeSlider, 7 | User, 8 | Media, 9 | NotificationCenter, 10 | ) 11 | from .menu import opened_menu 12 | 13 | app = IgnisApp.get_default() 14 | 15 | 16 | class ControlCenter(Widget.RevealerWindow): 17 | def __init__(self): 18 | revealer = Widget.Revealer( 19 | transition_type="slide_left", 20 | child=Widget.Box( 21 | vertical=True, 22 | css_classes=["control-center"], 23 | child=[ 24 | Widget.Box( 25 | vertical=True, 26 | css_classes=["control-center-widget"], 27 | child=[ 28 | QuickSettings(), 29 | VolumeSlider("speaker"), 30 | VolumeSlider("microphone"), 31 | Brightness(), 32 | User(), 33 | Media(), 34 | ], 35 | ), 36 | NotificationCenter(), 37 | ], 38 | ), 39 | transition_duration=300, 40 | reveal_child=True, 41 | ) 42 | 43 | super().__init__( 44 | visible=False, 45 | popup=True, 46 | kb_mode="on_demand", 47 | layer="top", 48 | css_classes=["unset"], 49 | anchor=["top", "right", "bottom", "left"], 50 | namespace="ignis_CONTROL_CENTER", 51 | child=Widget.Box( 52 | child=[ 53 | Widget.Button( 54 | vexpand=True, 55 | hexpand=True, 56 | css_classes=["unset"], 57 | on_click=lambda x: app.close_window("ignis_CONTROL_CENTER"), 58 | ), 59 | revealer, 60 | ], 61 | ), 62 | setup=lambda self: self.connect( 63 | "notify::visible", lambda x, y: opened_menu.set_value("") 64 | ), 65 | revealer=revealer, 66 | ) 67 | -------------------------------------------------------------------------------- /ignis/modules/control_center/menu.py: -------------------------------------------------------------------------------- 1 | from gi.repository import GObject # type: ignore 2 | from ignis.widgets import Widget 3 | from ignis.variable import Variable 4 | from ignis.base_widget import BaseWidget 5 | 6 | opened_menu = Variable() 7 | 8 | 9 | class Menu(Widget.Revealer): 10 | def __init__(self, name: str, child: list[BaseWidget], **kwargs): 11 | self._name = name 12 | self._box = Widget.Box( 13 | vertical=True, 14 | css_classes=["control-center-menu"], 15 | child=child, 16 | ) 17 | 18 | super().__init__( 19 | transition_type="slide_down", 20 | transition_duration=300, 21 | reveal_child=opened_menu.bind("value", lambda value: value == self._name), 22 | child=self._box, 23 | **kwargs, 24 | ) 25 | 26 | def toggle(self) -> None: 27 | if self.reveal_child: 28 | opened_menu.value = "" 29 | else: 30 | opened_menu.value = self._name 31 | 32 | @GObject.Property 33 | def box(self) -> Widget.Box: 34 | return self._box 35 | -------------------------------------------------------------------------------- /ignis/modules/control_center/qs_button.py: -------------------------------------------------------------------------------- 1 | from ignis.widgets import Widget 2 | from gi.repository import GObject # type: ignore 3 | from typing import Callable 4 | from ignis.gobject import Binding 5 | from .menu import Menu 6 | 7 | 8 | class QSButton(Widget.Button): 9 | def __init__( 10 | self, 11 | label: str | Binding, 12 | icon_name: str | Binding, 13 | on_activate: Callable | None = None, 14 | on_deactivate: Callable | None = None, 15 | menu: Menu | None = None, 16 | **kwargs, 17 | ): 18 | self.on_activate = on_activate 19 | self.on_deactivate = on_deactivate 20 | self._active = False 21 | self._menu = menu 22 | super().__init__( 23 | child=Widget.Box( 24 | child=[ 25 | Widget.Icon(image=icon_name), 26 | Widget.Label(label=label, css_classes=["qs-button-label"]), 27 | Widget.Arrow( 28 | halign="end", 29 | hexpand=True, 30 | pixel_size=20, 31 | rotated=menu.bind("reveal_child"), 32 | ) 33 | if menu 34 | else None, 35 | ] 36 | ), 37 | on_click=self.__callback, 38 | css_classes=["qs-button", "unset"], 39 | hexpand=True, 40 | **kwargs, 41 | ) 42 | 43 | def __callback(self, *args) -> None: 44 | if self.active: 45 | if self.on_deactivate: 46 | self.on_deactivate(self) 47 | else: 48 | if self.on_activate: 49 | self.on_activate(self) 50 | 51 | @GObject.Property 52 | def active(self) -> bool: 53 | return self._active 54 | 55 | @active.setter 56 | def active(self, value: bool) -> None: 57 | self._active = value 58 | if value: 59 | self.add_css_class("active") 60 | else: 61 | self.remove_css_class("active") 62 | 63 | @GObject.Property 64 | def menu(self) -> Menu | None: 65 | return self._menu 66 | -------------------------------------------------------------------------------- /ignis/modules/control_center/widgets/__init__.py: -------------------------------------------------------------------------------- 1 | from .quick_settings import QuickSettings 2 | from .brightness import Brightness 3 | from .volume import VolumeSlider 4 | from .user import User 5 | from .media import Media 6 | from .notification_center import NotificationCenter 7 | 8 | __all__ = [ 9 | "QuickSettings", 10 | "Brightness", 11 | "VolumeSlider", 12 | "User", 13 | "Media", 14 | "NotificationCenter", 15 | ] 16 | -------------------------------------------------------------------------------- /ignis/modules/control_center/widgets/brightness.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from ignis.services.backlight import BacklightService 3 | from ignis.widgets import Widget 4 | 5 | backlight = BacklightService.get_default() 6 | 7 | 8 | class Brightness(Widget.Box): 9 | def __init__(self): 10 | super().__init__( 11 | visible=backlight.bind("available"), 12 | hexpand=True, 13 | style="margin-top: 0.25rem;", 14 | child=[ 15 | Widget.Icon( 16 | image="display-brightness-symbolic", 17 | css_classes=["material-slider-icon"], 18 | pixel_size=18, 19 | ), 20 | Widget.Scale( 21 | min=0, 22 | max=backlight.max_brightness, 23 | hexpand=True, 24 | value=backlight.bind("brightness"), 25 | css_classes=["material-slider"], 26 | on_change=lambda x: asyncio.create_task(backlight.set_brightness_async(x.value)), 27 | ), 28 | ], 29 | ) 30 | -------------------------------------------------------------------------------- /ignis/modules/control_center/widgets/media.py: -------------------------------------------------------------------------------- 1 | import os 2 | import ignis 3 | import asyncio 4 | from ignis.widgets import Widget 5 | from ignis.services.mpris import MprisService, MprisPlayer 6 | from ignis.utils import Utils 7 | from services.material import MaterialService 8 | from ignis.app import IgnisApp 9 | from ignis.exceptions import StylePathNotFoundError 10 | 11 | 12 | mpris = MprisService.get_default() 13 | app = IgnisApp.get_default() 14 | material = MaterialService.get_default() 15 | 16 | MEDIA_TEMPLATE = Utils.get_current_dir() + "/media.scss" 17 | MEDIA_SCSS_CACHE_DIR = ignis.CACHE_DIR + "/media" # type: ignore 18 | MEDIA_ART_FALLBACK = Utils.get_current_dir() + "/../../../misc/media-art-fallback.png" 19 | os.makedirs(MEDIA_SCSS_CACHE_DIR, exist_ok=True) 20 | 21 | 22 | PLAYER_ICONS = { 23 | "spotify": "spotify-symbolic", 24 | "firefox": "firefox-browser-symbolic", 25 | "chrome": "chrome-symbolic", 26 | None: "folder-music-symbolic", 27 | } 28 | 29 | 30 | class Player(Widget.Revealer): 31 | def __init__(self, player: MprisPlayer) -> None: 32 | self._player = player 33 | self._colors_path = f"{MEDIA_SCSS_CACHE_DIR}/{self.clean_desktop_entry()}.scss" 34 | player.connect("closed", lambda x: self.destroy()) 35 | player.connect("notify::art-url", lambda x, y: self.load_colors()) 36 | self.load_colors() 37 | 38 | super().__init__( 39 | transition_type="slide_down", 40 | reveal_child=False, 41 | css_classes=[self.get_css("media")], 42 | child=Widget.Overlay( 43 | child=Widget.Box(css_classes=[self.get_css("media-image")]), 44 | overlays=[ 45 | Widget.Box( 46 | hexpand=True, 47 | vexpand=True, 48 | css_classes=[self.get_css("media-image-gradient")], 49 | ), 50 | Widget.Icon( 51 | icon_name=self.get_player_icon(), 52 | pixel_size=22, 53 | halign="start", 54 | valign="start", 55 | css_classes=[self.get_css("media-player-icon")], 56 | ), 57 | Widget.Box( 58 | vertical=True, 59 | hexpand=True, 60 | css_classes=[self.get_css("media-content")], 61 | child=[ 62 | Widget.Box( 63 | vexpand=True, 64 | valign="center", 65 | child=[ 66 | Widget.Box( 67 | hexpand=True, 68 | vertical=True, 69 | child=[ 70 | Widget.Label( 71 | ellipsize="end", 72 | label=player.bind("title"), 73 | max_width_chars=30, 74 | halign="start", 75 | css_classes=[ 76 | self.get_css("media-title") 77 | ], 78 | ), 79 | Widget.Label( 80 | label=player.bind("artist"), 81 | max_width_chars=30, 82 | ellipsize="end", 83 | halign="start", 84 | css_classes=[ 85 | self.get_css("media-artist") 86 | ], 87 | ), 88 | ], 89 | ), 90 | Widget.Button( 91 | child=Widget.Icon( 92 | image=player.bind( 93 | "playback_status", 94 | lambda value: "media-playback-pause-symbolic" 95 | if value == "Playing" 96 | else "media-playback-start-symbolic", 97 | ), 98 | pixel_size=18, 99 | ), 100 | on_click=lambda x: asyncio.create_task(player.play_pause_async()), 101 | visible=player.bind("can_play"), 102 | css_classes=player.bind( 103 | "playback_status", 104 | lambda value: [ 105 | self.get_css("media-playback-button"), 106 | "playing", 107 | ] 108 | if value == "Playing" 109 | else [ 110 | self.get_css("media-playback-button"), 111 | "paused", 112 | ], 113 | ), 114 | ), 115 | ], 116 | ), 117 | ], 118 | ), 119 | Widget.Box( 120 | vexpand=True, 121 | valign="end", 122 | style="padding: 1rem;", 123 | child=[ 124 | Widget.Scale( 125 | value=player.bind("position"), 126 | max=player.bind("length"), 127 | hexpand=True, 128 | css_classes=[self.get_css("media-scale")], 129 | on_change=lambda x: asyncio.create_task(self._player.set_position_async(x.value)), 130 | visible=player.bind( 131 | "position", lambda value: value != -1 132 | ), 133 | ), 134 | Widget.Button( 135 | child=Widget.Icon( 136 | image="media-skip-backward-symbolic", 137 | pixel_size=20, 138 | ), 139 | css_classes=[self.get_css("media-skip-button")], 140 | on_click=lambda x: asyncio.create_task(player.previous_async()), 141 | visible=player.bind("can_go_previous"), 142 | style="margin-left: 1rem;", 143 | ), 144 | Widget.Button( 145 | child=Widget.Icon( 146 | image="media-skip-forward-symbolic", 147 | pixel_size=20, 148 | ), 149 | css_classes=[self.get_css("media-skip-button")], 150 | on_click=lambda x: asyncio.create_task(player.next_async()), 151 | visible=player.bind("can_go_next"), 152 | style="margin-left: 1rem;", 153 | ), 154 | ], 155 | ), 156 | ], 157 | ), 158 | ) 159 | 160 | def get_player_icon(self) -> str: 161 | if self._player.desktop_entry == "firefox": 162 | return PLAYER_ICONS["firefox"] 163 | elif self._player.desktop_entry == "spotify": 164 | return PLAYER_ICONS["spotify"] 165 | elif self._player.track_id is not None: 166 | if "chromium" in self._player.track_id or "chrome" in self._player.track_id: 167 | return PLAYER_ICONS["chrome"] 168 | 169 | return PLAYER_ICONS[None] 170 | 171 | def destroy(self) -> None: 172 | self.set_reveal_child(False) 173 | Utils.Timeout(self.transition_duration, super().unparent) 174 | 175 | def get_css(self, class_name: str) -> str: 176 | return f"{class_name}-{self.clean_desktop_entry()}" 177 | 178 | def load_colors(self) -> None: 179 | if not self._player.art_url: 180 | art_url = MEDIA_ART_FALLBACK 181 | else: 182 | art_url = self._player.art_url 183 | 184 | try: 185 | app.remove_css(self._colors_path) 186 | except StylePathNotFoundError: 187 | pass 188 | 189 | colors = material.get_colors_from_img(art_url, True) 190 | colors["art_url"] = art_url 191 | colors["desktop_entry"] = self.clean_desktop_entry() 192 | material.render_template( 193 | colors, input_file=MEDIA_TEMPLATE, output_file=self._colors_path 194 | ) 195 | app.apply_css(self._colors_path) 196 | 197 | def clean_desktop_entry(self) -> str: 198 | return self._player.desktop_entry.replace(".", "-") 199 | 200 | 201 | class Media(Widget.Box): 202 | def __init__(self): 203 | super().__init__( 204 | vertical=True, 205 | setup=lambda self: mpris.connect( 206 | "player_added", lambda x, player: self.__add_player(player) 207 | ), 208 | css_classes=["rec-unset"], 209 | ) 210 | 211 | def __add_player(self, obj: MprisPlayer) -> None: 212 | player = Player(obj) 213 | self.append(player) 214 | player.set_reveal_child(True) 215 | -------------------------------------------------------------------------------- /ignis/modules/control_center/widgets/media.scss: -------------------------------------------------------------------------------- 1 | // JINJA2 TEMPLATE 2 | // WARNING: Do not import this file in style.scss 3 | 4 | .media-{{ desktop_entry }} { 5 | margin-top: 1rem; 6 | } 7 | 8 | 9 | .media-image-{{ desktop_entry }} { 10 | background-image: url('file:///{{ art_url }}'); 11 | background-repeat: no-repeat; 12 | background-size: cover; 13 | background-position: center; 14 | min-height: 11rem; 15 | border-radius: 1.5rem; 16 | } 17 | 18 | .media-image-gradient-{{ desktop_entry }} { 19 | border-radius: 1.3rem; 20 | background: linear-gradient(90deg, rgba({{ onPrimary }}, 0.9) 0%, rgba({{ onPrimary }}, 0.4) 50%, rgba({{ onPrimary }}, 0.9) 100%); 21 | } 22 | 23 | .media-scale-{{ desktop_entry }} trough, 24 | .media-scale-{{ desktop_entry }} trough highlight { 25 | background-color: rgba({{ onSurface }}, 0.2); 26 | border-radius: 1rem; 27 | min-height: 0.2rem; 28 | } 29 | 30 | 31 | .media-scale-{{ desktop_entry }} trough highlight { 32 | background-color: {{ onSurface }}; 33 | } 34 | 35 | .media-scale-{{ desktop_entry }} slider { 36 | background-color: {{ onSurface }}; 37 | padding: 0.5rem 0.1rem; 38 | margin: -0.5rem -0.1rem; 39 | } 40 | 41 | .media-title-{{ desktop_entry }} { 42 | color: {{ onSurface }}; 43 | } 44 | 45 | .media-artist-{{ desktop_entry }} { 46 | color: {{ onSurfaceVariant }}; 47 | font-weight: normal; 48 | } 49 | 50 | 51 | .media-content-{{ desktop_entry }} { 52 | padding: 1rem; 53 | } 54 | 55 | .media-playback-button-{{ desktop_entry }} { 56 | background-color: {{ primary }}; 57 | color: {{ onPrimary }}; 58 | border-radius: 4rem; 59 | min-width: 2.7rem; 60 | min-height: 2.7rem; 61 | transition: 0.3s; 62 | 63 | &:hover { 64 | background-color: lighten({{ primary }}, 5%); 65 | } 66 | 67 | &.playing { 68 | border-radius: 1rem; 69 | } 70 | } 71 | 72 | .media-player-icon-{{ desktop_entry }} { 73 | color: {{ primary }}; 74 | padding: 1rem; 75 | padding-top: 1.2rem; 76 | } 77 | 78 | .media-skip-button-{{ desktop_entry }} { 79 | transition: 0.3s; 80 | color: {{ onSurface }}; 81 | &:hover { 82 | color: {{ primary }}; 83 | } 84 | } -------------------------------------------------------------------------------- /ignis/modules/control_center/widgets/notification_center.py: -------------------------------------------------------------------------------- 1 | from ignis.widgets import Widget 2 | from ignis.services.notifications import Notification, NotificationService 3 | from ignis.utils import Utils 4 | from gi.repository import GLib # type: ignore 5 | from ...shared_widgets import NotificationWidget 6 | 7 | notifications = NotificationService.get_default() 8 | 9 | 10 | class Popup(Widget.Revealer): 11 | def __init__(self, notification: Notification, **kwargs): 12 | widget = NotificationWidget(notification) 13 | super().__init__(child=widget, transition_type="slide_down", **kwargs) 14 | 15 | notification.connect("closed", lambda x: self.destroy()) 16 | 17 | def destroy(self): 18 | self.reveal_child = False 19 | Utils.Timeout(self.transition_duration, self.unparent) 20 | 21 | 22 | class NotificationList(Widget.Box): 23 | __gtype_name__ = "NotificationList" 24 | 25 | def __init__(self): 26 | loading_notifications_label = Widget.Label( 27 | label="Loading notifications...", 28 | valign="center", 29 | vexpand=True, 30 | css_classes=["notification-center-info-label"], 31 | ) 32 | 33 | super().__init__( 34 | vertical=True, 35 | child=[loading_notifications_label], 36 | vexpand=True, 37 | css_classes=["rec-unset"], 38 | setup=lambda self: notifications.connect( 39 | "notified", 40 | lambda x, notification: self.__on_notified(notification), 41 | ), 42 | ) 43 | 44 | Utils.ThreadTask( 45 | self.__load_notifications, 46 | lambda result: self.set_child(result), 47 | ).run() 48 | 49 | def __on_notified(self, notification: Notification) -> None: 50 | notify = Popup(notification) 51 | self.prepend(notify) 52 | notify.reveal_child = True 53 | 54 | def __load_notifications(self) -> list[Widget.Label | Popup]: 55 | widgets: list[Widget.Label | Popup] = [] 56 | for i in notifications.notifications: 57 | GLib.idle_add(lambda i=i: widgets.append(Popup(i, reveal_child=True))) 58 | 59 | widgets.append( 60 | Widget.Label( 61 | label="No notifications", 62 | valign="center", 63 | vexpand=True, 64 | visible=notifications.bind( 65 | "notifications", lambda value: len(value) == 0 66 | ), 67 | css_classes=["notification-center-info-label"], 68 | ) 69 | ) 70 | return widgets 71 | 72 | 73 | class NotificationCenter(Widget.Box): 74 | __gtype_name__ = "NotificationCenter" 75 | 76 | def __init__(self): 77 | super().__init__( 78 | vertical=True, 79 | vexpand=True, 80 | css_classes=["notification-center"], 81 | child=[ 82 | Widget.Box( 83 | css_classes=["notification-center-header", "rec-unset"], 84 | child=[ 85 | Widget.Label( 86 | label=notifications.bind( 87 | "notifications", lambda value: str(len(value)) 88 | ), 89 | css_classes=["notification-count"], 90 | ), 91 | Widget.Label( 92 | label="notifications", 93 | css_classes=["notification-header-label"], 94 | ), 95 | Widget.Button( 96 | child=Widget.Label(label="Clear all"), 97 | halign="end", 98 | hexpand=True, 99 | on_click=lambda x: notifications.clear_all(), 100 | css_classes=["notification-clear-all"], 101 | ), 102 | ], 103 | ), 104 | Widget.Scroll( 105 | child=NotificationList(), 106 | vexpand=True, 107 | ), 108 | ], 109 | ) 110 | -------------------------------------------------------------------------------- /ignis/modules/control_center/widgets/quick_settings/__init__.py: -------------------------------------------------------------------------------- 1 | from .quick_settings import QuickSettings 2 | 3 | __all__ = ["QuickSettings"] 4 | -------------------------------------------------------------------------------- /ignis/modules/control_center/widgets/quick_settings/bluetooth.py: -------------------------------------------------------------------------------- 1 | from ignis.widgets import Widget 2 | from ...qs_button import QSButton 3 | from ...menu import Menu 4 | from ....shared_widgets import ToggleBox 5 | from ignis.services.bluetooth import BluetoothService, BluetoothDevice 6 | 7 | bluetooth = BluetoothService.get_default() 8 | 9 | 10 | class BluetoothDeviceItem(Widget.Button): 11 | def __init__(self, device: BluetoothDevice): 12 | super().__init__( 13 | css_classes=["network-item", "unset"], 14 | on_click=lambda x: device.disconnect_from() 15 | if device.connected 16 | else device.connect_to(), 17 | child=Widget.Box( 18 | child=[ 19 | Widget.Icon( 20 | image=device.bind("icon_name"), 21 | ), 22 | Widget.Label( 23 | label=device.alias, 24 | halign="start", 25 | css_classes=["wifi-network-label"], 26 | ), 27 | Widget.Icon( 28 | image="object-select-symbolic", 29 | halign="end", 30 | hexpand=True, 31 | visible=device.bind("connected"), 32 | ), 33 | ] 34 | ), 35 | ) 36 | 37 | 38 | class BluetoothMenu(Menu): 39 | def __init__(self): 40 | super().__init__( 41 | name="bluetooth", 42 | child=[ 43 | ToggleBox( 44 | label="Bluetooth", 45 | active=bluetooth.powered, 46 | on_change=lambda x, state: bluetooth.set_powered(state), 47 | css_classes=["network-header-box"], 48 | ), 49 | Widget.Box( 50 | vertical=True, 51 | child=bluetooth.bind( 52 | "devices", 53 | transform=lambda value: [BluetoothDeviceItem(i) for i in value], 54 | ), 55 | ), 56 | ], 57 | ) 58 | 59 | 60 | class BluetoothButton(QSButton): 61 | def __init__(self): 62 | menu = BluetoothMenu() 63 | 64 | def get_label(devices: list[BluetoothDevice]) -> str: 65 | if len(devices) == 0: 66 | return "Bluetooth" 67 | elif len(devices) == 1: 68 | return devices[0].alias 69 | else: 70 | return f"{len(devices)} pairs" 71 | 72 | def toggle_menu(x) -> None: 73 | bluetooth.set_setup_mode(True) 74 | menu.toggle() 75 | 76 | super().__init__( 77 | label=bluetooth.bind("connected_devices", get_label), 78 | icon_name="bluetooth-active-symbolic", 79 | on_activate=toggle_menu, 80 | on_deactivate=toggle_menu, 81 | active=bluetooth.bind("powered"), 82 | menu=menu, 83 | ) 84 | 85 | 86 | def bluetooth_control() -> list[QSButton]: 87 | return [] if bluetooth.state == "absent" else [BluetoothButton()] 88 | -------------------------------------------------------------------------------- /ignis/modules/control_center/widgets/quick_settings/dark_mode.py: -------------------------------------------------------------------------------- 1 | from ...qs_button import QSButton 2 | from user_options import user_options 3 | 4 | 5 | class DarkModeButton(QSButton): 6 | __gtype_name__ = "DarkModeButton" 7 | 8 | def __init__(self): 9 | super().__init__( 10 | label="Dark", 11 | icon_name="night-light-symbolic", 12 | on_activate=lambda x: user_options.material.set_dark_mode(True), 13 | on_deactivate=lambda x: user_options.material.set_dark_mode(False), 14 | active=user_options.material.bind("dark_mode"), 15 | ) 16 | -------------------------------------------------------------------------------- /ignis/modules/control_center/widgets/quick_settings/dnd.py: -------------------------------------------------------------------------------- 1 | from ...qs_button import QSButton 2 | from ignis.services.notifications import NotificationService 3 | from ignis.options import options 4 | 5 | notifications = NotificationService.get_default() 6 | 7 | 8 | class DNDButton(QSButton): 9 | __gtype_name__ = "DNDButton" 10 | 11 | def __init__(self): 12 | super().__init__( 13 | label=options.notifications.bind( 14 | "dnd", lambda value: "Silent" if value else "Noisy" 15 | ), 16 | icon_name=options.notifications.bind( 17 | "dnd", 18 | transform=lambda value: "notification-disabled-symbolic" 19 | if value 20 | else "notification-symbolic", 21 | ), 22 | on_activate=lambda x: self.__activate(True), 23 | on_deactivate=lambda x: self.__activate(False), 24 | active=options.notifications.bind("dnd"), 25 | ) 26 | 27 | def __activate(self, state: bool) -> None: 28 | options.notifications.dnd = state 29 | -------------------------------------------------------------------------------- /ignis/modules/control_center/widgets/quick_settings/ethernet.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from ignis.widgets import Widget 3 | from ignis.utils import Utils 4 | from ...qs_button import QSButton 5 | from ...menu import Menu 6 | from ignis.services.network import NetworkService, EthernetDevice 7 | 8 | network = NetworkService.get_default() 9 | 10 | 11 | class EthernetConnectionItem(Widget.Button): 12 | def __init__(self, device: EthernetDevice): 13 | super().__init__( 14 | css_classes=["network-item", "unset"], 15 | on_click=lambda x: asyncio.create_task(device.disconnect_from()) 16 | if device.is_connected 17 | else asyncio.create_task(device.connect_to()), 18 | child=Widget.Box( 19 | child=[ 20 | Widget.Icon(image="network-wired-symbolic"), 21 | Widget.Label( 22 | label=device.name, 23 | ellipsize="end", 24 | max_width_chars=20, 25 | halign="start", 26 | ), 27 | Widget.Button( 28 | child=Widget.Label( 29 | label=device.bind( 30 | "is_connected", 31 | lambda value: "Disconnect" if value else "Connect", 32 | ) 33 | ), 34 | css_classes=["connect-label", "unset"], 35 | halign="end", 36 | hexpand=True, 37 | ), 38 | ] 39 | ), 40 | ) 41 | 42 | 43 | class EthernetMenu(Menu): 44 | def __init__(self): 45 | super().__init__( 46 | name="ethernet", 47 | child=[ 48 | Widget.Box( 49 | css_classes=["network-header-box"], 50 | child=[ 51 | Widget.Icon(icon_name="network-wired-symbolic", pixel_size=28), 52 | Widget.Label( 53 | label="Wired connections", 54 | css_classes=["network-header-label"], 55 | ), 56 | ], 57 | ), 58 | Widget.Box( 59 | vertical=True, 60 | child=network.ethernet.bind( 61 | "devices", 62 | lambda value: [EthernetConnectionItem(i) for i in value], 63 | ), 64 | ), 65 | Widget.Separator(), 66 | Widget.Button( 67 | css_classes=["network-item", "unset"], 68 | style="margin-bottom: 0;", 69 | on_click=lambda x: asyncio.create_task(Utils.exec_sh_async("nm-connection-editor")), 70 | child=Widget.Box( 71 | child=[ 72 | Widget.Icon(image="preferences-system-symbolic"), 73 | Widget.Label( 74 | label="Network Settings", 75 | halign="start", 76 | ), 77 | ] 78 | ), 79 | ), 80 | ], 81 | ) 82 | 83 | 84 | class EthernetButton(QSButton): 85 | def __init__(self): 86 | menu = EthernetMenu() 87 | 88 | super().__init__( 89 | label="Wired", 90 | icon_name="network-wired-symbolic", 91 | on_activate=lambda x: menu.toggle(), 92 | on_deactivate=lambda x: menu.toggle(), 93 | menu=menu, 94 | active=network.ethernet.bind("is_connected"), 95 | ) 96 | 97 | 98 | def ethernet_control() -> list[QSButton]: 99 | if len(network.ethernet.devices) > 0: 100 | return [EthernetButton()] 101 | else: 102 | return [] 103 | -------------------------------------------------------------------------------- /ignis/modules/control_center/widgets/quick_settings/quick_settings.py: -------------------------------------------------------------------------------- 1 | from ignis.widgets import Widget 2 | from .wifi import wifi_control 3 | from .record import RecordButton 4 | from .dnd import DNDButton 5 | from .dark_mode import DarkModeButton 6 | from .ethernet import ethernet_control 7 | from .vpn import vpn_control 8 | from .bluetooth import bluetooth_control 9 | from ...qs_button import QSButton 10 | from ignis.services.network import NetworkService 11 | 12 | network = NetworkService.get_default() 13 | 14 | 15 | class QuickSettings(Widget.Box): 16 | def __init__(self): 17 | super().__init__(vertical=True, css_classes=["qs-main-box"]) 18 | network.wifi.connect("notify::devices", lambda x, y: self.__refresh()) 19 | network.ethernet.connect("notify::devices", lambda x, y: self.__refresh()) 20 | network.vpn.connect("notify::connections", lambda x, y: self.__refresh()) 21 | 22 | self.__refresh() 23 | 24 | def __refresh(self) -> None: 25 | self.child = [] 26 | self.__configure() 27 | 28 | def __configure(self) -> None: 29 | self.__qs_fabric( 30 | *wifi_control(), 31 | *ethernet_control(), 32 | *vpn_control(), 33 | *bluetooth_control(), 34 | DNDButton(), 35 | DarkModeButton(), 36 | RecordButton(), 37 | ) 38 | 39 | def __qs_fabric(self, *buttons: QSButton) -> None: 40 | for i in range(0, len(buttons), 2): 41 | self.__add_row(buttons, i) 42 | 43 | def __add_row(self, buttons: tuple[QSButton, ...], i: int) -> None: 44 | row = Widget.Box(homogeneous=True) 45 | if len(self.child) > 0: 46 | row.style = "margin-top: 0.5rem;" 47 | 48 | self.append(row) 49 | 50 | button1 = buttons[i] 51 | 52 | self.__add_button(row, button1, buttons, i) 53 | 54 | if i + 1 < len(buttons): 55 | button2 = buttons[i + 1] 56 | button2.style = "margin-left: 0.5rem;" 57 | self.__add_button(row, button2, buttons, i) 58 | 59 | def __add_button( 60 | self, row: Widget.Box, button: QSButton, buttons: tuple[QSButton, ...], i: int 61 | ) -> None: 62 | row.append(button) 63 | 64 | if button.menu: 65 | self.append(button.menu) 66 | 67 | if i == len(buttons) - 1 or i == len(buttons) - 2: 68 | button.menu.box.add_css_class("control-center-menu-last-row") 69 | -------------------------------------------------------------------------------- /ignis/modules/control_center/widgets/quick_settings/record.py: -------------------------------------------------------------------------------- 1 | from ignis.widgets import Widget 2 | from ...qs_button import QSButton 3 | from ...menu import Menu 4 | from ignis.services.recorder import RecorderService 5 | 6 | recorder = RecorderService.get_default() 7 | 8 | 9 | class RecordMenu(Menu): 10 | def __init__(self): 11 | self._audio_switch = Widget.Switch(halign="end", hexpand=True, valign="center") 12 | self._dropdown = Widget.DropDown( 13 | items=["Internal audio", "Microphone", "Both sources"], 14 | css_classes=["record-dropdown"], 15 | ) 16 | 17 | super().__init__( 18 | name="recording", 19 | child=[ 20 | Widget.Icon( 21 | image="media-record-symbolic", 22 | pixel_size=36, 23 | halign="center", 24 | css_classes=["record-icon"], 25 | ), 26 | Widget.Label( 27 | label="Start recording?", 28 | halign="center", 29 | style="font-size: 1.2rem;", 30 | ), 31 | Widget.Box( 32 | style="margin-top: 0.5rem;", 33 | child=[ 34 | Widget.Icon( 35 | image="microphone-sensitivity-medium-symbolic", 36 | pixel_size=20, 37 | style="margin-right: 0.5rem;", 38 | ), 39 | Widget.Box( 40 | vertical=True, 41 | child=[ 42 | Widget.Label( 43 | label="Record audio", 44 | style="font-size: 1.1rem;", 45 | halign="start", 46 | ), 47 | self._dropdown, 48 | ], 49 | ), 50 | self._audio_switch, 51 | ], 52 | ), 53 | Widget.Box( 54 | style="margin-top: 1rem;", 55 | child=[ 56 | Widget.Button( 57 | child=Widget.Label(label="Cancel"), 58 | css_classes=["record-cancel-button", "unset"], 59 | on_click=lambda x: self.set_reveal_child(False), # type: ignore 60 | ), 61 | Widget.Button( 62 | child=Widget.Label(label="Start recording"), 63 | halign="end", 64 | hexpand=True, 65 | css_classes=["record-start-button", "unset"], 66 | on_click=lambda x: self.__start_recording(), # type: ignore 67 | ), 68 | ], 69 | ), 70 | ], 71 | ) 72 | 73 | def __start_recording(self) -> None: 74 | self.set_reveal_child(False) 75 | microphone = False 76 | internal = False 77 | if self._audio_switch.active: 78 | if self._dropdown.selected == "Internal audio": 79 | internal = True 80 | elif self._dropdown.selected == "Microphone": 81 | microphone = True 82 | else: 83 | internal = True 84 | microphone = True 85 | 86 | recorder.start_recording( 87 | record_microphone=microphone, record_internal_audio=internal 88 | ) 89 | 90 | 91 | class RecordButton(QSButton): 92 | def __init__(self): 93 | record_menu = RecordMenu() 94 | 95 | super().__init__( 96 | label="Recording", 97 | icon_name="media-record-symbolic", 98 | on_activate=lambda x: record_menu.toggle(), 99 | on_deactivate=lambda x: recorder.stop_recording(), 100 | active=recorder.bind("active"), 101 | menu=record_menu, 102 | ) 103 | -------------------------------------------------------------------------------- /ignis/modules/control_center/widgets/quick_settings/vpn.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from ignis.widgets import Widget 3 | from ignis.utils import Utils 4 | from ...qs_button import QSButton 5 | from ...menu import Menu 6 | from ignis.services.network import NetworkService, VpnConnection 7 | 8 | 9 | network = NetworkService.get_default() 10 | 11 | 12 | class VpnNetworkItem(Widget.Button): 13 | def __init__(self, conn: VpnConnection): 14 | super().__init__( 15 | css_classes=["network-item", "unset"], 16 | on_click=lambda x: asyncio.create_task(conn.toggle_connection()), 17 | child=Widget.Box( 18 | child=[ 19 | Widget.Label( 20 | label=conn.name, 21 | ellipsize="end", 22 | max_width_chars=20, 23 | halign="start", 24 | ), 25 | Widget.Button( 26 | child=Widget.Label( 27 | label=conn.bind( 28 | "is_connected", 29 | lambda value: "Disconnect" if value else "Connect", 30 | ) 31 | ), 32 | css_classes=["connect-label", "unset"], 33 | halign="end", 34 | hexpand=True, 35 | ), 36 | ] 37 | ), 38 | ) 39 | 40 | 41 | class VpnMenu(Menu): 42 | def __init__(self): 43 | super().__init__( 44 | name="vpn", 45 | child=[ 46 | Widget.Box( 47 | css_classes=["network-header-box"], 48 | child=[ 49 | Widget.Icon(icon_name="network-vpn-symbolic", pixel_size=28), 50 | Widget.Label( 51 | label="VPN connections", 52 | css_classes=["network-header-label"], 53 | ), 54 | ], 55 | ), 56 | Widget.Box( 57 | vertical=True, 58 | child=network.vpn.bind( 59 | "connections", 60 | transform=lambda value: [VpnNetworkItem(i) for i in value], 61 | ), 62 | ), 63 | Widget.Separator(), 64 | Widget.Button( 65 | css_classes=["network-item", "unset"], 66 | on_click=lambda x: asyncio.create_task(Utils.exec_sh_async("nm-connection-editor")), 67 | style="margin-bottom: 0;", 68 | child=Widget.Box( 69 | child=[ 70 | Widget.Icon(image="preferences-system-symbolic"), 71 | Widget.Label( 72 | label="Network Manager", 73 | halign="start", 74 | ), 75 | ] 76 | ), 77 | ), 78 | ], 79 | ) 80 | 81 | 82 | class VpnButton(QSButton): 83 | def __init__(self): 84 | menu = VpnMenu() 85 | 86 | def get_label(id: str) -> str: 87 | if id: 88 | return id 89 | else: 90 | return "VPN" 91 | 92 | def get_icon(icon_name: str) -> str: 93 | if network.vpn.is_connected: 94 | return icon_name 95 | else: 96 | return "network-vpn-symbolic" 97 | 98 | super().__init__( 99 | label=network.vpn.bind("active_vpn_id", get_label), 100 | icon_name=network.vpn.bind("icon-name", get_icon), 101 | on_activate=lambda x: menu.toggle(), 102 | on_deactivate=lambda x: menu.toggle(), 103 | active=network.vpn.bind("is-connected"), 104 | menu=menu, 105 | ) 106 | 107 | 108 | def vpn_control() -> list[QSButton]: 109 | if len(network.vpn.connections) > 0: 110 | return [VpnButton()] 111 | else: 112 | return [] 113 | -------------------------------------------------------------------------------- /ignis/modules/control_center/widgets/quick_settings/wifi.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from ignis.widgets import Widget 3 | from ignis.utils import Utils 4 | from ...qs_button import QSButton 5 | from ...menu import Menu 6 | from ....shared_widgets import ToggleBox 7 | from ignis.services.network import NetworkService, WifiAccessPoint, WifiDevice 8 | 9 | network = NetworkService.get_default() 10 | 11 | 12 | class WifiNetworkItem(Widget.Button): 13 | def __init__(self, access_point: WifiAccessPoint): 14 | super().__init__( 15 | css_classes=["network-item", "unset"], 16 | on_click=lambda x: asyncio.create_task(access_point.connect_to_graphical()), 17 | child=Widget.Box( 18 | child=[ 19 | Widget.Icon( 20 | image=access_point.bind( 21 | "strength", transform=lambda value: access_point.icon_name 22 | ), 23 | ), 24 | Widget.Label( 25 | label=access_point.ssid, 26 | halign="start", 27 | ), 28 | Widget.Icon( 29 | image="object-select-symbolic", 30 | halign="end", 31 | hexpand=True, 32 | visible=access_point.bind("is_connected"), 33 | ), 34 | ] 35 | ), 36 | ) 37 | 38 | 39 | class WifiMenu(Menu): 40 | def __init__(self, device: WifiDevice): 41 | super().__init__( 42 | name="wifi", 43 | child=[ 44 | ToggleBox( 45 | label="Wi-Fi", 46 | active=network.wifi.enabled, 47 | on_change=lambda x, state: network.wifi.set_enabled(state), 48 | css_classes=["network-header-box"], 49 | ), 50 | Widget.Box( 51 | vertical=True, 52 | child=device.bind( 53 | "access_points", 54 | transform=lambda value: [WifiNetworkItem(i) for i in value], 55 | ), 56 | ), 57 | Widget.Separator(), 58 | Widget.Button( 59 | css_classes=["network-item", "unset"], 60 | on_click=lambda x: asyncio.create_task(Utils.exec_sh_async("nm-connection-editor")), 61 | style="margin-bottom: 0;", 62 | child=Widget.Box( 63 | child=[ 64 | Widget.Icon(image="preferences-system-symbolic"), 65 | Widget.Label( 66 | label="Network Settings", 67 | halign="start", 68 | ), 69 | ] 70 | ), 71 | ), 72 | ], 73 | ) 74 | 75 | 76 | class WifiButton(QSButton): 77 | def __init__(self, device: WifiDevice): 78 | menu = WifiMenu(device) 79 | 80 | def get_label(ssid: str) -> str: 81 | if ssid: 82 | return ssid 83 | else: 84 | return "Wi-Fi" 85 | 86 | def get_icon(icon_name: str) -> str: 87 | if device.ap.is_connected: 88 | return icon_name 89 | else: 90 | return "network-wireless-symbolic" 91 | 92 | def toggle_list(x) -> None: 93 | asyncio.create_task(device.scan()) 94 | menu.toggle() 95 | 96 | super().__init__( 97 | label=device.ap.bind("ssid", get_label), 98 | icon_name=device.ap.bind("icon-name", get_icon), 99 | on_activate=toggle_list, 100 | on_deactivate=toggle_list, 101 | active=network.wifi.bind("enabled"), 102 | menu=menu, 103 | ) 104 | 105 | 106 | def wifi_control() -> list[QSButton]: 107 | return [WifiButton(dev) for dev in network.wifi.devices] 108 | -------------------------------------------------------------------------------- /ignis/modules/control_center/widgets/user.py: -------------------------------------------------------------------------------- 1 | import os 2 | from ignis.widgets import Widget 3 | from ignis.utils import Utils 4 | from ignis.app import IgnisApp 5 | from ignis.services.fetch import FetchService 6 | from user_options import user_options 7 | 8 | fetch = FetchService.get_default() 9 | app = IgnisApp.get_default() 10 | 11 | 12 | def format_uptime(value: tuple[int, int, int, int]) -> str: 13 | days, hours, minutes, seconds = value 14 | if days: 15 | return f"up {days:02}:{hours:02}:{minutes:02}" 16 | else: 17 | return f"up {hours:02}:{minutes:02}" 18 | 19 | 20 | class User(Widget.Box): 21 | def __init__(self): 22 | user_image = Widget.Picture( 23 | image=user_options.user.bind( 24 | "avatar", 25 | lambda value: "user-info" if not os.path.exists(value) else value, 26 | ), 27 | width=44, 28 | height=44, 29 | content_fit="cover", 30 | style="border-radius: 10rem;", 31 | ) 32 | 33 | username = Widget.Box( 34 | child=[ 35 | Widget.Label( 36 | label=os.getenv("USER"), css_classes=["user-name"], halign="start" 37 | ), 38 | Widget.Label( 39 | label=Utils.Poll( 40 | timeout=60 * 1000, callback=lambda x: fetch.uptime 41 | ).bind("output", lambda value: format_uptime(value)), 42 | halign="start", 43 | css_classes=["user-name-secondary"], 44 | ), 45 | ], 46 | vertical=True, 47 | css_classes=["user-name-box"], 48 | ) 49 | 50 | settings_button = Widget.Button( 51 | child=Widget.Icon(image="emblem-system-symbolic", pixel_size=20), 52 | halign="end", 53 | hexpand=True, 54 | css_classes=["user-settings", "unset"], 55 | on_click=lambda x: self.__on_settings_button_click(), 56 | ) 57 | 58 | power_button = Widget.Button( 59 | child=Widget.Icon(image="system-shutdown-symbolic", pixel_size=20), 60 | halign="end", 61 | css_classes=["user-power", "unset"], 62 | on_click=lambda x: app.toggle_window("ignis_POWERMENU"), 63 | ) 64 | super().__init__( 65 | child=[user_image, username, settings_button, power_button], 66 | css_classes=["user"], 67 | ) 68 | 69 | def __on_settings_button_click(self) -> None: 70 | window = app.get_window("ignis_SETTINGS") 71 | window.visible = not window.visible # type: ignore 72 | -------------------------------------------------------------------------------- /ignis/modules/control_center/widgets/volume.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from ignis.widgets import Widget 3 | from ignis.utils import Utils 4 | from ignis.services.audio import AudioService, Stream 5 | from typing import Literal 6 | from ..menu import Menu 7 | from ...shared_widgets import MaterialVolumeSlider 8 | 9 | audio = AudioService.get_default() 10 | 11 | AUDIO_TYPES = { 12 | "speaker": {"menu_icon": "audio-headphones-symbolic", "menu_label": "Sound Output"}, 13 | "microphone": { 14 | "menu_icon": "microphone-sensitivity-high-symbolic", 15 | "menu_label": "Sound Input", 16 | }, 17 | } 18 | 19 | 20 | class DeviceItem(Widget.Button): 21 | def __init__(self, stream: Stream, _type: Literal["speaker", "microphone"]): 22 | super().__init__( 23 | child=Widget.Box( 24 | child=[ 25 | Widget.Icon(image="audio-card-symbolic"), 26 | Widget.Label( 27 | label=stream.description, 28 | ellipsize="end", 29 | max_width_chars=30, 30 | halign="start", 31 | css_classes=["volume-entry-label"], 32 | ), 33 | Widget.Icon( 34 | image="object-select-symbolic", 35 | halign="end", 36 | hexpand=True, 37 | visible=stream.bind("is_default"), 38 | ), 39 | ] 40 | ), 41 | css_classes=["volume-entry", "unset"], 42 | hexpand=True, 43 | setup=lambda self: stream.connect("removed", lambda x: self.unparent()), 44 | on_click=lambda x: setattr(audio, _type, stream), 45 | ) 46 | 47 | 48 | class DeviceMenu(Menu): 49 | def __init__(self, _type: Literal["speaker", "microphone"], style: str = ""): 50 | data = AUDIO_TYPES[_type] 51 | 52 | super().__init__( 53 | name=f"volume-{_type}", 54 | child=[ 55 | Widget.Box( 56 | child=[ 57 | Widget.Icon(image=data["menu_icon"], pixel_size=24), 58 | Widget.Label( 59 | label=data["menu_label"], 60 | halign="start", 61 | css_classes=["volume-entry-list-header-label"], 62 | ), 63 | ], 64 | css_classes=["volume-entry-list-header-box"], 65 | ), 66 | Widget.Box( 67 | vertical=True, 68 | setup=lambda self: audio.connect( 69 | f"{_type}-added", 70 | lambda x, stream: self.append(DeviceItem(stream, _type)), 71 | ), 72 | ), 73 | Widget.Separator(css_classes=["volume-entry-list-separator"]), 74 | Widget.Button( 75 | child=Widget.Box( 76 | child=[ 77 | Widget.Icon(image="preferences-system-symbolic"), 78 | Widget.Label( 79 | label="Sound Settings", 80 | halign="start", 81 | css_classes=["volume-entry-label"], 82 | ), 83 | ] 84 | ), 85 | css_classes=["volume-entry", "unset"], 86 | style="margin-bottom: 0;", 87 | on_click=lambda x: asyncio.create_task(Utils.exec_sh_async("pavucontrol")), 88 | ), 89 | ], 90 | ) 91 | 92 | self.box.add_css_class(f"volume-menubox-{_type}") 93 | 94 | 95 | class VolumeSlider(Widget.Box): 96 | def __init__(self, _type: Literal["speaker", "microphone"]): 97 | stream = getattr(audio, _type) 98 | 99 | icon = Widget.Button( 100 | child=Widget.Icon( 101 | image=stream.bind("icon_name"), 102 | pixel_size=18, 103 | ), 104 | css_classes=["material-slider-icon", "unset", "hover-surface"], 105 | on_click=lambda x: stream.set_is_muted(not stream.is_muted), 106 | ) 107 | 108 | device_menu = DeviceMenu(_type=_type) 109 | 110 | scale = MaterialVolumeSlider( 111 | stream=stream, 112 | on_change=lambda x: stream.set_volume(x.value), 113 | sensitive=stream.bind("is_muted", lambda value: not value), 114 | ) 115 | 116 | arrow = Widget.Button( 117 | child=Widget.Arrow(pixel_size=20, rotated=device_menu.bind("reveal_child")), 118 | css_classes=["material-slider-arrow", "hover-surface"], 119 | on_click=lambda x: device_menu.toggle(), 120 | ) 121 | super().__init__( 122 | vertical=True, 123 | child=[ 124 | Widget.Box(child=[icon, scale, arrow]), 125 | device_menu, 126 | ], 127 | css_classes=[f"volume-mainbox-{_type}"], 128 | ) 129 | -------------------------------------------------------------------------------- /ignis/modules/launcher/__init__.py: -------------------------------------------------------------------------------- 1 | from .launcher import Launcher 2 | 3 | __all__ = ["Launcher"] 4 | -------------------------------------------------------------------------------- /ignis/modules/launcher/launcher.py: -------------------------------------------------------------------------------- 1 | import re 2 | import asyncio 3 | from ignis.widgets import Widget 4 | from ignis.app import IgnisApp 5 | from ignis.services.applications import ( 6 | ApplicationsService, 7 | Application, 8 | ApplicationAction, 9 | ) 10 | from ignis.utils import Utils 11 | from ignis.menu_model import IgnisMenuModel, IgnisMenuItem, IgnisMenuSeparator 12 | from gi.repository import Gio # type: ignore 13 | 14 | app = IgnisApp.get_default() 15 | 16 | applications = ApplicationsService.get_default() 17 | 18 | TERMINAL_FORMAT = "kitty %command%" 19 | 20 | 21 | def is_url(url: str) -> bool: 22 | regex = re.compile( 23 | r"^(?:http|ftp)s?://" # http:// or https:// 24 | r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|" # domain 25 | r"localhost|" # localhost 26 | r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|" # or ipv4 27 | r"\[?[A-F0-9]*:[A-F0-9:]+\]?)" # or ipv6 28 | r"(?::\d+)?" # optional port 29 | r"(?:/?|[/?]\S+)$", 30 | re.IGNORECASE, 31 | ) 32 | 33 | return re.match(regex, url) is not None 34 | 35 | 36 | class LauncherAppItem(Widget.Button): 37 | def __init__(self, application: Application) -> None: 38 | self._menu = Widget.PopoverMenu() 39 | 40 | self._application = application 41 | super().__init__( 42 | on_click=lambda x: self.launch(), 43 | on_right_click=lambda x: self._menu.popup(), 44 | css_classes=["launcher-app"], 45 | child=Widget.Box( 46 | child=[ 47 | Widget.Icon(image=application.icon, pixel_size=48), 48 | Widget.Label( 49 | label=application.name, 50 | ellipsize="end", 51 | max_width_chars=30, 52 | css_classes=["launcher-app-label"], 53 | ), 54 | self._menu, 55 | ] 56 | ), 57 | ) 58 | self.__sync_menu() 59 | application.connect("notify::is-pinned", lambda x, y: self.__sync_menu()) 60 | 61 | def launch(self) -> None: 62 | self._application.launch(terminal_format=TERMINAL_FORMAT) 63 | app.close_window("ignis_LAUNCHER") 64 | 65 | def launch_action(self, action: ApplicationAction) -> None: 66 | action.launch() 67 | app.close_window("ignis_LAUNCHER") 68 | 69 | def __sync_menu(self) -> None: 70 | self._menu.model = IgnisMenuModel( 71 | IgnisMenuItem(label="Launch", on_activate=lambda x: self.launch()), 72 | IgnisMenuSeparator(), 73 | *( 74 | IgnisMenuItem( 75 | label=i.name, 76 | on_activate=lambda x, action=i: self.launch_action(action), 77 | ) 78 | for i in self._application.actions 79 | ), 80 | IgnisMenuSeparator(), 81 | IgnisMenuItem(label="Pin", on_activate=lambda x: self._application.pin()) 82 | if not self._application.is_pinned 83 | else IgnisMenuItem( 84 | label="Unpin", on_activate=lambda x: self._application.unpin() 85 | ), 86 | ) 87 | 88 | 89 | class SearchWebButton(Widget.Button): 90 | def __init__(self, query: str): 91 | self._query = query 92 | self._url = "" 93 | 94 | browser_desktop_file = Utils.exec_sh( 95 | "xdg-settings get default-web-browser" 96 | ).stdout.replace("\n", "") 97 | 98 | app_info = Gio.DesktopAppInfo.new(desktop_id=browser_desktop_file) 99 | 100 | icon_name = "applications-internet-symbolic" 101 | if app_info: 102 | icon_string = app_info.get_string("Icon") 103 | if icon_string: 104 | icon_name = icon_string 105 | 106 | if not query.startswith(("http://", "https://")) and "." in query: 107 | query = "https://" + query 108 | 109 | if is_url(query): 110 | label = f"Visit {query}" 111 | self._url = query 112 | else: 113 | label = "Search in Google" 114 | self._url = f"https://www.google.com/search?q={query.replace(' ', '+')}" 115 | 116 | super().__init__( 117 | on_click=lambda x: self.launch(), 118 | css_classes=["launcher-app"], 119 | child=Widget.Box( 120 | child=[ 121 | Widget.Icon(image=icon_name, pixel_size=48), 122 | Widget.Label( 123 | label=label, 124 | css_classes=["launcher-app-label"], 125 | ), 126 | ] 127 | ), 128 | ) 129 | 130 | def launch(self) -> None: 131 | asyncio.create_task(Utils.exec_sh_async(f"xdg-open {self._url}")) 132 | app.close_window("ignis_LAUNCHER") 133 | 134 | 135 | class Launcher(Widget.Window): 136 | def __init__(self): 137 | self._app_list = Widget.Box( 138 | vertical=True, visible=False, style="margin-top: 1rem;" 139 | ) 140 | self._entry = Widget.Entry( 141 | hexpand=True, 142 | placeholder_text="Search", 143 | css_classes=["launcher-search"], 144 | on_change=self.__search, 145 | on_accept=self.__on_accept, 146 | ) 147 | 148 | main_box = Widget.Box( 149 | vertical=True, 150 | valign="start", 151 | halign="center", 152 | css_classes=["launcher"], 153 | child=[ 154 | Widget.Box( 155 | css_classes=["launcher-search-box"], 156 | child=[ 157 | Widget.Icon( 158 | icon_name="system-search-symbolic", 159 | pixel_size=24, 160 | style="margin-right: 0.5rem;", 161 | ), 162 | self._entry, 163 | ], 164 | ), 165 | self._app_list, 166 | ], 167 | ) 168 | 169 | super().__init__( 170 | namespace="ignis_LAUNCHER", 171 | visible=False, 172 | popup=True, 173 | kb_mode="on_demand", 174 | css_classes=["unset"], 175 | setup=lambda self: self.connect("notify::visible", self.__on_open), 176 | anchor=["top", "right", "bottom", "left"], 177 | child=Widget.Overlay( 178 | child=Widget.Button( 179 | vexpand=True, 180 | hexpand=True, 181 | can_focus=False, 182 | css_classes=["unset"], 183 | on_click=lambda x: app.close_window("ignis_LAUNCHER"), 184 | style="background-color: rgba(0, 0, 0, 0.3);", 185 | ), 186 | overlays=[main_box], 187 | ), 188 | ) 189 | 190 | def __on_open(self, *args) -> None: 191 | if not self.visible: 192 | return 193 | 194 | self._entry.text = "" 195 | self._entry.grab_focus() 196 | 197 | def __on_accept(self, *args) -> None: 198 | if len(self._app_list.child) > 0: 199 | self._app_list.child[0].launch() 200 | 201 | def __search(self, *args) -> None: 202 | query = self._entry.text 203 | 204 | if query == "": 205 | self._entry.grab_focus() 206 | self._app_list.visible = False 207 | return 208 | 209 | apps = applications.search(applications.apps, query) 210 | if apps == []: 211 | self._app_list.child = [SearchWebButton(query)] 212 | else: 213 | self._app_list.visible = True 214 | self._app_list.child = [LauncherAppItem(i) for i in apps[:5]] 215 | -------------------------------------------------------------------------------- /ignis/modules/notification_popup/__init__.py: -------------------------------------------------------------------------------- 1 | from .notification_popup import NotificationPopup 2 | 3 | __all__ = ["NotificationPopup"] 4 | -------------------------------------------------------------------------------- /ignis/modules/notification_popup/notification_popup.py: -------------------------------------------------------------------------------- 1 | from ignis.widgets import Widget 2 | from ignis.app import IgnisApp 3 | from ignis.utils import Utils 4 | from ignis.services.notifications import Notification, NotificationService 5 | from ..shared_widgets import NotificationWidget 6 | 7 | 8 | app = IgnisApp.get_default() 9 | 10 | notifications = NotificationService.get_default() 11 | 12 | 13 | class Popup(Widget.Box): 14 | def __init__( 15 | self, box: "PopupBox", window: "NotificationPopup", notification: Notification 16 | ): 17 | self._box = box 18 | self._window = window 19 | 20 | widget = NotificationWidget(notification) 21 | widget.css_classes = ["notification-popup"] 22 | self._inner = Widget.Revealer(transition_type="slide_left", child=widget) 23 | self._outer = Widget.Revealer(transition_type="slide_down", child=self._inner) 24 | super().__init__(child=[self._outer], halign="end") 25 | 26 | notification.connect("dismissed", lambda x: self.destroy()) 27 | 28 | def destroy(self): 29 | def box_destroy(): 30 | self.unparent() 31 | if len(notifications.popups) == 0: 32 | self._window.visible = False 33 | 34 | def outer_close(): 35 | self._outer.reveal_child = False 36 | Utils.Timeout(self._outer.transition_duration, box_destroy) 37 | 38 | self._inner.transition_type = "crossfade" 39 | self._inner.reveal_child = False 40 | Utils.Timeout(self._outer.transition_duration, outer_close) 41 | 42 | 43 | class PopupBox(Widget.Box): 44 | def __init__(self, window: "NotificationPopup", monitor: int): 45 | self._window = window 46 | self._monitor = monitor 47 | 48 | super().__init__( 49 | vertical=True, 50 | valign="start", 51 | setup=lambda self: notifications.connect( 52 | "new_popup", 53 | lambda x, notification: self.__on_notified(notification), 54 | ), 55 | ) 56 | 57 | def __on_notified(self, notification: Notification) -> None: 58 | self._window.visible = True 59 | popup = Popup(box=self, window=self._window, notification=notification) 60 | self.prepend(popup) 61 | popup._outer.reveal_child = True 62 | Utils.Timeout( 63 | popup._outer.transition_duration, popup._inner.set_reveal_child, True 64 | ) 65 | 66 | 67 | class NotificationPopup(Widget.Window): 68 | def __init__(self, monitor: int): 69 | super().__init__( 70 | anchor=["right", "top", "bottom"], 71 | monitor=monitor, 72 | namespace=f"ignis_NOTIFICATION_POPUP_{monitor}", 73 | layer="top", 74 | child=PopupBox(window=self, monitor=monitor), 75 | visible=False, 76 | dynamic_input_region=True, 77 | css_classes=["rec-unset"], 78 | style="min-width: 29rem;", 79 | ) 80 | -------------------------------------------------------------------------------- /ignis/modules/osd/__init__.py: -------------------------------------------------------------------------------- 1 | from .osd import OSD 2 | 3 | __all__ = ["OSD"] 4 | -------------------------------------------------------------------------------- /ignis/modules/osd/osd.py: -------------------------------------------------------------------------------- 1 | from ignis.widgets import Widget 2 | from ignis.utils import Utils 3 | from ignis.services.audio import AudioService 4 | from ..shared_widgets import MaterialVolumeSlider 5 | 6 | audio = AudioService.get_default() 7 | 8 | 9 | class OSD(Widget.Window): 10 | def __init__(self): 11 | super().__init__( 12 | layer="overlay", 13 | anchor=["bottom"], 14 | namespace="ignis_OSD", 15 | visible=False, 16 | css_classes=["rec-unset"], 17 | child=Widget.Box( 18 | css_classes=["osd"], 19 | child=[ 20 | Widget.Icon( 21 | pixel_size=26, 22 | style="margin-right: 0.5rem;", 23 | image=audio.speaker.bind("icon_name"), 24 | ), 25 | MaterialVolumeSlider(stream=audio.speaker, sensitive=False), 26 | ], 27 | ), 28 | ) 29 | 30 | def set_property(self, property_name, value): 31 | if property_name == "visible": 32 | self.__update_visible() 33 | 34 | super().set_property(property_name, value) 35 | 36 | @Utils.debounce(3000) 37 | def __update_visible(self) -> None: 38 | super().set_property("visible", False) 39 | -------------------------------------------------------------------------------- /ignis/modules/powermenu/__init__.py: -------------------------------------------------------------------------------- 1 | from .powermenu import Powermenu 2 | 3 | __all__ = ["Powermenu"] 4 | -------------------------------------------------------------------------------- /ignis/modules/powermenu/powermenu.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from ignis.widgets import Widget 3 | from ignis.utils import Utils 4 | from ignis.app import IgnisApp 5 | from typing import Callable 6 | 7 | app = IgnisApp.get_default() 8 | 9 | def create_exec_task(cmd: str) -> None: 10 | asyncio.create_task(Utils.exec_sh_async(cmd)) 11 | 12 | class PowermenuButton(Widget.Box): 13 | def __init__(self, label: str, icon_name: str, on_click: Callable) -> None: 14 | super().__init__( 15 | child=[ 16 | Widget.Button( 17 | child=Widget.Icon(image=icon_name, pixel_size=36), 18 | on_click=on_click, 19 | css_classes=["powermenu-button", "unset"], 20 | ), 21 | Widget.Label(label=label, css_classes=["powermenu-button-label"]), 22 | ], 23 | vertical=True, 24 | css_classes=["powermenu-button-box"], 25 | ) 26 | 27 | 28 | class PowerOffButton(PowermenuButton): 29 | def __init__(self): 30 | super().__init__( 31 | label="Power off", 32 | icon_name="system-shutdown-symbolic", 33 | on_click=lambda *args: create_exec_task("poweroff"), 34 | ) 35 | 36 | 37 | class RebootButton(PowermenuButton): 38 | def __init__(self): 39 | super().__init__( 40 | label="Reboot", 41 | icon_name="system-reboot-symbolic", 42 | on_click=lambda *args: create_exec_task("reboot"), 43 | ) 44 | 45 | 46 | class SuspendButton(PowermenuButton): 47 | def __init__(self): 48 | super().__init__( 49 | label="Suspend", icon_name="night-light-symbolic", on_click=self.__invoke 50 | ) 51 | 52 | def __invoke(self, *args) -> None: 53 | app.close_window("ignis_POWERMENU") 54 | create_exec_task("systemctl suspend && hyprlock") 55 | 56 | 57 | class HyprlandExitButton(PowermenuButton): 58 | def __init__(self): 59 | super().__init__( 60 | label="Sign out", 61 | icon_name="system-log-out-symbolic", 62 | on_click=lambda *args: create_exec_task("hyprctl dispatch exit 0"), 63 | ) 64 | 65 | 66 | class Powermenu(Widget.Window): 67 | def __init__(self): 68 | main_box = Widget.Box( 69 | vertical=True, 70 | valign="center", 71 | halign="center", 72 | css_classes=["powermenu"], 73 | child=[ 74 | Widget.Box( 75 | child=[ 76 | PowerOffButton(), 77 | RebootButton(), 78 | ] 79 | ), 80 | Widget.Box( 81 | child=[ 82 | SuspendButton(), 83 | HyprlandExitButton(), 84 | ] 85 | ), 86 | ], 87 | ) 88 | super().__init__( 89 | popup=True, 90 | kb_mode="on_demand", 91 | namespace="ignis_POWERMENU", 92 | exclusivity="ignore", 93 | anchor=["left", "right", "top", "bottom"], 94 | visible=False, 95 | child=Widget.Overlay( 96 | child=Widget.Button( 97 | vexpand=True, 98 | hexpand=True, 99 | can_focus=False, 100 | css_classes=["unset", "powermenu-overlay"], 101 | on_click=lambda x: app.close_window("ignis_POWERMENU"), 102 | ), 103 | overlays=[main_box], 104 | ), 105 | css_classes=["unset"], 106 | ) 107 | -------------------------------------------------------------------------------- /ignis/modules/settings/__init__.py: -------------------------------------------------------------------------------- 1 | from .settings import Settings 2 | 3 | __all__ = [ 4 | "Settings", 5 | ] 6 | -------------------------------------------------------------------------------- /ignis/modules/settings/active_page.py: -------------------------------------------------------------------------------- 1 | from ignis.variable import Variable 2 | from .elements import SettingsPage 3 | 4 | fallback_page = SettingsPage(name="Settings") 5 | active_page = Variable(value=fallback_page) 6 | -------------------------------------------------------------------------------- /ignis/modules/settings/elements/__init__.py: -------------------------------------------------------------------------------- 1 | from .row import SettingsRow 2 | from .page import SettingsPage 3 | from .group import SettingsGroup 4 | from .switchrow import SwitchRow 5 | from .filerow import FileRow 6 | from .spinrow import SpinRow 7 | from .entryrow import EntryRow 8 | from .entry import SettingsEntry 9 | 10 | __all__ = [ 11 | "SettingsRow", 12 | "SettingsPage", 13 | "SettingsGroup", 14 | "SettingsRow", 15 | "SwitchRow", 16 | "FileRow", 17 | "SpinRow", 18 | "EntryRow", 19 | "SettingsEntry", 20 | ] 21 | -------------------------------------------------------------------------------- /ignis/modules/settings/elements/entry.py: -------------------------------------------------------------------------------- 1 | from ignis.widgets import Widget 2 | from .page import SettingsPage 3 | 4 | 5 | class SettingsEntry(Widget.ListBoxRow): 6 | def __init__( 7 | self, 8 | icon: str, 9 | label: str, 10 | page: SettingsPage, 11 | **kwargs, 12 | ): 13 | from ..active_page import active_page # avoid a circular import 14 | 15 | super().__init__( 16 | child=Widget.Box( 17 | child=[ 18 | Widget.Icon(image=icon, pixel_size=20), 19 | Widget.Label(label=label, style="margin-left: 0.75rem;"), 20 | ], 21 | ), 22 | css_classes=["settings-sidebar-entry"], 23 | on_activate=lambda x: active_page.set_value(page), 24 | **kwargs, 25 | ) 26 | -------------------------------------------------------------------------------- /ignis/modules/settings/elements/entryrow.py: -------------------------------------------------------------------------------- 1 | from ignis.widgets import Widget 2 | from .row import SettingsRow 3 | from typing import Callable 4 | from ignis.gobject import Binding 5 | 6 | 7 | class EntryRow(SettingsRow): 8 | def __init__( 9 | self, 10 | text: str | Binding | None = None, 11 | on_change: Callable | None = None, 12 | width: int | None = None, 13 | **kwargs, 14 | ): 15 | super().__init__(**kwargs) 16 | self._entry = Widget.Entry( 17 | on_change=on_change, 18 | text=text, 19 | halign="end", 20 | valign="center", 21 | width_request=width, 22 | hexpand=True, 23 | ) 24 | self.child.append(self._entry) 25 | -------------------------------------------------------------------------------- /ignis/modules/settings/elements/filerow.py: -------------------------------------------------------------------------------- 1 | from ignis.widgets import Widget 2 | from .row import SettingsRow 3 | from ignis.gobject import Binding 4 | 5 | 6 | class FileRow(SettingsRow): 7 | def __init__( 8 | self, 9 | dialog: Widget.FileDialog, 10 | button_label: str | Binding | None = None, 11 | **kwargs, 12 | ): 13 | super().__init__(**kwargs) 14 | self._button = Widget.FileChooserButton( 15 | dialog=dialog, 16 | label=Widget.Label( 17 | label=button_label, ellipsize="start", max_width_chars=20 18 | ), 19 | hexpand=True, 20 | halign="end", 21 | ) 22 | 23 | self.child.append(self._button) 24 | -------------------------------------------------------------------------------- /ignis/modules/settings/elements/group.py: -------------------------------------------------------------------------------- 1 | from ignis.widgets import Widget 2 | from .row import SettingsRow 3 | from ignis.base_widget import BaseWidget 4 | 5 | 6 | class SettingsGroup(Widget.Box): 7 | def __init__( 8 | self, name: str | None, rows: list[SettingsRow | BaseWidget] = [], **kwargs 9 | ): 10 | super().__init__( 11 | vertical=True, 12 | css_classes=["settings-group"], 13 | child=[ 14 | Widget.Label( 15 | label=name, 16 | css_classes=["settings-group-name"], 17 | halign="start", 18 | visible=True if name else False, 19 | ), 20 | Widget.ListBox(rows=[*rows]), 21 | ], 22 | **kwargs, 23 | ) 24 | -------------------------------------------------------------------------------- /ignis/modules/settings/elements/page.py: -------------------------------------------------------------------------------- 1 | from ignis.widgets import Widget 2 | from .group import SettingsGroup 3 | from ignis.base_widget import BaseWidget 4 | 5 | 6 | class SettingsPage(Widget.Scroll): 7 | def __init__(self, name: str, groups: list[SettingsGroup | BaseWidget] = []): 8 | super().__init__( 9 | hexpand=True, 10 | vexpand=True, 11 | child=Widget.Box( 12 | vertical=True, 13 | hexpand=True, 14 | vexpand=True, 15 | css_classes=["settings-page"], 16 | child=[ 17 | Widget.Label( 18 | label=name, css_classes=["settings-page-name"], halign="start" 19 | ), 20 | *groups, 21 | ], 22 | ), 23 | ) 24 | -------------------------------------------------------------------------------- /ignis/modules/settings/elements/row.py: -------------------------------------------------------------------------------- 1 | from ignis.widgets import Widget 2 | 3 | 4 | class SettingsRow(Widget.ListBoxRow): 5 | def __init__( 6 | self, 7 | label: str | None = None, 8 | sublabel: str | None = None, 9 | **kwargs, 10 | ): 11 | super().__init__( 12 | css_classes=["settings-row"], 13 | child=Widget.Box( 14 | child=[ 15 | Widget.Box( 16 | vertical=True, 17 | child=[ 18 | Widget.Label( 19 | label=label, 20 | css_classes=["settings-row-label"], 21 | halign="start", 22 | vexpand=True, 23 | wrap=True, 24 | visible=True if label else False, 25 | ), 26 | Widget.Label( 27 | label=sublabel, 28 | css_classes=["settings-row-sublabel"], 29 | halign="start", 30 | vexpand=True, 31 | wrap=True, 32 | visible=True if sublabel else False, 33 | ), 34 | ], 35 | ) 36 | ] 37 | ), 38 | **kwargs, 39 | ) 40 | -------------------------------------------------------------------------------- /ignis/modules/settings/elements/spinrow.py: -------------------------------------------------------------------------------- 1 | from ignis.widgets import Widget 2 | from .row import SettingsRow 3 | from typing import Callable 4 | from ignis.gobject import Binding 5 | 6 | 7 | class SpinRow(SettingsRow): 8 | def __init__( 9 | self, 10 | value: int | Binding = 0, 11 | on_change: Callable | None = None, 12 | min: int = 0, 13 | max: int = 100, 14 | step: int = 1, 15 | width: int = 0, 16 | **kwargs, 17 | ): 18 | super().__init__(**kwargs) 19 | self._spin_button = Widget.SpinButton( 20 | value=value, 21 | on_change=on_change, 22 | min=min, 23 | max=max, 24 | halign="end", 25 | valign="center", 26 | width_request=width, 27 | hexpand=True, 28 | step=step, 29 | ) 30 | self.child.append(self._spin_button) 31 | -------------------------------------------------------------------------------- /ignis/modules/settings/elements/switchrow.py: -------------------------------------------------------------------------------- 1 | from ignis.widgets import Widget 2 | from .row import SettingsRow 3 | from typing import Callable 4 | from ignis.gobject import Binding 5 | 6 | 7 | class SwitchRow(SettingsRow): 8 | def __init__( 9 | self, 10 | active: bool | Binding = False, 11 | on_change: Callable | None = None, 12 | **kwargs, 13 | ): 14 | super().__init__(**kwargs) 15 | self._switch = Widget.Switch( 16 | active=active, 17 | on_change=on_change, 18 | halign="end", 19 | valign="center", 20 | hexpand=True, 21 | ) 22 | self.on_activate = lambda x: self._switch.emit( 23 | "activate" 24 | ) # if set "active" property animation will not work 25 | self.child.append(self._switch) 26 | -------------------------------------------------------------------------------- /ignis/modules/settings/pages/__init__.py: -------------------------------------------------------------------------------- 1 | from .about import AboutEntry 2 | from .appearance import AppearanceEntry 3 | from .notifications import NotificationsEntry 4 | from .recorder import RecorderEntry 5 | from .user import UserEntry 6 | 7 | __all__ = [ 8 | "AboutEntry", 9 | "AppearanceEntry", 10 | "NotificationsEntry", 11 | "RecorderEntry", 12 | "UserEntry", 13 | ] 14 | -------------------------------------------------------------------------------- /ignis/modules/settings/pages/about.py: -------------------------------------------------------------------------------- 1 | from ..elements import SettingsPage, SettingsRow, SettingsEntry, SettingsGroup 2 | from ignis.utils import Utils 3 | from ignis.widgets import Widget 4 | from ignis.services.fetch import FetchService 5 | from user_options import user_options 6 | 7 | fetch = FetchService.get_default() 8 | 9 | 10 | class AboutEntry(SettingsEntry): 11 | def __init__(self): 12 | page = SettingsPage( 13 | name="About", 14 | groups=[ 15 | Widget.Box( 16 | child=[ 17 | Widget.Picture( 18 | image=user_options.material.bind( 19 | "dark_mode", 20 | transform=lambda value: fetch.os_logo_text_dark 21 | if value 22 | else fetch.os_logo_text, 23 | ), 24 | width=300, 25 | height=100, 26 | ) 27 | ], 28 | halign="center", 29 | width_request=300, 30 | height_request=100, 31 | ), 32 | SettingsGroup( 33 | name="Info", 34 | rows=[ 35 | SettingsRow(label="OS", sublabel=fetch.os_name), 36 | SettingsRow( 37 | label="Ignis version", sublabel=Utils.get_ignis_version() 38 | ), 39 | SettingsRow(label="Session type", sublabel=fetch.session_type), 40 | SettingsRow( 41 | label="Wayland compositor", sublabel=fetch.current_desktop 42 | ), 43 | SettingsRow(label="Kernel", sublabel=fetch.kernel), 44 | ], 45 | ), 46 | ], 47 | ) 48 | super().__init__( 49 | label="About", 50 | icon="help-about-symbolic", 51 | page=page, 52 | ) 53 | -------------------------------------------------------------------------------- /ignis/modules/settings/pages/appearance.py: -------------------------------------------------------------------------------- 1 | import os 2 | from services.material import MaterialService 3 | from ..elements import SwitchRow, SettingsPage, SettingsGroup, FileRow, SettingsEntry 4 | from ignis.widgets import Widget 5 | from user_options import user_options 6 | from ignis.options import options 7 | 8 | material = MaterialService.get_default() 9 | 10 | 11 | class AppearanceEntry(SettingsEntry): 12 | def __init__(self): 13 | page = SettingsPage( 14 | name="Appearance", 15 | groups=[ 16 | SettingsGroup( 17 | name=None, 18 | rows=[ 19 | Widget.ListBoxRow( 20 | child=Widget.Picture( 21 | image=options.wallpaper.bind("wallpaper_path"), 22 | width=1920 // 4, 23 | height=1080 // 4, 24 | halign="center", 25 | style="border-radius: 1rem;", 26 | content_fit="cover", 27 | ), 28 | selectable=False, 29 | activatable=False, 30 | ), 31 | SwitchRow( 32 | label="Dark mode", 33 | active=user_options.material.bind("dark_mode"), 34 | on_change=lambda x, 35 | state: user_options.material.set_dark_mode(state), 36 | style="margin-top: 1rem;", 37 | ), 38 | FileRow( 39 | label="Wallpaper path", 40 | button_label=os.path.basename( 41 | options.wallpaper.wallpaper_path 42 | ) 43 | if options.wallpaper.wallpaper_path 44 | else None, 45 | dialog=Widget.FileDialog( 46 | on_file_set=lambda x, file: material.generate_colors( 47 | file.get_path() 48 | ), 49 | initial_path=options.wallpaper.bind("wallpaper_path"), 50 | filters=[ 51 | Widget.FileFilter( 52 | mime_types=["image/jpeg", "image/png"], 53 | default=True, 54 | name="Images JPEG/PNG", 55 | ) 56 | ], 57 | ), 58 | ), 59 | ], 60 | ) 61 | ], 62 | ) 63 | super().__init__( 64 | label="Appearance", 65 | icon="preferences-desktop-wallpaper-symbolic", 66 | page=page, 67 | ) 68 | -------------------------------------------------------------------------------- /ignis/modules/settings/pages/notifications.py: -------------------------------------------------------------------------------- 1 | from ..elements import SwitchRow, SettingsPage, SettingsGroup, SpinRow, SettingsEntry 2 | from ignis.options import options 3 | 4 | 5 | class NotificationsEntry(SettingsEntry): 6 | def __init__(self): 7 | page = SettingsPage( 8 | name="Notifications", 9 | groups=[ 10 | SettingsGroup( 11 | name="General", 12 | rows=[ 13 | SwitchRow( 14 | label="Do not disturb", 15 | active=options.notifications.bind("dnd"), 16 | on_change=lambda x, state: options.notifications.set_dnd( 17 | state 18 | ), 19 | ), 20 | SpinRow( 21 | label="Maximum popups count", 22 | sublabel="The first popup will automatically dismiss", 23 | value=options.notifications.bind("max_popups_count"), 24 | min=1, 25 | on_change=lambda x, 26 | value: options.notifications.set_max_popups_count(value), 27 | ), 28 | SpinRow( 29 | label="Popup timeout", 30 | sublabel="Timeout before popup will be dismissed, in milliseconds.", 31 | max=100000, 32 | step=100, 33 | value=options.notifications.bind("popup_timeout"), 34 | on_change=lambda x, 35 | value: options.notifications.set_popup_timeout(value), 36 | ), 37 | ], 38 | ) 39 | ], 40 | ) 41 | super().__init__( 42 | label="Notifications", 43 | icon="notification-symbolic", 44 | page=page, 45 | ) 46 | -------------------------------------------------------------------------------- /ignis/modules/settings/pages/recorder.py: -------------------------------------------------------------------------------- 1 | from ..elements import ( 2 | SpinRow, 3 | SettingsPage, 4 | SettingsGroup, 5 | EntryRow, 6 | FileRow, 7 | SettingsEntry, 8 | ) 9 | from ignis.widgets import Widget 10 | from ignis.options import options 11 | 12 | 13 | class RecorderEntry(SettingsEntry): 14 | def __init__(self): 15 | page = SettingsPage( 16 | name="Recorder", 17 | groups=[ 18 | SettingsGroup( 19 | name="General", 20 | rows=[ 21 | SpinRow( 22 | label="Recording bitrate", 23 | sublabel="Affects the recording quality", 24 | value=options.recorder.bind("bitrate"), 25 | max=640000, 26 | width=150, 27 | on_change=lambda x, value: options.recorder.set_bitrate( 28 | int(value) 29 | ), 30 | step=1000, 31 | ), 32 | FileRow( 33 | label="Recording path", 34 | button_label=options.recorder.bind("default_file_location"), 35 | dialog=Widget.FileDialog( 36 | on_file_set=lambda x, 37 | file: options.recorder.set_default_file_location( 38 | file.get_path() 39 | ), 40 | select_folder=True, 41 | initial_path=options.recorder.default_file_location, 42 | ), 43 | ), 44 | EntryRow( 45 | label="Recording filename", 46 | sublabel="Support time formatting", 47 | text=options.recorder.bind("default_filename"), 48 | on_change=lambda x: options.recorder.set_default_filename( 49 | x.text 50 | ), 51 | width=200, 52 | ), 53 | ], 54 | ) 55 | ], 56 | ) 57 | super().__init__( 58 | label="Recorder", 59 | icon="media-record-symbolic", 60 | page=page, 61 | ) 62 | -------------------------------------------------------------------------------- /ignis/modules/settings/pages/user.py: -------------------------------------------------------------------------------- 1 | import os 2 | from ..elements import SettingsGroup, SettingsPage, SettingsEntry, FileRow 3 | from ignis.widgets import Widget 4 | from user_options import user_options 5 | 6 | 7 | class UserEntry(SettingsEntry): 8 | def __init__(self): 9 | page = SettingsPage( 10 | name="User", 11 | groups=[ 12 | Widget.Box( 13 | halign="start", 14 | style="margin-left: 2rem;", 15 | child=[ 16 | Widget.Picture( 17 | image=user_options.user.bind( 18 | "avatar", 19 | lambda value: "user-info" 20 | if not os.path.exists(value) 21 | else value, 22 | ), 23 | width=96, 24 | height=96, 25 | style="border-radius: 10rem;", 26 | ), 27 | Widget.Label( 28 | label=os.getenv("USER"), css_classes=["settings-user-name"] 29 | ), 30 | ], 31 | ), 32 | SettingsGroup( 33 | style="margin-top: 2rem;", 34 | valign="start", 35 | name="General", 36 | vexpand=True, 37 | rows=[ 38 | FileRow( 39 | label="Avatar", 40 | dialog=Widget.FileDialog( 41 | initial_path=user_options.user.bind("avatar"), 42 | on_file_set=lambda x, 43 | gfile: user_options.user.set_avatar(gfile.get_path()), 44 | ), 45 | ) 46 | ], 47 | ), 48 | ], 49 | ) 50 | super().__init__( 51 | label="User", 52 | icon="user-available-symbolic", 53 | page=page, 54 | ) 55 | -------------------------------------------------------------------------------- /ignis/modules/settings/settings.py: -------------------------------------------------------------------------------- 1 | from ignis.widgets import Widget 2 | from ignis.app import IgnisApp 3 | from user_options import user_options 4 | from .active_page import active_page 5 | from .pages import ( 6 | AboutEntry, 7 | AppearanceEntry, 8 | NotificationsEntry, 9 | RecorderEntry, 10 | UserEntry, 11 | ) 12 | 13 | app = IgnisApp.get_default() 14 | 15 | 16 | class Settings(Widget.RegularWindow): 17 | def __init__(self) -> None: 18 | content = Widget.Box( 19 | hexpand=True, 20 | vexpand=True, 21 | child=active_page.bind("value", transform=lambda value: [value]), 22 | ) 23 | self._listbox = Widget.ListBox() 24 | 25 | navigation_sidebar = Widget.Box( 26 | vertical=True, 27 | css_classes=["settings-sidebar"], 28 | child=[ 29 | Widget.Label( 30 | label="Settings", 31 | halign="start", 32 | css_classes=["settings-sidebar-label"], 33 | ), 34 | self._listbox, 35 | ], 36 | ) 37 | 38 | super().__init__( 39 | default_width=900, 40 | default_height=600, 41 | resizable=False, 42 | hide_on_close=True, 43 | visible=False, 44 | child=Widget.Box(child=[navigation_sidebar, content]), 45 | namespace="ignis_SETTINGS", 46 | ) 47 | 48 | self.connect("notify::visible", self.__on_open) 49 | 50 | def __on_open(self, *args) -> None: 51 | if self.visible is False: 52 | return 53 | 54 | if len(self._listbox.rows) != 0: 55 | return 56 | 57 | rows = [ 58 | NotificationsEntry(), 59 | RecorderEntry(), 60 | AppearanceEntry(), 61 | UserEntry(), 62 | AboutEntry(), 63 | ] 64 | 65 | self._listbox.rows = rows 66 | self._listbox.activate_row(rows[user_options.settings.last_page]) 67 | 68 | self._listbox.connect("row-activated", self.__update_last_page) 69 | 70 | def __update_last_page(self, x, row) -> None: 71 | user_options.settings.last_page = self._listbox.rows.index(row) 72 | -------------------------------------------------------------------------------- /ignis/modules/shared_widgets/__init__.py: -------------------------------------------------------------------------------- 1 | from .toggle_box import ToggleBox 2 | from .notification import NotificationWidget 3 | from .volume_slider import MaterialVolumeSlider 4 | 5 | __all__ = ["ToggleBox", "NotificationWidget", "MaterialVolumeSlider"] 6 | -------------------------------------------------------------------------------- /ignis/modules/shared_widgets/notification.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from ignis.widgets import Widget 3 | from ignis.services.notifications import Notification 4 | from ignis.utils import Utils 5 | 6 | 7 | class ScreenshotLayout(Widget.Box): 8 | def __init__(self, notification: Notification) -> None: 9 | super().__init__( 10 | vertical=True, 11 | hexpand=True, 12 | child=[ 13 | Widget.Box( 14 | child=[ 15 | Widget.Picture( 16 | image=notification.icon, 17 | content_fit="cover", 18 | width=1920 // 7, 19 | height=1080 // 7, 20 | style="border-radius: 1rem; background-color: black;", 21 | ), 22 | Widget.Button( 23 | child=Widget.Icon( 24 | image="window-close-symbolic", pixel_size=20 25 | ), 26 | halign="end", 27 | valign="start", 28 | hexpand=True, 29 | css_classes=["notification-close"], 30 | on_click=lambda x: notification.close(), 31 | ), 32 | ], 33 | ), 34 | Widget.Label( 35 | label="Screenshot saved", 36 | css_classes=["notification-screenshot-label"], 37 | ), 38 | Widget.Box( 39 | homogeneous=True, 40 | style="margin-top: 0.75rem;", 41 | spacing=10, 42 | child=[ 43 | Widget.Button( 44 | child=Widget.Label(label="Open"), 45 | css_classes=["notification-action"], 46 | on_click=lambda x: asyncio.create_task( 47 | Utils.exec_sh_async(f"xdg-open {notification.icon}") 48 | ), 49 | ), 50 | Widget.Button( 51 | child=Widget.Label(label="Close"), 52 | css_classes=["notification-action"], 53 | on_click=lambda x: notification.close(), 54 | ), 55 | ], 56 | ), 57 | ], 58 | ) 59 | 60 | 61 | class NormalLayout(Widget.Box): 62 | def __init__(self, notification: Notification) -> None: 63 | super().__init__( 64 | vertical=True, 65 | hexpand=True, 66 | child=[ 67 | Widget.Box( 68 | child=[ 69 | Widget.Icon( 70 | image=notification.icon 71 | if notification.icon 72 | else "dialog-information-symbolic", 73 | pixel_size=48, 74 | halign="start", 75 | valign="start", 76 | ), 77 | Widget.Box( 78 | vertical=True, 79 | style="margin-left: 0.75rem;", 80 | child=[ 81 | Widget.Label( 82 | ellipsize="end", 83 | label=notification.summary, 84 | halign="start", 85 | visible=notification.summary != "", 86 | css_classes=["notification-summary"], 87 | ), 88 | Widget.Label( 89 | label=notification.body, 90 | ellipsize="end", 91 | halign="start", 92 | css_classes=["notification-body"], 93 | visible=notification.body != "", 94 | ), 95 | ], 96 | ), 97 | Widget.Button( 98 | child=Widget.Icon( 99 | image="window-close-symbolic", pixel_size=20 100 | ), 101 | halign="end", 102 | valign="start", 103 | hexpand=True, 104 | css_classes=["notification-close"], 105 | on_click=lambda x: notification.close(), 106 | ), 107 | ], 108 | ), 109 | Widget.Box( 110 | child=[ 111 | Widget.Button( 112 | child=Widget.Label(label=action.label), 113 | on_click=lambda x, action=action: action.invoke(), 114 | css_classes=["notification-action"], 115 | ) 116 | for action in notification.actions 117 | ], 118 | homogeneous=True, 119 | style="margin-top: 0.75rem;" if notification.actions else "", 120 | spacing=10, 121 | ), 122 | ], 123 | ) 124 | 125 | 126 | class NotificationWidget(Widget.Box): 127 | def __init__(self, notification: Notification) -> None: 128 | layout: NormalLayout | ScreenshotLayout 129 | 130 | if notification.app_name == "grimblast": 131 | layout = ScreenshotLayout(notification) 132 | else: 133 | layout = NormalLayout(notification) 134 | 135 | super().__init__( 136 | css_classes=["notification"], 137 | child=[layout], 138 | ) 139 | -------------------------------------------------------------------------------- /ignis/modules/shared_widgets/toggle_box.py: -------------------------------------------------------------------------------- 1 | from ignis.widgets import Widget 2 | from typing import Callable 3 | 4 | 5 | class ToggleBox(Widget.Box): 6 | def __init__( 7 | self, 8 | label: str, 9 | active: bool, 10 | on_change: Callable, 11 | css_classes: list[str] = [], 12 | **kwargs, 13 | ): 14 | super().__init__( 15 | child=[ 16 | Widget.Label(label=label), 17 | Widget.Switch( 18 | halign="end", 19 | hexpand=True, 20 | active=active, 21 | on_change=on_change, 22 | ), 23 | ], 24 | css_classes=["toggle-box"] + css_classes, 25 | ) 26 | -------------------------------------------------------------------------------- /ignis/modules/shared_widgets/volume_slider.py: -------------------------------------------------------------------------------- 1 | from ignis.widgets import Widget 2 | from ignis.services.audio import AudioService, Stream 3 | 4 | audio = AudioService.get_default() 5 | 6 | 7 | class MaterialVolumeSlider(Widget.Scale): 8 | def __init__(self, stream: Stream, **kwargs): 9 | super().__init__( 10 | value=stream.bind_many( 11 | ["volume", "is_muted"], 12 | lambda volume, is_muted: 0 if is_muted or volume is None else volume, 13 | ), 14 | css_classes=["material-slider"], 15 | hexpand=True, 16 | step=5, 17 | **kwargs, 18 | ) 19 | -------------------------------------------------------------------------------- /ignis/scripts/recording.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from ignis.client import IgnisClient 5 | 6 | CODE_TEMPLATE = """ 7 | from ignis.services.recorder import RecorderService 8 | 9 | recorder = RecorderService.get_default() 10 | recorder.{}_recording() 11 | """ 12 | 13 | 14 | def run_code(_type: str) -> None: 15 | client = IgnisClient() 16 | client.run_python(CODE_TEMPLATE.format(_type)) 17 | 18 | 19 | if len(sys.argv) < 2: 20 | sys.exit(1) 21 | 22 | elif sys.argv[1] == "start": 23 | run_code("start") 24 | 25 | elif sys.argv[1] == "stop": 26 | run_code("stop") 27 | 28 | elif sys.argv[1] == "pause": 29 | run_code("pause") 30 | 31 | elif sys.argv[1] == "continue": 32 | run_code("continue") 33 | -------------------------------------------------------------------------------- /ignis/scss/bar.scss: -------------------------------------------------------------------------------- 1 | .bar-widget { 2 | padding: 0.4rem 1rem; 3 | margin: 0; 4 | border-radius: 0; 5 | transition: 0.3s; 6 | background-color: rgba($surface, 0.3); 7 | } 8 | 9 | .clock { 10 | @include hover($surface); 11 | color: $onSurface; 12 | background-color: $surface; 13 | font-size: 1.5rem; 14 | border-radius: 1rem; 15 | padding: 0 0.75rem; 16 | margin: 0 0.5rem; 17 | margin-right: 0; 18 | transition: 0.3s; 19 | 20 | &.active { 21 | background-color: $primary; 22 | color: $surface; 23 | 24 | .record-indicator { 25 | color: $surface; 26 | } 27 | 28 | @include hover($primary) 29 | } 30 | } 31 | 32 | .kb-layout { 33 | border-radius: 0.5rem; 34 | padding: 0 0.25rem; 35 | margin: 0 0.25rem; 36 | transition: 0.3s; 37 | font-size: 1.3rem; 38 | } 39 | 40 | .kb-layout:hover { 41 | background-color: rgba(white, 0.2); 42 | } 43 | 44 | .pinned-app { 45 | margin: 0 0.25rem; 46 | transition: 0.3s; 47 | padding: 0 0.1rem; 48 | border-radius: 0.25rem; 49 | } 50 | 51 | .pinned-app:hover { 52 | background-color: rgba(white, 0.2); 53 | } 54 | 55 | 56 | .tray { 57 | padding: 0 0.25rem; 58 | margin: 0 0.25rem; 59 | } 60 | 61 | .workspaces { 62 | border-radius: 1rem; 63 | padding: 0.5rem; 64 | background-color: $surface; 65 | margin: 0 0.5rem; 66 | margin-left: 0; 67 | } 68 | 69 | .workspace { 70 | @include hover($primaryContainer); 71 | border-radius: 1rem; 72 | margin: 0 0.25rem; 73 | min-width: 0.7rem; 74 | min-height: 0.7rem; 75 | transition: 0.3s; 76 | 77 | &.active { 78 | @include hover($primary); 79 | min-width: 2rem; 80 | } 81 | } 82 | 83 | @keyframes recorder-animation { 84 | 0% { 85 | opacity: 0; 86 | } 87 | 88 | 50% { 89 | opacity: 1; 90 | } 91 | 92 | 90% { 93 | opacity: 1; 94 | } 95 | 96 | 100% { 97 | opacity: 0; 98 | } 99 | } 100 | 101 | .record-indicator { 102 | color: $error; 103 | } 104 | 105 | .record-indicator.active { 106 | animation: recorder-animation 2s infinite; 107 | } 108 | 109 | .battery-item { 110 | margin: 0 0.25rem; 111 | } 112 | 113 | .battery-scale { 114 | min-width: 5rem; 115 | all: unset; 116 | 117 | * { 118 | all: unset; 119 | } 120 | } 121 | 122 | .battery-scale trough, 123 | .battery-scale trough highlight { 124 | background: $surface; 125 | border-radius: 1rem; 126 | min-height: 1.3rem; 127 | min-width: 5rem; 128 | } 129 | 130 | .battery-scale trough highlight { 131 | background: $primary; 132 | } 133 | 134 | .battery-icon { 135 | color: $onSurface; 136 | margin-right: 0.3rem; 137 | } 138 | 139 | .battery-percent { 140 | color: $onSurface; 141 | margin-right: 0.3rem; 142 | } -------------------------------------------------------------------------------- /ignis/scss/control_center.scss: -------------------------------------------------------------------------------- 1 | .control-center { 2 | @include window; 3 | min-width: 28rem; 4 | padding: 0; 5 | 6 | .control-center-widget { 7 | padding: 1rem; 8 | margin-bottom: 1rem; 9 | } 10 | 11 | .qs-main-box { 12 | margin-top: 0.5rem; 13 | } 14 | 15 | .user { 16 | margin-top: 1rem; 17 | } 18 | 19 | .qs-button { 20 | @include hover($surfaceContainerHigh); 21 | color: $onSurface; 22 | min-height: 2.4rem; 23 | border-radius: 1.2rem; 24 | transition: 0.3s; 25 | padding: 0.3rem 0.8rem; 26 | 27 | &.active { 28 | @include hover($primary); 29 | color: $surface; 30 | } 31 | 32 | } 33 | 34 | .qs-button-label { 35 | margin-left: 0.5rem; 36 | } 37 | 38 | .control-center-menu { 39 | background-color: $surfaceContainerHigh; 40 | border-radius: 1rem; 41 | padding: 1rem; 42 | margin-top: 1rem; 43 | margin-bottom: 0.5rem; 44 | 45 | separator { 46 | margin: 0.5rem 0; 47 | } 48 | 49 | } 50 | .control-center-menu-last-row { 51 | margin-bottom: 0; 52 | } 53 | 54 | .record-icon { 55 | color: $error; 56 | } 57 | 58 | .record-dropdown { 59 | button { 60 | all: unset; 61 | color: $onSurfaceVariant; 62 | font-size: 0.9rem; 63 | } 64 | } 65 | 66 | .record-cancel-button, 67 | .record-start-button { 68 | color: $surface; 69 | border-radius: 1rem; 70 | padding: 0.25rem 0.75rem; 71 | font-size: 0.95rem; 72 | transition: 0.3s; 73 | } 74 | 75 | .record-cancel-button { 76 | @include hover($surfaceContainerHigh); 77 | color: $onSurface; 78 | border: 1px solid $primary; 79 | } 80 | 81 | .record-start-button { 82 | @include hover($primary); 83 | } 84 | 85 | .volume-entry { 86 | @include hover($surfaceContainerHigh); 87 | transition: 0.3s; 88 | border-radius: 1rem; 89 | padding: 0.5rem 0.5rem; 90 | } 91 | 92 | .volume-entry-list-header-label { 93 | font-size: 1.35rem; 94 | margin-left: 0.5rem; 95 | } 96 | 97 | .volume-entry-list-header-box { 98 | margin-bottom: 0.75rem; 99 | } 100 | 101 | .user-name-box { 102 | margin-left: 0.5rem; 103 | } 104 | 105 | .user-name { 106 | font-size: 1.2rem; 107 | } 108 | 109 | .user-name-secondary { 110 | color: $onSurfaceVariant; 111 | font-size: 0.9rem; 112 | } 113 | 114 | .user-settings { 115 | @include hover($surfaceContainerHighest); 116 | color: $onSurface; 117 | border-radius: 2rem; 118 | min-width: 2.35rem; 119 | margin: 0.4rem 0rem; 120 | margin-right: 0.5rem; 121 | transition: 0.3s; 122 | } 123 | 124 | .user-power { 125 | @include hover($primary); 126 | color: $surface; 127 | border-radius: 1.5rem; 128 | min-width: 2.35rem; 129 | margin: 0.4rem 0rem; 130 | transition: 0.3s; 131 | } 132 | 133 | .network-header-box { 134 | margin-bottom: 0.5rem; 135 | } 136 | 137 | .network-header-label { 138 | font-size: 1.35rem; 139 | margin-left: 0.75rem; 140 | } 141 | 142 | .network-item { 143 | @include hover($surfaceContainerHigh); 144 | transition: 0.3s; 145 | border-radius: 1rem; 146 | padding: 0.5rem 0.5rem; 147 | 148 | label { 149 | margin-left: 0.5rem; 150 | } 151 | 152 | .connect-label { 153 | font-size: 1rem; 154 | color: $onSurfaceVariant; 155 | } 156 | } 157 | 158 | .volume-entry-label { 159 | margin-left: 0.5rem; 160 | margin-right: 1rem; 161 | } 162 | 163 | .volume-mainbox-speaker { 164 | margin-top: 1rem; 165 | } 166 | 167 | .volume-mainbox-microphone { 168 | margin-top: 0.25rem; 169 | } 170 | 171 | .volume-menubox-speaker { 172 | margin-bottom: 0.75rem; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /ignis/scss/launcher.scss: -------------------------------------------------------------------------------- 1 | .launcher { 2 | @include window; 3 | margin-top: 20rem; 4 | } 5 | 6 | .launcher-search { 7 | all: unset; 8 | } 9 | 10 | .launcher-search-box { 11 | padding: 0.75rem 1rem; 12 | background-color: $surfaceContainer; 13 | border-radius: 1rem; 14 | min-width: 30rem; 15 | } 16 | 17 | .launcher-scroll { 18 | min-height: 30rem; 19 | } 20 | 21 | .launcher-app { 22 | all: unset; 23 | @include hover($surface); 24 | border-radius: 1rem; 25 | padding: 0.5rem 0.25rem; 26 | border: 2px solid $surface; 27 | margin-top: 0.25rem; 28 | transition: 0.3s; 29 | 30 | &:focus { 31 | border: 2px solid $primary; 32 | } 33 | } 34 | 35 | .launcher-app-label { 36 | font-size: 1.1rem; 37 | margin-left: 1rem; 38 | } -------------------------------------------------------------------------------- /ignis/scss/lib.scss: -------------------------------------------------------------------------------- 1 | .rec-unset { 2 | * { 3 | all: unset; 4 | } 5 | } 6 | 7 | .unset { 8 | all: unset; 9 | } 10 | 11 | tooltip { 12 | all: unset; 13 | background-color: $surface; 14 | padding: 0.5rem 1rem; 15 | border-radius: 1rem; 16 | } 17 | 18 | .toggle-box { 19 | padding: 0.75rem 1rem; 20 | border-radius: 1rem; 21 | font-size: 1.35rem; 22 | background-color: $primaryContainer; 23 | } 24 | 25 | .material-slider { 26 | all: unset; 27 | 28 | * { 29 | all: unset; 30 | } 31 | } 32 | 33 | .material-slider trough, 34 | .material-slider trough highlight { 35 | background-color: $secondaryContainer; 36 | border-radius: 1rem; 37 | min-height: 1rem; 38 | } 39 | 40 | 41 | .material-slider trough highlight { 42 | background-color: $primary; 43 | } 44 | 45 | .material-slider-icon { 46 | padding: 0.25rem; 47 | min-width: 1.5rem; 48 | min-height: 1.5rem; 49 | border-radius: 1rem; 50 | margin-right: 0.5rem; 51 | } 52 | 53 | .material-slider-arrow { 54 | all: unset; 55 | padding: 0.25rem; 56 | min-width: 1.5rem; 57 | min-height: 1.5rem; 58 | border-radius: 1rem; 59 | margin-left: 0.5rem; 60 | } 61 | 62 | .hover-surface { 63 | @include hover($surface); 64 | transition: 0.3s; 65 | } 66 | -------------------------------------------------------------------------------- /ignis/scss/mixins/hover.scss: -------------------------------------------------------------------------------- 1 | @mixin hover($bg) { 2 | background-color: $bg; 3 | 4 | &:hover { 5 | @if $darkmode == true { 6 | background-color: lighten($bg, 5%); 7 | } 8 | @else { 9 | background-color: darken($bg, 5%); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ignis/scss/mixins/window.scss: -------------------------------------------------------------------------------- 1 | @mixin window { 2 | background-color: $surface; 3 | border-radius: 1rem; 4 | padding: 1rem; 5 | color: $onSurface; 6 | box-shadow: 0 0 4px 2px $shadow; 7 | margin: 1rem; 8 | } -------------------------------------------------------------------------------- /ignis/scss/notification_center.scss: -------------------------------------------------------------------------------- 1 | .notification-center { 2 | background-color: $surfaceContainerLow; 3 | border-radius: 1.5rem 1.5rem 1rem 1rem; 4 | padding: 1rem; 5 | } 6 | 7 | .notification-center-header { 8 | margin-bottom: 1rem; 9 | } 10 | 11 | .notification { 12 | background-color: $surfaceContainerHigh; 13 | border-radius: 1rem; 14 | padding: 1rem; 15 | margin-top: 1rem; 16 | } 17 | 18 | .notification-count { 19 | background-color: $tertiary; 20 | color: $onTertiary; 21 | min-width: 2rem; 22 | font-size: 1.5rem; 23 | border-radius: 2rem; 24 | } 25 | 26 | .notification-header-label { 27 | font-size: 1.1rem; 28 | margin-left: 0.5rem; 29 | } 30 | 31 | .notification-close { 32 | @include hover($surfaceContainerHigh); 33 | color: $onSurface; 34 | border-radius: 1rem; 35 | padding: 0.25rem; 36 | margin-left: 0.5rem; 37 | transition: 0.3s; 38 | } 39 | 40 | .notification-clear-all { 41 | @include hover($surfaceContainerHigh); 42 | transition: 0.3s; 43 | border-radius: 1rem; 44 | padding: 0.25rem 1rem; 45 | } 46 | 47 | .notification-action { 48 | color: $primary; 49 | @include hover($surfaceContainerHighest); 50 | border-radius: 0.5rem; 51 | padding: 0.25rem 0.25rem; 52 | transition: 0.3s; 53 | } 54 | 55 | .notification-screenshot-label { 56 | margin-top: 0.5rem; 57 | font-size: 1.1rem; 58 | } 59 | 60 | .notification-center-info-label { 61 | color: $onSurfaceVariant; 62 | } 63 | 64 | .notification-body { 65 | color: $onSurfaceVariant; 66 | } -------------------------------------------------------------------------------- /ignis/scss/notification_popup.scss: -------------------------------------------------------------------------------- 1 | .notification-popup { 2 | @include window; 3 | min-width: 25rem; 4 | 5 | & .notification-close { 6 | @include hover($surface); 7 | } 8 | 9 | & .notification-action { 10 | @include hover($surfaceContainer); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ignis/scss/osd.scss: -------------------------------------------------------------------------------- 1 | .osd { 2 | @include window; 3 | min-width: 15rem; 4 | margin-bottom: 5rem; 5 | * { 6 | all: unset 7 | } 8 | } -------------------------------------------------------------------------------- /ignis/scss/powermenu.scss: -------------------------------------------------------------------------------- 1 | .powermenu { 2 | @include window; 3 | } 4 | 5 | .powermenu-button { 6 | @include hover($surfaceContainer); 7 | transition: 0.3s; 8 | border-radius: 10rem; 9 | min-width: 8rem; 10 | min-height: 8rem; 11 | 12 | &:focus { 13 | @include hover($primary); 14 | color: $surface; 15 | } 16 | } 17 | 18 | .powermenu-button-label { 19 | margin-top: 0.5rem; 20 | } 21 | 22 | .powermenu-button-box { 23 | margin: 0.5rem; 24 | } 25 | 26 | .powermenu-overlay { 27 | background-color: rgba(0, 0, 0, 0.5); 28 | } 29 | -------------------------------------------------------------------------------- /ignis/scss/settings.scss: -------------------------------------------------------------------------------- 1 | .settings-sidebar { 2 | background-color: $surfaceContainer; 3 | min-width: 300px; 4 | } 5 | 6 | .settings-sidebar-label { 7 | font-size: 2rem; 8 | margin-left: 1rem; 9 | margin-bottom: 0.5rem; 10 | margin-top: 2rem; 11 | } 12 | 13 | .settings-sidebar-entry { 14 | all: unset; 15 | background-color: $surfaceContainer; 16 | padding: 1rem 2rem; 17 | padding-left: 0.75rem; 18 | font-size: 1.2rem; 19 | 20 | &:selected { 21 | @if $darkmode == true { 22 | background-color: lighten($surfaceContainer, 10%); 23 | } 24 | @else { 25 | background-color: darken($surfaceContainer, 10%); 26 | } 27 | } 28 | } 29 | 30 | 31 | .settings-page-name { 32 | font-size: 2rem; 33 | margin-left: 2.5rem; 34 | margin-bottom: 2rem; 35 | margin-top: 2rem; 36 | } 37 | 38 | .settings-group-name { 39 | margin-left: 2.5rem; 40 | color: $primary; 41 | } 42 | 43 | .settings-row { 44 | all: unset; 45 | padding: 0.7rem 2.5rem; 46 | transition: 0.2s; 47 | 48 | &:active { 49 | @if $darkmode == true { 50 | background-color: rgba(255, 255, 255, 0.05); 51 | } 52 | @else { 53 | background-color: rgba(0, 0, 0, 0.05); 54 | } 55 | } 56 | } 57 | 58 | .settings-row-label { 59 | font-size: 1.2rem; 60 | } 61 | 62 | .settings-row-sublabel { 63 | font-size: 0.9rem; 64 | font-weight: normal; 65 | color: $onSurfaceVariant; 66 | } 67 | 68 | .settings-user-name { 69 | font-size: 2.3rem; 70 | margin-left: 1.5rem; 71 | } -------------------------------------------------------------------------------- /ignis/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/ignis/services/__init__.py -------------------------------------------------------------------------------- /ignis/services/material/__init__.py: -------------------------------------------------------------------------------- 1 | from .service import MaterialService 2 | 3 | __all__ = ["MaterialService"] 4 | -------------------------------------------------------------------------------- /ignis/services/material/constants.py: -------------------------------------------------------------------------------- 1 | import os 2 | import ignis 3 | from ignis.utils import Utils 4 | 5 | 6 | MATERIAL_CACHE_DIR = f"{ignis.CACHE_DIR}/material" # type: ignore 7 | 8 | TEMPLATES = Utils.get_current_dir() + "/templates" 9 | SAMPLE_WALL = Utils.get_current_dir() + "/sample_wall.png" 10 | 11 | os.makedirs(MATERIAL_CACHE_DIR, exist_ok=True) 12 | -------------------------------------------------------------------------------- /ignis/services/material/sample_wall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/dotfiles/c45dd17fd36b898f9743cc022de8e14151d4025f/ignis/services/material/sample_wall.png -------------------------------------------------------------------------------- /ignis/services/material/service.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import os 3 | import asyncio 4 | from gi.repository import GLib # type: ignore 5 | from jinja2 import Template 6 | from PIL import Image 7 | from materialyoucolor.quantize import QuantizeCelebi 8 | from materialyoucolor.hct import Hct 9 | from materialyoucolor.scheme.scheme_tonal_spot import SchemeTonalSpot 10 | from materialyoucolor.dynamiccolor.material_dynamic_colors import MaterialDynamicColors 11 | from materialyoucolor.score.score import Score 12 | 13 | from ignis.utils import Utils 14 | from ignis.app import IgnisApp 15 | from ignis.base_service import BaseService 16 | from ignis.options import options 17 | from user_options import user_options 18 | 19 | from .constants import MATERIAL_CACHE_DIR, TEMPLATES, SAMPLE_WALL 20 | from .util import rgba_to_hex, calculate_optimal_size 21 | 22 | app = IgnisApp.get_default() 23 | 24 | 25 | class MaterialService(BaseService): 26 | def __init__(self): 27 | super().__init__() 28 | 29 | if not options.wallpaper.wallpaper_path: 30 | self.__on_colors_not_found() 31 | if user_options.material.colors == {}: 32 | self.__on_colors_not_found() 33 | 34 | user_options.material.connect_option( 35 | "dark_mode", lambda: self.generate_colors(options.wallpaper.wallpaper_path) 36 | ) 37 | 38 | def __on_colors_not_found(self) -> None: 39 | options.wallpaper.set_wallpaper_path(SAMPLE_WALL) 40 | self.generate_colors(SAMPLE_WALL) 41 | asyncio.create_task(Utils.exec_sh_async("hyprctl reload")) 42 | 43 | def get_colors_from_img(self, path: str, dark_mode: bool) -> dict[str, str]: 44 | image = Image.open(path) 45 | wsize, hsize = image.size 46 | wsize_new, hsize_new = calculate_optimal_size(wsize, hsize, 128) 47 | if wsize_new < wsize or hsize_new < hsize: 48 | image = image.resize((wsize_new, hsize_new), Image.Resampling.BICUBIC) # type: ignore 49 | 50 | pixel_len = image.width * image.height 51 | image_data = image.getdata() 52 | pixel_array = [image_data[_] for _ in range(0, pixel_len, 1)] 53 | 54 | colors = QuantizeCelebi(pixel_array, 128) 55 | argb = Score.score(colors)[0] 56 | 57 | hct = Hct.from_int(argb) 58 | scheme = SchemeTonalSpot(hct, dark_mode, 0.0) 59 | 60 | material_colors = {} 61 | for color in vars(MaterialDynamicColors).keys(): 62 | color_name = getattr(MaterialDynamicColors, color) 63 | if hasattr(color_name, "get_hct"): 64 | rgba = color_name.get_hct(scheme).to_rgba() 65 | material_colors[color] = rgba_to_hex(rgba) 66 | 67 | return material_colors 68 | 69 | def generate_colors(self, path: str) -> None: 70 | colors = self.get_colors_from_img(path, user_options.material.dark_mode) 71 | dark_colors = self.get_colors_from_img(path, True) 72 | user_options.material.colors = colors 73 | self.__render_templates(colors, dark_colors) 74 | asyncio.create_task(self.__setup(path)) 75 | 76 | def __render_templates(self, colors: dict, dark_colors: dict) -> None: 77 | for template in os.listdir(TEMPLATES): 78 | self.render_template( 79 | colors=colors, 80 | dark_mode=user_options.material.dark_mode, 81 | input_file=f"{TEMPLATES}/{template}", 82 | output_file=f"{MATERIAL_CACHE_DIR}/{template}", 83 | ) 84 | 85 | for template in os.listdir(TEMPLATES): 86 | self.render_template( 87 | colors=dark_colors, 88 | dark_mode=True, 89 | input_file=f"{TEMPLATES}/{template}", 90 | output_file=f"{MATERIAL_CACHE_DIR}/dark_{template}", 91 | ) 92 | 93 | def render_template( 94 | self, 95 | colors: dict, 96 | input_file: str, 97 | output_file: str, 98 | dark_mode: bool | None = None, 99 | ) -> None: 100 | if dark_mode is None: 101 | colors["dark_mode"] = str(user_options.material.dark_mode).lower() 102 | else: 103 | colors["dark_mode"] = str(dark_mode).lower() 104 | with open(input_file) as file: 105 | template_rendered = Template(file.read()).render(colors) 106 | 107 | with open(output_file, "w") as file: 108 | file.write(template_rendered) 109 | 110 | async def __reload_gtk_theme(self) -> None: 111 | THEME_CMD = "gsettings set org.gnome.desktop.interface gtk-theme {}" 112 | COLOR_SCHEME_CMD = "gsettings set org.gnome.desktop.interface color-scheme {}" 113 | await Utils.exec_sh_async(THEME_CMD.format("Adwaita")) 114 | await Utils.exec_sh_async(THEME_CMD.format("Material")) 115 | await Utils.exec_sh_async(COLOR_SCHEME_CMD.format("default")) 116 | await Utils.exec_sh_async(COLOR_SCHEME_CMD.format("prefer-dark")) 117 | await Utils.exec_sh_async(COLOR_SCHEME_CMD.format("default")) 118 | 119 | async def __setup(self, image_path: str) -> None: 120 | try: 121 | await Utils.exec_sh_async("pkill -SIGUSR1 kitty") 122 | except GLib.Error: 123 | ... 124 | options.wallpaper.set_wallpaper_path(image_path) 125 | app.reload_css() 126 | await self.__reload_gtk_theme() 127 | -------------------------------------------------------------------------------- /ignis/services/material/templates/colors-hyprland.conf: -------------------------------------------------------------------------------- 1 | $primary = rgb({{ primary | replace("#", "") }}) 2 | $surface = rgb({{ surface | replace("#", "") }}) 3 | $onSurface = rgb({{ onSurface | replace("#", "") }}) 4 | 5 | $primaryHex = #{{ primary }} 6 | $surfaceHex = #{{ surface }} 7 | $onSurfaceHex = #{{ onSurface }} -------------------------------------------------------------------------------- /ignis/services/material/templates/colors-kitty.conf: -------------------------------------------------------------------------------- 1 | foreground {{ onBackground }} 2 | background {{ background }} 3 | cursor {{ primaryFixed }} 4 | 5 | # Base 8 Colors (0-7) 6 | color0 {{ surfaceContainerLowest }} 7 | color1 {{ error }} 8 | color2 {{ primary }} 9 | color3 {{ tertiary }} 10 | color4 {{ primaryContainer }} 11 | color5 {{ secondaryFixedDim }} 12 | color6 {{ tertiaryFixed }} 13 | color7 {{ onSurface }} 14 | 15 | # Bright (Bold) Variants (8–15) 16 | color8 {{ outlineVariant }} 17 | color9 {{ errorContainer }} 18 | color10 {{ primaryContainer }} 19 | color11 {{ tertiaryContainer }} 20 | color12 {{ secondaryContainer }} 21 | color13 {{ onSecondaryFixedVariant }} 22 | color14 {{ onTertiaryFixedVariant }} 23 | color15 {{ onSurfaceVariant }} 24 | -------------------------------------------------------------------------------- /ignis/services/material/templates/colors.scss: -------------------------------------------------------------------------------- 1 | $primary_paletteKeyColor: {{ primary_paletteKeyColor }}; 2 | $secondary_paletteKeyColor: {{ secondary_paletteKeyColor }}; 3 | $tertiary_paletteKeyColor: {{ tertiary_paletteKeyColor }}; 4 | $neutral_paletteKeyColor: {{ neutral_paletteKeyColor }}; 5 | $neutral_variant_paletteKeyColor: {{ neutral_variant_paletteKeyColor }}; 6 | $background: {{ background }}; 7 | $onBackground: {{ onBackground }}; 8 | $surface: {{ surface }}; 9 | $surfaceDim: {{ surfaceDim }}; 10 | $surfaceBright: {{ surfaceBright }}; 11 | $surfaceContainerLowest: {{ surfaceContainerLowest }}; 12 | $surfaceContainerLow: {{ surfaceContainerLow }}; 13 | $surfaceContainer: {{ surfaceContainer }}; 14 | $surfaceContainerHigh: {{ surfaceContainerHigh }}; 15 | $surfaceContainerHighest: {{ surfaceContainerHighest }}; 16 | $onSurface: {{ onSurface }}; 17 | $surfaceVariant: {{ surfaceVariant }}; 18 | $onSurfaceVariant: {{ onSurfaceVariant }}; 19 | $inverseSurface: {{ inverseSurface }}; 20 | $inverseOnSurface: {{ inverseOnSurface }}; 21 | $outline: {{ outline }}; 22 | $outlineVariant: {{ outlineVariant }}; 23 | $shadow: {{ shadow }}; 24 | $scrim: {{ scrim }}; 25 | $surfaceTint: {{ surfaceTint }}; 26 | $primary: {{ primary }}; 27 | $onPrimary: {{ onPrimary }}; 28 | $primaryContainer: {{ primaryContainer }}; 29 | $onPrimaryContainer: {{ onPrimaryContainer }}; 30 | $inversePrimary: {{ inversePrimary }}; 31 | $secondary: {{ secondary }}; 32 | $onSecondary: {{ onSecondary }}; 33 | $secondaryContainer: {{ secondaryContainer }}; 34 | $onSecondaryContainer: {{ onSecondaryContainer }}; 35 | $tertiary: {{ tertiary }}; 36 | $onTertiary: {{ onTertiary }}; 37 | $tertiaryContainer: {{ tertiaryContainer }}; 38 | $onTertiaryContainer: {{ onTertiaryContainer }}; 39 | $error: {{ error }}; 40 | $onError: {{ onError }}; 41 | $errorContainer: {{ errorContainer }}; 42 | $onErrorContainer: {{ onErrorContainer }}; 43 | $primaryFixed: {{ primaryFixed }}; 44 | $primaryFixedDim: {{ primaryFixedDim }}; 45 | $onPrimaryFixed: {{ onPrimaryFixed }}; 46 | $onPrimaryFixedVariant: {{ onPrimaryFixedVariant }}; 47 | $secondaryFixed: {{ secondaryFixed }}; 48 | $secondaryFixedDim: {{ secondaryFixedDim }}; 49 | $onSecondaryFixed: {{ onSecondaryFixed }}; 50 | $onSecondaryFixedVariant: {{ onSecondaryFixedVariant }}; 51 | $tertiaryFixed: {{ tertiaryFixed }}; 52 | $tertiaryFixedDim: {{ tertiaryFixedDim }}; 53 | $onTertiaryFixed: {{ onTertiaryFixed }}; 54 | $onTertiaryFixedVariant: {{ onTertiaryFixedVariant }}; 55 | $darkmode: {{ dark_mode }}; -------------------------------------------------------------------------------- /ignis/services/material/templates/gtk.css: -------------------------------------------------------------------------------- 1 | @define-color accent_color {{ primary }}; 2 | @define-color accent_bg_color {{ primary }}; 3 | @define-color accent_fg_color {{ surface }}; 4 | @define-color destructive_color {{ errorContainer }}; 5 | @define-color destructive_bg_color {{ errorContainer }}; 6 | @define-color destructive_fg_color {{ onSurface }}; 7 | @define-color success_color {{ tertiary }}; 8 | @define-color success_bg_color {{ tertiary }}; 9 | @define-color success_fg_color {{ onSurface }}; 10 | @define-color warning_color {{ secondary }}; 11 | @define-color warning_bg_color {{ secondary }}; 12 | @define-color warning_fg_color {{ onSurface }}; 13 | @define-color error_color {{ errorContainer }}; 14 | @define-color error_bg_color {{ errorContainer }}; 15 | @define-color error_fg_color {{ onSurface }}; 16 | @define-color window_bg_color {{ surface }}; 17 | @define-color window_fg_color {{ onSurface }}; 18 | @define-color view_bg_color {{ surface }}; 19 | @define-color view_fg_color {{ onSurface }}; 20 | @define-color headerbar_bg_color {{ surface }}; 21 | @define-color headerbar_fg_color {{ onSurface }}; 22 | @define-color headerbar_border_color {{ outline }}; 23 | @define-color headerbar_backdrop_color {{ surface }}; 24 | @define-color headerbar_shade_color {{ outline }}; 25 | @define-color sidebar_bg_color {{ surfaceContainer }}; 26 | @define-color sidebar_fg_color {{ onSurface }}; 27 | @define-color sidebar_backdrop_color {{ surfaceContainer }}; 28 | @define-color card_bg_color {{ surfaceContainer }}; 29 | @define-color card_fg_color {{ onSurface }}; 30 | @define-color card_shade_color rgba(0, 0, 0, 0.07); 31 | @define-color thumbnail_bg_color {{ surface }}; 32 | @define-color thumbnail_fg_color {{ onSurface }}; 33 | @define-color dialog_bg_color {{ surface }}; 34 | @define-color dialog_fg_color {{ onSurface }}; 35 | @define-color popover_bg_color {{ surfaceContainer }}; 36 | @define-color popover_fg_color {{ onSurface }}; 37 | @define-color shade_color rgba(0, 0, 0, 0.36); 38 | @define-color scrollbar_outline_color {{ outline }}; 39 | 40 | .navigation-sidebar { 41 | background-color: {{ surfaceContainer }}; 42 | } 43 | 44 | switch:checked { 45 | background-color: {{ primary }}; 46 | } 47 | 48 | switch:checked slider { 49 | background-color: {{ onPrimary }}; 50 | } -------------------------------------------------------------------------------- /ignis/services/material/templates/swaylock: -------------------------------------------------------------------------------- 1 | # ---------------------SETTINGS---------------------------# 2 | daemonize 3 | show-failed-attempts 4 | clock 5 | screenshot 6 | ignore-empty-password 7 | indicator 8 | font=JetBrainsMono 9 | indicator-radius=200 10 | indicator-thickness=20 11 | 12 | 13 | #----------------------EFFECTS----------------------------# 14 | effect-blur=9x5 15 | effect-vignette=0.5:0.5 16 | 17 | #--------------------INPUT IGNORE-------------------------# 18 | grace-no-mouse 19 | grace-no-touch 20 | 21 | 22 | #--------------------DATE & TIME--------------------------# 23 | datestr=%a,%e %B 24 | timestr=%H:%M 25 | fade-in=0.2 26 | 27 | 28 | #----------------------COLORS-----------------------------# 29 | bs-hl-color={{ error }} 30 | inside-color=#FFFFFF00 31 | ring-color=#FFFFFF00 32 | line-color=#FFFFFF00 33 | separator-color=#FFFFFF00 34 | key-hl-color={{ primaryFixedDim }} 35 | text-color={{ primaryFixedDim }} 36 | text-caps-lock-color={{ tertiary }} 37 | 38 | ring-clear-color=#FFFFFF00 39 | text-clear-color={{ onTertiary }} 40 | inside-clear-color={{ tertiary }} 41 | line-clear-color=#FFFFFF00 42 | 43 | ring-ver-color=#FFFFFF00 44 | text-ver-color={{ onSecondary }} 45 | inside-ver-color={{ secondary }} 46 | line-ver-color=#FFFFFF00 47 | 48 | ring-wrong-color=#FFFFFF00 49 | text-wrong-color={{ onError }} 50 | inside-wrong-color={{ error }} 51 | line-wrong-color=#FFFFFF00 -------------------------------------------------------------------------------- /ignis/services/material/util.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | 4 | def rgba_to_hex(rgba: list) -> str: 5 | return "#{:02x}{:02x}{:02x}".format(*rgba) 6 | 7 | 8 | def calculate_optimal_size(width: int, height: int, bitmap_size: int) -> tuple: 9 | image_area = width * height 10 | bitmap_area = bitmap_size**2 11 | scale = math.sqrt(bitmap_area / image_area) if image_area > bitmap_area else 1 12 | new_width = round(width * scale) 13 | new_height = round(height * scale) 14 | if new_width == 0: 15 | new_width = 1 16 | if new_height == 0: 17 | new_height = 1 18 | return new_width, new_height 19 | -------------------------------------------------------------------------------- /ignis/style.scss: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: JetBrainsMono; 3 | font-weight: bold; 4 | } 5 | @import "../../.cache/ignis/material/colors.scss"; 6 | 7 | @import "scss/mixins/window.scss"; 8 | @import "scss/mixins/hover.scss"; 9 | 10 | @import "scss/lib.scss"; 11 | 12 | 13 | @import "./scss/bar.scss"; 14 | @import "./scss/control_center.scss"; 15 | @import "./scss/osd.scss"; 16 | @import "./scss/notification_popup.scss"; 17 | @import "./scss/launcher.scss"; 18 | @import "./scss/settings.scss"; 19 | @import "./scss/powermenu.scss"; 20 | @import "./scss/notification_center.scss"; -------------------------------------------------------------------------------- /ignis/user_options.py: -------------------------------------------------------------------------------- 1 | import os 2 | from ignis.options_manager import OptionsGroup, OptionsManager 3 | from ignis import DATA_DIR, CACHE_DIR # type: ignore 4 | 5 | USER_OPTIONS_FILE = f"{DATA_DIR}/user_options.json" 6 | OLD_USER_OPTIONS_FILE = f"{CACHE_DIR}/user_options.json" 7 | 8 | 9 | # FIXME: remove someday 10 | def _migrate_old_options_file() -> None: 11 | with open(OLD_USER_OPTIONS_FILE) as f: 12 | data = f.read() 13 | 14 | with open(USER_OPTIONS_FILE, "w") as f: 15 | f.write(data) 16 | 17 | 18 | class UserOptions(OptionsManager): 19 | def __init__(self): 20 | if not os.path.exists(USER_OPTIONS_FILE) and os.path.exists( 21 | OLD_USER_OPTIONS_FILE 22 | ): 23 | _migrate_old_options_file() 24 | 25 | try: 26 | super().__init__(file=USER_OPTIONS_FILE) 27 | except FileNotFoundError: 28 | pass 29 | 30 | class User(OptionsGroup): 31 | avatar: str = f"/var/lib/AccountsService/icons/{os.getenv('USER')}" 32 | 33 | class Settings(OptionsGroup): 34 | last_page: int = 0 35 | 36 | class Material(OptionsGroup): 37 | dark_mode: bool = True 38 | colors: dict[str, str] = {} 39 | 40 | user = User() 41 | settings = Settings() 42 | material = Material() 43 | 44 | 45 | user_options = UserOptions() 46 | -------------------------------------------------------------------------------- /installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## Clone repository 4 | 5 | ``` 6 | git clone https://github.com/linkfrg/dotfiles.git --depth 1 --branch main 7 | ``` 8 | 9 | ## Copy config files 10 | 11 | ``` 12 | cd dotfiles 13 | mkdir -p ~/.local/share/themes 14 | cp -R .config/* ~/.config/ 15 | cp -R ignis ~/.config/ 16 | cp -R Material ~/.local/share/themes 17 | ``` 18 | 19 | ## Install dependencies 20 | 21 | Firstly, you need to install AUR helper (e.g., paru). 22 | 23 | ``` 24 | paru -S --needed - < dependencies.txt 25 | ``` 26 | 27 | If using nvidia install also 28 | ``` 29 | paru -S --needed - < nvidia_deps.txt 30 | ``` 31 | 32 | ## Install tide prompt 33 | 34 | Install fisher. 35 | 36 | ```bash 37 | paru -S fisher 38 | ``` 39 | 40 | Install tide. 41 | 42 | ```bash 43 | fisher install IlanCosman/tide@v6 44 | ``` 45 | 46 | Configure tide prompt. 47 | 48 | ```bash 49 | tide configure --auto --style=Rainbow --prompt_colors='16 colors' --show_time=No --rainbow_prompt_separators=Angled --powerline_prompt_heads=Sharp --powerline_prompt_tails=Round --powerline_prompt_style='Two lines, frame' --prompt_connection=Disconnected --powerline_right_prompt_frame=Yes --prompt_spacing=Sparse --icons='Many icons' --transient=No 50 | ``` -------------------------------------------------------------------------------- /keybindings.md: -------------------------------------------------------------------------------- 1 | # Keybindings 2 | This a cheat sheet with some keybindings 3 | 4 | ## Ignis 5 | | Bind | Action | 6 | | ---- | ------ | 7 | | `Super` + `X` | Toggle Launcher | 8 | | `Super` + `M` | Toggle Powermenu | 9 | | `Alt` + `F4` | Toggle Powermenu | 10 | 11 | ## Software 12 | | Bind | Action | 13 | | ---- | ------ | 14 | | `Super` + `Q` | Open Kitty | 15 | | `Super` + `L` | Lock screen | 16 | | `Super` + `E` | Open Thunar | 17 | | `Super` + `Shift` + `S` | Make screenshot from area | 18 | | `Super` + `S` | Make screenshot from area | 19 | | `Print screen` | Make fullscreen screenshot | 20 | 21 | 22 | ## Windows 23 | | Bind | Action | 24 | | ---- | ------ | 25 | | `Super` + `C` | Close window | 26 | | `Super` + `Shift` + `M` | Force quit Hyprland | 27 | | `F11` | Make window fullscreen | 28 | | `Super` + `G` | Center window | 29 | | `Super` + `D` | Pin window | -------------------------------------------------------------------------------- /nvidia_deps.txt: -------------------------------------------------------------------------------- 1 | libva-nvidia-driver -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.mypy] 2 | python_version = "3.10" 3 | packages = ["ignis"] 4 | exclude = ["venv"] 5 | disable_error_code = [ 6 | "no-redef", # allow variable redefinition (needed for GObject.Property decorator) 7 | "method-assign", # also needed for GObject.Property 8 | "import-not-found", 9 | "import-untyped" 10 | ] 11 | mypy_path = ["stubs"] 12 | check_untyped_defs = true 13 | 14 | [[tool.mypy.overrides]] 15 | module = ["gi.repository.*"] 16 | disable_error_code = ["assignment"] 17 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | materialyoucolor>=2.0.9 2 | Jinja2>=3.1.4 3 | pillow>=10.4.0 -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | [lint] 2 | select = [ 3 | "F", # pyflakes 4 | "E", # pycodestyle errors 5 | "W", # pycodestyle warnings 6 | "I", # isort 7 | "UP", # pyupgrade 8 | "B", # flake8-bugbear 9 | "C4", # flake8-comprehensions 10 | ] 11 | ignore = [ 12 | "E501", # line too long, handled by black 13 | "B008", # do not perform function calls in argument defaults 14 | "C901", # too complex 15 | "W191", # indentation contains tabs 16 | "I001", # import block is un-sorted or un-formatted 17 | "B006" 18 | ] 19 | 20 | fixable = ["ALL"] 21 | unfixable = [] --------------------------------------------------------------------------------