├── .gitignore ├── LICENSE ├── README.md ├── app.ts ├── assets ├── default_wallpaper └── icons │ └── hicolor │ └── scalable │ └── devices │ ├── arch-symbolic.svg │ ├── nix-symbolic.svg │ └── topbar-show-symbolic.svg ├── end4_scripts ├── applycolor.sh ├── colorgen.sh ├── generate_colors_material.py └── templates │ ├── fuzzel │ └── fuzzel.ini │ ├── gradience │ └── preset.json │ ├── hypr │ ├── hyprland_colors.conf │ └── hyprlock_colors.conf │ ├── scheme-base.json │ ├── terminal │ └── sequences.txt │ └── wlogout │ └── wlogout.css ├── flake.lock ├── flake.nix ├── install.sh ├── matugen └── templates │ ├── ags.scss │ ├── gtk.css │ ├── hyprland_colors.conf │ └── hyprlock_colors.conf ├── nix └── hm-module.nix ├── options.ts ├── style ├── abstracts │ ├── _index.scss │ ├── _material-colors_end4.scss │ ├── _mixins.scss │ ├── _variables.scss │ └── _variables_end4.scss ├── base │ ├── _index.scss │ └── _reset.scss ├── components │ ├── _bar.scss │ ├── _control-center.scss │ ├── _index.scss │ ├── _launcher.scss │ ├── _logout-menu.scss │ ├── _music.scss │ ├── _notifications.scss │ ├── _osd.scss │ └── _system-menu.scss ├── layouts │ ├── _general.scss │ └── _index.scss ├── main.css └── main.scss ├── tsconfig.json ├── utils ├── battery.ts ├── bluetooth-agent.ts ├── bluetooth.ts ├── brightness.ts ├── hwmonitor.ts ├── hyprland.ts ├── mpris.ts ├── notifd.ts ├── option.ts ├── osd.ts └── wifi.ts └── widgets ├── bar ├── main.tsx └── modules │ ├── Cpu.tsx │ ├── Media.tsx │ ├── Mem.tsx │ ├── OsIcon.tsx │ ├── Separator.tsx │ ├── SysTray.tsx │ ├── SystemInfo │ ├── main.tsx │ └── modules │ │ ├── Battery.tsx │ │ ├── Bluetooth.tsx │ │ └── Net.tsx │ ├── Time.tsx │ └── Workspaces.tsx ├── common └── circularprogress.tsx ├── control-panel ├── main.tsx └── modules │ ├── AstalComboBoxText.tsx │ ├── CategoryButton.tsx │ ├── OptionSelect.tsx │ ├── OptionToggle.tsx │ └── Section.tsx ├── launcher └── main.tsx ├── logout-menu └── main.tsx ├── music ├── main.tsx └── modules │ ├── Controls.tsx │ ├── Cover.tsx │ ├── Info.tsx │ ├── PlayerInfo.tsx │ ├── TimeInfo.tsx │ ├── TitleArtists.tsx │ └── cava │ ├── core │ ├── CavaStyle.ts │ └── CavaWidget.ts │ ├── index.ts │ ├── utils │ ├── animation.ts │ ├── index.ts │ ├── rendering.ts │ ├── types.ts │ └── visualization.ts │ └── visualizers │ ├── bars.ts │ ├── catmull-rom.ts │ ├── circular.ts │ ├── dots.ts │ ├── index.ts │ ├── jumping-bars.ts │ ├── mesh.ts │ ├── particles.ts │ ├── smooth.ts │ ├── waterfall.ts │ └── wave-particles.ts ├── notifications ├── main.tsx └── modules │ ├── Icon.tsx │ └── Notification.tsx ├── osd ├── main.tsx └── modules │ └── Progress.tsx └── system-menu ├── main.tsx └── modules ├── BatteryBox.tsx ├── PowerProfileBox.tsx ├── Sliders.tsx ├── Toggles.tsx ├── bluetooth-box ├── main.tsx └── modules │ ├── BluetoothDevices.tsx │ └── BluetoothItem.tsx └── wifi-box ├── main.tsx └── modules ├── NetworkItem.tsx └── PasswordDialog.tsx /.gitignore: -------------------------------------------------------------------------------- 1 | style.css.map 2 | types 3 | node_modules/ 4 | @girs/ 5 | env.d.ts 6 | package.json 7 | config.json 8 | -------------------------------------------------------------------------------- /app.ts: -------------------------------------------------------------------------------- 1 | import { App } from "astal/gtk4"; 2 | import { exec, monitorFile, GLib } from "astal"; 3 | import Hyprland from "gi://AstalHyprland"; 4 | import { hyprToGdk } from "utils/hyprland.ts"; 5 | import Bar from "./widgets/bar/main.tsx"; 6 | import SystemMenu from "./widgets/system-menu/main.tsx"; 7 | import OnScreenDisplay from "./widgets/osd/main.tsx"; 8 | import Notifications from "./widgets/notifications/main.tsx"; 9 | import LogoutMenu from "widgets/logout-menu/main.tsx"; 10 | import Applauncher from "./widgets/launcher/main.tsx"; 11 | import MusicPlayer from "./widgets/music/main.tsx"; 12 | import ControlPanel from "./widgets/control-panel/main.tsx"; 13 | 14 | const scss = `${GLib.get_user_config_dir()}/ags/style/main.scss`; 15 | const css = `${GLib.get_user_config_dir()}/ags/style/main.css`; 16 | const icons = `${GLib.get_user_config_dir()}/ags/assets/icons`; 17 | const styleDirectories = ["abstracts", "components", "layouts", "base"]; 18 | 19 | function reloadCss() { 20 | console.log("scss change detected"); 21 | exec(`sass ${scss} ${css}`); 22 | App.apply_css(css); 23 | } 24 | 25 | App.start({ 26 | icons: icons, 27 | css: css, 28 | instanceName: "js", 29 | requestHandler(request, res) { 30 | print(request); 31 | res("ok"); 32 | }, 33 | main() { 34 | exec(`sass ${scss} ${css}`); 35 | styleDirectories.forEach((dir) => 36 | monitorFile(`${GLib.get_user_config_dir()}/ags/style/${dir}`, reloadCss), 37 | ); 38 | 39 | const barNames = new Map(); // Map Hyprland ID to window name 40 | 41 | Notifications(); 42 | OnScreenDisplay(); 43 | SystemMenu(); 44 | MusicPlayer(); 45 | Applauncher(); 46 | LogoutMenu(); 47 | ControlPanel(); 48 | 49 | const hypr = Hyprland.get_default(); 50 | 51 | // Initialize 52 | for (const hyprMonitor of hypr.monitors) { 53 | const gdkmonitor = hyprToGdk(hyprMonitor); 54 | if (gdkmonitor) { 55 | const windowName = Bar(gdkmonitor); 56 | barNames.set(hyprMonitor.id, windowName); 57 | } 58 | } 59 | 60 | hypr.connect("monitor-added", (_, monitor) => { 61 | const gdkmonitor = hyprToGdk(monitor); 62 | if (gdkmonitor) { 63 | const windowName = Bar(gdkmonitor); 64 | barNames.set(monitor.id, windowName); 65 | console.log(`Monitor added - ID: ${monitor.id}`); 66 | } 67 | }); 68 | 69 | hypr.connect("monitor-removed", (_, id) => { 70 | console.log(`Monitor removed - ID: ${id}`); 71 | const windowName = barNames.get(id); 72 | if (windowName) { 73 | const window = App.get_window(windowName); 74 | if (window) { 75 | console.log(`Removing bar: ${windowName}`); 76 | App.toggle_window(windowName); 77 | window.set_child(null); 78 | App.remove_window(window); 79 | } 80 | barNames.delete(id); 81 | } 82 | }); 83 | }, 84 | }); 85 | -------------------------------------------------------------------------------- /assets/default_wallpaper: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neurarian/matshell/e94a700bf1401340bb4f6782ee40b221f2c9c0a3/assets/default_wallpaper -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/devices/arch-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 16 | 34 | 37 | 41 | 45 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/devices/nix-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/devices/topbar-show-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /end4_scripts/colorgen.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}" 4 | XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}" 5 | XDG_STATE_HOME="${XDG_STATE_HOME:-$HOME/.local/state}" 6 | CONFIG_DIR="$XDG_CONFIG_HOME/ags" 7 | CACHE_DIR="$XDG_CACHE_HOME/ags" 8 | STATE_DIR="$XDG_STATE_HOME/ags" 9 | 10 | # check if no arguments 11 | if [ $# -eq 0 ]; then 12 | echo "Usage: colorgen.sh /path/to/image (--apply)" 13 | exit 1 14 | fi 15 | 16 | # check if the file $STATE_DIR/user/colormode.txt exists. if not, create it. else, read it to $lightdark 17 | colormodefile="$STATE_DIR/user/colormode.txt" 18 | lightdark="dark" 19 | transparency="opaque" 20 | materialscheme="vibrant" 21 | terminalscheme="$CONFIG_DIR/scripts/templates/scheme-base.json" 22 | 23 | if [ ! -f $colormodefile ]; then 24 | echo "dark" > $colormodefile 25 | echo "opaque" >> $colormodefile 26 | echo "vibrant" >> $colormodefile 27 | elif [[ $(wc -l < $colormodefile) -ne 3 || $(wc -w < $colormodefile) -ne 3 ]]; then 28 | echo "dark" > $colormodefile 29 | echo "opaque" >> $colormodefile 30 | echo "vibrant" >> $colormodefile 31 | else 32 | lightdark=$(sed -n '1p' $colormodefile) 33 | transparency=$(sed -n '2p' $colormodefile) 34 | materialscheme=$(sed -n '3p' $colormodefile) 35 | if [ "$materialscheme" = "monochrome" ]; then 36 | terminalscheme="$XDG_CONFIG_HOME/ags/scripts/templates/terminal/scheme-monochrome.json" 37 | fi 38 | fi 39 | backend="material" # color generator backend 40 | if [ ! -f "$STATE_DIR/user/colorbackend.txt" ]; then 41 | echo "material" > "$STATE_DIR/user/colorbackend.txt" 42 | else 43 | backend=$(cat "$STATE_DIR/user/colorbackend.txt") # either "" or "-l" 44 | fi 45 | 46 | cd "$CONFIG_DIR/scripts/" || exit 47 | if [[ "$1" = "#"* ]]; then # this is a color 48 | ./generate_colors_material.py --color "$1" \ 49 | --mode "$lightdark" --scheme "$materialscheme" --transparency "$transparency" \ 50 | --termscheme $terminalscheme --blend_bg_fg \ 51 | > "$CACHE_DIR"/user/generated/material_colors.scss 52 | if [ "$2" = "--apply" ]; then 53 | cp "$CACHE_DIR"/user/generated/material_colors.scss "$STATE_DIR/scss/_material.scss" 54 | applycolor.sh 55 | fi 56 | elif [ "$backend" = "material" ]; then 57 | smartflag='' 58 | if [ "$3" = "--smart" ]; then 59 | smartflag='--smart' 60 | fi 61 | ./generate_colors_material.py --path "$1" \ 62 | --mode "$lightdark" --scheme "$materialscheme" --transparency "$transparency" \ 63 | --termscheme $terminalscheme --blend_bg_fg \ 64 | --cache "$STATE_DIR/user/color.txt" $smartflag \ 65 | > "$CACHE_DIR"/user/generated/material_colors.scss 66 | if [ "$2" = "--apply" ]; then 67 | cp "$CACHE_DIR"/user/generated/material_colors.scss "$STATE_DIR/scss/_material.scss" 68 | ./applycolor.sh 69 | fi 70 | elif [ "$backend" = "pywal" ]; then 71 | # clear and generate 72 | wal -c 73 | wal -i "$1" -n $lightdark -q 74 | # copy scss 75 | cp "$XDG_CACHE_HOME/wal/colors.scss" "$CACHE_DIR"/user/generated/material_colors.scss 76 | 77 | cat color_generation/pywal_to_material.scss >> "$CACHE_DIR"/user/generated/material_colors.scss 78 | if [ "$2" = "--apply" ]; then 79 | sass -I "$STATE_DIR/scss" -I "$CONFIG_DIR/scss/fallback" "$CACHE_DIR"/user/generated/material_colors.scss "$CACHE_DIR"/user/generated/colors_classes.scss --style compressed 80 | sed -i "s/ { color//g" "$CACHE_DIR"/user/generated/colors_classes.scss 81 | sed -i "s/\./$/g" "$CACHE_DIR"/user/generated/colors_classes.scss 82 | sed -i "s/}//g" "$CACHE_DIR"/user/generated/colors_classes.scss 83 | if [ "$lightdark" = "-l" ]; then 84 | printf "\n""\$darkmode: false;""\n" >> "$CACHE_DIR"/user/generated/colors_classes.scss 85 | else 86 | printf "\n""\$darkmode: true;""\n" >> "$CACHE_DIR"/user/generated/colors_classes.scss 87 | fi 88 | 89 | cp "$CACHE_DIR"/user/generated/colors_classes.scss "$STATE_DIR/scss/_material.scss" 90 | 91 | applycolor.sh 92 | fi 93 | fi 94 | -------------------------------------------------------------------------------- /end4_scripts/templates/fuzzel/fuzzel.ini: -------------------------------------------------------------------------------- 1 | font=JetBrains Mono 2 | terminal=kitty -e 3 | prompt=">> " 4 | layer=overlay 5 | 6 | [colors] 7 | background={{ $background }}ff 8 | text={{ $onBackground }}ff 9 | selection={{ $surfaceVariant }}ff 10 | selection-text={{ $onSurfaceVariant }}ff 11 | border={{ $outline }}dd 12 | match={{ $primary }}ff 13 | selection-match={{ $primary }}ff 14 | 15 | 16 | [border] 17 | radius=17 18 | width=2 19 | 20 | [dmenu] 21 | exit-immediately-if-empty=yes 22 | -------------------------------------------------------------------------------- /end4_scripts/templates/gradience/preset.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Material3_Generated", 3 | "variables": { 4 | "theme_fg_color": "#AEE5FA", 5 | "theme_text_color": "#AEE5FA", 6 | "theme_bg_color": "#1a1b26", 7 | "theme_base_color": "#1a1b26", 8 | "theme_selected_bg_color": "#AEE5FA", 9 | "theme_selected_fg_color": "rgba(0, 0, 0, 0.87)", 10 | "insensitive_bg_color": "#1a1b26", 11 | "insensitive_fg_color": "rgba(192, 202, 245, 0.5)", 12 | "insensitive_base_color": "#24283b", 13 | "theme_unfocused_fg_color": "#AEE5FA", 14 | "theme_unfocused_text_color": "#c0caf5", 15 | "theme_unfocused_bg_color": "#1a1b26", 16 | "theme_unfocused_base_color": "#1a1b26", 17 | "theme_unfocused_selected_bg_color": "#a9b1d6", 18 | "theme_unfocused_selected_fg_color": "rgba(0, 0, 0, 0.87)", 19 | "unfocused_insensitive_color": "rgba(192, 202, 245, 0.5)", 20 | "borders": "rgba(192, 202, 245, 0.12)", 21 | "unfocused_borders": "rgba(192, 202, 245, 0.12)", 22 | "warning_color": "#FDD633", 23 | "error_color": "#BA1B1B", 24 | "success_color": "#81C995", 25 | "wm_title": "#AEE5FA", 26 | "wm_unfocused_title": "rgba(192, 202, 245, 0.7)", 27 | "wm_highlight": "rgba(192, 202, 245, 0.1)", 28 | "wm_bg": "#1a1b26", 29 | "wm_unfocused_bg": "#1a1b26", 30 | "wm_button_close_icon": "#1a1b26", 31 | "wm_button_close_hover_bg": "#a9b1d6", 32 | "wm_button_close_active_bg": "#c7c7c7", 33 | "content_view_bg": "#1a1b26", 34 | "placeholder_text_color": "silver", 35 | "text_view_bg": "#1d1d1d", 36 | "budgie_tasklist_indicator_color": "#90D1F6", 37 | "budgie_tasklist_indicator_color_active": "#90D1F6", 38 | "budgie_tasklist_indicator_color_active_window": "#999999", 39 | "budgie_tasklist_indicator_color_attention": "#FDD633", 40 | "STRAWBERRY_100": "#FF9262", 41 | "STRAWBERRY_300": "#FF793E", 42 | "STRAWBERRY_500": "#F15D22", 43 | "STRAWBERRY_700": "#CF3B00", 44 | "STRAWBERRY_900": "#AC1800", 45 | "ORANGE_100": "#FFDB91", 46 | "ORANGE_300": "#FFCA40", 47 | "ORANGE_500": "#FAA41A", 48 | "ORANGE_700": "#DE8800", 49 | "ORANGE_900": "#C26C00", 50 | "BANANA_100": "#FFFFA8", 51 | "BANANA_300": "#FFFA7D", 52 | "BANANA_500": "#FFCE51", 53 | "BANANA_700": "#D1A023", 54 | "BANANA_900": "#A27100", 55 | "LIME_100": "#A2F3BE", 56 | "LIME_300": "#8ADBA6", 57 | "LIME_500": "#73C48F", 58 | "LIME_700": "#479863", 59 | "LIME_900": "#1C6D38", 60 | "BLUEBERRY_100": "#94A6FF", 61 | "BLUEBERRY_300": "#6A7CE0", 62 | "BLUEBERRY_500": "#3F51B5", 63 | "BLUEBERRY_700": "#213397", 64 | "BLUEBERRY_900": "#031579", 65 | "GRAPE_100": "#D25DE6", 66 | "GRAPE_300": "#B84ACB", 67 | "GRAPE_500": "#9C27B0", 68 | "GRAPE_700": "#830E97", 69 | "GRAPE_900": "#6A007E", 70 | "COCOA_100": "#9F9792", 71 | "COCOA_300": "#7B736E", 72 | "COCOA_500": "#574F4A", 73 | "COCOA_700": "#463E39", 74 | "COCOA_900": "#342C27", 75 | "SILVER_100": "#EEE", 76 | "SILVER_300": "#CCC", 77 | "SILVER_500": "#AAA", 78 | "SILVER_700": "#888", 79 | "SILVER_900": "#666", 80 | "SLATE_100": "#888", 81 | "SLATE_300": "#666", 82 | "SLATE_500": "#444", 83 | "SLATE_700": "#222", 84 | "SLATE_900": "#111", 85 | "BLACK_100": "#474341", 86 | "BLACK_300": "#403C3A", 87 | "BLACK_500": "#393634", 88 | "BLACK_700": "#33302F", 89 | "BLACK_900": "#2B2928", 90 | "accent_bg_color": "{{ $primary }}", 91 | "accent_fg_color": "{{ $onPrimary }}", 92 | "accent_color": "{{ $primary }}", 93 | "destructive_bg_color": "{{ $error }}", 94 | "destructive_fg_color": "{{ $onError }}", 95 | "destructive_color": "{{ $error }}", 96 | "success_bg_color": "#81C995", 97 | "success_fg_color": "rgba(0, 0, 0, 0.87)", 98 | "warning_bg_color": "#FDD633", 99 | "warning_fg_color": "rgba(0, 0, 0, 0.87)", 100 | "error_bg_color": "{{ $error }}", 101 | "error_fg_color": "{{ $onError }}", 102 | "window_bg_color": "{{ $background }}", 103 | "window_fg_color": "{{ $onBackground }}", 104 | "view_bg_color": "{{ $surface }}", 105 | "view_fg_color": "{{ $onSurface }}", 106 | "headerbar_bg_color": "mix(@dialog_bg_color, @window_bg_color, 0.5)", 107 | "headerbar_fg_color": "{{ $onSecondaryContainer }}", 108 | "headerbar_border_color": "{{ $secondaryContainer }}", 109 | "headerbar_backdrop_color": "@headerbar_bg_color", 110 | "headerbar_shade_color": "rgba(0, 0, 0, 0.09)", 111 | "card_bg_color": "{{ $background }}", 112 | "card_fg_color": "{{ $onSecondaryContainer }}", 113 | "card_shade_color": "rgba(0, 0, 0, 0.09)", 114 | "dialog_bg_color": "{{ $secondaryContainer }}", 115 | "dialog_fg_color": "{{ $onSecondaryContainer }}", 116 | "popover_bg_color": "{{ $secondaryContainer }}", 117 | "popover_fg_color": "{{ $onSecondaryContainer }}", 118 | "thumbnail_bg_color": "#1a1b26", 119 | "thumbnail_fg_color": "#AEE5FA", 120 | "shade_color": "rgba(0, 0, 0, 0.36)", 121 | "scrollbar_outline_color": "rgba(0, 0, 0, 0.5)", 122 | 123 | "sidebar_bg_color": "@window_bg_color", 124 | "sidebar_fg_color":"@window_fg_color", 125 | "sidebar_border_color": "@sidebar_bg_color", 126 | "sidebar_backdrop_color": "@sidebar_bg_color" 127 | }, 128 | "palette": { 129 | "blue_": {}, 130 | "green_": {}, 131 | "yellow_": {}, 132 | "orange_": {}, 133 | "red_": {}, 134 | "purple_": {}, 135 | "brown_": {}, 136 | "light_": {}, 137 | "dark_": {} 138 | }, 139 | "custom_css": { 140 | "gtk4": "", 141 | "gtk3": "" 142 | }, 143 | "plugins": {} 144 | } 145 | -------------------------------------------------------------------------------- /end4_scripts/templates/hypr/hyprland_colors.conf: -------------------------------------------------------------------------------- 1 | $activeBorder = rgba({{ $primaryContainer }}99) 2 | $activeBorderGrad = rgba({{ $tertiaryContainer }}99) 3 | $inactiveBorder = rgba({{ $outline }}30) 4 | $backgroundColor = rgba({{ $surface }}FF) 5 | $pinnedWindow = rgba({{ $primaryContainer }}FF) 6 | $pinnedWindowGrad = rgba({{ $tertiaryContainer }}77) 7 | -------------------------------------------------------------------------------- /end4_scripts/templates/hypr/hyprlock_colors.conf: -------------------------------------------------------------------------------- 1 | # $text_color = rgba({{ $onBackground }}FF) 2 | # $entry_background_color = rgba({{ $background }}11) 3 | # $entry_border_color = rgba({{ $outline }}55) 4 | # $entry_color = rgba({{ $onSurfaceVariant }}FF) 5 | $text_color = rgba(FFFFFFFF) 6 | $entry_background_color = rgba(33333311) 7 | $entry_border_color = rgba(3B3B3B55) 8 | $entry_color = rgba(FFFFFFFF) 9 | -------------------------------------------------------------------------------- /end4_scripts/templates/scheme-base.json: -------------------------------------------------------------------------------- 1 | { 2 | "dark": { 3 | "term0" : "#282828", 4 | "term1" : "#CC241D", 5 | "term2" : "#98971A", 6 | "term3" : "#D79921", 7 | "term4" : "#458588", 8 | "term5" : "#B16286", 9 | "term6" : "#689D6A", 10 | "term7" : "#A89984", 11 | "term8" : "#928374", 12 | "term9" : "#FB4934", 13 | "term10" : "#B8BB26", 14 | "term11" : "#FABD2F", 15 | "term12" : "#83A598", 16 | "term13" : "#D3869B", 17 | "term14" : "#8EC07C", 18 | "term15" : "#EBDBB2" 19 | }, 20 | "light": { 21 | "term0" : "#FDF9F3", 22 | "term1" : "#FF6188", 23 | "term2" : "#A9DC76", 24 | "term3" : "#FC9867", 25 | "term4" : "#FFD866", 26 | "term5" : "#F47FD4", 27 | "term6" : "#78DCE8", 28 | "term7" : "#333034", 29 | "term8" : "#121212", 30 | "term9" : "#FF6188", 31 | "term10" : "#A9DC76", 32 | "term11" : "#FC9867", 33 | "term12" : "#FFD866", 34 | "term13" : "#F47FD4", 35 | "term14" : "#78DCE8", 36 | "term15" : "#333034" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /end4_scripts/templates/terminal/sequences.txt: -------------------------------------------------------------------------------- 1 | ]4;0;#$term0 #\]4;1;#$term1 #\]4;2;#$term2 #\]4;3;#$term3 #\]4;4;#$term4 #\]4;5;#$term5 #\]4;6;#$term6 #\]4;7;#$term7 #\]4;8;#$term8 #\]4;9;#$term9 #\]4;10;#$term10 #\]4;11;#$term11 #\]4;12;#$term12 #\]4;13;#$term13 #\]4;14;#$term14 #\]4;15;#$term15 #\]10;#$term7 #\]11;[$alpha]#$term0 #\]12;#$term7 #\]13;#$term7 #\]17;#$term7 #\]19;#$term0 #\]4;232;#$term7 #\]4;256;#$term7 #\]708;[$alpha]#$term0 #\ 2 | 3 | -------------------------------------------------------------------------------- /end4_scripts/templates/wlogout/wlogout.css: -------------------------------------------------------------------------------- 1 | * { 2 | all: unset; 3 | background-image: none; 4 | transition: 400ms cubic-bezier(0.05, 0.7, 0.1, 1); 5 | } 6 | 7 | window { 8 | background: alpha(#{{ $surface }}, 0.6); 9 | } 10 | 11 | button { 12 | font-family: 'Material Symbols Outlined'; 13 | font-size: 20rem; 14 | background-color: alpha(#{{ $onSurface }}, 0.6); 15 | border-color: alpha(#{{ $onSurface }}, 0); 16 | color: alpha(#{{ $surface }}, 0.9); 17 | margin: 2rem; 18 | border-radius: 2rem; 19 | border-style: solid; 20 | border-width: .3rem; 21 | padding: 3rem; 22 | } 23 | 24 | button:focus, 25 | button:active, 26 | button:hover { 27 | background-color: alpha(#{{ $inverseSurface }}, 0.9); 28 | border-color: alpha(#{{ $primaryContainer }}, 0.7); 29 | box-shadow: inset -0.6rem -0.6rem 0.6rem alpha(#{{ $shadow }}, 0.4), inset 0.6rem 0.6rem 0.6rem alpha(#{{ $shadow }}, 0.4); 30 | border-radius: 4rem; 31 | } 32 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; 4 | 5 | flake-parts = { 6 | url = "github:hercules-ci/flake-parts"; 7 | }; 8 | 9 | systems = { 10 | url = "systems"; 11 | }; 12 | 13 | ags = { 14 | url = "github:aylur/ags"; 15 | inputs.nixpkgs.follows = "nixpkgs"; 16 | inputs.astal = { 17 | url = "github:aylur/astal"; 18 | inputs.nixpkgs.follows = "nixpkgs"; 19 | }; 20 | }; 21 | matugen = { 22 | url = "github:InioX/matugen"; 23 | inputs.nixpkgs.follows = "nixpkgs"; 24 | }; 25 | 26 | image-hct = { 27 | url = "github:Neurarian/image-hct"; 28 | inputs.nixpkgs.follows = "nixpkgs"; 29 | }; 30 | }; 31 | 32 | outputs = inputs @ { 33 | self, 34 | nixpkgs, 35 | ags, 36 | systems, 37 | flake-parts, 38 | ... 39 | }: let 40 | mkPkgs = system: 41 | import nixpkgs { 42 | inherit system; 43 | }; 44 | mkMatshellDeps = system: let 45 | pkgs = mkPkgs system; 46 | agsPkgs = ags.packages.${system}; 47 | in 48 | (with pkgs; [ 49 | wrapGAppsHook 50 | gobject-introspection 51 | typescript 52 | dart-sass 53 | mission-center 54 | gnome-control-center 55 | imagemagick 56 | libgtop 57 | ]) 58 | ++ (with agsPkgs; [ 59 | io 60 | notifd 61 | apps 62 | hyprland 63 | wireplumber 64 | mpris 65 | network 66 | tray 67 | bluetooth 68 | cava 69 | battery 70 | powerprofiles 71 | ]); 72 | 73 | # Create a static map for all systems. 74 | # Required for deprecated hm-module options. TODO: Remove after grace period. 75 | sys = import systems; 76 | matshellDeps = builtins.listToAttrs ( 77 | map 78 | (system: { 79 | name = system; 80 | value = mkMatshellDeps system; 81 | }) 82 | sys 83 | ); 84 | in 85 | flake-parts.lib.mkFlake {inherit inputs;} { 86 | systems = sys; 87 | 88 | perSystem = {system, ...}: let 89 | pkgs = mkPkgs system; 90 | in { 91 | packages.default = let 92 | matshell-bundle = ags.lib.bundle { 93 | inherit pkgs; 94 | name = "matshell"; 95 | src = builtins.path { 96 | path = ./.; 97 | }; 98 | entry = "app.ts"; 99 | gtk4 = true; 100 | extraPackages = matshellDeps.${system} ++ [ags.packages.${system}.default]; 101 | }; 102 | in 103 | pkgs.runCommand "copy-matshell-styles" { 104 | nativeBuildInputs = [pkgs.makeWrapper]; 105 | } '' 106 | mkdir -p $out/bin 107 | 108 | # Copy the bundled app 109 | cp -r ${matshell-bundle}/* $out/ 110 | 111 | # Create a wrapper script for matshell to copy files that require mutability out of the store 112 | mv $out/bin/matshell $out/bin/.matshell-unwrapped 113 | 114 | makeWrapper $out/bin/.matshell-unwrapped $out/bin/matshell \ 115 | --run 'STYLE_DIR="$HOME/.config/ags/style" 116 | ICONS_DIR="$HOME/.config/ags/assets/icons" 117 | 118 | # Check if either directory needs to be set up 119 | if [ ! -d "$STYLE_DIR" ] || [ ! -d "$ICONS_DIR" ]; then 120 | # Create necessary directories 121 | mkdir -p "$STYLE_DIR" 122 | mkdir -p "$ICONS_DIR" 123 | 124 | # Copy style files if source exists and destination is empty 125 | if [ -d "'"$out"'/share/style" ]; then 126 | cp -r "'"$out"'/share/style/"* "$STYLE_DIR/" 127 | echo "Installed Matshell styles to $STYLE_DIR" 128 | fi 129 | 130 | # Copy icon files if source exists and destination is empty 131 | if [ -d "'"$out"'/share/assets/icons" ]; then 132 | cp -r "'"$out"'/share/assets/icons/"* "$ICONS_DIR/" 133 | echo "Installed Matshell icons to $ICONS_DIR" 134 | fi 135 | 136 | # Make copied files writable by the user 137 | find "$HOME/.config/ags" -type d -exec chmod 755 {} \; 138 | find "$HOME/.config/ags" -type f -exec chmod 644 {} \; 139 | fi' 140 | ''; 141 | apps.default = { 142 | type = "app"; 143 | program = "${self.packages.${system}.default}/bin/matshell"; 144 | }; 145 | 146 | devShells.default = pkgs.mkShell { 147 | inputsFrom = builtins.attrValues { 148 | inherit (self.packages.${system}) default; 149 | }; 150 | }; 151 | }; 152 | 153 | flake = { 154 | homeManagerModules = { 155 | default = self.homeManagerModules.matshell; 156 | matshell = import ./nix/hm-module.nix self; 157 | }; 158 | inherit matshellDeps; #TODO: Deprecated. Remove after grave period 159 | }; 160 | }; 161 | } 162 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | repo="https://github.com/Neurarian/matshell/" 2 | dest="$HOME/.config/ags/" 3 | 4 | while true; do 5 | read -p "Do you want create directories needed for end-4 colorgen scripts (y/n) " yn 6 | case $yn in 7 | [yY]) echo "Creating directories..." 8 | mkdir -p $$HOME/.local/state/ags/{scss,user} $HOME/.cache/ags/user/generated 9 | break 10 | ;; 11 | [nN]) echo "Skipping..." 12 | break 13 | ;; 14 | *) echo "Invalid response" 15 | ;; 16 | esac 17 | done 18 | 19 | if [ ! -d "${dest}" ]; then 20 | echo "Cloning matshell repository..." 21 | git clone --depth 1 "$repo" "$dest" 22 | else 23 | echo "Skipping matshell clone ($dest already exists)" 24 | fi 25 | 26 | -------------------------------------------------------------------------------- /matugen/templates/ags.scss: -------------------------------------------------------------------------------- 1 | @use "sass:color"; 2 | 3 | /* 4 | * Css Colors 5 | * Generated with Matugen 6 | */ 7 | 8 | <* for name, value in colors *> 9 | ${{name}}: {{value.default.rgba}}; 10 | <* endfor *> 11 | 12 | /* general color variables */ 13 | $red: #d17678; 14 | $accent_green: #afbea2; 15 | $tooltipBg: $background; 16 | $fg: $on_background; 17 | $bg: color.adjust($background, $alpha: -0.5); 18 | $barBg: color.adjust($background, $alpha: -0.5); 19 | $surface: color.adjust($surface_bright, $alpha: -0.4); 20 | $overlay: color.adjust($on_surface, $alpha: -0.25); 21 | $accent: $tertiary_container; 22 | 23 | /* os icon colors */ 24 | $os: $on_surface_variant; 25 | 26 | /* workspaces colors */ 27 | $primaryMon: color.mix( 28 | $primary_container, 29 | $on_primary_container, 30 | 70%, 31 | $method: rgb 32 | ); 33 | $primaryMonHover: color.mix( 34 | $primary_container, 35 | $on_primary_container, 36 | 45%, 37 | $method: rgb 38 | ); 39 | $secondaryMon: color.mix( 40 | $tertiary_container, 41 | $on_tertiary_container, 42 | 50%, 43 | $method: rgb 44 | ); 45 | $secondaryMonHover: color.mix( 46 | $tertiary_container, 47 | $on_tertiary_container, 48 | 30%, 49 | $method: rgb 50 | ); 51 | $tertiaryMon: color.mix( 52 | $secondary_container, 53 | $on_secondary_container, 54 | 70%, 55 | $method: rgb 56 | ); 57 | $tertiaryMonHover: color.mix( 58 | $secondary_container, 59 | $on_secondary_container, 60 | 45%, 61 | $method: rgb 62 | ); 63 | $fourthMon: color.mix($secondary, $on_secondary, 70%, $method: rgb); 64 | $fourthMonHover: color.mix($secondary, $on_secondary, 45%, $method: rgb); 65 | 66 | /* button colors */ 67 | $buttonEnabled: color.adjust($primaryMon, $alpha: -0.5); 68 | $buttonEnabledHover: color.adjust($buttonEnabled, $lightness: -15%); 69 | 70 | $buttonDisabled: $surface; 71 | $buttonDisabledHover: color.adjust($buttonDisabled, $alpha: +0.8); 72 | 73 | /* hw circular proc colors */ 74 | $ramProc: color.mix($primary_fixed_dim, $on_primary_fixed_variant, 66%); 75 | $cpuProc: color.mix($tertiary_fixed_dim, $on_tertiary_fixed_variant, 66%); 76 | $procBg: $surface_dim; 77 | 78 | /* numeric variables */ 79 | $round: 8px; 80 | $round2: 16px; 81 | $spacing-xs: 0.25rem; 82 | $spacing-sm: 0.4rem; 83 | $spacing-md: 0.75rem; 84 | $border-width: 2px; 85 | $scale: 0.5rem; 86 | $font: 1.1rem; 87 | 88 | -------------------------------------------------------------------------------- /matugen/templates/gtk.css: -------------------------------------------------------------------------------- 1 | /* 2 | * GTK Colors 3 | * Generated with Matugen 4 | */ 5 | @define-color theme_fg_color #AEE5FA; 6 | @define-color theme_text_color #AEE5FA; 7 | @define-color theme_bg_color #1a1b26; 8 | @define-color theme_base_color #1a1b26; 9 | @define-color theme_selected_bg_color #AEE5FA; 10 | @define-color theme_selected_fg_color rgba(0, 0, 0, 0.87); 11 | @define-color insensitive_bg_color #1a1b26; 12 | @define-color insensitive_fg_color rgba(192, 202, 245, 0.5); 13 | @define-color insensitive_base_color #24283b; 14 | @define-color theme_unfocused_fg_color #AEE5FA; 15 | @define-color theme_unfocused_text_color #c0caf5; 16 | @define-color theme_unfocused_bg_color #1a1b26; 17 | @define-color theme_unfocused_base_color #1a1b26; 18 | @define-color theme_unfocused_selected_bg_color #a9b1d6; 19 | @define-color theme_unfocused_selected_fg_color rgba(0, 0, 0, 0.87); 20 | @define-color unfocused_insensitive_color rgba(192, 202, 245, 0.5); 21 | @define-color borders rgba(192, 202, 245, 0.12); 22 | @define-color unfocused_borders rgba(192, 202, 245, 0.12); 23 | @define-color warning_color #FDD633; 24 | @define-color error_color #BA1B1B; 25 | @define-color success_color #81C995; 26 | @define-color wm_title #AEE5FA; 27 | @define-color wm_unfocused_title rgba(192, 202, 245, 0.7); 28 | @define-color wm_highlight rgba(192, 202, 245, 0.1); 29 | @define-color wm_bg #1a1b26; 30 | @define-color wm_unfocused_bg #1a1b26; 31 | @define-color wm_button_close_icon #1a1b26; 32 | @define-color wm_button_close_hover_bg #a9b1d6; 33 | @define-color wm_button_close_active_bg #c7c7c7; 34 | @define-color content_view_bg #1a1b26; 35 | @define-color placeholder_text_color silver; 36 | @define-color text_view_bg #1d1d1d; 37 | @define-color budgie_tasklist_indicator_color #90D1F6; 38 | @define-color budgie_tasklist_indicator_color_active #90D1F6; 39 | @define-color budgie_tasklist_indicator_color_active_window #999999; 40 | @define-color budgie_tasklist_indicator_color_attention #FDD633; 41 | @define-color STRAWBERRY_100 #FF9262; 42 | @define-color STRAWBERRY_300 #FF793E; 43 | @define-color STRAWBERRY_500 #F15D22; 44 | @define-color STRAWBERRY_700 #CF3B00; 45 | @define-color STRAWBERRY_900 #AC1800; 46 | @define-color ORANGE_100 #FFDB91; 47 | @define-color ORANGE_300 #FFCA40; 48 | @define-color ORANGE_500 #FAA41A; 49 | @define-color ORANGE_700 #DE8800; 50 | @define-color ORANGE_900 #C26C00; 51 | @define-color BANANA_100 #FFFFA8; 52 | @define-color BANANA_300 #FFFA7D; 53 | @define-color BANANA_500 #FFCE51; 54 | @define-color BANANA_700 #D1A023; 55 | @define-color BANANA_900 #A27100; 56 | @define-color LIME_100 #A2F3BE; 57 | @define-color LIME_300 #8ADBA6; 58 | @define-color LIME_500 #73C48F; 59 | @define-color LIME_700 #479863; 60 | @define-color LIME_900 #1C6D38; 61 | @define-color BLUEBERRY_100 #94A6FF; 62 | @define-color BLUEBERRY_300 #6A7CE0; 63 | @define-color BLUEBERRY_500 #3F51B5; 64 | @define-color BLUEBERRY_700 #213397; 65 | @define-color BLUEBERRY_900 #031579; 66 | @define-color GRAPE_100 #D25DE6; 67 | @define-color GRAPE_300 #B84ACB; 68 | @define-color GRAPE_500 #9C27B0; 69 | @define-color GRAPE_700 #830E97; 70 | @define-color GRAPE_900 #6A007E; 71 | @define-color COCOA_100 #9F9792; 72 | @define-color COCOA_300 #7B736E; 73 | @define-color COCOA_500 #574F4A; 74 | @define-color COCOA_700 #463E39; 75 | @define-color COCOA_900 #342C27; 76 | @define-color SILVER_100 #EEE; 77 | @define-color SILVER_300 #CCC; 78 | @define-color SILVER_500 #AAA; 79 | @define-color SILVER_700 #888; 80 | @define-color SILVER_900 #666; 81 | @define-color SLATE_100 #888; 82 | @define-color SLATE_300 #666; 83 | @define-color SLATE_500 #444; 84 | @define-color SLATE_700 #222; 85 | @define-color SLATE_900 #111; 86 | @define-color BLACK_100 #474341; 87 | @define-color BLACK_300 #403C3A; 88 | @define-color BLACK_500 #393634; 89 | @define-color BLACK_700 #33302F; 90 | @define-color BLACK_900 #2B2928; 91 | @define-color success_bg_color #81C995; 92 | @define-color success_fg_color rgba(0, 0, 0, 0.87); 93 | @define-color warning_bg_color #FDD633; 94 | @define-color warning_fg_color rgba(0, 0, 0, 0.87); 95 | @define-color view_bg_color #1D1108; 96 | @define-color view_fg_color #F7DECE; 97 | @define-color headerbar_backdrop_color @headerbar_bg_color; 98 | @define-color headerbar_shade_color rgba(0, 0, 0, 0.09); 99 | @define-color card_shade_color rgba(0, 0, 0, 0.09); 100 | @define-color dialog_bg_color #61431E; 101 | @define-color dialog_fg_color #FFDDB9; 102 | @define-color popover_bg_color #61431E; 103 | @define-color popover_fg_color #FFDDB9; 104 | @define-color thumbnail_bg_color #1a1b26; 105 | @define-color thumbnail_fg_color #AEE5FA; 106 | @define-color shade_color rgba(0, 0, 0, 0.36); 107 | @define-color scrollbar_outline_color rgba(0, 0, 0, 0.5); 108 | 109 | @define-color accent_color {{colors.primary.default.hex}}; 110 | @define-color accent_fg_color {{colors.on_primary.default.hex}}; 111 | @define-color accent_bg_color {{colors.primary.default.hex}}; 112 | @define-color window_bg_color {{colors.background.default.hex}}; 113 | @define-color window_fg_color {{colors.on_background.default.hex}}; 114 | @define-color headerbar_fg_color {{colors.on_secondary_container.default.hex}}; 115 | @define-color headerbar_border_color {{colors.secondary_container.default.hex}}; 116 | @define-color headerbar_bg_color mix(@dialog_bg_color, @window_bg_color, 0.5); 117 | 118 | @define-color popover_bg_color {{colors.secondary_container.default.hex}}; 119 | @define-color popover_fg_color {{colors.on_secondary_container.default.hex}}; 120 | @define-color view_bg_color {{colors.surface.default.hex}}; 121 | @define-color view_fg_color {{colors.on_surface.default.hex}}; 122 | @define-color card_bg_color {{colors.background.default.hex}}; 123 | @define-color card_fg_color {{colors.on_secondary_container.default.hex}}; 124 | @define-color destructive_color {{colors.error.default.hex}}; 125 | @define-color destructive_bg_color {{colors.error.default.hex}}; 126 | @define-color destructive_fg_color {{colors.on_error.default.hex}}; 127 | @define-color error_bg_color {{colors.error.default.hex}}; 128 | @define-color error_fg_color {{colors.on_error.default.hex}}; 129 | 130 | @define-color sidebar_bg_color @window_bg_color; 131 | @define-color sidebar_fg_color @window_fg_color; 132 | @define-color sidebar_border_color @window_bg_color; 133 | @define-color sidebar_backdrop_color @window_bg_color; 134 | -------------------------------------------------------------------------------- /matugen/templates/hyprland_colors.conf: -------------------------------------------------------------------------------- 1 | $image = {{image}} 2 | $activeBorder = rgba({{colors.primary_container.default.hex_stripped}}99) 3 | $activeBorderGrad = rgba({{colors.tertiary_container.default.hex_stripped}}99) 4 | $inactiveBorder = rgba({{colors.outline.default.hex_stripped}}30) 5 | $backgroundColor = rgba({{colors.surface.default.hex_stripped}}FF) 6 | $pinnedWindow = rgba({{colors.primary_container.default.hex_stripped}}FF) 7 | $pinnedWindowGrad = rgba({{colors.tertiary_container.default.hex_stripped}}77) 8 | -------------------------------------------------------------------------------- /matugen/templates/hyprlock_colors.conf: -------------------------------------------------------------------------------- 1 | $image = {{image}} 2 | 3 | $text_color = rgba({{colors.secondary_fixed.default.hex_stripped}}FF) 4 | $entry_background_color = rgba({{colors.background.default.hex_stripped}}11) 5 | $entry_border_color = rgba({{colors.outline.default.hex_stripped}}55) 6 | $entry_color = rgba({{colors.secondary_fixed.default.hex_stripped}}FF) 7 | -------------------------------------------------------------------------------- /options.ts: -------------------------------------------------------------------------------- 1 | import { execAsync, GLib } from "astal"; 2 | import { 3 | initializeConfig, 4 | defineOption, // Renamed for clarity 5 | ConfigValue, 6 | saveConfig, 7 | } from "./utils/option"; 8 | 9 | const options = await (async () => { 10 | const currentWallpaper = await execAsync( 11 | "hyprctl hyprpaper listloaded", 12 | ).catch(() => ""); 13 | 14 | const config = initializeConfig( 15 | `${GLib.get_user_config_dir()}/ags/config.json`, 16 | { 17 | "wallpaper.folder": defineOption(GLib.get_home_dir(), { 18 | useCache: true, 19 | }), 20 | "wallpaper.current": defineOption(currentWallpaper, { 21 | useCache: true, 22 | }), 23 | "bar.position": defineOption("top"), // "top", "bottom" 24 | "bar.style": defineOption("expanded"), // "floating" or "expanded" 25 | "bar.modules.cava.show": defineOption(false), 26 | /* "catmull_rom", "smooth", "rounded", "bars","jumping_bars", 27 | "dots", "circular", "particles", "wave_particles","waterfall", "mesh" */ 28 | "bar.modules.cava.style": defineOption("catmull_rom"), 29 | "bar.modules.media.cava.show": defineOption(true), 30 | "bar.modules.showOsIcon": defineOption(true), 31 | "musicPlayer.modules.cava.show": defineOption(true), 32 | "musicPlayer.modules.cava.style": 33 | defineOption("catmull_rom"), 34 | "system-menu.modules.bluetooth.enableOverskride": 35 | defineOption(true), 36 | "system-menu.modules.wifi.enableGnomeControlCenter": 37 | defineOption(true), 38 | }, 39 | ); 40 | 41 | saveConfig(); 42 | return config; 43 | })(); 44 | 45 | export default options; 46 | -------------------------------------------------------------------------------- /style/abstracts/_index.scss: -------------------------------------------------------------------------------- 1 | @forward "mixins"; 2 | /* comment this when using end4 backend */ 3 | @forward "variables"; 4 | /* uncomment when using end4 backend */ 5 | /* @forward "variables_end4"; 6 | @forward "_material-colors_end4"; */ 7 | -------------------------------------------------------------------------------- /style/abstracts/_material-colors_end4.scss: -------------------------------------------------------------------------------- 1 | $darkmode: True; 2 | $transparent: False; 3 | $primary_paletteKeyColor: #FE7600; 4 | $secondary_paletteKeyColor: #966F49; 5 | $tertiary_paletteKeyColor: #967132; 6 | $neutral_paletteKeyColor: #887368; 7 | $neutral_variant_paletteKeyColor: #8B7265; 8 | $background: #1D1009; 9 | $onBackground: #F8DDD0; 10 | $surface: #1D1009; 11 | $surfaceDim: #1D1009; 12 | $surfaceBright: #46362D; 13 | $surfaceContainerLowest: #170B05; 14 | $surfaceContainerLow: #261811; 15 | $surfaceContainer: #2B1C14; 16 | $surfaceContainerHigh: #36271E; 17 | $surfaceContainerHighest: #413128; 18 | $onSurface: #F8DDD0; 19 | $surfaceVariant: #574237; 20 | $onSurfaceVariant: #DEC0B1; 21 | $inverseSurface: #F8DDD0; 22 | $inverseOnSurface: #3D2D24; 23 | $outline: #A68B7D; 24 | $outlineVariant: #574237; 25 | $shadow: #000000; 26 | $scrim: #000000; 27 | $surfaceTint: #FFB68D; 28 | $primary: #FFB68D; 29 | $onPrimary: #532200; 30 | $primaryContainer: #763300; 31 | $onPrimaryContainer: #FFDBC9; 32 | $inversePrimary: #9B4600; 33 | $secondary: #EDBD92; 34 | $onSecondary: #472A0A; 35 | $secondaryContainer: #634220; 36 | $onSecondaryContainer: #FFDEC2; 37 | $tertiary: #ECBF78; 38 | $onTertiary: #432C00; 39 | $tertiaryContainer: #B28A48; 40 | $onTertiaryContainer: #000000; 41 | $error: #FFB4AB; 42 | $onError: #690005; 43 | $errorContainer: #93000A; 44 | $onErrorContainer: #FFDAD6; 45 | $primaryFixed: #FFDBC9; 46 | $primaryFixedDim: #FFB68D; 47 | $onPrimaryFixed: #331200; 48 | $onPrimaryFixedVariant: #763300; 49 | $secondaryFixed: #FFDCBE; 50 | $secondaryFixedDim: #EDBD92; 51 | $onSecondaryFixed: #2D1600; 52 | $onSecondaryFixedVariant: #60401E; 53 | $tertiaryFixed: #FFDEAD; 54 | $tertiaryFixedDim: #ECBF78; 55 | $onTertiaryFixed: #281900; 56 | $onTertiaryFixedVariant: #5F4103; 57 | $success: #B5CCBA; 58 | $onSuccess: #213528; 59 | $successContainer: #374B3E; 60 | $onSuccessContainer: #D1E9D6; 61 | $term0: #27160E; 62 | $term1: #E66E2C; 63 | $term2: #FFBF87; 64 | $term3: #FFE0CC; 65 | $term4: #B8AC66; 66 | $term5: #F09170; 67 | $term6: #FEBD73; 68 | $term7: #EED4C3; 69 | $term8: #CBB5A9; 70 | $term9: #FFAD85; 71 | $term10: #FFFCFF; 72 | $term11: #FFFFFF; 73 | $term12: #F9D7AC; 74 | $term13: #FFD0BF; 75 | $term14: #FFF9F7; 76 | $term15: #FFDBC9; 77 | -------------------------------------------------------------------------------- /style/abstracts/_mixins.scss: -------------------------------------------------------------------------------- 1 | @use "sass:color"; 2 | /* comment if using end4 backend*/ 3 | @use "variables" as *; 4 | /* uncomment if using end4 backend*/ 5 | /* @use "variables_end4" as *; 6 | @use "_material-colors_end4" as *; */ 7 | 8 | @mixin animate { 9 | transition: 400ms cubic-bezier(0.05, 0.7, 0.1, 1); 10 | } 11 | 12 | @mixin border { 13 | // border: 1px solid color.adjust($background, $alpha: -0.9); 14 | box-shadow: 15 | // inset 0 0 0 1px color.ajust($background, $alpha: -0.9), 16 | 0 3px 5px 1px color.adjust($background, $alpha: -0.9); 17 | } 18 | 19 | /* mixins */ 20 | @mixin window-rounding { 21 | border-radius: $round2; 22 | } 23 | 24 | @mixin rounding { 25 | border-radius: calc(#{$round2} - #{$spacing-sm} - #{$border-width}); 26 | } 27 | 28 | @mixin window-box { 29 | @include rounding; 30 | 31 | background: $surface; 32 | box-shadow: 0 1px 5px -5px rgba(0, 0, 0, 0.5); 33 | margin: $spacing-sm; 34 | padding: $spacing-sm; 35 | } 36 | 37 | @mixin window { 38 | @include border; 39 | @include window-rounding; 40 | 41 | background: $bg; 42 | margin: 5px 10px 15px; 43 | padding: $spacing-sm; 44 | } 45 | 46 | /* buttons */ 47 | @mixin button-active { 48 | @include animate; 49 | background: $buttonEnabled; 50 | border-radius: 5rem; 51 | padding: 0.4rem; 52 | @if $darkmode { 53 | } @else { 54 | // Light mode enhancements 55 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.18); 56 | } 57 | 58 | &:hover { 59 | background: $buttonEnabledHover; 60 | } 61 | } 62 | 63 | @mixin button { 64 | @include animate; 65 | @if $darkmode { 66 | background: $buttonDisabled; 67 | } @else { 68 | background: color.adjust($buttonDisabled, $alpha: +0.5); 69 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.18); 70 | } 71 | border-radius: 5rem; 72 | padding: 0.4rem; 73 | 74 | &:hover { 75 | /* would prefer to handle this with transparency, 76 | but that currently glitches on Hyprland */ 77 | @if $darkmode { 78 | background: color.adjust($buttonDisabled, $lightness: +20%); 79 | } @else { 80 | background: color.adjust($primaryMon, $lightness: +22%); 81 | } 82 | } 83 | } 84 | 85 | @mixin menu { 86 | background: $tooltipBg; 87 | border-radius: $round; 88 | font-size: $font; 89 | 90 | separator { 91 | background-color: $surface; 92 | } 93 | 94 | menuitem { 95 | @include button; 96 | border-radius: 0; 97 | padding: 0.4rem 0.7rem; 98 | 99 | &:first-child { 100 | border-radius: $round $round 0 0; 101 | } 102 | &:last-child { 103 | border-radius: 0 0 $round $round; 104 | } 105 | &:only-child { 106 | border-radius: $round; 107 | } 108 | } 109 | } 110 | 111 | @mixin hw-circular-progress { 112 | margin-right: 0.4rem; 113 | margin-left: 0.3rem; 114 | 115 | circularprogress { 116 | progress { 117 | color: $ramProc; 118 | min-width: 2.3rem; 119 | } 120 | 121 | radius { 122 | color: color.adjust($procBg, $alpha: -0.5); 123 | } 124 | } 125 | button { 126 | label { 127 | font-family: "Material Symbols Outlined"; 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /style/abstracts/_variables.scss: -------------------------------------------------------------------------------- 1 | @use "sass:color"; 2 | 3 | /* 4 | * Css Colors 5 | * Generated with Matugen 6 | */ 7 | 8 | 9 | $background: rgba(30, 16, 12, 1.0); 10 | 11 | $error: rgba(255, 180, 171, 1.0); 12 | 13 | $error_container: rgba(147, 0, 10, 1.0); 14 | 15 | $inverse_on_surface: rgba(61, 44, 39, 1.0); 16 | 17 | $inverse_primary: rgba(174, 50, 0, 1.0); 18 | 19 | $inverse_surface: rgba(249, 220, 212, 1.0); 20 | 21 | $on_background: rgba(249, 220, 212, 1.0); 22 | 23 | $on_error: rgba(105, 0, 5, 1.0); 24 | 25 | $on_error_container: rgba(255, 218, 214, 1.0); 26 | 27 | $on_primary: rgba(94, 23, 0, 1.0); 28 | 29 | $on_primary_container: rgba(255, 219, 208, 1.0); 30 | 31 | $on_primary_fixed: rgba(58, 11, 0, 1.0); 32 | 33 | $on_primary_fixed_variant: rgba(133, 36, 0, 1.0); 34 | 35 | $on_secondary: rgba(73, 40, 15, 1.0); 36 | 37 | $on_secondary_container: rgba(255, 220, 198, 1.0); 38 | 39 | $on_secondary_fixed: rgba(48, 20, 1, 1.0); 40 | 41 | $on_secondary_fixed_variant: rgba(99, 62, 35, 1.0); 42 | 43 | $on_surface: rgba(249, 220, 212, 1.0); 44 | 45 | $on_surface_variant: rgba(220, 193, 185, 1.0); 46 | 47 | $on_tertiary: rgba(71, 42, 0, 1.0); 48 | 49 | $on_tertiary_container: rgba(255, 221, 183, 1.0); 50 | 51 | $on_tertiary_fixed: rgba(42, 23, 0, 1.0); 52 | 53 | $on_tertiary_fixed_variant: rgba(100, 63, 7, 1.0); 54 | 55 | $outline: rgba(163, 139, 133, 1.0); 56 | 57 | $outline_variant: rgba(85, 66, 61, 1.0); 58 | 59 | $primary: rgba(255, 181, 158, 1.0); 60 | 61 | $primary_container: rgba(133, 36, 0, 1.0); 62 | 63 | $primary_fixed: rgba(255, 219, 208, 1.0); 64 | 65 | $primary_fixed_dim: rgba(255, 181, 158, 1.0); 66 | 67 | $scrim: rgba(0, 0, 0, 1.0); 68 | 69 | $secondary: rgba(241, 187, 152, 1.0); 70 | 71 | $secondary_container: rgba(99, 62, 35, 1.0); 72 | 73 | $secondary_fixed: rgba(255, 220, 198, 1.0); 74 | 75 | $secondary_fixed_dim: rgba(241, 187, 152, 1.0); 76 | 77 | $shadow: rgba(0, 0, 0, 1.0); 78 | 79 | $source_color: rgba(204, 76, 32, 1.0); 80 | 81 | $surface: rgba(30, 16, 12, 1.0); 82 | 83 | $surface_bright: rgba(71, 53, 48, 1.0); 84 | 85 | $surface_container: rgba(43, 28, 23, 1.0); 86 | 87 | $surface_container_high: rgba(54, 38, 33, 1.0); 88 | 89 | $surface_container_highest: rgba(66, 49, 44, 1.0); 90 | 91 | $surface_container_low: rgba(39, 24, 19, 1.0); 92 | 93 | $surface_container_lowest: rgba(24, 11, 7, 1.0); 94 | 95 | $surface_dim: rgba(30, 16, 12, 1.0); 96 | 97 | $surface_tint: rgba(255, 181, 158, 1.0); 98 | 99 | $surface_variant: rgba(85, 66, 61, 1.0); 100 | 101 | $tertiary: rgba(243, 189, 123, 1.0); 102 | 103 | $tertiary_container: rgba(100, 63, 7, 1.0); 104 | 105 | $tertiary_fixed: rgba(255, 221, 183, 1.0); 106 | 107 | $tertiary_fixed_dim: rgba(243, 189, 123, 1.0); 108 | 109 | 110 | /* general color variables */ 111 | $red: #d17678; 112 | $accent_green: #afbea2; 113 | $tooltipBg: $background; 114 | $fg: $on_background; 115 | $bg: color.adjust($background, $alpha: -0.5); 116 | $barBg: color.adjust($background, $alpha: -0.5); 117 | $surface: color.adjust($surface_bright, $alpha: -0.4); 118 | $overlay: color.adjust($on_surface, $alpha: -0.25); 119 | $accent: $tertiary_container; 120 | 121 | /* os icon colors */ 122 | $os: $on_surface_variant; 123 | 124 | /* workspaces colors */ 125 | $primaryMon: color.mix( 126 | $primary_container, 127 | $on_primary_container, 128 | 70%, 129 | $method: rgb 130 | ); 131 | $primaryMonHover: color.mix( 132 | $primary_container, 133 | $on_primary_container, 134 | 45%, 135 | $method: rgb 136 | ); 137 | $secondaryMon: color.mix( 138 | $tertiary_container, 139 | $on_tertiary_container, 140 | 50%, 141 | $method: rgb 142 | ); 143 | $secondaryMonHover: color.mix( 144 | $tertiary_container, 145 | $on_tertiary_container, 146 | 30%, 147 | $method: rgb 148 | ); 149 | $tertiaryMon: color.mix( 150 | $secondary_container, 151 | $on_secondary_container, 152 | 70%, 153 | $method: rgb 154 | ); 155 | $tertiaryMonHover: color.mix( 156 | $secondary_container, 157 | $on_secondary_container, 158 | 45%, 159 | $method: rgb 160 | ); 161 | $fourthMon: color.mix($secondary, $on_secondary, 70%, $method: rgb); 162 | $fourthMonHover: color.mix($secondary, $on_secondary, 45%, $method: rgb); 163 | 164 | /* button colors */ 165 | $buttonEnabled: color.adjust($primaryMon, $alpha: -0.5); 166 | $buttonEnabledHover: color.adjust($buttonEnabled, $lightness: -15%); 167 | 168 | $buttonDisabled: $surface; 169 | $buttonDisabledHover: color.adjust($buttonDisabled, $alpha: +0.8); 170 | 171 | /* hw circular proc colors */ 172 | $ramProc: color.mix($primary_fixed_dim, $on_primary_fixed_variant, 66%); 173 | $cpuProc: color.mix($tertiary_fixed_dim, $on_tertiary_fixed_variant, 66%); 174 | $procBg: $surface_dim; 175 | 176 | /* numeric variables */ 177 | $round: 8px; 178 | $round2: 16px; 179 | $margin: 0.4rem; 180 | $padding: 0.4rem; 181 | $spacing-xs: 0.25rem; 182 | $spacing-sm: 0.4rem; 183 | $spacing-md: 0.75rem; 184 | $border-width: 2px; 185 | $scale: 0.5rem; 186 | $font: 1.1rem; 187 | 188 | 189 | /* Theme mode and scheme variables */ 190 | $darkmode: true; 191 | $material-color-scheme: "scheme-vibrant"; 192 | -------------------------------------------------------------------------------- /style/abstracts/_variables_end4.scss: -------------------------------------------------------------------------------- 1 | @use "sass:color"; 2 | @use "material-colors_end4" as *; 3 | 4 | $red: #d17678; 5 | $accent_green: #afbea2; 6 | 7 | $primaryMon: $primaryContainer; 8 | $secondaryMon: $tertiaryContainer; 9 | $thirdMon: $tertiary; 10 | $fourthMon: $success; 11 | 12 | $os: $onSurfaceVariant; 13 | 14 | $primaryMon: color.mix( 15 | $primaryContainer, 16 | $onPrimaryContainer, 17 | 70%, 18 | $method: rgb 19 | ); 20 | $primaryMonHover: color.mix( 21 | $primaryContainer, 22 | $onPrimaryContainer, 23 | 45%, 24 | $method: rgb 25 | ); 26 | $secondaryMon: color.mix( 27 | $tertiaryContainer, 28 | $onTertiaryContainer, 29 | 50%, 30 | $method: rgb 31 | ); 32 | $secondaryMonHover: color.mix( 33 | $tertiaryContainer, 34 | $onTertiaryContainer, 35 | 30%, 36 | $method: rgb 37 | ); 38 | $tertiaryMon: color.mix( 39 | $secondaryContainer, 40 | $onSecondaryContainer, 41 | 70%, 42 | $method: rgb 43 | ); 44 | $tertiaryMonHover: color.mix( 45 | $secondaryContainer, 46 | $onSecondaryContainer, 47 | 45%, 48 | $method: rgb 49 | ); 50 | $fourthMon: color.mix($secondary, $onSecondary, 70%, $method: rgb); 51 | $fourthMonHover: color.mix($secondary, $onSecondary, 45%, $method: rgb); 52 | 53 | $tooltipBg: $background; 54 | $fg: $onBackground; 55 | $bg: color.adjust($background, $alpha: -0.5); 56 | $barBg: color.adjust($background, $alpha: -0.5); 57 | 58 | $surface: color.adjust($surfaceBright, $alpha: -0.6); 59 | $overlay: color.adjust($onSurface, $alpha: -0.25); 60 | 61 | $accent: $tertiaryContainer; 62 | 63 | $buttonEnabled: $accent; 64 | $buttonEnabledHover: color.adjust($buttonEnabled, $lightness: -15%); 65 | 66 | $buttonDisabled: $surface; 67 | $buttonDisabledHover: color.adjust($buttonDisabled, $alpha: +0.8); 68 | 69 | $ramProc: $onPrimaryFixedVariant; 70 | $cpuProc: $onTertiaryFixedVariant; 71 | $procBg: $surfaceDim; 72 | 73 | $ramProc: color.mix($primaryFixedDim, $onPrimaryFixedVariant, 66%); 74 | $cpuProc: color.mix($tertiaryFixedDim, $onTertiaryFixedVariant, 66%); 75 | $procBg: $surfaceDim; 76 | 77 | /* numeric variables */ 78 | $round: 8px; 79 | $round2: 16px; 80 | $spacing-xs: 0.25rem; 81 | $spacing-sm: 0.4rem; 82 | $spacing-md: 0.75rem; 83 | $border-width: 2px; 84 | $scale: 0.5rem; 85 | $font: 1.1rem; 86 | -------------------------------------------------------------------------------- /style/base/_index.scss: -------------------------------------------------------------------------------- 1 | @forward "reset"; 2 | -------------------------------------------------------------------------------- /style/base/_reset.scss: -------------------------------------------------------------------------------- 1 | 2 | @use "sass:color"; 3 | @use "../abstracts" as *; 4 | 5 | * { 6 | all: unset; 7 | font-family: FiraCode Nerd Font; 8 | color: $fg; 9 | text-shadow: 0 2px 3px color.adjust($background, $alpha: -0.8); 10 | } 11 | -------------------------------------------------------------------------------- /style/components/_bar.scss: -------------------------------------------------------------------------------- 1 | @use "../layouts" as *; 2 | @use "../abstracts" as *; 3 | @use "sass:color"; 4 | 5 | /* general */ 6 | /* Base bar styles */ 7 | .Bar { 8 | border-radius: calc(#{$round} * 4); 9 | font-size: $font; 10 | 11 | /* Common styles for both modes */ 12 | cava { 13 | opacity: 0.1; 14 | } 15 | 16 | .module { 17 | margin: 0 0.4rem; 18 | } 19 | 20 | /* OS Icon */ 21 | .OsIcon { 22 | -gtk-icon-size: 2rem; 23 | margin-left: 0.8rem; 24 | margin-right: -0.5rem; 25 | color: $os; 26 | } 27 | 28 | /* Hyperland */ 29 | .Workspaces { 30 | margin: 0.4rem 0.8rem; 31 | 32 | button { 33 | background: rgba(0, 0, 0, 0.3); 34 | border-radius: 0.7rem; 35 | margin: 0.6rem 0.35rem; 36 | min-width: 1.5rem; 37 | min-height: 1.5rem; 38 | transition: 100ms linear; 39 | box-shadow: inset -2px -2px 2px color.adjust($bg, $alpha: -0.2); 40 | } 41 | 42 | .focused { 43 | min-width: 2.5rem; 44 | } 45 | 46 | .monitor0 { 47 | background: $primaryMon; 48 | box-shadow: inset -2px -2px 2px color.scale($primaryMon, $lightness: -25%); 49 | &:hover { 50 | background: $primaryMonHover; 51 | box-shadow: inset -2px -2px 2px 52 | color.scale($primaryMonHover, $lightness: -25%); 53 | } 54 | } 55 | 56 | .monitor1 { 57 | background: $secondaryMon; 58 | box-shadow: inset -2px -2px 2px 59 | color.scale($secondaryMon, $lightness: -25%); 60 | &:hover { 61 | background: $secondaryMonHover; 62 | box-shadow: inset -2px -2px 2px 63 | color.scale($secondaryMonHover, $lightness: -25%); 64 | } 65 | } 66 | 67 | .monitor2 { 68 | background: $tertiaryMon; 69 | box-shadow: inset -2px -2px 2px 70 | color.scale($tertiaryMon, $lightness: -25%); 71 | &:hover { 72 | background: $tertiaryMonHover; 73 | box-shadow: inset -2px -2px 2px 74 | color.scale($tertiaryMonHover, $lightness: -25%); 75 | } 76 | } 77 | 78 | .monitor3 { 79 | background: $fourthMon; 80 | box-shadow: inset -2px -2px 2px color.scale($fourthMon, $lightness: -25%); 81 | &:hover { 82 | background: $fourthMonHover; 83 | box-shadow: inset -2px -2px 2px 84 | color.scale($fourthMonHover, $lightness: -25%); 85 | } 86 | } 87 | } 88 | 89 | /* music */ 90 | .Media { 91 | & > box { 92 | @include animate; 93 | border-radius: $round2; 94 | margin: 0.4rem; 95 | font-size: $font; 96 | cava { 97 | opacity: 0.3; 98 | } 99 | } 100 | 101 | &.active > box { 102 | background: $surface; 103 | } 104 | 105 | .cover { 106 | background-size: cover; 107 | background-position: center; 108 | border-radius: 50%; 109 | min-width: 2.5rem; 110 | min-height: 2rem; 111 | margin: 0.1rem 0.4rem; 112 | } 113 | } 114 | 115 | /* tray */ 116 | .SysTray { 117 | background: none; 118 | image { 119 | -gtk-icon-size: $font; 120 | } 121 | 122 | .tray-item { 123 | margin: 0rem 0.3rem; 124 | } 125 | 126 | &:not(:last-child) { 127 | margin-right: -0.3rem; 128 | } 129 | 130 | &.active { 131 | background: $surface; 132 | } 133 | } 134 | 135 | /* hw-monitor */ 136 | .bar-hw-ram-box { 137 | @include hw-circular-progress; 138 | 139 | circularprogress progress { 140 | color: $ramProc; 141 | } 142 | 143 | button { 144 | label { 145 | margin: 1px 0px 0px 2px; 146 | font-size: 1.2rem; 147 | } 148 | } 149 | } 150 | 151 | .bar-hw-cpu-box { 152 | @include hw-circular-progress; 153 | 154 | circularprogress progress { 155 | color: $cpuProc; 156 | } 157 | 158 | button { 159 | label { 160 | margin: 1px 0px 0px 1px; 161 | font-size: 1.3rem; 162 | } 163 | } 164 | } 165 | 166 | /* System menu */ 167 | .system-menu-toggler { 168 | box { 169 | @include animate; 170 | margin: 0.4rem 0; 171 | border-radius: $round2; 172 | -gtk-icon-size: $font; 173 | } 174 | 175 | &.active box { 176 | background: $surface; 177 | } 178 | } 179 | 180 | .separator { 181 | font-size: 1.8rem; 182 | color: $outline; 183 | } 184 | 185 | .clock { 186 | margin: 0 1.2rem 0 0.4rem; 187 | } 188 | 189 | .power-button { 190 | margin: 0 1.2rem 0 0rem; 191 | } 192 | } 193 | /* Floating elements style */ 194 | .bar-style-floating { 195 | .centerbox > box { 196 | background: $barBg; 197 | border-radius: calc(#{$round} * 4); 198 | box-shadow: 199 | inset -2px -2px 3px rgba(0, 0, 0, 0.4), 200 | -1px -1px 2px $bg; 201 | padding: 0 0.5rem; 202 | margin: 0 0.3rem; 203 | } 204 | } 205 | 206 | /* Full-width style */ 207 | .bar-style-expanded { 208 | background: $barBg; 209 | border-radius: calc(#{$round} * 4); 210 | box-shadow: 211 | inset -2px -2px 3px rgba(0, 0, 0, 0.4), 212 | -1px -1px 2px $bg; 213 | padding: 0 0.5rem; 214 | margin: 0 0.3rem; 215 | } 216 | -------------------------------------------------------------------------------- /style/components/_control-center.scss: -------------------------------------------------------------------------------- 1 | @use "../abstracts" as *; 2 | @use "sass:color"; 3 | 4 | /* General */ 5 | .control-panel { 6 | @include window; 7 | -gtk-icon-size: $font; 8 | border: 1px solid rgba(0, 0, 0, 0.1); 9 | & > box { 10 | @include window-box; 11 | border: 1px solid rgba(0, 0, 0, 0.1); 12 | @if $darkmode { 13 | } @else { 14 | // Light mode enhancements 15 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); 16 | } 17 | } 18 | @if $darkmode { 19 | } @else { 20 | // Light mode enhancements 21 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); 22 | } 23 | separator { 24 | background-color: color.adjust($fg, $alpha: -0.8); 25 | margin: 0.5rem 0 0.5rem 0; 26 | min-height: 1px; 27 | } 28 | image { 29 | margin-right: $spacing-md; 30 | } 31 | } 32 | 33 | /* Option rows */ 34 | 35 | .option-row .option-label { 36 | margin: 0 $spacing-sm; 37 | } 38 | 39 | /* Option elements */ 40 | .option-icon { 41 | margin-right: $spacing-sm; 42 | } 43 | 44 | .option-switch { 45 | border-radius: 5rem; 46 | padding: 0.4rem; 47 | @if $darkmode { 48 | background-color: $bg; 49 | } @else { 50 | // Light mode enhancements 51 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); 52 | background-color: $buttonEnabled; 53 | } 54 | 55 | slider { 56 | min-width: $spacing-md; 57 | min-height: 2rem; 58 | border-radius: 1.5rem; 59 | @if $darkmode { 60 | background-color: $buttonDisabled; 61 | } @else { 62 | // Light mode enhancements 63 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); 64 | background-color: $buttonEnabled; 65 | } 66 | } 67 | 68 | &:checked { 69 | slider { 70 | @if $darkmode { 71 | background-color: $buttonEnabled; 72 | } @else { 73 | // Light mode enhancements 74 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); 75 | background-color: $buttonDisabled; 76 | } 77 | } 78 | } 79 | } 80 | 81 | .option-dropdown { 82 | min-width: 132px; 83 | margin: $spacing-xs 0; 84 | } 85 | 86 | .option-dropdown button { 87 | @include button-active; 88 | @if $darkmode { 89 | } @else { 90 | // Light mode enhancements 91 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.18); 92 | } 93 | } 94 | 95 | .option-row { 96 | @include window-box; 97 | border: 1px solid rgba(0, 0, 0, 0.1); 98 | @if $darkmode { 99 | } @else { 100 | // Light mode enhancements 101 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); 102 | } 103 | } 104 | 105 | .category-content { 106 | > box { 107 | @include window; 108 | margin: $spacing-sm; 109 | border: 1px solid rgba(0, 0, 0, 0.1); 110 | } 111 | .category-button { 112 | @include button; 113 | @include window-box; 114 | @include rounding; 115 | border: 1px solid rgba(0, 0, 0, 0.1); 116 | @if $darkmode { 117 | } @else { 118 | // Light mode enhancements 119 | &:hover, 120 | &:focus { 121 | border: 1px solid rgba(0, 0, 0, 0.04); 122 | } 123 | } 124 | image { 125 | &:first-child { 126 | margin-right: $spacing-sm; 127 | } 128 | } 129 | label { 130 | margin: 0 $spacing-sm; 131 | } 132 | } 133 | } 134 | .section-label { 135 | font-weight: bold; 136 | margin: $spacing-sm 0 $spacing-xs 0; 137 | } 138 | -------------------------------------------------------------------------------- /style/components/_index.scss: -------------------------------------------------------------------------------- 1 | @forward "bar"; 2 | @forward "launcher"; 3 | @forward "logout-menu"; 4 | @forward "music"; 5 | @forward "notifications"; 6 | @forward "osd"; 7 | @forward "system-menu"; 8 | @forward "control-center"; 9 | -------------------------------------------------------------------------------- /style/components/_launcher.scss: -------------------------------------------------------------------------------- 1 | @use "../layouts" as *; 2 | @use "../abstracts" as *; 3 | @use "sass:color"; 4 | 5 | box.applauncher { 6 | @include window; 7 | 8 | box.search { 9 | @include window-box; 10 | margin: 1rem 0.5rem 1rem 0.5rem; 11 | background-color: color.adjust($primaryMon, $alpha: -0.6); 12 | 13 | image { 14 | -gtk-icon-size: 2rem; 15 | margin: 0.3rem; 16 | } 17 | entry { 18 | font-size: 1.5rem; 19 | margin: 0.3rem; 20 | menu { 21 | @include menu; 22 | } 23 | } 24 | } 25 | 26 | box.apps { 27 | @include window-box; 28 | 29 | button { 30 | padding: 0.4rem; 31 | 32 | image { 33 | -gtk-icon-size: 3rem; 34 | margin-right: 0.3rem; 35 | margin-left: 0.3rem; 36 | } 37 | 38 | label.name { 39 | font-weight: bold; 40 | font-size: 1.1rem; 41 | } 42 | 43 | label.description { 44 | color: color.adjust($fg, $alpha: -0.2); 45 | } 46 | 47 | &:hover { 48 | @include button; 49 | } 50 | &:focus { 51 | @include button; 52 | @if $darkmode { 53 | background: color.adjust($buttonDisabled, $lightness: +20%); 54 | } @else { 55 | background: color.adjust($primaryMon, $lightness: +22%); 56 | } 57 | } 58 | } 59 | } 60 | 61 | box.not-found { 62 | padding: 1rem; 63 | 64 | image { 65 | -gtk-icon-size: 6rem; 66 | color: color.adjust($fg, $alpha: -0.3); 67 | } 68 | 69 | label { 70 | color: color.adjust($fg, $alpha: -0.1); 71 | font-size: 1.2em; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /style/components/_logout-menu.scss: -------------------------------------------------------------------------------- 1 | @use "../layouts" as *; 2 | @use "../abstracts" as *; 3 | @use "sass:color"; 4 | 5 | box.logout-background { 6 | background-color: $bg; 7 | .logout-menu { 8 | @include window; 9 | background-color: color.adjust($background, $alpha: -0.3); 10 | button { 11 | @include window-box; 12 | @include button; 13 | margin: 1rem; 14 | border: 1px solid rgba(0, 0, 0, 0.1); 15 | @if $darkmode { 16 | } @else { 17 | // Light mode enhancements 18 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.18); 19 | } 20 | &:focus { 21 | @if $darkmode { 22 | background: color.adjust($buttonDisabled, $lightness: +20%); 23 | } @else { 24 | background: color.adjust($primaryMon, $lightness: +22%); 25 | } 26 | } 27 | label { 28 | font-family: "Material Symbols Outlined"; 29 | font-size: 15rem; 30 | margin: 0rem 1rem; 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /style/components/_music.scss: -------------------------------------------------------------------------------- 1 | @use "../layouts" as *; 2 | @use "../abstracts" as *; 3 | 4 | .music.window { 5 | @include window; 6 | background: none; 7 | padding: 0rem; 8 | margin-top: 0.7rem; 9 | 10 | .cover { 11 | background-position: center; 12 | background-size: cover; 13 | border-radius: $round; 14 | box-shadow: 0 1px 2px -1px $bg; 15 | margin: 0.4rem; 16 | min-height: 13rem; 17 | min-width: 13rem; 18 | opacity: 0.9; 19 | } 20 | .blurred-cover { 21 | border-radius: $round; 22 | opacity: 0.9; 23 | } 24 | .cava-container { 25 | cava { 26 | opacity: 0.2; 27 | } 28 | } 29 | } 30 | 31 | .music.window .info { 32 | margin: 0.5rem; 33 | 34 | label, 35 | scale { 36 | margin: 0.3rem 0; 37 | } 38 | 39 | // Always use light colors on player text and controls. 40 | // Dark almost never works on on top of most cover arts. 41 | label, 42 | .title, 43 | image { 44 | @if $darkmode { 45 | } @else { 46 | color: $background; 47 | } 48 | } 49 | 50 | label.position, 51 | label.length { 52 | font-size: 0.8rem; 53 | margin-bottom: 0; 54 | } 55 | 56 | scale { 57 | margin-top: 0; 58 | margin-bottom: 0; 59 | } 60 | 61 | .title { 62 | font-size: 1.5rem; 63 | font-weight: bold; 64 | min-width: 14rem; 65 | } 66 | } 67 | 68 | .music.window .controls { 69 | button { 70 | margin: 0 1.6rem; 71 | -gtk-icon-size: 1.5rem; 72 | @if $darkmode { 73 | } @else { 74 | color: $background; 75 | } 76 | } 77 | } 78 | 79 | .music.window .player-info { 80 | margin-bottom: 0; 81 | 82 | .player-icon { 83 | -gtk-icon-size: $font; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /style/components/_notifications.scss: -------------------------------------------------------------------------------- 1 | @use "../layouts" as *; 2 | @use "../abstracts" as *; 3 | @use "sass:color"; 4 | 5 | .notification { 6 | margin: 5px 5px 5px 10px; 7 | min-width: 20rem; 8 | border-radius: $round2; 9 | background-color: $bg; 10 | border: 1px solid transparent; 11 | padding: 0.25rem 0.75rem; 12 | 13 | &.critical { 14 | border: 1px solid $red; 15 | } 16 | 17 | .header { 18 | .app-name { 19 | font-size: 0.9rem; 20 | font-weight: bold; 21 | color: color.adjust($fg, $alpha: -0.3); 22 | } 23 | .time { 24 | font-size: 0.9rem; 25 | font-weight: bold; 26 | color: color.adjust($fg, $alpha: -0.3); 27 | } 28 | } 29 | 30 | separator { 31 | background-color: color.adjust($fg, $alpha: -0.5); 32 | min-height: 1px; 33 | } 34 | 35 | .content { 36 | margin-top: 0.75rem; 37 | margin-bottom: 0.5rem; 38 | 39 | > box { 40 | min-width: 64px; 41 | min-height: 64px; 42 | border-radius: $round; 43 | background-size: contain; 44 | background-repeat: no-repeat; 45 | } 46 | 47 | image { 48 | -gtk-icon-size: 64px; 49 | } 50 | .thumb { 51 | margin-right: 0.5rem; 52 | } 53 | 54 | .title { 55 | margin-right: 0.5rem; 56 | font-size: 1.1rem; 57 | font-weight: 500; 58 | color: $fg; 59 | } 60 | 61 | .body { 62 | margin-right: 0.5rem; 63 | font-size: 0.95rem; 64 | color: color.adjust($fg, $alpha: -0.2); 65 | margin-top: 0.75rem; 66 | } 67 | } 68 | .actions { 69 | margin-top: 1rem; 70 | 71 | .action-button { 72 | @include window-box; 73 | @include animate; 74 | padding: 0.5rem 1rem; 75 | font-size: 0.9rem; 76 | 77 | &:hover { 78 | background: $buttonDisabledHover; 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /style/components/_osd.scss: -------------------------------------------------------------------------------- 1 | @use "../layouts" as *; 2 | @use "../abstracts" as *; 3 | 4 | .osd { 5 | @include window; 6 | border-radius: 3rem; 7 | box-shadow: none; 8 | margin: 5rem; 9 | padding: 1rem; 10 | 11 | image { 12 | -gtk-icon-size: 4rem; 13 | margin: 0.5rem; 14 | } 15 | 16 | box { 17 | margin: 0.5rem; 18 | } 19 | 20 | label { 21 | margin: 0 0 0.5rem; 22 | } 23 | 24 | levelbar { 25 | trough { 26 | margin: 0.5rem 0.5rem; 27 | } 28 | block { 29 | min-height: 1rem; 30 | } 31 | block.filled { 32 | border-radius: 1.5rem; 33 | background-color: $fg; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /style/components/_system-menu.scss: -------------------------------------------------------------------------------- 1 | @use "../layouts" as *; 2 | @use "../abstracts" as *; 3 | @use "sass:color"; 4 | 5 | /* general */ 6 | 7 | .system-menu { 8 | -gtk-icon-size: $font; 9 | @include window; 10 | margin-top: $spacing-md; 11 | margin-right: $spacing-md; 12 | 13 | & > box { 14 | @include window-box; 15 | border: 1px solid rgba(0, 0, 0, 0.1); 16 | @if $darkmode { 17 | } @else { 18 | // Light mode enhancements 19 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); 20 | } 21 | } 22 | } 23 | 24 | /* toggles */ 25 | .system-menu .toggle { 26 | min-width: 20rem; 27 | &:not(:last-child) { 28 | margin-bottom: $spacing-sm; 29 | } 30 | 31 | button { 32 | margin-right: $spacing-sm; 33 | } 34 | } 35 | 36 | /* power profiles */ 37 | .system-menu .power-profiles { 38 | padding: 0; 39 | 40 | .current-profile { 41 | padding: $spacing-sm; 42 | } 43 | 44 | image, 45 | label { 46 | margin: $spacing-sm; 47 | } 48 | 49 | .profile-options { 50 | box { 51 | padding: $spacing-sm; 52 | } 53 | } 54 | } 55 | 56 | /* sliders */ 57 | .system-menu .sliders { 58 | min-height: $spacing-sm; 59 | image { 60 | margin: $spacing-sm; 61 | } 62 | trough { 63 | margin: $spacing-sm; 64 | } 65 | block { 66 | min-height: $spacing-sm; 67 | filled { 68 | border-radius: 1.5rem; 69 | background-color: $fg; 70 | } 71 | } 72 | } 73 | 74 | .wifi-menu { 75 | } 76 | .arrow-indicator { 77 | @include animate; 78 | } 79 | 80 | .arrow-down { 81 | @include animate; 82 | transform: rotate(90deg); 83 | } 84 | .network-list { 85 | @include window; 86 | margin: $spacing-sm; 87 | } 88 | 89 | .network-item { 90 | @include button; 91 | @include rounding; 92 | margin: $spacing-xs $spacing-sm $spacing-sm $spacing-md; 93 | padding: $spacing-md; 94 | border: 1px solid rgba(0, 0, 0, 0.1); 95 | @if $darkmode { 96 | } @else { 97 | // Light mode enhancements 98 | &:hover, 99 | &:focus { 100 | border: 1px solid rgba(0, 0, 0, 0.04); 101 | } 102 | } 103 | image { 104 | &:first-child { 105 | margin-right: $spacing-sm; 106 | } 107 | } 108 | label { 109 | margin: 0 $spacing-sm; 110 | } 111 | } 112 | .network-item-connected { 113 | @include button-active; 114 | @include rounding; 115 | margin: $spacing-xs $spacing-sm $spacing-sm $spacing-md; 116 | padding: $spacing-md; 117 | } 118 | 119 | .password-dialog { 120 | @include window-box; 121 | margin-bottom: $spacing-md; 122 | } 123 | 124 | .error-message { 125 | color: $red; 126 | margin: $spacing-xs 0; 127 | } 128 | 129 | .section-label { 130 | font-weight: bold; 131 | margin: $spacing-sm 0 $spacing-xs 0; 132 | } 133 | 134 | .refresh-button, 135 | .disconnect-button, 136 | .settings-button { 137 | @include button; 138 | margin: $spacing-sm; 139 | } 140 | 141 | .disconnect-button { 142 | padding: 0.58rem 0; 143 | } 144 | 145 | .connect-button, 146 | .cancel-button, 147 | .forget-button, 148 | .pair-button, 149 | .trust-button { 150 | margin: $spacing-sm; 151 | margin-top: $spacing-sm; 152 | margin-bottom: $spacing-sm; 153 | @if $darkmode { 154 | } @else { 155 | // Light mode enhancements 156 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.18); 157 | } 158 | } 159 | image { 160 | margin: $spacing-sm; 161 | } 162 | .password-search { 163 | @include window-box; 164 | background: $bg; 165 | 166 | entry { 167 | menu { 168 | @include menu; 169 | } 170 | } 171 | } 172 | 173 | .empty-label { 174 | margin: $spacing-sm; 175 | color: $red; 176 | } 177 | 178 | .saved-network { 179 | @include window-box; 180 | border: 1px solid rgba(0, 0, 0, 0.1); 181 | margin: $spacing-sm $spacing-md $spacing-sm $spacing-md; 182 | padding: $spacing-xs; 183 | > label { 184 | margin-left: $spacing-sm; 185 | } 186 | @if $darkmode { 187 | } @else { 188 | // Light mode enhancements 189 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); 190 | } 191 | } 192 | 193 | .bt-button-container { 194 | margin: 0 $spacing-xs $spacing-sm $spacing-sm; 195 | } 196 | -------------------------------------------------------------------------------- /style/layouts/_general.scss: -------------------------------------------------------------------------------- 1 | @use "../abstracts" as *; 2 | 3 | tooltip.background, 4 | tooltip > box { 5 | background-color: $tooltipBg; 6 | border-radius: $round; 7 | box-shadow: 8 | inset 0 0 0 1px rgba(255, 255, 255, 0.1), 9 | 0 0 rgba(0, 0, 0, 0.4); 10 | label { 11 | margin: 0.1rem 0.4rem; 12 | } 13 | } 14 | 15 | /* scales */ 16 | scale { 17 | trough { 18 | background-color: $surface; 19 | border-radius: $scale; 20 | min-width: calc(#{$scale} * 10); 21 | padding: 0 calc(#{$scale} / 2); 22 | } 23 | 24 | highlight, 25 | progress { 26 | background: $overlay; 27 | border-radius: $scale; 28 | margin: 0 calc(0px - #{$scale} / 2); 29 | min-height: $scale; 30 | } 31 | } 32 | 33 | /* popovers */ 34 | popover { 35 | background: $tooltipBg; 36 | border-radius: $round; 37 | font-size: $font; 38 | modelbutton { 39 | @include button; 40 | border-radius: 0; 41 | padding: 0.4rem 0.7rem; 42 | 43 | &:first-child { 44 | border-radius: $round $round 0 0; 45 | } 46 | &:last-child { 47 | border-radius: 0 0 $round $round; 48 | } 49 | &:only-child { 50 | border-radius: $round; 51 | } 52 | } 53 | } 54 | 55 | /* buttons */ 56 | .button { 57 | @include button-active; 58 | } 59 | 60 | .button-disabled { 61 | @include button; 62 | } 63 | -------------------------------------------------------------------------------- /style/layouts/_index.scss: -------------------------------------------------------------------------------- 1 | @forward "general"; 2 | -------------------------------------------------------------------------------- /style/main.scss: -------------------------------------------------------------------------------- 1 | 2 | /* prelude */ 3 | @use "base" as *; 4 | 5 | /* variables & mixins */ 6 | @use "abstracts" as *; 7 | 8 | /* general styles */ 9 | @use "layouts" as *; 10 | 11 | /* modules & windows */ 12 | @use "components" as *; 13 | 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "allowImportingTsExtensions": true, 5 | "allowJs": true, 6 | "baseUrl": ".", 7 | "experimentalDecorators": true, 8 | "jsx": "react-jsx", 9 | "jsxImportSource": "astal/gtk4", 10 | "module": "ES2022", 11 | "moduleResolution": "Bundler", 12 | "noImplicitAny": false, 13 | "paths": { 14 | "astal": [ 15 | "${pkgs.astal3}/share/astal/gjs" 16 | ], 17 | "astal/*": [ 18 | "${pkgs.astal3}/share/astal/gjs/*" 19 | ] 20 | }, 21 | "strict": true, 22 | "target": "ES2022" 23 | } 24 | } -------------------------------------------------------------------------------- /utils/battery.ts: -------------------------------------------------------------------------------- 1 | export const toTime = (time: number) => { 2 | const MINUTE = 60; 3 | const HOUR = MINUTE * 60; 4 | 5 | if (time > 24 * HOUR) return ""; 6 | 7 | const hours = Math.round(time / HOUR); 8 | const minutes = Math.round((time - hours * HOUR) / MINUTE); 9 | 10 | const hoursDisplay = hours > 0 ? `${hours}h ` : ""; 11 | const minutesDisplay = minutes > 0 ? `${minutes}m ` : ""; 12 | 13 | return `${hoursDisplay}${minutesDisplay}`; 14 | }; 15 | -------------------------------------------------------------------------------- /utils/bluetooth-agent.ts: -------------------------------------------------------------------------------- 1 | /* ONLY allows NOINPUTNOOUTPUT! 2 | If you want to pair devices and access features 3 | which require more auth, use something like overskride. */ 4 | import Gio from "gi://Gio"; 5 | import GLib from "gi://GLib"; 6 | 7 | // D-Bus interface constants 8 | const AGENT_PATH = "/org/bluez/agent"; 9 | const CAPABILITY = "NoInputNoOutput"; 10 | const BLUEZ_SERVICE = "org.bluez"; 11 | const AGENT_MANAGER_INTERFACE = "org.bluez.AgentManager1"; 12 | 13 | function logMessage(message: string): void { 14 | console.log(`[Bluetooth Agent] ${message}`); 15 | } 16 | 17 | export class BluetoothAgent { 18 | private connection: Gio.DBusConnection; 19 | private registrationId: number = 0; 20 | private isRegistered: boolean = false; 21 | 22 | constructor() { 23 | this.connection = Gio.DBus.system; 24 | } 25 | 26 | // D-Bus method handler 27 | private handleMethodCall = ( 28 | methodName: string, 29 | invocation: Gio.DBusMethodInvocation, 30 | ): void => { 31 | logMessage(`Handling method: ${methodName}`); 32 | 33 | try { 34 | switch (methodName) { 35 | case "Release": 36 | case "Cancel": 37 | case "DisplayPasskey": 38 | // These methods don't require any response beyond acknowledgment 39 | invocation.return_value(null); 40 | break; 41 | 42 | default: 43 | // With NoInputNoOutput capability, methods requiring user input 44 | // should never be called. If they are, reject them. 45 | logMessage(`Unexpected method call: ${methodName}`); 46 | invocation.return_error_literal( 47 | "org.bluez.Error.Rejected", 48 | "This agent only supports automatic pairing", 49 | ); 50 | } 51 | } catch (error) { 52 | logMessage(`Error handling method ${methodName}: ${error}`); 53 | invocation.return_error_literal( 54 | "org.bluez.Error.Failed", 55 | `Error handling request: ${error}`, 56 | ); 57 | } 58 | }; 59 | 60 | // Register the agent on D-Bus 61 | register(): boolean { 62 | if (this.isRegistered) return true; 63 | 64 | try { 65 | // Define introspection XML for the agent interface 66 | const introspectionXml = ` 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | `; 79 | 80 | // Register the agent object 81 | const nodeInfo = Gio.DBusNodeInfo.new_for_xml(introspectionXml); 82 | this.registrationId = this.connection.register_object( 83 | AGENT_PATH, 84 | nodeInfo.interfaces[0], 85 | this.handleMethodCall, 86 | null, 87 | null, 88 | ); 89 | 90 | if (this.registrationId === 0) { 91 | logMessage("Failed to register agent object"); 92 | return false; 93 | } 94 | 95 | // Register with BlueZ 96 | this.connection.call( 97 | BLUEZ_SERVICE, 98 | "/org/bluez", 99 | AGENT_MANAGER_INTERFACE, 100 | "RegisterAgent", 101 | new GLib.Variant("(os)", [AGENT_PATH, CAPABILITY]), 102 | null, 103 | Gio.DBusCallFlags.NONE, 104 | -1, 105 | null, 106 | (connection, res) => { 107 | try { 108 | connection.call_finish(res); 109 | this.setDefaultAgent(); 110 | logMessage("Agent registered successfully"); 111 | } catch (error) { 112 | logMessage(`Error registering agent: ${error}`); 113 | this.isRegistered = false; 114 | } 115 | }, 116 | ); 117 | 118 | this.isRegistered = true; 119 | return true; 120 | } catch (error) { 121 | logMessage(`Error setting up agent: ${error}`); 122 | if (this.registrationId !== 0) { 123 | this.connection.unregister_object(this.registrationId); 124 | this.registrationId = 0; 125 | } 126 | return false; 127 | } 128 | } 129 | 130 | // Set as default agent 131 | private setDefaultAgent(): void { 132 | this.connection.call( 133 | BLUEZ_SERVICE, 134 | "/org/bluez", 135 | AGENT_MANAGER_INTERFACE, 136 | "RequestDefaultAgent", 137 | new GLib.Variant("(o)", [AGENT_PATH]), 138 | null, 139 | Gio.DBusCallFlags.NONE, 140 | -1, 141 | null, 142 | (connection, res) => { 143 | try { 144 | connection.call_finish(res); 145 | logMessage("Agent set as default successfully"); 146 | } catch (error) { 147 | logMessage(`Error setting agent as default: ${error}`); 148 | } 149 | }, 150 | ); 151 | } 152 | 153 | // Unregister the agent 154 | unregister(): boolean { 155 | if (!this.isRegistered) return true; 156 | 157 | try { 158 | // Unregister from BlueZ 159 | this.connection.call( 160 | BLUEZ_SERVICE, 161 | "/org/bluez", 162 | AGENT_MANAGER_INTERFACE, 163 | "UnregisterAgent", 164 | new GLib.Variant("(o)", [AGENT_PATH]), 165 | null, 166 | Gio.DBusCallFlags.NONE, 167 | -1, 168 | null, 169 | (connection, res) => { 170 | try { 171 | connection.call_finish(res); 172 | logMessage("Agent unregistered successfully"); 173 | } catch (error) { 174 | logMessage(`Error unregistering agent: ${error}`); 175 | } 176 | }, 177 | ); 178 | 179 | // Unregister object 180 | if (this.registrationId !== 0) { 181 | this.connection.unregister_object(this.registrationId); 182 | this.registrationId = 0; 183 | } 184 | 185 | this.isRegistered = false; 186 | return true; 187 | } catch (error) { 188 | logMessage(`Error unregistering agent: ${error}`); 189 | return false; 190 | } 191 | } 192 | } 193 | 194 | // Create and register agent 195 | export function startBluetoothAgent() { 196 | const agent = new BluetoothAgent(); 197 | if (agent.register()) return agent; 198 | 199 | return null; 200 | } 201 | -------------------------------------------------------------------------------- /utils/bluetooth.ts: -------------------------------------------------------------------------------- 1 | import Bluetooth from "gi://AstalBluetooth"; 2 | import { startBluetoothAgent, BluetoothAgent } from "./bluetooth-agent.ts"; 3 | import { Variable, bind, timeout } from "astal"; 4 | 5 | export const isExpanded = Variable(false); 6 | export const refreshIntervalId = Variable(null); 7 | export const selectedDevice = Variable(null); 8 | export const isConnecting = Variable(false); 9 | export const errorMessage = Variable(""); 10 | const bluetoothAgent = Variable(null); 11 | const hasBluetoothAgent = Variable(false); 12 | 13 | export const getBluetoothIcon = (bt: Bluetooth.Bluetooth) => { 14 | if (!bt.is_powered) return "bluetooth-disabled-symbolic"; 15 | if (bt.is_connected) return "bluetooth-active-symbolic"; 16 | return "bluetooth-disconnected-symbolic"; 17 | }; 18 | 19 | export const getBluetoothText = (bt: Bluetooth.Bluetooth) => { 20 | if (!bt.is_powered) return "Bluetooth off"; 21 | return "Bluetooth on"; 22 | }; 23 | 24 | export const getBluetoothDeviceText = (device) => { 25 | { 26 | let battery_str = ""; 27 | if (device.connected && device.battery_percentage > 0) { 28 | battery_str = ` ${device.battery_percentage * 100}%`; 29 | } 30 | return `${device.name} ${battery_str}`; 31 | } 32 | }; 33 | 34 | export const ensureBluetoothAgent = () => { 35 | if (bluetoothAgent.get() === null) { 36 | console.log("Starting Bluetooth agent"); 37 | bluetoothAgent.set(startBluetoothAgent()); 38 | } 39 | }; 40 | 41 | export const stopBluetoothAgent = () => { 42 | const agent = bluetoothAgent.get(); 43 | if (agent) { 44 | console.log("Stopping Bluetooth agent"); 45 | if (agent.unregister()) { 46 | console.log("Bluetooth agent stopped successfully"); 47 | bluetoothAgent.set(null); 48 | hasBluetoothAgent.set(false); 49 | return true; 50 | } else { 51 | console.error("Failed to stop Bluetooth agent"); 52 | return false; 53 | } 54 | } 55 | return true; // No agent running 56 | }; 57 | 58 | // Scanning functions 59 | export const scanDevices = () => { 60 | const bluetooth = Bluetooth.get_default(); 61 | bluetooth && bluetooth.adapter && bluetooth.adapter.start_discovery(); 62 | }; 63 | 64 | export const stopScan = () => { 65 | const bluetooth = Bluetooth.get_default(); 66 | bluetooth && bluetooth.adapter && bluetooth.adapter.stop_discovery(); 67 | }; 68 | 69 | // Device interaction functions 70 | export const connectToDevice = (device) => { 71 | if (!device) return; 72 | 73 | isConnecting.set(true); 74 | device.connect_device(() => { 75 | isConnecting.set(false); 76 | }); 77 | }; 78 | 79 | export const disconnectDevice = (device) => { 80 | if (!device) return; 81 | 82 | device.disconnect_device(() => { 83 | console.log(`Successfully disconnected with ${device.name}`); 84 | }); 85 | }; 86 | 87 | export const pairDevice = (device) => { 88 | if (!device) return; 89 | 90 | // Start agent if not running 91 | let agentWasStarted = false; 92 | if (!hasBluetoothAgent.get()) { 93 | ensureBluetoothAgent(); 94 | agentWasStarted = true; 95 | } 96 | 97 | // Create a binding for the paired state 98 | const pairedBinding = bind(device, "paired"); 99 | 100 | // Set up cleanup to run when paired becomes true 101 | const unsubscribe = pairedBinding.subscribe((paired) => { 102 | if (paired) { 103 | console.log(`Successfully paired with ${device.name}`); 104 | 105 | // Unsubscribe to prevent memory leaks 106 | unsubscribe(); 107 | 108 | // Stop the agent when paired 109 | if (agentWasStarted) { 110 | console.log("Pairing successful, stopping Bluetooth agent"); 111 | timeout(1000, () => { 112 | stopBluetoothAgent(); 113 | }); 114 | } 115 | } 116 | }); 117 | 118 | // Set up timeout for pairing process 119 | timeout(30000, () => { 120 | console.log("Pairing timeout reached"); 121 | unsubscribe(); 122 | 123 | agentWasStarted && stopBluetoothAgent(); 124 | }); 125 | 126 | try { 127 | console.log(`Initiating pairing with ${device.name}`); 128 | device.pair(); 129 | } catch (error) { 130 | console.error("Error pairing device:", error); 131 | unsubscribe(); 132 | 133 | agentWasStarted && stopBluetoothAgent(); 134 | } 135 | }; 136 | 137 | export const unpairDevice = (device) => { 138 | const bluetooth = Bluetooth.get_default(); 139 | bluetooth && bluetooth.adapter && bluetooth.adapter.remove_device(device); 140 | }; 141 | 142 | export const toggleTrust = (device) => { 143 | if (!device) return; 144 | device.set_trusted(!device.trusted); 145 | }; 146 | -------------------------------------------------------------------------------- /utils/brightness.ts: -------------------------------------------------------------------------------- 1 | import GObject, { register, property } from "astal/gobject"; 2 | import { monitorFile, readFileAsync } from "astal/file"; 3 | import { exec, execAsync } from "astal/process"; 4 | 5 | const get = (args: string) => Number(exec(`brightnessctl ${args}`)); 6 | const screen = exec(`bash -c "ls -w1 /sys/class/backlight | head -1"`); 7 | const kbd = exec(`bash -c "ls -w1 /sys/class/leds | head -1"`); 8 | 9 | @register({ GTypeName: "Brightness" }) 10 | export default class Brightness extends GObject.Object { 11 | static instance: Brightness; 12 | static get_default() { 13 | if (!this.instance) this.instance = new Brightness(); 14 | return this.instance; 15 | } 16 | 17 | #hasBacklight = false; 18 | #kbdMax = 0; 19 | #kbd = 0; 20 | #screenMax = 0; 21 | #screen = 0; 22 | 23 | constructor() { 24 | super(); 25 | 26 | this.#hasBacklight = 27 | exec(`bash -c "ls /sys/class/backlight"`).length > 0; 28 | 29 | // Do not initialize without backlight, use on notebooks only. 30 | if (!this.#hasBacklight) return; 31 | 32 | // Initialize values 33 | this.#kbdMax = get(`--device ${kbd} max`); 34 | this.#kbd = get(`--device ${kbd} get`); 35 | this.#screenMax = get("max"); 36 | this.#screen = get("get") / (get("max") || 1); 37 | 38 | // Setup file monitoring 39 | monitorFile( 40 | `/sys/class/backlight/${screen}/brightness`, 41 | async (f) => { 42 | const v = await readFileAsync(f); 43 | this.#screen = Number(v) / this.#screenMax; 44 | this.notify("screen"); 45 | }, 46 | ); 47 | 48 | monitorFile(`/sys/class/leds/${kbd}/brightness`, async (f) => { 49 | const v = await readFileAsync(f); 50 | this.#kbd = Number(v); 51 | this.notify("kbd"); 52 | }); 53 | } 54 | @property(Boolean) 55 | get hasBacklight() { 56 | return this.#hasBacklight; 57 | } 58 | 59 | @property(Number) 60 | get kbd() { 61 | return this.#kbd; 62 | } 63 | 64 | set kbd(value) { 65 | if (value < 0 || value > this.#kbdMax) return; 66 | execAsync(`brightnessctl -d ${kbd} s ${value} -q`).then(() => { 67 | this.#kbd = value; 68 | this.notify("kbd"); 69 | }); 70 | } 71 | 72 | @property(Number) 73 | get screen() { 74 | return this.#screen; 75 | } 76 | 77 | set screen(percent) { 78 | percent = Math.max(0, Math.min(1, percent)); 79 | execAsync( 80 | `brightnessctl -d ${screen} set ${Math.floor(percent * 100)}% -q`, 81 | ).then(() => { 82 | this.#screen = percent; 83 | this.notify("screen"); 84 | }); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /utils/hwmonitor.ts: -------------------------------------------------------------------------------- 1 | import GObject, { register, property } from "astal/gobject"; 2 | import GLib from "gi://GLib"; 3 | import GTop from "gi://GTop"; 4 | import { readFile } from "astal/file"; 5 | 6 | @register({ GTypeName: "SystemMonitor" }) 7 | export default class SystemMonitor extends GObject.Object { 8 | static instance: SystemMonitor; 9 | private static readonly CPU_INFO_PATH = "/proc/cpuinfo"; 10 | 11 | // On lower intervals the tooltips start to take forever to show 12 | private static readonly UPDATE_INTERVAL = 1000; 13 | private static readonly BYTE_UNITS = ["B", "KB", "MB", "GB", "TB"]; 14 | 15 | // State tracking 16 | #memory = new GTop.glibtop_mem(); 17 | #cpuLoad = 0; 18 | #lastUsed = 0; 19 | #lastTotal = 0; 20 | #cpuFreq = 0; 21 | 22 | // Notification batching 23 | #pendingNotifications = new Set(); 24 | #notifyTimeoutId: number | null = null; 25 | 26 | static get_default(): SystemMonitor { 27 | return this.instance || (this.instance = new SystemMonitor()); 28 | } 29 | 30 | constructor() { 31 | super(); 32 | this.initializeBaseMetrics(); 33 | this.startMonitoring(); 34 | } 35 | 36 | private initializeBaseMetrics(): void { 37 | GTop.glibtop_get_mem(this.#memory); 38 | const initialCpu = new GTop.glibtop_cpu(); 39 | GTop.glibtop_get_cpu(initialCpu); 40 | this.#lastUsed = this.calculateCpuUsed(initialCpu); 41 | this.#lastTotal = this.calculateCpuTotal(initialCpu); 42 | } 43 | 44 | private startMonitoring(): void { 45 | // Use GTK4's idle priority for better integration with event loop 46 | GLib.timeout_add(GLib.PRIORITY_LOW, SystemMonitor.UPDATE_INTERVAL, () => { 47 | this.updateMetrics(); 48 | return GLib.SOURCE_CONTINUE; 49 | }); 50 | } 51 | 52 | // Unified update method to minimize main thread blocking 53 | private updateMetrics(): void { 54 | // Update CPU metrics 55 | const currentCpu = new GTop.glibtop_cpu(); 56 | GTop.glibtop_get_cpu(currentCpu); 57 | 58 | const currentUsed = this.calculateCpuUsed(currentCpu); 59 | const currentTotal = this.calculateCpuTotal(currentCpu); 60 | const [diffUsed, diffTotal] = [ 61 | currentUsed - this.#lastUsed, 62 | currentTotal - this.#lastTotal, 63 | ]; 64 | 65 | this.#cpuLoad = 66 | diffTotal > 0 ? Math.min(1, Math.max(0, diffUsed / diffTotal)) : 0; 67 | this.#lastUsed = currentUsed; 68 | this.#lastTotal = currentTotal; 69 | 70 | // Update memory metrics 71 | GTop.glibtop_get_mem(this.#memory); 72 | 73 | // Update CPU frequency 74 | try { 75 | const frequencies = this.parseCpuFrequencies(); 76 | if (frequencies.length > 0) { 77 | this.#cpuFreq = 78 | frequencies.reduce((a, b) => a + b, 0) / frequencies.length; 79 | } 80 | } catch (error) { 81 | console.error(`CPU frequency update failed: ${error}`); 82 | } 83 | 84 | // Queue all notifications in batch 85 | this.queueNotifications([ 86 | "cpu-load", 87 | "memory-used", 88 | "memory-utilization", 89 | "cpu-frequency", 90 | ]); 91 | } 92 | 93 | // Batch notification method 94 | private queueNotifications(properties: string[]): void { 95 | for (const prop of properties) { 96 | this.#pendingNotifications.add(prop); 97 | } 98 | 99 | if (this.#notifyTimeoutId === null) { 100 | // Process notifications on next frame for better GTK4 integration 101 | this.#notifyTimeoutId = GLib.timeout_add( 102 | GLib.PRIORITY_DEFAULT_IDLE, 103 | 100, 104 | () => { 105 | this.processNotifications(); 106 | this.#notifyTimeoutId = null; 107 | return GLib.SOURCE_REMOVE; 108 | }, 109 | ); 110 | } 111 | } 112 | 113 | private processNotifications(): void { 114 | for (const prop of this.#pendingNotifications) { 115 | this.notify(prop); 116 | } 117 | this.#pendingNotifications.clear(); 118 | } 119 | 120 | private parseCpuFrequencies(): number[] { 121 | return readFile(SystemMonitor.CPU_INFO_PATH) 122 | .split("\n") 123 | .filter((line) => line.includes("cpu MHz")) 124 | .map((line) => { 125 | const value = line.split(":")[1]?.trim(); 126 | return value ? parseFloat(value) : NaN; 127 | }) 128 | .filter((freq) => !isNaN(freq)); 129 | } 130 | 131 | // Helper functions 132 | private calculateCpuUsed(cpu: GTop.glibtop_cpu): number { 133 | return cpu.user + cpu.sys + cpu.nice + cpu.irq + cpu.softirq; 134 | } 135 | 136 | private calculateCpuTotal(cpu: GTop.glibtop_cpu): number { 137 | return this.calculateCpuUsed(cpu) + cpu.idle + cpu.iowait; 138 | } 139 | 140 | private formatBytes(bytes: number): string { 141 | if (bytes === 0) return "0 B"; 142 | const exp = Math.floor(Math.log(bytes) / Math.log(1024)); 143 | const value = bytes / Math.pow(1024, exp); 144 | return `${Math.round(value * 100) / 100} ${SystemMonitor.BYTE_UNITS[exp]}`; 145 | } 146 | 147 | // Property getters 148 | @property(Number) 149 | get memoryUtilization(): number { 150 | return this.#memory.user / this.#memory.total; 151 | } 152 | 153 | @property(String) 154 | get memoryUsed(): string { 155 | return this.formatBytes(this.#memory.user); 156 | } 157 | 158 | @property(Number) 159 | get cpuLoad(): number { 160 | return this.#cpuLoad; 161 | } 162 | 163 | @property(Number) 164 | get cpuFrequency(): number { 165 | return Math.round(this.#cpuFreq); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /utils/hyprland.ts: -------------------------------------------------------------------------------- 1 | import { App, Gdk } from "astal/gtk4"; 2 | import Hyprland from "gi://AstalHyprland"; 3 | 4 | /* Match Hyprland monitor to GDK monitor 5 | THIS MAY NOT WORK AS INTENDED IF YOU HAVE MONITORS OF THE SAME MODEL 6 | I did not find a more elegant solution to this. 7 | On my setup GDK coordinates and hyprland coordinates are flipped, 8 | so I cant match by coordinates. */ 9 | 10 | export function hyprToGdk(monitor: Hyprland.Monitor): Gdk.Monitor | null { 11 | const monitors = App.get_monitors(); 12 | if (!monitors || monitors.length === 0) return null; 13 | 14 | for (let gdkmonitor of monitors) { 15 | if ( 16 | monitor && 17 | gdkmonitor && 18 | monitor.get_name() === gdkmonitor.get_connector() 19 | ) 20 | return gdkmonitor; 21 | } 22 | 23 | // Default monitor with null safety 24 | return monitors.length > 0 ? monitors[0] : null; 25 | } 26 | -------------------------------------------------------------------------------- /utils/mpris.ts: -------------------------------------------------------------------------------- 1 | import Mpris from "gi://AstalMpris"; 2 | import GLib from "gi://GLib"; 3 | import { exec, bind } from "astal"; 4 | 5 | const mpris = Mpris.get_default(); 6 | const MEDIA_CACHE_PATH = GLib.get_user_cache_dir() + "/media"; 7 | const blurredPath = MEDIA_CACHE_PATH + "/blurred"; 8 | 9 | export function findPlayer(players: Mpris.Player[]): Mpris.Player | undefined { 10 | // try to get the first active player 11 | const activePlayer = players.find( 12 | (p) => p.playback_status === Mpris.PlaybackStatus.PLAYING, 13 | ); 14 | if (activePlayer) return activePlayer; 15 | 16 | // otherwise get the first "working" player 17 | return players.find((p) => p.title !== undefined); 18 | } 19 | 20 | export function mprisStateIcon(status: Mpris.PlaybackStatus): string { 21 | return status === Mpris.PlaybackStatus.PLAYING 22 | ? "media-playback-pause-symbolic" 23 | : "media-playback-start-symbolic"; 24 | } 25 | 26 | export function generateBackground(coverpath: string | null): string { 27 | if (!coverpath) return ""; 28 | 29 | // Construct blurred path using path.join for safe concatenation 30 | const relativePath = coverpath.substring(MEDIA_CACHE_PATH.length + 1); // +1 to skip slash 31 | const blurred = GLib.build_filenamev([blurredPath, relativePath]); 32 | 33 | // Create parent directory for blurred file 34 | const blurredDir = GLib.path_get_dirname(blurred); 35 | !GLib.file_test(blurredDir, GLib.FileTest.EXISTS) && 36 | GLib.mkdir_with_parents(blurredDir, 0o755); 37 | 38 | try { 39 | // Using async can cause race condition and idk how to use 40 | // this function in a binding if the entire function is async. 41 | exec(`magick "${coverpath}" -blur 0x22 "${blurred}"`); 42 | } catch (e) { 43 | console.error("Background generation failed:", e); 44 | return ""; // Fallback 45 | } 46 | return blurred; 47 | } 48 | 49 | export function lengthStr(length: number) { 50 | const min = Math.floor(length / 60).toString(); 51 | const sec = Math.floor(length % 60) 52 | .toString() 53 | .padStart(2, "0"); 54 | return min + ":" + sec; 55 | } 56 | 57 | export function filterActivePlayers(players) { 58 | return players.filter((player) => { 59 | // Check for essential properties that indicate a usable player 60 | if (!player.title && !player.artist) { 61 | return false; 62 | } 63 | 64 | // Check playback status 65 | // Only include players that are playing or paused 66 | if (player.playback_status) { 67 | return [ 68 | Mpris.PlaybackStatus.PLAYING, 69 | Mpris.PlaybackStatus.PAUSED, 70 | ].includes(player.playback_status); 71 | } 72 | 73 | return true; 74 | }); 75 | } 76 | 77 | export const hasActivePlayers = bind(mpris, "players").as( 78 | (players) => filterActivePlayers(players).length > 0, 79 | ); 80 | export const firstActivePlayer = bind(mpris, "players").as((players) => { 81 | const active = filterActivePlayers(players); 82 | return active.length > 0 ? active[0] : null; 83 | }); 84 | -------------------------------------------------------------------------------- /utils/notifd.ts: -------------------------------------------------------------------------------- 1 | import Notifd from "gi://AstalNotifd"; 2 | import { GLib } from "astal"; 3 | import { Gtk, Gdk } from "astal/gtk4"; 4 | 5 | type TimeoutManager = { 6 | setupTimeout: () => void; 7 | clearTimeout: () => void; 8 | handleHover: () => void; 9 | handleHoverLost: () => void; 10 | cleanup: () => void; 11 | }; 12 | 13 | export const createTimeoutManager = ( 14 | dismissCallback: () => void, 15 | timeoutDelay: number, 16 | ): TimeoutManager => { 17 | let isHovered = false; 18 | let timeoutId: number | null = null; 19 | 20 | const clearTimeout = () => { 21 | if (timeoutId !== null) { 22 | GLib.source_remove(timeoutId); 23 | timeoutId = null; 24 | } 25 | }; 26 | 27 | const setupTimeout = () => { 28 | clearTimeout(); 29 | 30 | if (!isHovered) { 31 | timeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, timeoutDelay, () => { 32 | clearTimeout(); 33 | dismissCallback(); 34 | return GLib.SOURCE_REMOVE; 35 | }); 36 | } 37 | }; 38 | 39 | return { 40 | setupTimeout, 41 | clearTimeout, 42 | handleHover: () => { 43 | isHovered = true; 44 | clearTimeout(); 45 | }, 46 | handleHoverLost: () => { 47 | isHovered = false; 48 | setupTimeout(); 49 | }, 50 | cleanup: clearTimeout, 51 | }; 52 | }; 53 | 54 | export const time = (time: number, format = "%H:%M") => 55 | GLib.DateTime.new_from_unix_local(time).format(format)!; 56 | 57 | export const urgency = (notification: Notifd.Notification) => { 58 | const { LOW, NORMAL, CRITICAL } = Notifd.Urgency; 59 | 60 | switch (notification.urgency) { 61 | case LOW: 62 | return "low"; 63 | case CRITICAL: 64 | return "critical"; 65 | case NORMAL: 66 | default: 67 | return "normal"; 68 | } 69 | }; 70 | 71 | export const isIcon = (icon: string) => { 72 | const display = Gdk.Display.get_default(); 73 | if (!display) return false; 74 | const iconTheme = Gtk.IconTheme.get_for_display(display); 75 | return iconTheme.has_icon(icon); 76 | }; 77 | 78 | export const fileExists = (path: string) => 79 | GLib.file_test(path, GLib.FileTest.EXISTS); 80 | -------------------------------------------------------------------------------- /utils/osd.ts: -------------------------------------------------------------------------------- 1 | import Variable from "astal/variable"; 2 | import { timeout } from "astal/time"; 3 | 4 | type OSDParams = { 5 | visible: Variable; 6 | value: Variable; 7 | label: Variable; 8 | icon: Variable; 9 | showProgress: Variable; 10 | timeoutMs?: number; 11 | }; 12 | 13 | export default class OSDManager { 14 | private count = 0; 15 | private timeoutMs: number; 16 | 17 | constructor(private params: OSDParams) { 18 | this.timeoutMs = params.timeoutMs || 2000; 19 | } 20 | 21 | show(value: number, label: string, icon: string, progress = true) { 22 | this.params.visible.set(true); 23 | this.params.value.set(value); 24 | this.params.label.set(label); 25 | this.params.icon.set(icon); 26 | this.params.showProgress.set(progress); 27 | this.count++; 28 | 29 | timeout(this.timeoutMs, () => { 30 | this.count--; 31 | this.count === 0 && this.params.visible.set(false); 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /utils/wifi.ts: -------------------------------------------------------------------------------- 1 | import { execAsync, Variable } from "astal"; 2 | import Network from "gi://AstalNetwork"; 3 | 4 | // State trackers 5 | export const availableNetworks = Variable([]); 6 | export const savedNetworks = Variable([]); 7 | export const activeNetwork = Variable(null); 8 | export const isConnecting = Variable(false); 9 | export const showPasswordDialog = Variable(false); 10 | export const errorMessage = Variable(""); 11 | export const isExpanded = Variable(false); 12 | export const passwordInput = Variable(""); 13 | export const selectedNetwork = Variable(null); 14 | export const scanTimer = Variable(null); 15 | 16 | // Function to scan for available networks 17 | export const scanNetworks = () => { 18 | const network = Network.get_default(); 19 | if (network && network.wifi) { 20 | network.wifi.scan(); 21 | 22 | // Get available networks from access points 23 | const networks = network.wifi.accessPoints 24 | .map((ap) => ({ 25 | ssid: ap.ssid, 26 | strength: ap.strength, 27 | secured: ap.flags !== 0, 28 | active: network.wifi.activeAccessPoint?.ssid === ap.ssid, 29 | accessPoint: ap, 30 | iconName: ap.iconName, 31 | })) 32 | .filter((n) => n.ssid); 33 | 34 | // Sort by signal strength 35 | networks.sort((a, b) => b.strength - a.strength); 36 | 37 | // Remove duplicates (same SSID) 38 | const uniqueNetworks = []; 39 | const seen = new Set(); 40 | networks.forEach((network) => { 41 | if (!seen.has(network.ssid)) { 42 | seen.add(network.ssid); 43 | uniqueNetworks.push(network); 44 | } 45 | }); 46 | 47 | availableNetworks.set(uniqueNetworks); 48 | 49 | // Update active network 50 | network.wifi.activeAccessPoint 51 | ? activeNetwork.set({ 52 | ssid: network.wifi.activeAccessPoint.ssid, 53 | strength: network.wifi.activeAccessPoint.strength, 54 | secured: network.wifi.activeAccessPoint.flags !== 0, 55 | }) 56 | : activeNetwork.set(null); 57 | } 58 | }; 59 | 60 | // Function to list saved networks 61 | export const getSavedNetworks = () => { 62 | execAsync(["bash", "-c", "nmcli -t -f NAME,TYPE connection show"]) 63 | .then((output) => { 64 | if (typeof output === "string") { 65 | const savedWifiNetworks = output 66 | .split("\n") 67 | .filter((line) => line.includes("802-11-wireless")) 68 | .map((line) => line.split(":")[0].trim()); 69 | savedNetworks.set(savedWifiNetworks); 70 | } 71 | }) 72 | .catch((error) => console.error("Error fetching saved networks:", error)); 73 | }; 74 | 75 | // Function to connect to a network 76 | 77 | export const connectToNetwork = (ssid, password = null) => { 78 | isConnecting.set(true); 79 | errorMessage.set(""); 80 | const network = Network.get_default(); 81 | const currentSsid = network.wifi.ssid; 82 | 83 | const performConnection = () => { 84 | let command = ""; 85 | password 86 | ? (command = `echo '${password}' | nmcli device wifi connect "${ssid}" --ask`) 87 | : (command = `nmcli connection up "${ssid}" || nmcli device wifi connect "${ssid}"`); 88 | 89 | execAsync(["bash", "-c", command]) 90 | .then(() => { 91 | showPasswordDialog.set(false); 92 | isConnecting.set(false); 93 | scanNetworks(); 94 | getSavedNetworks(); 95 | }) 96 | .catch((error) => { 97 | console.error("Connection error:", error); 98 | isConnecting.set(false); 99 | errorMessage.set("Check Password"); 100 | 101 | // Immediately remove network again when the connection failed 102 | execAsync(["bash", "-c", `nmcli connection show "${ssid}" 2>/dev/null`]) 103 | .then((output) => { 104 | output && forgetNetwork(ssid); 105 | }) 106 | .catch(() => { 107 | // Network wasn't saved (desired) 108 | }); 109 | }); 110 | }; 111 | 112 | // If already connected to a network, disconnect first 113 | if (currentSsid && currentSsid !== ssid) { 114 | console.log( 115 | `Disconnecting from ${currentSsid} before connecting to ${ssid}`, 116 | ); 117 | execAsync(["bash", "-c", `nmcli connection down "${currentSsid}"`]) 118 | .then(() => { 119 | // Wait a moment for the disconnection to complete fully 120 | setTimeout(() => { 121 | performConnection(); 122 | }, 500); // 500ms delay for clean disconnection 123 | }) 124 | .catch((error) => { 125 | console.error("Disconnect error:", error); 126 | // Continue with connection attempt even if disconnect fails 127 | performConnection(); 128 | }); 129 | } else { 130 | // No active connection or connecting to same network (reconnect case) 131 | performConnection(); 132 | } 133 | }; 134 | 135 | // Function to disconnect from a network 136 | export const disconnectNetwork = (ssid) => { 137 | execAsync(["bash", "-c", `nmcli connection down "${ssid}"`]) 138 | .then(() => { 139 | scanNetworks(); // Refresh network list 140 | }) 141 | .catch((error) => { 142 | console.error("Disconnect error:", error); 143 | }); 144 | }; 145 | 146 | // Function to forget a saved network 147 | export const forgetNetwork = (ssid) => { 148 | execAsync(["bash", "-c", `nmcli connection delete "${ssid}"`]) 149 | .then(() => { 150 | getSavedNetworks(); // Refresh saved networks list 151 | scanNetworks(); // Refresh network list 152 | }) 153 | .catch((error) => { 154 | console.error("Forget network error:", error); 155 | }); 156 | }; 157 | -------------------------------------------------------------------------------- /widgets/bar/main.tsx: -------------------------------------------------------------------------------- 1 | import { App, Astal, Gtk, Gdk } from "astal/gtk4"; 2 | import { bind } from "astal"; 3 | import { SysTray, hasTrayItems } from "./modules/SysTray.tsx"; 4 | import Separator from "./modules/Separator.tsx"; 5 | import Workspaces from "./modules/Workspaces.tsx"; 6 | import Mem from "./modules/Mem.tsx"; 7 | import Cpu from "./modules/Cpu.tsx"; 8 | import { CavaDraw } from "widgets/music/modules/cava"; 9 | import Media from "./modules/Media.tsx"; 10 | import { hasActivePlayers } from "utils/mpris.ts"; 11 | import SystemInfo from "./modules/SystemInfo/main.tsx"; 12 | import Time from "./modules/Time.tsx/"; 13 | import OsIcon from "./modules/OsIcon.tsx"; 14 | import options from "options.ts"; 15 | 16 | function Bar({ gdkmonitor, ...props }: any) { 17 | console.log("Bar initialization started"); 18 | 19 | const { TOP, LEFT, RIGHT, BOTTOM } = Astal.WindowAnchor; 20 | 21 | return ( 22 | { 27 | return ["Bar", `bar-style-${style}`]; 28 | })} 29 | gdkmonitor={gdkmonitor} 30 | exclusivity={Astal.Exclusivity.EXCLUSIVE} 31 | application={App} 32 | anchor={bind(options["bar.position"]).as((pos) => { 33 | switch (pos) { 34 | case "top": 35 | return TOP | LEFT | RIGHT; 36 | case "bottom": 37 | return BOTTOM | LEFT | RIGHT; 38 | default: 39 | return TOP | LEFT | RIGHT; 40 | } 41 | })} 42 | marginTop={bind(options["bar.position"]).as((pos) => { 43 | if (pos === "top") return 5; 44 | else return 0; 45 | })} 46 | marginLeft={5} 47 | marginRight={5} 48 | marginBottom={bind(options["bar.position"]).as((pos) => { 49 | if (pos === "bottom") return 5; 50 | else return 0; 51 | })} 52 | {...props} 53 | > 54 | 55 | 59 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 85 | 86 | 87 | 88 | ); 89 | } 90 | 91 | export default function (monitor: Gdk.Monitor) { 92 | const windowName = `bar-${monitor.get_connector()}`; 93 | 94 | function createBar() { 95 | console.log(`Creating bar for monitor ${monitor.get_connector()}`); 96 | return ; 97 | } 98 | 99 | // Create the initial bar 100 | createBar(); 101 | 102 | return windowName; 103 | } 104 | -------------------------------------------------------------------------------- /widgets/bar/modules/Cpu.tsx: -------------------------------------------------------------------------------- 1 | import { bind } from "astal"; 2 | import Gsk from "gi://Gsk"; 3 | import { execAsync } from "astal/process"; 4 | import SystemMonitor from "utils/hwmonitor"; 5 | import { CircularProgressBar } from "widgets/common/circularprogress"; 6 | 7 | export default function Cpu() { 8 | const sysmon = SystemMonitor.get_default(); 9 | 10 | return ( 11 | 12 | 21 |