Merge pull request #3 from retrozinndev/ryoland

Migrate to Aylur's GTK Shell + Astal
This commit is contained in:
João Dias
2025-05-13 15:26:04 -03:00
committed by GitHub
241 changed files with 8995 additions and 3254 deletions
+2
View File
@@ -2,5 +2,7 @@
# Ignore personal configurations (e.g.: hyprpaper.conf, input.conf) # Ignore personal configurations (e.g.: hyprpaper.conf, input.conf)
hypr/hyprpaper.conf hypr/hyprpaper.conf
hypr/input.conf hypr/input.conf
hypr/hyprsunset.conf
*_wal.scss
*.log *.log
+79 -37
View File
@@ -1,68 +1,109 @@
# Retrozinn's Hyprland Dots # colorshell
My customized Hyprland dotfiles that I keep improving almost everyday 🤩 ! <p>(previously retrozinndev/Hyprland-Dots)</p>
> [!warning] My Hyprland desktop shell that I keep improving almost everyday! 🤩 (i love doing this) <br>
> This is the branch for the Elkowar's Wacky Widgets(eww) edition! If you're <br>
> here for the waybar version instead, go to the [`waybar-edition`](https://github.com/retrozinndev/Hyprland-Dots/tree/waybar-edition) branch!
> [!note] This repository includes a desktop shell made with [GTK], using [Astal] and [AGS] + [TypeScript].
> I'm currently developing an [AGS](https://aylur.github.io/ags)+[Astal](https://aylur.github.io/astal) version of these dots! Peek a little on <br> It really took me a lot of time to make this, so please star the repo if you like it! :star:
> how developement is going in the [`ryoland`](https://github.com/retrozinndev/Hyprland-Dots/tree/ryoland) branch. <br>
## 🌄 Screenshots ## 🌄 Screenshots
<div align="center" ![Kitty](repo/shots/desktop.png)
![Widgets](repo/shots/widgets.png)
<img src="repo/shots/desktop.png" width="45%"> ![Runner](repo/shots/runner.png)
<img src="repo/shots/control_center.png" width="45%"> ![Browser + Neovim](repo/shots/browser-neovim.png)
<img src="repo/shots/showing_off_rice.png" width="45%">
<img src="repo/shots/runner.png" width="45%">
<img src="repo/shots/floating_github.png" width="45%">
<img src="repo/shots/volume_control.png" width="45%">
<img src="repo/shots/floating_media.png" width="45%">
<img src="repo/shots/power_menu.png" width="45%">
more shots in [`repo/shots`](https://github.com/retrozinndev/Hyprland-Dots/tree/ryo/repo/shots)
</div>
## 🎨 Colors ## 🎨 Colors
All the colors of the interface are dynamically generated from your wallpaper! This is possible by using [pywal16] (fork of pywal), a cli tool to generate color schemes on the fly. All the shell colors are dynamically generated from your wallpaper!
This is possible by using [pywal16](fork of the archived [pywal](https://github.com/dylanaraps/pywal) project), a cli tool to generate color schemes on the fly.
## 🖼️ Wallpapers ## 🖼️ Wallpapers
When you're at the [Installation](#Installation) process, you can choose whether to install my wallpapers. If you chose to install, you can select any of them by clicking to change wallpaper in the Control Center. Or if you haven't chose to install, you can create the directory `~/wallpapers` in your home directory `~` and put an image you want to use as wallpaper and choose it using the menu inside control center and also by pressing <kbd>SUPER</kbd> + <kbd>W</kbd>! When you're at the [Installation](#Installation) process, you can choose whether to install the wallpapers.
Or if you haven't, you can just create a directory `~/wallpapers` in your home `~` and put images you want to use as wallpapers!
See more bindings inside the `~/.config/hypr/bindings.conf` file or check the [Wiki/Usage] page! You can select any of the images inside `~/wallpapers` by pressing <kbd>SUPER</kbd> + <kbd>W</kbd> or by accessing the
Control Center and clicking in the image icon on top.
### ️ Source ### ️ Source
All wallpapers inside this repo are not made by me! You can find all sources inside the [`WALLPAPERS.md`](https://github.com/retrozinndev/Hyprland-Dots/blob/ryo/WALLPAPERS.md) file. None of the wallpapers available in this repo are made by me! You can find sources inside the [`WALLPAPERS.md`](https://github.com/retrozinndev/Hyprland-Dots/blob/ryo/WALLPAPERS.md) file. (it took me a lot of time to make this sources list 😭)
### ✔️ What's included in this shell
- Pretty Top-Bar
- Apps button(basically the "start menu", opens the full-screen app launcher)
- Workspace indicator(contains icon of last used application on each)
- Focused Client(Window) information(title, class and icon)
- Clock(with date)
- Media(shows only when media is being played)
- Tray(Applications running in the background)
- Status (volume information, bluetooth, network and notification status)
- Control Center
- Volume Controls (Microphone and Speaker)
- Volume Mixer(per-app volume)
- Pages(the thing that shows up when you click the arrow on a tile)
- Bluetooth devices
- Network devices
- Night Light controls
- Tiles
- Screen Recording
- Bluetooth
- Night Light
- Network(wifi needs work, i don't have wifi in my machine)
- Don't Disturb(disables notification popups)
- Center Window(clock, calendar + media management)
- Notifications with support for application actions + Notification History
- Localization(see [🌐 Internationalization](#-internationalization) for available languages)
- Application Runner with support for plugins ([anyrun](https://github.com/anyrun-org/anyrun)-like)
- Shell(`!`): Run shell commands with the user shell
- Clipboard(`>`): Search through your clipboard history
- Wallpapers(`#`): Search and select to change wallpaper
- Media(`:`): Control playing media
- Search(`?`): Search something on the internet with your default browser
- Gnome-like application runner(the fullscreen one)
- Support for your multiple monitors
## ⌨️ Binds
You can see pre-configured bindings in the [Wiki/Bindings] page!
## 🌐 Internationalization
Colorshell supports i18n! The shell automatically matches the shell language with the system's, if available. <br>
Currently, there's support for the following languages:
- English (United States), maintained by [@retrozinndev](https://github.com/retrozinndev)
- Português (Brasil), maintained by [@retrozinndev](https://github.com/retrozinndev)
Don't see your language here? You can contribute and make translations too! <br>
You can do so by forking this repository, translating the shell in your fork and then opening a pull request to this repository, simple as that!
(I'll create a more detailed guide for that soon)
## ⚙️ Installation ## ⚙️ Installation
See the Installation Guide on [Wiki/Installation]. See the Installation Guide on [Wiki/Installation]. (needs updates, shell was just launched)
### 🎉 Tools ## 🎉 Tools
- Browser: [Zen Browser] - Browser: [Zen Browser]
- Text Editor: [Neovim], my config is [here](https://github.com/retrozinndev/nvim-conf.lua) - Text Editor: [Neovim], my config is [here](https://github.com/retrozinndev/nvim-conf.lua)
- Terminal Emulator: [Kitty] - Terminal Emulator: [Kitty]
- Shell: [Nushell] - Terminal shell: [Nushell]
- See more on the [wiki]!
## ❗ Issues ## ❗ Issues
Having issues? Please create a [new Issue] here, I'll be happy to help you out! Having issues? Please create a [new Issue] here, I'll be happy to help you out!
## 📜 License ## 📜 License
This repo is licensed under the [MIT License]. This repo is licensed under the [MIT License], project is made and maintained by [retrozinndev](https://github.com/retrozinndev).
## 🌠 Stargazers Graph ## 🌠 Stargazers
Thanks to everyone who starred my dotfiles! 💖 Thanks to everyone who starred my project! 💖
[![Stargazers over time](https://starchart.cc/retrozinndev/Hyprland-Dots.svg?background=%2324292e&axis=%23fafbfc&line=%232dba4e)](https://starchart.cc/retrozinndev/Hyprland-Dots) [![Stargazers over time](
https://starchart.cc/retrozinndev/Hyprland-Dots.svg?background=%2324292e&axis=%23fafbfc&line=%232dba4e
)](https://starchart.cc/retrozinndev/Hyprland-Dots)
<!-- References of other projects --> <!-- References of other projects -->
[pywal16]: https://github.com/eylles/pywal16 [pywal16]: https://github.com/eylles/pywal16
[zen browser]: https://zen-browser.app [zen browser]: https://zen-browser.app
[neovim]: https://neovim.io [neovim]: https://neovim.io
[nushell]: https://nushell.sh [nushell]: https://nushell.sh
[kitty]: https://sw.kovidgoyal.net/kitty/ [kitty]: https://sw.kovidgoyal.net/kitty
[ags]: https://aylur.github.io/ags
[astal]: https://aylur.github.io/astal
[typescript]: https://typescriptlang.org
[gtk]: https://www.gtk.org
<!-- Web refs --> <!-- Web refs -->
[mit license]: https://en.wikipedia.org/wiki/MIT_License [mit license]: https://en.wikipedia.org/wiki/MIT_License
@@ -75,6 +116,7 @@ Thanks to everyone who starred my dotfiles! 💖
[wiki/dependencies]: https://github.com/retrozinndev/Hyprland-Dots/wiki/Dependencies [wiki/dependencies]: https://github.com/retrozinndev/Hyprland-Dots/wiki/Dependencies
[wiki/usage]: https://github.com/retrozinndev/Hyprland-Dots/wiki/Usage [wiki/usage]: https://github.com/retrozinndev/Hyprland-Dots/wiki/Usage
[wiki/installation]: https://github.com/retrozinndev/Hyprland-Dots/wiki/Installation [wiki/installation]: https://github.com/retrozinndev/Hyprland-Dots/wiki/Installation
[wiki/bindings]: https://github.com/retrozinndev/Hyprland-Dots/wiki/Bindings
<!-- Action Links --> <!-- Actions -->
[new issue]: https://github.com/retrozinndev/Hyprland-Dots/issues/new [new issue]: https://github.com/retrozinndev/Hyprland-Dots/issues/new
+501 -18
View File
@@ -1,40 +1,523 @@
# About Walppapers # About Walppapers
None of them are made by me. You can find their artists, and more wallpapers in None of them are made by me. You can find their artists, and more wallpapers
the links down below. in their source link.
## Bocchi The Rock! Wallpapers ## Bocchi The Rock!
- [Pinterest](https://pinterest.com) <details>
- [Alpha Coders](https://alphacoders.com/bocchi-the-rock!-wallpapers) <summary>
- [Wallpaper Cave](https://wallpapercave.com/bocchi-the-rock-wallpapers) <b>Kessoku Band Rooftop (cropped borders)</b>
- [Wallpaper Flare](https://www.wallpaperflare.com/search?wallpaper=BOCCHI+THE+ROCK%21) </summary>
<img src="wallpapers/Kessoku Band Rooftop.jpeg"></img>
- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1319345)
</details>
<details>
<summary>
<b>Bocchi The Rock! (the wallpaper)</b>
</summary>
<img src="wallpapers/Bocchi The Rock!.png"></img>
- Source: [Twitter/X (artist only, post was deleted)](https://x.com/mofujiro_mofum2)
</details>
<details>
<summary>
<b>Ryo Yamada Maid Dress</b>
</summary>
<img src="wallpapers/Ryo Yamada Maid Dress.png"></img>
- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1363565)
</details>
<details>
<summary>
<b>Ryo Yamada</b>
</summary>
<img src="wallpapers/Ryo Yamada.png"></img>
- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1323120)
</details>
<details>
<summary>
<b>Ryo Vending Machine</b>
</summary>
<img src="wallpapers/Ryo Vending Machine.png"></img>
- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1293921)
</details>
<details>
<summary>
<b>Nijika Train</b>
</summary>
<img src="wallpapers/Nijika Train.jpeg"></img>
- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1304192)
</details>
<details>
<summary>
<b>Nijika Ijichi</b>
</summary>
<img src="wallpapers/Nijika Ijichi.jpg"></img>
- Source: [Wallpaper Flare](https://www.wallpaperflare.com/blonde-nijika-ijichi-bocchi-the-rock-anime-girls-sunset-glow-wallpaper-yjrwx)
</details>
<details>
<summary>
<b>Kita Street</b>
</summary>
<img src="wallpapers/Kita Street.jpeg"></img>
- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1304193)
</details>
<details>
<summary>
<b>Kita-chan!!</b>
</summary>
<img src="wallpapers/Kita-chan!!.jpg"></img>
- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1296783)
</details>
<details>
<summary>
<b>Kikuri Hiroi</b>
</summary>
<img src="wallpapers/Kikuri Hiroi.jpg"></img>
- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1295717)
</details>
<details>
<summary>
<b>Kessoku Band Reunited</b>
</summary>
<img src="wallpapers/Kessoku Band Reunited.jpg"></img>
- Source: [Wallpaper Cave](https://wallpapercave.com/w/wp11695992)
</details>
<details>
<summary>
<b>Kessoku Albums</b>
</summary>
<img src="wallpapers/Kessoku Albums.jpeg"></img>
- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1316133)
</details>
<details>
<summary>
<b>Hitori Gotoh College Corridor</b>
</summary>
<img src="wallpapers/Hitori Gotoh College Corridor.png"></img>
- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1302067)
</details>
<details>
<summary>
<b>Garden Kita</b>
</summary>
<img src="wallpapers/Garden Kita.png"></img>
- Source: [Gruvbox Wallpapers](https://gruvbox-wallpapers.pages.dev)
</details>
<!----------------- -->
## Vocaloid Wallpapers ## Vocaloid Wallpapers
- [Arch Miku (DeviantArt, nesyah)](https://www.deviantart.com/nesyah/art/Arch-linux-feat-Hatsune-Miku-858316759) <details>
- [Others (Alpha Coders)](https://alphacoders.com/vocaloid-wallpapers) <summary>
<b>Arch Linux Miku</b>
</summary>
<img src="wallpapers/Arch Linux Miku.jpg"></img>
## Dan Da Dan Wallpapers - Source: [DeviantArt](https://www.deviantart.com/nesyah/art/Arch-linux-feat-Hatsune-Miku-858316759)
</details>
- [Alpha Coders](https://alphacoders.com/dandadan-wallpapers) <details>
<summary>
<b>Gumi Bridge</b>
</summary>
<img src="wallpapers/Gumi Bridge.jpg"></img>
## Frieren: Beyond Journey's End Wallpapers - Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=593482)
</details>
- [Alpha Coders](https://alphacoders.com/frieren-beyond-journeys-end-wallpapers) <details>
<summary>
<b>Gumi VOCALOID</b>
</summary>
<img src="wallpapers/Gumi VOCALOID.png"></img>
## Hypr-chan Wallpaper - Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=768096)
</details>
- [GitHub (hyprwm/Hyprland)](https://github.com/hyprwm/Hyprland) <details>
<summary>
<b>Miku Stylish with Glasses</b>
</summary>
<img src="wallpapers/Miku Stylish with Glasses.jpg"></img>
## Linux Girl Wallpaper - Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1305668)
</details>
- [WallHere](https://wallhere.com/en/wallpaper/2284648) <details>
<summary>
<b>Miku Winter</b>
</summary>
<img src="wallpapers/Miku Winter.jpg"></img>
- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1305841)
</details>
<details>
<summary>
<b>Vocaloid Karaoke</b>
</summary>
<img src="wallpapers/Vocaloid Karaoke.jpg"></img>
- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=770194)
</details>
<details>
<summary>
<b>Miku, Rin and Luka Chibi</b>
</summary>
<img src="wallpapers/Miku, Rin and Luka Chibi.jpg"></img>
- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=770164)
</details>
<details>
<summary>
<b>Miku Guitar</b>
</summary>
<img src="wallpapers/Miku Guitar.jpg"></img>
- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=867976)
</details>
<details>
<summary>
<b>Miku Garden</b>
</summary>
<img src="wallpapers/Miku Garden.jpg"></img>
- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1315430)
</details>
<details>
<summary>
<b>Miku Setup</b>
</summary>
<img src="wallpapers/Miku Setup.png"></img>
- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=672757)
</details>
<details>
<summary>
<b>Miku Flower Field</b>
</summary>
<img src="wallpapers/Miku Flower Field.jpg"></img>
- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=688123)
</details>
<details>
<summary>
<b>Miku Door</b>
</summary>
<img src="wallpapers/Miku Door.png"></img>
- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=845583)
</details>
<details>
<summary>
<b>Miku Crying with Mask</b>
</summary>
<img src="wallpapers/Miku Crying with Mask.jpg"></img>
- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=524092)
</details>
<details>
<summary>
<b>Miku City Sky</b>
</summary>
<img src="wallpapers/Miku City Sky.png"></img>
- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=698444)
</details>
<details>
<summary>
<b>Miku Bush</b>
</summary>
<img src="wallpapers/Miku Bush.jpg"></img>
- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=631739)
</details>
<details>
<summary>
<b>Hatsune Miku Birthday!</b>
</summary>
<img src="wallpapers/Hatsune Miku Birthday!.png"></img>
- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=731810)
</details>
<details>
<summary>
<b>Hatsune Miku and Megurine Luka</b>
</summary>
<img src="wallpapers/Hatsune Miku and Megurine Luka.jpg"></img>
- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1313438)
</details>
<details>
<summary>
<b>Gumi Ocean Sunset</b>
</summary>
<img src="wallpapers/Gumi Ocean Sunset.jpg"></img>
- Source: [WallHaven](https://wallhaven.cc/w/we8pgx)
</details>
<details>
<summary>
<b>Gumi Street Bike</b>
</summary>
<img src="wallpapers/Gumi Street Bike.jpg"></img>
- Source: [WallHaven](https://wallhaven.cc/w/4x7e7o)
</details>
<details>
<summary>
<b>Inabakumori Kaai Yuki</b>
</summary>
<img src="wallpapers/Inabakumori Kaai Yuki.png"></img>
- Source: [WallHaven](https://wallhaven.cc/w/wed3m7)
</details>
<details>
<summary>
<b>Inabakumori Osage</b>
</summary>
<img src="wallpapers/Inabakumori Osage.jpg"></img>
- Source: [WallHaven](https://wallhaven.cc/w/o3r8z9)
</details>
<!----------------- -->
## Frieren: Beyond Journey's End
<details>
<summary>
<b>Frieren Underwater</b>
</summary>
<img src="wallpapers/Frieren Underwater.jpg"></img>
- Source: [Pixiv](https://www.pixiv.net/en/artworks/114234634)
</details>
<details>
<summary>
<b>Frieren Rain</b>
</summary>
<img src="wallpapers/Frieren Rain.jpg"></img>
- Source: [Pixiv](https://www.pixiv.net/en/artworks/114234634)
</details>
<details>
<summary>
<b>Frieren At The Funeral</b>
</summary>
<img src="wallpapers/Frieren At The Funeral.jpg"></img>
- Source: [Pixiv](https://www.pixiv.net/en/artworks/114234634)
</details>
<details>
<summary>
<b>Frieren Sunset</b>
</summary>
<img src="wallpapers/Frieren Sunset.jpeg"></img>
- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1354394)
</details>
<details>
<summary>
<b>Frieren Sending Kiss</b>
</summary>
<img src="wallpapers/Frieren Sending Kiss.jpeg"></img>
- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1344010)
</details>
<details>
<summary>
<b>Frieren Ring</b>
</summary>
<img src="wallpapers/Frieren Ring.jpeg"></img>
- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1351964)
</details>
<details>
<summary>
<b>Frieren Night Film</b>
</summary>
<img src="wallpapers/Frieren Night Film.jpeg"></img>
- Source: [Wallpaper Flare](https://www.wallpaperflare.com/anime-anime-girls-sousou-no-frieren-wallpaper-yvcxe)
</details>
<details>
<summary>
<b>Frieren Blue</b>
</summary>
<img src="wallpapers/Frieren Blue.jpeg"></img>
- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1357998)
</details>
<!----------------- -->
## Oshi no Ko
<details>
<summary>
<b>Oshi no Ko Kana Arima</b>
</summary>
<img src="wallpapers/Oshi no Ko Kana Arima.png"></img>
- Source: [WallHaven](https://wallhaven.cc/w/x6pp5z)
</details>
<!---------------- -->
## Gruvbox-styled
<details>
<summary>
<b>Balcony Girl</b>
</summary>
<img src="wallpapers/Balcony Girl.png"></img>
- Source: [Gruvbox Wallpapers](https://gruvbox-wallpapers.pages.dev)
</details>
<details>
<summary>
<b>Gruvbox Girl</b>
</summary>
<img src="wallpapers/Gruvbox Girl.png"></img>
- Source: [Gruvbox Wallpapers](https://gruvbox-wallpapers.pages.dev)
</details>
- [Gruvbox Wallpapers](https://gruvbox-wallpapers.pages.dev)
<!---------------- -->
## Gruvbox-styled Wallpapers ## Gruvbox-styled Wallpapers
- [Gruvbox Wallpapers](https://gruvbox-wallpapers.pages.dev) - [Gruvbox Wallpapers](https://gruvbox-wallpapers.pages.dev)
## Genshin Impact Wallpaper(s) ## Genshin Impact Wallpaper(s)
Those can be get on web events in Genshin Impact, and also on [HoYoLAB](https://hoyolab.com).
<details>
<summary>
<b>Mualani!!</b>
</summary>
<img src="wallpapers/Mualani!!.jpg"></img>
Those can be get on web events from Genshin Impact, the game, and also on [HoYoLAB](https://hoyolab.com) - Source: Genshin Impact Web Event (not available anymore)
</details>
## Others
<details>
<summary>
<b>Hypr-chan</b>
</summary>
<img src="wallpapers/Hypr-chan.png"></img>
- Source: [GitHub (hyprwm/Hyprland)](https://github.com/hyprwm/Hyprland)
</details>
<details>
<summary>
<b>Linux Anime Girl</b>
</summary>
<img src="wallpapers/Linux Anime Girl.png"></img>
- Source: [WallHere](https://wallhere.com/en/wallpaper/2284648)
</details>
### More sources
- [Pinterest](https://pinterest.com)
- [AlphaCoders](https://alphacoders.com/bocchi-the-rock!-wallpapers)
- [WallpaperCave](https://wallpapercave.com/bocchi-the-rock-wallpapers)
- [WallpaperFlare](https://www.wallpaperflare.com/search?wallpaper=BOCCHI+THE+ROCK%21)
+2
View File
@@ -0,0 +1,2 @@
node_modules/
@girs/
+108
View File
@@ -0,0 +1,108 @@
import AstalNotifd from "gi://AstalNotifd";
import { App } from "astal/gtk3"
import { Wireplumber } from "./scripts/volume";
import { handleArguments } from "./scripts/arg-handler";
import { Time, timeout } from "astal/time";
import { OSDModes, setOSDMode } from "./window/OSD";
import { Runner } from "./runner/Runner";
import { PluginApps } from "./runner/plugins/apps";
import { PluginShell } from "./runner/plugins/shell";
import { PluginWebSearch } from "./runner/plugins/websearch";
import { PluginMedia } from "./runner/plugins/media";
import { Windows } from "./windows";
import { Notifications } from "./scripts/notifications";
import { GObject } from "astal";
import { PluginWallpapers } from "./runner/plugins/wallpapers";
import { Wallpaper } from "./scripts/wallpaper";
import { Stylesheet } from "./scripts/stylesheet";
import { Clipboard } from "./scripts/clipboard";
import { PluginClipboard } from "./runner/plugins/clipboard";
let osdTimer: (Time|undefined);
let connections = new Map<GObject.Object, (Array<number> | number)>();
const defaultWindows: Array<keyof typeof Windows.windows> = [ "bar" ];
const runnerPlugins: Array<Runner.Plugin> = [
PluginApps,
PluginShell,
PluginWebSearch,
PluginMedia,
new PluginWallpapers(),
PluginClipboard
];
App.start({
instanceName: "astal",
requestHandler: (request: string, response: (result: any) => void): void => {
response(handleArguments(request));
},
main: (..._args: Array<string>) => {
console.log(`Initialized astal instance as: ${ App.instanceName || "astal" }`);
Stylesheet.getDefault().compileApply();
App.vfunc_dispose = () => {
console.log("Disconnecting stuff");
connections.forEach((v, k) => Array.isArray(v) ?
v.map(id => k.disconnect(id))
: k.disconnect(v));
};
// Init clipboard module
Clipboard.getDefault();
connections.set(Wireplumber.getDefault(), [
Wireplumber.getDefault().getDefaultSink().connect("notify::volume", () =>
triggerOSD(OSDModes.SINK))
]);
connections.set(Notifications.getDefault(), [
Notifications.getDefault().connect("notification-added", (_, _notif: AstalNotifd.Notification) => {
Windows.open("floating-notifications");
}),
Notifications.getDefault().connect("notification-removed", (_: Notifications, _id: number) => {
_.notifications.length === 0 && Windows.close("floating-notifications");
})
]);
console.log("Initializing wallpaper handler");
Wallpaper.getDefault();
console.log("Adding runner plugins");
runnerPlugins.map(plugin => Runner.addPlugin(plugin));
console.log("Opening default windows");
// Open openOnStart windows
defaultWindows.map(name => {
if(Windows.isVisible(name)) return;
Windows.open(name);
});
}
});
function triggerOSD(osdModeParam: OSDModes) {
if(Windows.isVisible("control-center")) return;
Windows.open("osd");
if(!osdTimer) {
setOSDMode(osdModeParam);
osdTimer = timeout(3000, () => {
osdTimer = undefined;
Windows.close("osd");
});
return;
}
osdTimer.cancel();
osdTimer = timeout(3000, () => {
Windows.close("osd");
osdTimer = undefined;
});
}
+21
View File
@@ -0,0 +1,21 @@
declare const SRC: string
declare module "inline:*" {
const content: string
export default content
}
declare module "*.scss" {
const content: string
export default content
}
declare module "*.blp" {
const content: string
export default content
}
declare module "*.css" {
const content: string
export default content
}
+56
View File
@@ -0,0 +1,56 @@
import { GLib } from "astal";
const i18nKeys = {
"en_US": (await import("./lang/en_US")).default,
"pt_BR": (await import("./lang/pt_BR")).default
};
const languages: Array<string> = Object.keys(i18nKeys);
let language: string = getSystemLanguage();
export function getSystemLanguage(): string {
const sysLanguage: (string|null|undefined) = GLib.getenv("LANG") || GLib.getenv("LANGUAGE");
if(!sysLanguage) {
console.log(`[WARNING] Couldn't get system language, fallback to default ${languages[0]}`);
console.log("[TIP] Please set the LANG or LANGUAGE environment variable");
return languages[0];
}
return sysLanguage.split('.')[0];
}
export function setLanguage(lang: string): string {
languages.map((cur: string) => {
if(cur === lang) {
language = lang;
return lang;
}
});
throw new Error(`(i18n/intl) Couldn't set language: ${lang}`, {
cause: `Language ${lang} not found in languages of type ${typeof languages}`
});
}
export function tr(key: string): string {
let result = i18nKeys[language as keyof typeof i18nKeys],
defResult = i18nKeys[languages[0] as keyof typeof i18nKeys];
for(const keyString of key.split('.')) {
result = result[keyString as keyof typeof result] as never;
defResult = defResult[keyString as keyof typeof defResult] as never;
}
return (typeof result == "string") ?
result
: ((typeof defResult == "string") ?
defResult
: "not found / is not of type \"string\"");
}
export function trGet() {
return i18nKeys[getSystemLanguage() as keyof typeof i18nKeys];
}
+82
View File
@@ -0,0 +1,82 @@
import { i18nStruct } from "../struct";
export default {
language: "English (United States)",
cancel: "Cancel",
accept: "Ok",
devices: "Devices",
others: "Others",
connected: "Connected",
disconnected: "Disconnected",
unknown: "Unknown",
connecting: "Connecting",
apps: "Applications",
connect: "Connect",
disconnect: "Disconnect",
control_center: {
tiles: {
enabled: "Enabled",
disabled: "Disabled",
more: "More",
network: {
network: "Network",
wireless: "Wireless",
wired: "Wired"
},
recording: {
title: "Screen Recording",
disabled_desc: "Start recording",
enabled_desc: "Stop recording",
},
dnd: {
title: "Do Not Disturb"
},
night_light: {
title: "Night Light",
default_desc: "Fidelity"
}
},
pages: {
more_settings: "More settings",
sound: {
title: "Sound",
description: "Configure the audio output"
},
microphone: {
title: "Microphone",
description: "Configure the audio input"
},
night_light: {
title: "Night Light",
description: "Control Night Light and Gamma filters",
gamma: "Gamma",
temperature: "Temperature"
},
bluetooth: {
title: "Bluetooth",
description: "Manage Bluetooth devices",
new_devices: "New devices",
adapters: "Adapters",
paired_devices: "Paired Devices",
start_discovering: "Start discovering",
stop_discovering: "Stop discovering",
untrust_device: "Untrust device",
unpair_device: "Unpair device",
trust_device: "Trust device",
pair_device: "Pair device"
},
network: {
title: "Network",
interface: "Interface"
}
}
},
ask_popup: {
title: "Question"
}
} as i18nStruct;
+82
View File
@@ -0,0 +1,82 @@
import { i18nStruct } from "../struct";
export default {
language: "Português (Brasil)",
cancel: "Cancelar",
accept: "Ok",
devices: "Dispositivos",
others: "Outros",
connected: "Conectado",
disconnected: "Desconectado",
unknown: "Desconhecido",
connecting: "Conectando",
disconnect: "Desconectar",
connect: "Conectar",
apps: "Aplicativos",
control_center: {
tiles: {
enabled: "Ligado",
disabled: "Desligado",
more: "Mais",
network: {
network: "Rede",
wireless: "Wi-Fi",
wired: "Cabeada"
},
recording: {
title: "Gravação de Tela",
disabled_desc: "Iniciar gravação",
enabled_desc: "Parar gravação",
},
dnd: {
title: "Não Perturbe"
},
night_light: {
title: "Luz Noturna",
default_desc: "Fidelidade"
}
},
pages: {
more_settings: "Mais configurações",
sound: {
title: "Som",
description: "Controle a saída de áudio"
},
microphone: {
title: "Microfone",
description: "Configure a entrada de áudio"
},
night_light: {
title: "Luz Noturna",
description: "Controle os filtros de Luz Noturna e Gama",
temperature: "Temperatura",
gamma: "Gama"
},
bluetooth: {
title: "Bluetooth",
description: "Gerencie dispositivos Bluetooth",
new_devices: "Novos Dispositivos",
adapters: "Adaptadores",
paired_devices: "Dispositivos Pareados",
start_discovering: "Começar a procurar dispositivos",
stop_discovering: "Parar de procurar dispositivos",
pair_device: "Parear dispositivo",
trust_device: "Confiar no dispositivo",
unpair_device: "Desparear dispositivo",
untrust_device: "Deixar de confiar no dispositivo"
},
network: {
title: "Rede",
interface: "Interface"
}
}
},
ask_popup: {
title: "Pergunta"
}
} as i18nStruct;
+83
View File
@@ -0,0 +1,83 @@
export type i18nStruct = {
language: string,
cancel: string,
accept: string,
connected: string,
disconnected: string,
connecting: string,
unknown: string,
devices: string,
others: string,
disconnect: string,
connect: string,
apps: string;
control_center: {
tiles: {
enabled: string,
disabled: string,
more: string,
network: {
network: string,
wireless: string,
wired: string
},
recording: {
title: string,
disabled_desc: string,
enabled_desc: string
},
dnd: {
title: string
},
night_light: {
title: string,
default_desc: string
}
},
pages: {
more_settings: string,
sound: {
title: string,
description: string
},
microphone: {
title: string,
description: string
},
network: {
title: string,
interface: string
},
bluetooth: {
title: string,
description: string,
adapters: string,
new_devices: string,
paired_devices: string,
start_discovering: string,
stop_discovering: string,
trust_device: string,
untrust_device: string,
pair_device: string,
unpair_device: string
},
night_light: {
title: string,
description: string,
temperature: string,
gamma: string
}
}
},
ask_popup: {
title: string
}
};
+21
View File
@@ -0,0 +1,21 @@
{
"name": "astal-shell",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "astal-shell",
"dependencies": {
"astal": "/usr/share/astal/gjs"
}
},
"../../../../usr/share/astal/gjs": {
"name": "astal",
"license": "LGPL-2.1"
},
"node_modules/astal": {
"resolved": "../../../../usr/share/astal/gjs",
"link": true
}
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"name": "astal-shell",
"dependencies": {
"astal": "/usr/share/astal/gjs"
}
}
+268
View File
@@ -0,0 +1,268 @@
import { AstalIO, timeout } from "astal";
import { Astal, Gdk, Gtk, Widget } from "astal/gtk3";
import { PopupWindow, PopupWindowProps } from "../widget/PopupWindow";
import { updateApps } from "../scripts/apps";
import { ResultWidget, ResultWidgetProps } from "../widget/runner/ResultWidget";
import { Windows } from "../windows";
export namespace Runner {
export type RunnerProps = {
halign?: Gtk.Align;
valign?: Gtk.Align;
width?: number;
height?: number;
entryPlaceHolder?: string;
initialText?: string;
showResultsPlaceHolderOnStartup?: boolean;
};
export interface Plugin {
/** prefix to call the plugin. if undefined, will be triggered like applications plugin */
readonly prefix?: string;
/** name of the plugin. e.g.: websearch, shell */
readonly name?: string;
/** ran on runner open */
readonly init?: () => void;
/** handle the user input to return results (does not include plugin's prefix) */
readonly handle: (inputText: string) => (ResultWidget|Array<ResultWidget>|null|undefined);
/** ran on runner close */
readonly onClose?: () => void;
/** hide other plugins when using this plugin */
prioritize?: boolean;
}
export let instance: (Widget.Window|null) = null;
let gtkEntry: (Widget.Entry|null) = null;
const plugins = new Set<Runner.Plugin>();
export function close() { instance?.close(); }
export function regExMatch(search: string, item: string): boolean {
search = search.replace(/[\\^$.*?()[\]{}|]/g, "\\$&");
return new RegExp(`${search.split('').map(c =>
`.*(${c.toLowerCase()}|${c.toUpperCase()}).*`).join('')}`
).test(item);
}
export function addPlugin(plugin: Runner.Plugin, force?: boolean) {
if(!force && plugin.prefix && plugins.has(plugin))
throw new Error(`Runner plugin with prefix ${plugin.prefix} already exists`);
plugins.delete(plugin);
plugins.add(plugin);
}
export function getPlugins(): Array<Runner.Plugin> {
return [...plugins.values()];
}
/** Removes a plugin from the runner plugin list
* @returns true if plugin was removed or false if plugin wasn't found
*/
export function removePlugin(plugin: Plugin): boolean {
return plugins.delete(plugin);
}
export function setEntryText(text: string): void {
gtkEntry?.set_text(text);
gtkEntry?.set_position(gtkEntry.textLength);
gtkEntry?.grab_focus_without_selecting();
}
export function openDefault(initialText?: string) {
return Runner.openRunner({
entryPlaceHolder: "Start typing...",
showResultsPlaceHolderOnStartup: false,
initialText
} as Runner.RunnerProps,
() => [
new ResultWidget({
icon: "application-x-executable-symbolic",
title: "Use your applications",
description: "Search for any app installed in your computer",
closeOnClick: false,
onClick: () => gtkEntry?.grab_focus()
} as ResultWidgetProps),
new ResultWidget({
icon: "edit-paste-symbolic",
title: "See your clipboard history",
description: "Start your search with '>' to go through your clipboard history",
closeOnClick: false,
onClick: () => setEntryText('>')
} as ResultWidgetProps),
new ResultWidget({
icon: "image-x-generic-symbolic",
title: "Change your wallpaper",
description: "Add '#' at the start to search through the wallpapers folder!",
closeOnClick: false,
onClick: () => setEntryText('#'),
} as ResultWidgetProps),
new ResultWidget({
icon: "utilities-terminal-symbolic",
title: "Run shell commands",
description: "Add '!' before your command to run it (pro tip: add a second '!' to show command output)",
closeOnClick: false,
onClick: () => setEntryText('!!')
} as ResultWidgetProps),
new ResultWidget({
icon: "media-playback-start-symbolic",
title: "Control media",
description: "Type ':' to control playing media",
closeOnClick: false,
onClick: () => setEntryText(':')
} as ResultWidgetProps),
new ResultWidget({
icon: "applications-internet-symbolic",
title: "Search the Web",
description: "Start typing with '?' prefix to search the web",
closeOnClick: false,
onClick: () => setEntryText('?')
} as ResultWidgetProps)
]);
}
export function openRunner(props?: RunnerProps, placeholder?: () => Array<ResultWidget>): Widget.Window {
let onClickTimeout: (AstalIO.Time|undefined);
gtkEntry = new Widget.Entry({
className: "search",
placeholderText: props?.entryPlaceHolder || "",
onChanged: (self) => {
updateResultsList(self.text);
resultsList.get_row_at_index(0) &&
resultsList.select_row(resultsList.get_row_at_index(0));
},
onActivate: (entry) => {
const resultWidget = resultsList.get_selected_row()?.get_child();
if(resultWidget instanceof ResultWidget) {
entry.isFocus = false;
resultWidget.onClick();
resultWidget.closeOnClick && Runner.close();
}
},
primary_icon_name: "system-search"
} as Widget.EntryProps);
const resultsList: Gtk.ListBox = new Gtk.ListBox({
visible: true,
expand: true
} as Gtk.ListBox.ConstructorProps);
if(props?.showResultsPlaceHolderOnStartup && placeholder) {
const placeholderWidgets = placeholder();
placeholderWidgets.map(widget =>
resultsList.insert(widget, -1));
}
function cleanResults() {
resultsList.get_children().map((listItem) => {
resultsList.remove(listItem);
});
}
function getPluginResults(input: string): Array<ResultWidget> {
let calledPlugins: Array<Plugin> = getPlugins().filter((plugin) =>
plugin.prefix ? (input.startsWith(plugin.prefix) ? true : false) : true
).sort((plugin) => plugin.prefix != null ? 0 : 1);
for(const plugin of calledPlugins) {
if(plugin.prioritize) {
calledPlugins = [ plugin ];
break;
}
}
return calledPlugins.map(plugin => plugin.handle(
plugin.prefix ? input.replace(plugin.prefix, "") : input)
).filter(value => value !== undefined && value !== null).flat(1);
}
function updateResultsList(entryText: string) {
const widgets: Array<ResultWidget> = [];
// Remove all previous results
cleanResults();
widgets.push(...getPluginResults(entryText))
// Insert placeholder if there are no results
if(placeholder && widgets.length === 0)
widgets.push(...placeholder());
// Insert results inside GtkListBox
widgets.map((resultWidget: ResultWidget) => {
resultsList.insert(resultWidget, -1);
resultsList.connect("row-activated", (_, row: Gtk.ListBoxRow) => {
const rWidget = row.get_child();
if(rWidget instanceof ResultWidget) {
if(onClickTimeout) return;
// Timeout, so it doesn't fire the event a hundred times :skull:
onClickTimeout = timeout(500, () => onClickTimeout = undefined);
rWidget.onClick();
rWidget.closeOnClick && Runner.close();
}
});
});
}
if(!instance)
instance = Windows.createWindowForFocusedMonitor((mon: number): (Widget.Window) => PopupWindow({
namespace: "runner",
monitor: mon,
widthRequest: props?.width ?? 750,
heightRequest: props?.height ?? 450,
marginTop: 240,
anchor: Astal.WindowAnchor.TOP | Astal.WindowAnchor.BOTTOM,
setup: () => {
// Init plugins
plugins.forEach(plugin => plugin.init && plugin.init());
if(props?.initialText)
Runner.setEntryText(props.initialText);
},
onKeyPressEvent: (_, event: Gdk.Event) => {
const keyVal = event.get_keyval()[1];
if(!gtkEntry!.has_focus && keyVal !== Gdk.KEY_F5
&& keyVal !== Gdk.KEY_Down && keyVal !== Gdk.KEY_Up
&& keyVal !== Gdk.KEY_Return) {
gtkEntry!.grab_focus_without_selecting();
return;
}
event.get_keyval()[1] === Gdk.KEY_F5 &&
updateApps();
},
onDestroy: () => {
gtkEntry = null;
[...plugins.values()].map(plugin =>
plugin && plugin.onClose && plugin.onClose());
instance = null;
},
child: new Widget.Box({
className: "runner main",
orientation: Gtk.Orientation.VERTICAL,
expand: false,
valign: Gtk.Align.START,
children: [
gtkEntry,
new Widget.Scrollable({
className: "results-scrollable",
vscroll: Gtk.PolicyType.AUTOMATIC,
hscroll: Gtk.PolicyType.NEVER,
expand: true,
propagateNaturalHeight: true,
maxContentHeight: props?.height ?? 450,
child: resultsList
})
]
} as Widget.BoxProps)
} as PopupWindowProps))();
return instance!;
}
}
+20
View File
@@ -0,0 +1,20 @@
import { ResultWidget, ResultWidgetProps } from "../../widget/runner/ResultWidget";
import AstalApps from "gi://AstalApps";
import { cleanExec, getAstalApps } from "../../scripts/apps";
import { Runner } from "../Runner";
import { Astal } from "astal/gtk3";
export const PluginApps = {
// Do not provide prefix, so it's always ran.
name: "Apps",
handle: (text: string) => {
return getAstalApps().fuzzy_query(text).map((app: AstalApps.Application) =>
new ResultWidget({
title: app.get_name(),
description: app.get_description(),
icon: Astal.Icon.lookup_icon(app.iconName) ? app.iconName : "application-x-executable-symbolic",
onClick: () => cleanExec(app)
} as ResultWidgetProps)
);
}
} as Runner.Plugin;
+33
View File
@@ -0,0 +1,33 @@
import { Widget } from "astal/gtk3";
import { Clipboard } from "../../scripts/clipboard";
import { ResultWidget, ResultWidgetProps } from "../../widget/runner/ResultWidget";
import { Runner } from "../Runner";
import { Gio } from "astal";
export const PluginClipboard = {
prefix: '>',
prioritize: true,
handle: (search) => {
if(Clipboard.getDefault().history.length < 1)
return new ResultWidget({
icon: "edit-paste-symbolic",
title: "No clipboard items found!",
description: "When something is copied, it'll be shown right here!"
} as ResultWidgetProps);
return Clipboard.getDefault().history.filter(item => // not the best way to search, but it works
Runner.regExMatch(search, item.id.toString()) || Runner.regExMatch(search, item.preview)).map((item) =>
new ResultWidget({
icon: new Widget.Label({
label: item.id.toString(),
css: "font-size: 16px; margin-right: 8px; font-weight: 600;"
} as Widget.LabelProps),
title: item.preview,
onClick: () => Clipboard.getDefault().selectItem(item).catch((err: Gio.IOErrorEnum) => {
console.error(`Runner(Plugin/Clipboard): An error occurred while selecting clipboard item. Stderr:\n${
err.message ? `${err.message}\n` : ""}Stack: ${err.stack}`);
})
} as ResultWidgetProps));
}
} as Runner.Plugin;
+79
View File
@@ -0,0 +1,79 @@
import { bind, Variable } from "astal";
import { ResultWidget, ResultWidgetProps } from "../../widget/runner/ResultWidget";
import { Runner } from "../Runner";
import AstalMpris from "gi://AstalMpris";
export const PluginMedia = (() => {
let playTitle: Variable<string>|null;
let previousTitle: Variable<string>|null;
let nextTitle: Variable<string>|null;
return {
prefix: ":",
onClose: () => {
playTitle?.drop();
previousTitle?.drop();
nextTitle?.drop();
previousTitle = null;
playTitle = null;
nextTitle = null;
},
handle() {
const player = AstalMpris.get_default().players[0];
playTitle = Variable.derive([
bind(player, "title"),
bind(player, "artist"),
bind(player, "playbackStatus")
], (title, artist, status) => `${ status === AstalMpris.PlaybackStatus.PLAYING ?
"Pause" : "Play"
} ${title} | ${artist}`);
previousTitle = Variable.derive([
bind(player, "title"),
bind(player, "artist")
], (title, artist) =>
`Go Previous ${ title ? title : player.busName }${ artist ? ` | ${artist}` : "" }`
);
nextTitle = Variable.derive([
bind(player, "title"),
bind(player, "artist")
], (title, artist) =>
`Go Next ${ title ? title : player.busName }${ artist ? ` | ${artist}` : "" }`
);
if(!player) return new ResultWidget({
icon: "folder-music-symbolic",
title: "Couldn't find any players",
closeOnClick: false,
description: "No media / player found with mpris"
} as ResultWidgetProps);
return [
new ResultWidget({
icon: bind(player, "playbackStatus").as((status) => status === AstalMpris.PlaybackStatus.PLAYING ?
"media-playback-pause-symbolic"
: "media-playback-start-symbolic"),
closeOnClick: false,
title: playTitle(),
onClick: () => player && player.play_pause()
} as ResultWidgetProps),
new ResultWidget({
icon: "media-skip-backward-symbolic",
closeOnClick: false,
title: previousTitle(),
onClick: () => player && player.canGoPrevious && player.previous()
} as ResultWidgetProps),
new ResultWidget({
icon: "media-skip-forward-symbolic",
closeOnClick: false,
title: nextTitle(),
onClick: () => player && player.canGoNext && player.next()
} as ResultWidgetProps)
]
},
} as Runner.Plugin
})();
+59
View File
@@ -0,0 +1,59 @@
import { ResultWidget, ResultWidgetProps } from "../../widget/runner/ResultWidget";
import { Gio, GLib } from "astal";
import { Runner } from "../Runner";
import { Notifications } from "../../scripts/notifications";
export const PluginShell = (() => {
const shell = GLib.getenv("SHELL") ?? "/bin/sh";
const procLauncher = Gio.SubprocessLauncher.new(
Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE);
procLauncher.set_cwd(GLib.get_home_dir());
return {
prefix: '!',
prioritize: true,
handle: (input: string): ResultWidget => {
let showOutputNotif: boolean = false;
if(input.startsWith('!')) {
input = input.replace('!', "");
showOutputNotif = true;
}
const command = input ? GLib.shell_parse_argv(input) : undefined;
return new ResultWidget({
onClick: () => {
if(!command || !command[0]) return;
const proc = procLauncher.spawnv([ shell, "-c", `${input}` ]);
proc.communicate_utf8_async(null, null, (_, asyncResult) => {
const [ success, stdout, stderr ] = proc.communicate_utf8_finish(asyncResult);
if(!success || stderr) {
Notifications.getDefault().sendNotification({
appName: shell,
summary: "Command error",
body: `An error occurred on \`${input}\`. Stderr: ${stderr}`
});
return;
}
if(!showOutputNotif) return;
Notifications.getDefault().sendNotification({
appName: shell,
summary: "Command output",
body: stdout
});
});
},
title: `Run ${input ? ` \`${input}\`` : `with ${shell.split('/')[shell.split('/').length-1]}`}`,
description: (input || showOutputNotif) && `${input ? `${shell}\t` : ""}${ showOutputNotif ? "(showing output on notification)" : "" }`,
icon: "utilities-terminal-symbolic"
} as ResultWidgetProps)
}
} as Runner.Plugin
})();
+41
View File
@@ -0,0 +1,41 @@
import { Gio } from "astal";
import { Wallpaper } from "../../scripts/wallpaper";
import { Runner } from "../Runner";
import { ResultWidget, ResultWidgetProps } from "../../widget/runner/ResultWidget";
export class PluginWallpapers implements Runner.Plugin {
prefix = "#";
prioritize = true;
#files: (Array<string>|undefined);
init() {
this.#files = [];
const dir = Gio.File.new_for_path(Wallpaper.getDefault().wallpapersPath);
if(dir.query_file_type(null, null) === Gio.FileType.DIRECTORY) {
for(const file of dir.enumerate_children(
"standard::*",
Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
null
)) {
this.#files.push(`${dir.get_path()}/${file.get_name()}`);
}
}
}
handle(search: string) {
if(this.#files!.length > 0)
return this.#files!.filter(file => // not the best way to search, but it works
Runner.regExMatch(search, file.split('/')[file.split('/').length-1])
).map(path => new ResultWidget({
title: path.split('/')[path.split('/').length-1].replace(/\..*$/, ""),
onClick: () => Wallpaper.getDefault().setWallpaper(path)
} as ResultWidgetProps));
return new ResultWidget({
title: "No wallpapers found!",
description: "Define the $WALLPAPERS variable on Hyprland or create a ~/wallpapers directory",
icon: "image-missing-symbolic"
} as ResultWidgetProps);
}
}
+28
View File
@@ -0,0 +1,28 @@
import AstalHyprland from "gi://AstalHyprland";
import { ResultWidget, ResultWidgetProps } from "../../widget/runner/ResultWidget";
import { Runner } from "../Runner";
const searchEngines = {
duckduckgo: "https://duckduckgo.com/?q=",
google: "https://google.com/search?q=",
yahoo: "https://search.yahoo.com/search?p="
};
let engine: string = searchEngines.google;
export const PluginWebSearch = {
prefix: '?',
name: "Web Search",
handle: (search: string): ResultWidget => {
return new ResultWidget({
icon: "system-search-symbolic",
title: search || "Type your search...",
description: `Search the Web`,
onClick: () => AstalHyprland.get_default().dispatch(
"exec",
`xdg-open \"${engine + search}\"`
)
} as ResultWidgetProps);
}
} as Runner.Plugin;
+67
View File
@@ -0,0 +1,67 @@
import { Astal } from "astal/gtk3";
import AstalApps from "gi://AstalApps";
import AstalHyprland from "gi://AstalHyprland";
const astalApps: AstalApps.Apps = new AstalApps.Apps();
let appsList: Array<AstalApps.Application> = astalApps.get_list();
export function getApps(): Array<AstalApps.Application> {
return appsList;
}
export function updateApps(): void {
astalApps.reload();
appsList = astalApps.get_list();
}
export function getAstalApps(): AstalApps.Apps {
return astalApps;
}
export function cleanExec(app: AstalApps.Application): void {
AstalHyprland.get_default().dispatch("exec", app.executable.replace(/(%f|%F|%u|%U|%i|%c|%k)/g, ""));
}
export function getAppsByName(appName: string): (Array<AstalApps.Application>|undefined) {
let found: Array<AstalApps.Application> = [];
getApps().map((app: AstalApps.Application) => {
if(app.get_name().trim().toLowerCase() === appName.trim().toLowerCase()
|| (app?.wmClass && app.wmClass.trim().toLowerCase() === appName.trim().toLowerCase()))
found.push(app);
});
return (found.length > 0 ? found : undefined);
}
export function getIconByAppName(appName: string): (string|undefined) {
if(Astal.Icon.lookup_icon(appName))
return appName;
if(Astal.Icon.lookup_icon(appName.toLowerCase()))
return appName.toLowerCase();
const nameReverseDNS = appName.split('.');
if(Astal.Icon.lookup_icon(nameReverseDNS[nameReverseDNS.length - 1]))
return nameReverseDNS[nameReverseDNS.length - 1];
const found: (AstalApps.Application|undefined) = getAppsByName(appName)?.[0];
if(Boolean(found))
return found?.iconName;
return undefined;
}
export function getAppIcon(app: (string|AstalApps.Application)): (string|undefined) {
if(typeof app === "string")
return getIconByAppName(app);
if(app.iconName && Astal.Icon.lookup_icon(app.iconName))
return app.iconName;
if(app.wmClass)
return getIconByAppName(app.wmClass);
return getIconByAppName(app.name);
}
+176
View File
@@ -0,0 +1,176 @@
import { Wireplumber } from "./volume";
import { Windows } from "../windows";
import { restartInstance } from "./reload-handler";
import { AstalIO, timeout } from "astal";
import { Runner } from "../runner/Runner";
import { showWorkspaceNumber } from "../widget/bar/Workspaces";
let wsTimeout: (AstalIO.Time|undefined);
export function handleArguments(request: string): any {
const args: Array<string> = request.split(" ");
switch(args[0]) {
case "open":
case "close":
case "toggle":
return handleWindowArgs(args);
case "help":
case "h":
return getHelp(); // stop it, get some help
case "volume":
return handleVolumeArgs(args);
case "reload":
restartInstance();
return "Restarting instance..."
case "windows":
return Object.keys(Windows.windows).map(name =>
`${name}: ${Windows.isVisible(name) ? "open" : "closed" }`).join('\n');
case "runner":
!Runner.instance ?
Runner.openDefault(args[1] || undefined)
: Runner.close();
return "Opening runner..."
case "peek-workspace-num":
if(wsTimeout) return "Workspace numbers are already showing";
showWorkspaceNumber(true);
wsTimeout = timeout(2200, () => {
showWorkspaceNumber(false);
wsTimeout = undefined;
});
return "Toggled workspace numbers";
default:
return "command not found! try checking help";
}
}
// Didn't want to bloat the switch statement, so I just separated it into functions
function handleWindowArgs(args: Array<string>): string {
if(!args[1])
return "Window argument not specified!";
const specifiedWindow: string = args[1];
if(!Windows.hasWindow(specifiedWindow))
return `Name "${specifiedWindow}" not found windows map! Make sure to add new Windows on the Map!`
switch(args[0]) {
case "open":
if(!Windows.isVisible(specifiedWindow)) {
Windows.open(specifiedWindow);
return `Setting visibility of window "${args[1]}" to true`;
}
return `Window is already open, ignored`;
case "close":
if(Windows.isVisible(specifiedWindow)) {
Windows.close(specifiedWindow);
return `Setting visibility of window "${args[1]}" to false`
}
return `Window is already closed, ignored`
case "toggle":
if(!Windows.isVisible(specifiedWindow)) {
Windows.open(specifiedWindow);
return `Toggle opening window "${args[1]}"`;
}
Windows.close(specifiedWindow);
return `Toggle closing window "${args[1]}"`
}
return "Couldn't handle window management arguments"
}
function handleVolumeArgs(args: Array<string>) {
if(!args[1])
return `Please specify what you want to do!\n\n${volumeHelp()}`
if(/^(sink|source)(\-increase|\-decrease|\-set)$/.test(args[1]) && !args[2])
return `You forgot to add a value to be set!`;
if(Number.isNaN(Number.parseFloat(args[2])) && Number.isSafeInteger(Number.parseFloat(args[2])))
return `Argument "${args[2]} is not a valid number! Please use integers"`;
const command: Array<string> = args[1].split('-');
if(/help/.test(args[1]))
return volumeHelp();
switch(command[1]) {
case "set":
command[0] === "sink" ?
Wireplumber.getDefault().setSinkVolume(Number.parseInt(args[2]))
:
Wireplumber.getDefault().setSourceVolume(Number.parseInt(args[2]))
return `Done! Set ${command[0]} volume to ${args[2]}`;
case "mute":
command[0] === "sink" ?
Wireplumber.getDefault().toggleMuteSink()
:
Wireplumber.getDefault().toggleMuteSource()
return `Done toggling mute!`;
case "increase":
command[0] === "sink" ?
Wireplumber.getDefault().increaseSinkVolume(Number.parseInt(args[2]))
:
Wireplumber.getDefault().increaseSourceVolume(Number.parseInt(args[2]))
return `Done increasing volume by ${args[2]}`;
case "decrease":
command[0] === "sink" ?
Wireplumber.getDefault().decreaseSinkVolume(Number.parseInt(args[2]))
:
Wireplumber.getDefault().decreaseSourceVolume(Number.parseInt(args[2]))
return `Done decreasing volume to ${args[2]}`;
}
return `Couldn't resolve arguments! "${args.join(' ').replace(new RegExp(`^${args[0]}`), "")}"`;
function volumeHelp(): string {
return `
Control speaker and microphone volumes easily!
Options:
(sink|source)-set [number]: set speaker/microphone volume.
(sink|source)-mute: toggle mute for the speaker/microphone device.
(sink|source)-increase [number]: increases speaker/microphone volume.
(sink|source)-decrease [number]: decreases speaker/microphone volume.
`.trim();
}
}
function getHelp(): string {
return `Manage Astal Windows and do more stuff. From
retrozinndev's Hyprland Dots, using Astal and AGS by Aylur.
Window and Audio options:
open [window]: opens the specified window.
close [window]: closes all instances of specified window.
toggle [window]: toggle-open/close the specified window.
windows: list shell windows.
reload: quit this instance and start a new one.
volume: speaker and microphone volume controller, see "volume help".
h, help: shows this help message.
Other options:
runner [initial_text]: open the application runner, optionally add an initial search.
peek-workspace-num: peek the workspace numbers on bar window.
2025 (c) retrozinndev's Hyprland-Dots, licensed under the MIT License.
https://github.com/retrozinndev/Hyprland-Dots
`.split('\n').map(l => l.replace(/^ {8}/, "")).join('\n');
}
+73
View File
@@ -0,0 +1,73 @@
import { execAsync, Gio, GLib, register } from "astal";
import Polkit from "gi://Polkit";
import PolkitAgent from "gi://PolkitAgent";
import { EntryPopup, EntryPopupProps } from "../widget/EntryPopup";
import AstalAuth from "gi://AstalAuth";
import { AskPopup, AskPopupProps } from "../widget/AskPopup";
export { Auth };
@register({ GTypeName: "AuthAgent" })
class Auth extends PolkitAgent.Listener {
private static instance: Auth;
#subject: Polkit.Subject;
constructor() {
super();
this.#subject = Polkit.UnixSession.new(GLib.get_user_name());
this.register(PolkitAgent.RegisterFlags.NONE,
this.#subject,
"/io/github/retrozinndev/Colorshell/PolicyKit/AuthAgent",
null
);
}
vfunc_dispose() {
PolkitAgent.Listener.unregister();
}
static initiate_authentication(action_id: string, message: string, icon_name: string, details: Polkit.Details, cookie: string, identities: Array<Polkit.Identity>, cancellable?: Gio.Cancellable, callback?: Gio.AsyncReadyCallback): void | Promise<boolean> {
const authPopup = EntryPopup({
title: "Authentication",
text: message,
isPassword: true,
onFinish: callback,
onCancel: () => cancellable?.cancel(),
closeOnAccept: false,
onAccept: (input: string) => {
if(this.validatePasswd(input)) {
authPopup.close();
}
AskPopup({
} as AskPopupProps)
}
} as EntryPopupProps);
}
public static initAgent(): Auth {
if(!this.instance)
this.instance = new Auth();
return this.instance;
}
private static validatePasswd(passwd: string): boolean {
return AstalAuth.Pam.authenticate(passwd, null);
}
/** @returns if successful, true, or else, false */
public async polkitExecute(cmd: string | Array<string>): Promise<boolean> {
let success: boolean = true;
await execAsync([ "pkexec", "--", ...(Array.isArray(cmd) ?
cmd as Array<string> : [ cmd as string ]) ]
).catch((r) => {
success = false;
console.error(`Polkit: Couldn't authenticate. Stderr: ${r}`);
});
return success;
}
}
+42
View File
@@ -0,0 +1,42 @@
import { exec, execAsync, GObject, monitorFile, readFileAsync, register, signal } from "astal";
import { Connectable } from "astal/binding";
/** !!TODO!! Needs more work and testing
* I(retrozinndev) don't have a monitor that has software-controlled brightness :(
*/
@register({ GTypeName: "Brightness" })
class Brightness extends GObject.Object implements Connectable {
private readonly backlight: string|undefined;
private max: number;
private brightness: number;
@signal(Number)
declare brightnessChanged: (value: number) => void;
constructor(backlightDevice?: string) {
super();
this.backlight = backlightDevice || "intel_backlight";
this.max = Number.parseInt(exec(`brightnessctl -d ${backlightDevice} max`))
this.brightness = Number.parseInt(exec(`brightnessctl -d ${backlightDevice} get`))
readFileAsync(`/sys/class/backlight/${backlightDevice}/brightness`).catch(() => {
throw new Error(`Couldn't find backlight ${backlightDevice}`);
});
monitorFile(`/sys/class/backlight/${backlightDevice}/brightness`, async () => {
this.brightness = Number.parseInt(await execAsync(`brightnessctl -d ${backlightDevice} get`));
this.max = Number.parseInt(await execAsync(`brightnessctl -d ${backlightDevice} max`));
this.emit("brightness-changed", this.brightness);
});
}
public setBrightness(newBrightness: number): void {
execAsync(`brightnessctl -d ${this.backlight} set ${newBrightness || this.brightness}`).catch(() => {
throw new Error(`Couldn't set brightness of backlight ${this.backlight}`);
});
this.emit("brightness-changed", newBrightness);
}
}
+232
View File
@@ -0,0 +1,232 @@
import { AstalIO, execAsync, Gio, GLib, GObject, monitorFile, property, readFile, register, signal, timeout } from "astal";
export enum ClipboardItemType {
TEXT = 0,
IMAGE = 1
}
export type ClipboardItem = {
id: number;
type: ClipboardItemType;
preview: string;
}
export { Clipboard };
/** Cliphist Manager and event listener
* This only supports wipe and store events from cliphist */
@register({ GTypeName: "Clipboard" })
class Clipboard extends GObject.Object {
private static instance: Clipboard;
#dbFile: Gio.File;
#dbMonitor: Gio.FileMonitor;
#updateDone: boolean = false;
#history = new Array<ClipboardItem>;
#changesTimeout: (AstalIO.Time|undefined);
#ignoreChanges: boolean = false;
@signal(Object)
declare copied: () => ClipboardItem;
@signal()
declare wiped: () => void;
@property()
public get history() { return this.#history; }
constructor() {
super();
this.#dbFile = this.getCliphistDatabase();
this.#dbMonitor = monitorFile(this.#dbFile.get_path()!, () => {
if(this.#ignoreChanges || this.#changesTimeout)
return;
this.#changesTimeout = timeout(300, () => this.#changesTimeout = undefined);
if(this.#updateDone) {
this.updateDatabase();
return;
}
this.init();
});
if(this.#dbFile.query_exists(null)) {
this.init();
return;
}
console.log("Clipboard: cliphist database not found. Try copying something first!");
}
vfunc_dispose(): void {
this.#dbMonitor.cancel();
this.#dbMonitor.unref();
}
private init() {
console.log("Clipboard: Starting to read cliphist history...");
this.updateDatabase().then(() => {
console.log("Clipboard: Done reading cliphist history!");
}).catch((err) =>
console.error(`Clipboard: An error occurred while reading cliphist history. Stderr: ${err}`)
);
}
public async copyAsync(content: string): Promise<void> {
await execAsync(`wl-copy "${content}"`).catch((err: Gio.IOErrorEnum) => {
console.error(`Clipboard: Couldn't copy text using wl-copy. Stderr:\n\t${err.message
} | Stack:\n\t\t${err.stack}`);
});
}
public async selectItem(itemToSelect: number|ClipboardItem): Promise<boolean> {
const item = await this.getItemContent(itemToSelect);
let res: boolean = true;
if(item)
await this.copyAsync(item).catch(() => res = false);
return res;
}
/** Gets history item's content by its ID.
* @returns the clipboard item's content */
public async getItemContent(item: number|ClipboardItem): Promise<string|undefined> {
const id = (typeof item === "number") ?
item : item.id;
const cmd = Gio.Subprocess.new([ "cliphist", "decode", id.toString() ],
Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE);
const [ , stdout, stderr ] = cmd.communicate_utf8(null, null);
if(stderr) {
console.error(`Clipboard: An error occurred while getting item content. Stderr:\n${stderr}`);
return;
}
return stdout;
}
/** Searches for the cliphist database file
* Will not work if cliphist config file is not on default path */
private getCliphistDatabase(): Gio.File {
// Check if env variable is set
const path = GLib.getenv("CLIPHIST_DB_PATH");
if(path != null)
return Gio.File.new_for_path(path);
// Check config file
const confFile = Gio.File.new_for_path(`${GLib.get_user_config_dir()}/cliphist/config`);
if(confFile.query_exists(null)) {
const cliphistConf = readFile(confFile.get_path()!);
for(const line of cliphistConf.split('\n').map(l => l.trim())) {
if(line.startsWith('#'))
continue;
const [ key, value ] = line.split('\s', 1);
if(key === "db-path") {
return Gio.File.new_for_path(value.trimStart());
}
}
}
// return default path if none of the above matches
return Gio.File.new_for_path(`${GLib.get_user_cache_dir()}/cliphist/db`);
}
private getContentType(preview: string): ClipboardItemType {
return /^\[\[.*binary data.*x.*\]\]$/u.test(preview) ?
ClipboardItemType.IMAGE
: ClipboardItemType.TEXT;
}
public async wipeHistory(noExec?: boolean): Promise<void> {
if(noExec) {
this.#history = [];
this.emit("wiped");
return;
}
this.#ignoreChanges = true;
await execAsync("cliphist wipe").then(() => {
this.#history = [];
this.emit("wiped");
}).catch((err: Gio.IOErrorEnum) =>
console.error(`Clipboard: An error occurred on cliphist database wipe. Stderr: ${
err.message ? `${err.message}\n` : ""}${err.stack}`)
).finally(() => this.#ignoreChanges = false);
}
public async updateDatabase(): Promise<void> {
const proc = Gio.Subprocess.new([ "cliphist", "list" ],
Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE);
proc.communicate_utf8_async(null, null, (_, asyncRes) => {
const [ success, stdout, stderr ] = proc.communicate_utf8_finish(asyncRes);
if(!success || stderr) {
console.error("Clipboard: Couldn't communicate with cliphist! Is it installed?");
return;
}
if(!stdout.trim()) {
this.wipeHistory(true);
this.notify("history");
return;
}
const items = stdout.split('\n');
if(this.#updateDone) {
const [ id, preview ] = items[0].split('\t');
const clipItem = {
id: Number.parseInt(id),
preview,
type: this.getContentType(preview)
} as ClipboardItem;
this.#history.unshift(clipItem);
this.emit("copied", clipItem);
this.notify("history");
return;
}
for(const item of items) {
if(!item) continue;
const [ id, preview ] = item.split('\t');
const clipItem = {
id: Number.parseInt(id),
preview,
type: this.getContentType(preview)
} as ClipboardItem;
this.#history.push(clipItem);
this.emit("copied", clipItem);
this.notify("history");
}
this.#updateDone = true;
});
}
public static getDefault(): Clipboard {
if(!this.instance)
this.instance = new Clipboard();
return this.instance;
}
}
+138
View File
@@ -0,0 +1,138 @@
import { AstalIO, execAsync, GObject, interval, property, register } from "astal";
export { NightLight };
@register({ GTypeName: "NightLight" })
class NightLight extends GObject.Object {
private static instance: NightLight;
#watchInterval: (AstalIO.Time|null) = null;
#temperature: number = 4500;
#gamma: number = 100;
#identity: boolean = false;
#prevTemperature: (number|null) = null;
#prevGamma: (number|null) = null;
@property(Number)
public get temperature() { return this.#temperature; }
public set temperature(newValue: number) { this.setTemperature(newValue); }
@property(Number)
public get gamma() { return this.#gamma; }
public set gamma(newValue: number) { this.setGamma(newValue); }
@property(Number)
public get maxTemperature() { return 20000; }
@property(Number)
public get maxGamma() { return 100; }
@property(Boolean)
public get identity() { return this.#identity; }
public set identity(newValue: boolean) {
newValue ? this.applyIdentity() : this.filter();
}
constructor() {
super();
this.#watchInterval = interval(1000, () => {
execAsync("hyprctl hyprsunset temperature").then(t => {
if(t.trim() !== "" && t.trim().length <= 5) {
const val = Number.parseInt(t.trim());
if(this.#temperature !== val) {
this.#temperature = val;
this.notify("temperature");
}
}
}).catch((r) => console.error(r));
execAsync("hyprctl hyprsunset gamma").then(g => {
if(g.trim() !== "" && g.trim().length <= 5) {
const val = Number.parseInt(g.trim());
if(this.#gamma !== val) {
this.#gamma = val;
this.notify("gamma");
}
}
}).catch((r) => console.error(r));
});
this.vfunc_dispose = () => this.#watchInterval &&
this.#watchInterval.cancel();
}
public static getDefault(): NightLight {
if(!this.instance)
this.instance = new NightLight();
return this.instance;
}
private async setTemperature(value: number): Promise<void> {
if(value === this.temperature) return;
if(value > this.maxTemperature || value < 1000) {
console.error(`Night Light(hyprsunset): provided temperatue ${value
} is out of bounds (min: 1000; max: ${this.maxTemperature})`);
return;
}
execAsync(`hyprctl hyprsunset temperature ${value}`).then(() => {
this.#temperature = value;
this.notify("temperature");
this.#identity = false;
this.#prevTemperature = null;
this.#prevGamma = null;
}).catch((r) => console.error(
`Night Light(hyprsunset): Couldn't set temperature. Stderr: ${r}`
));
}
private async setGamma(value: number) {
if(value === this.gamma) return;
if(value > this.maxGamma || value < 0) {
console.error(`Night Light(hyprsunset): provided gamma ${value
} is out of bounds (min: 0; max: ${this.maxTemperature})`);
return;
}
execAsync(`hyprctl hyprsunset gamma ${value}`).then(() => {
this.#gamma = value;
this.notify("gamma");
this.#identity = false;
this.#prevTemperature = null;
this.#prevGamma = null;
}).catch((r) => console.error(
`Night Light(hyprsunset): Couldn't set gamma. Stderr: ${r}`
));
}
private applyIdentity(): void {
if(this.#identity) return;
this.#prevGamma = this.#gamma;
this.#prevTemperature = this.#temperature;
this.#identity = true;
this.temperature = 6000;
this.gamma = this.maxGamma;
}
public filter(): void {
if(!this.#identity) return;
this.#identity = false;
this.setTemperature(this.#prevTemperature ?? 1000);
this.setGamma(this.#prevGamma ?? 100);
this.#prevTemperature = null;
this.#prevGamma = null;
}
}
+298
View File
@@ -0,0 +1,298 @@
import { AstalIO, execAsync, Gio, GObject, property, register, signal, timeout } from "astal";
import AstalNotifd from "gi://AstalNotifd";
export let
NOTIFICATION_TIMEOUT_URGENT: number = 0,
NOTIFICATION_TIMEOUT_NORMAL: number = 4000,
NOTIFICATION_TIMEOUT_LOW: number = 2000;
export interface HistoryNotification {
id: number;
appName: string;
body: string;
summary: string;
urgency: AstalNotifd.Urgency;
appIcon?: string;
time: number;
image?: string;
}
@register({ GTypeName: "Notifications" })
class Notifications extends GObject.Object {
private static instance: (Notifications|null) = null;
#notifications: Array<AstalNotifd.Notification> = [];
#history: Array<HistoryNotification> = [];
#notificationsOnHold: Set<number> = new Set<number>();
#connections: Array<number> = [];
#historyLimit: number = 10;
@property()
public get notifications() { return this.#notifications };
@property()
public get history() { return this.#history };
@property()
public get historyLimit() { return this.#historyLimit };
public set historyLimit(newValue: number) {
this.#historyLimit = newValue;
this.notify("historyLimit");
}
@signal(AstalNotifd.Notification)
declare notificationAdded: (notification: AstalNotifd.Notification) => void;
@signal(Number)
declare notificationRemoved: (id: number) => void;
@signal(Object) // It's an Object, beacuase HistoryNotification is just an interface
declare historyAdded: (notification: AstalNotifd.Notification) => void;
@signal(Number)
declare historyRemoved: (id: number) => void;
@signal(Number)
declare notificationReplaced: (id: number) => void;
constructor() {
super();
this.#connections.push(
AstalNotifd.get_default().connect("notified", (notifd, id) => {
const notification = notifd.get_notification(id);
const notifTimeout = notification.urgency === AstalNotifd.Urgency.LOW ?
NOTIFICATION_TIMEOUT_LOW
: (notification.urgency === AstalNotifd.Urgency.CRITICAL ?
NOTIFICATION_TIMEOUT_URGENT
: NOTIFICATION_TIMEOUT_NORMAL);
if(this.getNotifd().dontDisturb) {
this.addHistory(notification, () => notification.dismiss());
return;
}
this.addNotification(notification, () => {
if(notification.urgency !== AstalNotifd.Urgency.CRITICAL ||
(notification.urgency === AstalNotifd.Urgency.CRITICAL &&
NOTIFICATION_TIMEOUT_URGENT > 0)) {
let notifTimer: (AstalIO.Time|undefined) = undefined;
let replacedConnectionId: number;
const removeFun = () => { // Funny name haha lmao remove fun :skull:
notifTimer = undefined;
if(this.#notificationsOnHold.has(notification.id)) return;
this.addHistory(notification, () => {
replacedConnectionId && this.disconnect(replacedConnectionId);
this.removeNotification(id);
});
}
notifTimer = timeout(notifTimeout, removeFun);
replacedConnectionId = this.connect("notification-replaced", (_, id: number) => {
if(notification.id === id) {
notifTimer?.cancel();
notifTimer = timeout(notifTimeout, removeFun);
}
});
}
});
}),
AstalNotifd.get_default().connect("resolved", (notifd, id, _reason) => {
this.removeNotification(id);
this.addHistory(notifd.get_notification(id));
})
);
this.run_dispose = () => {
super.run_dispose();
this.#connections.map((id: number) =>
AstalNotifd.get_default().disconnect(id));
};
}
public static getDefault(): Notifications {
if(!this.instance)
this.instance = new Notifications();
return this.instance;
}
public async sendNotification(props: {
urgency?: AstalNotifd.Urgency;
appName?: string;
image?: string;
summary: string;
body?: string;
replaceId?: number;
actions?: Array<{
id?: (string|number);
text: string;
onAction?: () => void
}>
}): Promise<{
id?: (string|number);
text: string;
onAction?: () => void
}|null|void> {
return await execAsync([
"notify-send",
...(props.urgency ? [
"-u", this.getUrgencyString(props.urgency)
] : []), ...(props.appName ? [
"-a", props.appName
] : []), ...(props.image ? [
"-i", props.image
] : []), ...(props.actions ? props.actions.map((action) =>
[ "-A", action.text ]
).flat(2) : []), ...(props.replaceId ? [
"-r", props.replaceId.toString()
] : []), props.summary, props.body ? props.body : ""
]).then((stdout) => {
stdout = stdout.trim();
if(!stdout) {
if(props.actions && props.actions.length > 0)
return null;
return;
}
if(props.actions && props.actions.length > 0) {
const action = props.actions[Number.parseInt(stdout)];
action?.onAction?.();
return action ?? undefined;
}
}).catch((err: Gio.IOErrorEnum) => {
console.error(`Notifications: Couldn't send notification! Is the daemon running? Stderr:\n${
err.message ? `${err.message}\n` : ""}Stack: ${err.stack}`);
});
}
public getUrgencyString(urgency: AstalNotifd.Notification|AstalNotifd.Urgency) {
switch((urgency instanceof AstalNotifd.Notification) ?
urgency.urgency : urgency) {
case AstalNotifd.Urgency.LOW:
return "low";
case AstalNotifd.Urgency.CRITICAL:
return "critical";
}
return "normal";
}
private addHistory(notif: AstalNotifd.Notification, onAdded?: (notif: AstalNotifd.Notification) => void): void {
if(!notif) return;
this.#history.length === this.#historyLimit &&
this.removeHistory(this.#history[this.#history.length - 1]);
this.#history.map((notifb, i) =>
notifb.id === notif.id && this.#history.splice(i, 1));
this.#history.unshift({
id: notif.id,
appName: notif.appName,
body: notif.body,
summary: notif.summary,
urgency: notif.urgency,
appIcon: notif.appIcon,
time: notif.time,
image: notif.image ? notif.image : undefined
} as HistoryNotification);
this.notify("history");
this.emit("history-added", this.#history[0]);
onAdded && onAdded(notif);
}
public clearHistory(): void {
const hist = this.#history.reverse();
hist.map((notif) => {
this.#history = this.history.filter((n) => n.id !== notif.id);
this.emit("history-removed", notif.id);
this.notify("history");
});
}
public removeHistory(notif: (HistoryNotification|number)): void {
const notifId = (typeof notif === "number") ? notif : notif.id;
this.#history = this.#history.filter((item: HistoryNotification) =>
item.id !== notifId);
this.notify("history");
this.emit("history-removed", notifId);
}
private addNotification(notif: AstalNotifd.Notification, onAdded?: (notif: AstalNotifd.Notification) => void): void {
const newArray = this.#notifications.reverse().filter((item) => item.id !== notif.id);
if(newArray !== this.notifications) {
this.emit("notification-replaced", notif.id);
}
newArray.push(notif);
this.#notifications = newArray.reverse();
this.notify("notifications");
this.emit("notification-added", notif);
onAdded && onAdded(notif);
}
public removeNotification(notif: (AstalNotifd.Notification|number)): void {
const notificationId = (notif instanceof AstalNotifd.Notification) ? notif.id : notif;
this.#notificationsOnHold.has(notificationId) &&
this.#notificationsOnHold.delete(notificationId);
this.#notifications = this.#notifications.filter((item: AstalNotifd.Notification) =>
item.id !== notificationId);
AstalNotifd.get_default().get_notification(notificationId)?.dismiss();
this.notify("notifications");
this.emit("notification-removed", notificationId);
}
private getNotificationById(id: number): AstalNotifd.Notification|undefined {
return this.#notifications.filter(notif => notif.id === id)?.[0];
}
public holdNotification(notif: (AstalNotifd.Notification|number)): void {
notif = (typeof notif === "number") ?
this.getNotificationById(notif)!
: notif;
if(!notif) return;
this.#notificationsOnHold.add(notif.id);
}
public toggleDoNotDisturb(): boolean {
if(AstalNotifd.get_default().dontDisturb) {
AstalNotifd.get_default().dontDisturb = false;
return false;
}
AstalNotifd.get_default().dontDisturb = true;
return true;
}
public getNotifd(): AstalNotifd.Notifd { return AstalNotifd.get_default(); }
connect(signal: string, callback: (...args: any[]) => void): number {
return super.connect(signal, callback);
}
disconnect(id: number) {
super.disconnect(id);
}
}
export { Notifications };
+154
View File
@@ -0,0 +1,154 @@
import { execAsync, Gio, GLib, GObject } from "astal";
import { property, register, signal } from "astal/gobject";
import { Gdk } from "astal/gtk3";
import { getDateTime } from "./time";
import { makeDirectory } from "./utils";
import { Notifications } from "./notifications";
export { Recording };
@register({ GTypeName: "Recording" })
class Recording extends GObject.Object {
private static instance: Recording;
@signal()
declare started: () => void;
@signal()
declare stopped: () => void;
#recording: boolean = false;
#path: string = "~/Recordings";
/** Default extension: mp4(h264) */
#extension: string = "mp4";
#recordAudio: boolean = false;
#area: (Gdk.Rectangle|null) = null;
#startedAt: (GLib.DateTime|null) = null;
#process: (Gio.Subprocess|null) = null;
#output: (string|null) = null;
@property()
/** GLib.DateTime of when recording started */
public get startedAt() { return this.#startedAt; }
@property(Boolean)
public get recording() { return this.#recording; }
private set recording(newValue: boolean) {
(!newValue && this.#recording) ?
this.stopRecording()
: this.startRecording(this.#area || undefined);
this.#recording = newValue;
this.notify("recording");
}
@property(String)
public get path() { return this.#path; }
public set path(newPath: string) {
if(this.recording) return;
this.#path = newPath;
this.notify("path");
}
@property(String)
public get extension() { return this.#extension; }
public set extension(newExt: string) {
if(this.recording) return;
this.#extension = newExt;
this.notify("extension");
}
/** Recording output file name. %NULL if screen is not being recorded */
public get output() { return this.#output; }
/** Currently unsupported property */
public get recordAudio() { return this.#recordAudio; }
public set recordAudio(newValue: boolean) {
if(this.recording) return;
this.#recordAudio = newValue;
this.notify("record-audio");
}
constructor() {
super();
const videosDir = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_VIDEOS);
if(videosDir) this.#path = `${videosDir}/Recordings`;
}
public static getDefault() {
if(!this.instance)
this.instance = new Recording();
return this.instance;
}
public startRecording(area?: Gdk.Rectangle) {
if(this.recording)
throw new Error("Screen Recording is already running!");
this.#output = `${getDateTime().get().format("%Y-%m-%d-%H%M%S")}_rec.${this.extension || "mp4"}`;
this.#recording = true;
this.notify("recording");
this.emit("started");
makeDirectory(this.path);
const cancellable = Gio.Cancellable.new();
cancellable.cancel = () => {};
const areaString = `${area?.x ?? 0},${area?.y ?? 0} ${area?.width ?? 1}x${area?.height ?? 1}`;
this.#process = Gio.Subprocess.new([
"wf-recorder",
...(area ? [ `-g`, areaString ] : []),
"-f",
`${this.path}/${this.output!}`
], Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE);
this.#process.wait_async(cancellable, () => {
this.stopRecording();
});
this.#startedAt = getDateTime().get();
}
public stopRecording() {
if(!this.#process) return;
!this.#process.get_if_exited() && execAsync([
"kill", "-s", "SIGTERM", this.#process.get_identifier()!
]);
const path = this.#path;
const output = this.#output;
this.#process = null;
this.#recording = false;
this.#startedAt = null;
this.#output = null;
this.notify("recording");
this.emit("stopped");
Notifications.getDefault().sendNotification({
actions: [
{
text: "View",
onAction: () => {
execAsync(["nautilus", "-s", output!, path]);
}
},
{
text: "Open",
onAction: () => {
execAsync(["xdg-open", `${path}/${output}`]);
}
}
],
appName: "Screen Recording",
summary: "Screen Recording saved",
body: `Saved as ${path}/${output}`
});
}
};
+18
View File
@@ -0,0 +1,18 @@
import { monitorFile, Process } from "astal";
import { App } from "astal/gtk3";
const monitoringPaths = [ "./scripts", "./window", "./app.ts", "env.d.ts" ];
export function restartInstance(instanceName?: string): void {
Process.exec_async(`astal -q ${ instanceName || App.instanceName || "astal" }`, () => {});
Process.exec_async(`ags run`, () => {});
}
export function monitorPaths(): void {
monitoringPaths.map((path: string) => {
monitorFile(
path,
() => restartInstance()
)
});
}
+81
View File
@@ -0,0 +1,81 @@
// handles stylesheet compiling and reloading
import { monitorFile, AstalIO, timeout, GLib, Gio, execAsync, exec, readFile } from "astal";
import { App } from "astal/gtk3";
export class Stylesheet {
private static instance: Stylesheet;
#watchDelay: (AstalIO.Time|undefined);
#outputPath = Gio.File.new_for_path(`${GLib.get_user_state_dir()}/ags/style`);
#styles = [
"./style",
"./style.scss"
];
public compileSass(): void {
console.log("Stylesheet: Compiling Sass");
exec(`bash -c "sass ${this.#styles.map(style => `-I ${style}`).join('\s')
} ${this.#outputPath.get_path()!}/style.css"`);
}
public async reapply(cssFilePath: string): Promise<void> {
console.log("Stylesheet: Applying stylesheet");
const content = readFile(cssFilePath);
if(content) {
App.reset_css();
App.apply_css(content);
console.log("Stylesheet: done applying stylesheet to shell");
return;
}
console.error(`Stylesheet: An error occurred while trying to read the css file: ${
cssFilePath}`);
}
public async compileApply(): Promise<void> {
this.compileSass();
this.reapply(this.#outputPath.get_path()! + "/style.css");
}
public static getDefault(): Stylesheet {
if(!this.instance)
this.instance = new Stylesheet();
return this.instance;
}
constructor() {
(async () => !this.#outputPath.query_exists(null) &&
this.#outputPath.make_directory_with_parents(null))();
this.#styles.map((path: string) =>
monitorFile(
`${path}`,
(file: string) => {
if(this.#watchDelay || file.endsWith('~') || Number.isNaN(file))
return;
this.#watchDelay = timeout(250, () => this.#watchDelay = undefined);
console.log(`Stylesheet: \`${file.startsWith(GLib.get_home_dir()) ?
file.replace(GLib.get_home_dir(), '~')
: file}\` changed`)
this.compileApply();
}
)
)
monitorFile(
`${GLib.get_user_cache_dir()}/wal/colors.scss`,
(file: string) => {
execAsync(`bash -c "cp -f ${file} ./style/_wal.scss"`).catch(r => {
console.error(`Stylesheet: Failed to copy pywal stylesheet to style dir. Stderr: ${r}`);
});
}
);
}
}
+6
View File
@@ -0,0 +1,6 @@
import { GLib, Variable } from "astal";
const time = new Variable<GLib.DateTime>(GLib.DateTime.new_now_local()).poll(500, () =>
GLib.DateTime.new_now_local())();
export const getDateTime = () => time;
+26
View File
@@ -0,0 +1,26 @@
import { exec, execAsync, GLib } from "astal";
export function getHyprlandInstanceSig(): (string|null) {
return GLib.getenv("HYPRLAND_INSTANCE_SIGNATURE");
}
export function getHyprlandVersion(): string {
return exec(`${GLib.getenv("HYPRLAND_CMD") || "Hyprland"} --version | head -n1`).split(" ")[1];
}
export function makeDirectory(dir: string): void {
execAsync([ "mkdir", "-p", dir ]);
}
export function deleteFile(path: string): void {
execAsync([ "rm", "-r", path ]);
}
export function isInstalled(commandName: string): boolean {
const output = exec(["bash", "-c", `command -v ${commandName}`]);
if(output)
return true;
return false;
}
+89
View File
@@ -0,0 +1,89 @@
import { Subscribable } from "astal/binding";
export class VarMap<K, V> implements Subscribable {
#subs = new Set<(v: Map<K, V>) => void>();
#map: Map<K, V>;
constructor(initial?: Map<K, V>) {
this.#map = initial || new Map<K, V>();
}
private notifyMap() {
const subs = this.#subs;
for(const sub of subs) {
sub(this.#map);
}
}
public get(): Map<K, V> {
return this.#map;
}
public get size(): number {
return this.#map.size;
}
public getValue(key: K): (V|undefined) {
return this.#map.get(key);
}
public getKeyAt(index: number): (K|undefined) {
return [...this.#map.keys()][index];
}
public getValueAt(index: number): (V|undefined) {
return [...this.#map.values()][index];
}
public set(key: K, value: V): Map<K, V> {
const newMap: Map<K, V> = this.#map.set(key, value);
this.notifyMap();
return newMap;
}
public delete(key: K): boolean {
const deleted: boolean = this.#map.delete(key);
this.notifyMap();
return deleted;
}
public has(key: K): boolean {
return this.#map.has(key);
}
public clear(): void {
this.#map.clear();
this.notifyMap();
}
public entries(): MapIterator<[K, V]> {
return this.#map.entries();
}
public keys(): MapIterator<K> {
return this.#map.keys();
}
public values(): MapIterator<V> {
return this.#map.values();
}
public forEach<ReturnType = any> (callback: (value: V, key: K, map: Map<K, V>) => ReturnType): ReturnType[] {
const result: Array<ReturnType> = [];
for(const entry of this.#map.entries()) {
result.push(callback(entry[1], entry[0], this.#map));
}
return result;
}
public subscribe(callback: (v: Map<K, V>) => void): () => void {
this.#subs.add(callback);
return () => {
this.#subs.delete(callback);
}
}
}
+149
View File
@@ -0,0 +1,149 @@
import { GObject, register } from "astal";
import AstalWp from "gi://AstalWp";
export { WireplumberClass as Wireplumber };
@register({ GTypeName: "Wireplumber" })
class WireplumberClass extends GObject.Object {
private static astalWireplumber: (AstalWp.Wp|null) = AstalWp.get_default();
private static inst: WireplumberClass;
private defaultSink: AstalWp.Endpoint = WireplumberClass.astalWireplumber!.get_default_speaker()!;
private defaultSource: AstalWp.Endpoint = WireplumberClass.astalWireplumber!.get_default_microphone()!;
private maxSinkVolume: number = 100;
private maxSourceVolume: number = 100;
constructor() {
super();
if(!WireplumberClass.astalWireplumber)
throw new Error("Audio features will not work correctly! Please install wireplumber first", {
cause: "Wireplumber library not found"
});
}
public static getDefault(): WireplumberClass {
if(!WireplumberClass.inst)
WireplumberClass.inst = new WireplumberClass();
return WireplumberClass.inst;
}
public static getWireplumber(): AstalWp.Wp {
return WireplumberClass.astalWireplumber!;
}
public getMaxSinkVolume(): number {
return this.maxSinkVolume;
}
public getMaxSourceVolume(): number {
return this.maxSourceVolume;
}
public getDefaultSink(): AstalWp.Endpoint {
return this.defaultSink;
}
public getDefaultSource(): AstalWp.Endpoint {
return this.defaultSource;
}
public getSinkVolume(): number {
return Math.floor(this.getDefaultSink().get_volume() * 100);
}
public getSourceVolume(): number {
return Math.floor(this.getDefaultSource().get_volume() * 100);
}
public setSinkVolume(newSinkVolume: number): void {
this.defaultSink.set_volume(
(newSinkVolume > this.maxSinkVolume ? this.maxSinkVolume : newSinkVolume) / 100
);
}
public setSourceVolume(newSourceVolume: number): void {
this.defaultSource.set_volume(
newSourceVolume > this.maxSourceVolume ? this.maxSourceVolume : newSourceVolume / 100
);
}
public increaseEndpointVolume(endpoint: AstalWp.Endpoint, volumeIncrease: number): void {
volumeIncrease = Math.abs(volumeIncrease) / 100;
if((endpoint.get_volume() + volumeIncrease) > this.maxSinkVolume) {
endpoint.set_volume(1.0);
return;
}
endpoint.set_volume(endpoint.get_volume() + volumeIncrease);
}
public increaseSinkVolume(volumeIncrease: number): void {
this.increaseEndpointVolume(this.getDefaultSink(), volumeIncrease);
}
public increaseSourceVolume(volumeIncrease: number): void {
this.increaseEndpointVolume(this.getDefaultSource(), volumeIncrease);
}
public decreaseEndpointVolume(endpoint: AstalWp.Endpoint, volumeDecrease: number): void {
volumeDecrease = Math.abs(volumeDecrease) / 100;
if((endpoint.get_volume() - volumeDecrease) < 0) {
endpoint.set_volume(0);
return;
}
endpoint.set_volume(endpoint.get_volume() - volumeDecrease);
}
public decreaseSinkVolume(volumeDecrease: number): void {
this.decreaseEndpointVolume(this.getDefaultSink(), volumeDecrease);
}
public decreaseSourceVolume(volumeDecrease: number): void {
this.decreaseEndpointVolume(this.getDefaultSource(), volumeDecrease);
}
public muteSink(): void {
this.getDefaultSink().set_mute(true);
}
public muteSource(): void {
this.getDefaultSource().set_mute(true);
}
public unmuteSink(): void {
this.getDefaultSink().set_mute(false);
}
public unmuteSource(): void {
this.getDefaultSource().set_mute(false);
}
public isMutedSink(): boolean {
return this.getDefaultSink().get_mute();
}
public isMutedSource(): boolean {
return this.getDefaultSource().get_mute();
}
public toggleMuteSink(): void {
if(this.isMutedSink())
return this.unmuteSink();
return this.muteSink();
}
public toggleMuteSource(): void {
if(this.isMutedSource())
return this.unmuteSource();
return this.muteSource();
}
}
+177
View File
@@ -0,0 +1,177 @@
import { AstalIO, execAsync, Gio, GLib, GObject, monitorFile, property, register, timeout } from "astal";
export { Wallpaper };
@register({ GTypeName: "Wallpaper" })
class Wallpaper extends GObject.Object {
private static instance: Wallpaper;
#wallpaper: (string|undefined);
#splash: boolean = true;
#monitor: Gio.FileMonitor;
#hyprpaperFile: Gio.File;
#wallpapersPath: string;
#ignoreWatch: boolean = false;
@property(Boolean)
public get splash() { return this.#splash; }
public set splash(showSplash: boolean) {
this.#splash = showSplash;
this.notify("splash");
}
@property(String)
public get wallpaper(): (string|undefined) { return this.#wallpaper; }
public set wallpaper(newValue: string) { this.setWallpaper(newValue); }
public get wallpapersPath() { return this.#wallpapersPath; }
constructor() {
super();
this.#wallpapersPath = GLib.getenv("WALLPAPERS") ?? `${GLib.get_home_dir()}/wallpapers`;
this.#hyprpaperFile = Gio.File.new_for_path(`${GLib.get_user_config_dir()}/hypr/hyprpaper.conf`);
this.getWallpaper().then((wall) => {
if(wall?.trim()) this.#wallpaper = wall.trim();
});
let tmeout: (AstalIO.Time|undefined) = undefined;
this.#monitor = monitorFile(this.#hyprpaperFile.get_path()!, (_, event) => {
if(event !== Gio.FileMonitorEvent.CHANGED && event !== Gio.FileMonitorEvent.CREATED &&
event !== Gio.FileMonitorEvent.MOVED_IN)
return;
if(tmeout) return;
else tmeout = timeout(1500, () => tmeout = undefined);
if(this.#ignoreWatch) {
this.#ignoreWatch = false;
return;
}
const [ loaded, text ] = this.#hyprpaperFile.load_contents(null);
if(!loaded)
console.error("Wallpaper: Couldn't read changes inside the hyprpaper file!");
const content = new TextDecoder().decode(text);
if(content) {
let setWall: boolean = true;
for(const line of content.split('\n')) {
if(line.trim().startsWith('#'))
continue;
const lineSplit = line.split('=');
const key = lineSplit[0].trim(),
value = lineSplit.filter((_, i) => i !== 0).join('=').trim();
switch(key) {
case "splash":
this.splash = /(yes|true|on|enable|enabled)/.test(value) ? true : false;
break;
case "wallpaper":
if(this.#wallpaper !== value && setWall) {
this.setWallpaper(value, false);
setWall = false; // wallpaper already set
}
break;
}
}
}
});
}
vfunc_dispose(): void {
this.#monitor.cancel();
}
public static getDefault(): Wallpaper {
if(!this.instance)
this.instance = new Wallpaper();
return this.instance;
}
private writeChanges(): void {
this.#ignoreWatch = true; // tell monitor to ignore file replace
this.#hyprpaperFile.replace_async(null, false,
Gio.FileCreateFlags.REPLACE_DESTINATION,
GLib.PRIORITY_DEFAULT, null, (_, result) => {
const res = this.#hyprpaperFile.replace_finish(result);
if(res) {
// success
this.#ignoreWatch = true; // tell monitor to ignore this change
res.write_bytes_async(new TextEncoder().encode(`# This file was automatically generated by color-shell
preload = ${this.#wallpaper}
splash = ${this.#splash}
wallpaper = , ${this.#wallpaper}`.split('\n').map(str => str.trimStart()).join('\n')),
GLib.PRIORITY_DEFAULT, null, (_, asyncRes) => {
if(_!.write_finish(asyncRes)) res.flush(null);
res.close(null);
}
);
return;
}
console.error(`Wallpaper: an error occurred when trying to replace the hyprpaper file`);
}
);
}
public async getWallpaper(): Promise<string|undefined> {
return await execAsync("sh -c \"hyprctl hyprpaper listactive | tail -n 1\"").then(stdout => {
const loaded: (string|undefined) = stdout.split('=')[1]?.trim();
if(!loaded)
console.warn(`Wallpaper: Couldn't get wallpaper. There is(are) no loaded wallpaper(s)`);
return loaded;
}).catch((err: Gio.IOErrorEnum) => {
console.error(`Wallpaper: Couldn't get wallpaper. Stderr: \n${err.message ? `${err.message} /` : ""} Stack: \n ${err.stack}`);
return undefined;
});
}
public reloadColors(): void {
execAsync(`wal -t --cols16 darken -i "${this.#wallpaper}"`).then(() => {
console.log("Wallpaper: reloaded shell colors");
}).catch(r => {
console.error(`Wallpaper: Couldn't update shell colors. Stderr: ${r}`);
});
}
public setWallpaper(path: string|Gio.File, write: boolean = true): void {
execAsync("hyprctl hyprpaper unload all").then(() =>
execAsync(`hyprctl hyprpaper preload ${path}`).then(() =>
execAsync(`hyprctl hyprpaper wallpaper ${path}`).then(() => {
this.#wallpaper = (typeof path === "string") ? path : path.get_path()!;
this.reloadColors();
write && this.writeChanges();
}).catch(r => {
console.error(`Wallpaper: Couldn't set wallpaper. Stderr: ${r}`);
})
).catch(r => {
console.error(`Wallpaper: Couldn't preload image. Stderr: ${r}`);
})
).catch(r => {
console.error(`Wallpaper: Couldn't unload images from memory. Stderr: ${r}`);
});
}
public async pickWallpaper(): Promise<string|undefined> {
return (await execAsync(`zenity --file-selection`).then(wall => {
if(!wall.trim()) return undefined;
this.setWallpaper(wall);
return wall;
}).catch(r => {
console.error(`Wallpaper: Couldn't pick wallpaper, is \`zenity\` installed? Stderr: ${r}`);
return undefined;
}));
}
}
+24
View File
@@ -0,0 +1,24 @@
import { Gtk, Widget } from "astal/gtk3";
export function addSliderMarksFromMinMax(slider: Widget.Slider, amountOfMarks: number = 2, markup?: (string | null)) {
if(markup && !markup.includes("{}"))
markup = `${markup}{}`
slider.add_mark(slider.min, Gtk.PositionType.BOTTOM, markup ?
markup.replaceAll("{}", `${slider.min}`) : null);
const num = (amountOfMarks - 1);
for(let i = 1; i <= num; i++) {
const part = (slider.max / num) | 0;
if(i > num) {
slider.add_mark(slider.max, Gtk.PositionType.BOTTOM, `${slider.max}K`);
break;
}
slider.add_mark(part*i, Gtk.PositionType.BOTTOM, markup ?
markup.replaceAll("{}", `${part*i}`) : null);
}
return slider;
}
+313
View File
@@ -0,0 +1,313 @@
@use "sass:color";
@use "./style/wal";
@use "./style/mixins";
@use "./style/functions";
@use "./style/colors";
@use "./style/bar";
@use "./style/osd";
@use "./style/control-center";
@use "./style/center-window";
@use "./style/float-notifications";
@use "./style/logout-menu";
@use "./style/apps-window";
@use "./style/runner";
* {
@include mixins.reset-props;
/*&:focus {
box-shadow: inset 0 0 0 2px colors.$fg-primary;
}*/
}
entry {
background: colors.$bg-primary;
padding: 10px 9px;
border-radius: 12px;
&:focus {
box-shadow: inset 0 0 0 2px colors.$bg-secondary;
}
& image.left {
margin-right: 6px;
}
}
.custom-dialog-container {
background: colors.$bg-translucent;
padding: 18px;
border-radius: 24px;
& .title {
font-size: 21px;
font-weight: 700;
margin-bottom: 10px;
}
& .text {
font-size: 16px;
font-weight: 400;
}
& .options {
& button {
background: colors.$bg-primary;
border-radius: 12px;
padding: 9px 6px;
& label {
font-size: 16px;
font-weight: 600;
}
margin: {
left: 4px;
right: 4px;
};
&:hover, &:focus {
background: colors.$bg-secondary;
}
}
}
&.entry-popup-box entry {
margin-bottom: 10px;
&.password {
font-size: 14px;
font-family: "Adwaita Mono", "Cantarell Mono", "Noto Sans Mono", monospace;
font-weight: 400;
}
}
}
.notification {
background: colors.$bg-translucent-secondary;
border-radius: 16px;
& > .top {
padding: 8px;
padding-bottom: 0;
& .app-icon {
margin-right: 6px;
}
& .app-name {
font-size: 12px;
}
& label.time {
font-size: 11px;
font-weight: 500;
color: colors.$fg-disabled;
margin-right: 6px;
}
& button.close {
padding: 2px;
border-radius: 8px;
&:hover, &:focus {
background: colors.$bg-secondary;
}
}
& icon.close {
font-size: 16px;
}
}
& .content {
padding: 6px;
padding-top: 0;
& .image {
$size: 78px;
min-width: $size;
min-height: $size;
background-size: cover;
background-position: center;
margin: 6px;
border-radius: 8px;
}
& .summary {
font-size: 17.3px;
font-weight: 700;
margin-bottom: 4px;
}
& .body {
font-size: 14.5px;
font-weight: 400;
}
}
& .actions {
padding: 6px;
& button.action {
@include mixins.hover-shadow;
border-radius: 4px;
background: colors.$bg-secondary;
padding: 6px;
& label {
font-size: 14px;
font-weight: 600;
}
&:first-child {
border-top-left-radius: 12px;
border-bottom-left-radius: 12px;
}
&:last-child {
border-top-right-radius: 12px;
border-bottom-right-radius: 12px;
}
}
}
}
tooltip {
padding: 16px;
& > box {
padding: 7px 8px;
border-radius: 10px;
background: rgba(colors.$bg-primary, .98);
font-size: 13.1px;
font-weight: 500;
color: colors.$fg-primary;
box-shadow: 0 1px 4px 1px rgba(colors.$bg-primary, .6);
}
}
menu {
padding: 4px;
background: wal.$background;
border-radius: 14px;
& separator {
margin: 0 4px;
color: wal.$background;
}
& menuitem {
padding: 8px 16px;
border-radius: 10px;
font-size: 12px;
font-weight: 600;
&:hover, &:focus {
background: wal.$color1;
}
}
}
.button-row {
& > button {
background: colors.$bg-secondary;
margin: 0 1px;
padding: 4px 6px;
border-radius: 2px;
&:hover {
background: colors.$bg-tertiary;
}
&:first-child {
border-top-left-radius: 10px;
border-bottom-left-radius: 10px;
margin-left: 0;
}
&:last-child {
border-top-right-radius: 10px;
border-bottom-right-radius: 10px;
margin-right: 0;
}
}
}
selection {
background: colors.$bg-tertiary;
}
label.nf,
button.nf label {
font-size: 12px;
font-family: "Symbols Nerd Font Mono", "Noto Sans Nerd Font Mono",
"0xProto Nerd Font Mono", "Fira Code Nerd Font Mono",
"Symbols Nerd Font", "Noto Sans Nerd Font", "Fira Code Nerd Font",
"Font Awesome";
}
trough {
background: functions.toRGB(color.adjust($color: wal.$color1, $lightness: -20%));
border-radius: 8px;
margin: 2px 0;
}
trough highlight {
background: wal.$color1;
min-height: .9em;
border-top-left-radius: inherit;
border-bottom-left-radius: inherit;
}
trough slider {
border-radius: 50%;
margin: -2px 0;
background: wal.$foreground;
margin-left: -1px;
min-width: 1.2em;
min-height: 1.2em;
}
scrollbar trough {
@include mixins.reset-props;
background: colors.$bg-translucent;
border-radius: 8px;
& slider {
@include mixins.reset-props;
min-width: .45em;
background: colors.$bg-tertiary;
border-radius: 12px;
&:hover, &:active, &:focus {
margin: 2px;
}
}
&:hover, &:active, &:focus {
padding: 2px;
}
}
scale {
& marks mark {
& indicator {
color: colors.$fg-disabled;
min-width: 1px;
min-height: 6px;
}
& label {
font-size: 11px;
font-weight: 400;
color: colors.$fg-disabled;
}
}
}
+44
View File
@@ -0,0 +1,44 @@
@use "sass:color";
@use "./mixins";
@use "./colors";
@use "./functions";
.apps-window-container {
padding: 24px;
background: colors.$bg-translucent;
border-top-left-radius: 24px;
border-top-right-radius: 24px;
& > entry {
background: rgba(colors.$bg-primary, .4);
margin-bottom: 32px;
min-width: 400px;
}
& flowbox {
padding: 16px 24px;
& > flowboxchild {
& > button {
padding: 8px;
border-radius: 24px;
& icon {
font-size: 64px;
}
& label {
margin-top: 6px;
text-shadow: 1px 1px 1px rgba(colors.$bg-primary, .2);
font-weight: 500;
}
}
&:focus > button,
&:selected > button,
& > button:hover {
background-color: rgba($color: colors.$bg-secondary, $alpha: .5);
}
}
}
}
+270
View File
@@ -0,0 +1,270 @@
@use "sass:color";
@use "./mixins";
@use "./colors";
@use "./wal";
@use "./functions";
.bar-container {
@include mixins.reset-props;
padding: 6px;
padding-bottom: 0px;
label {
@include mixins.reset-props;
font-size: 12px;
font-weight: 600;
}
// Style widget groups
& > .bar-centerbox > * {
$radius: 18px;
$color-hover: colors.$bg-primary;
$padding: 4px;
background: rgba(colors.$bg-translucent, .6);
border-radius: $radius;
padding: 0 $padding;
& > eventbox {
&:hover {
& > box:not(.workspaces):not(.special-workspaces) {
background: $color-hover;
}
}
& > box {
border-radius: calc($radius - $padding);
margin: $padding 0;
}
& > box:not(.workspaces):not(.special-workspaces) {
padding: 0 8px;
}
}
& > button,
& > box > button {
border-radius: calc($radius - $padding);
margin: $padding 0;
padding: 0 9px;
&:hover {
background: $color-hover;
}
}
}
.workspaces, .special-workspaces {
@include mixins.reset-props;
padding: 0 4px;
& > eventbox {
& > box {
margin: 3px 0;
border-radius: 16px;
transition: 80ms linear;
min-width: 15px;
padding: 0 6px;
background: colors.$bg-tertiary;
& label.id {
font-weight: 600;
margin-right: 4px;
opacity: 0;
}
}
&.focus > box {
background: colors.$fg-primary;
min-width: 32px;
& label.id {
color: colors.$fg-light;
margin-right: 0;
}
}
& icon {
font-size: 16px;
}
&.show label.id {
opacity: 1;
}
&:hover > box {
box-shadow: inset 0 0 0 100px rgba($color: colors.$fg-primary, $alpha: .2);
}
}
&.special-workspaces {
& > eventbox {
& box {
background: wal.$color4;
}
&:hover > box {
background: functions.toRGB(color.adjust(wal.$color4, $lightness: -6%));
}
}
}
}
.focused-client {
padding: 0 6px;
& > .icon {
margin-right: 6px;
}
& > .text-content {
& > .class {
font-size: 9px;
font-family: monospace;
font-weight: 600;
color: colors.$fg-disabled;
margin-top: 0px;
}
& > .title {
font-size: 12px;
font-weight: 500;
margin-top: -2px;
}
}
}
.clock.open > button {
background-color: colors.$bg-primary;
}
.media-eventbox {
& > .media {
background: colors.$bg-primary;
padding: 0 8px;
}
&:hover > .media {
box-shadow: inset 0 0 0 300px rgba(colors.$fg-primary, .2);
}
& .icon {
font-size: 14px;
}
& .media-controls {
transition: none;
margin-left: 6px;
border-top-right-radius: 12px;
border-bottom-right-radius: 12px;
& > button {
margin: 4px 1px;
& label {
font-size: 8px;
}
}
}
&.reveal {
& .media > box {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
}
.tray {
padding: 0 6px;
& .item {
all: unset;
&:hover {
background: none;
}
margin: 0 6px;
padding: 0;
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
}
}
.status {
@include mixins.reset-props;
&:hover > box,
&.open > box {
background: colors.$bg-primary;
}
& > box {
padding: 0 8px;
& > * > * {
margin: 0 2px;
}
& trough {
min-width: 65px;
min-height: 10px;
margin-right: 4px;
}
& slider {
min-width: 10px;
min-height: 10px;
}
& highlight {
min-height: 10px;
}
& .nf {
margin: {
right: 3px;
left: 2px;
};
font-size: 12px;
}
& .status-icons {
padding: 0 4px;
& > * {
margin: 0 4px;
}
}
}
}
.apps {
& > box {
min-width: 24px;
& > icon {
transition: 120ms linear;
font-size: 14px;
}
}
&.open > box {
background: colors.$bg-primary;
}
&:hover icon {
-gtk-icon-transform: scale(1.14);
}
}
}
+128
View File
@@ -0,0 +1,128 @@
@use "sass:color";
@use "./wal";
@use "./colors";
.center-window-container {
background: colors.$bg-translucent;
border-radius: 18px;
padding: 12px;
& .big-media {
padding: 6px;
& > box > .image {
background-size: cover;
background-position: center center;
border-radius: 10px;
}
& > .info {
padding: {
top: 4px;
bottom: 6px;
};
& .title {
font-size: 16px;
font-weight: 700;
}
& .artist {
font-size: 14px;
font-weight: 600;
color: colors.$fg-disabled;
}
}
& slider {
background: transparent;
min-height: .6em;
}
& trough {
border-radius: 4px;
min-height: .6em;
}
& trough highlight {
border-radius: 4px;
min-height: .6em;
}
& .bottom {
& .controls {
margin-top: 5px;
& button {
padding: 7px;
& label {
font-size: 10px;
}
}
}
& .elapsed,
& .length {
font-size: 12px;
color: colors.$fg-disabled;
}
}
}
& .left .datetime {
padding-bottom: 10px;
& .time {
font-size: 28px;
font-weight: 800;
}
& .date {
font-size: 14px;
font-weight: 500;
color: colors.$fg-disabled;
}
}
& .calendar-box {
& calendar {
$border-radius: 10px;
font-weight: 600;
padding-bottom: 2px;
&.view {
background: colors.$bg-primary;
border-radius: $border-radius;
}
&.header {
background: colors.$bg-secondary;
border-top-left-radius: $border-radius;
border-top-right-radius: $border-radius;
padding: 4px;
}
&.button {
transition: 80ms linear;
border-radius: 6px;
&:hover {
background-color: colors.$bg-tertiary;
}
}
&:selected {
background: colors.$bg-secondary;
border-radius: 6px;
}
&.highlight {
background: transparent;
box-shadow: 0 2px 0 -1px rgba(colors.$bg-secondary, .5);
}
&:focus {
outline: 1px white;
}
}
}
}
+13
View File
@@ -0,0 +1,13 @@
@use "sass:color";
@use "./wal";
@use "./functions";
$bg-primary: functions.toRGB(color.adjust($color: wal.$color1, $lightness: -34%));
$bg-secondary: functions.toRGB(color.adjust($color: wal.$color1, $lightness: -16%));
$bg-tertiary: functions.toRGB(color.adjust($color: $bg-secondary, $lightness: 10%));
$bg-light: wal.$foreground;
$bg-translucent: functions.toRGB(color.change($color: $bg-primary, $alpha: 75%));
$bg-translucent-secondary: functions.toRGB(color.change($color: $bg-translucent, $alpha: 78%));
$fg-primary: wal.$foreground;
$fg-light: $bg-primary;
$fg-disabled: functions.toRGB(color.adjust($color: wal.$foreground, $lightness: -11%));
+264
View File
@@ -0,0 +1,264 @@
@use "sass:color";
@use "./wal";
@use "./colors";
@use "./functions" as funs;
@use "./mixins";
.control-center-container {
@include mixins.reset-props;
background: colors.$bg-translucent;
border-radius: 28px;
padding: 20px;
& > * {
margin: 9px 0;
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
}
& .quickactions {
& .hostname {
font-size: 15px;
font-weight: 600;
}
& .uptime {
font-size: 10.1px;
font-family: "Symbols Nerd Font Mono";
color: colors.$fg-disabled;
}
& .button-row {
& button {
padding: 7px;
margin: {
top: 2px;
bottom: 2px;
};
}
}
}
& .sliders {
padding: 0;
& > box {
margin: 8px 0;
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
}
label.nf,
button.nf label {
margin-right: 8px;
font-size: 15px;
}
button.more {
@include mixins.hover-shadow;
padding: 4px;
border-radius: 16px;
margin-left: 6px;
}
& .page .content {
& > eventbox > box {
margin: 6px 0;
}
& label.name {
font-size: 14px;
font-weight: 500;
}
& trough {
margin-right: 10px;
}
& label.sub-header {
margin-top: 6px;
}
& button.default {
background: colors.$bg-tertiary;
}
}
}
& .page {
transition: 120ms linear;
background: colors.$bg-secondary;
padding: 14px;
border-radius: 24px;
& .header {
margin-bottom: 12px;
& .top > .title {
font-size: 20px;
font-weight: 600;
}
& > .description {
font-size: 12px;
font-weight: 500;
color: colors.$fg-disabled;
}
}
& .sub-header {
font-size: 14px;
font-weight: 500;
margin-bottom: 6px;
}
& button {
@include mixins.hover-shadow;
padding: 6px;
border-radius: 12px;
& label {
font-size: 14px;
}
}
& .extra-buttons {
margin-left: 2px;
& > button {
border-radius: 12px;
}
}
}
}
box.history {
margin-top: 10px;
background: colors.$bg-translucent;
border-radius: 24px;
padding: 20px;
transition: 120ms linear;
&.hide {
opacity: 0;
}
& .notifications {
& .notification {
background: colors.$bg-primary;
}
}
& > .button-row {
margin-top: 12px;
& button {
padding: 6px;
& label.nf {
font-size: 16px;
}
& label:not(.nf) {
font-size: 12px;
font-weight: 600;
}
}
}
}
.tiles-container {
@include mixins.reset-props;
& > flowbox {
& > flowboxchild .tile {
$radius: 16px;
&:not(.toggled) > .toggle-button,
&:not(.toggled) > button.more {
@include mixins.hover-shadow;
background: colors.$bg-primary;
}
&.toggled .toggle-button:hover,
&.toggled button.more:hover {
background: colors.$bg-tertiary;
}
&.toggled > .toggle-button,
&.toggled > button.more {
background: colors.$bg-secondary;
}
&.has-more > .toggle-button {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
& > .toggle-button {
border-radius: $radius;
& .content {
padding: 8px;
& > .icon {
margin-right: 6px;
}
& > .text {
& > .title {
font-weight: 600;
font-size: 15.1px;
}
& > .description {
font-size: 12px;
color: colors.$fg-disabled;
font-weight: 400;
}
}
}
}
& > button.more {
border-top-right-radius: $radius;
border-bottom-right-radius: $radius;
& label {
font-size: 16px;
}
}
}
}
}
.tile-pages {
& > .page {
margin-top: 10px;
&.bluetooth {
button.connected {
background: colors.$bg-tertiary;
}
&.paired {
margin-bottom: 20px;
}
}
}
&.revealed {
padding-top: 12px;
}
}
+14
View File
@@ -0,0 +1,14 @@
@use "./colors";
@use "./mixins";
.floating-notifications-container {
padding: {
right: 6px;
top: 6px;
};
& .notification {
margin: 6px;
box-shadow: 0 0 4px .5px colors.$bg-primary;
}
}
+14
View File
@@ -0,0 +1,14 @@
@use "sass:color";
/**
* GTK3 only supports sRGB color space, unfortunatly
*/
@function toRGB($color) {
@return rgba(
color.channel($color, "red"),
color.channel($color, "green"),
color.channel($color, "blue"),
color.alpha($color)
);
}
+41
View File
@@ -0,0 +1,41 @@
@use "./colors";
.logout-menu {
.top {
.time {
font-size: 128px;
font-weight: 900;
text-shadow: 1px 1px 2px colors.$bg-translucent;
}
.date {
font-size: 24px;
font-weight: 500;
text-shadow: 1px 1px 2px colors.$bg-translucent;
}
}
.button-row {
margin: 0 150px;
& > button {
& label {
font-size: 96px;
}
margin: {
left: 4px;
right: 4px;
}
border-radius: 6px;
&:first-child {
border-top-left-radius: 28px;
border-bottom-left-radius: 28px;
}
&:last-child {
border-top-right-radius: 28px;
border-bottom-right-radius: 28px;
}
}
}
}
+33
View File
@@ -0,0 +1,33 @@
@use "sass:color";
@use "./wal";
@use "./colors";
@use "./functions" as funs;
@mixin reset-props {
all: unset;
transition: 120ms linear;
font-family: "Adwaita Sans", "Cantarell", "Noto Sans",
"Noto Sans CJK JP", "Noto Sans CJK KR",
"Noto Sans CJK HK", "Noto Sans CJK SC",
"Noto Sans CJK TC", sans-serif,
"Symbols Nerd Font Mono";
color: colors.$fg-primary;
}
@mixin hover-shadow {
&:hover {
box-shadow: inset 0 0 0 500px rgba(colors.$fg-primary, .1);
}
}
@mixin hover-shadow2 {
&:hover {
box-shadow: inset 0 0 0 500px rgba(colors.$fg-primary, .2);
}
}
@mixin hover-shadow3 {
&:hover {
box-shadow: inset 0 0 0 500px rgba(colors.$fg-primary, .3);
}
}
+46
View File
@@ -0,0 +1,46 @@
@use "sass:color";
@use "./wal";
@use "./functions" as funs;
.osd {
background: funs.toRGB(color.change($color: wal.$background, $alpha: 65%));
padding: 14px 16px;
border-radius: 20px;
.icon {
margin-right: 10px;
font-size: 24px;
}
.volume {
margin-top: -6px;
.device {
margin-bottom: 5px;
font-size: 14px;
font-weight: 600;
}
levelbar {
trough block {
border-radius: 2px;
background: funs.toRGB(color.adjust($color: wal.$color1, $lightness: -36%));
&.empty {
border-radius: 2px;
}
&.filled {
padding: 3px 0;
background: wal.$color1;
}
}
}
.value {
font-size: 11px;
font-weight: 400;
padding: 0 4px;
}
}
}
+76
View File
@@ -0,0 +1,76 @@
@use "./colors";
.runner.main {
background: colors.$bg-translucent;
padding: 10px;
border-radius: 22px;
& entry {
background: colors.$bg-primary;
padding: 10px 9px;
border-radius: 12px;
margin-bottom: 1px;
min-height: 1.4em;
&:focus {
box-shadow: inset 0 0 0 2px colors.$bg-secondary;
}
& image.left {
margin-right: 6px;
}
}
& list {
all: unset;
& > *:selected > .result,
& > *:active > .result,
& > *:hover > .result {
background: colors.$bg-secondary;
}
& > *:first-child {
margin-top: 12px;
}
&:last-child {
margin-bottom: 0;
}
}
& list .result {
padding: 10px;
background: colors.$bg-primary;
margin: 2px 0;
border-radius: 14px;
& icon {
font-size: 28px;
margin-right: 6px;
}
& .title {
font-weight: 500;
font-size: 16px;
}
& .description {
font-size: 12px;
color: colors.$fg-disabled;
}
}
& .not-found {
padding-top: 24px;
& icon {
font-size: 64px;
margin-bottom: .4em;
}
& label {
font-size: 16px;
}
}
}
+26
View File
@@ -0,0 +1,26 @@
// SCSS Variables
// Generated by 'wal'
$wallpaper: "/home/joaov/wallpapers/Frieren Underwater.jpg";
// Special
$background: #0a1d30;
$foreground: #c1c6cb;
$cursor: #c1c6cb;
// Colors
$color0: #0a1d30;
$color1: #0e7ea2;
$color2: #3a829e;
$color3: #1f96b0;
$color4: #717880;
$color5: #9c8a87;
$color6: #758899;
$color7: #93989d;
$color8: #606b76;
$color9: #13A9D8;
$color10: #4EAED3;
$color11: #2AC9EB;
$color12: #97A0AB;
$color13: #D1B8B5;
$color14: #9CB6CD;
$color15: #c1c6cb;
+14
View File
@@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"experimentalDecorators": true,
"strict": true,
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "Bundler",
"checkJs": true,
"allowJs": false,
"jsx": "react-jsx",
"jsxImportSource": "astal/gtk3"
}
}
+46
View File
@@ -0,0 +1,46 @@
import { Binding } from "astal";
import { Widget } from "astal/gtk3";
import { tr } from "../i18n/intl";
import { CustomDialog, CustomDialogProps } from "./CustomDialog";
export type AskPopupProps = {
title?: string | Binding<string | undefined>;
text: string | Binding<string | undefined>;
cancelText?: string;
acceptText?: string;
onAccept?: () => void;
onCancel?: () => void;
};
/**
* A Popup Widget that asks yes or no to a defined promt.
* Runs onAccept() when user accepts, or else onDecline() when
* user doesn't accept / closes window.
* This window isn't usually registered in this shell windowing
* system.
*/
export function AskPopup(props: AskPopupProps): Widget.Window {
let accepted: boolean = false;
const window = CustomDialog({
namespace: "ask-popup",
widthRequest: 400,
heightRequest: 250,
title: props.title ?? tr("ask_popup.title"),
text: props.text,
onFinish: () => !accepted && props.onCancel?.(),
options: [
{ text: props.cancelText ?? tr("cancel") },
{
text: props.acceptText ?? tr("accept"),
onClick: () => {
accepted = true;
props.onAccept?.();
}
}
]
} as CustomDialogProps);
return window;
}
+59
View File
@@ -0,0 +1,59 @@
import { Binding } from "astal";
import { Astal, Gdk, Widget } from "astal/gtk3";
const { TOP, LEFT, RIGHT, BOTTOM } = Astal.WindowAnchor;
export type BackgroundWindowProps = {
/** GtkWindow Layer */
layer?: Astal.Layer | Binding<Astal.Layer | undefined>;
/** Monitor number where the window should open */
monitor: number | Binding<number | undefined>;
/** Custom stylesheet used in the window. default: `background: rgba(0, 0, 0, .2)` */
css?: string | Binding<string | undefined>;
/** Function that is called when the user triggers a mouse-click or escape action on the window */
onAction?: (window: Widget.Window) => void;
/** Function that is called when the user clicks on the window with primary mouse button */
onClickPrimary?: (window: Widget.Window) => void;
/** Function that is called when the user clicks on the window with secodary mouse button */
onClickSecondary?: (window: Widget.Window) => void;
keymode?: Astal.Keymode;
};
/** Creates a fullscreen GtkWindow that is used for making
* the user focus on the content after this window(e.g.: AskPopup,
* Authentication Window(futurely) or any PopupWindow)
*
* @param props Properties for background-window
*
* @returns The generated background window
*/
export function BackgroundWindow(props: BackgroundWindowProps) {
return new Widget.Window({
namespace: "background-window",
css: props.css ?? "background: rgba(0, 0, 0, .2);",
monitor: props.monitor,
layer: props.layer ?? Astal.Layer.OVERLAY,
anchor: TOP | LEFT | BOTTOM | RIGHT,
keymode: props.keymode ?? Astal.Keymode.NONE,
exclusivity: Astal.Exclusivity.IGNORE,
onKeyPressEvent: (self, event: Gdk.Event) => {
event.get_keyval()[1] === Gdk.KEY_Escape &&
props.onAction?.(self);
},
onButtonPressEvent: (self, event: Gdk.Event) => {
if(event.get_button()[1]) {
props.onAction?.(self);
return;
}
if(event.get_button()[1] === Gdk.BUTTON_PRIMARY) {
props.onClickPrimary?.(self);
return;
}
if(event.get_button()[1] === Gdk.BUTTON_SECONDARY)
props.onClickSecondary?.(self);
}
} as Widget.WindowProps);
}
+107
View File
@@ -0,0 +1,107 @@
import { Binding } from "astal";
import { Astal, Gtk, Widget } from "astal/gtk3";
import { Windows } from "../windows";
import { PopupWindow, PopupWindowProps } from "./PopupWindow";
import { Separator } from "./Separator";
import { tr } from "../i18n/intl";
export type CustomDialogProps = {
namespace?: string | Binding<string>;
className?: string | Binding<string>;
cssBackground?: string;
title?: string | Binding<string>;
text?: string | Binding<string>;
heightRequest?: number | Binding<number>;
widthRequest?: number | Binding<number>;
childOrientation?: Gtk.Orientation | Binding<Gtk.Orientation>;
children?: Array<Gtk.Widget> | Binding<Array<Gtk.Widget>>;
child?: Gtk.Widget | Binding<Gtk.Widget>;
onFinish?: () => void;
options?: Array<CustomDialogOption>;
optionsOrientation?: Gtk.Orientation | Binding<Gtk.Orientation>;
};
export interface CustomDialogOption {
onClick?: () => void;
text: string | Binding<string>;
closeOnClick?: boolean | Binding<boolean>;
}
export function CustomDialog(props: CustomDialogProps = {
options: [{ text: tr("accept") }]
}): Widget.Window {
const window = Windows.createWindowForFocusedMonitor((mon: number) => PopupWindow({
namespace: props.namespace ?? "custom-dialog",
monitor: mon,
cssBackgroundWindow: props.cssBackground ?? "background: rgba(0, 0, 0, .3);",
exclusivity: Astal.Exclusivity.IGNORE,
layer: Astal.Layer.OVERLAY,
widthRequest: props.widthRequest ?? 400,
heightRequest: props.heightRequest ?? 220,
onDestroy: props.onFinish,
child: new Widget.Box({
className: props.className ?? "custom-dialog-container",
orientation: Gtk.Orientation.VERTICAL,
children: [
new Widget.Label({
className: "title",
visible: props.title,
label: props.title
} as Widget.LabelProps),
new Widget.Label({
className: "text",
visible: props.text,
label: props.text,
yalign: 0,
expand: true
} as Widget.LabelProps),
new Widget.Box({
className: "custom-children custom-child",
visible: props.children || props.child,
orientation: props.childOrientation ?? Gtk.Orientation.VERTICAL,
children: props.children,
child: props.child
} as Widget.BoxProps),
Separator({
alpha: .2,
visible: props.options && props.options.length > 0,
spacing: 8,
orientation: Gtk.Orientation.VERTICAL
}),
new Widget.Box({
className: "options",
orientation: props.optionsOrientation ?? Gtk.Orientation.HORIZONTAL,
hexpand: true,
heightRequest: 38,
homogeneous: true,
children: props.options && props.options.map(option => {
const onClick = () => {
option.onClick?.();
(option.closeOnClick ?? true) &&
window.close();
};
const connections: Array<number> = [];
const btn = new Widget.Button({
className: "option",
label: option.text,
hexpand: true,
setup: (self) => {
connections.push(
self.connect("click-release", (_, event: Astal.ClickEvent) =>
event.button === Astal.MouseButton.PRIMARY &&
onClick()),
self.connect("activate", (_) => onClick())
);
},
onDestroy: (self) => connections.map(id => self.disconnect(id))
} as Widget.ButtonProps);
return btn;
})
} as Widget.BoxProps)
]
} as Widget.BoxProps)
} as PopupWindowProps))();
return window;
}
+69
View File
@@ -0,0 +1,69 @@
import { Binding } from "astal";
import { Widget } from "astal/gtk3";
import { tr } from "../i18n/intl";
import { CustomDialog, CustomDialogProps } from "./CustomDialog";
export type EntryPopupProps = {
title: string | Binding<string>;
text?: string | Binding<string>;
cancelText?: string | Binding<string>;
acceptText?: string | Binding<string>;
closeOnAccept?: boolean;
entryPlaceholder?: string | Binding<string>;
onAccept: (userInput: string) => void;
onCancel?: () => void;
onFinish?: () => void;
isPassword?: boolean | Binding<string>;
};
export function EntryPopup(props: EntryPopupProps): Widget.Window {
props.closeOnAccept = props.closeOnAccept ?? true;
const entry = new Widget.Entry({
className: props.isPassword && "password",
visibility: (props.isPassword instanceof Binding) ?
props.isPassword.as(isPasswd => !isPasswd)
: !props.isPassword,
invisibleChar: 0x00B7, // set '·' as the invisible char
xalign: .5,
placeholderText: props.entryPlaceholder,
onActivate: (self) => {
props.closeOnAccept && window.close();
entered = true;
props.onAccept(self.text);
self.text = "";
},
} as Widget.EntryProps);
let entered: boolean = false;
const window = CustomDialog({
namespace: "entry-popup",
widthRequest: 420,
heightRequest: 220,
title: props.title,
text: props.text,
child: entry,
options: [
{
text: props.cancelText ?? tr("cancel"),
onClick: props.onCancel
},
{
text: props.acceptText ?? tr("accept"),
closeOnClick: props.closeOnAccept,
onClick: () => {
entered = true;
props.onAccept(entry.text);
entry.text = "";
}
}
],
onFinish: () => {
!entered && props.onCancel?.()
props.onFinish?.();
}
} as CustomDialogProps);
return window;
}
+172
View File
@@ -0,0 +1,172 @@
import { Astal, Gtk, Widget } from "astal/gtk3";
import AstalNotifd from "gi://AstalNotifd";
import { Separator } from "./Separator";
import { HistoryNotification, Notifications } from "../scripts/notifications";
import { GLib } from "astal";
import { getAppIcon } from "../scripts/apps";
function getNotificationImage(notif: AstalNotifd.Notification|HistoryNotification): (string|undefined) {
const img = notif.image ?? notif.appIcon;
if(!img) return undefined;
if(!img.includes('/')) return undefined;
if(img.startsWith('/'))
return `file://${img}`;
if(img.startsWith('~') || img.startsWith("file://~"))
return `file://${GLib.get_home_dir()}/${img.replace(/^(file\:\/\/|\~|file\:\/\/~)/g, "")}`;
return img;
}
export function NotificationWidget(notification: AstalNotifd.Notification|number|HistoryNotification,
onClose?: (notif: AstalNotifd.Notification|HistoryNotification) => void,
showTime?: boolean /* It's showTime :speaking_head: :boom: :bangbang: */,
holdOnHover?: boolean): Gtk.Widget {
notification = (typeof notification === "number") ?
AstalNotifd.get_default().get_notification(notification)
: notification;
const body: string = notification.body.split(' ').map(strPart => {
if(/^\<(.*)\>/.test(strPart) || /<\/(.*)\>$/.test(strPart))
return strPart;
return strPart.length >= 25 ? `${strPart.substring(0, 22)}...`
: strPart
}).join(' ');
return new Widget.EventBox({
onClick: () => {
if(notification instanceof AstalNotifd.Notification) {
const viewAction = notification.actions.filter(action =>
action.label.toLowerCase() === "view")?.[0];
viewAction && notification.invoke(viewAction.id);
}
onClose?.(notification);
},
onHover: () => holdOnHover && Notifications.getDefault().holdNotification(notification.id),
onHoverLost: () => holdOnHover && onClose?.(notification),
hexpand: true,
vexpand: false,
child: new Widget.Box({
className: `notification ${ (notification instanceof AstalNotifd.Notification) ?
Notifications.getDefault().getUrgencyString(notification.urgency) : "" }`,
homogeneous: false,
expand: true,
orientation: Gtk.Orientation.VERTICAL,
spacing: 5,
children: [
new Widget.Box({
className: "top",
orientation: Gtk.Orientation.HORIZONTAL,
hexpand: true,
vexpand: false,
children: [
new Widget.Icon({
className: "icon app-icon",
icon: notification.appIcon && Astal.Icon.lookup_icon(notification.appIcon) ?
notification.appIcon
: getAppIcon(notification.appName),
setup: (self) => self.set_visible(Boolean(self.get_icon())),
halign: Gtk.Align.START,
css: "font-size: 16px;"
}),
new Widget.Label({
className: "app-name",
halign: Gtk.Align.START,
hexpand: true,
label: notification.appName || "Unknown Application"
} as Widget.LabelProps),
new Widget.Box({
halign: Gtk.Align.END,
children: [
new Widget.Label({
xalign: 1,
visible: !showTime ? false : true,
className: "time",
label: GLib.DateTime.new_from_unix_local(notification.time).format("%H:%M"),
} as Widget.LabelProps),
new Widget.Button({
className: "close nf",
onClick: () => onClose && onClose(notification),
image: new Widget.Icon({
className: "close icon",
icon: "window-close-symbolic"
} as Widget.IconProps)
} as Widget.ButtonProps)
]
} as Widget.BoxProps)
]
} as Widget.BoxProps),
Separator({
orientation: Gtk.Orientation.VERTICAL,
alpha: 10
}),
new Widget.Box({
className: "content",
orientation: Gtk.Orientation.HORIZONTAL,
children: [
new Widget.Box({
className: "image",
setup: (box) => {
const img = getNotificationImage(notification);
box.set_visible(Boolean(img));
img && box.set_css(`background-image: image(url("${img}"))`);
}
} as Widget.BoxProps),
new Widget.Box({
className: "text",
orientation: Gtk.Orientation.VERTICAL,
expand: true,
children: [
new Widget.Label({
className: "summary",
useMarkup: true,
xalign: 0,
truncate: true,
label: notification.summary.replace(/\&/g, "&amp;")
}),
new Widget.Label({
className: "body",
useMarkup: true,
halign: Gtk.Align.START,
xalign: 0,
truncate: false,
wrap: true,
label: body.replace(/\&/g, "&amp;")
} as Widget.LabelProps)
]
} as Widget.BoxProps)
]
} as Widget.BoxProps),
new Widget.Box({
className: "actions button-row",
hexpand: true,
visible: (notification instanceof AstalNotifd.Notification) ?
(notification.actions.filter(action => action.label.toLowerCase() !== "view").length > 0)
: false,
children: (notification instanceof AstalNotifd.Notification) ?
notification.actions.filter(action => action.label.toLowerCase() !== "view")
.map((action: AstalNotifd.Action) =>
new Widget.Button({
className: "action",
label: action.label,
hexpand: true,
onClicked: () => {
notification.invoke(action.id);
onClose && onClose(notification);
}
} as Widget.ButtonProps)
)
: []
} as Widget.BoxProps)
]
} as Widget.BoxProps),
} as Widget.EventBoxProps);
}
+46
View File
@@ -0,0 +1,46 @@
import { Binding } from "astal";
import { Astal, Gdk, Widget } from "astal/gtk3";
import { BackgroundWindow } from "./BackgroundWindow";
type PopupWindowSpecificProps = {
onDestroy?: (self: Widget.Window) => void;
onKeyPressEvent?: (win: Widget.Window, event: Gdk.Event) => void;
/** Stylesheet for the background of the popup-window */
cssBackgroundWindow?: string;
};
export type PopupWindowProps = Omit<Widget.WindowProps, "keymode"> & PopupWindowSpecificProps;
export function PopupWindow(props: PopupWindowProps): Widget.Window {
props.layer = props.layer ?? Astal.Layer.OVERLAY;
const bgWindow = BackgroundWindow({
monitor: props.monitor ?? 0,
layer: props.layer!,
css: props.cssBackgroundWindow ?? "",
onAction: () => window.close()
});
const window = new Widget.Window({
...props,
namespace: props?.namespace ?? "popup-window",
className: `popup-window ${(props.namespace instanceof Binding ?
props.namespace.get() : props.namespace) || ""}`,
keymode: Astal.Keymode.EXCLUSIVE,
layer: props.layer!,
onDestroy: (self) => {
bgWindow.close();
props.onDestroy?.(self);
},
onKeyPressEvent: (self, event: Gdk.Event) => {
if(event.get_keyval()[1] === Gdk.KEY_Escape) {
self.close();
return;
}
props.onKeyPressEvent?.(self, event);
},
} as Widget.WindowProps);
return window;
}
+57
View File
@@ -0,0 +1,57 @@
import { Binding } from "astal";
import { Gtk, Widget } from "astal/gtk3";
export interface SeparatorProps {
class?: string;
alpha?: number;
cssColor?: string;
orientation?: Gtk.Orientation;
size?: number;
spacing?: number;
margin?: number;
visible?: boolean | Binding<boolean>;
}
export function Separator(props: SeparatorProps = {
orientation: Gtk.Orientation.HORIZONTAL
}) {
props.alpha = props.alpha ?
(props.alpha > 1 ?
props.alpha / 100
: props.alpha)
: 1;
props.orientation = props.orientation ?? Gtk.Orientation.HORIZONTAL;
return new Widget.Box({
name: "separator",
...(props.orientation === Gtk.Orientation.HORIZONTAL ?
{ vexpand: true } : { hexpand: true }),
className: `separator ${ props.orientation === Gtk.Orientation.VERTICAL ?
"vertical" : "horizontal" }`,
visible: props.visible,
css: `.vertical {
padding: ${props.spacing ?? 0}px ${props.margin ?? 7}px;
}
.horizontal {
padding: ${props.margin ?? 4}px ${props.spacing ?? 0}px;
}`,
child: new Widget.Box({
className: `${ props.orientation === Gtk.Orientation.VERTICAL ?
"vertical" : "horizontal" } ${ props.class ? props.class : "" }`,
...(props.orientation === Gtk.Orientation.HORIZONTAL ?
{ vexpand: true } : { hexpand: true }),
css: `* {
background: ${ props.cssColor ?? "lightgray" };
opacity: ${props.alpha};
}
.horizontal {
min-width: ${ props.size ?? 1 }px;
}
.vertical {
min-height: ${ props.size ?? 1 }px;
}`
} as Widget.BoxProps)
} as Widget.BoxProps);
}
+20
View File
@@ -0,0 +1,20 @@
import { Gtk, Widget } from "astal/gtk3";
import { tr } from "../../i18n/intl";
import { Windows } from "../../windows";
import { bind } from "astal";
export function Apps(): Gtk.Widget {
return new Widget.EventBox({
onClickRelease: () => Windows.open("apps-window"),
className: bind(Windows, "openWindows").as((openWindows) =>
Object.hasOwn(openWindows, "apps-window") ? "apps open" : "apps"),
child: new Widget.Box({
child: new Widget.Icon({
tooltipText: tr("apps"),
icon: "applications-other-symbolic",
halign: Gtk.Align.CENTER,
hexpand: true
} as Widget.IconProps)
} as Widget.BoxProps)
} as Widget.EventBoxProps);
}
+17
View File
@@ -0,0 +1,17 @@
import { Gtk, Widget } from "astal/gtk3";
import { getDateTime } from "../../scripts/time";
import { bind, GLib } from "astal";
import { Windows } from "../../windows";
export function Clock(): Gtk.Widget {
return new Widget.Box({
className: bind(Windows, "openWindows").as((openWins) =>
Object.hasOwn(openWins, "center-window") ? "open clock" : "clock"),
child: new Widget.Button({
onClick: () => Windows.toggle("center-window"),
label: getDateTime().as((dateTime: GLib.DateTime) => {
return dateTime.format("%A %d, %H:%M")
})
} as Widget.ButtonProps)
} as Widget.BoxProps);
}
+50
View File
@@ -0,0 +1,50 @@
import { bind } from "astal";
import { Gtk, Widget } from "astal/gtk3";
import AstalHyprland from "gi://AstalHyprland";
import { getAppIcon } from "../../scripts/apps";
const hyprland = AstalHyprland.get_default();
export function FocusedClient(): Gtk.Widget {
return new Widget.Box({
className: "focused-client",
visible: bind(hyprland, "focusedClient").as(Boolean),
children: bind(hyprland, "focusedClient").as(focusedClient => focusedClient ? [
new Widget.Icon({
className: "icon",
vexpand: true,
css: ".icon { font-size: 18px; }",
icon: bind(focusedClient, "class").as(clss =>
getAppIcon(clss) ?? "application-x-executable-symbolic")
}),
new Widget.Box({
className: "text-content",
orientation: Gtk.Orientation.VERTICAL,
homogeneous: false,
valign: Gtk.Align.CENTER,
children: [
new Widget.Label({
className: "class",
xalign: 0,
visible: bind(focusedClient, "class").as(Boolean),
maxWidthChars: 55,
truncate: true,
tooltipText: bind(focusedClient, "class").as((clientClass: string) =>
clientClass.length > 55 ? clientClass : ""),
label: bind(focusedClient, "class")
} as Widget.LabelProps),
new Widget.Label({
className: "title",
xalign: 0,
maxWidthChars: 50,
visible: bind(focusedClient, "title").as(Boolean),
truncate: true,
tooltipText: bind(focusedClient, "title").as((clientTitle: string) =>
clientTitle.length > 55 ? clientTitle : ""),
label: bind(focusedClient, "title")
} as Widget.LabelProps)
]
})
]: [])
} as Widget.BoxProps);
}
+131
View File
@@ -0,0 +1,131 @@
import { bind, execAsync, GLib } from "astal";
import { Gtk, Widget } from "astal/gtk3";
import AstalMpris from "gi://AstalMpris";
import { Separator, SeparatorProps } from "../Separator";
import { Windows } from "../../windows";
const playerIcons = {
spotify: '󰓇',
clapper: '󰿎',
mpv: '',
spotube: '󰋋',
firefox: '󰈹'
}
export function Media(): Gtk.Widget {
const connections: Array<number> = [];
const mediaControlsRevealer: Widget.Revealer = new Widget.Revealer({
transitionType: Gtk.RevealerTransitionType.SLIDE_RIGHT,
transitionDuration: 260,
revealChild: false,
child: new Widget.Box({
className: "media-controls button-row",
expand: false,
homogeneous: false,
children: bind(AstalMpris.get_default(), "players").as((players: Array<AstalMpris.Player>) =>
players[0] ? [
new Widget.Button({
className: "link nf",
label: "󰌹",
tooltipText: "Copy link to Clipboard",
visible: bind(players[0], "metadata").as((_metadata: GLib.HashTable) =>
players[0].get_meta("xesam:url") === null),
onClick: () => execAsync(`sh -c "wl-copy \\"$(playerctl metadata 'xesam:url')\\""`)
} as Widget.ButtonProps),
new Widget.Button({
className: "previous nf",
label: "󰒮",
tooltipText: "Previous",
onClick: () => players[0].canGoPrevious && players[0].previous()
} as Widget.ButtonProps),
new Widget.Button({
className: "pause nf",
tooltipText: bind(players[0], "playback_status").as((status: AstalMpris.PlaybackStatus) =>
status === AstalMpris.PlaybackStatus.PLAYING ? "Pause" : "Play"),
label: bind(players[0], "playbackStatus").as((status: AstalMpris.PlaybackStatus) =>
status === AstalMpris.PlaybackStatus.PLAYING ? "󰏤" : "󰐊"),
onClick: () => {
players[0].playbackStatus === AstalMpris.PlaybackStatus.PAUSED ?
players[0].play()
:
players[0].pause()
}
} as Widget.ButtonProps),
new Widget.Button({
className: "next nf",
label: "󰒭",
tooltipText: "Next",
onClick: () => players[0].canGoNext && players[0].next()
} as Widget.ButtonProps)
] : new Widget.Label({
label: "Don't Stop The Music!"
} as Widget.LabelProps)
)
} as Widget.BoxProps)
} as Widget.RevealerProps);
const mediaWidget = new Widget.EventBox({
className: "media-eventbox",
visible: bind(AstalMpris.get_default(), "players").as((players: Array<AstalMpris.Player>) =>
players[0] && players[0].get_available()),
onDestroy: (_) => connections.map(id => _.disconnect(id)),
onClick: () => Windows.toggle("center-window"),
child: new Widget.Box({
className: "media",
children: [
new Widget.Box({
spacing: 4,
children: bind(AstalMpris.get_default(), "players").as((players: Array<AstalMpris.Player>) =>
players[0] ? [
new Widget.Label({
className: "icon nf",
label: bind(players[0], "busName").as((busName: string) => {
const playerName: string = busName.split('.')[busName.split('.').length-1];
return playerIcons[playerName.toLowerCase() as keyof typeof playerIcons] || "󰎇";
})
} as Widget.LabelProps),
new Widget.Label({
className: "title",
label: bind(players[0], "title").as((title: string) => title || "No Title"),
maxWidthChars: 20,
truncate: true
} as Widget.LabelProps),
Separator({
orientation: Gtk.Orientation.HORIZONTAL,
size: 1,
margin: 5,
//cssColor: `rgb(180, 180, 180)`,
alpha: .3
} as SeparatorProps),
new Widget.Label({
className: "artist",
label: bind(players[0], "artist").as((artist: string) => artist || "No Artist"),
maxWidthChars: 18,
truncate: true
} as Widget.LabelProps)
] : new Widget.Label({
label: "Crazy to think this widget haven't disappeared yet!"
} as Widget.LabelProps)
)
} as Widget.BoxProps),
mediaControlsRevealer
]
} as Widget.BoxProps)
} as Widget.EventBoxProps);
connections.push(
mediaWidget.connect("hover", () => {
mediaControlsRevealer.set_reveal_child(true);
mediaWidget.className = mediaWidget.className + " reveal";
}),
mediaWidget.connect("hover-lost", (_) => {
mediaControlsRevealer.set_reveal_child(false);
_.className = mediaWidget.className.replaceAll(" reveal", "");
})
);
return mediaWidget;
}
+43
View File
@@ -0,0 +1,43 @@
import { bind, Variable } from "astal";
import { Gtk, Widget } from "astal/gtk3"
import AstalHyprland from "gi://AstalHyprland";
import { getAppIcon } from "../../scripts/apps";
export const SpecialWorkspaces: (() => Gtk.Widget) = () => new Widget.EventBox({
className: "special-ws-eventbox",
visible: bind(AstalHyprland.get_default(), "workspaces").as((workspaces) =>
workspaces.filter(ws => ws.id < 0).sort((a, b) => a.id - b.id).length > 0),
child: new Widget.Box({
className: "special-workspaces",
spacing: 4,
children: bind(AstalHyprland.get_default(), "workspaces").as((workspaces) =>
workspaces.filter(ws => ws.id < 0).sort((a, b) => a.id - b.id).map((workspace) =>
new Widget.EventBox({
className: bind(AstalHyprland.get_default(), "focusedWorkspace").as(focusWs =>
`${focusWs.id === workspace.id ? "focus" : ""}`),
tooltipText: bind(workspace, "name").as((name) => {
name = name.replace(/^special\:/, "");
return name.charAt(0).toUpperCase().concat(name.substring(1, name.length));
}),
child: new Widget.Box({
child: new Widget.Icon({
className: "last-app-icon",
visible: Variable.derive([
bind(workspace, "lastClient"),
bind(AstalHyprland.get_default(), "focusedWorkspace")
], (lastClient, focusedWorkspace) => focusedWorkspace?.id === workspace.id ?
false : Boolean(lastClient))(),
icon: bind(workspace, "lastClient").as((lastClient) =>
lastClient ?
getAppIcon(lastClient.initialClass) || "image-missing"
: "image-missing")
} as Widget.IconProps)
} as Widget.BoxProps),
onClickRelease: () => AstalHyprland.get_default().dispatch(
"togglespecialworkspace", workspace.name.replace(/^special\:/, "")
)
} as Widget.EventBoxProps)
)
)
} as Widget.BoxProps)
} as Widget.EventBoxProps);
+159
View File
@@ -0,0 +1,159 @@
import AstalBluetooth from "gi://AstalBluetooth";
import AstalNetwork from "gi://AstalNetwork";
import AstalWp from "gi://AstalWp";
import { bind, Binding, Variable } from "astal";
import { Gtk, Widget } from "astal/gtk3";
import { Wireplumber } from "../../scripts/volume";
import { Notifications } from "../../scripts/notifications";
import { Windows } from "../../windows";
import { Recording } from "../../scripts/recording";
import { getDateTime } from "../../scripts/time";
import { tr } from "../../i18n/intl";
export function Status(): Gtk.Widget {
return new Widget.EventBox({
className: bind(Windows, "openWindows").as((openWins) =>
Object.hasOwn(openWins, "control-center") ? "open status" : "status"),
onClick: () => Windows.toggle("control-center"),
child: new Widget.Box({
children: [
volumeStatus({
className: "sink",
endpoint: Wireplumber.getDefault().getDefaultSink(),
icon: bind(Wireplumber.getDefault().getDefaultSink(), "mute").as((muted) =>
!muted ? "󰕾" : "󰖁")
}),
volumeStatus({
className: "source",
endpoint: Wireplumber.getDefault().getDefaultSource(),
icon: bind(Wireplumber.getDefault().getDefaultSource(), "mute").as((muted) =>
!muted ? "󰍬" : "󰍭")
}),
StatusIcons()
]
} as Widget.BoxProps)
} as Widget.EventBoxProps);
}
function volumeStatus(props: { className?: string, endpoint: AstalWp.Endpoint, icon?: (string|Binding<string>) }): Gtk.Widget {
return new Widget.EventBox({
className: props.className,
onScroll: (_, event) =>
event.delta_y > 0 ?
Wireplumber.getDefault().decreaseEndpointVolume(props.endpoint, 5)
:
Wireplumber.getDefault().increaseEndpointVolume(props.endpoint, 5),
child: new Widget.Box({
children: [
new Widget.Label({
className: "nf",
visible: props.icon,
label: props.icon,
} as Widget.LabelProps),
new Widget.Label({
className: "volume",
label: bind(props.endpoint, "volume").as((volume: number) =>
Math.floor(volume * 100) + "%")
} as Widget.LabelProps),
]
} as Widget.BoxProps)
} as Widget.EventBoxProps)
}
function StatusIcons(): Gtk.Widget {
const bluetoothIcon: Variable<string> = Variable.derive([
bind(AstalBluetooth.get_default(), "isPowered"),
bind(AstalBluetooth.get_default(), "isConnected")
], (powered, connected) => {
return powered ? (
connected ? "󰂱"
: "󰂯"
) : "󰂲"
});
const networkIcon: Variable<string> = Variable.derive([
bind(AstalNetwork.get_default(), "primary"),
bind(AstalNetwork.get_default(), "wired"),
bind(AstalNetwork.get_default(), "wifi")
],
(primary, wired, wifi) => {
switch(primary) {
case AstalNetwork.Primary.WIRED: return wired ?
"󰛳"
: "󰛵";
case AstalNetwork.Primary.WIFI: return wifi ?
"󰤨"
: "󰤭";
}
return "󰲊";
});
const recordingTimer: Variable<string> = Variable.derive([
bind(Recording.getDefault(), "recording"),
getDateTime()
], (recording, dateTime) => {
if(!recording || !Recording.getDefault().startedAt)
return "...";
const startedAtSeconds = dateTime.to_unix() - Recording.getDefault().startedAt!.to_unix();
if(startedAtSeconds <= 0) return "00:00";
const hours = Math.floor(startedAtSeconds / 120);
const minutes = Math.floor(startedAtSeconds / 60);
const seconds = Math.floor(startedAtSeconds % 60);
return `${ hours > 0 ? `${hours < 10 ? `0${hours}` : hours }:` : ""
}${ minutes < 10 ? `0${minutes}` : minutes
}:${ seconds < 10 ? `0${seconds}` : seconds }`;
});
return new Widget.Box({
className: "status-icons",
children: [
new Widget.Label({
className: "bluetooth nf state",
visible: bind(AstalBluetooth.get_default(), "adapter").as(Boolean),
label: bluetoothIcon(),
onDestroy: () => bluetoothIcon.drop()
} as Widget.LabelProps),
new Widget.Label({
className: "network nf state",
label: networkIcon(),
onDestroy: () => networkIcon.drop()
} as Widget.LabelProps),
new Widget.Revealer({
revealChild: bind(Recording.getDefault(), "recording"),
transitionDuration: 500,
transitionType: Gtk.RevealerTransitionType.SLIDE_LEFT,
setup: (revealer) => revealer.add(
new Widget.EventBox({
onClick: () => Recording.getDefault().recording &&
Recording.getDefault().stopRecording(),
tooltipText: tr("control_center.tiles.recording.enabled_desc"),
child: new Widget.Box({
children: [
new Widget.Label({
className: "recording nf state",
label: '󰻃'
} as Widget.LabelProps),
new Widget.Label({
className: "rec-time",
label: recordingTimer()
} as Widget.LabelProps)
]
} as Widget.BoxProps)
} as Widget.EventBoxProps)
)
} as Widget.RevealerProps),
new Widget.Label({
className: "bell nf state",
label: bind(Notifications.getDefault().getNotifd(), "dontDisturb").as((dnd: boolean) =>
dnd ? "󰂠" : "󰂚")
} as Widget.LabelProps),
]
} as Widget.BoxProps);
}
+48
View File
@@ -0,0 +1,48 @@
import { bind, Gio, Variable } from "astal";
import { Astal, Gdk, Gtk, Widget } from "astal/gtk3";
import AstalTray from "gi://AstalTray"
const astalTray = AstalTray.get_default();
function menuFromModel(model: Gio.MenuModel, actionGroup: Gio.ActionGroup | null): Gtk.Menu {
const menu = Gtk.Menu.new_from_model(model);
menu.insert_action_group("dbusmenu", actionGroup)
return menu;
}
export function Tray(): Gtk.Widget {
return new Widget.Box({
className: "tray",
visible: bind(astalTray, "items").as((items: Array<AstalTray.TrayItem>) => items.length > 0),
children: bind(astalTray, "items").as((items: Array<AstalTray.TrayItem>) =>
items.map((item: AstalTray.TrayItem) =>
new Widget.Box({
className: "item",
child: Variable.derive(
[ bind(item, "menuModel"), bind(item, "actionGroup") ],
(menuModel: Gio.MenuModel, actionGroup: Gio.ActionGroup) => {
const menu = menuFromModel(menuModel, actionGroup);
return new Widget.Button({
className: "item-button",
tooltipMarkup: bind(item, "tooltipMarkup"),
onClick: (_, event: Astal.ClickEvent) => {
if(event.button === Astal.MouseButton.SECONDARY) {
item.about_to_show();
menu.popup_at_widget(_, Gdk.Gravity.NORTH, Gdk.Gravity.SOUTH_WEST, null);
} else if(event.button === Astal.MouseButton.PRIMARY)
item.activate(event.x, event.y);
},
halign: Gtk.Align.CENTER,
child: new Widget.Icon({
gIcon: bind(item, "gicon")
})
} as Widget.ButtonProps)
}
)()
} as Widget.BoxProps)
)
)
} as Widget.BoxProps);
}
+86
View File
@@ -0,0 +1,86 @@
import { bind, Variable } from "astal";
import { Gtk, Widget } from "astal/gtk3";
import AstalHyprland from "gi://AstalHyprland";
import { getAppIcon } from "../../scripts/apps";
let showWsNum: (Variable<boolean>|undefined);
export const showWorkspaceNumber = (show: boolean) =>
showWsNum?.set(show);
export function Workspaces(): Gtk.Widget {
showWsNum = new Variable<boolean>(false);
return new Widget.EventBox({
onScroll: (_, event) =>
event.delta_y > 0 ?
AstalHyprland.get_default().dispatch("workspace", "e-1")
: AstalHyprland.get_default().dispatch("workspace", "e+1"),
onHover: () => showWorkspaceNumber(true),
onHoverLost: () => showWorkspaceNumber(false),
onDestroy: () => {
showWsNum?.drop();
showWsNum = undefined;
},
child: new Widget.Box({
className: "workspaces",
spacing: 4,
children: bind(AstalHyprland.get_default(), "workspaces").as((workspaces) =>
workspaces.filter((ws) => ws.id > 0).sort((a, b) => a.id - b.id).map((workspace) => {
const className = Variable.derive([
bind(AstalHyprland.get_default(), "focusedWorkspace"),
showWsNum!()
], (focusedWs, showWsNumbers) =>
`${focusedWs.id === workspace.id ? "focus" : ""} ${showWsNumbers ? "show" : ""}`
);
const tooltipText = Variable.derive([
bind(workspace, "lastClient"),
bind(AstalHyprland.get_default(), "focusedWorkspace")
], (lastClient, focusWs) => focusWs.id === workspace.id ? "" :
`Workspace ${workspace.id}${ lastClient ? ` - ${
!lastClient.title.toLowerCase().includes(lastClient.class) ?
`${lastClient.get_class()}: `
: ""
} ${lastClient.title}` : "" }`
);
return new Widget.EventBox({
className: className(),
onClickRelease: () => workspace.focus(),
tooltipText: tooltipText(),
onDestroy: () => {
className.drop();
tooltipText.drop();
},
child: new Widget.Box({
children: bind(workspace, "lastClient").as((lastClient) => [
new Widget.Revealer({
transitionDuration: 200,
transitionType: Gtk.RevealerTransitionType.SLIDE_LEFT,
revealChild: showWsNum!(),
child: new Widget.Label({
label: bind(workspace, "id").as(String),
className: "id",
hexpand: true
} as Widget.LabelProps)
} as Widget.RevealerProps),
new Widget.Icon({
className: "last-app-icon",
visible: bind(AstalHyprland.get_default(), "focusedWorkspace").as(focusedWorkspace =>
workspace.id === focusedWorkspace.id ?
false
: Boolean(lastClient)),
icon: lastClient ?
bind(lastClient, "class").as((clss) =>
getAppIcon(clss) ?? "application-x-executable-symbolic")
: undefined
} as Widget.IconProps)
])
} as Widget.BoxProps)
} as Widget.EventBoxProps);
})
)
} as Widget.BoxProps)
} as Widget.EventBoxProps);
}
+191
View File
@@ -0,0 +1,191 @@
import { AstalIO, bind, Binding, execAsync, GLib, timeout } from "astal";
import { Gtk, Widget } from "astal/gtk3";
import AstalMpris from "gi://AstalMpris";
export function BigMedia(): Gtk.Widget {
let dragTimer: (AstalIO.Time|undefined);
return new Widget.Box({
className: "big-media",
orientation: Gtk.Orientation.VERTICAL,
homogeneous: false,
width_request: 250,
visible: bind(AstalMpris.get_default(), "players").as((players: Array<AstalMpris.Player>) =>
players[0] ? true : false),
children: bind(AstalMpris.get_default(), "players").as((players: Array<AstalMpris.Player>) =>
players[0] && [
new Widget.Box({
halign: Gtk.Align.CENTER,
child: new Widget.Box({
className: "image",
hexpand: false,
orientation: Gtk.Orientation.VERTICAL,
marginTop: 6,
visible: getAlbumArt(players[0]).as(Boolean),
css: getAlbumArt(players[0]).as((artUrl: string|undefined) =>
artUrl ? `.image { background-image: url('${artUrl}'); }` : undefined),
width_request: 132,
height_request: 128
} as Widget.BoxProps)
} as Widget.BoxProps),
new Widget.Box({
className: "info",
orientation: Gtk.Orientation.VERTICAL,
vexpand: true,
valign: Gtk.Align.CENTER,
children: [
new Widget.Label({
className: "title",
tooltipText: bind(players[0], "title").as((title: string) => !title ? "No Title" : title),
label: bind(players[0], "title").as((title: string) => !title ? "No Title" : title),
truncate: true,
maxWidthChars: 25,
} as Widget.LabelProps),
new Widget.Label({
className: "artist",
tooltipText: bind(players[0], "artist").as((artist: string) => !artist ? "No Artist" : artist),
label: bind(players[0], "artist").as((artist: string) => !artist ? "No Artist" : artist),
maxWidthChars: 28,
truncate: true,
} as Widget.LabelProps)
]
} as Widget.BoxProps),
new Widget.Box({
className: "progress",
hexpand: true,
visible: bind(players[0], "canSeek"),
children: [
new Widget.Slider({
min: 0,
hexpand: true,
max: bind(players[0], "length").as((length: number) =>
Math.floor(length)),
value: bind(players[0], "position").as((position: number) =>
Math.floor(position)),
onDragged: (slider: Widget.Slider) => {
if(dragTimer === undefined)
dragTimer = timeout(600, () =>
players[0].set_position(Math.round(slider.value)));
else {
dragTimer.cancel();
dragTimer = timeout(600, () =>
players[0].set_position(Math.round(slider.value)));
}
}
})
]
}),
new Widget.CenterBox({
className: "bottom",
homogeneous: false,
hexpand: true,
marginBottom: 6,
startWidget: new Widget.Label({
className: "elapsed",
valign: Gtk.Align.START,
halign: Gtk.Align.START,
label: bind(players[0], "position").as((pos: number) => {
const sec: number = Math.floor(pos % 60);
return pos > 0 && players[0].length > 0 ?
`${Math.floor(pos / 60)}:${sec < 10 ? "0" : ""}${sec}`
: `0:00`;
})
} as Widget.LabelProps),
centerWidget: new Widget.Box({
className: "controls button-row",
children: [
new Widget.Button({
className: "link nf",
label: "󰌹",
tooltipText: "Copy link to Clipboard",
visible: bind(players[0], "metadata").as((_meta: GLib.HashTable) =>
players[0].get_meta("xesam:url") === null),
onClick: () => execAsync(`sh -c "wl-copy \\"$(playerctl metadata 'xesam:url')\\""`)
} as Widget.ButtonProps),
new Widget.Button({
className: "shuffle nf",
visible: bind(players[0], "shuffleStatus").as((shuffleStatus: AstalMpris.Shuffle) =>
shuffleStatus !== AstalMpris.Shuffle.UNSUPPORTED),
label: bind(players[0], "shuffleStatus").as((shuffleStatus: AstalMpris.Shuffle) =>
shuffleStatus === AstalMpris.Shuffle.ON ? "󰒝" : "󰒞"),
tooltipText: "Toggle Shuffle",
onClick: () => players[0].shuffle()
} as Widget.ButtonProps),
new Widget.Button({
className: "previous nf",
label: "󰒮",
tooltipText: "Previous",
onClick: () => players[0].canGoPrevious && players[0].previous()
} as Widget.ButtonProps),
new Widget.Button({
className: "pause nf",
tooltipText: bind(players[0], "playback_status").as((status: AstalMpris.PlaybackStatus) =>
status === AstalMpris.PlaybackStatus.PLAYING ? "Pause" : "Play"),
label: bind(players[0], "playbackStatus").as((status: AstalMpris.PlaybackStatus) =>
status === AstalMpris.PlaybackStatus.PLAYING ? "󰏤" : "󰐊"),
onClick: () => {
players[0].playbackStatus === AstalMpris.PlaybackStatus.PAUSED ?
players[0].play()
:
players[0].pause()
}
} as Widget.ButtonProps),
new Widget.Button({
className: "next nf",
label: "󰒭",
tooltipText: "Next",
onClick: () => players[0].canGoNext && players[0].next()
} as Widget.ButtonProps),
new Widget.Button({
className: "repeat nf",
visible: bind(players[0], "loopStatus").as((loopStatus: AstalMpris.Loop) =>
loopStatus !== AstalMpris.Loop.UNSUPPORTED),
label: bind(players[0], "loopStatus").as((loopStatus: AstalMpris.Loop) => {
switch(loopStatus) {
case AstalMpris.Loop.TRACK: return "󰑘";
case AstalMpris.Loop.PLAYLIST: return "󰑖";
default: return "󰑗";
}
}),
tooltipText: "Toggle Loop",
onClick: () => players[0].loop()
} as Widget.ButtonProps)
]
} as Widget.BoxProps),
endWidget: new Widget.Label({
className: "length",
valign: Gtk.Align.START,
halign: Gtk.Align.END,
label: bind(players[0], "length").as((len/* bananananananana */: number) => {
const sec: number = Math.floor(len % 60);
return len > 0 ?
`${Math.floor(len / 60)}:${sec < 10 ? "0" : ""}${sec}`
: "0:00";
})
} as Widget.LabelProps)
})
])
} as Widget.BoxProps);
}
/**
* This function handles album art/cover of playing media. If a file is provided
* by the player, it adds the "file://" uri as a prefix, so you can use it in css.
*
* @param player the player you want to pull album art from
* @returns Binding to player.artUrl containing the album art uri, or an undefined binding ig none was found.
* */
function getAlbumArt(player: AstalMpris.Player): Binding<string | undefined> {
return bind(player, "artUrl").as((artUrl: string) => {
if(!artUrl)
return undefined;
if(artUrl.startsWith("/"))
return "file://" + artUrl;
return artUrl;
});
}
+46
View File
@@ -0,0 +1,46 @@
import { register, Variable } from "astal";
import { Gtk, Widget } from "astal/gtk3";
type CalendarProps = Pick<Widget.BoxProps,
"name"
| "className"
| "css"
| "expand"
| "halign"
| "valign"> & {
showWeekDays?: boolean;
showHeader?: boolean;
fillGrid?: boolean; // I need a better name for this LMAOOO
};
@register({ GTypeName: "Calendar" })
class Calendar extends Gtk.Box {
#showWeekDays = new Variable<boolean>(true);
#showHeader = new Variable<boolean>(true);
#fillGrid = new Variable<boolean>(false);
set fillGrid(newValue: boolean) { this.#fillGrid.set(newValue); }
get fillGrid() { return this.#fillGrid.get(); }
set showHeader(newValue: boolean) { this.#showHeader.set(newValue); }
get showHeader() { return this.#showHeader.get(); }
set showWeekDays(newValue: boolean) { this.#showWeekDays.set(newValue); }
get showWeekDays() { return this.#showWeekDays.get(); }
constructor(props?: CalendarProps) {
super();
this.add(new Widget.Box({
...props,
widthRequest: 128,
heightRequest: 128,
children: [
new Widget.Box({
className: "header",
heightRequest: 24,
hexpand: true,
} as Widget.BoxProps)
]
} as Widget.BoxProps));
}
}
+65
View File
@@ -0,0 +1,65 @@
import { bind } from "astal";
import { Gtk, Widget } from "astal/gtk3";
import { HistoryNotification, Notifications } from "../../scripts/notifications";
import { NotificationWidget } from "../Notification";
export const NotifHistory = () => {
return new Widget.Box({
orientation: Gtk.Orientation.VERTICAL,
className: bind(Notifications.getDefault(), "history").as(history => history.length > 0 ? "history" : "history hide"),
children: [
new Widget.Scrollable({
className: "history",
hscroll: Gtk.PolicyType.NEVER,
vscroll: Gtk.PolicyType.AUTOMATIC,
propagateNaturalHeight: true,
propagateNaturalWidth: false,
onDraw: (scrollable) => {
if(!(scrollable.get_child()! as Gtk.Viewport).get_child()) return;
scrollable.minContentHeight =
((scrollable.get_child()! as Gtk.Viewport).get_child() as Widget.Box
).get_children()?.[0].get_allocation().height
|| 0;
},
child: new Widget.Box({
className: "notifications",
hexpand: true,
orientation: Gtk.Orientation.VERTICAL,
homogeneous: false,
spacing: 4,
valign: Gtk.Align.START,
children: bind(Notifications.getDefault(), "history").as((history: Array<HistoryNotification>) =>
history.map((notification: HistoryNotification) => NotificationWidget(notification,
() => Notifications.getDefault().removeHistory(notification.id), true)
))
} as Widget.BoxProps)
} as Widget.ScrollableProps),
new Widget.Box({
vexpand: false,
hexpand: true,
halign: Gtk.Align.END,
className: "button-row",
children: [
new Widget.Button({
className: "clear-all",
child: new Widget.Box({
children: [
new Widget.Label({
className: "nf",
css: "margin-right: 6px",
label: "󰎟"
} as Widget.LabelProps),
new Widget.Label({
label: "Clear"
} as Widget.LabelProps)
]
} as Widget.BoxProps),
onClick: () => Notifications.getDefault().clearHistory(),
} as Widget.ButtonProps)
]
})
]
} as Widget.BoxProps);
}
+79
View File
@@ -0,0 +1,79 @@
import { property, register, timeout } from "astal";
import { Gtk, Widget } from "astal/gtk3";
import { Page } from "./pages/Page";
export { Pages };
export type PagesProps = {
initialPage?: Page;
className?: string;
transitionType?: Gtk.RevealerTransitionType;
transitionDuration?: number;
};
@register({ GTypeName: "Pages" })
class Pages extends Widget.Revealer {
#page: (Page|undefined);
@property(Page)
get page(): Page | undefined { return this.#page; }
private set page(newPage: Page | undefined) {
this.#page = newPage;
this.notify("page");
}
get isOpen() { return this.revealChild; }
constructor(props?: PagesProps) {
super({
className: props?.className
});
this.name = "pages";
if(props?.className !== null && props?.className !== undefined)
this.className = props?.className;
this.transitionType = props?.transitionType ??
Gtk.RevealerTransitionType.SLIDE_DOWN;
this.transitionDuration = props?.transitionDuration ?? 350;
if(props?.initialPage)
this.open(props.initialPage);
}
toggle(newPage?: Page): void {
if(this.isOpen) {
if(newPage && this.#page!.id !== newPage.id) {
this.close(() => this.open(newPage));
return;
}
this.close();
return;
}
if(newPage) this.open(newPage);
}
open(newPage: Page, onOpened?: () => void) {
if(this.isOpen) return;
this.page = newPage;
this.add(newPage);
this.revealChild = true;
onOpened && timeout(this.transitionDuration, onOpened);
}
close(onClosed?: () => void): void {
if(!this.isOpen) return;
this.revealChild = false;
timeout(this.transitionDuration, () => {
this.remove(this.#page!);
this.page = undefined;
onClosed?.();
});
}
}
+103
View File
@@ -0,0 +1,103 @@
import { exec, execAsync, GLib, Variable } from "astal";
import { Gtk, Widget } from "astal/gtk3";
import AstalHyprland from "gi://AstalHyprland";
import { Windows } from "../../windows";
import { Wallpaper } from "../../scripts/wallpaper";
function LockButton(): Widget.Button {
return new Widget.Button({
className: "nf",
label: "󰌾",
onClick: () => {
Windows.close("control-center");
AstalHyprland.get_default().dispatch("exec", "hyprlock");
}
} as Widget.ButtonProps)
}
function ColorPickerButton(): Widget.Button {
return new Widget.Button({
className: "nf",
label: "󰴱",
onClick: () => AstalHyprland.get_default().dispatch(
"exec",
"sh $HOME/.config/hypr/scripts/color-picker.sh"
)
} as Widget.ButtonProps)
}
function ScreenshotButton(): Widget.Button {
return new Widget.Button({
className: "nf",
label: "󰹑",
onClick: () => {
Windows.close("control-center");
execAsync(`sh ${GLib.get_user_config_dir()}/hypr/scripts/screenshot.sh`);
}
} as Widget.ButtonProps);
}
function SelectWallpaperButton(): Widget.Button {
return new Widget.Button({
className: "nf",
label: "󰸉",
onClick: () => {
Windows.close("control-center");
Wallpaper.getDefault().pickWallpaper();
}
} as Widget.ButtonProps);
}
function LogoutButton(): Widget.Button {
return new Widget.Button({
className: "nf",
label: "󰗽",
onClick: () => Windows.open("logout-menu")
} as Widget.ButtonProps);
}
export const QuickActions = () => {
const uptime = new Variable<string>("Just turned on").poll(1000,
() => exec("uptime -p").replace(/^up /, ""));
return new Widget.Box({
className: "quickactions",
children: [
new Widget.Box({
orientation: Gtk.Orientation.VERTICAL,
halign: Gtk.Align.START,
hexpand: true,
className: "left",
children: [
new Widget.Label({
className: "hostname",
xalign: 0,
tooltipText: "Host name",
label: GLib.get_host_name()
} as Widget.LabelProps),
new Widget.Label({
className: "uptime",
xalign: 0,
tooltipText: "Uptime",
onDestroy: () => uptime.drop(),
label: uptime().as((uptime: string) => `󰥔 ${uptime}`)
} as Widget.LabelProps)
]
} as Widget.BoxProps),
new Widget.Box({
orientation: Gtk.Orientation.HORIZONTAL,
className: "right button-row",
halign: Gtk.Align.END,
hexpand: true,
children: [
LockButton(),
ColorPickerButton(),
ScreenshotButton(),
SelectWallpaperButton(),
LogoutButton()
]
} as Widget.BoxProps)
]
} as Widget.BoxProps);
}
+69
View File
@@ -0,0 +1,69 @@
import { bind } from "astal";
import { Gtk, Widget } from "astal/gtk3";
import { Wireplumber } from "../../scripts/volume";
import { Pages } from "./Pages";
import { PageSound } from "./pages/Sound";
import { PageMicrophone } from "./pages/Microphone";
export function Sliders() {
const slidersPages = new Pages();
return new Widget.Box({
className: "sliders",
orientation: Gtk.Orientation.VERTICAL,
expand: true,
children: [
new Widget.Box({
className: "sink speaker",
children: bind(Wireplumber.getWireplumber(), "defaultSpeaker").as((sink) => [
new Widget.Button({
className: "nf",
label: bind(sink, "mute").as((muted) => !muted ? "󰕾" : "󰖁"),
onClick: () => Wireplumber.getDefault().toggleMuteSink()
} as Widget.ButtonProps),
new Widget.Slider({
drawValue: false,
hexpand: true,
setup: (slider) => slider.value = Math.floor(sink.volume * 100),
value: bind(sink, "volume").as((vol) => Math.floor(vol * 100)),
max: Wireplumber.getDefault().getMaxSinkVolume(),
onDragged: (slider) => sink.volume = slider.value / 100
} as Widget.SliderProps),
new Widget.Button({
className: "more",
image: new Widget.Icon({
icon: "go-next-symbolic",
} as Widget.IconProps),
onClick: (_) => slidersPages.toggle(PageSound())
} as Widget.ButtonProps)
])
} as Widget.BoxProps),
new Widget.Box({
className: "source microphone",
children: bind(Wireplumber.getWireplumber(), "defaultMicrophone").as((source) => [
new Widget.Button({
className: "nf",
label: bind(source, "mute").as((muted) => !muted ? "󰍬" : "󰍭"),
onClick: () => Wireplumber.getDefault().toggleMuteSource()
} as Widget.ButtonProps),
new Widget.Slider({
drawValue: false,
hexpand: true,
setup: (slider) => slider.set_value(Math.floor(source.volume * 100)),
value: bind(source, "volume").as((vol) => Math.floor(vol * 100)),
max: Wireplumber.getDefault().getMaxSourceVolume(),
onDragged: (slider) => source.volume = slider.value / 100
} as Widget.SliderProps),
new Widget.Button({
className: "more",
image: new Widget.Icon({
icon: "go-next-symbolic",
} as Widget.IconProps),
onClick: (_) => slidersPages.toggle(PageMicrophone())
} as Widget.ButtonProps)
])
} as Widget.BoxProps),
slidersPages
]
} as Widget.BoxProps);
}
+63
View File
@@ -0,0 +1,63 @@
import { Gtk, Widget } from "astal/gtk3";
import { TileNetwork } from "./tiles/Network";
import { TileBluetooth } from "./tiles/Bluetooth";
import { TileDND } from "./tiles/DoNotDisturb";
import { TileRecording } from "./tiles/Recording";
import { TileNightLight } from "./tiles/NightLight";
import { Pages } from "./Pages";
import { GObject } from "astal";
export const tileList: Array<() => Gtk.Widget> = [
TileNetwork,
TileBluetooth,
TileRecording,
TileDND,
TileNightLight
];
export let TilesPages: (Pages|null) = null;
export function Tiles(): Gtk.Widget {
const tilesFlowBox: Gtk.FlowBox = new Gtk.FlowBox({
visible: true,
orientation: Gtk.Orientation.HORIZONTAL,
rowSpacing: 6,
columnSpacing: 6,
minChildrenPerLine: 2,
maxChildrenPerLine: 2,
expand: true,
homogeneous: true,
} as Gtk.FlowBox.ConstructorProps);
tileList.map((item: (() => Gtk.Widget)) => {
const tile = item();
tilesFlowBox.insert(tile, -1);
const children = tilesFlowBox.get_children();
children[children.length-1]!.set_can_focus(false);
const binding: GObject.Binding = tile.bind_property("visible",
children[children.length-1], "visible",
GObject.BindingFlags.SYNC_CREATE);
const destroyId: number = tile.connect("destroy-event", (self: typeof tile) => {
binding.unbind();
self.disconnect(destroyId);
});
});
return new Widget.Box({
className: "tiles-container",
orientation: Gtk.Orientation.VERTICAL,
onDestroy: () => TilesPages = null,
setup: (box) => {
if(!TilesPages) TilesPages = new Pages({
className: "tile-pages"
});
box.set_children([
tilesFlowBox,
TilesPages!
]);
}
} as Widget.BoxProps);
}
@@ -0,0 +1,210 @@
import { bind, Variable } from "astal";
import { astalify, Gtk, Widget } from "astal/gtk3";
import AstalBluetooth from "gi://AstalBluetooth";
import { Page, PageButton } from "./Page";
import { Separator, SeparatorProps } from "../../Separator";
import { tr } from "../../../i18n/intl";
import AstalHyprland from "gi://AstalHyprland?version=0.1";
import { Windows } from "../../../windows";
const AstalSpinner = astalify(Gtk.Spinner);
export const BluetoothPage: (() => Page) = () => new Page({
id: "bluetooth",
title: tr("control_center.pages.bluetooth.title"),
description: tr("control_center.pages.bluetooth.description"),
className: "bluetooth",
headerButtons: [
new Widget.Button({
className: "discover nf",
label: bind(AstalBluetooth.get_default().adapter, "discovering").as((discovering) =>
!discovering ? '󰑓' : '󰙦'),
tooltipText: bind(AstalBluetooth.get_default().adapter, "discovering").as((discovering) =>
!discovering ?
tr("control_center.pages.bluetooth.start_discovering")
: tr("control_center.pages.bluetooth.stop_discovering")),
onClick: () => {
if(AstalBluetooth.get_default().adapter.discovering) {
stopBluetoothDevicesWatch();
return;
}
watchNewDevices();
}
} as Widget.ButtonProps)
],
onClose: () => stopBluetoothDevicesWatch(),
spacing: 2,
children: [
new Widget.Box({
className: "adapters",
visible: bind(AstalBluetooth.get_default(), "adapters").as((adapters) =>
adapters.length > 1),
spacing: 2,
children: bind(AstalBluetooth.get_default(), "adapters").as((adapters) => [
new Widget.Label({
className: "sub-header",
label: tr("control_center.pages.bluetooth.adapters")
} as Widget.LabelProps),
...adapters.map(adapter =>
PageButton({
title: adapter.alias ?? "Adapter",
icon: "bluetooth-active-symbolic",
onClick: () => AstalBluetooth.get_default(),
})
)
]
)
} as Widget.BoxProps),
new Widget.Box({
className: "connections",
orientation: Gtk.Orientation.VERTICAL,
hexpand: true,
spacing: 2,
children: [
new Widget.Box({
className: "paired",
orientation: Gtk.Orientation.VERTICAL,
visible: bind(AstalBluetooth.get_default(), "devices").as((devs) =>
devs.filter(dev => dev.paired || dev.connected).length > 0),
children: bind(AstalBluetooth.get_default(), "devices").as((devs: Array<AstalBluetooth.Device>) => {
const connectedDevices = devs.filter((dev: AstalBluetooth.Device) => dev.connected || dev.paired)
return [
new Widget.Label({
className: "sub-header",
label: tr("control_center.pages.bluetooth.paired_devices"),
xalign: 0,
} as Widget.LabelProps),
...connectedDevices.map((dev: AstalBluetooth.Device) => DeviceWidget(dev))
]
})
} as Widget.BoxProps),
new Widget.Box({
className: "discovered",
orientation: Gtk.Orientation.VERTICAL,
visible: bind(AstalBluetooth.get_default(), "devices").as((devs) =>
devs.filter((dev) => !dev.connected && !dev.paired).length > 0),
children: bind(AstalBluetooth.get_default(), "devices").as((devices: Array<AstalBluetooth.Device>) => {
const discoveredDevices = devices.filter((dev: AstalBluetooth.Device) => !dev.connected && !dev.paired);
return [
new Widget.Label({
className: "sub-header",
label: tr("control_center.pages.bluetooth.new_devices"),
xalign: 0
} as Widget.LabelProps),
...discoveredDevices.map((dev: AstalBluetooth.Device) => DeviceWidget(dev))
]
})
} as Widget.BoxProps),
Separator({
size: .2,
orientation: Gtk.Orientation.VERTICAL,
cssColor: "gray",
alpha: .2
} as SeparatorProps),
new Widget.Button({
className: "more",
label: tr("control_center.pages.more_settings"),
onClick: () => {
Windows.close("control-center");
AstalHyprland.get_default().dispatch("exec", "[float; animation slide right] overskride");
},
setup: (self) => self.set_alignment(0, 0.5)
} as Widget.ButtonProps)
]
} as Widget.BoxProps)
]
});
function DeviceWidget(dev: AstalBluetooth.Device): Gtk.Widget {
return PageButton({
className: bind(dev, "connected").as((connected) => connected ? "connected" : ""),
title: bind(dev, "alias").as(alias => alias ?? "Unknown Device"),
icon: dev.icon ?? "bluetooth-active-symbolic",
onClick: () => {
if(dev.paired) {
dev.connected ?
dev.disconnect_device(null)
: dev.connect_device(null);
return;
}
dev.pair();
dev.connected ?
dev.disconnect_device(null)
: dev.connect_device(null);
},
endWidget: new Widget.Box({
visible: bind(dev, "batteryPercentage").as((bat: number) =>
bat <= -1 ? false : true),
children: [
new Widget.Box({
visible: bind(dev, "connected"),
children: [
new Widget.Label({
halign: Gtk.Align.END,
label: bind(dev, "batteryPercentage").as((bat: number) =>
`${Math.floor(bat * 100)}%`)
} as Widget.LabelProps),
new Widget.Icon({
icon: "battery-symbolic",
css: "font-size: 18px; margin-left: 6px;"
} as Widget.IconProps)
]
} as Widget.BoxProps),
new Widget.Box({
visible: bind(dev, "connecting"),
setup: (self) => {
const spinner = new AstalSpinner();
self.add(spinner);
}
} as Widget.BoxProps)
// Spinner here
]
} as Widget.BoxProps),
extraButtons: Variable.derive([
bind(dev, "connected"),
bind(dev, "paired"),
bind(dev, "trusted")
], (connected, paired, trusted) => [
new Widget.Button({
className: "nf",
visible: paired && connected,
label: connected ? "󰅖" : "",
tooltipText: tr("disconnect"),
onClick: () => dev.disconnect_device()
} as Widget.ButtonProps),
new Widget.Button({
visible: !connected && paired,
className: "nf",
label: "󰢃",
tooltipText: tr("control_center.pages.bluetooth.unpair_device"),
onClick: () => AstalBluetooth.get_default().adapter?.remove_device(dev)
} as Widget.ButtonProps),
new Widget.Button({
className: "nf",
visible: paired,
label: trusted ? "󰫜" : "󰫚",
tooltipText: trusted ?
tr("control_center.pages.bluetooth.untrust_device")
: tr("control_center.pages.bluetooth.trust_device"),
onClick: () => trusted ? dev.set_trusted(false) : dev.set_trusted(true)
} as Widget.ButtonProps)
])()
});
}
function watchNewDevices(): void {
!AstalBluetooth.get_default().adapter.discovering &&
AstalBluetooth.get_default().adapter.start_discovery();
}
export function stopBluetoothDevicesWatch(): void {
AstalBluetooth.get_default().adapter.discovering &&
AstalBluetooth.get_default().adapter.stop_discovery();
}
@@ -0,0 +1,35 @@
import { bind } from "astal";
import { Page, PageButton, PageProps } from "./Page";
import AstalWp from "gi://AstalWp?version=0.1";
import { Wireplumber } from "../../../scripts/volume";
import { Astal, Widget } from "astal/gtk3";
import { tr } from "../../../i18n/intl";
export function PageMicrophone(): Page {
return new Page({
id: "microphone",
title: tr("control_center.pages.microphone.title"),
description: tr("control_center.pages.microphone.description"),
children: bind(Wireplumber.getWireplumber(), "endpoints").as((endpoints) => [
new Widget.Label({
className: "sub-header",
label: tr("devices"),
setup: (self) => self.set_alignment(0, .5)
} as Widget.LabelProps),
...endpoints.filter(ep => ep.mediaClass === AstalWp.MediaClass.AUDIO_MICROPHONE).map((ep) =>
PageButton({
className: bind(ep, "isDefault").as(isDefault => isDefault ? "default" : ""),
icon: Astal.Icon.lookup_icon(ep.icon) ? ep.icon : "audio-input-microphone-symbolic",
title: ep.name ?? "Microphone",
onClick: () => ep.set_is_default(true),
endWidget: new Widget.Icon({
icon: "object-select-symbolic",
visible: bind(ep, "isDefault"),
css: "font-size: 18px;"
} as Widget.IconProps)
})
)
])
} as PageProps);
}
+112
View File
@@ -0,0 +1,112 @@
import { Gtk, Widget } from "astal/gtk3";
import { Page, PageButton } from "./Page";
import AstalNetwork from "gi://AstalNetwork";
import { bind } from "astal";
import NM from "gi://NM";
import { Separator, SeparatorProps } from "../../Separator";
import { Windows } from "../../../windows";
import AstalHyprland from "gi://AstalHyprland?version=0.1";
import { tr } from "../../../i18n/intl";
export const PageNetwork: (() => Page) = () => new Page({
id: "network",
title: tr("control_center.pages.network.title"),
className: "network",
headerButtons: [
new Widget.Button({
className: "reload nf",
label: "󰑓",
visible: bind(AstalNetwork.get_default(), "primary").as(
(primary: AstalNetwork.Primary) => primary === AstalNetwork.Primary.WIFI
),
tooltipText: "Re-scan connections",
onClick: () => AstalNetwork.get_default().wifi.scan()
} as Widget.ButtonProps)
],
children: [
new Widget.Box({
className: "devices",
hexpand: true,
orientation: Gtk.Orientation.VERTICAL,
visible: bind(AstalNetwork.get_default().get_client(), "devices").as((devs) => devs.length > 0),
children: bind(AstalNetwork.get_default().get_client(), "devices").as((devices) => {
devices = devices.filter(dev => dev.interface !== "lo");
return [
new Widget.Label({
label: tr("devices"),
xalign: 0,
className: "sub-header",
} as Widget.LabelProps),
...devices.filter(device => device.real).map(dev => PageButton({
className: "device",
icon: bind(dev, "deviceType").as(deviceType =>
deviceType === NM.DeviceType.WIFI ?
"network-wireless-symbolic"
: "network-wired-symbolic"),
title: bind(dev, "interface").as(iface => iface ??
tr("control_center.pages.network.interface")),
extraButtons: [
new Widget.Button({
image: new Widget.Icon({
icon: "view-more-symbolic"
} as Widget.IconProps),
onClick: () => {
Windows.close("control-center");
AstalHyprland.get_default().dispatch("exec",
`[animationstyle gnomed; float] nm-connection-editor --edit ${
dev.activeConnection?.connection.get_uuid()
}`);
}
} as Widget.ButtonProps)
]
})
)
]
})
} as Widget.BoxProps),
new Widget.Box({
className: "wireless-aps",
visible: bind(AstalNetwork.get_default(), "primary").as((primary) => primary === AstalNetwork.Primary.WIFI),
hexpand: true,
orientation: Gtk.Orientation.VERTICAL,
children: AstalNetwork.get_default().wifi ? bind(AstalNetwork.get_default().wifi.get_device(), "accessPoints").as((aps) =>
aps.map(ap => new Widget.Button({
hexpand: true,
onClick: () => console.log("connect to " + ap.get_ssid().toArray().toString()), // TODO I don't have a WiFi board :(
child: new Widget.Box({
hexpand: true,
children: [
new Widget.Icon({
halign: Gtk.Align.START,
className: "icon",
icon: "network-wireless-signal-excellent-symbolic"
} as Widget.IconProps),
new Widget.Label({
className: "ssid",
halign: Gtk.Align.START,
label: ap.ssid.get_data()?.toString() ?? "Wi-Fi"
} as Widget.LabelProps),
new Widget.Label({
className: "status",
} as Widget.LabelProps)
]
} as Widget.BoxProps)
} as Widget.ButtonProps))) : [],
} as Widget.BoxProps),
Separator({
orientation: Gtk.Orientation.VERTICAL,
alpha: .2,
size: .2
} as SeparatorProps),
new Widget.Button({
label: tr("control_center.pages.more_settings"),
setup: (self) => self.set_alignment(0, 0.5),
onClick: () => {
Windows.close("control-center");
AstalHyprland.get_default().dispatch("exec", "[animationstyle gnomed] nm-connection-editor");
}
} as Widget.ButtonProps)
]
});
@@ -0,0 +1,51 @@
import { Widget } from "astal/gtk3";
import { Page, PageProps } from "./Page";
import { bind } from "astal";
import { NightLight } from "../../../scripts/nightlight";
import { addSliderMarksFromMinMax } from "../../../scripts/widget-utils";
import { tr } from "../../../i18n/intl";
export const PageNightLight: (() => Page) = () => new Page({
id: "night-light",
title: tr("control_center.pages.night_light.title"),
description: tr("control_center.pages.night_light.description"),
className: "night-light",
children: [
new Widget.Label({
className: "sub-header",
label: tr("control_center.pages.night_light.temperature"),
xalign: 0
} as Widget.LabelProps),
new Widget.Slider({
className: "temperature",
setup: (slider) => {
slider.value = NightLight.getDefault().temperature;
addSliderMarksFromMinMax(slider, 5, "{}K");
},
value: bind(NightLight.getDefault(), "temperature"),
tooltipText: bind(NightLight.getDefault(), "temperature").as((temp) => `${temp}K`),
min: 1000,
max: bind(NightLight.getDefault(), "maxTemperature"),
onDragged: (slider) =>
NightLight.getDefault().temperature = (Math.floor(slider.value)),
} as Widget.SliderProps),
new Widget.Label({
className: "sub-header",
label: tr("control_center.pages.night_light.gamma"),
css: "margin-top: 6px;",
xalign: 0
} as Widget.LabelProps),
new Widget.Slider({
className: "gamma",
setup: (slider) => {
slider.value = NightLight.getDefault().gamma;
addSliderMarksFromMinMax(slider, 5, "{}%");
},
value: bind(NightLight.getDefault(), "gamma"),
max: bind(NightLight.getDefault(), "maxGamma"),
tooltipText: bind(NightLight.getDefault(), "gamma").as((gamma) => `${gamma}%`),
onDragged: (slider) =>
NightLight.getDefault().gamma = (Math.floor(slider.value)),
} as Widget.SliderProps)
]
} as PageProps);
+146
View File
@@ -0,0 +1,146 @@
import { Binding, register } from "astal";
import { Gtk, Widget } from "astal/gtk3";
export type PageProps = {
setup?: () => void;
onClose?: () => void;
onOpen?: () => void;
id: string;
className?: string | Binding<string>;
title: string | Binding<string>;
description?: string | Binding<string>;
headerButtons?: Array<Gtk.Button> | Binding<Array<Gtk.Button>>;
orientation?: Gtk.Orientation | Binding<Gtk.Orientation>;
spacing?: number;
child?: Gtk.Widget | Binding<Gtk.Widget>;
children?: Array<Gtk.Widget> | Binding<Array<Gtk.Widget>>;
};
export { Page };
@register({ GTypeName: "Page" })
class Page extends Widget.Box {
readonly #id: string;
#title: string | Binding<string>;
#description: string | undefined | Binding<string>;
public get title() { return this.#title; }
public get description() { return this.#description; }
public get id() { return this.#id; }
constructor(props: PageProps) {
super({
hexpand: true,
orientation: Gtk.Orientation.VERTICAL,
className: (props.className instanceof Binding) ?
props.className.as((clsName) => `page ${ clsName ?? "" }`)
: `page ${props.className ?? ""}`,
children: [
new Widget.Box({
className: "header",
orientation: Gtk.Orientation.VERTICAL,
hexpand: true,
children: [
new Widget.Box({
className: "top",
children: [
new Widget.Label({
hexpand: true,
className: "title",
truncate: true,
visible: (props.title instanceof Binding) ?
props.title.as(Boolean)
: (props.title ? true : false),
label: props.title,
halign: Gtk.Align.START
} as Widget.LabelProps),
new Widget.Box({
className: "button-row",
visible: (props.headerButtons instanceof Binding) ?
props.headerButtons.as(Boolean)
: (props.headerButtons ? true : false),
children: props.headerButtons
} as Widget.BoxProps)
]
} as Widget.BoxProps),
new Widget.Label({
className: "description",
hexpand: true,
truncate: true,
xalign: 0,
visible: (props.description instanceof Binding) ?
props.description.as(Boolean)
: props.description ? true : false,
label: props.description
} as Widget.LabelProps),
]
} as Widget.BoxProps),
new Widget.Box({
className: "content",
spacing: props.spacing ?? 4,
orientation: props.orientation ?? Gtk.Orientation.VERTICAL,
expand: true,
setup: props.setup,
child: props.child,
children: props.children
} as Widget.BoxProps)
]
});
this.#id = props.id;
this.#title = props.title;
this.#description = props.description;
}
}
export function PageButton(props: {
className?: string | Binding<string>;
icon?: string | Binding<string>;
title: string | Binding<string>;
endWidget?: Gtk.Widget | Binding<Gtk.Widget>;
extraButtons?: Array<Widget.Button> | Binding<Array<Gtk.Widget>>;
onClick?: (self: Widget.Button) => void;
}): Gtk.Widget {
return new Widget.Box({
children: [
new Widget.Button({
onClick: props.onClick,
className: props.className,
hexpand: true,
child: new Widget.Box({
className: "page-button",
orientation: Gtk.Orientation.HORIZONTAL,
expand: true,
children: [
new Widget.Icon({
className: "icon",
icon: props.icon,
visible: props.icon,
css: "font-size: 20px; margin-right: 6px;"
} as Widget.IconProps),
new Widget.Label({
className: "title",
halign: Gtk.Align.START,
hexpand: true,
truncate: true,
label: props.title
} as Widget.LabelProps),
new Widget.Box({
visible: (props.endWidget instanceof Binding) ?
props.endWidget.as(Boolean)
: props.endWidget,
child: props.endWidget
} as Widget.BoxProps)
]
} as Widget.BoxProps)
} as Widget.ButtonProps),
new Widget.Box({
className: "button-row extra-buttons",
visible: (props.extraButtons instanceof Binding) ?
props.extraButtons.as(extra => extra.length > 0)
: (props.extraButtons ? props.extraButtons.length > 0 : false),
children: props.extraButtons
} as Widget.BoxProps)
]
} as Widget.BoxProps);
}
+90
View File
@@ -0,0 +1,90 @@
import { Page, PageButton, PageProps } from "./Page";
import { bind } from "astal";
import { Astal, Gtk, Widget } from "astal/gtk3";
import AstalWp from "gi://AstalWp";
import { getAppIcon } from "../../../scripts/apps";
import { Wireplumber } from "../../../scripts/volume";
import { tr } from "../../../i18n/intl";
export function PageSound(): Page {
return new Page({
id: "sound",
title: tr("control_center.pages.sound.title"),
description: tr("control_center.pages.sound.description"),
children: bind(Wireplumber.getWireplumber(), "endpoints").as((endpoints) => [
new Widget.Label({
className: "sub-header",
label: tr("devices"),
setup: (self) => self.set_alignment(0, .5)
} as Widget.LabelProps),
...endpoints.filter(ep => ep.mediaClass === AstalWp.MediaClass.AUDIO_SPEAKER).map((ep) =>
PageButton({
className: bind(ep, "isDefault").as(isDefault => isDefault ? "default" : ""),
icon: Astal.Icon.lookup_icon(ep.icon) ? ep.icon : "audio-card-symbolic",
title: ep.name ?? "Speaker",
onClick: () => ep.set_is_default(true),
endWidget: new Widget.Icon({
icon: "object-select-symbolic",
visible: bind(ep, "isDefault"),
css: "font-size: 18px;"
} as Widget.IconProps)
})
),
new Widget.Label({
className: "sub-header",
label: tr("apps"),
visible: endpoints.filter((ep) => ep.mediaClass === AstalWp.MediaClass.AUDIO_STREAM ||
ep.mediaClass === AstalWp.MediaClass.VIDEO_STREAM).length > 0,
setup: (self) => self.set_alignment(0, .5)
} as Widget.LabelProps),
...endpoints.filter((ep) => ep.mediaClass === AstalWp.MediaClass.AUDIO_STREAM ||
ep.mediaClass === AstalWp.MediaClass.VIDEO_STREAM).map((ep) =>
new Widget.EventBox({
hexpand: true,
setup: (eventbox) => {
const connections: Array<number> = [];
eventbox.add(new Widget.Box({
orientation: Gtk.Orientation.HORIZONTAL,
children: [
new Widget.Icon({
icon: getAppIcon(ep.name.split(' ')[0]) || "application-x-executable-symbolic",
css: "font-size: 18px; margin-right: 6px;"
} as Widget.IconProps),
new Widget.Box({
orientation: Gtk.Orientation.VERTICAL,
hexpand: true,
children: [
new Widget.Revealer({
transitionDuration: 180,
transitionType: Gtk.RevealerTransitionType.SLIDE_DOWN,
setup: (self) => connections.push(
eventbox.connect("hover", () => self.revealChild = true),
eventbox.connect("hover-lost", () => self.revealChild = false)
),
onDestroy: () => connections.map(id => eventbox.disconnect(id)),
child: new Widget.Label({
label: ep.name || "Unknown",
truncate: true,
tooltipText: ep.name,
className: "name",
xalign: 0
} as Widget.LabelProps)
} as Widget.RevealerProps),
new Widget.Slider({
min: 0,
drawValue: false,
max: 100,
setup: (self) => self.value = Math.floor(ep.volume * 100),
value: bind(ep, "volume").as((vol) => Math.floor(vol * 100)),
onDragged: (self) => ep.volume = self.value / 100
} as Widget.SliderProps)
]
} as Widget.BoxProps)
]
} as Widget.BoxProps))
}
} as Widget.EventBoxProps)
)
])
} as PageProps);
}
@@ -0,0 +1,28 @@
import { bind, Variable } from "astal";
import { Tile, TileProps } from "./Tile";
import AstalBluetooth from "gi://AstalBluetooth";
import { BluetoothPage } from "../pages/Bluetooth";
import { TilesPages } from "../Tiles";
export const TileBluetooth = Tile({
title: "Bluetooth",
visible: bind(AstalBluetooth.get_default(), "adapter").as(Boolean),
description: bind(AstalBluetooth.get_default(), "isConnected").as((connected) => {
const connectedDev = AstalBluetooth.get_default().devices.filter(dev => dev.connected)?.[0];
return connected && connectedDev ? connectedDev.get_alias() : ""
}),
onToggledOn: () => AstalBluetooth.get_default().adapter?.set_powered(true),
onToggledOff: () => AstalBluetooth.get_default().adapter?.set_powered(false),
onClickMore: () => TilesPages?.toggle(BluetoothPage()),
enableOnClickMore: true,
icon: Variable.derive([
bind(AstalBluetooth.get_default(), "isPowered"),
bind(AstalBluetooth.get_default(), "isConnected")
],
(powered: boolean, isConnected: boolean) =>
powered ? ( isConnected ? "󰂱" : "󰂯" ) : "󰂲"
)(),
iconSize: 16,
toggleState: bind(AstalBluetooth.get_default(), "isPowered")
} as TileProps);
@@ -0,0 +1,15 @@
import { bind } from "astal";
import { Notifications } from "../../../scripts/notifications";
import { Tile } from "./Tile";
import { tr } from "../../../i18n/intl";
export const TileDND = Tile({
title: tr("control_center.tiles.dnd.title"),
description: bind(Notifications.getDefault().getNotifd(), "dontDisturb").as(
(dnd: boolean) => dnd ? tr("control_center.tiles.enabled") : tr("control_center.tiles.disabled")),
onToggledOff: () => Notifications.getDefault().getNotifd().dontDisturb = false,
onToggledOn: () => Notifications.getDefault().getNotifd().dontDisturb = true,
icon: "󰍶",
iconSize: 16,
toggleState: Notifications.getDefault().getNotifd().dontDisturb
});
@@ -0,0 +1,87 @@
import { bind, execAsync, Variable } from "astal";
import { Tile, TileProps } from "./Tile";
import AstalNetwork from "gi://AstalNetwork";
import { Widget } from "astal/gtk3";
import { PageNetwork } from "../pages/Network";
import { tr } from "../../../i18n/intl";
import { TilesPages } from "../Tiles";
export const TileNetwork = () => new Widget.Box({
child: Variable.derive([
bind(AstalNetwork.get_default(), "primary"),
bind(AstalNetwork.get_default(), "wired"),
bind(AstalNetwork.get_default(), "wifi")
],
(primary: AstalNetwork.Primary, wired: AstalNetwork.Wired, wifi: AstalNetwork.Wifi) => {
if(primary === AstalNetwork.Primary.WIFI) {
return Tile({
title: tr("control_center.tiles.network.wireless"),
description: Variable.derive(
[ bind(wifi, "ssid"), bind(wifi, "internet") ],
(ssid: string, internet: AstalNetwork.Internet) =>
ssid ? ssid : (() => {
switch(internet) {
case AstalNetwork.Internet.CONNECTED:
return tr("connected");
case AstalNetwork.Internet.DISCONNECTED:
return tr("disconnected");
case AstalNetwork.Internet.CONNECTING:
return tr("connecting") + "...";
}
})()
)(),
onToggledOn: () => wifi.set_enabled(true),
onToggledOff: () => wifi.set_enabled(false),
onClickMore: () => TilesPages?.toggle(PageNetwork()),
icon: "󰤨",
iconSize: 16,
toggleState: bind(wifi, "enabled")
} as TileProps)();
} else if(primary === AstalNetwork.Primary.WIRED) {
return Tile({
title: tr("control_center.tiles.network.wired") || "Wired",
description: bind(wired, "internet").as((internet: AstalNetwork.Internet) => {
switch(internet) {
case AstalNetwork.Internet.CONNECTED:
return tr("connected");
case AstalNetwork.Internet.DISCONNECTED:
return tr("disconnected");
case AstalNetwork.Internet.CONNECTING:
return tr("connecting") + "...";
}
}),
onToggledOn: () => execAsync("nmcli n on"),
onToggledOff: () => execAsync("nmcli n off"),
onClickMore: () => TilesPages?.toggle(PageNetwork()),
icon: bind(wired, "internet").as((internet: AstalNetwork.Internet) => {
switch(internet) {
case AstalNetwork.Internet.CONNECTED:
return '󰛳';
case AstalNetwork.Internet.DISCONNECTED:
return '󰲛';
}
return "󰛵";
}),
iconSize: 16,
toggleState: bind(wired, "internet").as((internet: AstalNetwork.Internet) =>
internet === AstalNetwork.Internet.CONNECTING
|| internet === AstalNetwork.Internet.CONNECTED
)
} as TileProps)();
}
return Tile({
title: tr("control_center.tiles.network.network"),
description: tr("disconnected"),
onToggledOn: () => execAsync("nmcli n on"),
onToggledOff: () => execAsync("nmcli n off"),
onClickMore: () => TilesPages?.toggle(PageNetwork()),
icon: "󰲛",
iconSize: 16,
toggleState: bind(wired, "internet").as((internet: AstalNetwork.Internet) =>
internet === AstalNetwork.Internet.CONNECTING || internet === AstalNetwork.Internet.CONNECTED)
} as TileProps)();
})()
} as Widget.BoxProps);
@@ -0,0 +1,25 @@
import { bind, Variable } from "astal";
import { Tile, TileProps } from "./Tile";
import { NightLight } from "../../../scripts/nightlight";
import { PageNightLight } from "../pages/NightLight";
import { tr } from "../../../i18n/intl";
import { TilesPages } from "../Tiles";
export const TileNightLight = Tile({
title: tr("control_center.tiles.night_light.title"),
icon: "󰖔",
description: Variable.derive([
bind(NightLight.getDefault(), "temperature"),
bind(NightLight.getDefault(), "gamma")
], (temp, gamma) =>
(temp === 6000 ? tr("control_center.tiles.night_light.default_desc")
: `${temp}K`) + (gamma < NightLight.getDefault().maxGamma ?
` (${gamma}%)` : "")
)(),
iconSize: 16,
onToggledOff: () => NightLight.getDefault().identity = true,
onToggledOn: () => NightLight.getDefault().identity = false,
enableOnClickMore: true,
onClickMore: () => TilesPages?.toggle(PageNightLight()),
toggleState: bind(NightLight.getDefault(), "identity").as(identity => !identity)
} as TileProps);
@@ -0,0 +1,32 @@
import { Tile, TileProps } from "./Tile";
import { Recording } from "../../../scripts/recording";
import { bind, Variable } from "astal";
import { tr } from "../../../i18n/intl";
import { getDateTime } from "../../../scripts/time";
export const TileRecording = Tile({
title: tr("control_center.tiles.recording.title") || "Screen Recording",
description: Variable.derive([
bind(Recording.getDefault(), "recording"),
getDateTime()
], (recording, dateTime) => {
if(!recording || !Recording.getDefault().startedAt)
return tr("control_center.tiles.recording.disabled_desc") || "Start recording";
const startedAtSeconds = dateTime.to_unix() - Recording.getDefault().startedAt!.to_unix();
if(startedAtSeconds <= 0) return "00:00";
const hours = Math.floor(startedAtSeconds / 120);
const minutes = Math.floor(startedAtSeconds / 60);
const seconds = Math.floor(startedAtSeconds % 60);
return `${ hours > 0 ? `${hours < 10 ? `0${hours}` : hours }:` : ""
}${ minutes < 10 ? `0${minutes}` : minutes
}:${ seconds < 10 ? `0${seconds}` : seconds }`;
})(),
icon: "󰻂",
onToggledOff: () => Recording.getDefault().stopRecording(),
onToggledOn: () => Recording.getDefault().startRecording(),
toggleState: bind(Recording.getDefault(), "recording"),
iconSize: 16
} as TileProps);
+124
View File
@@ -0,0 +1,124 @@
import { Binding, Variable } from "astal";
import { Gtk, Widget } from "astal/gtk3";
import { tr } from "../../../i18n/intl";
export type TileProps = {
className?: string | Binding<string | undefined>;
icon?: string | Binding<string | undefined>;
visible?: boolean | Binding<boolean | undefined>;
iconSize?: number | Binding<number | undefined>;
title: string | Binding<string | undefined>;
description?: string | Binding<string | undefined>;
toggleState?: boolean | Binding<boolean | undefined>;
enableOnClickMore?: boolean | Binding<boolean | undefined>;
onDestroy?: () => void;
onToggledOn: () => void;
onToggledOff: () => void;
onClickMore?: () => void;
}
export function Tile(props: TileProps): (() => Gtk.Widget) {
const toggled = new Variable<boolean>(props.toggleState instanceof Binding ?
(props.toggleState.get() || false) : (props.toggleState || false));
let subscription: () => void;
if(props?.toggleState instanceof Binding)
subscription = props.toggleState.subscribe(val => toggled.set(val || false));
return () => new Widget.Box({
className: (props.className instanceof Binding) ?
Variable.derive([
props.className,
toggled()
], (className, isToggled) =>
`tile ${className} ${isToggled ? "toggled" : ""} ${
props.onClickMore ? "has-more" : ""
}`
)()
: toggled().as((state: boolean) =>
`tile ${state ? "toggled" : ""} ${
props.onClickMore ? "has-more" : ""
}`
),
expand: true,
visible: props.visible,
onDestroy: () => {
props.onDestroy?.();
subscription?.();
},
children: [
new Widget.Button({
className: "toggle-button",
onClick: () => {
if(toggled.get()) {
toggled.set(false);
props.onToggledOff && props.onToggledOff();
return;
}
toggled.set(true);
props.onToggledOn && props.onToggledOn();
},
child: new Widget.Box({
className: "content",
expand: true,
hexpand: true,
children: [
new Widget.Label({
className: "icon nf",
label: props.icon || "icon",
css: `label { font-size: ${props.iconSize || 12}px; }`
} as Widget.LabelProps),
new Widget.Box({
className: "text",
orientation: Gtk.Orientation.VERTICAL,
vexpand: true,
hexpand: true,
valign: Gtk.Align.CENTER,
children: [
new Widget.Label({
className: "title",
xalign: 0,
halign: Gtk.Align.START,
truncate: true,
label: props.title
} as Widget.LabelProps),
new Widget.Label({
className: "description",
visible: (props.description instanceof Binding) ?
props.description.as(Boolean)
: Boolean(props.description),
halign: Gtk.Align.START,
truncate: true,
xalign: 0,
label: (props.description instanceof Binding) ?
props.description.as((desc) => desc ? desc : "")
: (props.description || "")
} as Widget.LabelProps)
]
} as Widget.BoxProps)
]
} as Widget.BoxProps)
} as Widget.ButtonProps),
new Widget.Button({
className: "more icon",
visible: props.onClickMore !== undefined,
halign: Gtk.Align.END,
tooltipText: tr("control_center.tiles.more") || "More",
image: new Widget.Icon({
icon: "go-next-symbolic",
css: "icon { font-size: 16px; }"
}),
onClick: () => {
((props.enableOnClickMore instanceof Binding) ?
props.enableOnClickMore.get()
: props.enableOnClickMore) && props?.onToggledOn();
props.onClickMore && props?.onClickMore()
},
widthRequest: 32
})
]
});
}
+78
View File
@@ -0,0 +1,78 @@
import { Binding, register } from "astal";
import { Gtk, Widget } from "astal/gtk3";
export { ResultWidget, ResultWidgetProps };
type ResultWidgetProps = {
icon?: string | Binding<string> | Gtk.Widget | Binding<Gtk.Widget>;
title: string | Binding<string | undefined>;
description?: string | Binding<string | undefined>;
closeOnClick?: boolean;
setup?: () => void;
onClick?: () => void;
};
@register({ GTypeName: "ResultWidget" })
class ResultWidget extends Widget.Box {
public readonly onClick: (() => void);
public readonly setup: ((() => void)|undefined);
public icon: (string | Binding<string> | Gtk.Widget | Binding<Gtk.Widget> | undefined);
public closeOnClick: boolean = true;
constructor(props: ResultWidgetProps) {
super({
className: "result",
hexpand: true
});
this.icon = props.icon;
this.setup = props.setup;
this.closeOnClick = props.closeOnClick ?? true;
this.onClick = () => props.onClick?.();
if(this.icon !== undefined) {
if(this.icon instanceof Binding) {
if(typeof this.icon.get() === "string") {
this.add(new Widget.Icon({
icon: this.icon as Binding<string>
} as Widget.IconProps));
} else {
this.add(new Widget.Box({
child: this.icon as Binding<Gtk.Widget>
} as Widget.BoxProps));
}
} else {
if(typeof this.icon === "string") {
this.add(new Widget.Icon({
icon: this.icon as string
} as Widget.IconProps));
} else
this.add(this.icon as Gtk.Widget);
}
}
this.add(new Widget.Box({
orientation: Gtk.Orientation.VERTICAL,
valign: Gtk.Align.CENTER,
children: [
new Widget.Label({
className: "title",
xalign: 0,
truncate: true,
label: props.title
} as Widget.LabelProps),
new Widget.Label({
className: "description",
visible: (props.description instanceof Binding) ?
props.description.as(Boolean)
: Boolean(props.description),
truncate: true,
xalign: 0,
label: props.description || ""
} as Widget.LabelProps)
]
} as Widget.BoxProps));
}
}
+172
View File
@@ -0,0 +1,172 @@
import { GObject, Variable } from "astal";
import { Astal, Gdk, Gtk, Widget } from "astal/gtk3";
import { cleanExec, getAppIcon, getApps, getAstalApps } from "../scripts/apps";
import AstalApps from "gi://AstalApps";
import { PopupWindow } from "../widget/PopupWindow";
const { TOP, LEFT, RIGHT, BOTTOM } = Astal.WindowAnchor;
export const AppsWindow = (mon: number): (Widget.Window) => {
const searchString = new Variable<string>("");
const searchSubscription = searchString.subscribe((str: string) => {
updateResults(str);
});
let results: Array<AstalApps.Application> = [];
const flowboxConnections: Array<number> = [];
const flowbox = new Gtk.FlowBox({
rowSpacing: 6,
columnSpacing: 6,
homogeneous: true,
visible: true,
minChildrenPerLine: 1,
activateOnSingleClick: true
} as Gtk.FlowBox.ConstructorProps);
const entry = new Widget.Entry({
className: "entry",
halign: Gtk.Align.CENTER,
placeholderText: "Start typing...",
primary_icon_name: "system-search",
onChanged: (entry) => searchString.set(entry.text),
onActivate: () => flowbox.get_selected_children()?.[0]?.get_child()?.activate()
} as Widget.EntryProps);
async function updateResults(str?: string) {
if(!str) results = getApps().sort((a, b) =>
a.name > b.name ? 1 : -1);
else results = getAstalApps().fuzzy_query(str);
// Destroy is handled by GnomeJS
flowbox.get_children().map(flowboxChild => flowbox.remove(flowboxChild));
results.map(app => {
flowbox.insert(AppWidget(app), -1);
const flowboxchild = flowbox.get_child_at_index(flowbox.get_children().length-1)!.get_child();
if(!flowboxchild) return;
flowboxchild.set_valign(Gtk.Align.START);
flowboxchild.set_halign(Gtk.Align.START);
});
const firstChild = flowbox.get_child_at_index(0);
firstChild && flowbox.select_child(firstChild);
}
function AppWidget(app: AstalApps.Application) {
const connections: Array<number> = [];
// Astal3.Button doesn't work the way I need, so I'll use normal GtkButton
const button = new Gtk.Button({
visible: true,
widthRequest: 180,
heightRequest: 140,
expand: false,
tooltipMarkup: `${app.name}${app.description ?
`\n<span foreground="#7f7f7f">${app.description}</span>`
: ""}`.replace(/\&/g, "&amp;"),
child: new Widget.Box({
orientation: Gtk.Orientation.VERTICAL,
children: [
new Widget.Icon({
className: "icon",
expand: true,
icon: getAppIcon(app) || "application-x-executable"
} as Widget.IconProps),
new Widget.Label({
className: "name",
truncate: true,
maxWidthChars: 10,
valign: Gtk.Align.START,
label: app.name || "Unnamed App"
} as Widget.LabelProps)
]
} as Widget.BoxProps) as Gtk.Widget,
} as Gtk.Button.ConstructorProps);
button.set_can_focus(false);
const openFun = () => {
cleanExec(app);
window.close();
};
connections.push(
button.connect("activate", openFun),
button.connect("clicked", openFun)
);
button.vfunc_destroy = () => {
connections.map(id =>
GObject.signal_handler_is_connected(button, id) &&
button.disconnect(id)
);
};
return button;
}
const window = PopupWindow({
namespace: "apps-window",
layer: Astal.Layer.OVERLAY,
exclusivity: Astal.Exclusivity.IGNORE,
anchor: TOP | LEFT | RIGHT | BOTTOM,
monitor: mon,
cssBackgroundWindow: "background: rgba(0, 0, 0, .2)",
marginTop: 64,
onDestroy: () => {
searchSubscription?.();
searchString.drop();
flowboxConnections.map(id => flowbox.disconnect(id));
},
onKeyPressEvent: (_, event: Gdk.Event) => {
if(event.get_keyval()[1] === Gdk.KEY_Escape) {
_.close();
return;
}
if(event.get_keyval()[1] !== Gdk.KEY_Right &&
event.get_keyval()[1] !== Gdk.KEY_Down &&
event.get_keyval()[1] !== Gdk.KEY_Up &&
event.get_keyval()[1] !== Gdk.KEY_Left &&
event.get_keyval()[1] !== Gdk.KEY_Return &&
event.get_keyval()[1] !== Gdk.KEY_space &&
event.get_keyval()[1] !== Gdk.KEY_Escape) {
!entry.isFocus && entry.grab_focus_without_selecting();
}
},
child: new Widget.Box({
className: "apps-window-container",
expand: true,
orientation: Gtk.Orientation.VERTICAL,
children: [
entry,
new Widget.Box({
className: "apps-area",
child: new Widget.Scrollable({
vscroll: Gtk.PolicyType.AUTOMATIC,
hscroll: Gtk.PolicyType.NEVER,
overlayScrolling: true,
expand: true,
child: flowbox
} as Widget.ScrollableProps)
} as Widget.BoxProps)
]
} as Widget.BoxProps)
});
const connId = window.connect("focus-in-event", (_) => {
updateResults();
window.disconnect(connId);
});
flowboxConnections.push(
flowbox.connect("child-activated", (_, item) => {
if(!item || !item.get_child()) return;
item.get_child()!.activate();
})
);
return window;
}
+74
View File
@@ -0,0 +1,74 @@
import { Astal, Gtk, Widget } from "astal/gtk3";
import { Tray } from "../widget/bar/Tray";
import { Workspaces } from "../widget/bar/Workspaces";
import { FocusedClient } from "../widget/bar/FocusedClient";
import { Media } from "../widget/bar/Media";
import { Apps } from "../widget/bar/Apps";
import { Clock } from "../widget/bar/Clock";
import { Status } from "../widget/bar/Status";
import { SpecialWorkspaces } from "../widget/bar/SpecialWorkspaces";
import { Separator, SeparatorProps } from "../widget/Separator";
import AstalHyprland from "gi://AstalHyprland?version=0.1";
import { bind } from "astal";
export const Bar = (mon: number) => {
const widgetSpacing = 4;
return new Widget.Window({
namespace: "top-bar",
anchor: Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT,
layer: Astal.Layer.TOP,
exclusivity: Astal.Exclusivity.EXCLUSIVE,
heightRequest: 46,
monitor: mon,
visible: true,
canFocus: false,
child: new Widget.Box({
className: "bar-container",
child: new Widget.CenterBox({
className: "bar-centerbox",
expand: true,
homogeneous: false,
startWidget: new Widget.Box({
className: "widgets-left",
homogeneous: false,
halign: Gtk.Align.START,
spacing: widgetSpacing,
children: [
Apps(),
SpecialWorkspaces(),
Separator({
alpha: .2,
orientation: Gtk.Orientation.HORIZONTAL,
margin: 14,
visible: bind(AstalHyprland.get_default(), "workspaces").as(wss =>
wss.filter(ws => ws.id < 0).length > 0)
} as SeparatorProps),
Workspaces(),
FocusedClient()
]
} as Widget.BoxProps),
centerWidget: new Widget.Box({
className: "widgets-center",
homogeneous: false,
spacing: widgetSpacing,
halign: Gtk.Align.CENTER,
children: [
Clock(),
Media()
]
} as Widget.BoxProps),
endWidget: new Widget.Box({
className: "widgets-right",
homogeneous: false,
spacing: widgetSpacing,
halign: Gtk.Align.END,
children: [
Tray(),
Status()
]
} as Widget.BoxProps)
} as Widget.CenterBoxProps)
} as Widget.BoxProps)
} as Widget.WindowProps);
}
+64
View File
@@ -0,0 +1,64 @@
import { Astal, Gtk, Widget } from "astal/gtk3";
import { bind, GLib } from "astal";
import { getDateTime } from "../scripts/time";
import { Separator, SeparatorProps } from "../widget/Separator";
import { PopupWindow, PopupWindowProps } from "../widget/PopupWindow";
import { BigMedia } from "../widget/center-window/BigMedia";
import AstalMpris from "gi://AstalMpris";
export const CenterWindow = (mon: number) => PopupWindow({
namespace: "center-window",
marginTop: 10,
anchor: Astal.WindowAnchor.TOP,
monitor: mon,
child: new Widget.Box({
className: "center-window-container",
spacing: 6,
children: [
new Widget.Box({
className: "left",
orientation: Gtk.Orientation.VERTICAL,
children: [
new Widget.Box({
className: "datetime",
orientation: Gtk.Orientation.VERTICAL,
valign: Gtk.Align.START,
children: [
new Widget.Label({
className: "time",
label: getDateTime().as((dateTime: GLib.DateTime) =>
dateTime.format("%H:%M"))
} as Widget.LabelProps),
new Widget.Label({
className: "date",
label: getDateTime().as((dateTime: GLib.DateTime) =>
dateTime.format("%A, %B %d"))
} as Widget.LabelProps)
]
} as Widget.BoxProps),
new Widget.Box({
className: "calendar-box",
vexpand: false,
hexpand: true,
valign: Gtk.Align.START,
child: new Gtk.Calendar({
visible: true,
showHeading: true,
showDayNames: true,
showWeekNumbers: false
} as Gtk.Calendar.ConstructorProps)
} as Widget.BoxProps)
]
} as Widget.BoxProps),
Separator({
orientation: Gtk.Orientation.HORIZONTAL,
cssColor: "gray",
margin: 5,
alpha: .3,
visible: bind(AstalMpris.get_default(), "players").as(players => players.length > 0),
} as SeparatorProps),
BigMedia()
]
} as Widget.BoxProps)
} as PopupWindowProps);
+37
View File
@@ -0,0 +1,37 @@
import { Astal, Gtk, Widget } from "astal/gtk3";
import { QuickActions } from "../widget/control-center/QuickActions";
import { Tiles } from "../widget/control-center/Tiles";
import { Sliders } from "../widget/control-center/Sliders";
import { NotifHistory } from "../widget/control-center/NotifHistory";
import { PopupWindow } from "../widget/PopupWindow";
export const ControlCenter = (mon: number) => PopupWindow({
namespace: "control-center",
className: "control-center",
exclusivity: Astal.Exclusivity.NORMAL,
anchor: Astal.WindowAnchor.TOP | Astal.WindowAnchor.RIGHT | Astal.WindowAnchor.BOTTOM,
layer: Astal.Layer.OVERLAY,
focusOnMap: true,
marginTop: 10,
marginRight: 10,
marginBottom: 10,
monitor: mon,
widthRequest: 395,
child: new Widget.Box({
orientation: Gtk.Orientation.VERTICAL,
children: [
new Widget.Box({
className: "control-center-container",
orientation: Gtk.Orientation.VERTICAL,
vexpand: false,
children: [
QuickActions(),
Sliders(),
Tiles()
]
} as Widget.BoxProps),
NotifHistory()
]
} as Widget.BoxProps)
} as Widget.WindowProps);
+26
View File
@@ -0,0 +1,26 @@
import { Astal, Gtk, Widget } from "astal/gtk3";
import { bind } from "astal/binding";
import { Notifications } from "../scripts/notifications";
import { NotificationWidget } from "../widget/Notification";
export const FloatingNotifications = (mon: number) => new Widget.Window({
namespace: "floating-notifications",
canFocus: false,
anchor: Astal.WindowAnchor.TOP | Astal.WindowAnchor.RIGHT,
monitor: mon,
layer: Astal.Layer.OVERLAY,
widthRequest: 450,
exclusivity: Astal.Exclusivity.NORMAL,
child: new Widget.Box({
className: "floating-notifications-container",
orientation: Gtk.Orientation.VERTICAL,
homogeneous: false,
visible: bind(Notifications.getDefault(), "notifications").as(notifs => notifs.length > 0),
children: bind(Notifications.getDefault(), "notifications").as((notifs) =>
notifs.map((item) => NotificationWidget(item,
() => Notifications.getDefault().removeNotification(item),
false, true))
),
} as Widget.BoxProps)
} as Widget.WindowProps);
+104
View File
@@ -0,0 +1,104 @@
import { Astal, Gdk, Gtk, Widget } from "astal/gtk3";
import { getDateTime } from "../scripts/time";
import { exec, execAsync, GLib } from "astal";
import { AskPopup } from "../widget/AskPopup";
import { Windows } from "../windows";
const { TOP, LEFT, RIGHT, BOTTOM } = Astal.WindowAnchor;
export const LogoutMenu = (mon: number) => new Widget.Window({
namespace: "logout-menu",
anchor: TOP | LEFT | RIGHT | BOTTOM,
layer: Astal.Layer.OVERLAY,
exclusivity: Astal.Exclusivity.IGNORE,
keymode: Astal.Keymode.EXCLUSIVE,
monitor: mon,
onKeyPressEvent: (_, event: Gdk.Event) => {
event.get_keyval()[1] === Gdk.KEY_Escape &&
_.close();
},
child: new Widget.EventBox({
className: "logout-menu",
onClick: () => Windows.close("logout-menu"),
child: new Widget.Box({
expand: true,
orientation: Gtk.Orientation.VERTICAL,
children: [
new Widget.Box({
className: "top",
hexpand: true,
vexpand: false,
orientation: Gtk.Orientation.VERTICAL,
valign: Gtk.Align.START,
children: [
new Widget.Label({
className: "time",
label: getDateTime().as((dateTime: GLib.DateTime) =>
dateTime.format("%H:%M"))
} as Widget.LabelProps),
new Widget.Label({
className: "date",
label: getDateTime().as((dateTime: GLib.DateTime) =>
dateTime.format("%A, %B %d %Y"))
} as Widget.LabelProps)
]
} as Widget.BoxProps),
new Widget.Box({
className: "button-row",
homogeneous: true,
vexpand: true,
valign: Gtk.Align.CENTER,
height_request: 360,
children: [
new Widget.Button({
className: "poweroff nf",
label: "󰐥",
onClick: () => AskPopup({
title: "Power Off",
text: "Are you sure you want to power off? Unsaved work will be lost.",
onAccept: () => {
exec(`sh "${GLib.getenv("XDG_CONFIG_HOME")}/hypr/scripts/save-hyprsunset.sh"`);
execAsync("systemctl poweroff");
}
})
} as Widget.ButtonProps),
new Widget.Button({
className: "reboot nf",
label: "󰜉",
onClick: () => AskPopup({
title: "Reboot",
text: "Are you sure you want to Reboot? Unsaved work will be lost.",
onAccept: () => {
exec(`sh "${GLib.getenv("XDG_CONFIG_HOME")}/hypr/scripts/save-hyprsunset.sh"`);
execAsync("systemctl reboot");
}
})
} as Widget.ButtonProps),
new Widget.Button({
className: "suspend nf",
label: "󰤄",
onClick: () => AskPopup({
title: "Suspend",
text: "Are you sure you want to Suspend?",
onAccept: () => execAsync("systemctl suspend")
})
} as Widget.ButtonProps),
new Widget.Button({
className: "logout nf",
label: "󰗽",
onClick: () => AskPopup({
title: "Log out",
text: "Are you sure you want to log out? Your session will be ended.",
onAccept: () => {
exec(`sh "${GLib.getenv("XDG_CONFIG_HOME")}/hypr/scripts/save-hyprsunset.sh"`);
execAsync(`sh -c "loginctl terminate-user ${GLib.getenv("USER") || "$USER"}"`);
}
})
} as Widget.ButtonProps),
]
} as Widget.BoxProps)
]
})
} as Widget.EventBoxProps)
} as Widget.WindowProps);
+92
View File
@@ -0,0 +1,92 @@
import { bind, Binding, Variable } from "astal";
import { Astal, Gtk, Widget } from "astal/gtk3";
import { Wireplumber } from "../scripts/volume";
export enum OSDModes {
SINK,
BRIGHTNESS
}
let osdMode: (Variable<OSDModes>|null);
let osdIcon: (Binding<string | undefined>|null);
export function setOSDMode(newMode: OSDModes): void {
if(!osdMode) return;
osdMode.set(newMode);
}
export const OSD = (mon: number) => {
osdMode = new Variable<OSDModes>(OSDModes.SINK);
osdIcon = osdMode().as((mode: OSDModes) => {
switch(mode) {
case OSDModes.SINK: return "󰕾";
case OSDModes.BRIGHTNESS: return "󰃠";
default: return "󱧣";
}
});
return new Widget.Window({
namespace: "osd",
layer: Astal.Layer.OVERLAY,
anchor: Astal.WindowAnchor.BOTTOM,
canFocus: false,
marginBottom: 80,
focusOnClick: false,
clickThrough: true,
monitor: mon,
onDestroy: () => {
osdMode?.drop();
osdMode = null;
osdIcon = null;
},
child: new Widget.Box({
className: "osd",
children: [
new Widget.Label({
className: "icon",
label: osdIcon
} as Widget.LabelProps),
new Widget.Box({
className: "volume",
orientation: Gtk.Orientation.VERTICAL,
valign: Gtk.Align.CENTER,
children: [
new Widget.Label({
className: "device",
label: bind(Wireplumber.getDefault().getDefaultSink(), "name").as((name: string) =>
name || "Speaker"),
halign: Gtk.Align.CENTER
} as Widget.LabelProps),
new Widget.Box({
vexpand: false,
expand: false,
children: [
new Widget.LevelBar({
className: "levelbar",
width_request: 120,
value: bind(Wireplumber.getDefault().getDefaultSink(), "volume").as((volume: number) =>
Math.floor(volume * 100)),
maxValue: bind(Wireplumber.getWireplumber(), "defaultSpeaker").as(() =>
Wireplumber.getDefault().getMaxSinkVolume()),
vexpand: false,
expand: false,
halign: Gtk.Align.CENTER
} as Widget.LevelBarProps),
/*new Widget.Label({
className: "value",
label: bind(Wireplumber.getDefault().getDefaultSink(), "volume").as((volume: number) =>
`${Math.floor(volume * 100)}%`),
vexpand: false,
expand: false,
halign: Gtk.Align.CENTER
} as Widget.LabelProps)*/
]
} as Widget.BoxProps)
]
} as Widget.BoxProps)
]
} as Widget.BoxProps)
} as Widget.WindowProps);
}
+286
View File
@@ -0,0 +1,286 @@
import { App, Widget } from "astal/gtk3";
import { Bar } from "./window/Bar";
import { OSD } from "./window/OSD";
import { ControlCenter } from "./window/ControlCenter";
import { CenterWindow } from "./window/CenterWindow";
import { LogoutMenu } from "./window/LogoutMenu";
import { FloatingNotifications } from "./window/FloatingNotifications";
import { AppsWindow } from "./window/AppsWindow";
import AstalHyprland from "gi://AstalHyprland";
import { GObject, property, register, signal } from "astal";
/**
* Windowing System
* Possible actions: getting window states(visible or not), close, open or toggle windows,
* registering windows(they are monitored through signals, and their state is changed when needed)
* Also contains util functions to create dynamic windows, opening the window only on focused
* monitor, or all available monitors!
*/
@register({ GTypeName: "Windows" })
class WindowsClass extends GObject.Object {
#openWindows: Record<string, Widget.Window | Array<Widget.Window>> = {};
private static instance: (WindowsClass | null);
@signal(String)
declare opened: () => string;
@signal(String)
declare closed: () => string;
#windows: Record<string, (() => (Widget.Window | Array<Widget.Window>))> = {
"bar": this.createWindowForMonitors(Bar),
"osd": this.createWindowForFocusedMonitor(OSD),
"control-center": this.createWindowForFocusedMonitor(ControlCenter),
"center-window": this.createWindowForFocusedMonitor(CenterWindow),
"logout-menu": this.createWindowForFocusedMonitor(LogoutMenu),
"floating-notifications": this.createWindowForFocusedMonitor(FloatingNotifications),
"apps-window": this.createWindowForFocusedMonitor(AppsWindow)
};
#windowConnections: Record<string, (Array<number> | Array<Array<number>>)> = {};
#appConnections: Array<number> = [];
get windows() { return this.#windows; }
@property(Object)
get openWindows(): Record<string, Widget.Window | Array<Widget.Window>> { return this.#openWindows; };
constructor() {
super();
// Listen to monitor events
this.#appConnections.push(
App.connect("monitor-added", (_, _monitor) => {
AstalHyprland.get_default().get_monitors().length > 0 &&
this.reopen();
}),
App.connect("monitor-removed", (_, monitor) => {
Object.values(this.openWindows).map((window: (Array<Widget.Window> | Widget.Window), i: number) => {
if(Array.isArray(window)) {
window = window as Array<Widget.Window>;
window.map(win => {
if(win.get_current_monitor() === monitor) {
win?.close();
this.openWindows[i] = (this.openWindows[i] as Array<Widget.Window>).filter(item =>
item !== win);
}
});
if((this.openWindows[i] as Array<Widget.Window>).length < 1)
delete this.openWindows[i];
}
window = window as Widget.Window;
if(window.get_current_monitor() === monitor)
window.close();
});
})
);
}
vfunc_dispose() {
Object.keys(this.#windowConnections).map(name =>
this.disconnectWindow(name));
this.#appConnections.map(id =>
GObject.signal_handler_is_connected(App, id) &&
App.disconnect(id));
}
private disconnectWindow(name: keyof typeof this.windows) {
const window = this.openWindows[name];
if(!window) {
console.log("couldn't disconnect, window is not open");
return;
}
this.#windowConnections[name].map((id: Array<number> | number) => {
if(Array.isArray(window)) {
window.map((win, i) => {
const curId = (id as Array<number>)[i];
GObject.signal_handler_is_connected(win, curId) &&
win.disconnect(curId);
});
return;
}
GObject.signal_handler_is_connected(window, id as number) &&
window.disconnect(id as number);
});
delete this.#windowConnections[name];
}
private connectWindow(name: keyof typeof this.windows) {
if(Object.hasOwn(this.#windowConnections, name)) return;
if(!this.openWindows?.[name]) {
console.log(`${name} is not open, will not connect`);
return;
}
if(Array.isArray(this.openWindows[name])) {
this.#windowConnections[name] = this.openWindows[name].map(win => [
win.connect("map", (window) => {
if(this.isVisible(name)) return;
this.#openWindows[name] = window;
this.notify("open-windows");
}),
win.connect("destroy", () => {
this.disconnectWindow(name);
this.notify("open-windows");
})
]);
return;
}
this.#windowConnections[name] = [
this.openWindows[name].connect("map", (window) => {
if(this.isVisible(name)) return;
this.#openWindows[name] = window;
this.notify("open-windows");
}),
this.openWindows[name].connect("destroy", () => {
this.disconnectWindow(name);
delete this.#openWindows[name];
this.notify("open-windows");
})
];
}
public static getDefault(): WindowsClass {
if(!this.instance)
this.instance = new WindowsClass();
return this.instance;
}
/**
* Creates a window instance for every monitor connected
* @param windowFun function: (mon: number) => Widget.Window, returned window must use provided monitor number
* @returns a function that when called, returns Array<Widget.Window>
* @throws Error if there are no monitors connected
*/
public createWindowForMonitors(windowFun: (mon: number) => Widget.Window): (() => Array<Widget.Window>) {
const monitors = AstalHyprland.get_default().get_monitors();
if(monitors.length < 1)
throw new Error("Couldn't create window for monitors", {
cause: `No monitors connected on Hyprland`
});
return () => monitors.map(mon => windowFun(mon.id));
}
/**
* Creates a window instance for focused monitor only
* @param windowFun function: (mon: number) => Widget.Window, returned window must use provided monitor number
* @returns a function that when called, returns a Widget.Window instance
* @throws Error if no focused monitor is found
*/
public createWindowForFocusedMonitor(windowFun: (mon: number) => Widget.Window): (() => Widget.Window) {
const focusedMonitor = AstalHyprland.get_default()
.get_monitors().filter(mon => mon.focused)[0];
if(!focusedMonitor)
throw new Error("Couldn't create window for focused monitor", {
cause: `No focused monitor found (${typeof focusedMonitor})`
});
return () => windowFun(focusedMonitor.id);
}
public addWindow(name: string, window: (() => (Widget.Window | Array<Widget.Window>))): void {
this.#windows[name] = window;
}
public hasWindow(name: keyof typeof this.windows): boolean {
return Boolean(this.windows?.[name as keyof typeof this.windows]);
}
public getWindow(name: (keyof typeof this.windows | string)): ((() => (Widget.Window | Array<Widget.Window>)) | undefined) {
return this.windows?.[name as keyof typeof this.windows];
}
public getOpenWindow(name: (keyof typeof this.openWindows)): (Widget.Window | Array<Widget.Window> | undefined) {
return this.openWindows?.[name as keyof typeof this.openWindows];
}
public getWindows(): Array<(() => (Widget.Window | Array<Widget.Window>))> {
return Object.values(this.windows);
}
public getFocusedMonitorId(): (number|null) {
return AstalHyprland.get_default().get_monitors().filter(mon => mon.focused)?.[0]?.id ?? null;
}
public isVisible(name: keyof typeof this.windows): boolean {
return Object.hasOwn(this.#openWindows, name) || Object.hasOwn(this.#windowConnections, name);
}
public open(name: keyof typeof this.windows): void {
if(this.isVisible(name)) return;
let window: (() => (Widget.Window | Array<Widget.Window>)) = this.getWindow(name)!;
const openWindows: (Array<Widget.Window> | Widget.Window) = window();
this.#openWindows[name] = openWindows;
this.connectWindow(name);
this.emit("opened", name);
this.notify("open-windows");
if(Array.isArray(openWindows)) {
openWindows.map(win => win.show());
return;
}
openWindows.show();
}
public close(name: keyof typeof this.windows): void {
if(!this.isVisible(name)) return;
this.disconnectWindow(name);
const window = this.#openWindows[name];
delete this.#openWindows[name];
if(Array.isArray(window)) {
window.map(win => win.close());
this.emit("closed", name);
this.notify("open-windows");
return;
}
window.close();
this.emit("closed", name);
this.notify("open-windows");
}
public toggle(name: keyof typeof this.windows): void {
this.isVisible(name) ? this.close(name) : this.open(name);
}
public closeAll(): void {
Object.keys(this.openWindows).map(name => this.close(name));
}
public reopen(): void {
const openWins = Object.keys(this.openWindows);
this.closeAll();
openWins.map(name => this.open(name));
}
}
/**
* Windowing System
* Possible actions: getting window states(visible or not), close, open or toggle windows,
* registering windows(they are monitored through signals, and their state is changed when needed)
* Also contains util functions to create dynamic windows, opening the window only on focused
* monitor, or all available monitors!
*/
export const Windows = WindowsClass.getDefault();
+5 -1
View File
@@ -1,4 +1,8 @@
Config ( Config (
max_entries: 8, max_entries: 8,
desktop_actions: false desktop_actions: false,
terminal: Some(Terminal(
command: "kitty",
args: "-c {}"
))
) )
+43 -85
View File
@@ -1,74 +1,31 @@
#!/usr/bin/bash #!/usr/bin/bash
source ./utils.sh
set -e set -e
trap "printf \"\nOk! Quitting beacuse you entered an exit signal.\n\"; exit 1" SIGINT trap "printf \"\nOk, quitting beacuse you entered an exit signal. (SIGINT).\n\"; exit 1" SIGINT
trap "printf \"\nOh noo!! Some application just told the script to end!\"; exit 2" SIGTERM
printf "\n"
echo "######################################"
echo "## Retrozinndev's Hyprland Dotfiles ##"
echo "######################################"
printf "\n"
CONFIG_DIR="$HOME/.config"
DOTFILES_DIRS=("hypr" "eww" "kitty" "anyrun" "wal" "fastfetch" "mako")
DOTFILES_BACKUP_DIR="$HOME/hyprland-dotfiles-bkp"
echo "Welcome to my dotfiles installation script!"
# Warn user of possible problems that can happen
echo "WARN! Running this script may cause problems with your system. When continuing, you're confirming that any problem that may happen with your system is of **your** responsability."
function Backup_previous_dotfiles { function Backup_previous_dotfiles {
echo -n "Would you like to make a backup of the current dotfiles? [y/n] " echo -n "Would you like to make a backup of the current dotfiles? [y/n] "
read make_backup_answer read answer
printf "\n" printf "\n"
if [[ $make_backup_answer =~ "y" ]] if [[ $answer =~ "y" ]]
then then
echo "[info] Creating backup dir in $DOTFILES_BACKUP_DIR" . ./backup-dots.sh
if [[ -d $DOTFILES_BACKUP_DIR ]]
then
echo "Looks like the backup directory already exists!"
echo -n "Would you like to override it with the current configuration? (Will be moved to trash) [y/n] "
read override_backup
if [[ $override_backup = "y" ]] || [[ $override_backup = "yes" ]]
then
echo "Ok! The backup folder will be ovewritten with the current user configuration."
trash-put $DOTFILES_BACKUP_DIR
fi
else
mkdir $DOTFILES_BACKUP_DIR
fi
# Make backup of existing configurations
for dir in ${DOTFILES_DIRS[@]}; do
if [[ -d "$CONFIG_DIR/$dir" ]]
then
echo "-> Making backup of $dir"
cp -r "$CONFIG_DIR/$dir" $DOTFILES_BACKUP_DIR
else
echo "[info] $dir backup was skipped, because it wasn't found."
fi
done
echo "Finished backup!!"
else else
echo "Fine! Current settings will be overwritten, skipping backup :D" echo "Ok! Directories will be overwritten, skipping backup :3"
fi fi
} }
function Apply_wallpapers { function Apply_wallpapers {
echo -n "Would you also like to apply the wallpapers folder? :3 [y/n] " echo -n "Would you also like to apply the wallpapers folder? :3 [y/n] "
read input_wallpaper read answer
printf "\n" printf "\n"
if [[ $input_wallpaper =~ "y" ]] if [[ $answer =~ "y" ]]; then
then echo "Thanks for choosing! Please remember that I am not the author of the wallpapers!"
echo "Thanks for installing these wallpapers! Oh, remember that I am not the author of them!"
echo "You can see sources in the repo: https://github.com/retrozinndev/Hyprland-Dots/WALLPAPERS.md" echo "You can see sources in the repo: https://github.com/retrozinndev/Hyprland-Dots/WALLPAPERS.md"
echo "-> Copying wallpapers to ~/wallpapers" echo "-> Copying wallpapers to ~/wallpapers"
@@ -80,48 +37,49 @@ function Apply_wallpapers {
fi fi
} }
function Apply_dotfiles { for dir in ${config_dirs[@]}; do
if ! [[ -d ./$dir ]]; then
printf "\n" Send_log error "$dir is in fault, or you didn't run this script in its directory!"
exit 1
fi
done
# Start of actual script
Print_header
echo "Welcome to my dotfiles installation script!"
# Warn user of possible problems that can happen
echo "!!!WARNING!!! Running this may cause issues to your system if you don't know what you're doing! When continuing, you agree that any problem that may happen with the system is of your responsability!"
echo -n "Do you want to run the retrozinndev/Hyprland-Dots installer? [y/n] "
read input
if [[ $input =~ "y" ]]; then
printf "\n"
Backup_previous_dotfiles Backup_previous_dotfiles
printf "\n" printf "\n"
printf "Starting dotfiles installation...\n" Send_log "Starting installation\n"
for dir in ${DOTFILES_DIRS[@]}; do for dir in ${config_dirs[@]}; do
echo "-> Installing $dir in $CONFIG_DIR/$dir" dest=$XDG_CONFIG_HOME/$dir
mkdir -p $CONFIG_DIR/$dir
cp -rf ./$dir/* $CONFIG_DIR/$dir echo "-> Installing $dir in $dest"
mkdir -p $dest
cp -rf ./$dir/* $dest
done done
# Ask if user wants to apply repo's wallpapers dir # Ask if user wants to apply repo's wallpapers dir
Apply_wallpapers Apply_wallpapers
echo "Ah yes! Looks like it's ready to use, yay :3" echo "Ah yes! Looks like it's ready to use, yay :3"
echo "If you find any issue, please report at: https://github.com/retrozinndev/Hyprland-Dots/issues" echo -e "If you find any issue, please report it in:
echo "Thanks for using my dotfiles! I'm really happy about that :3" https://github.com/retrozinndev/Hyprland-Dots/issues"
echo "Thanks for using my Hyprland-Dots! I'm really happy about that :3"
printf "\n" printf "\n"
}
for dir in ${DOTFILES_DIRS[@]}; do
if ! [[ -d ./$dir ]]; then
echo "[error] Looks like $dir configuration is in fault, or you didn't run this script in its directory!"
echo "[tip] If directory doesn't exist, try cloning the dotfiles again."
exit 1
fi
done
echo -n "Do you want to install the dotfiles? [y/n] "
read input
if [[ $input =~ "y" ]]
then
Apply_dotfiles
else else
printf "Ok, doing as you said! Bye bye!\n" printf "Ok, doing as you said! Bye bye!\n"
exit 0 exit 0
fi fi
printf "\n"
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
# This script backups current configuration dirs
# listed in utils.sh script.
# --------
# Made by retrozinndev (João Dias)
# Licensed under the MIT License
# From: https://github.com/retrozinndev/Hyprland-Dots
source ./utils.sh
bkp_dir="$HOME/hyprland-dots-bkp"
Send_log "Creating backup in $bkp_dir"
if [[ -d $bkp_dir ]]
then
Send_log "Found existing backup in $bkp_dir!"
echo "Looks like the backup directory already exists!"
echo -n "Would you like to move it to trash/override it? [y/n] "
read answer
if [[ $answer =~ "y" ]]; then
echo "Fine! Previous backup is goning to be moved to trash"
trash-put $bkp_dir
else
echo "Ok! Quitting doing backup because it already exists"
exit 1
fi
fi
# Make backup of existing configurations
for dir in ${config_dirs[@]}; do
if [[ -d "$CONFIG_DIR/$dir" ]]
then
echo "-> backuping $dir"
cp -r "$CONFIG_DIR/$dir" $DOTFILES_BACKUP_DIR
else
echo "[info] $dir backup was skipped, because it wasn't found."
fi
done
echo "Finished backup!!"
-10
View File
@@ -1,10 +0,0 @@
@import "../../.cache/wal/colors.scss";
@import "styles/general.scss";
@import "styles/bar.scss";
@import "styles/calendar.scss";
@import "styles/control-center.scss";
@import "styles/powermenu.scss";
@import "styles/volume-control.scss";
@import "styles/floating-notifications.scss";
-17
View File
@@ -1,17 +0,0 @@
; Variables
(include "variables.yuck")
; Windows
(include "windows/calendar-window.yuck")
(include "windows/control-center.yuck")
(include "windows/bar.yuck")
(include "windows/powermenu.yuck")
(include "windows/volume-control.yuck")
(include "windows/volume-popup.yuck")
(include "windows/floating-media.yuck")
(include "windows/floating-notifications.yuck")
(include "windows/hardware-monitor.yuck")
; Universal Widgets
(include "widgets/big-media.yuck")
(include "widgets/separator.yuck")
-12
View File
@@ -1,12 +0,0 @@
#!/usr/bin/env bash
# output current window before event trigger to prevent issues
hyprctl -j activewindow | jq -c | sed 's/\\[n]//g'
handle() {
case $1 in
activewindow*) hyprctl -j activewindow | jq -c | sed 's/\\[n]//g' ;;
esac
}
socat -U - UNIX-CONNECT:$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock | while read -r line; do handle "$line"; done
-17
View File
@@ -1,17 +0,0 @@
#!/usr/bin/env bash
# This script reloads eww configuration and updates
# window status variables, used in eww-window.sh
# script. Avoids issues with widgets that use status
# variables to dinamically change their content.
# ----------
# Licensed under the MIT License
# Made by retrozinndev (João Dias)
# From https://github.com/retrozinndev/Hyprland-Dots
# TODO
open_windows=$(eww active-windows | awk -F: '{ print $1 }' | sed -e 's/ /\\[n]/g')
echo $open_windows
#for window in $()
-104
View File
@@ -1,104 +0,0 @@
#!/usr/bin/env bash
# arg1($1) should be one of the options listed in help command;
# arg2($2) should be the literal window name as defined in eww configuration.
function help_message() {
printf \
"This is a helper script that helps opening/closing eww windows on
retrozinndev's Hyprland Dots, by checking if the window is already
open/closed before doing anything, also changes eww state variables.
This needs a variable like window_state_WINDOWNAME to work, where
WINDOWNAME is the literal name of the declared window inside the
eww configuration you are using.
Usage:
arg1 arg2
./eww-window.sh [SINGLE OPTION] [WINDOW_NAME]
Options:
-h, --help, help: Shows this help message;
--open, open: Opens a window and changes its state variable to
\"open\" if not open, or else does nothing;
--toggle, toggle: Toggles a window. If open, the specified
window is closed, if closed, the
specified window is open. Same thing goes to
the eww window state variable;
--close, close: Closes a window and also changes its state
variable to \"closed\" if open, or else does
nothing.
Developer: retrozinndev (João Dias), https://github.com/retrozinndev
Issue tracker: https://github.com/retrozinndev/Hyprland-Dots/issues
Licensed under the MIT License, as in retrozinndev's Hyprland-Dots repo."
}
function send_log() {
case "$1" in
err*)
tag_color="\e[31m"
;;
warn*)
tag_color="\e[33m"
;;
*)
tag_color="\e[34m"
;;
esac
echo -e "[$tag_color$1\e[0m] $2"
}
function check_if_empty() {
if [[ $1 == "" ]]; then
send_log "error" "argument \$1 is empty!"
help_message
exit 1
fi
}
function toggle_eww_window() {
if ! [[ $(eww active-windows) =~ "$1" ]]; then
eww open "$1"
eww update "window_state_$1=open"
else
eww close "$1"
eww update "window_state_$1=closed"
fi
}
case "$1" in
--help | -h | help | h)
help_message
;;
--open | open)
check_if_empty $2 "WINDOW_NAME"
if ! [[ $(eww active-windows) =~ "$2" ]]; then
eww open "$2"
eww update "window_state_$2=open"
else
send_log "info" "Window '$2' is already open, ignored."
fi
;;
--close | close)
check_if_empty $2 "WINDOW_NAME"
if [[ $(eww active-windows) =~ "$2" ]]; then
eww close "$2"
eww update "window_state_$2=closed"
else
send_log "info" "Window '$2' is already closed, ignored."
fi
;;
--toggle | toggle)
check_if_empty $2 "WINDOW_NAME"
toggle_eww_window $2
;;
*)
send_log "error" "Action not specified or incorrect command order. Good example: \`./eww-window.sh open bar\`"
help_message
;;
esac

Some files were not shown because too many files have changed in this diff Show More