├── .gitignore ├── README.md ├── app.ts ├── build.js ├── env.d.ts ├── options.ts ├── package.json ├── request.ts ├── styles ├── common.scss ├── styles.scss └── widgets │ ├── applauncher.scss │ ├── audio.scss │ ├── bar.scss │ ├── battery.scss │ ├── clock.scss │ ├── datemenu.scss │ ├── dock.scss │ ├── notification.scss │ ├── powermenu.scss │ ├── quicksettings.scss │ └── wallpaperpicker.scss ├── tsconfig.json ├── utils ├── brightness.ts ├── hyprland.ts ├── index.ts ├── networkspeed.ts ├── option.ts ├── powermenu.ts ├── screenrecord.ts └── styles.ts ├── widgets ├── applauncher │ └── Applauncher.tsx ├── bar │ ├── Bar.tsx │ ├── LauncherPanelButton.tsx │ ├── NetworkSpeedPanelButton.tsx │ ├── NotifPanelButton.tsx │ ├── QSPanelButton.tsx │ ├── RecordIndicatorPanelButton.tsx │ ├── TimePanelButton.tsx │ ├── TrayPanelButton.tsx │ └── WorkspacesPanelButton.tsx ├── clock │ └── DesktopClock.tsx ├── common │ ├── FlowBox.ts │ ├── PanelButton.tsx │ ├── Picture.ts │ └── PopupWindow.tsx ├── datemenu │ └── DateMenu.tsx ├── dock │ ├── Dock.tsx │ └── DockApps.tsx ├── notification │ ├── Notification.tsx │ ├── NotificationPopup.tsx │ └── NotificationWindow.tsx ├── powermenu │ ├── PowerMenu.tsx │ └── VerificationWindow.tsx ├── quicksettings │ ├── BrightnessBox.tsx │ ├── QSButton.tsx │ ├── QSWindow.tsx │ ├── VolumeBox.tsx │ ├── buttons │ │ ├── ColorPickerQS.tsx │ │ ├── DarkModeQS.tsx │ │ ├── DontDisturbQS.tsx │ │ ├── MicQS.tsx │ │ ├── RecordQS.tsx │ │ └── ScreenshotQS.tsx │ └── pages │ │ ├── BatteryPage.tsx │ │ ├── SpeakerPage.tsx │ │ └── WifiPage.tsx └── wallpaperpicker │ └── WallpaperPicker.tsx └── windows.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | @girs/ 3 | style.css 4 | *.css.map 5 | app.js.map 6 | traceify/ 7 | /epikshell.js 8 | /styles/variables.scss 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Epik Shell 2 | 3 | > [!WARNING] 4 | > This repository is regularly updated 5 | 6 | A desktop shell based on [Astal](https://github.com/Aylur/Astal/). 7 | 8 | ## Screenshots 9 | 10 | ![2025-01-30_02-33-03](https://github.com/user-attachments/assets/12d46e4f-bbec-4c90-865f-3cbb36866bc9) 11 | ![2025-01-30_02-36-54](https://github.com/user-attachments/assets/1fa2dc55-41f8-46d6-bfac-afef2e83c32c) 12 | ![2025-01-30_02-37-16](https://github.com/user-attachments/assets/d9702b1a-2816-48a5-a9f0-00b7999447dd) 13 | --- 14 | 15 | ## Notes 16 | 17 | - Most widgets are copied from [Aylur dotfiles](https://github.com/Aylur/dotfiles), the creator of Astal/AGS. Thanks, Aylur! 18 | - Some features may not work as expected. Feel free to ask if you encounter any issues. 19 | - **Only Hyprland is supported**, although some widgets might work with other Wayland compositors. 20 | 21 | --- 22 | 23 | ## Dependencies 24 | 25 | ### Required 26 | 27 | - `astal` (`libastal-meta` & `libastal-gjs`) or `aylurs-gtk-shell` 28 | - `dart-sass` 29 | - `esbuild` 30 | 31 | ### Optional 32 | 33 | - `hyprpicker` 34 | - `swappy` 35 | - `wf-recorder` 36 | - `wayshot` 37 | - `slurp` 38 | - `wl-copy` 39 | - `brightnessctl` 40 | 41 | ```bash 42 | sudo pacman -S libastal-meta libastal-gjs-git dart-sass esbuild hyprpicker swappy wf-recorder wayshot slurp wl-copy brightnessctl 43 | ``` 44 | 45 | --- 46 | 47 | ## Quick Start Guide 48 | 49 | 1. Clone the repository 50 | ```bash 51 | git clone https://github.com/ezerinz/epik-shell 52 | ``` 53 | 2. Navigate to project directory 54 | ```bash 55 | cd epik-shell 56 | ``` 57 | 3. Run 58 | ```bash 59 | LD_PRELOAD=/usr/lib/libgtk4-layer-shell.so gjs -m build.js 60 | ``` 61 | You can also use ags: 62 | ```bash 63 | ags run --gtk4 -d . 64 | ``` 65 | or: 66 | ```bash 67 | ags run --gtk4 -d 68 | ``` 69 | 70 | --- 71 | 72 | ## Configuration 73 | Epik Shell looks for a configuration file in the config directory (`~/.config/epik-shell/config.json`). 74 | Configuration comes with the following defaults: 75 | 76 | You can check some configurations in the [wiki](https://github.com/ezerinz/epik-shell/wiki/Configuration-Recipes) 77 | > [!WARNING] 78 | > Don't copy and paste this entire block into your `config.json`, it's just to show which configurations are available. 79 | 80 | ```jsonc 81 | { 82 | "dock": { 83 | "position": "bottom", // "top" | "bottom" 84 | "pinned": ["firefox", "Alacritty", "org.gnome.Nautilus", "localsend"], // array of application classname 85 | }, 86 | "bar": { 87 | "position": "top", // "top" | "bottom" 88 | "separator": true, 89 | // modules to show in start, center, and end of bar. 90 | // available options: "launcher", "workspace", "time", "notification", "network_speed", "quicksetting" 91 | "start": ["launcher", "workspace"], 92 | "center": ["time", "notification"], 93 | "end": ["network_speed", "quicksetting"], 94 | }, 95 | "desktop_clock": { 96 | "position": "top_left", // "top_left" | "top" | "top_right" | "left" | "center" | "right" | "bottom_left" | "bottom" | "bottom_right" 97 | }, 98 | "theme": { 99 | "bar": { 100 | "bg_color": "$bg", // css color values (name -> red, rgb -> rgb(50, 50, 50), etc), or use theme color with "$" prefix ($bg, $accent, etc) 101 | "opacity": 1, 102 | "border_radius": 6, // in px, support css style (top, right, bottom, left -> [10, 15, 20, 10]) 103 | "margin": 10, // in px, support css style 104 | "padding": 3, // in px, support css style 105 | "border_width": 2, 106 | "border_color": "$fg", // css color values or use theme color 107 | "shadow": { 108 | "offset": [6, 6], // in px, can be [horizontal, vertical] or single number 109 | "blur": 0, 110 | "spread": 0, 111 | "color": "$fg", // css color values or use theme color 112 | "opacity": 1, 113 | }, 114 | "button": { 115 | "bg_color": "$bg", 116 | "fg_color": "$fg", 117 | "opacity": 1, 118 | "border_radius": 8, 119 | "border_width": 0, 120 | "border_color": "$fg", 121 | "padding": [0, 4], 122 | "shadow": { 123 | "offset": [0, 0], 124 | "blur": 0, 125 | "spread": 0, 126 | "color": "$fg", 127 | "opacity": 1, 128 | }, 129 | }, 130 | }, 131 | "window": { 132 | "opacity": 1, 133 | "border_radius": 6, 134 | "margin": 10, 135 | "padding": 10, 136 | "dock_padding": 4, 137 | "desktop_clock_padding": 4, 138 | "border_width": 2, 139 | "border_color": "$fg", 140 | "shadow": { 141 | "offset": [6, 6], 142 | "blur": 0, 143 | "spread": 0, 144 | "color": "$fg", 145 | "opacity": 1, 146 | }, 147 | }, 148 | "light": { 149 | "bg": "#fbf1c7", 150 | "fg": "#3c3836", 151 | "accent": "#3c3836", 152 | "red": "#cc241d", 153 | }, 154 | "dark": { 155 | "bg": "#282828", 156 | "fg": "#ebdbb2", 157 | "accent": "#ebdbb2", 158 | "red": "#cc241d", 159 | }, 160 | }, 161 | } 162 | ``` 163 | 164 | --- 165 | 166 | ## GTK Theme 167 | 168 | ### Theme Settings 169 | 170 | - **Theme:** `adw-gtk3` 171 | - **Icons:** `Colloid` 172 | 173 | ### Making GTK Apps Match Astal Theme 174 | 175 | 1. Install `libadwaita-without-adwaita`. 176 | 2. This configuration generates a `colors.css` file in `$HOME/.themes` based on theme settings in `src/theme.json`. Import the `colors.css` file into the `adw-gtk3` theme to apply it to your GTK apps. 177 | 178 | Locate the following files: 179 | 180 | - `adw-gtk3/gtk-3.0/gtk.css` 181 | - `adw-gtk3/gtk-4.0/gtk.css` 182 | - `adw-gtk3-dark/gtk-3.0/gtk-dark.css` 183 | - `adw-gtk3.dark/gtk-4.0/gtk-dark.css` 184 | 185 | Add the following line after the `define-color` section: 186 | 187 | > **This assumes your adw-gtk3 folder is inside $HOME/.themes. If it's not, adjust the path accordingly.** 188 | 189 | ```css 190 | /* Import after many define-color lines */ 191 | @import "../../colors.css"; 192 | ``` 193 | -------------------------------------------------------------------------------- /app.ts: -------------------------------------------------------------------------------- 1 | import { App } from "astal/gtk4"; 2 | import windows from "./windows"; 3 | import request from "./request"; 4 | import initStyles from "./utils/styles"; 5 | import initHyprland from "./utils/hyprland"; 6 | 7 | initStyles(); 8 | 9 | App.start({ 10 | requestHandler(req, res) { 11 | request(req, res); 12 | }, 13 | main() { 14 | windows.map((win) => App.get_monitors().map(win)); 15 | initHyprland(); 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | import AstalIO from "gi://AstalIO?version=0.1"; 2 | import GLib from "gi://GLib"; 3 | import { exit, programPath } from "system"; 4 | 5 | async function execAsync(cmd) { 6 | return new Promise((resolve, reject) => { 7 | AstalIO.Process.exec_asyncv(cmd, (_, res) => { 8 | try { 9 | resolve(AstalIO.Process.exec_asyncv_finish(res)); 10 | } catch (error) { 11 | reject(error); 12 | } 13 | }); 14 | }); 15 | } 16 | 17 | const currentDir = GLib.path_get_dirname(programPath); 18 | 19 | const entry = `${currentDir}/app.ts`; 20 | const outfile = `${GLib.get_user_runtime_dir()}/epikshell.js`; 21 | 22 | //bundle js 23 | try { 24 | //GLib.setenv("NODE_ENV", "production", true); 25 | //await execAsync([ 26 | // "bun", 27 | // "build", 28 | // entry, 29 | // "--outfile", 30 | // outfile, 31 | // "--external", 32 | // "gi://*", 33 | // "--external", 34 | // "system", 35 | // "--define", 36 | // `SRC=${currentDir}`, 37 | // "--target", 38 | // "bun", 39 | //]); 40 | 41 | await execAsync([ 42 | "esbuild", 43 | "--bundle", 44 | entry, 45 | `--outfile=${outfile}`, 46 | "--sourcemap=inline", 47 | "--format=esm", 48 | "--external:gi://*", 49 | "--external:system", 50 | "--platform=node", 51 | "--loader:.js=ts", 52 | `--define:SRC="${currentDir}"`, 53 | ]); 54 | } catch (error) { 55 | console.error(error); 56 | exit(0); 57 | } 58 | 59 | await import(`file://${outfile}`).catch(console.error); 60 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | declare const SRC: string; 2 | declare const COMPILED_CSS: string; 3 | 4 | declare module "inline:*" { 5 | const content: string; 6 | export default content; 7 | } 8 | 9 | declare module "*.scss" { 10 | const content: string; 11 | export default content; 12 | } 13 | 14 | declare module "*.blp" { 15 | const content: string; 16 | export default content; 17 | } 18 | 19 | declare module "*.css" { 20 | const content: string; 21 | export default content; 22 | } 23 | -------------------------------------------------------------------------------- /options.ts: -------------------------------------------------------------------------------- 1 | import { execAsync, GLib } from "astal"; 2 | import { mkOptions, opt } from "./utils/option"; 3 | import { gsettings } from "./utils"; 4 | 5 | const options = mkOptions( 6 | `${GLib.get_user_config_dir()}/epik-shell/config.json`, 7 | { 8 | wallpaper: { 9 | folder: opt(GLib.get_home_dir(), { cached: true }), 10 | current: opt( 11 | await execAsync("swww query") 12 | .then((out) => out.split("image:")[1].trim()) 13 | .catch(() => ""), 14 | { cached: true }, 15 | ), 16 | }, 17 | dock: { 18 | position: opt("bottom"), 19 | pinned: opt(["firefox", "Alacritty", "org.gnome.Nautilus", "localsend"]), 20 | }, 21 | bar: { 22 | position: opt("top"), 23 | separator: opt(true), 24 | start: opt(["launcher", "workspace"]), 25 | center: opt(["time", "notification"]), 26 | end: opt(["network_speed", "quicksetting"]), 27 | }, 28 | desktop_clock: { 29 | position: opt< 30 | | "top_left" 31 | | "top" 32 | | "top_right" 33 | | "left" 34 | | "center" 35 | | "right" 36 | | "bottom_left" 37 | | "bottom" 38 | | "bottom_right" 39 | >("top_left"), 40 | }, 41 | theme: { 42 | mode: opt( 43 | gsettings.get_string("color-scheme") == "prefer-light" 44 | ? "light" 45 | : "dark", 46 | { cached: true }, 47 | ), 48 | bar: { 49 | bg_color: opt("$bg"), 50 | opacity: opt(1), 51 | border_radius: opt(6), 52 | margin: opt(10), 53 | padding: opt(3), 54 | border_width: opt(2), 55 | border_color: opt("$fg"), 56 | shadow: { 57 | offset: opt([6, 6]), 58 | blur: opt(0), 59 | spread: opt(0), 60 | color: opt("$fg"), 61 | opacity: opt(1), 62 | }, 63 | button: { 64 | bg_color: opt("$bg"), 65 | fg_color: opt("$fg"), 66 | opacity: opt(1), 67 | border_radius: opt(8), 68 | border_width: opt(0), 69 | border_color: opt("$fg"), 70 | padding: opt([0, 4]), 71 | shadow: { 72 | offset: opt([0, 0]), 73 | blur: opt(0), 74 | spread: opt(0), 75 | color: opt("$fg"), 76 | opacity: opt(1), 77 | }, 78 | }, 79 | }, 80 | window: { 81 | opacity: opt(1), 82 | border_radius: opt(6), 83 | margin: opt(10), 84 | padding: opt(10), 85 | dock_padding: opt(4), 86 | desktop_clock_padding: opt(4), 87 | border_width: opt(2), 88 | border_color: opt("$fg"), 89 | shadow: { 90 | offset: opt([6, 6]), 91 | blur: opt(0), 92 | spread: opt(0), 93 | color: opt("$fg"), 94 | opacity: opt(1), 95 | }, 96 | }, 97 | light: { 98 | bg: opt("#fbf1c7"), 99 | fg: opt("#3c3836"), 100 | accent: opt("#3c3836"), 101 | red: opt("#cc241d"), 102 | }, 103 | dark: { 104 | bg: opt("#282828"), 105 | fg: opt("#ebdbb2"), 106 | accent: opt("#ebdbb2"), 107 | red: opt("#cc241d"), 108 | }, 109 | }, 110 | }, 111 | ); 112 | 113 | export default options; 114 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astal-shell", 3 | "dependencies": { 4 | "astal": "/usr/share/astal/gjs" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /request.ts: -------------------------------------------------------------------------------- 1 | import ScreenRecord from "./utils/screenrecord"; 2 | 3 | export default function requestHandler( 4 | request: string, 5 | res: (response: any) => void, 6 | ): void { 7 | const screenRecord = ScreenRecord.get_default(); 8 | switch (request) { 9 | case "screen-record": 10 | res("ok"); 11 | screenRecord.start(); 12 | break; 13 | case "screenshot": 14 | res("ok"); 15 | screenRecord.screenshot(true); 16 | break; 17 | case "screenshot-select": 18 | res("ok"); 19 | screenRecord.screenshot(); 20 | break; 21 | default: 22 | res("not ok"); 23 | break; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /styles/common.scss: -------------------------------------------------------------------------------- 1 | @use "sass:color"; 2 | @use "sass:math"; 3 | @use "variables" as *; 4 | 5 | * { 6 | font-family: "Google Sans"; 7 | font-size: 0.95rem; 8 | font-weight: 600; 9 | } 10 | 11 | scale { 12 | margin: 0; 13 | padding: 0; 14 | trough { 15 | border-radius: calc(nth($window-border-radius, 1) * 99); 16 | highlight { 17 | background-color: $accent; 18 | min-height: 25px; 19 | // min-width: 25px; 20 | border-radius: calc(nth($window-border-radius, 1) * 99); 21 | } 22 | slider { 23 | box-shadow: none; 24 | outline: unset; 25 | background-color: transparent; 26 | } 27 | } 28 | } 29 | 30 | menubutton { 31 | padding: 0; 32 | button { 33 | padding: unset; 34 | margin: unset; 35 | border-radius: calc(nth($window-border-radius, 1) / 2); 36 | 37 | &:hover { 38 | background-color: color.adjust($fg, $alpha: -0.85); 39 | } 40 | } 41 | } 42 | 43 | button { 44 | padding: unset; 45 | margin: unset; 46 | border-radius: calc(nth($window-border-radius, 1) / 2); 47 | background-color: transparent; 48 | transition: transform 0.2s ease-in-out; 49 | 50 | &:active { 51 | > * { 52 | transform: scale(0.85); 53 | } 54 | } 55 | 56 | &:hover { 57 | background-color: color.adjust($fg, $alpha: -0.85); 58 | } 59 | } 60 | 61 | window { 62 | background-color: transparent; 63 | .button-padding { 64 | background-color: transparent; 65 | } 66 | } 67 | 68 | separator { 69 | min-height: 2px; 70 | margin: 0.3rem 0.5rem; 71 | border-radius: calc(nth($window-border-radius, 1) * 99); 72 | 73 | &.vertical { 74 | min-height: unset; 75 | min-width: 2px; 76 | margin: 0.5rem 0.3rem; 77 | } 78 | } 79 | 80 | .bar .icon { 81 | font-size: 1.1rem; 82 | } 83 | 84 | $shadow-h: nth($bar-button-shadow-offset, 1); 85 | $shadow-v: nth($bar-button-shadow-offset, 2); 86 | .panel-button { 87 | background-color: color.adjust( 88 | $bar-button-bg-color, 89 | $alpha: calc($bar-button-opacity - 1) 90 | ); 91 | box-shadow: 92 | 0 0 0 $bar-button-border-width $bar-button-border-color, 93 | $bar-button-shadow-offset $bar-button-shadow-blur $bar-button-shadow-spread 94 | color.adjust( 95 | $bar-button-shadow-color, 96 | $alpha: calc($bar-button-shadow-opacity - 1) 97 | ); 98 | color: $bar-button-fg-color; 99 | border-radius: calc(nth($bar-button-border-radius, 1) / 2); 100 | padding: $bar-button-padding; 101 | margin-right: if($shadow-h > 0, $shadow-h, 0); 102 | margin-left: if($shadow-h <= 0, $shadow-h, 0); 103 | margin-bottom: if($shadow-v > 0, $shadow-v, 0); 104 | margin-top: if($shadow-v <= 0, $shadow-v, 0); 105 | 106 | transition: 107 | background-color 0.2s ease-in-out, 108 | color 0.2s ease-in-out; 109 | 110 | &.notifications { 111 | color: $bar-button-fg-color; 112 | .circle { 113 | border-radius: 99px; 114 | } 115 | } 116 | 117 | &:hover { 118 | background-color: if( 119 | $bar-opacity < 1, 120 | $bar-button-bg-color, 121 | color.adjust($bar-button-fg-color, $alpha: -0.9) 122 | ); 123 | } 124 | 125 | &.active { 126 | background-color: $accent; 127 | color: $bar-button-bg-color; 128 | } 129 | } 130 | 131 | .window-content { 132 | background-color: color.adjust($bg, $alpha: calc($window-opacity - 1)); 133 | color: $fg; 134 | margin: $window-margin; 135 | box-shadow: 136 | 0 0 0 $window-border-width $window-border-color, 137 | $window-shadow-offset $window-shadow-blur $window-shadow-spread 138 | color.adjust( 139 | $window-shadow-color, 140 | $alpha: calc($window-shadow-opacity - 1) 141 | ); 142 | border-radius: $window-border-radius; 143 | } 144 | -------------------------------------------------------------------------------- /styles/styles.scss: -------------------------------------------------------------------------------- 1 | @use "./common.scss"; 2 | @use "./widgets/bar.scss"; 3 | @use "./widgets/datemenu.scss"; 4 | @use "./widgets/clock.scss"; 5 | @use "./widgets/notification.scss"; 6 | @use "./widgets/applauncher.scss"; 7 | @use "./widgets/dock.scss"; 8 | @use "./widgets/quicksettings.scss"; 9 | @use "./widgets/powermenu.scss"; 10 | @use "./widgets/battery.scss"; 11 | @use "./widgets/audio.scss"; 12 | @use "./widgets/wallpaperpicker.scss"; 13 | -------------------------------------------------------------------------------- /styles/widgets/applauncher.scss: -------------------------------------------------------------------------------- 1 | @use "sass:color"; 2 | @use "../variables.scss" as *; 3 | 4 | .applauncher-container { 5 | font-size: 4rem; 6 | min-height: 400px; 7 | min-width: 450px; 8 | 9 | .background-entry { 10 | border-radius: $window-border-radius; 11 | box-shadow: 0 0 0 $window-border-width $window-border-color; 12 | } 13 | 14 | > overlay { 15 | margin: $window-padding; 16 | margin-bottom: 6px; 17 | entry { 18 | outline: none; 19 | padding: $window-padding; 20 | 21 | image { 22 | -gtk-icon-size: 1.5rem; 23 | } 24 | 25 | text { 26 | font-size: 1.2rem; 27 | placeholder { 28 | font-size: 1.2rem; 29 | } 30 | } 31 | } 32 | } 33 | 34 | .entry-overlay { 35 | box-shadow: 0 0 0 $window-border-width $fg; 36 | border-radius: $window-border-radius; 37 | scrolledwindow viewport picture { 38 | border-radius: $window-border-radius; 39 | } 40 | } 41 | 42 | > scrolledwindow viewport { 43 | padding: $window-padding; 44 | padding-top: $window-border-width; 45 | box { 46 | outline-width: $window-border-width; 47 | 48 | .not-found label { 49 | font-size: 1rem; 50 | } 51 | } 52 | } 53 | 54 | .app-button { 55 | -gtk-icon-size: 2rem; 56 | padding: 0.4rem 0.8rem; 57 | background-color: color.adjust($fg, $alpha: -0.9); 58 | image { 59 | margin-right: 6px; 60 | } 61 | 62 | &:active { 63 | > * { 64 | transform: none; 65 | } 66 | 67 | image { 68 | transform: scale(0.85); 69 | } 70 | } 71 | 72 | &:focus { 73 | outline-color: $accent; 74 | } 75 | &:hover { 76 | background-color: color.adjust($fg, $alpha: -0.75); 77 | } 78 | .description { 79 | font-size: 0.85rem; 80 | font-weight: 400; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /styles/widgets/audio.scss: -------------------------------------------------------------------------------- 1 | @use "../variables.scss" as *; 2 | 3 | .audio-container { 4 | padding: $window-padding; 5 | min-width: 300px; 6 | } 7 | -------------------------------------------------------------------------------- /styles/widgets/bar.scss: -------------------------------------------------------------------------------- 1 | @use "sass:color"; 2 | @use "sass:math"; 3 | @use "../variables.scss" as *; 4 | 5 | $shadow-h: nth($bar-button-shadow-offset, 1); 6 | $shadow-v: nth($bar-button-shadow-offset, 2); 7 | 8 | .bar-container { 9 | background-color: color.adjust($bar-bg-color, $alpha: calc($bar-opacity - 1)); 10 | margin: $bar-margin; 11 | border-radius: $bar-border-radius; 12 | padding: $bar-padding; 13 | box-shadow: 14 | 0 0 0 $bar-border-width $bar-border-color, 15 | $bar-shadow-offset $bar-shadow-blur $bar-shadow-spread 16 | color.adjust($bar-shadow-color, $alpha: calc($bar-shadow-opacity - 1)); 17 | 18 | separator { 19 | min-width: if($bar-separator, 2px, 0); 20 | } 21 | 22 | .workspace-container { 23 | background-color: color.adjust( 24 | $bar-button-bg-color, 25 | $alpha: calc($bar-button-opacity - 1) 26 | ); 27 | box-shadow: 28 | 0 0 0 $bar-button-border-width $bar-button-border-color, 29 | $bar-button-shadow-offset $bar-button-shadow-blur 30 | $bar-button-shadow-spread 31 | color.adjust( 32 | $bar-button-shadow-color, 33 | $alpha: calc($bar-button-shadow-opacity - 1) 34 | ); 35 | 36 | color: $bar-button-fg-color; 37 | border-radius: calc(nth($bar-button-border-radius, 1) / 2); 38 | padding: $bar-button-padding; 39 | margin-right: if($shadow-h > 0, $shadow-h, 0); 40 | margin-left: if($shadow-h <= 0, $shadow-h, 0); 41 | margin-bottom: if($shadow-v > 0, $shadow-v, 0); 42 | margin-top: if($shadow-v <= 0, $shadow-v, 0); 43 | 44 | .workspace-button { 45 | background-color: color.adjust($bar-button-fg-color, $alpha: -0.55); 46 | min-height: 0.65em; 47 | min-width: 0.65rem; 48 | border-radius: calc(nth($bar-button-border-radius, 1) * 1.5); 49 | 50 | transition: min-width 0.25s ease-out; 51 | 52 | &:hover { 53 | background-color: $bar-button-fg-color; 54 | &.occupied { 55 | background-color: $bar-button-fg-color; 56 | } 57 | } 58 | 59 | &.active { 60 | min-width: 2.5rem; 61 | min-height: 0.95rem; 62 | background-color: $accent; 63 | } 64 | 65 | &.occupied { 66 | box-shadow: unset; 67 | background-color: $bar-button-fg-color; 68 | &.active { 69 | background-color: $accent; 70 | } 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /styles/widgets/battery.scss: -------------------------------------------------------------------------------- 1 | @use "sass:color"; 2 | @use "../variables.scss" as *; 3 | 4 | .battery-container { 5 | padding: $window-padding; 6 | min-width: 300px; 7 | 8 | overlay { 9 | levelbar trough block { 10 | min-height: 45px; 11 | border-radius: $window-border-radius; 12 | background-color: color.adjust($fg, $alpha: -0.65); 13 | &.filled { 14 | background-color: $fg; 15 | } 16 | } 17 | 18 | label { 19 | color: $bg; 20 | font-size: 1.2rem; 21 | margin-left: 0.7rem; 22 | } 23 | } 24 | 25 | .power-profiles { 26 | button { 27 | padding: 0.2rem; 28 | background-color: color.adjust($fg, $alpha: -0.9); 29 | color: $fg; 30 | border-radius: $window-border-radius; 31 | 32 | &:hover { 33 | background-color: color.adjust($fg, $alpha: -0.65); 34 | } 35 | 36 | &.active { 37 | background-color: $fg; 38 | color: $bg; 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /styles/widgets/clock.scss: -------------------------------------------------------------------------------- 1 | @use "../variables" as *; 2 | @use "sass:math"; 3 | @use "sass:color"; 4 | 5 | .clock-container { 6 | margin: $window-margin; 7 | color: $fg; 8 | .unit { 9 | padding: $window-desktop-clock-padding; 10 | > * { 11 | margin: 0 0.7rem; 12 | } 13 | } 14 | 15 | > box { 16 | background-color: color.adjust($bg, $alpha: calc($window-opacity - 1)); 17 | border-radius: $window-border-radius; 18 | box-shadow: 19 | 0 0 0 $window-border-width $window-border-color, 20 | $window-shadow-offset $window-shadow-blur $window-shadow-spread 21 | color.adjust( 22 | $window-shadow-color, 23 | $alpha: calc($window-shadow-opacity - 1) 24 | ); 25 | 26 | .box-label { 27 | font-size: 0.9rem; 28 | font-style: italic; 29 | } 30 | } 31 | 32 | label { 33 | font-size: 3rem; 34 | color: $fg; 35 | font-family: "Google Sans Display"; 36 | font-weight: 700; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /styles/widgets/datemenu.scss: -------------------------------------------------------------------------------- 1 | @use "../variables" as *; 2 | 3 | .datemenu-container { 4 | padding: $window-padding; 5 | 6 | .time { 7 | font-size: 1.5rem; 8 | } 9 | 10 | .date { 11 | font-size: 1rem; 12 | margin-bottom: 1rem; 13 | } 14 | 15 | calendar { 16 | background-color: transparent; 17 | border: unset; 18 | 19 | header { 20 | padding-bottom: 0.3rem; 21 | border: unset; 22 | } 23 | 24 | grid > * { 25 | border-radius: $window-border-radius; 26 | outline-color: $accent; 27 | // outline: unset; 28 | } 29 | 30 | .day-number { 31 | &:selected { 32 | color: $bg; 33 | background-color: $accent; 34 | } 35 | } 36 | .today { 37 | border-radius: $window-border-radius; 38 | box-shadow: 0 0 0 0.1rem $accent; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /styles/widgets/dock.scss: -------------------------------------------------------------------------------- 1 | @use "../variables.scss" as *; 2 | @use "sass:color"; 3 | 4 | .dock-padding { 5 | color: transparent; 6 | padding: 1px; 7 | } 8 | 9 | .dock-container { 10 | padding: $window-dock-padding; 11 | 12 | .media-player { 13 | min-width: 180px; 14 | background-color: color.adjust($fg, $alpha: -0.75); 15 | padding: 0.5rem; 16 | margin-left: 0.5rem; 17 | border-radius: $window-border-radius; 18 | 19 | .cover { 20 | border-radius: $window-border-radius; 21 | margin-right: 0.3rem; 22 | } 23 | } 24 | 25 | .app-button { 26 | image { 27 | transition: -gtk-icon-size cubic-bezier(0.25, 1, 0.5, 1) 100ms; 28 | } 29 | 30 | .box { 31 | padding: 0.2rem; 32 | min-width: 45px; 33 | min-height: 45px; 34 | } 35 | 36 | &:hover { 37 | background-color: transparent; 38 | image { 39 | margin-bottom: 10px; 40 | -gtk-icon-size: 40px; 41 | } 42 | } 43 | 44 | &.focused { 45 | .indicator { 46 | min-width: 25px; 47 | } 48 | } 49 | 50 | .indicator { 51 | min-width: 4px; 52 | min-height: 4px; 53 | border-radius: 99px; 54 | background-color: $accent; 55 | margin-bottom: 3px; 56 | transition: min-width 0.4s ease-in-out; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /styles/widgets/notification.scss: -------------------------------------------------------------------------------- 1 | @use "sass:color"; 2 | @use "sass:math"; 3 | @use "../variables" as *; 4 | 5 | .notifications-container { 6 | min-width: 400px; 7 | min-height: 420px; 8 | separator { 9 | margin: $window-padding; 10 | margin-top: 0.3rem; 11 | margin-bottom: 0.3rem; 12 | } 13 | .window-header { 14 | padding: $window-padding; 15 | padding-bottom: 0; 16 | 17 | .dnd { 18 | margin-right: 0.5rem; 19 | background-color: color.adjust($fg, $alpha: -0.9); 20 | padding: 0 0.6rem; 21 | 22 | &.active { 23 | color: $bg; 24 | background-color: $fg; 25 | 26 | &:hover { 27 | background-color: color.adjust($fg, $alpha: -0.2); 28 | } 29 | } 30 | 31 | &:hover { 32 | background-color: color.adjust($fg, $alpha: -0.7); 33 | } 34 | label { 35 | font-size: 0.95rem; 36 | } 37 | } 38 | 39 | .clear { 40 | padding: 0.2rem 0.5rem; 41 | } 42 | 43 | label { 44 | font-size: 1.2rem; 45 | } 46 | } 47 | 48 | scrolledwindow viewport > box { 49 | .not-found label { 50 | font-size: 1rem; 51 | } 52 | margin: $window-padding; 53 | margin-top: 1px; 54 | } 55 | 56 | .notification-container { 57 | min-width: 1px; 58 | margin: $window-border-width; 59 | background-color: color.adjust($fg, $alpha: -0.9); 60 | box-shadow: none; 61 | } 62 | } 63 | 64 | .notification-container { 65 | padding: $window-padding; 66 | min-width: 330px; 67 | 68 | &.critical { 69 | box-shadow: 0 0 0 0.1rem $red; 70 | } 71 | 72 | .header { 73 | .time { 74 | margin-right: 0.5rem; 75 | } 76 | 77 | button { 78 | padding: 0 0.3rem; 79 | background-color: color.adjust($fg, $alpha: -0.9); 80 | border-radius: calc(nth($window-border-radius, 1) / 2); 81 | } 82 | } 83 | 84 | separator { 85 | margin: 0.4rem 0; 86 | } 87 | 88 | .content { 89 | .summary { 90 | font-size: 1.2rem; 91 | } 92 | 93 | .body { 94 | font-size: 0.9rem; 95 | font-weight: 400; 96 | } 97 | 98 | .image image { 99 | border-radius: calc(nth($window-border-radius, 1) / 1.5); 100 | min-width: 60px; 101 | min-height: 60px; 102 | background-size: cover; 103 | background-position: center; 104 | } 105 | } 106 | 107 | .actions { 108 | margin-top: 0.5rem; 109 | button { 110 | padding: 0.1rem 0; 111 | background-color: color.adjust($fg, $alpha: -0.9); 112 | border-radius: calc(nth($window-border-radius, 1) / 2); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /styles/widgets/powermenu.scss: -------------------------------------------------------------------------------- 1 | @use "sass:color"; 2 | @use "../variables.scss" as *; 3 | 4 | .verification-container { 5 | min-width: 250px; 6 | padding: $window-padding; 7 | 8 | .title { 9 | font-size: 1.2rem; 10 | } 11 | .body { 12 | font-weight: 400; 13 | } 14 | 15 | .buttons { 16 | button { 17 | background-color: color.adjust($fg, $alpha: -0.9); 18 | padding: 0.2rem; 19 | &:focus { 20 | outline-color: $fg; 21 | } 22 | &:hover { 23 | background-color: color.adjust($fg, $alpha: -0.65); 24 | } 25 | } 26 | 27 | button:last-child { 28 | color: $red; 29 | } 30 | } 31 | } 32 | 33 | .powermenu-container { 34 | padding: $window-padding; 35 | 36 | flowboxchild { 37 | outline: unset; 38 | padding: 0; 39 | 40 | &:selected { 41 | // outline: unset; 42 | box-shadow: 0 0 0 2px $window-border-color; 43 | background-color: transparent; 44 | } 45 | } 46 | 47 | .system-button { 48 | background-color: color.adjust($fg, $alpha: -0.9); 49 | padding: 0.7rem; 50 | border-radius: $window-border-radius; 51 | &:selected { 52 | background-color: red; 53 | } 54 | 55 | &:focus { 56 | outline-color: $fg; 57 | } 58 | &:hover { 59 | background-color: color.adjust($fg, $alpha: -0.65); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /styles/widgets/quicksettings.scss: -------------------------------------------------------------------------------- 1 | @use "sass:color"; 2 | @use "../variables" as *; 3 | 4 | .qs-container { 5 | min-width: 300px; 6 | button { 7 | outline-color: $fg; 8 | } 9 | 10 | .arrow-button { 11 | border-radius: $window-border-radius; 12 | background-color: color.adjust($fg, $alpha: -0.9); 13 | 14 | > * { 15 | padding: 0 4px; 16 | 17 | &:first-child { 18 | padding: $window-padding; 19 | } 20 | } 21 | &.active { 22 | background-color: $accent; 23 | color: $bg; 24 | } 25 | } 26 | 27 | flowbox flowboxchild { 28 | padding: 0; 29 | outline-color: $fg; 30 | &:selected { 31 | background-color: transparent; 32 | } 33 | } 34 | 35 | separator { 36 | margin: 0; 37 | // margin: 0.7rem 0; 38 | // margin-top: 0.2rem; 39 | } 40 | 41 | .header { 42 | label { 43 | font-size: 1.2rem; 44 | } 45 | button { 46 | padding: 0.3rem 0.5rem; 47 | border-radius: $window-border-radius; 48 | background-color: color.adjust($fg, $alpha: -0.9); 49 | label { 50 | font-size: 0.95rem; 51 | } 52 | &:hover { 53 | background-color: color.adjust($fg, $alpha: -0.65); 54 | } 55 | } 56 | .powermenu { 57 | &:hover { 58 | color: $red; 59 | background-color: color.adjust($fg, $alpha: -0.9); 60 | } 61 | } 62 | } 63 | 64 | .qs-box { 65 | image { 66 | padding: 0.5rem; 67 | border-radius: $window-border-radius; 68 | background-color: color.adjust($fg, $alpha: -0.9); 69 | } 70 | // margin-bottom: 0.7rem; 71 | } 72 | 73 | .qs-button { 74 | border-radius: $window-border-radius; 75 | background-color: color.adjust($fg, $alpha: -0.9); 76 | outline-color: $fg; 77 | 78 | button { 79 | padding: 1rem 0; 80 | background-color: transparent; 81 | outline-color: $fg; 82 | &:checked { 83 | color: $bg; 84 | border-radius: $window-border-radius; 85 | background-color: $accent; 86 | } 87 | } 88 | 89 | popover contents { 90 | border-radius: $window-border-radius; 91 | modelbutton { 92 | background-color: color.adjust($fg, $alpha: -0.9); 93 | &:first-child { 94 | margin-bottom: 0.3rem; 95 | } 96 | &:hover { 97 | background-color: color.adjust($fg, $alpha: -0.65); 98 | } 99 | } 100 | } 101 | 102 | &:hover { 103 | background-color: color.adjust($fg, $alpha: -0.65); 104 | } 105 | > * { 106 | padding: 0.5rem; 107 | } 108 | 109 | .status { 110 | font-weight: 400; 111 | } 112 | 113 | &.active { 114 | color: $bg; 115 | background-color: $accent; 116 | } 117 | } 118 | 119 | .qs-page { 120 | margin: $window-padding; 121 | .button { 122 | padding: 0.4rem 0.8rem; 123 | background-color: color.adjust($fg, $alpha: -0.9); 124 | border-radius: $window-border-radius; 125 | 126 | &:hover { 127 | background-color: color.adjust($fg, $alpha: -0.65); 128 | } 129 | 130 | &:active { 131 | > * { 132 | transform: none; 133 | } 134 | 135 | image { 136 | transform: scale(0.85); 137 | } 138 | } 139 | 140 | &.active { 141 | background-color: $accent; 142 | color: $bg; 143 | } 144 | 145 | image { 146 | margin-right: 10px; 147 | } 148 | } 149 | } 150 | 151 | .wifi-page { 152 | margin: 0; 153 | .header { 154 | margin: $window-padding; 155 | margin-bottom: 0; 156 | } 157 | 158 | separator { 159 | margin: $window-padding; 160 | margin-top: 0; 161 | margin-bottom: 0; 162 | } 163 | 164 | scrolledwindow viewport { 165 | padding: $window-padding; 166 | padding-top: 0; 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /styles/widgets/wallpaperpicker.scss: -------------------------------------------------------------------------------- 1 | @use "sass:color"; 2 | @use "../variables.scss" as *; 3 | 4 | .wallpaperpicker-container { 5 | min-height: 150px; 6 | 7 | > box { 8 | margin: $window-padding; 9 | // margin-top: 0.5rem; 10 | margin-bottom: 0.5rem; 11 | label { 12 | font-size: 1.2rem; 13 | 14 | &.directory { 15 | font-size: 0.95rem; 16 | font-weight: 400; 17 | font-style: italic; 18 | } 19 | } 20 | 21 | button { 22 | background-color: color.adjust($fg, $alpha: -0.9); 23 | padding: 0 0.6rem; 24 | 25 | &:hover { 26 | background-color: color.adjust($fg, $alpha: -0.65); 27 | } 28 | } 29 | } 30 | 31 | separator { 32 | margin: $window-padding; 33 | margin-top: 0; 34 | margin-bottom: 0; 35 | } 36 | 37 | scrolledwindow viewport { 38 | > box { 39 | margin: $window-padding; 40 | margin-top: 6px; 41 | } 42 | } 43 | 44 | .image { 45 | border-radius: $window-border-radius; 46 | background-size: contain; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "experimentalDecorators": true, 5 | "strict": true, 6 | "noImplicitAny": false, 7 | "target": "ES2022", 8 | "module": "ES2022", 9 | "moduleResolution": "Bundler", 10 | //"checkJs": true, 11 | "allowJs": true, 12 | "jsx": "react-jsx", 13 | "jsxImportSource": "astal/gtk4", 14 | "paths": { 15 | "astal": ["/usr/share/astal/gjs"], 16 | "astal/*": ["/usr/share/astal/gjs/*"] 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /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 | 15 | return this.instance; 16 | } 17 | 18 | #kbdMax = get(`--device ${kbd} max`); 19 | #kbd = get(`--device ${kbd} get`); 20 | #screenMax = get("max"); 21 | #screen = get("get") / (get("max") || 1); 22 | 23 | @property(Number) 24 | get kbd() { 25 | return this.#kbd; 26 | } 27 | 28 | set kbd(value) { 29 | if (value < 0 || value > this.#kbdMax) return; 30 | 31 | execAsync(`brightnessctl -d ${kbd} s ${value} -q`).then(() => { 32 | this.#kbd = value; 33 | this.notify("kbd"); 34 | }); 35 | } 36 | 37 | @property(Number) 38 | get screen() { 39 | return this.#screen; 40 | } 41 | 42 | set screen(percent) { 43 | if (percent < 0) percent = 0; 44 | 45 | if (percent > 1) percent = 1; 46 | 47 | execAsync(`brightnessctl set ${Math.floor(percent * 100)}% -q`).then(() => { 48 | this.#screen = percent; 49 | this.notify("screen"); 50 | }); 51 | } 52 | 53 | constructor() { 54 | super(); 55 | 56 | const screenPath = `/sys/class/backlight/${screen}/brightness`; 57 | const kbdPath = `/sys/class/leds/${kbd}/brightness`; 58 | 59 | monitorFile(screenPath, async (f) => { 60 | const v = await readFileAsync(f); 61 | this.#screen = Number(v) / this.#screenMax; 62 | this.notify("screen"); 63 | }); 64 | 65 | monitorFile(kbdPath, async (f) => { 66 | const v = await readFileAsync(f); 67 | this.#kbd = Number(v) / this.#kbdMax; 68 | this.notify("kbd"); 69 | }); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /utils/hyprland.ts: -------------------------------------------------------------------------------- 1 | import { App } from "astal/gtk4"; 2 | import AstalHyprland from "gi://AstalHyprland?version=0.1"; 3 | import options from "../options"; 4 | 5 | const hyprland = AstalHyprland.get_default(); 6 | const { bar } = options; 7 | 8 | export const sendBatch = (batch: string[]) => { 9 | const cmd = batch 10 | .filter((x) => !!x) 11 | .map((x) => `keyword ${x}`) 12 | .join("; "); 13 | 14 | hyprland.message(`[[BATCH]]/${cmd}`); 15 | }; 16 | 17 | export function windowAnimation() { 18 | sendBatch( 19 | App.get_windows() 20 | .filter(({ animation }: any) => !!animation) 21 | .map( 22 | ({ animation, namespace }: any) => 23 | `layerrule animation ${namespace == "dock" ? `slide ${options.dock.position.get()}` : animation == "slide top" ? `slide ${bar.position.get()}` : animation}, ${namespace}`, 24 | ), 25 | ); 26 | } 27 | 28 | function windowBlur() { 29 | const noIgnorealpha = ["verification", "powermenu"]; 30 | 31 | sendBatch( 32 | App.get_windows().flatMap(({ namespace }: any) => { 33 | return [ 34 | `layerrule blur, ${namespace}`, 35 | noIgnorealpha.some((skip) => namespace?.includes(skip)) 36 | ? "" 37 | : `layerrule ignorealpha 0.3, ${namespace}`, 38 | ]; 39 | }), 40 | ); 41 | } 42 | 43 | export default function initHyprland() { 44 | windowAnimation(); 45 | windowBlur(); 46 | 47 | hyprland.connect("config-reloaded", () => { 48 | windowAnimation(); 49 | windowBlur(); 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /utils/index.ts: -------------------------------------------------------------------------------- 1 | import { execAsync, Gio, GLib, Variable } from "astal"; 2 | import { Gtk } from "astal/gtk4"; 3 | 4 | export function ensureDirectory(path: string) { 5 | if (!GLib.file_test(path, GLib.FileTest.EXISTS)) 6 | Gio.File.new_for_path(path).make_directory_with_parents(null); 7 | } 8 | 9 | export async function launchDefaultAsync(uri: string) { 10 | return new Promise((resolve, reject) => { 11 | Gio.AppInfo.launch_default_for_uri_async(uri, null, null, (_, res) => { 12 | try { 13 | resolve(Gio.AppInfo.launch_default_for_uri_finish(res)); 14 | } catch (error) { 15 | reject(error); 16 | } 17 | }); 18 | }); 19 | } 20 | 21 | export const now = () => 22 | GLib.DateTime.new_now_local().format("%Y-%m-%d_%H-%M-%S"); 23 | 24 | export const time = Variable(GLib.DateTime.new_now_local()).poll(1000, () => 25 | GLib.DateTime.new_now_local(), 26 | ); 27 | 28 | export function range(max: number) { 29 | return Array.from({ length: max + 1 }, (_, i) => i); 30 | } 31 | 32 | type NotifUrgency = "low" | "normal" | "critical"; 33 | export function notifySend({ 34 | appName, 35 | appIcon, 36 | urgency = "normal", 37 | image, 38 | icon, 39 | summary, 40 | body, 41 | actions, 42 | }: { 43 | appName?: string; 44 | appIcon?: string; 45 | urgency?: NotifUrgency; 46 | image?: string; 47 | icon?: string; 48 | summary: string; 49 | body: string; 50 | actions?: { 51 | [label: string]: () => void; 52 | }; 53 | }) { 54 | const actionsArray = Object.entries(actions || {}).map( 55 | ([label, callback], i) => ({ 56 | id: `${i}`, 57 | label, 58 | callback, 59 | }), 60 | ); 61 | execAsync( 62 | [ 63 | "notify-send", 64 | `-u ${urgency}`, 65 | appIcon && `-i ${appIcon}`, 66 | `-h "string:image-path:${!!icon ? icon : image}"`, 67 | `"${summary ?? ""}"`, 68 | `"${body ?? ""}"`, 69 | `-a "${appName ?? ""}"`, 70 | ...actionsArray.map((v) => `--action=\"${v.id}=${v.label}\"`), 71 | ].join(" "), 72 | ) 73 | .then((out) => { 74 | if (!isNaN(Number(out.trim())) && out.trim() !== "") { 75 | actionsArray[parseInt(out)].callback(); 76 | } 77 | }) 78 | .catch(console.error); 79 | } 80 | 81 | export async function sh(cmd: string | string[]) { 82 | return execAsync(cmd).catch((err) => { 83 | console.error(typeof cmd === "string" ? cmd : cmd.join(" "), err); 84 | return ""; 85 | }); 86 | } 87 | 88 | export async function bash(strings: string | string[], ...values: unknown[]) { 89 | const cmd = 90 | typeof strings === "string" 91 | ? strings 92 | : strings.flatMap((str, i) => str + `${values[i] ?? ""}`).join(""); 93 | 94 | return execAsync(["bash", "-c", cmd]).catch((err) => { 95 | console.error(cmd, err); 96 | return ""; 97 | }); 98 | } 99 | 100 | export const gsettings = new Gio.Settings({ 101 | schema: "org.gnome.desktop.interface", 102 | }); 103 | 104 | export const cacheDir = `${GLib.get_user_cache_dir()}/epik-shell`; 105 | 106 | export function separatorBetween( 107 | elements: Gtk.Widget[], 108 | orientation: Gtk.Orientation, 109 | ) { 110 | const spacedElements: Gtk.Widget[] = []; 111 | 112 | elements.forEach((element, index) => { 113 | if (index > 0) { 114 | spacedElements.push(new Gtk.Separator({ orientation: orientation })); 115 | } 116 | spacedElements.push(element); 117 | }); 118 | 119 | return spacedElements; 120 | } 121 | -------------------------------------------------------------------------------- /utils/networkspeed.ts: -------------------------------------------------------------------------------- 1 | import { Variable } from "astal"; 2 | 3 | const interval = 1000; 4 | let lastTotalDownBytes = 0; 5 | let lastTotalUpBytes = 0; 6 | 7 | const networkSpeed = Variable({ 8 | download: 0, 9 | upload: 0, 10 | }).poll(interval, ["cat", "/proc/net/dev"], (content, _) => { 11 | const lines = content.split("\n"); 12 | 13 | // Caculate the sum of all interfaces' traffic line by line. 14 | let totalDownBytes = 0; 15 | let totalUpBytes = 0; 16 | 17 | for (let i = 0; i < lines.length; ++i) { 18 | const fields = lines[i].trim().split(/\W+/); 19 | if (fields.length <= 2) { 20 | continue; 21 | } 22 | 23 | // Skip virtual interfaces. 24 | const interfce = fields[0]; 25 | const currentInterfaceDownBytes = Number.parseInt(fields[1]); 26 | const currentInterfaceUpBytes = Number.parseInt(fields[9]); 27 | if ( 28 | interfce === "lo" || 29 | // Created by python-based bandwidth manager "traffictoll". 30 | interfce.match(/^ifb[0-9]+/) || 31 | // Created by lxd container manager. 32 | interfce.match(/^lxdbr[0-9]+/) || 33 | interfce.match(/^virbr[0-9]+/) || 34 | interfce.match(/^br[0-9]+/) || 35 | interfce.match(/^vnet[0-9]+/) || 36 | interfce.match(/^tun[0-9]+/) || 37 | interfce.match(/^tap[0-9]+/) || 38 | isNaN(currentInterfaceDownBytes) || 39 | isNaN(currentInterfaceUpBytes) 40 | ) { 41 | continue; 42 | } 43 | 44 | totalDownBytes += currentInterfaceDownBytes; 45 | totalUpBytes += currentInterfaceUpBytes; 46 | } 47 | 48 | if (lastTotalDownBytes === 0) { 49 | lastTotalDownBytes = totalDownBytes; 50 | } 51 | if (lastTotalUpBytes === 0) { 52 | lastTotalUpBytes = totalUpBytes; 53 | } 54 | const downloadSpeed = (totalDownBytes - lastTotalDownBytes) / interval; 55 | const uploadSpeed = (totalUpBytes - lastTotalUpBytes) / interval; 56 | 57 | lastTotalDownBytes = totalDownBytes; 58 | lastTotalUpBytes = totalUpBytes; 59 | 60 | return { 61 | download: downloadSpeed, 62 | upload: uploadSpeed, 63 | }; 64 | }); 65 | 66 | export default networkSpeed; 67 | -------------------------------------------------------------------------------- /utils/option.ts: -------------------------------------------------------------------------------- 1 | import { readFile, readFileAsync, writeFile } from "astal"; 2 | import { Variable } from "astal"; 3 | import { GLib, Gio } from "astal"; 4 | import { monitorFile } from "astal"; 5 | import { cacheDir, ensureDirectory } from "."; 6 | 7 | function getNestedValue(obj: object, keyPath: string) { 8 | const keys = keyPath.split("."); 9 | let current = obj; 10 | 11 | for (let key of keys) { 12 | if (current && current.hasOwnProperty(key)) { 13 | current = current[key]; 14 | } else { 15 | return undefined; 16 | } 17 | } 18 | 19 | return current; 20 | } 21 | 22 | function setNestedValue(obj: object, keyPath: string, value: T) { 23 | const keys = keyPath.split("."); 24 | let current = obj; 25 | 26 | for (let i = 0; i < keys.length - 1; i++) { 27 | const key = keys[i]; 28 | 29 | if (!current[key]) { 30 | current[key] = {}; 31 | } 32 | 33 | current = current[key]; 34 | } 35 | 36 | current[keys[keys.length - 1]] = value; 37 | } 38 | 39 | export class Opt extends Variable { 40 | constructor(initial: T, { cached = false }: { cached?: boolean }) { 41 | super(initial); 42 | this.initial = initial; 43 | this.cached = cached; 44 | } 45 | 46 | initial: T; 47 | id = ""; 48 | cached = false; 49 | 50 | init(configFile: string) { 51 | const dir = this.cached ? `${cacheDir}/options.json` : configFile; 52 | 53 | if (GLib.file_test(dir, GLib.FileTest.EXISTS)) { 54 | let config: object; 55 | try { 56 | config = JSON.parse(readFile(dir)); 57 | } catch { 58 | config = {}; 59 | } 60 | const configV = this.cached 61 | ? config[this.id] 62 | : getNestedValue(config, this.id); 63 | if (configV !== undefined) { 64 | this.set(configV); 65 | } 66 | } 67 | 68 | if (this.cached) { 69 | this.subscribe((value) => { 70 | readFileAsync(`${cacheDir}/options.json`) 71 | .then((content) => { 72 | const cache = JSON.parse(content); 73 | cache[this.id] = value; 74 | writeFile( 75 | `${cacheDir}/options.json`, 76 | JSON.stringify(cache, null, 2), 77 | ); 78 | }) 79 | .catch(() => ""); 80 | }); 81 | } 82 | } 83 | } 84 | 85 | export const opt = (initial: T, opts = {}) => new Opt(initial, opts); 86 | 87 | function getOptions(object: object, path = ""): Opt[] { 88 | return Object.keys(object).flatMap((key) => { 89 | const obj = object[key]; 90 | const id = path ? path + "." + key : key; 91 | 92 | if (obj instanceof Opt) { 93 | obj.id = id; 94 | return obj; 95 | } 96 | 97 | if (typeof obj === "object") return getOptions(obj, id); 98 | 99 | return []; 100 | }); 101 | } 102 | 103 | function transformObject(obj: object, initial?: boolean) { 104 | if (obj instanceof Opt) { 105 | if (obj.cached) { 106 | return; 107 | } else { 108 | if (initial) { 109 | return obj.initial; 110 | } else { 111 | return obj.get(); 112 | } 113 | } 114 | } 115 | 116 | if (typeof obj !== "object") return; 117 | 118 | const newObj = {}; 119 | 120 | Object.keys(obj).forEach((key) => { 121 | newObj[key] = transformObject(obj[key], initial); 122 | }); 123 | 124 | const length = Object.keys(JSON.parse(JSON.stringify(newObj))).length; 125 | 126 | return length > 0 ? newObj : undefined; 127 | } 128 | 129 | function deepMerge(target: object, source: object) { 130 | if (typeof target !== "object" || target === null) { 131 | return source; 132 | } 133 | 134 | if (typeof source !== "object" || source === null) { 135 | return source; 136 | } 137 | 138 | const result = Array.isArray(target) ? [] : { ...target }; 139 | 140 | for (const key in source) { 141 | if (source.hasOwnProperty(key)) { 142 | if (Array.isArray(source[key])) { 143 | result[key] = [...source[key]]; 144 | } else if (typeof source[key] === "object" && source[key] !== null) { 145 | result[key] = deepMerge(target[key], source[key]); 146 | } else { 147 | result[key] = source[key]; 148 | } 149 | } 150 | } 151 | 152 | return result; 153 | } 154 | 155 | export function mkOptions(configFile: string, object: T) { 156 | for (const opt of getOptions(object)) { 157 | opt.init(configFile); 158 | } 159 | 160 | ensureDirectory(configFile.split("/").slice(0, -1).join("/")); 161 | const defaultConfig = transformObject(object, true); 162 | const configVar = Variable(transformObject(object)); 163 | 164 | if (GLib.file_test(configFile, GLib.FileTest.EXISTS)) { 165 | let configData: object; 166 | try { 167 | configData = JSON.parse(readFile(configFile) || "{}"); 168 | } catch { 169 | configData = {}; 170 | } 171 | configVar.set(deepMerge(configVar.get(), configData)); 172 | } 173 | 174 | function updateConfig(oldConfig: object, newConfig: object, path = "") { 175 | for (const key in newConfig) { 176 | const fullPath = path ? `${path}.${key}` : key; 177 | if ( 178 | typeof newConfig[key] === "object" && 179 | !Array.isArray(newConfig[key]) 180 | ) { 181 | updateConfig(oldConfig[key], newConfig[key], fullPath); 182 | } else if ( 183 | JSON.stringify(oldConfig[key]) != JSON.stringify(newConfig[key]) 184 | ) { 185 | const conf = getOptions(object).find((c) => c.id == fullPath); 186 | console.log(`${fullPath} updated`); 187 | if (conf) { 188 | const newC = configVar.get(); 189 | setNestedValue(newC, fullPath, newConfig[key]); 190 | configVar.set(newC); 191 | conf.set(newConfig[key]); 192 | } 193 | } 194 | } 195 | } 196 | 197 | monitorFile(configFile, (_, event) => { 198 | if (event == Gio.FileMonitorEvent.ATTRIBUTE_CHANGED) { 199 | let cache: object; 200 | try { 201 | cache = JSON.parse(readFile(configFile) || "{}"); 202 | } catch { 203 | cache = {}; 204 | } 205 | updateConfig(configVar.get(), deepMerge(defaultConfig, cache)); 206 | } 207 | }); 208 | 209 | return Object.assign(object, { 210 | configFile, 211 | handler(deps: string[], callback: () => void) { 212 | for (const opt of getOptions(object)) { 213 | if (deps.some((i) => opt.id.startsWith(i))) opt.subscribe(callback); 214 | } 215 | }, 216 | }); 217 | } 218 | -------------------------------------------------------------------------------- /utils/powermenu.ts: -------------------------------------------------------------------------------- 1 | import GObject, { property, register } from "astal/gobject"; 2 | import { App } from "astal/gtk4"; 3 | 4 | const options = { 5 | sleep: "systemctl suspend", 6 | reboot: "systemctl reboot", 7 | logout: "pkill Hyprland", 8 | shutdown: "shutdown now", 9 | }; 10 | 11 | @register({ GTypeName: "Powermenu" }) 12 | export default class Powermenu extends GObject.Object { 13 | static instance: Powermenu; 14 | 15 | static get_default() { 16 | if (!this.instance) this.instance = new Powermenu(); 17 | return this.instance; 18 | } 19 | 20 | #title = ""; 21 | #cmd = ""; 22 | 23 | @property(String) 24 | get title() { 25 | return this.#title; 26 | } 27 | 28 | @property(String) 29 | get cmd() { 30 | return this.#cmd; 31 | } 32 | 33 | action(action: string) { 34 | [this.#cmd, this.#title] = { 35 | sleep: [options.sleep, "Sleep"], 36 | reboot: [options.reboot, "Reboot"], 37 | logout: [options.logout, "Log Out"], 38 | shutdown: [options.shutdown, "Shutdown"], 39 | }[action]!; 40 | 41 | this.notify("cmd"); 42 | this.notify("title"); 43 | App.get_window("powermenu")?.hide(); 44 | App.get_window("verification")?.show(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /utils/screenrecord.ts: -------------------------------------------------------------------------------- 1 | import GObject, { register, GLib, property } from "astal/gobject"; 2 | import { bash, ensureDirectory, notifySend, now, sh } from "."; 3 | import { interval, Time } from "astal"; 4 | 5 | const HOME = GLib.get_home_dir(); 6 | 7 | @register({ GTypeName: "Screenrecord" }) 8 | export default class ScreenRecord extends GObject.Object { 9 | static instance: ScreenRecord; 10 | 11 | static get_default() { 12 | if (!this.instance) this.instance = new ScreenRecord(); 13 | return this.instance; 14 | } 15 | 16 | #recordings = `${HOME}/Videos/Screencasting`; 17 | #screenshots = `${HOME}/Pictures/Screenshots`; 18 | #file = ""; 19 | #interval?: Time; 20 | #recording = false; 21 | #timer = 0; 22 | 23 | @property(Boolean) 24 | get recording() { 25 | return this.#recording; 26 | } 27 | 28 | @property(Number) 29 | get timer() { 30 | return this.#timer; 31 | } 32 | 33 | async start() { 34 | if (this.#recording) return; 35 | 36 | ensureDirectory(this.#recordings); 37 | this.#file = `${this.#recordings}/${now()}.mp4`; 38 | sh( 39 | `wf-recorder -g "${await sh("slurp")}" -f ${this.#file} --pixel-format yuv420p`, 40 | ); 41 | 42 | this.#recording = true; 43 | this.notify("recording"); 44 | 45 | this.#timer = 0; 46 | this.#interval = interval(1000, () => { 47 | this.notify("timer"); 48 | this.#timer++; 49 | }); 50 | } 51 | 52 | async stop() { 53 | if (!this.#recording) return; 54 | 55 | await bash("killall -INT wf-recorder"); 56 | this.#recording = false; 57 | this.notify("recording"); 58 | this.#interval?.cancel(); 59 | 60 | notifySend({ 61 | icon: "folder-videos-symbolic", 62 | appName: "Screen Recorder", 63 | summary: "Screen recording saved", 64 | body: `Available in ${this.#recordings}`, 65 | actions: { 66 | "Show in Files": () => sh(`xdg-open ${this.#recordings}`), 67 | View: () => sh(`xdg-open ${this.#file}`), 68 | }, 69 | }); 70 | } 71 | 72 | async screenshot(full = false) { 73 | const file = `${this.#screenshots}/${now()}.png`; 74 | 75 | ensureDirectory(this.#screenshots); 76 | if (full) { 77 | await sh(`wayshot -f ${file}`); 78 | } else { 79 | const size = await sh("slurp -b#00000066 -w 0"); 80 | if (!size) return; 81 | 82 | await sh(`wayshot -f ${file} -s "${size}"`); 83 | } 84 | 85 | bash(`wl-copy < ${file}`); 86 | 87 | notifySend({ 88 | image: file, 89 | appName: "Screenshot", 90 | summary: "Screenshot saved", 91 | body: `Available in ${this.#screenshots}`, 92 | actions: { 93 | "Show in Files": () => sh(`xdg-open ${this.#screenshots}`), 94 | View: () => sh(`xdg-open ${file}`), 95 | Edit: () => sh(`swappy -f ${file}`), 96 | }, 97 | }); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /utils/styles.ts: -------------------------------------------------------------------------------- 1 | import { GLib } from "astal"; 2 | import { writeFileAsync } from "astal"; 3 | import options from "../options"; 4 | import { bash, gsettings } from "."; 5 | import { App } from "astal/gtk4"; 6 | import { Opt } from "./option"; 7 | 8 | const { theme } = options; 9 | const { window, bar } = theme; 10 | 11 | type ThemeMode = "dark" | "light"; 12 | type ShorthandProperty = { 13 | top: number; 14 | right: number; 15 | bottom?: number; 16 | left?: number; 17 | }; 18 | 19 | async function initGtk(mode: ThemeMode, reset = false) { 20 | const targetDir = `${GLib.get_home_dir()}/.themes/colors.css`; 21 | const colors = theme[mode]; 22 | const defineColor = (key: string, value: string, alpha = 1.0) => 23 | `@define-color ${key} alpha(${value}, ${alpha});`; 24 | 25 | const gtkVar = [ 26 | defineColor("window_bg_color", colors.bg.get(), window.opacity.get()), 27 | defineColor("view_fg_color", colors.fg.get(), window.opacity.get()), 28 | defineColor("view_bg_color", colors.bg.get(), window.opacity.get()), 29 | defineColor("sidebar_bg_color", colors.bg.get(), window.opacity.get()), 30 | defineColor("headerbar_bg_color", colors.bg.get(), window.opacity.get()), 31 | defineColor("popover_bg_color", colors.bg.get(), window.opacity.get()), 32 | ]; 33 | 34 | // generate colors.css in $HOME/.themes/ 35 | await writeFileAsync(targetDir, gtkVar.join("\n")) 36 | .then(() => { 37 | gsettings.set_string("color-scheme", `prefer-${mode}`); 38 | if (reset) { 39 | gsettings.reset("gtk-theme"); 40 | } 41 | gsettings.set_string( 42 | "gtk-theme", 43 | `adw-gtk3${mode === "light" ? "" : "-dark"}`, 44 | ); 45 | }) 46 | .catch(console.error); 47 | } 48 | 49 | function shorthand( 50 | value: number | number[], 51 | length: number, 52 | ): ShorthandProperty { 53 | if (typeof value === "number") { 54 | return length <= 2 55 | ? { top: value, right: value } 56 | : { top: value, right: value, bottom: value, left: value }; 57 | } 58 | 59 | const [top, right = top, bottom = top, left = right] = value; 60 | return length <= 2 ? { top, right } : { top, right, bottom, left }; 61 | } 62 | 63 | function applyOffsets( 64 | short: ShorthandProperty, 65 | element: typeof theme.bar | typeof theme.window, 66 | ) { 67 | const borderWidth = element.border_width.get(); 68 | const shadowHOffset = element.shadow.offset.get()[0]; 69 | const shadowVOffset = element.shadow.offset.get()[1]; 70 | 71 | short.top += borderWidth + (shadowVOffset <= 0 ? Math.abs(shadowVOffset) : 0); 72 | short.right += borderWidth + (shadowHOffset > 0 ? shadowHOffset : 0); 73 | short.bottom! += borderWidth + (shadowVOffset > 0 ? shadowVOffset : 0); 74 | short.left! += 75 | borderWidth + (shadowHOffset <= 0 ? Math.abs(shadowHOffset) : 0); 76 | 77 | return short; 78 | } 79 | 80 | function defineVar(opt: Opt, type = "string", slice = 2, arrayLength = 4) { 81 | const value = opt.get(); 82 | 83 | const typeChecks = { 84 | number_only: (val: any) => typeof val === "number", 85 | number: (val: any) => typeof val === "number", 86 | string: (val: any) => typeof val === "string" || typeof val === "boolean", 87 | number_or_array: (val: any) => 88 | typeof val === "number" || Array.isArray(val), 89 | }; 90 | 91 | if (!typeChecks[type](value)) { 92 | throw new Error( 93 | `Invalid value, ${opt.id} needs ${type.split("_").join(" ")}`, 94 | ); 95 | } 96 | 97 | let modifiedVal: Record | string | unknown; 98 | switch (type) { 99 | case "number": 100 | modifiedVal = `${value}px`; 101 | break; 102 | case "number_or_array": 103 | let short = shorthand(value as number | number[], arrayLength); 104 | 105 | if (opt.id.startsWith("theme.window.margin")) { 106 | short = applyOffsets(short, window); 107 | } 108 | if (opt.id.startsWith("theme.bar.margin")) { 109 | const barPos = options.bar.position.get(); 110 | short.bottom = barPos == "top" ? 0 : short.bottom; 111 | short.top = barPos == "bottom" ? 0 : short.top; 112 | short = applyOffsets(short, bar); 113 | } 114 | modifiedVal = Object.keys(short) 115 | .map((key) => `${short[key]}px`) 116 | .join(" "); 117 | break; 118 | default: 119 | modifiedVal = value; 120 | break; 121 | } 122 | 123 | const key = opt.id.split(".").slice(-slice).join("-").replace("_", "-"); 124 | return `$${key}: ${modifiedVal};`; 125 | } 126 | 127 | async function initScss(mode: ThemeMode) { 128 | const targetDir = `${SRC}/styles/variables.scss`; 129 | const scss = `${SRC}/styles/styles.scss`; 130 | const css = `${GLib.get_tmp_dir()}/styles.css`; 131 | const colors = theme[mode]; 132 | 133 | const scssVar = [ 134 | defineVar(colors.bg, "string", 1), 135 | defineVar(colors.fg, "string", 1), 136 | defineVar(colors.accent, "string", 1), 137 | defineVar(colors.red, "string", 1), 138 | defineVar(window.opacity, "number_only"), 139 | defineVar(window.border_radius, "number_or_array"), 140 | defineVar(window.margin, "number_or_array"), 141 | defineVar(window.padding, "number_or_array"), 142 | defineVar(window.dock_padding, "number_or_array"), 143 | defineVar(window.desktop_clock_padding, "number_or_array"), 144 | defineVar(window.border_width, "number"), 145 | defineVar(window.border_color), 146 | defineVar(window.shadow.offset, "number_or_array", 3, 2), 147 | defineVar(window.shadow.blur, "number", 3), 148 | defineVar(window.shadow.spread, "number", 3), 149 | defineVar(window.shadow.color, "string", 3), 150 | defineVar(window.shadow.opacity, "number_only", 3), 151 | defineVar(options.bar.separator), 152 | defineVar(bar.border_radius, "number_or_array"), 153 | defineVar(bar.bg_color), 154 | defineVar(bar.opacity, "number_only"), 155 | defineVar(bar.margin, "number_or_array"), 156 | defineVar(bar.padding, "number_or_array"), 157 | defineVar(bar.border_width, "number"), 158 | defineVar(bar.border_color), 159 | defineVar(bar.shadow.offset, "number_or_array", 3, 2), 160 | defineVar(bar.shadow.blur, "number", 3), 161 | defineVar(bar.shadow.spread, "number", 3), 162 | defineVar(bar.shadow.color, "string", 3), 163 | defineVar(bar.shadow.opacity, "number_only", 3), 164 | defineVar(bar.button.bg_color, "string", 3), 165 | defineVar(bar.button.fg_color, "string", 3), 166 | defineVar(bar.button.opacity, "number_only", 3), 167 | defineVar(bar.button.padding, "number_or_array", 3), 168 | defineVar(bar.button.border_radius, "number_or_array", 3), 169 | defineVar(bar.button.border_width, "number", 3), 170 | defineVar(bar.button.border_color, "string", 3), 171 | defineVar(bar.button.shadow.offset, "number_or_array", 4, 2), 172 | defineVar(bar.button.shadow.blur, "number", 4), 173 | defineVar(bar.button.shadow.spread, "number", 4), 174 | defineVar(bar.button.shadow.color, "string", 4), 175 | defineVar(bar.button.shadow.opacity, "number_only", 4), 176 | ]; 177 | 178 | await writeFileAsync(targetDir, scssVar.join("\n")).catch(console.error); 179 | await bash(`sass ${scss} ${css}`); 180 | App.apply_css(css, true); 181 | } 182 | 183 | export default async function () { 184 | options.handler(["theme", "bar.position", "bar.separator"], async () => { 185 | const mode = options.theme.mode.get() as ThemeMode; 186 | await initGtk(mode, true).catch(console.error); 187 | await initScss(mode).catch(console.error); 188 | }); 189 | 190 | const mode = options.theme.mode.get() as ThemeMode; 191 | await initGtk(mode).catch(console.error); 192 | await initScss(mode).catch(console.error); 193 | } 194 | -------------------------------------------------------------------------------- /widgets/applauncher/Applauncher.tsx: -------------------------------------------------------------------------------- 1 | import { App, Gtk, hook, Gdk } from "astal/gtk4"; 2 | import { Variable } from "astal"; 3 | import Pango from "gi://Pango"; 4 | import AstalApps from "gi://AstalApps"; 5 | import PopupWindow from "../common/PopupWindow"; 6 | import { Gio } from "astal"; 7 | import options from "../../options"; 8 | import Picture from "../common/Picture"; 9 | 10 | const { wallpaper } = options; 11 | const apps = new AstalApps.Apps(); 12 | const text = Variable(""); 13 | 14 | export const WINDOW_NAME = "applauncher"; 15 | 16 | function hide() { 17 | App.get_window(WINDOW_NAME)?.set_visible(false); 18 | } 19 | 20 | function AppButton({ app }: { app: AstalApps.Application }) { 21 | return ( 22 | 49 | ); 50 | } 51 | 52 | function SearchEntry() { 53 | const onEnter = () => { 54 | apps.fuzzy_query(text.get())?.[0].launch(); 55 | hide(); 56 | }; 57 | 58 | return ( 59 | 60 | 61 | Gio.file_new_for_path(w))} 63 | contentFit={Gtk.ContentFit.COVER} 64 | overflow={Gtk.Overflow.HIDDEN} 65 | /> 66 | 67 | { 74 | hook(self, App, "window-toggled", (_, win) => { 75 | const winName = win.name; 76 | const visible = win.visible; 77 | 78 | if (winName == WINDOW_NAME && visible) { 79 | text.set(""); 80 | self.set_text(""); 81 | self.grab_focus(); 82 | } 83 | }); 84 | }} 85 | onChanged={(self) => text.set(self.text)} 86 | onActivate={onEnter} 87 | /> 88 | 89 | ); 90 | } 91 | 92 | function AppsScrolledWindow() { 93 | const list = text((text) => apps.fuzzy_query(text)); 94 | 95 | return ( 96 | 97 | 98 | {list.as((list) => list.map((app) => ))} 99 | l.length === 0)} 106 | > 107 | 111 | 113 | 114 | 115 | ); 116 | } 117 | 118 | export default function Applauncher(_gdkmonitor: Gdk.Monitor) { 119 | return ( 120 | 121 | 126 | 127 | 128 | 129 | 130 | ); 131 | } 132 | -------------------------------------------------------------------------------- /widgets/bar/Bar.tsx: -------------------------------------------------------------------------------- 1 | import { App, Astal, Gtk, Gdk } from "astal/gtk4"; 2 | import TimePanelButton from "./TimePanelButton"; 3 | import WorkspacesPanelButton from "./WorkspacesPanelButton"; 4 | import NetworkSpeedPanelButton from "./NetworkSpeedPanelButton"; 5 | import RecordIndicatorPanelButton from "./RecordIndicatorPanelButton"; 6 | import LauncherPanelButton from "./LauncherPanelButton"; 7 | import NotifPanelButton from "./NotifPanelButton"; 8 | import QSPanelButton from "./QSPanelButton"; 9 | import { separatorBetween } from "../../utils"; 10 | import options from "../../options"; 11 | import { idle } from "astal"; 12 | import { windowAnimation } from "../../utils/hyprland"; 13 | import { WindowProps } from "astal/gtk4/widget"; 14 | import TrayPanelButton from "./TrayPanelButton"; 15 | 16 | const { bar } = options; 17 | const { start, center, end } = bar; 18 | 19 | const panelButton = { 20 | launcher: () => , 21 | workspace: () => , 22 | time: () => , 23 | notification: () => , 24 | network_speed: () => , 25 | quicksetting: () => , 26 | }; 27 | 28 | function Start() { 29 | return ( 30 | 31 | {start((s) => [ 32 | ...separatorBetween( 33 | s.map((s) => panelButton[s]()), 34 | Gtk.Orientation.VERTICAL, 35 | ), 36 | , 37 | ])} 38 | 39 | ); 40 | } 41 | 42 | function Center() { 43 | return ( 44 | 45 | {center((c) => 46 | separatorBetween( 47 | c.map((w) => panelButton[w]()), 48 | Gtk.Orientation.VERTICAL, 49 | ), 50 | )} 51 | 52 | ); 53 | } 54 | 55 | function End() { 56 | return ( 57 | 58 | {end((e) => 59 | separatorBetween( 60 | e.map((w) => panelButton[w]()), 61 | Gtk.Orientation.VERTICAL, 62 | ), 63 | )} 64 | 65 | ); 66 | } 67 | 68 | type BarProps = WindowProps & { 69 | gdkmonitor: Gdk.Monitor; 70 | animation: string; 71 | }; 72 | function Bar({ gdkmonitor, ...props }: BarProps) { 73 | const { TOP, LEFT, RIGHT, BOTTOM } = Astal.WindowAnchor; 74 | const anc = bar.position.get() == "top" ? TOP : BOTTOM; 75 | 76 | return ( 77 | { 80 | // problem when change bar size via margin/padding live 81 | // https://github.com/wmww/gtk4-layer-shell/issues/60 82 | self.set_default_size(1, 1); 83 | }} 84 | name={"bar"} 85 | namespace={"bar"} 86 | gdkmonitor={gdkmonitor} 87 | anchor={anc | LEFT | RIGHT} 88 | exclusivity={Astal.Exclusivity.EXCLUSIVE} 89 | application={App} 90 | {...props} 91 | > 92 | 93 | 94 |
95 | 96 | 97 | 98 | ); 99 | } 100 | 101 | export default function (gdkmonitor: Gdk.Monitor) { 102 | ; 103 | 104 | bar.position.subscribe(() => { 105 | App.toggle_window("bar"); 106 | const barWindow = App.get_window("bar")!; 107 | barWindow.set_child(null); 108 | App.remove_window(App.get_window("bar")!); 109 | idle(() => { 110 | ; 111 | windowAnimation(); 112 | }); 113 | }); 114 | } 115 | -------------------------------------------------------------------------------- /widgets/bar/LauncherPanelButton.tsx: -------------------------------------------------------------------------------- 1 | import PanelButton from "../common/PanelButton"; 2 | import { WINDOW_NAME } from "../applauncher/Applauncher"; 3 | import { App } from "astal/gtk4"; 4 | 5 | export default function LauncherPanelButton() { 6 | return ( 7 | App.toggle_window(WINDOW_NAME)} 10 | > 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /widgets/bar/NetworkSpeedPanelButton.tsx: -------------------------------------------------------------------------------- 1 | import networkSpeed from "../../utils/networkspeed"; 2 | import PanelButton from "../common/PanelButton"; 3 | 4 | export default function NetworkSpeedPanelButton() { 5 | return ( 6 | 7 | 8 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /widgets/bar/NotifPanelButton.tsx: -------------------------------------------------------------------------------- 1 | import AstalNotifd from "gi://AstalNotifd"; 2 | import PanelButton from "../common/PanelButton"; 3 | import { App } from "astal/gtk4"; 4 | import { bind, Variable } from "astal"; 5 | import AstalApps from "gi://AstalApps"; 6 | import { WINDOW_NAME } from "../notification/NotificationWindow"; 7 | 8 | const notifd = AstalNotifd.get_default(); 9 | 10 | function NotifIcon() { 11 | const getVisible = () => 12 | notifd.dont_disturb ? true : notifd.notifications.length <= 0; 13 | 14 | const visibility = Variable(getVisible()) 15 | .observe(notifd, "notify::dont-disturb", () => { 16 | return getVisible(); 17 | }) 18 | .observe(notifd, "notify::notifications", () => getVisible()); 19 | 20 | return ( 21 | visibility.drop()} 23 | visible={visibility()} 24 | cssClasses={["icon"]} 25 | iconName={bind(notifd, "dont_disturb").as( 26 | (dnd) => `notifications-${dnd ? "disabled-" : ""}symbolic`, 27 | )} 28 | /> 29 | ); 30 | } 31 | 32 | export default function NotifPanelButton() { 33 | const apps = new AstalApps.Apps(); 34 | const substitute = { 35 | "Screen Recorder": "screencast-recorded-symbolic", 36 | Screenshot: "screenshot-recorded-symbolic", 37 | Hyprpicker: "color-select-symbolic", 38 | }; 39 | 40 | return ( 41 | { 44 | App.toggle_window(WINDOW_NAME); 45 | }} 46 | > 47 | {bind(notifd, "dontDisturb").as((dnd) => 48 | !dnd ? ( 49 | 50 | {bind(notifd, "notifications").as((n) => { 51 | if (n.length > 0) { 52 | return [ 53 | ...n.slice(0, 3).map((e) => { 54 | const getFallback = (appName: string) => { 55 | const getApp = apps.fuzzy_query(appName); 56 | if (getApp.length != 0) { 57 | return getApp[0].get_icon_name(); 58 | } 59 | return "unknown"; 60 | }; 61 | const fallback = 62 | e.app_icon.trim() === "" 63 | ? getFallback(e.app_name) 64 | : e.app_icon; 65 | const icon = substitute[e.app_name] ?? fallback; 66 | return ; 67 | }), 68 | 78 | ) : ( 79 | 80 | ), 81 | )} 82 | 83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /widgets/bar/QSPanelButton.tsx: -------------------------------------------------------------------------------- 1 | import { App } from "astal/gtk4"; 2 | import PanelButton from "../common/PanelButton"; 3 | import { WINDOW_NAME } from "../quicksettings/QSWindow"; 4 | import AstalBattery from "gi://AstalBattery"; 5 | import AstalWp from "gi://AstalWp"; 6 | import { bind, Variable } from "astal"; 7 | import AstalPowerProfiles from "gi://AstalPowerProfiles"; 8 | import AstalNetwork from "gi://AstalNetwork"; 9 | import AstalBluetooth from "gi://AstalBluetooth"; 10 | 11 | function NetworkIcon() { 12 | const network = AstalNetwork.get_default(); 13 | if (!network.wifi) 14 | return ; 15 | 16 | const icon = Variable.derive( 17 | [ 18 | bind(network, "primary"), 19 | bind(network.wifi, "iconName"), 20 | bind(network.wired, "iconName"), 21 | ], 22 | (primary, wifiIcon, wiredIcon) => { 23 | if ( 24 | primary == AstalNetwork.Primary.WIRED || 25 | primary == AstalNetwork.Primary.UNKNOWN 26 | ) { 27 | return wiredIcon; 28 | } else { 29 | return wifiIcon; 30 | } 31 | }, 32 | ); 33 | return icon.drop()} />; 34 | } 35 | 36 | export default function QSPanelButton() { 37 | const battery = AstalBattery.get_default(); 38 | const bluetooth = AstalBluetooth.get_default(); 39 | const wp = AstalWp.get_default(); 40 | const speaker = wp?.audio.defaultSpeaker!; 41 | const powerprofile = AstalPowerProfiles.get_default(); 42 | 43 | return ( 44 | { 47 | App.toggle_window(WINDOW_NAME); 48 | }} 49 | > 50 | 51 | 52 | 56 | 60 | 61 | p === "power-saver", 64 | )} 65 | iconName={`power-profile-power-saver-symbolic`} 66 | /> 67 | 71 | 72 | 73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /widgets/bar/RecordIndicatorPanelButton.tsx: -------------------------------------------------------------------------------- 1 | import { bind } from "astal"; 2 | import ScreenRecord from "../../utils/screenrecord"; 3 | import PanelButton from "../common/PanelButton"; 4 | import { Gtk } from "astal/gtk4"; 5 | 6 | export default function RecordIndicatorPanelButton() { 7 | const screenRecord = ScreenRecord.get_default(); 8 | return ( 9 | 10 | 11 | screenRecord.stop().catch(() => "")}> 12 | 13 | 14 | 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /widgets/bar/TimePanelButton.tsx: -------------------------------------------------------------------------------- 1 | import { App } from "astal/gtk4"; 2 | import { time } from "../../utils"; 3 | import PanelButton from "../common/PanelButton"; 4 | import { WINDOW_NAME } from "../datemenu/DateMenu"; 5 | 6 | export default function TimePanelButton({ format = "%H:%M" }) { 7 | return ( 8 | App.toggle_window(WINDOW_NAME)} 11 | > 12 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /widgets/bar/TrayPanelButton.tsx: -------------------------------------------------------------------------------- 1 | import { bind } from "astal"; 2 | import { Gtk } from "astal/gtk4"; 3 | import AstalTray from "gi://AstalTray?version=0.1"; 4 | 5 | export default function TrayPanelButton() { 6 | const tray = AstalTray.get_default(); 7 | return ( 8 | 9 | {bind(tray, "items").as((items) => 10 | items.map((item) => ( 11 | { 13 | self.insert_action_group("dbusmenu", item.actionGroup); 14 | }} 15 | tooltipText={bind(item, "tooltipMarkup")} 16 | > 17 | 18 | {Gtk.PopoverMenu.new_from_model(item.menuModel)} 19 | 20 | )), 21 | )} 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /widgets/bar/WorkspacesPanelButton.tsx: -------------------------------------------------------------------------------- 1 | import { Gtk } from "astal/gtk4"; 2 | import AstalHyprland from "gi://AstalHyprland"; 3 | import { range } from "../../utils"; 4 | import { bind } from "astal"; 5 | import { Variable } from "astal"; 6 | import { ButtonProps } from "astal/gtk4/widget"; 7 | 8 | type WsButtonProps = ButtonProps & { 9 | ws: AstalHyprland.Workspace; 10 | }; 11 | 12 | function WorkspaceButton({ ws, ...props }: WsButtonProps) { 13 | const hyprland = AstalHyprland.get_default(); 14 | const classNames = Variable.derive( 15 | [bind(hyprland, "focusedWorkspace"), bind(hyprland, "clients")], 16 | (fws, _) => { 17 | const classes = ["workspace-button"]; 18 | 19 | const active = fws.id == ws.id; 20 | active && classes.push("active"); 21 | 22 | const occupied = hyprland.get_workspace(ws.id)?.get_clients().length > 0; 23 | occupied && classes.push("occupied"); 24 | return classes; 25 | }, 26 | ); 27 | 28 | return ( 29 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /widgets/common/Picture.ts: -------------------------------------------------------------------------------- 1 | import { ConstructProps } from "astal/gtk4"; 2 | import { astalify, Gtk } from "astal/gtk4"; 3 | 4 | export type PictureProps = ConstructProps< 5 | Gtk.Picture, 6 | Gtk.Picture.ConstructorProps 7 | >; 8 | 9 | const Picture = astalify( 10 | Gtk.Picture, 11 | ); 12 | 13 | export default Picture; 14 | -------------------------------------------------------------------------------- /widgets/common/PopupWindow.tsx: -------------------------------------------------------------------------------- 1 | import { App, Astal, Gdk, Gtk } from "astal/gtk4"; 2 | import { WindowProps } from "astal/gtk4/widget"; 3 | 4 | function Padding({ winName }: { winName: string }) { 5 | return ( 6 | 74 | ); 75 | } 76 | 77 | function AppsList() { 78 | const pinnedApps = Variable.derive([options.dock.pinned], (p) => { 79 | return p 80 | .map((term) => ({ 81 | app: application.list.find((e) => e.entry.split(".desktop")[0] == term), 82 | term, 83 | })) 84 | .filter(({ app }) => app); 85 | }); 86 | 87 | return ( 88 | 89 | {pinnedApps((apps) => 90 | apps.map(({ app, term }) => ( 91 | { 96 | for (const client of hyprland.get_clients()) { 97 | if (client.class.toLowerCase().includes(term.toLowerCase())) { 98 | timeout(1, () => { 99 | App.get_window("dock-hover")!.set_visible(true); 100 | }); 101 | return client.focus(); 102 | } 103 | } 104 | 105 | app!.launch(); 106 | }} 107 | /> 108 | )), 109 | )} 110 | {bind(hyprland, "clients").as((clients) => 111 | clients 112 | .reverse() 113 | .map((client) => { 114 | for (const appClass of options.dock.pinned.get()) { 115 | if (client.class.toLowerCase().includes(appClass.toLowerCase())) { 116 | return; 117 | } 118 | } 119 | 120 | for (const app of application.list) { 121 | if ( 122 | client.class && 123 | app.entry 124 | .split(".desktop")[0] 125 | .toLowerCase() 126 | .match(client.class.toLowerCase()) 127 | ) { 128 | return ( 129 | { 132 | timeout(1, () => { 133 | App.get_window("dock-hover")!.set_visible(true); 134 | }); 135 | client.focus(); 136 | }} 137 | term={client.class} 138 | client={client} 139 | /> 140 | ); 141 | } 142 | } 143 | }) 144 | .filter((item) => item !== undefined), 145 | )} 146 | 147 | ); 148 | } 149 | 150 | function MediaPlayer({ player }) { 151 | if (!player) { 152 | return ; 153 | } 154 | const title = bind(player, "title").as((t) => t || "Unknown Track"); 155 | const artist = bind(player, "artist").as((a) => a || "Unknown Artist"); 156 | const coverArt = bind(player, "coverArt"); 157 | 158 | const playIcon = bind(player, "playbackStatus").as((s) => 159 | s === AstalMpris.PlaybackStatus.PLAYING 160 | ? "media-playback-pause-symbolic" 161 | : "media-playback-start-symbolic", 162 | ); 163 | 164 | return ( 165 | 166 | 172 | 173 | 181 | 189 | 197 | 198 | ); 199 | } 200 | 201 | export default function DockApps() { 202 | const mpris = AstalMpris.get_default(); 203 | return ( 204 | 205 | 206 | {bind(mpris, "players").as((players) => ( 207 | 208 | ))} 209 | 210 | 215 | 216 | ); 217 | } 218 | -------------------------------------------------------------------------------- /widgets/notification/Notification.tsx: -------------------------------------------------------------------------------- 1 | import { Gtk } from "astal/gtk4"; 2 | import { GLib } from "astal"; 3 | import Pango from "gi://Pango"; 4 | import AstalNotifd from "gi://AstalNotifd"; 5 | 6 | const time = (time: number, format = "%H:%M") => 7 | GLib.DateTime.new_from_unix_local(time).format(format); 8 | 9 | const isIcon = (icon: string) => { 10 | const iconTheme = new Gtk.IconTheme(); 11 | return iconTheme.has_icon(icon); 12 | }; 13 | 14 | const fileExists = (path: string) => GLib.file_test(path, GLib.FileTest.EXISTS); 15 | 16 | const urgency = (n: AstalNotifd.Notification) => { 17 | const { LOW, NORMAL, CRITICAL } = AstalNotifd.Urgency; 18 | 19 | switch (n.urgency) { 20 | case LOW: 21 | return "low"; 22 | case CRITICAL: 23 | return "critical"; 24 | case NORMAL: 25 | default: 26 | return "normal"; 27 | } 28 | }; 29 | 30 | export default function Notification({ 31 | n, 32 | showActions = true, 33 | }: { 34 | n: AstalNotifd.Notification; 35 | showActions?: boolean; 36 | }) { 37 | return ( 38 | 44 | 45 | 46 | {(n.appIcon || n.desktopEntry) && ( 47 | 52 | )} 53 | 68 | 69 | 70 | {n.image && fileExists(n.image) && ( 71 | 72 | 73 | 74 | )} 75 | {n.image && isIcon(n.image) && ( 76 | 77 | 83 | 84 | )} 85 | 86 | 105 | 106 | {showActions && n.get_actions().length > 0 && ( 107 | 108 | {n.get_actions().map(({ label, id }) => ( 109 | 112 | ))} 113 | 114 | )} 115 | 116 | 117 | ); 118 | } 119 | -------------------------------------------------------------------------------- /widgets/notification/NotificationPopup.tsx: -------------------------------------------------------------------------------- 1 | import { timeout } from "astal"; 2 | import { App, Astal, hook, Gdk } from "astal/gtk4"; 3 | import AstalNotifd from "gi://AstalNotifd"; 4 | import Notification from "./Notification"; 5 | import { sendBatch } from "../../utils/hyprland"; 6 | 7 | export default function NotificationPopup(gdkmonitor: Gdk.Monitor) { 8 | const { TOP } = Astal.WindowAnchor; 9 | const notifd = AstalNotifd.get_default(); 10 | 11 | return ( 12 | { 15 | sendBatch([`layerrule animation slide top, ${self.namespace}`]); 16 | const notificationQueue: number[] = []; 17 | let isProcessing = false; 18 | 19 | hook(self, notifd, "notified", (_, id: number) => { 20 | if ( 21 | notifd.dont_disturb && 22 | notifd.get_notification(id).urgency != AstalNotifd.Urgency.CRITICAL 23 | ) { 24 | return; 25 | } 26 | notificationQueue.push(id); 27 | processQueue(); 28 | }); 29 | 30 | hook(self, notifd, "resolved", (_, __) => { 31 | self.visible = false; 32 | isProcessing = false; 33 | timeout(300, () => { 34 | processQueue(); 35 | }); 36 | }); 37 | 38 | function processQueue() { 39 | if (isProcessing || notificationQueue.length === 0) return; 40 | isProcessing = true; 41 | const id = notificationQueue.shift(); 42 | 43 | self.set_child( 44 | 45 | {Notification({ n: notifd.get_notification(id!) })} 46 | 47 | , 48 | ); 49 | self.visible = true; 50 | 51 | timeout(5000, () => { 52 | self.visible = false; 53 | isProcessing = false; 54 | self.set_child(null); 55 | timeout(300, () => { 56 | processQueue(); 57 | }); 58 | }); 59 | } 60 | }} 61 | gdkmonitor={gdkmonitor} 62 | application={App} 63 | anchor={TOP} 64 | > 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /widgets/notification/NotificationWindow.tsx: -------------------------------------------------------------------------------- 1 | import AstalNotifd from "gi://AstalNotifd"; 2 | import PopupWindow from "../common/PopupWindow"; 3 | import { App, Gtk, Gdk } from "astal/gtk4"; 4 | import { bind, Variable } from "astal"; 5 | import Notification from "./Notification"; 6 | import options from "../../options"; 7 | 8 | export const WINDOW_NAME = "notifications"; 9 | const notifd = AstalNotifd.get_default(); 10 | const { bar } = options; 11 | 12 | const layout = Variable.derive( 13 | [bar.position, bar.start, bar.center, bar.end], 14 | (pos, start, center, end) => { 15 | if (start.includes("notification")) return `${pos}_left`; 16 | if (center.includes("notification")) return `${pos}_center`; 17 | if (end.includes("notification")) return `${pos}_right`; 18 | 19 | return `${pos}_center`; 20 | }, 21 | ); 22 | 23 | function NotifsScrolledWindow() { 24 | const notifd = AstalNotifd.get_default(); 25 | return ( 26 | 27 | 28 | {bind(notifd, "notifications").as((notifs) => 29 | notifs.map((e) => ), 30 | )} 31 | n.length === 0)} 38 | > 39 | 43 | 45 | 46 | 47 | ); 48 | } 49 | 50 | function DNDButton() { 51 | return ( 52 | 78 | ); 79 | } 80 | 81 | function NotificationWindow(_gdkmonitor: Gdk.Monitor) { 82 | return ( 83 | layout.drop()} 88 | > 89 | 94 | 95 | 99 | 100 | 101 | 102 | 103 | ); 104 | } 105 | 106 | export default function (_gdkmonitor: Gdk.Monitor) { 107 | NotificationWindow(_gdkmonitor); 108 | 109 | layout.subscribe(() => { 110 | App.remove_window(App.get_window(WINDOW_NAME)!); 111 | NotificationWindow(_gdkmonitor); 112 | }); 113 | } 114 | -------------------------------------------------------------------------------- /widgets/powermenu/PowerMenu.tsx: -------------------------------------------------------------------------------- 1 | import { Astal, Gtk, Gdk } from "astal/gtk4"; 2 | import Powermenu from "../../utils/powermenu"; 3 | import PopupWindow from "../common/PopupWindow"; 4 | import { FlowBox } from "../common/FlowBox"; 5 | 6 | const powermenu = Powermenu.get_default(); 7 | export const WINDOW_NAME = "powermenu"; 8 | 9 | const icons = { 10 | sleep: "weather-clear-night-symbolic", 11 | reboot: "system-reboot-symbolic", 12 | logout: "system-log-out-symbolic", 13 | shutdown: "system-shutdown-symbolic", 14 | }; 15 | 16 | function SysButton({ action, label }: { action: string; label: string }) { 17 | return ( 18 | 27 | ); 28 | } 29 | 30 | export default function PowerMenu(_gdkmonitor: Gdk.Monitor) { 31 | return ( 32 | 37 | { 44 | self.connect("child-activated", (_, child) => { 45 | child.get_child()?.activate(); 46 | }); 47 | }} 48 | homogeneous 49 | > 50 | 51 | 52 | 53 | 54 | 55 | 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /widgets/powermenu/VerificationWindow.tsx: -------------------------------------------------------------------------------- 1 | import Powermenu from "../../utils/powermenu"; 2 | import PopupWindow from "../common/PopupWindow"; 3 | import { App, Astal, Gdk, hook } from "astal/gtk4"; 4 | import { exec, bind } from "astal"; 5 | 6 | const WINDOW_NAME = "verification"; 7 | 8 | export default function VerificationWindow(_gdkmonitor: Gdk.Monitor) { 9 | const powermenu = Powermenu.get_default(); 10 | 11 | return ( 12 | 17 | 22 |