├── .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 = []
--------------------------------------------------------------------------------