@@ -1 +1,6 @@
|
||||
node_modules/
|
||||
@types/
|
||||
build/
|
||||
|
||||
pnpm-lock.yaml
|
||||
*.log
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||

|
||||

|
||||

|
||||

|
||||
<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">
|
||||
|
||||
[](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
@@ -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)
|
||||
@@ -1,2 +0,0 @@
|
||||
node_modules/
|
||||
@girs/
|
||||
-113
@@ -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;
|
||||
});
|
||||
}
|
||||
Generated
-21
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "colorshell",
|
||||
"dependencies": {
|
||||
"astal": "/usr/share/astal/gjs"
|
||||
}
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
})();
|
||||
@@ -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;
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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`);
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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()
|
||||
)
|
||||
});
|
||||
}
|
||||
@@ -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}`);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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%));
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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, "&")
|
||||
}),
|
||||
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, "&")
|
||||
} 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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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?.();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)();
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
]
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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, "&"),
|
||||
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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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");
|
||||
}
|
||||
};
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user