Migrate to AGS v3 and GTK4

Migrate to AGS v3 and GTK 4, merges #14
This commit is contained in:
João Dias
2025-10-23 07:28:55 -03:00
committed by GitHub
297 changed files with 9166 additions and 7761 deletions
+5
View File
@@ -1 +1,6 @@
node_modules/
@types/
build/
pnpm-lock.yaml
*.log
+24 -17
View File
@@ -1,21 +1,28 @@
MIT License
BSD 3-Clause License
Copyright (c) 2024 João Dias
Copyright (c) 2025, João Dias (retrozinndev)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+90 -33
View File
@@ -1,54 +1,65 @@
# colorshell
<p>(previously retrozinndev/Hyprland-Dots)</p>
> [!note]
> The [AGS v3](https://github.com/aylur/ags/blob/main) version is already done! You can try it out in the branch [`gtk4-ags3`](https://github.com/retrozinndev/colorshell/blob/gtk4-ags3). <br>
> The updated version will be merged into the main branch as soon as AGS v3 reaches a stable release(tagged release).
> [!note]
> My personal dotfiles are now on [retrozinndev/Hyprland-Dots](https://github.com/retrozinndev/Hyprland-Dots)
My Hyprland Desktop Shell that I love to keep improving every day! 🤩 <br>
This is the repository for the colorshell desktop shell, made for Hyprland with [TypeScript], [GTK4], [AGS], [Gnim], and some of the [Astal] libraries.
This is the repository for my desktop shell, made with [GTK], [Astal] and [AGS] + [TypeScript]. <br>
It really took me a lot of time to make this, so please star the repo if you like it! :star:
## 🌄 Screenshots
![Kitty](repo/shots/kitty.png)
![Widgets](repo/shots/control-center-runner.png)
![Runner](repo/shots/clock-window.png)
![Browser](repo/shots/browser.png)
<div align="center">
<img src="./repo/shots/desktop.png" alt="desktop" width="49%" />
<img src="./repo/shots/runner.png" alt="Runner" width="49%" />
<img src="./repo/shots/center-window-control-center.png" alt="Control Center & Center Window" width="49%" />
<img src="./repo/shots/kitty.png" alt="Kitty" width="49%" />
<p align="right">
<i>more screenshots on <a href="repo/shots"><code>repo/shots</code></a></i>
</p>
</div>
## 🎨 Colors
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.
This is possible by using [pywal16], a fork of the archived [pywal](https://github.com/dylanaraps/pywal) project.
It's a cli tool to generate color schemes from an image.
## 🖼️ Wallpapers
When you're at the [Installation](#%EF%B8%8F-installation) process, you can choose whether you want to install the wallpapers or not. <br>
If you chose to not, either define the `$WALLPAPERS` variable in your hyprland user configuration to your wallpapers folder, or create the `~/wallpapers` directory.
These are not included in the shell anymore, because the repository was getting too big in size.
So you'll have to add it in your own.
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.
You can add more wallpapers either by adding your custom images to `~/wallpapers` or by defining `WALLPAPERS`
in your `hypr/user/environment`, pointing to the custom location.
### ️ Source
None of the wallpapers available in this repo are made by me! You can find sources inside the [`WALLPAPERS.md`](/WALLPAPERS.md) file. (it took me a lot of time to make this sources list 😭)
Also, 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.
## ✨ Features
<details>
### ✔️ 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)
- Apps(basically the "start menu", opens the full-screen app launcher)
- Workspaces indicator
- Focused Client(window) information(title, class and icon)
- Clock(with date)
- Media(shows only when media is being played)
- Media
- Change current player by scrolling on top of the widget or by opening the
Center Window and scrolling the player
- Only available when there's media playing
- 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)
- Sliders
- Speaker volume
- Microphone sensitivity
- Brightness amount
- Pages(the thing that shows up when you click the arrow on a tile)
- Bluetooth devices
- Network devices
- Night Light controls
- Brightness options
- Tiles
- Screen Recording
- Bluetooth
@@ -56,6 +67,9 @@ None of the wallpapers available in this repo are made by me! You can find sourc
- Network(wifi needs work, i don't have wifi in my machine)
- Don't Disturb(disables notification popups)
- Center Window(clock, calendar + media management)
- OSD (On-screen Display)
- Brightness(when changed)
- Volume(when changed)
- 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)
@@ -64,9 +78,9 @@ None of the wallpapers available in this repo are made by me! You can find sourc
- Wallpapers(`#`): Search and select to change wallpaper
- Media(`:`): Control playing media
- Search(`?`): Search something on the internet with your default browser
- Gnome-like application list
- Support for your multiple monitors
- Dynamic support for [UWSM](https://github.com/Vladimir-csp/uwsm)(dinamically enabled if current session is using UWSM)
- Dynamic support for [UWSM](https://github.com/Vladimir-csp/uwsm)(apps will use uwsm if current session is using it)
</details>
## ⌨️ Binds
You can see default bindings and usage information on the [Wiki/Usage] page!
@@ -85,18 +99,59 @@ You can do so by forking this repository, translating the shell in your fork and
## ⚙️ Installation
See the Installation Guide on [Wiki/Installation].
## 🛠️ Development
This project uses `pnpm` to manage packages and running scripts.
To build the shell, run a development build or make a release build, you can use the project's integrated scripts.<br>
The most complicated ones have a help flag, so you can learn from there.
### Dependencies
These are development-only dependencies(by package name on AUR):
- `aylurs-gtk-shell-git`
Plus, you also need the packages listed in [Wiki/Dependencies]!
### Building
In a common build, the shell's gresource(icons and sass) will be targeted to the build output directory by default.
If you want to ship it, you likely want to use the `pnpm build:release` command.
```zsh
pnpm build -d # remove the -d flag if you don't want a development build
```
If you want to ship the build(or install it on your local machine), you'll likely prefer a release build:
(the `build:release` command targets the gresource to `$XDG_DATA_HOME/colorshell/resources.gresource` by default)
```zsh
pnpm build:release
```
Don't forget to install the gresource to the actual target directory! Or else it'll not find the resource file and will fail
to load custom assets.<br>
Also, the environment variables are only actually used at runtime! It's passed as a literal string in the bash
variable format, then when the shell runs, it understands that it's an environment variable and replaces it with it's value.
### Testing/Running the project
```zsh
pnpm dev
```
or if you actually only want to run the current build instead of building again:
```
pnpm start
```
## ❗ Issues
Having issues? Please create a [new Issue] here, I'll be happy to help you out!
## 📜 License
This repo is licensed under the [MIT License], project is made and maintained by [retrozinndev](https://github.com/retrozinndev).
This repo is licensed under the [BSD 3-clause] license, project is made and maintained by [retrozinndev](https://github.com/retrozinndev).
## 🌠 Stargazers
<div align="center">
[![Star History Chart](https://api.star-history.com/svg?repos=retrozinndev/colorshell&type=Date)](https://www.star-history.com/#retrozinndev/colorshell&Date)
<br>
<p align="center">Thanks to everyone who starred my project! 💖</p>
<a href="https://www.star-history.com/#retrozinndev/colorshell&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=retrozinndev/colorshell&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=retrozinndev/colorshell&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=retrozinndev/colorshell&type=Date" />
</picture>
</a>
<br>
<p align="center">Thanks to everyone who starred my project! 💖</p>
</div>
<!-- References of other projects -->
@@ -106,12 +161,14 @@ This repo is licensed under the [MIT License], project is made and maintained by
[nushell]: https://nushell.sh
[kitty]: https://sw.kovidgoyal.net/kitty
[ags]: https://aylur.github.io/ags
[gnim]: https://aylur.github.io/gnim
[astal]: https://aylur.github.io/astal
[typescript]: https://typescriptlang.org
[gtk4]: https://www.gtk.org
[gtk]: https://www.gtk.org
<!-- Web refs -->
[mit license]: https://en.wikipedia.org/wiki/MIT_License
[bsd 3-clause]: https://en.wikipedia.org/wiki/BSD_licenses#4-clause_license_(original_%22BSD_License%22)
<!-- Tabs -->
[wiki]: https://github.com/retrozinndev/colorshell/wiki
-559
View File
@@ -1,559 +0,0 @@
# About
None of them are made by me. You can view and find their source
by expanding them below.
## Bocchi The Rock!
<details>
<summary>
<b>Kessoku Band Rooftop (cropped borders)</b>
</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
<details>
<summary>
<b>Arch Linux Miku</b>
</summary>
<img src="wallpapers/Arch Linux Miku.jpg"></img>
- Source: [DeviantArt](https://www.deviantart.com/nesyah/art/Arch-linux-feat-Hatsune-Miku-858316759)
</details>
<details>
<summary>
<b>Gumi Bridge</b>
</summary>
<img src="wallpapers/Gumi Bridge.jpg"></img>
- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=593482)
</details>
<details>
<summary>
<b>Gumi Forest Sunlight</b>
</summary>
<img src="wallpapers/Gumi Forest Sunlight.jpg"></img>
- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=930443)
</details>
<details>
<summary>
<b>Miku Balloons</b>
</summary>
<img src="wallpapers/Miku Balloons.jpg"></img>
- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=768576)
</details>
<details>
<summary>
<b>Miku Green Hair Glasses</b>
</summary>
<img src="wallpapers/Miku Green Hair Glasses.png"></img>
- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=858278)
</details>
<details>
<summary>
<b>Kagamine Rin Yellow Tapes</b>
</summary>
<img src="wallpapers/Kagamine Rin Yellow Tapes.png"></img>
- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1292852)
</details>
<details>
<summary>
<b>Gumi VOCALOID</b>
</summary>
<img src="wallpapers/Gumi VOCALOID.png"></img>
- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=768096)
</details>
<details>
<summary>
<b>Miku Stylish with Glasses</b>
</summary>
<img src="wallpapers/Miku Stylish with Glasses.jpg"></img>
- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1305668)
</details>
<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)
<!---------------- -->
## 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>
- 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
@@ -1,2 +0,0 @@
node_modules/
@girs/
-113
View File
@@ -1,113 +0,0 @@
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";
import { Config } from "./scripts/config";
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",
icons: "icons/",
requestHandler: (request: string, response: (result: any) => void): void => {
response(handleArguments(request));
},
main: (..._args: Array<string>) => {
console.log(`Initialized astal instance as: ${ App.instanceName || "astal" }`);
console.log("Config: initializing configuration file");
Config.getDefault();
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
@@ -1,21 +0,0 @@
{
"name": "colorshell",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "colorshell",
"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
@@ -1,6 +0,0 @@
{
"name": "colorshell",
"dependencies": {
"astal": "/usr/share/astal/gjs"
}
}
-315
View File
@@ -1,315 +0,0 @@
import { AstalIO, GObject, 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";
import AstalHyprland from "gi://AstalHyprland";
export namespace Runner {
export type RunnerProps = {
halign?: Gtk.Align;
valign?: Gtk.Align;
width?: number;
height?: number;
entryPlaceHolder?: string;
initialText?: string;
resultsLimit?: number;
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|number)): boolean {
search = search.replace(/[\\^$.*?()[\]{}|]/g, "\\$&");
if(typeof item === "number")
return new RegExp(`${search.split('').map(c =>
`${c}`).join('')}`,
"g").test(item.toString());
return new RegExp(`${search.split('').map(c =>
`${c}`).join('')}`,
"gi").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 plugins 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...",
initialText,
resultsLimit: 24
} 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);
const connections: Map<GObject.Object, number> = new Map();
props.width ??= 780;
props.height ??= 420;
gtkEntry = new Widget.Entry({
className: "search",
placeholderText: props?.entryPlaceHolder || "",
onChanged: async (self) => {
updateResultsList(self.text);
resultsList.get_row_at_index(0) &&
resultsList.select_row(resultsList.get_row_at_index(0));
if(self.text.trim().length < 1 && !mainBox.get_style_context().has_class("empty-input")) {
mainBox.get_style_context().add_class("empty-input");
return;
}
mainBox.get_style_context().has_class("empty-input") &&
mainBox.get_style_context().remove_class("empty-input");
},
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 mainBox = new Widget.Box({
className: `runner main ${props.showResultsPlaceHolderOnStartup ? "empty" : ""}`,
orientation: Gtk.Orientation.VERTICAL,
hexpand: true,
valign: Gtk.Align.START,
children: [
gtkEntry,
new Widget.Scrollable({
className: "results-scrollable",
vscroll: Gtk.PolicyType.AUTOMATIC,
hscroll: Gtk.PolicyType.NEVER,
expand: true,
visible: props.showResultsPlaceHolderOnStartup ?? false,
propagateNaturalHeight: true,
maxContentHeight: props.height,
child: new Gtk.ListBox({
visible: true,
expand: true,
} as Gtk.ListBox.ConstructorProps)
})
]
} as Widget.BoxProps);
const scrollable = mainBox.get_children()[1] as Widget.Scrollable;
const resultsList = scrollable.get_child() as Gtk.ListBox;
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;
}
}
const results = calledPlugins.map(plugin => plugin.handle(
plugin.prefix ? input.replace(plugin.prefix, "") : input)
).filter(value => value !== undefined && value !== null).flat(1);
return props?.resultsLimit != null &&
props.resultsLimit !== Infinity ?
results.splice(0, props.resultsLimit)
: results;
}
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);
const conns: Array<number> = [];
conns.push(
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();
}
}),
resultsList.connect("destroy", () =>
conns.forEach((id) => resultsList.disconnect(id))
)
);
});
widgets.length > 0 ?
(!scrollable.visible && scrollable.show())
: scrollable.hide();
}
if(!instance)
instance = Windows.createWindowForFocusedMonitor((mon: number): (Widget.Window) => PopupWindow({
namespace: "runner",
monitor: mon,
widthRequest: props.width,
heightRequest: props.height,
marginTop: (AstalHyprland.get_default().get_monitor(mon)?.height / 2) - (props.height! / 2),
exclusivity: Astal.Exclusivity.IGNORE,
halign: Gtk.Align.CENTER,
valign: Gtk.Align.START,
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: () => {
connections.forEach((id, obj) => GObject.signal_handler_is_connected(obj, id) &&
obj.disconnect(id));
gtkEntry = null;
[...plugins.values()].forEach(plugin =>
plugin && plugin.onClose && plugin.onClose());
instance = null;
},
child: mainBox
} as PopupWindowProps))();
return instance!;
}
}
-22
View File
@@ -1,22 +0,0 @@
import { ResultWidget, ResultWidgetProps } from "../../widget/runner/ResultWidget";
import AstalApps from "gi://AstalApps";
import { execApp, getAstalApps, updateApps } from "../../scripts/apps";
import { Runner } from "../Runner";
import { Astal } from "astal/gtk3";
export const PluginApps = {
// Do not provide prefix, so it always runs.
name: "Apps",
// asynchronously-refresh apps list on init
init: async () => updateApps(),
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: () => execApp(app)
} as ResultWidgetProps)
);
}
} as Runner.Plugin;
-33
View File
@@ -1,33 +0,0 @@
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: "Clipboard is empty",
description: "Copy something and it will 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) || 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
@@ -1,79 +0,0 @@
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
})();
-29
View File
@@ -1,29 +0,0 @@
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",
prioritize: true,
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;
-176
View File
@@ -1,176 +0,0 @@
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();
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
@@ -1,73 +0,0 @@
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
@@ -1,42 +0,0 @@
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);
}
}
-249
View File
@@ -1,249 +0,0 @@
import { AstalIO, Gio, GLib, GObject, monitorFile, readFileAsync, register, timeout } from "astal";
import Binding, { bind, Subscribable } from "astal/binding";
import { Notifications } from "./notifications";
import AstalNotifd from "gi://AstalNotifd";
import { encoder } from "./utils";
export { Config };
export type ConfigEntries = Partial<{
workspaces: Partial<{
/** this is the function that shows the Workspace's IDs
* around the current workspace if one breaks the crescent order.
* It basically helps keyboard navigation between workspaces.
* ---
* Example: 1(empty, current, shows ID), 2(empty, does not appear(makes
* the previous not to be in a crescent order)), 3(not empty, shows ID) */
enable_helper: boolean;
/** breaks `enable_helper`, makes all workspaces show their respective ID
* by default */
always_show_id: boolean;
}>;
clock: Partial<{
/** use the same formats as gnu's `date` command */
date_format: string;
}>;
notifications: Partial<{
timeout_low: number;
timeout_normal: number;
timeout_critical: number;
}>;
night_light: Partial<{
/** whether to save night light values to disk */
save_on_shutdown: boolean;
}>;
}>;
type ValueTypes = "string" | "boolean" | "object" | "number" | "undefined" | "any";
@register({ GTypeName: "Config" })
class Config extends GObject.Object implements Subscribable {
private static instance: Config;
private readonly defaultFile = Gio.File.new_for_path(
`${GLib.get_user_config_dir()}/colorshell/config.json`);
/** unmodified object with default entries. User-values are stored
* in the `entries` field */
public readonly defaults: ConfigEntries = {
notifications: {
timeout_low: 4000,
timeout_normal: 6000,
timeout_critical: 0
},
night_light: {
save_on_shutdown: true
},
workspaces: {
always_show_id: false,
enable_helper: true
},
clock: {
date_format: "%A %d, %H:%M"
}
};
private readonly entries: ConfigEntries = this.defaults;
#subs: Set<(entries: ConfigEntries) => void> = new Set();
#file: Gio.File;
private timeout: (AstalIO.Time|boolean|undefined);
public get file() { return this.#file; };
constructor(filePath?: (Gio.File|string)) {
super();
this.#file = (typeof filePath === "string") ?
Gio.File.new_for_path(filePath)
: (filePath ?? this.defaultFile);
if(!this.#file.query_exists(null)) {
this.#file.make_directory_with_parents(null);
this.#file.delete(null);
this.#file.create_readwrite_async(
Gio.FileCreateFlags.NONE, GLib.PRIORITY_DEFAULT,
null, (_, asyncRes) => {
const ioStream = this.#file.create_readwrite_finish(asyncRes);
ioStream.outputStream.write_bytes_async(
GLib.Bytes.new(encoder.encode(JSON.stringify(this.entries, undefined, 4))),
GLib.PRIORITY_DEFAULT, null,
(_, asyncRes) => {
const writtenBytes = ioStream.outputStream.write_bytes_finish(asyncRes);
if(!writtenBytes)
Notifications.getDefault().sendNotification({
appName: "colorshell",
summary: "Write error",
body: `Couldn't write default configuration file to "${this.#file.get_path()!}"`
});
}
);
});
}
monitorFile(this.#file.get_path()!,
() => {
if(this.timeout) return;
this.timeout = timeout(500, () => this.timeout = undefined);
if(this.#file.query_exists(null)) {
this.timeout?.cancel();
this.timeout = true;
this.readFile().finally(() =>
this.timeout = undefined);
return;
}
Notifications.getDefault().sendNotification({
appName: "colorshell",
summary: "Config error",
body: `Could not hot-reload configuration: config file not found in \`${this.#file.get_path()!}\`, last valid configuration is being used. Maybe it got deleted?`
});
}
);
}
public static getDefault(): Config {
if(!this.instance)
this.instance = new Config();
return this.instance;
}
private async readFile(): Promise<void> {
await readFileAsync(this.#file.get_path()!).then((content) => {
let config: (ConfigEntries|undefined);
try {
config = JSON.parse(content) as ConfigEntries;
} catch(e) {
Notifications.getDefault().sendNotification({
urgency: AstalNotifd.Urgency.NORMAL,
appName: "colorshell",
summary: "Config parsing error",
body: `An error occurred while parsing colorshell's config file: \nFile: ${
this.#file.get_path()!}\n${
(e as SyntaxError).message}\n${(e as SyntaxError).stack}`
});
}
if(!config) return;
// only change valid entries that are available in the defaults (with 1 of depth)
for(const k of Object.keys(this.entries)) {
if(config[k as keyof typeof config] === undefined)
return;
// TODO needs more work, like object-recursive(infinite depth) entry attributions
this.entries[k as keyof typeof this.entries] = config[k as keyof typeof config];
}
this.notifySubs();
}).catch((e: Gio.IOErrorEnum) => {
Notifications.getDefault().sendNotification({
urgency: AstalNotifd.Urgency.NORMAL,
appName: "colorshell",
summary: "Config read error",
body: `An error occurred while reading colorshell's config file: \nFile: ${`${
this.#file.get_path()!}\n${e.message ? `${e.message}\n` : ""}${e.stack}`.replace(/[<>]/g, "\\&")}`
});
});
}
private notifySubs(): void {
for(const sub of this.#subs) {
sub(this.entries);
}
}
public bindProperty(propertyPath: (keyof ConfigEntries|string), expectType?: ValueTypes): Binding<any|undefined> {
return bind(this).as(() =>
this.getProperty(propertyPath, expectType));
}
public getProperty(path: string, expectType?: ValueTypes): (any|undefined) {
return this._getProperty(path, this.entries, expectType);
}
public getPropertyDefault(path: string, expectType?: ValueTypes): (any|undefined) {
return this._getProperty(path, this.defaults, expectType);
}
private _getProperty(path: string, entries: ConfigEntries, expectType?: ValueTypes): (any|undefined) {
let property: any = entries;
const pathArray = path.split('.').filter(str => str);
for(let i = 0; i < pathArray.length; i++) {
const currentPath = pathArray[i];
property = property[currentPath as keyof typeof property];
}
if(expectType !== "any" && typeof property !== expectType) {
console.error(`Config: property with path \`${path
}\` is either \`undefined\` or not in the expected value type \`${expectType
}\`, returning default value`);
property = this.defaults;
for(let i = 0; i < pathArray.length; i++) {
const currentPath = pathArray[i];
property = property[currentPath as keyof typeof property];
}
}
if(expectType !== "any" && typeof property !== expectType) {
console.error(`Config: property with path \`${path}\` not found in defaults/user-entries, returning \`undefined\``);
property = undefined;
}
return property;
}
public get(): ConfigEntries {
return this.entries;
}
public subscribe(callback: (entries: ConfigEntries) => void): () => void {
this.#subs.add(callback);
return () => {
this.#subs.delete(callback);
};
}
}
-145
View File
@@ -1,145 +0,0 @@
import { AstalIO, exec, execAsync, GLib, 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); }
public readonly maxTemperature = 20000;
public readonly minTemperature = 1000;
public readonly identityTemperature = 6000;
public readonly maxGamma = 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 setTemperature(value: number): 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 setGamma(value: number): void {
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}`
));
}
public applyIdentity(): void {
if(this.#identity) return;
this.#prevGamma = this.#gamma;
this.#prevTemperature = this.#temperature;
this.#identity = true;
this.temperature = this.identityTemperature;
this.gamma = this.maxGamma;
}
public filter(): void {
if(!this.#identity) return;
this.#identity = false;
this.setTemperature(this.#prevTemperature ?? this.identityTemperature);
this.setGamma(this.#prevGamma ?? this.maxGamma);
this.#prevTemperature = null;
this.#prevGamma = null;
}
public saveData(): void {
exec(`sh ${GLib.get_user_config_dir()}/hypr/scripts/save-hyprsunset.sh`);
}
public loadData(): void {
exec(`sh ${GLib.get_user_config_dir()}/hypr/scripts/load-hyprsunset.sh`);
}
}
-285
View File
@@ -1,285 +0,0 @@
import { AstalIO, execAsync, Gio, GObject, property, register, signal, timeout } from "astal";
import AstalNotifd from "gi://AstalNotifd";
import { Config } from "./config";
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 = Config.getDefault().getProperty(
`notifications.timeout_${this.getUrgencyString(notification.urgency).toLowerCase()}`,
"number") as number;
if(this.getNotifd().dontDisturb) {
this.addHistory(notification, () => notification.dismiss());
return;
}
this.addNotification(notification, () => {
if(notification.urgency !== AstalNotifd.Urgency.CRITICAL ||
(notification.urgency === AstalNotifd.Urgency.CRITICAL &&
notifTimeout > 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) return;
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 async clearHistory(): Promise<void> {
this.#history.reverse().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 {
for(let i = 0; i < this.#notifications.length; i++) {
const item = this.#notifications[i];
if(item.id !== notif.id) continue;
this.#notifications.splice(i, 1);
this.emit("notification-replaced", item.id);
break;
}
this.#notifications.unshift(notif);
this.notify("notifications");
this.emit("notification-added", notif);
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(value?: boolean): boolean {
value = value ?? !AstalNotifd.get_default().dontDisturb;
AstalNotifd.get_default().dontDisturb = value;
return value;
}
public getNotifd(): AstalNotifd.Notifd { return AstalNotifd.get_default(); }
}
export { Notifications };
-24
View File
@@ -1,24 +0,0 @@
import { execAsync, Gio, monitorFile } from "astal";
import { App } from "astal/gtk3";
import { uwsmIsActive } from "./apps";
const monitoringPaths = [ "./scripts", "./window", "./app.ts", "env.d.ts" ];
export function restartInstance(instanceName?: string): void {
execAsync(`astal -q ${ instanceName ?? App.instanceName ?? "astal" }`);
Gio.Subprocess.new(
( uwsmIsActive ?
[ "uwsm", "app", "--", "ags", "run" ]
: [ "ags", "run" ]),
Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE
);
}
export function monitorPaths(): void {
monitoringPaths.map((path: string) => {
monitorFile(
path,
() => restartInstance()
)
});
}
-83
View File
@@ -1,83 +0,0 @@
// 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 async compileSass(): Promise<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> {
await this.compileSass().catch((err: Gio.IOErrorEnum) =>
console.error(`Stylesheet: An Error occurred and Sass couldn't be compiled. Stderr:\n${err.message ?
`\t${err.message}\n` : ""}${err.stack}\n`)
).then(() => 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
@@ -1,6 +0,0 @@
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;
-31
View File
@@ -1,31 +0,0 @@
import { exec, execAsync, Gio, GLib } from "astal";
export const decoder = new TextDecoder("utf-8"),
encoder = new TextEncoder();
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 proc = Gio.Subprocess.new(["bash", "-c", `command -v ${commandName}`],
Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE);
const [ , stdout, stderr ] = proc.communicate_utf8(null, null);
if(stdout && !stderr)
return true;
return false;
}
-89
View File
@@ -1,89 +0,0 @@
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);
}
}
}
-24
View File
@@ -1,24 +0,0 @@
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;
}
-249
View File
@@ -1,249 +0,0 @@
@use "sass:color";
@use "./mixins";
@use "./colors";
@use "./wal";
@use "./functions";
.bar-container {
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 {
background: $color-hover;
}
}
& > box {
border-radius: calc($radius - $padding);
margin: $padding 0;
}
& > box {
padding: 0 8px;
}
}
& > button,
& > box > button {
border-radius: calc($radius - $padding);
margin: $padding 0;
padding: 0 9px;
&:hover {
background: $color-hover;
}
}
}
.workspaces-row {
padding: 4px;
& eventbox > box > eventbox {
& > box {
margin: 3px 0;
border-radius: 16px;
transition: 80ms linear;
min-width: 16px;
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;
& icon {
font-size: 10px;
}
}
}
&.reveal {
& .media > box {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
}
.tray {
padding: 0 6px;
& .item {
&: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;
& icon {
font-size: 14px;
}
& revealer > eventbox > box {
background: rgba($color: colors.$bg-tertiary, $alpha: .7);
border-radius: 12px;
margin: 4px 0;
margin-left: 5px;
padding: 2px 6px;
}
& .status-icons {
padding-left: 4px;
& icon.notification-count {
font-size: 6px;
margin-top: -14px;
}
}
}
}
.apps {
& > box {
min-width: 22px;
& > icon {
transition: 120ms linear;
font-size: 14px;
}
}
&.open > box {
background: colors.$bg-primary;
}
&:hover icon {
-gtk-icon-transform: scale(1.144);
}
}
}
-14
View File
@@ -1,14 +0,0 @@
@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-primary: $bg-translucent;
$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%));
-14
View File
@@ -1,14 +0,0 @@
@use "sass:color";
/**
* GTK3 only supports sRGB color space, unfortunately
*/
@function toRGB($color) {
@return rgba(
color.channel($color, "red"),
color.channel($color, "green"),
color.channel($color, "blue"),
color.alpha($color)
);
}
-48
View File
@@ -1,48 +0,0 @@
@use "sass:color";
@use "./wal";
@use "./functions" as funs;
.osd {
background: funs.toRGB(color.change($color: wal.$background, $alpha: 65%));
padding: 16px;
border-radius: 24px;
min-width: 180px;
.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;
}
}
}
-86
View File
@@ -1,86 +0,0 @@
@use "./colors";
.runner.main {
$radius: 24px;
background: rgba($color: colors.$bg-primary, $alpha: .8);
border-radius: $radius;
box-shadow: inset 0 0 0 1px colors.$bg-secondary,
0 0 8px 1px colors.$bg-translucent;
padding: 4px;
& entry {
transition: 80ms ease-in;
min-height: 1.6em;
padding: 14px;
border-radius: inherit;
background: none;
&:focus {
box-shadow: none;
}
& image.left {
margin-right: 6px;
}
}
& list {
padding: 6px;
padding-top: 0;
& > *:selected > .result,
& > *:active > .result,
& > *:hover > .result {
background: colors.$bg-secondary;
}
& > *:first-child {
margin-top: 12px;
}
&:last-child {
margin-bottom: 0;
}
}
& trough {
margin-bottom: 10px;
}
& 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
@@ -1,26 +0,0 @@
// SCSS Variables
// Generated by 'wal'
$wallpaper: "/home/joaov/wallpapers/Frieren Ring.jpeg";
// Special
$background: #523c42;
$foreground: #d3cecf;
$cursor: #d3cecf;
// Colors
$color0: #523c42;
$color1: #6c839d;
$color2: #7a84a4;
$color3: #9f8a9d;
$color4: #84a2b5;
$color5: #9f9cab;
$color6: #b7a1b2;
$color7: #b0a7a9;
$color8: #937b81;
$color9: #90AFD2;
$color10: #A3B0DB;
$color11: #D4B9D2;
$color12: #B0D9F2;
$color13: #D5D0E5;
$color14: #F5D7EE;
$color15: #d3cecf;
-14
View File
@@ -1,14 +0,0 @@
{
"$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
@@ -1,46 +0,0 @@
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;
}
-60
View File
@@ -1,60 +0,0 @@
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;
exclusivity?: Astal.Exclusivity;
};
/** 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,
exclusivity: props.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);
}
-109
View File
@@ -1,109 +0,0 @@
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,
halign: Gtk.Align.CENTER,
valign: Gtk.Align.CENTER,
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
@@ -1,69 +0,0 @@
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;
}
-170
View File
@@ -1,170 +0,0 @@
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";
import Pango from "gi://Pango";
function getNotificationImage(notif: AstalNotifd.Notification|HistoryNotification): (string|undefined) {
const img = notif.image || notif.appIcon;
if(!img || !img.includes('/'))
return undefined;
switch(true) {
case /^[/]/.test(img):
return `file://${img}`;
case /^[~]/.test(img):
case /^file:\/\/[~]/i.test(img):
return `file://${GLib.get_home_dir()}/${img.replace(/^(file\:\/\/|[~]|file\:\/\[~])/i, "")}`;
}
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;
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",
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,
singleLineMode: false,
wrapMode: Pango.WrapMode.WORD_CHAR,
label: notification.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);
}
-122
View File
@@ -1,122 +0,0 @@
import { Binding } from "astal";
import { Astal, Gdk, Gtk, Widget } from "astal/gtk3";
import { BackgroundWindow } from "./BackgroundWindow";
type PopupWindowSpecificProps = {
onDestroy?: (self: Widget.Window) => void;
onKeyPressEvent?: (self: Widget.Window, event: Gdk.Event) => void;
onButtonPressEvent?: (self: Gtk.Widget, event: Gdk.Event) => void;
/** Stylesheet for the background of the popup-window */
cssBackgroundWindow?: string;
onClickedOutside?: (self: Widget.Window) => void;
};
export type PopupWindowProps = Pick<Widget.WindowProps,
"child"
| "monitor"
| "css"
| "layer"
| "exclusivity"
| "marginLeft"
| "marginTop"
| "marginRight"
| "marginBottom"
| "expand"
| "cursor"
| "canFocus"
| "hasFocus"
| "tooltipMarkup"
| "namespace"
| "widthRequest"
| "heightRequest"
| "halign"
| "valign"
| "vexpand"
| "hexpand"> & PopupWindowSpecificProps;
const { TOP, LEFT, RIGHT, BOTTOM } = Astal.WindowAnchor;
export function PopupWindow(props: PopupWindowProps): Widget.Window {
props.layer = props.layer ?? Astal.Layer.OVERLAY;
const bgWindow = props.cssBackgroundWindow ? BackgroundWindow({
monitor: props.monitor ?? 0,
layer: props.layer,
css: props.cssBackgroundWindow,
}) : undefined;
const winProps: Widget.WindowProps = {};
for(const key of Object.keys(props).filter(k => k !== "onClickedOutside")) {
// @ts-ignore ignore the `onClickedOutside()` method because astal thinks it's a signal
winProps[key as keyof typeof winProps] = props[key as keyof typeof props];
}
return new Widget.Window({
...winProps,
namespace: props?.namespace ?? "popup-window",
className: `popup-window ${(props.namespace instanceof Binding ?
props.namespace.get() : props.namespace) || ""}`,
keymode: Astal.Keymode.EXCLUSIVE,
anchor: TOP | LEFT | RIGHT | BOTTOM,
exclusivity: props.exclusivity ?? Astal.Exclusivity.NORMAL,
halign: undefined,
valign: undefined,
focusOnMap: true,
widthRequest: undefined,
heightRequest: undefined,
marginTop: undefined,
marginBottom: undefined,
marginLeft: undefined,
marginRight: undefined,
onDestroy: (self) => {
bgWindow?.close();
props.onDestroy?.(self);
},
onButtonPressEvent: (self, event) => {
if((event.get_button()[1] === Gdk.BUTTON_PRIMARY ||
event.get_button()[1] === Gdk.BUTTON_SECONDARY)) {
const [ , x, y ] = event.get_coords();
const allocation = (self.get_child()! as Widget.Box).get_child()!.get_allocation();
if((x < allocation.x || x > (allocation.x + allocation.width)) ||
(y < allocation.y || y > (allocation.y + allocation.height))) {
if(!props.onClickedOutside) {
self.close();
return;
}
props.onClickedOutside?.(self);
}
}
},
onKeyPressEvent: (self, event: Gdk.Event) => {
if(event.get_keyval()[1] === Gdk.KEY_Escape) {
self.close();
return;
}
props.onKeyPressEvent?.(self, event);
},
child: new Widget.Box({
expand: props.expand ?? false,
halign: props.halign,
valign: props.valign,
hexpand: true,
css: `box {
margin-left: ${props.marginLeft ?? 0}px;
margin-right: ${props.marginRight ?? 0}px;
margin-top: ${props.marginTop ?? 0}px;
margin-bottom: ${props.marginBottom ?? 0}px;
}`,
child: new Widget.Box({
onButtonPressEvent: props.onButtonPressEvent ?? (() => true),
widthRequest: props.widthRequest,
heightRequest: props.heightRequest,
child: props.child
} as Widget.BoxProps)
} as Widget.BoxProps)
} as Widget.WindowProps);
}
-57
View File
@@ -1,57 +0,0 @@
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
@@ -1,20 +0,0 @@
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
@@ -1,17 +0,0 @@
import { Gtk, Widget } from "astal/gtk3";
import { getDateTime } from "../../scripts/time";
import { bind, GLib } from "astal";
import { Windows } from "../../windows";
import { Config } from "../../scripts/config";
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) =>
dateTime.format(Config.getDefault().getProperty("clock.date_format", "string") as string))
} as Widget.ButtonProps)
} as Widget.BoxProps);
}
-53
View File
@@ -1,53 +0,0 @@
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(fClient =>
!fClient ? false : (fClient?.initialClass == null ? false : true)),
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 =>
clientClass ?? ""),
label: bind(focusedClient, "class").as(clientClass =>
clientClass ?? "no_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 ?? ""),
label: bind(focusedClient, "title").as(title =>
title ?? "")
} as Widget.LabelProps)
]
})
]: [])
} as Widget.BoxProps);
}
-141
View File
@@ -1,141 +0,0 @@
import { bind, exec } from "astal";
import { Gtk, Widget } from "astal/gtk3";
import AstalMpris from "gi://AstalMpris";
import { getSymbolicIcon } from "../../scripts/apps";
import { Separator, SeparatorProps } from "../Separator";
import { Windows } from "../../windows";
import { Clipboard } from "../../scripts/clipboard";
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",
image: new Widget.Icon({
icon: "edit-paste-symbolic"
} as Widget.IconProps),
tooltipText: "Copy link to Clipboard",
// AstalMpris.Player.metadata works only sometimes, so I'm not using it
visible: bind(players[0], "metadata").as(Boolean),
onClick: async () => {
const link = exec(`playerctl --player=${
players[0].busName.replace(/^org\.mpris\.MediaPlayer2\./i, "")
} metadata xesam:url`);
link && Clipboard.getDefault().copyAsync(link);
}
} as Widget.ButtonProps),
new Widget.Button({
className: "previous",
image: new Widget.Icon({
icon: "media-skip-backward-symbolic"
} as Widget.IconProps),
tooltipText: "Previous",
onClick: () => players[0].canGoPrevious && players[0].previous()
} as Widget.ButtonProps),
new Widget.Button({
className: "play-pause",
tooltipText: bind(players[0], "playback_status").as((status) =>
status === AstalMpris.PlaybackStatus.PLAYING ?
"Pause"
: "Play"),
image: new Widget.Icon({
icon: bind(players[0], "playbackStatus").as((status: AstalMpris.PlaybackStatus) =>
status === AstalMpris.PlaybackStatus.PLAYING ?
"media-playback-pause-symbolic"
: "media-playback-start-symbolic")
} as Widget.IconProps),
onClick: () => players[0].playbackStatus === AstalMpris.PlaybackStatus.PAUSED ?
players[0].play()
: players[0].pause()
} as Widget.ButtonProps),
new Widget.Button({
className: "next",
image: new Widget.Icon({
icon: "media-skip-forward-symbolic"
} as Widget.IconProps),
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.Icon({
icon: bind(players[0], "busName").as((busName: string) => {
const splitName = busName.split('.').filter(str => str !== "" && !str.toLowerCase().includes('instance'));
if (getSymbolicIcon(splitName[splitName.length - 1])) {
return getSymbolicIcon(splitName[splitName.length - 1]);
} else {
return "folder-music-symbolic"
};
})
} as Widget.IconProps),
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;
}
-170
View File
@@ -1,170 +0,0 @@
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 {
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 minutes = Math.floor(startedAtSeconds / 60);
const seconds = Math.floor(startedAtSeconds % 60);
return `${ minutes < 10 ? `0${minutes}` : minutes }:${ seconds < 10 ? `0${seconds}` : seconds }`;
});
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: [
new Widget.Box({
className: "volume-indicators",
spacing: 5,
children: [
volumeStatus({
className: "sink",
endpoint: Wireplumber.getDefault().getDefaultSink(),
icon: bind(Wireplumber.getDefault().getDefaultSink(), "volumeIcon").as(icon =>
!Wireplumber.getDefault().isMutedSink() && Wireplumber.getDefault().getSinkVolume() > 0 ?
icon : "audio-volume-muted-symbolic"),
}),
volumeStatus({
className: "source",
endpoint: Wireplumber.getDefault().getDefaultSource(),
icon: bind(Wireplumber.getDefault().getDefaultSource(), "volumeIcon").as(icon =>
!Wireplumber.getDefault().isMutedSource() && Wireplumber.getDefault().getSourceVolume() > 0 ?
icon : "microphone-sensitivity-muted-symbolic"),
})
]
} as Widget.BoxProps),
new Widget.Revealer({
revealChild: bind(Recording.getDefault(), "recording"),
transitionDuration: 500,
transitionType: Gtk.RevealerTransitionType.SLIDE_LEFT,
onDestroy: () => recordingTimer.drop(),
child: 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.Icon({
className: "recording state",
icon: "media-record-symbolic",
css: "margin-right: 4px;"
} as Widget.IconProps),
new Widget.Label({
className: "rec-time",
label: recordingTimer()
} as Widget.LabelProps)
]
} as Widget.BoxProps)
} as Widget.EventBoxProps)
} as Widget.RevealerProps),
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({
spacing: 2,
children: [
new Widget.Icon({
visible: props.icon,
icon: props.icon,
} as Widget.IconProps),
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 ?
"bluetooth-active-symbolic"
: "bluetooth-symbolic"
) : "bluetooth-disabled-symbolic"
});
const networkIcon: Variable<string> = Variable.derive([
bind(AstalNetwork.get_default(), "primary"),
],
(primary) => {
switch(primary) {
case AstalNetwork.Primary.WIRED: return AstalNetwork.get_default().wired.get_icon_name();
case AstalNetwork.Primary.WIFI: return AstalNetwork.get_default().wifi.get_icon_name();
}
return "network-no-route-symbolic";
});
return new Widget.Box({
className: "status-icons",
spacing: 8,
children: [
new Widget.Icon({
className: "bluetooth state",
visible: bind(AstalBluetooth.get_default(), "adapter").as(Boolean),
icon: bluetoothIcon(),
onDestroy: () => bluetoothIcon.drop()
} as Widget.IconProps),
new Widget.Icon({
className: "network state",
icon: networkIcon(),
onDestroy: () => networkIcon.drop()
} as Widget.IconProps),
new Widget.Box({
children: [
new Widget.Icon({
className: "bell state",
icon: bind(Notifications.getDefault().getNotifd(), "dontDisturb").as((dnd) =>
dnd ? "minus-circle-filled-symbolic"
: "preferences-system-notifications-symbolic")
} as Widget.IconProps),
new Widget.Icon({
className: "notification-count",
visible: bind(Notifications.getDefault(), "history").as(history =>
history.length > 0),
icon: "circle-filled-symbolic"
} as Widget.IconProps)
]
} as Widget.BoxProps)
]
} as Widget.BoxProps);
}
-49
View File
@@ -1,49 +0,0 @@
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
.filter(item => item?.gicon)
.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);
}
-181
View File
@@ -1,181 +0,0 @@
import { bind, Variable } from "astal";
import { Gtk, Widget } from "astal/gtk3";
import AstalHyprland from "gi://AstalHyprland";
import { getAppIcon, getSymbolicIcon } from "../../scripts/apps";
import { Windows } from "../../windows";
import { Config } from "../../scripts/config";
import { Separator, SeparatorProps } from "../Separator";
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.Box({
className: "workspaces-row",
orientation: Gtk.Orientation.HORIZONTAL,
children: [
new Widget.EventBox({
className: "special",
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({
hexpand: true,
child: bind(workspace, "lastClient").as(lastClient =>
new Widget.Icon({
className: "last-app-icon",
halign: Gtk.Align.CENTER,
visible: Variable.derive([
bind(workspace, "lastClient"),
bind(AstalHyprland.get_default(), "focusedWorkspace")
], (lastClient, focusedWorkspace) => focusedWorkspace?.id === workspace.id ?
false : Boolean(lastClient))(),
icon: bind(lastClient, "initialClass").as((initialClass) =>
getSymbolicIcon(initialClass) ?? getAppIcon(initialClass) ??
"application-x-executable-symbolic")
} 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),
Separator({
alpha: .2,
orientation: Gtk.Orientation.HORIZONTAL,
margin: 12,
spacing: 8,
visible: bind(AstalHyprland.get_default(), "workspaces").as(wss =>
wss.filter(ws => ws.id < 0).length > 0)
} as SeparatorProps),
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: () => {
// check if the current widgets is from the only bar
if((Windows.openWindows["bar"] as (Array<Widget.Window>|undefined))?.length === 1) {
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, wsIndex, workspaces) => {
const showIds: Variable<boolean> = Variable.derive([
Config.getDefault().bindProperty("workspaces.always_show_id", "boolean").as(Boolean),
Config.getDefault().bindProperty("workspaces.enable_helper", "boolean").as(Boolean),
showWsNum!()
], (alwaysShowIds, enableHelper, showIds) => {
if(enableHelper && !alwaysShowIds) {
const previousWorkspace = workspaces[wsIndex-1];
const nextWorkspace = workspaces[wsIndex+1];
if((workspaces.filter((_, i) => i < wsIndex).length > 0 &&
previousWorkspace?.id < (workspace.id-1)) ||
(workspaces.filter((_, i) => i > wsIndex).length > 0 &&
nextWorkspace?.id > (workspace.id+1))) {
return true;
}
}
return alwaysShowIds || showIds;
});
const className = Variable.derive([
bind(AstalHyprland.get_default(), "focusedWorkspace"),
showIds!()
], (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: () => {
showIds.drop();
className.drop();
tooltipText.drop();
},
child: new Widget.Box({
hexpand: true,
children: bind(workspace, "lastClient").as((lastClient) => {
const widgets: Array<Gtk.Widget> = [
new Widget.Revealer({
transitionDuration: 200,
transitionType: Gtk.RevealerTransitionType.SLIDE_LEFT,
revealChild: showIds!(),
hexpand: true,
child: new Widget.Label({
label: bind(workspace, "id").as(String),
className: "id",
} as Widget.LabelProps)
} as Widget.RevealerProps),
];
if(lastClient) {
widgets.push(new Widget.Icon({
className: "last-app-icon",
halign: Gtk.Align.CENTER,
expand: true,
visible: bind(AstalHyprland.get_default(), "focusedWorkspace").as(focusedWorkspace =>
workspace.id === focusedWorkspace.id ?
false
: Boolean(lastClient)),
icon: lastClient ?
bind(lastClient, "initialClass").as((clss) =>
getSymbolicIcon(clss) ?? getAppIcon(clss) ?? "application-x-executable-symbolic")
: undefined
} as Widget.IconProps));
}
return widgets;
})
} as Widget.BoxProps)
} as Widget.EventBoxProps);
})
)
} as Widget.BoxProps)
} as Widget.EventBoxProps)
]
} as Widget.BoxProps);
}
-227
View File
@@ -1,227 +0,0 @@
import { AstalIO, bind, Binding, exec, timeout } from "astal";
import { Gtk, Widget } from "astal/gtk3";
import AstalMpris from "gi://AstalMpris";
import { Clipboard } from "../../scripts/clipboard";
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",
image: new Widget.Icon({
icon: "edit-paste-symbolic"
} as Widget.IconProps),
tooltipText: "Copy link to Clipboard",
visible: bind(players[0], "metadata").as(Boolean),
onClick: async () => {
const link = exec(`playerctl --player=${
players[0].busName.replace(/^org\.mpris\.MediaPlayer2\./i, "")
} metadata xesam:url`);
link && Clipboard.getDefault().copyAsync(link);
}
} as Widget.ButtonProps),
new Widget.Button({
className: "shuffle",
visible: bind(players[0], "shuffleStatus").as((shuffleStatus) =>
shuffleStatus !== AstalMpris.Shuffle.UNSUPPORTED),
image: new Widget.Icon({
icon: bind(players[0], "shuffleStatus").as((shuffleStatus) =>
shuffleStatus === AstalMpris.Shuffle.ON ?
"media-playlist-shuffle-symbolic"
: "media-playlist-consecutive-symbolic")
} as Widget.IconProps),
tooltipText: bind(players[0], "shuffleStatus").as((shuffleStatus) =>
shuffleStatus === AstalMpris.Shuffle.ON ?
"Shuffle"
: "No shuffle"),
onClick: () => players[0].shuffle()
} as Widget.ButtonProps),
new Widget.Button({
className: "previous",
image: new Widget.Icon({
icon: "media-skip-backward-symbolic"
} as Widget.IconProps),
tooltipText: "Previous",
onClick: () => players[0].canGoPrevious && players[0].previous()
} as Widget.ButtonProps),
new Widget.Button({
className: "pause",
tooltipText: bind(players[0], "playback_status").as((status) =>
status === AstalMpris.PlaybackStatus.PLAYING ? "Pause" : "Play"),
image: new Widget.Icon({
icon: bind(players[0], "playbackStatus").as((status) =>
status === AstalMpris.PlaybackStatus.PLAYING ?
"media-playback-pause-symbolic"
: "media-playback-start-symbolic"),
} as Widget.IconProps),
onClick: () => players[0].playbackStatus === AstalMpris.PlaybackStatus.PAUSED ?
players[0].play()
: players[0].pause()
} as Widget.ButtonProps),
new Widget.Button({
className: "next",
image: new Widget.Icon({
icon: "media-skip-forward-symbolic"
} as Widget.IconProps),
tooltipText: "Next",
onClick: () => players[0].canGoNext && players[0].next()
} as Widget.ButtonProps),
new Widget.Button({
className: "repeat",
visible: bind(players[0], "loopStatus").as((loopStatus) =>
loopStatus !== AstalMpris.Loop.UNSUPPORTED),
image: new Widget.Icon({
icon: bind(players[0], "loopStatus").as((loopStatus) => {
switch(loopStatus) {
case AstalMpris.Loop.TRACK:
return "media-playlist-repeat-song-symbolic";
case AstalMpris.Loop.PLAYLIST:
return "media-playlist-repeat-symbolic";
}
return "loop-arrow-symbolic";
})
} as Widget.IconProps),
tooltipText: bind(players[0], "loopStatus").as((loopStatus) => {
switch(loopStatus) {
case AstalMpris.Loop.TRACK:
return "Loop song";
case AstalMpris.Loop.PLAYLIST:
return "Loop playlist";
}
return "No 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 && Number.isFinite(len)) ?
`${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
@@ -1,46 +0,0 @@
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
@@ -1,65 +0,0 @@
import { bind } from "astal";
import { Gtk, Widget } from "astal/gtk3";
import { HistoryNotification, Notifications } from "../../scripts/notifications";
import { NotificationWidget } from "../Notification";
import { tr } from "../../i18n/intl";
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.Icon({
css: "margin-right: 6px;",
icon: "edit-clear-all-symbolic"
} as Widget.IconProps),
new Widget.Label({
label: tr("clear")
} as Widget.LabelProps)
]
} as Widget.BoxProps),
onClick: () => Notifications.getDefault().clearHistory(),
} as Widget.ButtonProps)
]
})
]
} as Widget.BoxProps);
}
-84
View File
@@ -1,84 +0,0 @@
import { register, timeout } from "astal";
import { Gtk, Widget } from "astal/gtk3";
import { Page } from "./pages/Page";
export { Pages };
export type PagesProps = {
initialPage?: Page;
className?: string;
transitionDuration?: number;
};
@register({ GTypeName: "Pages" })
class Pages extends Widget.Box {
#page: (Page|undefined);
#transDuration: number;
#transType: Gtk.RevealerTransitionType = Gtk.RevealerTransitionType.SLIDE_DOWN;
get isOpen() { return (this.get_children().length > 0); }
constructor(props?: PagesProps) {
super({
className: props?.className,
orientation: Gtk.Orientation.VERTICAL
});
this.name = "pages";
if(props?.className !== null && props?.className !== undefined)
this.className = props?.className;
this.#transDuration = props?.transitionDuration ?? 280;
if(props?.initialPage)
this.open(props.initialPage);
}
toggle(newPage?: Page, onToggled?: () => void): void {
if(!newPage || (this.#page?.id === newPage?.id)) {
this.close(onToggled);
return;
}
if(!this.isOpen) {
newPage && this.open(newPage, onToggled);
return;
}
if(this.#page?.id !== newPage.id) {
this.close();
this.open(newPage, onToggled);
}
}
open(newPage: Page, onOpened?: () => void) {
this.add(new Widget.Revealer({
transitionDuration: this.#transDuration,
transitionType: this.#transType,
revealChild: false,
child: newPage
} as Widget.RevealerProps));
this.#page = newPage;
this.reorder_child(this.get_children()[this.get_children().length - 1], 0);
(this.get_children()[0] as Widget.Revealer).set_reveal_child(true);
onOpened?.();
}
close(onClosed?: () => void): void {
(this.get_children() as Array<Widget.Revealer>).forEach((pageRevealer, i, pageRevealers) => {
pageRevealer.set_reveal_child(false);
if(this.#page?.id === (pageRevealer.get_child() as Page).id)
this.#page = undefined;
timeout(this.#transDuration, () => {
this.remove(pageRevealer);
pageRevealer.destroy();
i === (pageRevealers.length - 1) &&
onClosed?.();
});
});
}
}
-118
View File
@@ -1,118 +0,0 @@
import { exec, GLib, Variable } from "astal";
import { Gtk, Widget } from "astal/gtk3";
import { Windows } from "../../windows";
import { Wallpaper } from "../../scripts/wallpaper";
import { execApp } from "../../scripts/apps";
function LockButton(): Widget.Button {
return new Widget.Button({
image: new Widget.Icon({
icon: "system-lock-screen-symbolic"
} as Widget.IconProps),
onClick: () => {
Windows.close("control-center");
execApp("hyprlock");
}
} as Widget.ButtonProps)
}
function ColorPickerButton(): Widget.Button {
return new Widget.Button({
image: new Widget.Icon({
icon: "color-select-symbolic"
} as Widget.IconProps),
onClick: () => {
Windows.close("control-center");
execApp("sh $HOME/.config/hypr/scripts/color-picker.sh");
}
} as Widget.ButtonProps)
}
function ScreenshotButton(): Widget.Button {
return new Widget.Button({
image: new Widget.Icon({
icon: "applets-screenshooter-symbolic"
} as Widget.IconProps),
onClick: () => {
Windows.close("control-center");
execApp(`sh ${GLib.get_user_config_dir()}/hypr/scripts/screenshot.sh`);
}
} as Widget.ButtonProps);
}
function SelectWallpaperButton(): Widget.Button {
return new Widget.Button({
image: new Widget.Icon({
icon: "preferences-desktop-wallpaper-symbolic"
} as Widget.IconProps),
onClick: () => {
Windows.close("control-center");
Wallpaper.getDefault().pickWallpaper();
}
} as Widget.ButtonProps);
}
function LogoutButton(): Widget.Button {
return new Widget.Button({
image: new Widget.Icon({
icon: "system-shutdown-symbolic"
} as Widget.IconProps),
onClick: () => {
Windows.close("control-center");
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.Box({
children: [
new Widget.Icon({
icon: "hourglass-symbolic"
} as Widget.IconProps),
new Widget.Label({
className: "uptime",
xalign: 0,
tooltipText: "Uptime",
onDestroy: () => uptime.drop(),
label: uptime()
} as Widget.LabelProps)
]
} as Widget.BoxProps)
]
} 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);
}
-76
View File
@@ -1,76 +0,0 @@
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,
spacing: 10,
children: [
new Widget.Box({
className: "sink speaker",
spacing: 3,
children: bind(Wireplumber.getWireplumber(), "defaultSpeaker").as((sink) => [
new Widget.Button({
onClick: () => Wireplumber.getDefault().toggleMuteSink(),
image: new Widget.Icon ({
icon: bind(sink, "volumeIcon").as((icon) =>
!Wireplumber.getDefault().isMutedSink() && Wireplumber.getDefault().getSinkVolume() > 0 ? icon : "audio-volume-muted-symbolic"),
} as Widget.IconProps),
} 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",
spacing: 3,
children: bind(Wireplumber.getWireplumber(), "defaultMicrophone").as((source) => [
new Widget.Button({
onClick: () => Wireplumber.getDefault().toggleMuteSource(),
image: new Widget.Icon ({
icon: bind(source, "volumeIcon").as((icon) =>
!Wireplumber.getDefault().isMutedSource() && Wireplumber.getDefault().getSourceVolume() > 0 ? icon : "microphone-sensitivity-muted-symbolic"),
} as Widget.IconProps),
} 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
@@ -1,63 +0,0 @@
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);
}
@@ -1,210 +0,0 @@
import { bind, Gio, Variable } from "astal";
import { Gtk, Widget } from "astal/gtk3";
import AstalBluetooth from "gi://AstalBluetooth";
import { Page, PageButton } from "./Page";
import { tr } from "../../../i18n/intl";
import { Windows } from "../../../windows";
import { Notifications } from "../../../scripts/notifications";
import AstalNotifd from "gi://AstalNotifd";
import { execApp } from "../../../scripts/apps";
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",
image: new Widget.Icon({
icon: bind(AstalBluetooth.get_default().adapter, "discovering").as((discovering) =>
!discovering ?
"arrow-circular-top-right-symbolic"
: "media-playback-stop-symbolic")
} as Widget.IconProps),
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) {
AstalBluetooth.get_default().adapter.stop_discovery();
return;
}
AstalBluetooth.get_default().adapter.start_discovery();
}
} as Widget.ButtonProps)
],
onClose: () => AstalBluetooth.get_default().adapter.discovering &&
AstalBluetooth.get_default().adapter.stop_discovery(),
bottomButtons: [{
title: tr("control_center.pages.more_settings"),
onClick: () => {
Windows.close("control-center");
execApp("overskride", "[float; animation slide right]");
}
}],
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,
spacing: 2,
visible: bind(AstalBluetooth.get_default(), "devices").as((devs) =>
devs.filter(dev => dev.paired || dev.connected || dev.trusted).length > 0),
children: bind(AstalBluetooth.get_default(), "devices").as((devs) => {
const connectedDevices = devs.filter((dev) => dev.connected || dev.paired || dev.trusted)
return [
new Widget.Label({
className: "sub-header",
label: tr("devices"),
xalign: 0,
} as Widget.LabelProps),
...connectedDevices.map((dev) => DeviceWidget(dev))
]
})
} as Widget.BoxProps),
new Widget.Box({
className: "discovered",
orientation: Gtk.Orientation.VERTICAL,
spacing: 2,
visible: bind(AstalBluetooth.get_default(), "devices").as((devs) =>
devs.filter((dev) => !dev.connected && !dev.paired && !dev.trusted).length > 0),
children: bind(AstalBluetooth.get_default(), "devices").as((devices) => {
const discoveredDevices = devices.filter((dev) => !dev.connected && !dev.paired && !dev.trusted);
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)
]
} as Widget.BoxProps)
]
});
function DeviceWidget(dev: AstalBluetooth.Device): Gtk.Widget {
const devActions: Variable<Array<Widget.Button>> = Variable.derive([
bind(dev, "connected"),
bind(dev, "paired"),
bind(dev, "trusted")
], (connected, paired, trusted) => paired ? [
new Widget.Button({
image: new Widget.Icon({
icon: connected ?
"list-remove-symbolic"
: "user-trash-symbolic"
} as Widget.IconProps),
tooltipText: tr(connected ? "disconnect" : "control_center.pages.bluetooth.unpair_device"),
onClick: () => {
if(!connected) {
AstalBluetooth.get_default().adapter?.remove_device(dev);
return;
}
dev.disconnect_device(null);
},
} as Widget.ButtonProps),
new Widget.Button({
image: new Widget.Icon({
icon: trusted ?
"shield-safe-symbolic"
: "shield-danger-symbolic"
} as Widget.IconProps),
tooltipText: tr(`control_center.pages.bluetooth.${trusted ? "un": ""}trust_device`),
onClick: () => dev.set_trusted(!trusted)
} as Widget.ButtonProps)
] : []);
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",
description: bind(dev, "connecting").as(connecting =>
connecting ? `${tr("connecting")}...` : ""),
tooltipText: bind(dev, "connected").as(connected => !connected ?
tr("connect")
: ""),
onDestroy: () => devActions.drop(),
onClick: () => {
if(dev.connected) return;
let skipConnection: boolean = false;
if(!dev.paired)
(async () => dev.pair())().catch((err: Gio.IOErrorEnum) => {
skipConnection = true;
Notifications.getDefault().sendNotification({
appName: "bluetooth",
summary: "Device pairing error",
body: `Couldn't connect to ${dev.alias ?? dev.name}, an error occurred: ${err.message || err.stack}`,
urgency: AstalNotifd.Urgency.NORMAL
})
}).then(() => dev.set_trusted(true));
if(!skipConnection)
(async () => dev.connect_device(null))().catch((err: Gio.IOErrorEnum) =>
Notifications.getDefault().sendNotification({
appName: "bluetooth",
summary: "Device connection error",
body: `Couldn't connect to ${dev.alias ?? dev.name}, an error occurred: ${err.message || err.stack}`,
urgency: AstalNotifd.Urgency.NORMAL
})
);
},
endWidget: new Widget.Box({
visible: bind(dev, "batteryPercentage").as((batt: number) =>
batt <= -1 ? false : true),
children: [
new Widget.Box({
visible: bind(dev, "connected"),
children: [
new Widget.Label({
halign: Gtk.Align.END,
label: bind(dev, "batteryPercentage").as((batt: number) =>
`${Math.floor(batt * 100)}%`)
} as Widget.LabelProps),
new Widget.Icon({
icon: bind(dev, "batteryPercentage").as(batt =>
`battery-level-${Math.floor(batt * 100)}-symbolic`),
css: "font-size: 16px; margin-left: 6px;"
} as Widget.IconProps)
]
} as Widget.BoxProps)
]
} as Widget.BoxProps),
extraButtons: devActions()
});
}
@@ -1,35 +0,0 @@
import { bind } from "astal";
import { Page, PageButton, PageProps } from "./Page";
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().get_audio()!, "microphones").as((microphones) => [
new Widget.Label({
className: "sub-header",
label: tr("devices"),
xalign: 0
} as Widget.LabelProps),
...microphones.map((microphone) =>
PageButton({
className: bind(microphone, "isDefault").as(isDefault => isDefault ? "default" : ""),
icon: bind(microphone, "icon").as(icon =>
Astal.Icon.lookup_icon(icon) ? icon : "audio-input-microphone-symbolic"),
title: bind(microphone, "description").as(desc => desc ?? "Microphone"),
onClick: () => microphone.set_is_default(true),
endWidget: new Widget.Icon({
icon: "object-select-symbolic",
visible: bind(microphone, "isDefault"),
css: "font-size: 18px;"
} as Widget.IconProps)
})
)
])
} as PageProps);
}
-210
View File
@@ -1,210 +0,0 @@
import { Gtk, Widget } from "astal/gtk3";
import { Page, PageButton } from "./Page";
import AstalNetwork from "gi://AstalNetwork";
import { bind, GLib } from "astal";
import NM from "gi://NM";
import { Windows } from "../../../windows";
import { tr } from "../../../i18n/intl";
import { execApp } from "../../../scripts/apps";
import { EntryPopup, EntryPopupProps } from "../../EntryPopup";
import { Notifications } from "../../../scripts/notifications";
import { AskPopup, AskPopupProps } from "../../AskPopup";
import { encoder } from "../../../scripts/utils";
export const PageNetwork: (() => Page) = () => new Page({
id: "network",
title: tr("control_center.pages.network.title"),
className: "network",
headerButtons: [
new Widget.Button({
className: "reload",
image: new Widget.Icon({
icon: "arrow-circular-top-right-symbolic"
} as Widget.IconProps),
visible: bind(AstalNetwork.get_default(), "primary").as((primary) =>
primary === AstalNetwork.Primary.WIFI),
tooltipText: "Re-scan connections",
onClick: () => AstalNetwork.get_default().wifi.scan()
} as Widget.ButtonProps)
],
bottomButtons: [{
title: tr("control_center.pages.more_settings"),
onClick: () => {
Windows.close("control-center");
execApp("nm-connection-editor", "[animationstyle gnomed]");
}
}],
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");
execApp(
`nm-connection-editor --edit ${dev.activeConnection?.connection.get_uuid()}`,
"[animationstyle gnomed; float]"
);
}
} 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, "accessPoints").as((aps) => [
new Widget.Label({
className: "sub-header",
label: "Wi-Fi"
} as Widget.LabelProps),
...aps.filter(ap => ap.ssid).map(ap => PageButton({
className: bind(AstalNetwork.get_default().wifi, "activeAccessPoint").as(activeAP =>
activeAP.ssid === ap.ssid ? "active" : ""),
title: bind(ap, "ssid").as(ssid =>
ssid ?? "Unknown SSID"),
icon: bind(ap, "iconName"),
endWidget: new Widget.Icon({
// @ts-ignore ts-for-gir generated the types wrong
icon: bind(ap, "flags").as(flags => flags & NM["80211ApFlags" as keyof typeof NM].PRIVACY ?
"channel-secure-symbolic"
: "channel-insecure-symbolic"),
css: "font-size: 18px;"
} as Widget.IconProps),
extraButtons: [
new Widget.Button({
image: new Widget.Icon({
icon: "window-close-symbolic",
css: "font-size: 18px;"
} as Widget.IconProps)
} as Widget.ButtonProps)
],
onClick: () => {
const ssid: string = ap.ssid ?? "Unknown SSID",
ssidBytes = GLib.Bytes.new(encoder.encode(ssid));
const connection = new NM.Connection();
const setting = NM.SettingWireless.new();
setting.ssid = ssidBytes;
setting.bssid = ap.bssid;
connection.add_setting(setting);
// @ts-ignore same as previous, type gen issues
// Check if access point has encryption(needs a password)
if(ap.flags & NM["80211ApFlags" as keyof typeof NM].PRIVACY) {
const passwdPopup = EntryPopup({
isPassword: true,
title: `${tr("connect")}: ${ssid}`,
acceptText: tr("connect"),
closeOnAccept: false,
text: `Input password for ${ssid}`,
onAccept: (input) => {
const pskSetting = NM.SettingWirelessSecurity.new();
pskSetting.keyMgmt = "wpa-psk";
// @ts-ignore type gen issues (the type exists)
if(ap.flags & NM["80211ApSecurityFlags" as keyof typeof NM].KEY_MGMT_SAE)
pskSetting.keyMgmt = "sae";
pskSetting.psk = input;
AstalNetwork.get_default().get_client().add_connection_async(
connection, true, null, (client, asyncRes) => {
const remoteConnection = client!.add_connection_finish(asyncRes);
if(!remoteConnection) {
notifyConnectionError(ssid);
return;
}
passwdPopup.close();
saveToDisk(remoteConnection, ssid);
}
);
},
} as EntryPopupProps);
return;
}
AstalNetwork.get_default().get_client().add_connection_async(connection, false, null, (_, asyncRes) => {
const remoteConnection = AstalNetwork.get_default().get_client().add_connection_finish(asyncRes);
if(!remoteConnection) {
notifyConnectionError(ssid);
return;
}
activateWirelessConnection(remoteConnection, ssid);
});
}
}))
]
) : [],
} as Widget.BoxProps)
]
});
function activateWirelessConnection(connection: NM.RemoteConnection, ssid: string): void {
AstalNetwork.get_default().get_client().activate_connection_async(
connection, AstalNetwork.get_default().wifi.get_device(), null, null, (_, asyncRes) => {
const activeConnection = AstalNetwork.get_default().get_client().activate_connection_finish(asyncRes);
if(!activeConnection) {
Notifications.getDefault().sendNotification({
appName: "network",
summary: "Couldn't activate wireless connection",
body: `An error occurred while activating the wireless connection "${ssid}"`
});
return;
}
}
);
}
function notifyConnectionError(ssid: string): void {
Notifications.getDefault().sendNotification({
appName: "network",
summary: "Coudn't connect Wi-Fi",
body: `An error occurred while trying to connect to the "${ssid}" access point. \nMaybe the password is invalid?`
});
}
function saveToDisk(remoteConnection: NM.RemoteConnection, ssid: string): void {
AskPopup({
text: `Save password for connection "${ssid}"?`,
acceptText: "Yes",
onAccept: () => remoteConnection.commit_changes_async(true, null, (_, asyncRes) =>
!remoteConnection.commit_changes_finish(asyncRes) && Notifications.getDefault().sendNotification({
appName: "network",
summary: "Couldn't save Wi-Fi password",
body: `An error occurred while trying to write the password for "${ssid}" to disk`
}))
} as AskPopupProps);
}
@@ -1,51 +0,0 @@
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: 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: 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);
-257
View File
@@ -1,257 +0,0 @@
import { Binding, register } from "astal";
import { Gtk, Widget } from "astal/gtk3";
import { Separator, SeparatorProps } from "../../Separator";
export type PageProps = {
setup?: () => void;
onClose?: () => void;
id: string;
className?: string | Binding<string>;
title: string | Binding<string>;
description?: string | Binding<string>;
headerButtons?: Array<Gtk.Button> | Binding<Array<Gtk.Button>>;
bottomButtons?: Array<BottomButton> | Binding<Array<BottomButton>>;
orientation?: Gtk.Orientation | Binding<Gtk.Orientation>;
spacing?: number;
child?: Gtk.Widget | Binding<Gtk.Widget>;
children?: Array<Gtk.Widget> | Binding<Array<Gtk.Widget>>;
};
export type BottomButton = {
title: string | Binding<string>;
description?: string | Binding<string>;
tooltipText?: string | Binding<string>;
tooltipMarkup?: string | Binding<string>;
onClick?: () => void;
};
export { Page };
@register({ GTypeName: "Page" })
class Page extends Widget.Box {
readonly #id: string | number;
readonly bottomButtons?: Array<BottomButton>;
#title: string | Binding<string>;
#description?: string | Binding<string>;
public get title() { return this.#title; }
public get description() { return this.#description; }
public get id() { return this.#id; }
public onClose?: () => void;
constructor(props: PageProps) {
super({
hexpand: true,
orientation: Gtk.Orientation.VERTICAL,
className: (props.className instanceof Binding) ?
props.className.as((clsName) => `page ${ clsName ?? "" }`)
: `page ${props.className ?? ""}`,
setup: props.setup,
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),
Separator({
alpha: .2,
spacing: 6,
orientation: Gtk.Orientation.VERTICAL,
visible: (props.bottomButtons instanceof Binding) ?
props.bottomButtons.as(buttons => buttons.length > 0)
: (!props.bottomButtons ? false : props.bottomButtons.length > 0)
} as SeparatorProps),
new Widget.Box({
className: "bottom-buttons",
orientation: Gtk.Orientation.VERTICAL,
visible: (props.bottomButtons instanceof Binding) ?
props.bottomButtons.as(buttons => buttons.length > 0)
: (!props.bottomButtons ? false : props.bottomButtons.length > 0),
spacing: 2,
children: (props.bottomButtons instanceof Binding) ?
props.bottomButtons.as(buttons => buttons.map(button =>
new Widget.Button({
onClicked: button.onClick,
tooltipMarkup: button.tooltipMarkup,
tooltipText: button.tooltipText,
child: new Widget.Box({
orientation: Gtk.Orientation.VERTICAL,
children: [
new Widget.Label({
className: "title",
label: button.title,
xalign: 0
} as Widget.LabelProps),
new Widget.Label({
className: "description",
label: button.description,
visible: Boolean(button.description),
xalign: 0
} as Widget.LabelProps)
]
} as Widget.BoxProps)
} as Widget.ButtonProps)
)
)
: (!props.bottomButtons ? [] : props.bottomButtons.map(button =>
new Widget.Button({
onClicked: button.onClick,
tooltipMarkup: button.tooltipMarkup,
tooltipText: button.tooltipText,
child: new Widget.Box({
orientation: Gtk.Orientation.VERTICAL,
children: [
new Widget.Label({
className: "title",
label: button.title,
xalign: 0
} as Widget.LabelProps),
new Widget.Label({
className: "description",
label: button.description,
visible: Boolean(button.description),
xalign: 0
} as Widget.LabelProps)
]
} as Widget.BoxProps)
} as Widget.ButtonProps)
))
} as Widget.BoxProps)
]
});
this.#id = props.id;
this.#title = props.title;
this.#description = props.description;
this.onClose = props.onClose;
}
}
export function PageButton({ onDestroy, ...props }: {
className?: string | Binding<string>;
icon?: string | Binding<string>;
title: string | Binding<string>;
endWidget?: Gtk.Widget | Binding<Gtk.Widget>;
description?: string | Binding<string>;
extraButtons?: Array<Widget.Button> | Binding<Array<Gtk.Widget>>;
onDestroy?: (self: Widget.Box) => void;
onClick?: (self: Widget.Button) => void;
tooltipText?: string | Binding<string>;
tooltipMarkup?: string | Binding<string>;
}): Gtk.Widget {
return new Widget.Box({
onDestroy,
children: [
new Widget.Button({
onClick: props.onClick,
className: props.className,
hexpand: true,
tooltipText: props.tooltipText,
tooltipMarkup: props.tooltipMarkup,
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,
hexpand: false,
css: "font-size: 20px; margin-right: 6px;"
} as Widget.IconProps),
new Widget.Box({
orientation: Gtk.Orientation.VERTICAL,
hexpand: true,
vexpand: false,
children: [
new Widget.Label({
className: "title",
xalign: 0,
// truncating is not working, so I had to do this
label: (props.title instanceof Binding) ?
props.title.as((title) =>
`${title.substring(0, 35)}${
title.length > 35 ? '…' : ""}`)
: `${props.title.substring(0, 35)}${
props.title.length > 35 ? '…' : ""}`,
tooltipText: props.title,
truncate: true,
} as Widget.LabelProps),
new Widget.Label({
className: "description",
xalign: 0,
visible: (props.description instanceof Binding) ?
props.description.as(Boolean)
: Boolean(props.description),
label: props.description,
truncate: true,
tooltipText: props.description
} as Widget.LabelProps)
]
} as Widget.BoxProps),
new Widget.Box({
visible: (props.endWidget instanceof Binding) ?
props.endWidget.as(Boolean)
: props.endWidget,
halign: Gtk.Align.END,
child: props.endWidget
} as Widget.BoxProps)
]
} as Widget.BoxProps)
} as Widget.ButtonProps),
new Widget.Box({
className: "extra-buttons button-row",
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);
}
-96
View File
@@ -1,96 +0,0 @@
import { Page, PageButton, PageProps } from "./Page";
import { bind, Variable } from "astal";
import { Astal, Gtk, Widget } from "astal/gtk3";
import { getAppIcon } from "../../../scripts/apps";
import { Wireplumber } from "../../../scripts/volume";
import { tr } from "../../../i18n/intl";
export function PageSound(): Page {
const endpoints = Variable.derive([
bind(Wireplumber.getWireplumber().get_audio()!, "speakers"),
bind(Wireplumber.getWireplumber().get_audio()!, "streams")
]);
return new Page({
id: "sound",
title: tr("control_center.pages.sound.title"),
description: tr("control_center.pages.sound.description"),
onClose: endpoints.drop,
children: endpoints(([speakers, streams]) => [
new Widget.Label({
className: "sub-header",
label: tr("devices"),
xalign: 0
} as Widget.LabelProps),
...speakers.map((speaker) =>
PageButton({
className: bind(speaker, "isDefault").as(isDefault => isDefault ? "default" : ""),
icon: bind(speaker, "icon").as(icon =>
Astal.Icon.lookup_icon(icon)? icon : "audio-card-symbolic"),
title: bind(speaker, "description").as(desc => desc ?? "Speaker"),
onClick: () => speaker.set_is_default(true),
endWidget: new Widget.Icon({
icon: "object-select-symbolic",
visible: bind(speaker, "isDefault"),
css: "font-size: 18px;"
} as Widget.IconProps)
})
),
new Widget.Label({
className: "sub-header",
label: tr("apps"),
visible: streams.length > 0,
xalign: 0
} as Widget.LabelProps),
...streams.map((stream) =>
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: bind(stream, "name").as(name =>
getAppIcon(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: bind(stream, "name").as(name => name || "Unknown"),
truncate: true,
tooltipText: bind(stream, "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(stream.volume * 100),
value: bind(stream, "volume").as((vol) => Math.floor(vol * 100)),
onDragged: (self) => stream.volume = self.value / 100
} as Widget.SliderProps)
]
} as Widget.BoxProps)
]
} as Widget.BoxProps))
}
} as Widget.EventBoxProps)
)
])
} as PageProps);
}
@@ -1,35 +0,0 @@
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 = () => {
const icon: Variable<string> = Variable.derive([
bind(AstalBluetooth.get_default(), "isPowered"),
bind(AstalBluetooth.get_default(), "isConnected")
],
(powered: boolean, isConnected: boolean) =>
powered ? ( isConnected ?
"bluetooth-active-symbolic"
: "bluetooth-symbolic"
) : "bluetooth-disabled-symbolic"
);
return 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() : ""
}),
onDestroy: () => icon.drop(),
onToggledOn: () => AstalBluetooth.get_default().adapter?.set_powered(true),
onToggledOff: () => AstalBluetooth.get_default().adapter?.set_powered(false),
onClickMore: () => TilesPages?.toggle(BluetoothPage()),
enableOnClickMore: true,
icon: icon(),
iconSize: 16,
toggleState: bind(AstalBluetooth.get_default(), "isPowered")
} as TileProps)();
}
@@ -1,15 +0,0 @@
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: "minus-circle-filled-symbolic",
iconSize: 16,
toggleState: Notifications.getDefault().getNotifd().dontDisturb
});
@@ -1,86 +0,0 @@
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: "network-wireless-signal-excellent-symbolic",
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 "network-wired-symbolic";
case AstalNetwork.Internet.DISCONNECTED:
return "network-wired-disconnected-symbolic";
}
return "network-wired-no-route-symbolic";
}),
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: "network-wired-disconnected-symbolic",
iconSize: 16,
toggleState: bind(wired, "internet").as((internet: AstalNetwork.Internet) =>
internet === AstalNetwork.Internet.CONNECTING || internet === AstalNetwork.Internet.CONNECTED)
} as TileProps)();
})()
} as Widget.BoxProps);
@@ -1,26 +0,0 @@
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";
import { isInstalled } from "../../../scripts/utils";
import { Widget } from "astal/gtk3";
export const TileNightLight = () => isInstalled("hyprsunset") ? Tile({
title: tr("control_center.tiles.night_light.title"),
icon: "weather-clear-night-symbolic",
description: Variable.derive([
bind(NightLight.getDefault(), "temperature"),
bind(NightLight.getDefault(), "gamma")
], (temp, gamma) => `${temp === NightLight.getDefault().identityTemperature ?
tr("control_center.tiles.night_light.default_desc") : `${temp}K`} ${
gamma < NightLight.getDefault().maxGamma ? `(${gamma}%)` : ""}`
)(),
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)()
: new Widget.Box({ visible: false } as Widget.BoxProps);
@@ -1,38 +0,0 @@
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";
import { isInstalled } from "../../../scripts/utils";
const wfRecorderInstalled = isInstalled("wf-recorder");
export const TileRecording = () => {
const description: Variable<string> = 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 minutes = Math.floor(startedAtSeconds / 60);
const seconds = Math.floor(startedAtSeconds % 60);
return `${ minutes < 10 ? `0${minutes}` : minutes }:${ seconds < 10 ? `0${seconds}` : seconds }`;
});
return Tile({
title: tr("control_center.tiles.recording.title") || "Screen Recording",
description: description(),
icon: "media-record-symbolic",
visible: wfRecorderInstalled,
onDestroy: () => description.drop(),
onToggledOff: () => Recording.getDefault().stopRecording(),
onToggledOn: () => Recording.getDefault().startRecording(),
toggleState: bind(Recording.getDefault(), "recording"),
iconSize: 16
} as TileProps)();
}
-129
View File
@@ -1,129 +0,0 @@
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 subs: Array<() => void> = [];
const toggled = new Variable<boolean>(((props.toggleState instanceof Binding) ?
props.toggleState.get()
: props.toggleState) ?? false);
if(props?.toggleState instanceof Binding)
subs.push(props.toggleState.subscribe((state) =>
toggled.set(state ?? 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: () => {
subs.map(sub => sub?.());
props.onDestroy?.();
},
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.Icon({
className: "icon",
icon: props.icon,
visible: (props.icon instanceof Binding) ?
props.icon.as(Boolean)
: Boolean(props.icon),
css: `font-size: ${props.iconSize ?? 16}px;`
} as Widget.IconProps),
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
@@ -1,78 +0,0 @@
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));
}
}
-166
View File
@@ -1,166 +0,0 @@
import { GObject, Variable } from "astal";
import { Astal, Gdk, Gtk, Widget } from "astal/gtk3";
import { execApp, getAppIcon, getApps, getAstalApps } from "../scripts/apps";
import AstalApps from "gi://AstalApps";
import { PopupWindow } from "../widget/PopupWindow";
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: 60,
columnSpacing: 60,
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);
flowbox.get_children().map(flowboxChild =>
flowbox.remove(flowboxChild));
results.map(app => {
flowbox.insert(AppWidget(app), -1);
const child = flowbox.get_child_at_index(flowbox.get_children().length - 1);
child?.set_valign(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: true,
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 = () => {
execApp(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,
monitor: mon,
marginTop: 64,
cssBackgroundWindow: "background: rgba(0, 0, 0, .2)",
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;
}
-62
View File
@@ -1,62 +0,0 @@
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";
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(),
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);
}
-68
View File
@@ -1,68 +0,0 @@
import { 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,
halign: Gtk.Align.CENTER,
valign: Gtk.Align.START,
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,
halign: Gtk.Align.CENTER,
valign: Gtk.Align.CENTER,
vexpand: true,
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,
spacing: 8,
alpha: .3,
visible: bind(AstalMpris.get_default(), "players").as(players => players.length > 0),
} as SeparatorProps),
BigMedia()
]
} as Widget.BoxProps)
} as PopupWindowProps);
-37
View File
@@ -1,37 +0,0 @@
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, PopupWindowProps } from "../widget/PopupWindow";
export const ControlCenter = (mon: number) => PopupWindow({
namespace: "control-center",
className: "control-center",
halign: Gtk.Align.END,
valign: Gtk.Align.START,
layer: Astal.Layer.OVERLAY,
marginTop: 10,
marginRight: 10,
marginBottom: 10,
monitor: mon,
widthRequest: 395,
child: new Widget.Box({
orientation: Gtk.Orientation.VERTICAL,
spacing: 16,
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 PopupWindowProps);
-30
View File
@@ -1,30 +0,0 @@
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,
spacing: 12,
visible: bind(Notifications.getDefault(), "notifications").as(notifs => notifs.length > 0),
children: bind(Notifications.getDefault(), "notifications").as((notifs) =>
notifs.map((item) => new Widget.Box({
className: "float-notification",
child: NotificationWidget(item,
() => Notifications.getDefault().removeNotification(item),
false, true)
} as Widget.BoxProps))
),
} as Widget.BoxProps)
} as Widget.WindowProps);
-155
View File
@@ -1,155 +0,0 @@
import { Astal, Gdk, Gtk, Widget } from "astal/gtk3";
import { getDateTime } from "../scripts/time";
import { execAsync, Gio, GLib } from "astal";
import { AskPopup, AskPopupProps } from "../widget/AskPopup";
import { Windows } from "../windows";
import { Notifications } from "../scripts/notifications";
import AstalNotifd from "gi://AstalNotifd";
import { NightLight } from "../scripts/nightlight";
import { Config } from "../scripts/config";
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",
image: new Widget.Icon({
icon: "system-shutdown-symbolic"
} as Widget.IconProps),
onClick: () => AskPopup(poweroffAsk),
onActivate: () => AskPopup(poweroffAsk)
} as Widget.ButtonProps),
new Widget.Button({
className: "reboot",
image: new Widget.Icon({
icon: "arrow-circular-top-right-symbolic"
} as Widget.IconProps),
onClick: () => AskPopup(rebootAsk),
onActivate: () => AskPopup(rebootAsk)
} as Widget.ButtonProps),
new Widget.Button({
className: "suspend",
image: new Widget.Icon({
icon: "weather-clear-night-symbolic"
} as Widget.IconProps),
onClick: () => AskPopup(suspendAsk),
onActivate: () => AskPopup(suspendAsk)
} as Widget.ButtonProps),
new Widget.Button({
className: "logout",
image: new Widget.Icon({
icon: "system-log-out-symbolic"
} as Widget.IconProps),
onClick: () => AskPopup(logoutAsk),
onActivate: () => AskPopup(logoutAsk)
} as Widget.ButtonProps),
]
} as Widget.BoxProps)
]
})
} as Widget.EventBoxProps)
} as Widget.WindowProps);
const logoutAsk: AskPopupProps = {
title: "Log out",
text: "Are you sure you want to log out? Your session will be ended.",
onAccept: () => {
Config.getDefault().getProperty("night_light.save_on_shutdown", "boolean") &&
NightLight.getDefault().saveData();
execAsync(`hyprctl dispatch exit`).catch((err: Gio.IOErrorEnum) =>
Notifications.getDefault().sendNotification({
appName: "colorshell",
summary: "Couldn't exit Hyprland",
body: `An error occurred and colorshell couldn't exit Hyprland. Stderr: \n${
err.message ? `${err.message}\n` : ""}${err.stack}`,
urgency: AstalNotifd.Urgency.NORMAL,
actions: [{
text: "Report Issue on colorshell",
onAction: () => execAsync(
`xdg-open https://github.com/retrozinndev/colorshell/issues/new`
).catch((err: Gio.IOErrorEnum) =>
Notifications.getDefault().sendNotification({
appName: "colorshell",
summary: "Couldn't open link",
body: `Do you have \`xdg-utils\` installed? Stderr: \n${
err.message ? `${err.message}\n` : ""}${err.stack}`
})
)
}]
})
)
}
};
const suspendAsk: AskPopupProps = {
title: "Suspend",
text: "Are you sure you want to Suspend?",
onAccept: () => execAsync("systemctl suspend")
};
const rebootAsk: AskPopupProps = {
title: "Reboot",
text: "Are you sure you want to Reboot? Unsaved work will be lost.",
onAccept: () => {
Config.getDefault().getProperty("night_light.save_on_shutdown", "boolean") &&
NightLight.getDefault().saveData();
execAsync("systemctl reboot");
}
};
const poweroffAsk: AskPopupProps = {
title: "Power Off",
text: "Are you sure you want to power off? Unsaved work will be lost.",
onAccept: () => {
Config.getDefault().getProperty("night_light.save_on_shutdown", "boolean") &&
NightLight.getDefault().saveData();
execAsync("systemctl poweroff");
}
};
-72
View File
@@ -1,72 +0,0 @@
import { bind, 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);
export function setOSDMode(newMode: OSDModes): void {
if(!osdMode) return;
osdMode.set(newMode);
}
export const OSD = (mon: number) => {
osdMode = new Variable<OSDModes>(OSDModes.SINK);
return new Widget.Window({
namespace: "osd",
className: "osd-window",
layer: Astal.Layer.OVERLAY,
anchor: Astal.WindowAnchor.BOTTOM,
canFocus: false,
clickThrough: true,
focusOnClick: false,
marginBottom: 80,
monitor: mon,
onDestroy: () => {
osdMode?.drop();
osdMode = null;
},
child: new Widget.Box({
className: "osd",
expand: true,
children: [
new Widget.Icon({
className: "icon",
icon: bind(Wireplumber.getDefault().getDefaultSink(), "volumeIcon").as(icon =>
!Wireplumber.getDefault().isMutedSink() && Wireplumber.getDefault().getSinkVolume() > 0 ? icon : "audio-volume-muted-symbolic"),
} as Widget.IconProps),
new Widget.Box({
className: "volume",
orientation: Gtk.Orientation.VERTICAL,
valign: Gtk.Align.CENTER,
expand: true,
children: [
new Widget.Label({
className: "device",
label: bind(Wireplumber.getDefault().getDefaultSink(), "description").as(description =>
description ?? "Speaker"),
truncate: true,
} as Widget.LabelProps),
new Widget.Box({
expand: true,
child: new Widget.LevelBar({
className: "levelbar",
value: bind(Wireplumber.getDefault().getDefaultSink(), "volume").as((volume: number) =>
Math.floor(volume * 100)),
maxValue: bind(Wireplumber.getWireplumber(), "defaultSpeaker").as(() =>
Wireplumber.getDefault().getMaxSinkVolume()),
expand: true
} as Widget.LevelBarProps)
} as Widget.BoxProps)
]
} as Widget.BoxProps)
]
} as Widget.BoxProps)
} as Widget.WindowProps);
}
-286
View File
@@ -1,286 +0,0 @@
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();
@@ -7,7 +7,7 @@ general {
}
listener {
timeout = 3600 # 1800 -> 30m | 3600 -> 1h | 7200 -> 2h
timeout = 1800 # 1800 -> 30m | 3600 -> 1h | 7200 -> 2h
on-timeout = hyprlock
# on-resume = notify-send "Welcome back to Hyprland, $USER!"
}
@@ -1,11 +1,11 @@
###############################
## Retrozinn's Hyprland Dots ##
###############################
###########################################
## Hyprland Core Settings for Colorshell ##
##########################################
# From https://github.com/retrozinndev/Hyprland-Dots
# Made with lots of love 󰋑 , by retrozinndev
# Licensed under the MIT License
# Licensed under the BSD 3-Clause License
# Shell configurations (it's not recommended to modify)
@@ -42,8 +42,8 @@ background {
# Time
label {
monitor =
text = cmd[update:30000] echo -e "$(date +"%R")" # 24-hours
# text = cmd[update:30000] echo -e "$(date +"%I:%M %p")" # 12-hours (AM/PM)
text = cmd[update:30000] echo -n "$(date +"%R")" # 24-hours
# text = cmd[update:30000] echo -n "$(date +"%I:%M %p")" # 12-hours (AM/PM)
color = $foreground
shadow_passes = 1
shadow_size = 2
@@ -59,7 +59,7 @@ label {
# Date
label {
monitor =
text = cmd[update:43200000] echo -e "$(date +"%A, %d %B %Y")"
text = cmd[update:43200000] echo -n "$(date +"%A, %d %B %Y")"
color = $foreground
shadow_passes = 1
shadow_size = 2
@@ -78,13 +78,13 @@ label {
font_size = 6
font_family = $minimalFont
color = $foreground
text = Currently logged in as $USER
text = cmd[update:0] echo -n "Logged as $USER in $(hostnamectl hostname)"
halign = center
valign = bottom
position = 0, 5
}
Media
# Media
label {
monitor =
font_size = 12
+8
View File
@@ -0,0 +1,8 @@
# default hyprpaper config from colorshell
# hypr-chan is a mascot of Hyprland, it's not made by retrozinndev
$wallpaper = ~/wallpapers/Default Hypr-chan.jpg
preload = $wallpaper
wallpaper = , $wallpaper
splash = true
@@ -1,20 +1,16 @@
#!usr/bin/env bash
# Prompts the user with dmenu(or dmenu-like app, see hypr/scripts/get-dmenu.sh)
# to choose an image file inside defined $WALLPAPERS_DIR. If the user selects
# to choose an image file inside defined $WALLPAPERS. If the user selects
# an entry, it automatically writes changes to the hyprpaper.conf file and
# hot-reloads if hyprpaper is running.
# --------------
# Licensed under the MIT License
# Licensed under the BSD 3-Clause License
# Made by retrozinndev (João Dias)
# From https://github.com/retrozinndev/colorshell
style="lighten" # lighten / darken
dmenu=$(sh "$XDG_CONFIG_HOME/hypr/scripts/get-dmenu.sh")
if [[ -z "$WALLPAPERS_DIR" ]]; then
WALLPAPERS_DIR="$HOME/wallpapers"
fi
WALLPAPERS=`[[ -z "$WALLPAPERS" ]] && echo -n "$HOME/wallpapers" || echo -n "$WALLPAPERS"`
function Write_changes() {
echo "[LOG] Writing to hyprpaper config file"
@@ -39,33 +35,30 @@ function Reload_pywal() {
wal -t --cols16 $style -i "$wall"
}
if [[ -z "$dmenu" ]]; then
notify-send -u normal -a "Wallpaper" "Dmenu not found" "Couldn't find anyrun or wofi for dmenu! Try installing one of these two before selecting wallpaper!"
exit 1
fi
if [[ -z $(ls -A $WALLPAPERS_DIR) ]]; then
if [[ -z $(ls -A -w1 $WALLPAPERS) ]]; then
notify-send -u normal -a "Wallpaper" "Wallpapers not found" "Couldn't find any wallpaper inside \`~/wallpapers\`, try putting an image you like in there to choose it!"
exit 1
fi
if [[ -z $1 ]]; then
if [[ -z $@ ]]; then
# Prompt wallpaper list
wall="$WALLPAPERS_DIR/$(ls $WALLPAPERS_DIR | $dmenu)"
selection=`ls -w1 "$WALLPAPERS" | wofi --show drun`
# Check if input wallpaper is empty
if [[ $wall == "$WALLPAPERS_DIR/" ]]; then
if [[ -z $selection ]]; then
echo "No wallpaper has been selected by user!"
if [[ $RANDOM_WALLPAPER_WHEN_EMPTY == true ]]; then
wall="$WALLPAPERS_DIR/$(ls $WALLPAPERS_DIR | shuf -n 1)"
echo "Selected random from $WALLPAPERS_DIR: $wall"
wall="$WALLPAPERS/$(ls $WALLPAPERS | shuf -n 1)"
echo "Selected random from $WALLPAPERS: $wall"
else
echo "Skipping hyprpaper changes and exiting."
exit 0
fi
else
wall="$WALLPAPERS/$selection" # wofi if no wallpaper specified
fi
else
wall=$1
wall=$@
fi
Reload_pywal
@@ -1,18 +1,19 @@
#!/usr/bin/env bash
function send_notification() {
notify-send -u normal -a "Color Picker" "$1" "$2"
(notify-send -u normal -a "color-picker" "$1" "$2" > /dev/null 2>&1) || \
(echo "$1: $2")
}
# Check if user has hyprpicker installed
if [[ -z $(command -v hyprpicker) ]]; then
# Check if hyprpicker is installed
if ! command -v hyprpicker > /dev/null; then
send_notification "An error occurred" "Looks like you don't have hyprpicker installed! Try installing it before using the Color Picker tool."
exit 1
fi
selected_color=$(hyprpicker | tail -n 1 | xargs | sed -e 's/ //g')
raw_output=`hyprpicker -al 2> /dev/null`
selected_color=`echo $raw_output | xargs | sed -e 's/ //g'`
if [[ ! -z "$selected_color" ]] && [[ ! "$selected_color" == " " ]]; then
wl-copy $selected_color
if ! [[ -z $selected_color ]]; then
send_notification "Selected Color" "The selected color is <span foreground='$selected_color'>$selected_color</span>, it was also copied to your clipboard!"
fi
@@ -1,15 +1,20 @@
#!/usr/bin/env bash
# This script executes the provided program with UWSM
# if in usage or launches it normally with hyprctl.
# if active, or else normally.
# ---------------
# Licensed under the MIT License
# Licensed under the BSD 3-Clause License
# Made by retrozinndev (João Dias)
# From: https://github.com/retrozinndev/colorshell
if uwsm check is-active "hyprland-uwsm.desktop"; then
exec uwsm app -- "$@"
if uwsm check is-active; then
exec uwsm-app -- "$@"
exit 0
fi
if [[ $1 =~ [.]desktop$ ]]; then
gtk-launch $@
exit 0
fi
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# This script loads/generate color schemes from current
# wallpaper using pywal16.
# ----------
# Licensed under the BSD 3-Clause License
# Made by retrozinndev (João Dias)
# From https://github.com/retrozinndev/colorshell
if ! [[ -f "$XDG_CONFIG_HOME/hypr/hyprpaper.conf" ]]; then
echo "[error] wallpaper file not found!"
exit 1
fi
raw=`cat "$XDG_CONFIG_HOME/hypr/hyprpaper.conf" | grep '$wallpaper =' | sed -e 's/^$wallpaper = //'`
wallpaper=${raw/\~/"$HOME"}
[[ -d "$XDG_CACHE_HOME/wal" ]] && wal -R || sh $XDG_CONFIG_HOME/hypr/scripts/change-wallpaper.sh "$wallpaper"
sleep .5 && hyprctl reload
@@ -3,7 +3,7 @@
# This script handles taking a screenshot using the
# hyprshot tool.
# --------------
# Licensed under the MIT License
# Licensed under the BSD 3-Clause License
# Made by retrozinndev (João Dias)
# From https://github.com/retrozinndev/colorshell
@@ -15,7 +15,6 @@ exec-once = systemctl enable --user --now hyprpaper
# Scripts
exec-once = sh $XDG_CONFIG_HOME/hypr/scripts/gen-pywal.sh
exec-once = sleep 3 && sh $XDG_CONFIG_HOME/hypr/scripts/load-hyprsunset.sh # wait some time to actually set the filters
# Shell
exec-once = $exec ags run
exec-once = uwsm check is-active && uwsm app colorshell.desktop || gtk-launch colorshell.desktop || colorshell
@@ -1,17 +1,14 @@
# color-shell specific configuration, please don't modify unless you know what you're doing!
# `astal` and some `.*ctl` commands don't need $exec (uwsm), since it's just some process communication
# some commands don't need $exec (uwsm) if they're just process communication tools
bind = $mainMod, SPACE, exec, $menu
bind = $mainMod, F11, fullscreen
bind = , Print, exec, $exec sh $XDG_CONFIG_HOME/hypr/scripts/screenshot.sh
bind = $mainMod, Print, exec, $exec sh $XDG_CONFIG_HOME/hypr/scripts/screenshot.sh full
bind = , Print, exec, $exec $scripts/screenshot.sh
bind = $mainMod, Print, exec, $exec $scripts/screenshot.sh full
# restarts colorshell
bind = $mainMod, F7, exec, astal reload
bind = $mainMod, F7, exec, $exec colorshell reload || $exec colorshell
bind = $mainMod, K, exec, $exec $terminal
bind = $mainMod, Q, killactive
@@ -19,24 +16,24 @@ bind = $mainMod, E, exec, $exec $fm
bind = $mainMod, F, togglefloating
bind = $mainMod, P, pseudo,
bind = $mainMod, J, togglesplit
bind = $mainMod, N, exec, astal toggle control-center
bind = $mainMod, M, exec, astal toggle center-window
bind = $mainMod, N, exec, colorshell toggle control-center
bind = $mainMod, M, exec, colorshell toggle center-window
bind = $mainMod, L, exec, $exec hyprlock
bind = $mainMod, V, exec, astal runner '>' || $exec sh $XDG_CONFIG_HOME/hypr/scripts/clipboard-menu.sh
bind = $mainMod, W, exec, astal runner '##'
bind = $mainMod, V, exec, colorshell runner '>'
bind = $mainMod, W, exec, colorshell runner '\##'
# bind = $mainMod, $mainMod_L, exec, astal toggle apps-window
bind = $mainMod, $mainMod_l, exec, astal peek-workspace-num
# bind = $mainMod, $mainMod_L, exec, colorshell toggle apps-window
bind = $mainMod, $mainMod_l, exec, colorshell peek-workspace-num
binde = , XF86AudioLowerVolume, exec, astal volume sink-decrease 5 || wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%- # Decrease volume
binde = , XF86AudioRaiseVolume, exec, astal volume sink-increase 5 || wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+ # Increase volume
bind = , XF86AudioMute, exec, astal volume sink-mute || wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle # Mute
bind = , XF86AudioPrev, exec, playerctl previous # Previous media
bind = , XF86AudioNext, exec, playerctl next # Next media
bind = , XF86AudioPlay, exec, playerctl play-pause # Toggle Play/Pause media
binde = , XF86AudioLowerVolume, exec, colorshell volume sink-decrease 5 || wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%- # Decrease volume
binde = , XF86AudioRaiseVolume, exec, colorshell volume sink-increase 5 || wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+ # Increase volume
bind = , XF86AudioMute, exec, colorshell volume sink-mute || wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle # Mute
bind = , XF86AudioPrev, exec, colorshell media previous || playerctl previous # Previous media
bind = , XF86AudioNext, exec, colorshell media next || playerctl next # Next media
bind = , XF86AudioPlay, exec, colorshell media play-pause || playerctl play-pause # Toggle Play/Pause media
bind = , XF86MonBrightnessDown, exec, brightnessctl s 5%- # Lower monitor brightness
bind = , XF86MonBrightnessUp, exec, brightnessctl s +5% # Increase monitor brightness
bind = , XF86MonBrightnessDown, exec, brightnessctl -c backlight s 5%- # Lower monitor brightness
bind = , XF86MonBrightnessUp, exec, brightnessctl -c backlight s +5% # Increase monitor brightness
# Move focus with mainMod + arrow keys
bind = $mainMod, left, movefocus, l
@@ -13,14 +13,12 @@ general {
col.inactive_border = $background
resize_on_border = false
allow_tearing = false
layout = dwindle
}
render {
ctm_animation = 1
ctm_animation = true
}
misc {
@@ -37,8 +35,8 @@ decoration {
shadow {
enabled = true
range = 4
render_power = 4
range = 5
render_power = 8
color = $background
}
@@ -46,9 +44,9 @@ decoration {
enabled = true
new_optimizations = true
xray = false # Setting to true may cause glitches on nvidia cards!
size = 18
passes = 3
vibrancy = 12
size = 0 # fixes flickering with new_optimizations
passes = 6
vibrancy = 6
popups = false # Enable blur for popups
popups_ignorealpha = 0.7
@@ -1,6 +1,9 @@
# color-shell configuration, please don't modify unless you know what you're doing!
# add colorshell to PATH by default
env = PATH, $PATH:$HOME/.local/bin
# XDG Vars
env = XDG_CONFIG_HOME, $HOME/.config
env = XDG_CACHE_HOME, $HOME/.cache
@@ -37,7 +37,6 @@ layerrule = animation fade, control-center
layerrule = animation fade, center-window
layerrule = animation fade, logout-menu
layerrule = animation slide bottom, apps-window
layerrule = animation slide right, floating-notifications
layerrule = animation fade, runner
layerrule = animation fade, background-window
layerrule = animation fade, background-window-blur
@@ -6,10 +6,11 @@
###############
# Wiki: https://wiki.hyprland.org/Hypr-Ecosystem/hyprlang#defining-variables
$scripts = sh $XDG_CONFIG_HOME/hypr/scripts
# Use this variable to execute apps dinamically (runs with uwsm if being used by compositor)
$exec = sh $XDG_CONFIG_HOME/hypr/scripts/exec.sh
$exec = $scripts/exec.sh
$mainMod = SUPER
$terminal = kitty
$fm = nautilus
$menu = astal runner
$menu = colorshell runner

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