diff --git a/.gitignore b/.gitignore
index 397b4a76..91fcc751 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,6 @@
+node_modules/
+@types/
+build/
+
+pnpm-lock.yaml
*.log
diff --git a/LICENSE b/LICENSE
index 2b5304a0..d4fc8481 100644
--- a/LICENSE
+++ b/LICENSE
@@ -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.
diff --git a/README.md b/README.md
index 2cfbcd89..08e45228 100644
--- a/README.md
+++ b/README.md
@@ -1,54 +1,65 @@
# colorshell
-
(previously retrozinndev/Hyprland-Dots)
-
-> [!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).
-> 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! 🤩
+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].
It really took me a lot of time to make this, so please star the repo if you like it! :star:
## 🌄 Screenshots
-
-
-
-
+
## 🎨 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.
-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 SUPER + W 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
+SUPER + W or by accessing the Control Center and clicking in the image
+icon on top.
-### ✔️ What's included in this shell
+## ✨ Features
+
+
- 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)
+
## ⌨️ 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.
+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.
+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
-
-[](https://www.star-history.com/#retrozinndev/colorshell&Date)
-
-
Thanks to everyone who starred my project! 💖
+
+
+
+
+
+
+
+
+
Thanks to everyone who starred my project! 💖
@@ -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
-[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)
[wiki]: https://github.com/retrozinndev/colorshell/wiki
diff --git a/WALLPAPERS.md b/WALLPAPERS.md
deleted file mode 100644
index d3b40169..00000000
--- a/WALLPAPERS.md
+++ /dev/null
@@ -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!
-
-
-
- Kessoku Band Rooftop (cropped borders)
-
-
-
-
-- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1319345)
-
-
-
-
-
- Bocchi The Rock! (the wallpaper)
-
-
-
-
-- Source: [Twitter/X (artist only, post was deleted)](https://x.com/mofujiro_mofum2)
-
-
-
-
- Ryo Yamada Maid Dress
-
-
-
-
-- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1363565)
-
-
-
-
- Ryo Yamada
-
-
-
-
-- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1323120)
-
-
-
-
- Ryo Vending Machine
-
-
-
-
-- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1293921)
-
-
-
-
- Nijika Train
-
-
-
-
-- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1304192)
-
-
-
-
- Nijika Ijichi
-
-
-
-
-- Source: [Wallpaper Flare](https://www.wallpaperflare.com/blonde-nijika-ijichi-bocchi-the-rock-anime-girls-sunset-glow-wallpaper-yjrwx)
-
-
-
-
- Kita Street
-
-
-
-
-- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1304193)
-
-
-
-
- Kita-chan!!
-
-
-
-
-- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1296783)
-
-
-
-
- Kikuri Hiroi
-
-
-
-
-- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1295717)
-
-
-
-
- Kessoku Band Reunited
-
-
-
-
-- Source: [Wallpaper Cave](https://wallpapercave.com/w/wp11695992)
-
-
-
-
- Kessoku Albums
-
-
-
-
-- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1316133)
-
-
-
-
- Hitori Gotoh College Corridor
-
-
-
-
-- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1302067)
-
-
-
-
- Garden Kita
-
-
-
-
-- Source: [Gruvbox Wallpapers](https://gruvbox-wallpapers.pages.dev)
-
-
-
-## Vocaloid Wallpapers
-
-
-
- Arch Linux Miku
-
-
-
-
-- Source: [DeviantArt](https://www.deviantart.com/nesyah/art/Arch-linux-feat-Hatsune-Miku-858316759)
-
-
-
-
- Gumi Bridge
-
-
-
-
-- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=593482)
-
-
-
-
- Gumi Forest Sunlight
-
-
-
-
-- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=930443)
-
-
-
-
- Miku Balloons
-
-
-
-
-- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=768576)
-
-
-
-
- Miku Green Hair Glasses
-
-
-
-
-- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=858278)
-
-
-
-
- Kagamine Rin Yellow Tapes
-
-
-
-
-- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1292852)
-
-
-
-
- Gumi VOCALOID
-
-
-
-
-- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=768096)
-
-
-
-
- Miku Stylish with Glasses
-
-
-
-
-- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1305668)
-
-
-
-
- Miku Winter
-
-
-
-
-- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1305841)
-
-
-
-
- Vocaloid Karaoke
-
-
-
-
-- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=770194)
-
-
-
-
- Miku, Rin and Luka Chibi
-
-
-
-
-- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=770164)
-
-
-
-
- Miku Guitar
-
-
-
-
-- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=867976)
-
-
-
-
- Miku Garden
-
-
-
-
-- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1315430)
-
-
-
-
- Miku Setup
-
-
-
-
-- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=672757)
-
-
-
-
- Miku Flower Field
-
-
-
-
-- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=688123)
-
-
-
-
- Miku Door
-
-
-
-
-- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=845583)
-
-
-
-
- Miku Crying with Mask
-
-
-
-
-- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=524092)
-
-
-
-
- Miku City Sky
-
-
-
-
-- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=698444)
-
-
-
-
- Miku Bush
-
-
-
-
-- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=631739)
-
-
-
-
- Hatsune Miku Birthday!
-
-
-
-
-- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=731810)
-
-
-
-
- Hatsune Miku and Megurine Luka
-
-
-
-
-- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1313438)
-
-
-
-
- Gumi Ocean Sunset
-
-
-
-
-- Source: [WallHaven](https://wallhaven.cc/w/we8pgx)
-
-
-
-
- Gumi Street Bike
-
-
-
-
-- Source: [WallHaven](https://wallhaven.cc/w/4x7e7o)
-
-
-
-
- Inabakumori Kaai Yuki
-
-
-
-
-- Source: [WallHaven](https://wallhaven.cc/w/wed3m7)
-
-
-
-
- Inabakumori Osage
-
-
-
-
-- Source: [WallHaven](https://wallhaven.cc/w/o3r8z9)
-
-
-
-## Frieren: Beyond Journey's End
-
-
- Frieren Underwater
-
-
-
-
-- Source: [Pixiv](https://www.pixiv.net/en/artworks/114234634)
-
-
-
-
- Frieren Rain
-
-
-
-
-- Source: [Pixiv](https://www.pixiv.net/en/artworks/114234634)
-
-
-
-
- Frieren At The Funeral
-
-
-
-
-- Source: [Pixiv](https://www.pixiv.net/en/artworks/114234634)
-
-
-
-
- Frieren Sunset
-
-
-
-
-- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1354394)
-
-
-
-
- Frieren Sending Kiss
-
-
-
-
-- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1344010)
-
-
-
-
- Frieren Ring
-
-
-
-
-- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1351964)
-
-
-
-
- Frieren Night Film
-
-
-
-
-- Source: [Wallpaper Flare](https://www.wallpaperflare.com/anime-anime-girls-sousou-no-frieren-wallpaper-yvcxe)
-
-
-
-
- Frieren Blue
-
-
-
-
-- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1357998)
-
-
-
-## Oshi no Ko
-
-
- Oshi no Ko Kana Arima
-
-
-
-
-- Source: [WallHaven](https://wallhaven.cc/w/x6pp5z)
-
-
-
-## Gruvbox-styled
-
-
- Balcony Girl
-
-
-
-
-- Source: [Gruvbox Wallpapers](https://gruvbox-wallpapers.pages.dev)
-
-
-
-
- Gruvbox Girl
-
-
-
-
-- Source: [Gruvbox Wallpapers](https://gruvbox-wallpapers.pages.dev)
-
-
-- [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).
-
-
- Mualani!!
-
-
-
-
-- Source: Genshin Impact Web Event (not available anymore)
-
-
-
-## Others
-
-
- Hypr-chan
-
-
-
-
-- Source: [GitHub (hyprwm/Hyprland)](https://github.com/hyprwm/Hyprland)
-
-
-
-
- Linux Anime Girl
-
-
-
-
-- Source: [WallHere](https://wallhere.com/en/wallpaper/2284648)
-
-
-### 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)
diff --git a/ags/.gitignore b/ags/.gitignore
deleted file mode 100644
index 298eb4de..00000000
--- a/ags/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-node_modules/
-@girs/
diff --git a/ags/app.ts b/ags/app.ts
deleted file mode 100644
index 7df09e16..00000000
--- a/ags/app.ts
+++ /dev/null
@@ -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 | number)>();
-
-const defaultWindows: Array = [ "bar" ];
-const runnerPlugins: Array = [
- 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) => {
- 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;
- });
-}
diff --git a/ags/package-lock.json b/ags/package-lock.json
deleted file mode 100644
index a31381cb..00000000
--- a/ags/package-lock.json
+++ /dev/null
@@ -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
- }
- }
-}
diff --git a/ags/package.json b/ags/package.json
deleted file mode 100644
index 02db1760..00000000
--- a/ags/package.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "colorshell",
- "dependencies": {
- "astal": "/usr/share/astal/gjs"
- }
-}
diff --git a/ags/runner/Runner.ts b/ags/runner/Runner.ts
deleted file mode 100644
index 92cad324..00000000
--- a/ags/runner/Runner.ts
+++ /dev/null
@@ -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|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();
-
-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 {
- 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): Widget.Window {
- let onClickTimeout: (AstalIO.Time|undefined);
- const connections: Map = 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 {
- let calledPlugins: Array = 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 = [];
-
- // 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 = [];
-
- 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!;
-}
-}
diff --git a/ags/runner/plugins/apps.ts b/ags/runner/plugins/apps.ts
deleted file mode 100644
index ab423e0e..00000000
--- a/ags/runner/plugins/apps.ts
+++ /dev/null
@@ -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;
diff --git a/ags/runner/plugins/clipboard.ts b/ags/runner/plugins/clipboard.ts
deleted file mode 100644
index 0e105821..00000000
--- a/ags/runner/plugins/clipboard.ts
+++ /dev/null
@@ -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;
diff --git a/ags/runner/plugins/media.ts b/ags/runner/plugins/media.ts
deleted file mode 100644
index 54ef694b..00000000
--- a/ags/runner/plugins/media.ts
+++ /dev/null
@@ -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|null;
- let previousTitle: Variable|null;
- let nextTitle: Variable|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
-})();
diff --git a/ags/runner/plugins/websearch.ts b/ags/runner/plugins/websearch.ts
deleted file mode 100644
index 9c1cc3f8..00000000
--- a/ags/runner/plugins/websearch.ts
+++ /dev/null
@@ -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;
diff --git a/ags/scripts/arg-handler.ts b/ags/scripts/arg-handler.ts
deleted file mode 100644
index e1a0b0e8..00000000
--- a/ags/scripts/arg-handler.ts
+++ /dev/null
@@ -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 = 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 {
- 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) {
- 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 = 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');
-}
diff --git a/ags/scripts/auth.ts b/ags/scripts/auth.ts
deleted file mode 100644
index 550a2469..00000000
--- a/ags/scripts/auth.ts
+++ /dev/null
@@ -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, cancellable?: Gio.Cancellable, callback?: Gio.AsyncReadyCallback): void | Promise {
- 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): Promise {
- let success: boolean = true;
- await execAsync([ "pkexec", "--", ...(Array.isArray(cmd) ?
- cmd as Array : [ cmd as string ]) ]
- ).catch((r) => {
- success = false;
- console.error(`Polkit: Couldn't authenticate. Stderr: ${r}`);
- });
-
- return success;
- }
-}
diff --git a/ags/scripts/brightness.ts b/ags/scripts/brightness.ts
deleted file mode 100644
index b98067df..00000000
--- a/ags/scripts/brightness.ts
+++ /dev/null
@@ -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);
- }
-}
diff --git a/ags/scripts/config.ts b/ags/scripts/config.ts
deleted file mode 100644
index 1d897ac5..00000000
--- a/ags/scripts/config.ts
+++ /dev/null
@@ -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 {
- 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 {
- 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);
- };
- }
-}
diff --git a/ags/scripts/nightlight.ts b/ags/scripts/nightlight.ts
deleted file mode 100644
index 63e7e678..00000000
--- a/ags/scripts/nightlight.ts
+++ /dev/null
@@ -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`);
- }
-}
diff --git a/ags/scripts/notifications.ts b/ags/scripts/notifications.ts
deleted file mode 100644
index 2996f38a..00000000
--- a/ags/scripts/notifications.ts
+++ /dev/null
@@ -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 = [];
- #history: Array = [];
- #notificationsOnHold: Set = new Set();
- #connections: Array = [];
- #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 {
- 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 };
diff --git a/ags/scripts/reload-handler.ts b/ags/scripts/reload-handler.ts
deleted file mode 100644
index e4a2de0d..00000000
--- a/ags/scripts/reload-handler.ts
+++ /dev/null
@@ -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()
- )
- });
-}
diff --git a/ags/scripts/stylesheet.ts b/ags/scripts/stylesheet.ts
deleted file mode 100644
index ee235b3f..00000000
--- a/ags/scripts/stylesheet.ts
+++ /dev/null
@@ -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 {
- 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 {
- 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 {
- 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}`);
- });
- }
- );
- }
-}
diff --git a/ags/scripts/time.ts b/ags/scripts/time.ts
deleted file mode 100644
index cdb804ad..00000000
--- a/ags/scripts/time.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { GLib, Variable } from "astal";
-
-const time = new Variable(GLib.DateTime.new_now_local()).poll(500, () =>
- GLib.DateTime.new_now_local())();
-
-export const getDateTime = () => time;
diff --git a/ags/scripts/utils.ts b/ags/scripts/utils.ts
deleted file mode 100644
index 35fb02ad..00000000
--- a/ags/scripts/utils.ts
+++ /dev/null
@@ -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;
-}
diff --git a/ags/scripts/varmap.ts b/ags/scripts/varmap.ts
deleted file mode 100644
index 16b59e8c..00000000
--- a/ags/scripts/varmap.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-import { Subscribable } from "astal/binding";
-
-export class VarMap implements Subscribable {
-
- #subs = new Set<(v: Map) => void>();
- #map: Map;
-
- constructor(initial?: Map) {
- this.#map = initial || new Map();
- }
-
- private notifyMap() {
- const subs = this.#subs;
- for(const sub of subs) {
- sub(this.#map);
- }
- }
-
- public get(): Map {
- 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 {
- const newMap: Map = 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 {
- return this.#map.keys();
- }
-
- public values(): MapIterator {
- return this.#map.values();
- }
-
- public forEach (callback: (value: V, key: K, map: Map) => ReturnType): ReturnType[] {
- const result: Array = [];
- for(const entry of this.#map.entries()) {
- result.push(callback(entry[1], entry[0], this.#map));
- }
-
- return result;
- }
-
- public subscribe(callback: (v: Map) => void): () => void {
- this.#subs.add(callback);
-
- return () => {
- this.#subs.delete(callback);
- }
- }
-}
diff --git a/ags/scripts/widget-utils.ts b/ags/scripts/widget-utils.ts
deleted file mode 100644
index 4824b255..00000000
--- a/ags/scripts/widget-utils.ts
+++ /dev/null
@@ -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;
-}
diff --git a/ags/style/_bar.scss b/ags/style/_bar.scss
deleted file mode 100644
index 6d9b9b32..00000000
--- a/ags/style/_bar.scss
+++ /dev/null
@@ -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);
- }
- }
-}
diff --git a/ags/style/_colors.scss b/ags/style/_colors.scss
deleted file mode 100644
index b487360e..00000000
--- a/ags/style/_colors.scss
+++ /dev/null
@@ -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%));
diff --git a/ags/style/_functions.scss b/ags/style/_functions.scss
deleted file mode 100644
index 2f0382d9..00000000
--- a/ags/style/_functions.scss
+++ /dev/null
@@ -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)
- );
-}
diff --git a/ags/style/_osd.scss b/ags/style/_osd.scss
deleted file mode 100644
index 5740e113..00000000
--- a/ags/style/_osd.scss
+++ /dev/null
@@ -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;
- }
- }
-}
diff --git a/ags/style/_runner.scss b/ags/style/_runner.scss
deleted file mode 100644
index 18600852..00000000
--- a/ags/style/_runner.scss
+++ /dev/null
@@ -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;
- }
- }
-}
diff --git a/ags/style/_wal.scss b/ags/style/_wal.scss
deleted file mode 100644
index a94b0429..00000000
--- a/ags/style/_wal.scss
+++ /dev/null
@@ -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;
diff --git a/ags/tsconfig.json b/ags/tsconfig.json
deleted file mode 100644
index d79ca1e5..00000000
--- a/ags/tsconfig.json
+++ /dev/null
@@ -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"
- }
-}
diff --git a/ags/widget/AskPopup.ts b/ags/widget/AskPopup.ts
deleted file mode 100644
index 84c9f215..00000000
--- a/ags/widget/AskPopup.ts
+++ /dev/null
@@ -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;
- text: string | Binding;
- 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;
-}
diff --git a/ags/widget/BackgroundWindow.ts b/ags/widget/BackgroundWindow.ts
deleted file mode 100644
index 6306c751..00000000
--- a/ags/widget/BackgroundWindow.ts
+++ /dev/null
@@ -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;
- /** Monitor number where the window should open */
- monitor: number | Binding;
- /** Custom stylesheet used in the window. default: `background: rgba(0, 0, 0, .2)` */
- css?: string | Binding;
- /** 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);
-}
diff --git a/ags/widget/CustomDialog.ts b/ags/widget/CustomDialog.ts
deleted file mode 100644
index 55523b69..00000000
--- a/ags/widget/CustomDialog.ts
+++ /dev/null
@@ -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;
- className?: string | Binding;
- cssBackground?: string;
- title?: string | Binding;
- text?: string | Binding;
- heightRequest?: number | Binding;
- widthRequest?: number | Binding;
- childOrientation?: Gtk.Orientation | Binding;
- children?: Array | Binding>;
- child?: Gtk.Widget | Binding;
- onFinish?: () => void;
- options?: Array;
- optionsOrientation?: Gtk.Orientation | Binding;
-};
-
-export interface CustomDialogOption {
- onClick?: () => void;
- text: string | Binding;
- closeOnClick?: boolean | Binding;
-}
-
-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 = [];
- 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;
-}
diff --git a/ags/widget/EntryPopup.ts b/ags/widget/EntryPopup.ts
deleted file mode 100644
index 266a4288..00000000
--- a/ags/widget/EntryPopup.ts
+++ /dev/null
@@ -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;
- text?: string | Binding;
- cancelText?: string | Binding;
- acceptText?: string | Binding;
- closeOnAccept?: boolean;
- entryPlaceholder?: string | Binding;
- onAccept: (userInput: string) => void;
- onCancel?: () => void;
- onFinish?: () => void;
- isPassword?: boolean | Binding;
-};
-
-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;
-}
diff --git a/ags/widget/Notification.ts b/ags/widget/Notification.ts
deleted file mode 100644
index e6cc6569..00000000
--- a/ags/widget/Notification.ts
+++ /dev/null
@@ -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);
-}
diff --git a/ags/widget/PopupWindow.ts b/ags/widget/PopupWindow.ts
deleted file mode 100644
index fe66b05c..00000000
--- a/ags/widget/PopupWindow.ts
+++ /dev/null
@@ -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 & 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);
-}
diff --git a/ags/widget/Separator.ts b/ags/widget/Separator.ts
deleted file mode 100644
index 34c50de8..00000000
--- a/ags/widget/Separator.ts
+++ /dev/null
@@ -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;
-}
-
-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);
-}
diff --git a/ags/widget/bar/Apps.ts b/ags/widget/bar/Apps.ts
deleted file mode 100644
index e80a83b3..00000000
--- a/ags/widget/bar/Apps.ts
+++ /dev/null
@@ -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);
-}
diff --git a/ags/widget/bar/Clock.ts b/ags/widget/bar/Clock.ts
deleted file mode 100644
index e137dd78..00000000
--- a/ags/widget/bar/Clock.ts
+++ /dev/null
@@ -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);
-}
diff --git a/ags/widget/bar/FocusedClient.ts b/ags/widget/bar/FocusedClient.ts
deleted file mode 100644
index c099fe6e..00000000
--- a/ags/widget/bar/FocusedClient.ts
+++ /dev/null
@@ -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);
-}
diff --git a/ags/widget/bar/Media.ts b/ags/widget/bar/Media.ts
deleted file mode 100644
index a2487cbc..00000000
--- a/ags/widget/bar/Media.ts
+++ /dev/null
@@ -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 = [];
-
- 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) =>
- 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) =>
- 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) =>
- 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;
-}
diff --git a/ags/widget/bar/Status.ts b/ags/widget/bar/Status.ts
deleted file mode 100644
index c0759810..00000000
--- a/ags/widget/bar/Status.ts
+++ /dev/null
@@ -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 = 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) }): 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 = 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 = 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);
-}
diff --git a/ags/widget/bar/Tray.ts b/ags/widget/bar/Tray.ts
deleted file mode 100644
index 512b442d..00000000
--- a/ags/widget/bar/Tray.ts
+++ /dev/null
@@ -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) => items.length > 0),
- children: bind(astalTray, "items").as((items: Array) => 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);
-}
diff --git a/ags/widget/bar/Workspaces.ts b/ags/widget/bar/Workspaces.ts
deleted file mode 100644
index 217ed60d..00000000
--- a/ags/widget/bar/Workspaces.ts
+++ /dev/null
@@ -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|undefined);
-export const showWorkspaceNumber = (show: boolean) =>
- showWsNum?.set(show);
-
-
-export function Workspaces(): Gtk.Widget {
- showWsNum ??= new Variable(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|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 = 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 = [
- 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);
-}
diff --git a/ags/widget/center-window/BigMedia.ts b/ags/widget/center-window/BigMedia.ts
deleted file mode 100644
index d283975c..00000000
--- a/ags/widget/center-window/BigMedia.ts
+++ /dev/null
@@ -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) =>
- players[0] ? true : false),
- children: bind(AstalMpris.get_default(), "players").as((players: Array) =>
- 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 {
- return bind(player, "artUrl").as((artUrl: string) => {
-
- if(!artUrl)
- return undefined;
-
- if(artUrl.startsWith("/"))
- return "file://" + artUrl;
-
- return artUrl;
- });
-}
diff --git a/ags/widget/center-window/Calendar.ts b/ags/widget/center-window/Calendar.ts
deleted file mode 100644
index 4fa52718..00000000
--- a/ags/widget/center-window/Calendar.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { register, Variable } from "astal";
-import { Gtk, Widget } from "astal/gtk3";
-
-type CalendarProps = Pick & {
-
- 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(true);
- #showHeader = new Variable(true);
- #fillGrid = new Variable(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));
- }
-}
diff --git a/ags/widget/control-center/NotifHistory.ts b/ags/widget/control-center/NotifHistory.ts
deleted file mode 100644
index a5728056..00000000
--- a/ags/widget/control-center/NotifHistory.ts
+++ /dev/null
@@ -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) =>
- 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);
-}
diff --git a/ags/widget/control-center/Pages.ts b/ags/widget/control-center/Pages.ts
deleted file mode 100644
index ef108d22..00000000
--- a/ags/widget/control-center/Pages.ts
+++ /dev/null
@@ -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).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?.();
- });
- });
- }
-}
diff --git a/ags/widget/control-center/QuickActions.ts b/ags/widget/control-center/QuickActions.ts
deleted file mode 100644
index b3b40fc4..00000000
--- a/ags/widget/control-center/QuickActions.ts
+++ /dev/null
@@ -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("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);
-}
diff --git a/ags/widget/control-center/Sliders.ts b/ags/widget/control-center/Sliders.ts
deleted file mode 100644
index 156134ad..00000000
--- a/ags/widget/control-center/Sliders.ts
+++ /dev/null
@@ -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);
-}
diff --git a/ags/widget/control-center/Tiles.ts b/ags/widget/control-center/Tiles.ts
deleted file mode 100644
index 5630a2af..00000000
--- a/ags/widget/control-center/Tiles.ts
+++ /dev/null
@@ -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);
-}
diff --git a/ags/widget/control-center/pages/Bluetooth.ts b/ags/widget/control-center/pages/Bluetooth.ts
deleted file mode 100644
index 5d93a4f5..00000000
--- a/ags/widget/control-center/pages/Bluetooth.ts
+++ /dev/null
@@ -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> = 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()
- });
-}
diff --git a/ags/widget/control-center/pages/Microphone.ts b/ags/widget/control-center/pages/Microphone.ts
deleted file mode 100644
index 3d101cc3..00000000
--- a/ags/widget/control-center/pages/Microphone.ts
+++ /dev/null
@@ -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);
-}
diff --git a/ags/widget/control-center/pages/Network.ts b/ags/widget/control-center/pages/Network.ts
deleted file mode 100644
index 3f39b774..00000000
--- a/ags/widget/control-center/pages/Network.ts
+++ /dev/null
@@ -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);
-}
diff --git a/ags/widget/control-center/pages/NightLight.ts b/ags/widget/control-center/pages/NightLight.ts
deleted file mode 100644
index c0d10474..00000000
--- a/ags/widget/control-center/pages/NightLight.ts
+++ /dev/null
@@ -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);
diff --git a/ags/widget/control-center/pages/Page.ts b/ags/widget/control-center/pages/Page.ts
deleted file mode 100644
index a30f15aa..00000000
--- a/ags/widget/control-center/pages/Page.ts
+++ /dev/null
@@ -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;
- title: string | Binding;
- description?: string | Binding;
- headerButtons?: Array | Binding>;
- bottomButtons?: Array | Binding>;
- orientation?: Gtk.Orientation | Binding;
- spacing?: number;
- child?: Gtk.Widget | Binding;
- children?: Array | Binding>;
-};
-
-export type BottomButton = {
- title: string | Binding;
- description?: string | Binding;
- tooltipText?: string | Binding;
- tooltipMarkup?: string | Binding;
- onClick?: () => void;
-};
-
-export { Page };
-
-@register({ GTypeName: "Page" })
-class Page extends Widget.Box {
- readonly #id: string | number;
- readonly bottomButtons?: Array;
-
- #title: string | Binding;
- #description?: string | Binding;
-
- 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;
- icon?: string | Binding;
- title: string | Binding;
- endWidget?: Gtk.Widget | Binding;
- description?: string | Binding;
- extraButtons?: Array | Binding>;
- onDestroy?: (self: Widget.Box) => void;
- onClick?: (self: Widget.Button) => void;
- tooltipText?: string | Binding;
- tooltipMarkup?: string | Binding;
-}): 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);
-}
diff --git a/ags/widget/control-center/pages/Sound.ts b/ags/widget/control-center/pages/Sound.ts
deleted file mode 100644
index cd0d9fce..00000000
--- a/ags/widget/control-center/pages/Sound.ts
+++ /dev/null
@@ -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 = [];
-
- 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);
-}
diff --git a/ags/widget/control-center/tiles/Bluetooth.ts b/ags/widget/control-center/tiles/Bluetooth.ts
deleted file mode 100644
index 655ab068..00000000
--- a/ags/widget/control-center/tiles/Bluetooth.ts
+++ /dev/null
@@ -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 = 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)();
-}
diff --git a/ags/widget/control-center/tiles/DoNotDisturb.ts b/ags/widget/control-center/tiles/DoNotDisturb.ts
deleted file mode 100644
index 5a085886..00000000
--- a/ags/widget/control-center/tiles/DoNotDisturb.ts
+++ /dev/null
@@ -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
-});
diff --git a/ags/widget/control-center/tiles/Network.ts b/ags/widget/control-center/tiles/Network.ts
deleted file mode 100644
index a68daaa7..00000000
--- a/ags/widget/control-center/tiles/Network.ts
+++ /dev/null
@@ -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);
diff --git a/ags/widget/control-center/tiles/NightLight.ts b/ags/widget/control-center/tiles/NightLight.ts
deleted file mode 100644
index dbbf1bc4..00000000
--- a/ags/widget/control-center/tiles/NightLight.ts
+++ /dev/null
@@ -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);
diff --git a/ags/widget/control-center/tiles/Recording.ts b/ags/widget/control-center/tiles/Recording.ts
deleted file mode 100644
index 47c78265..00000000
--- a/ags/widget/control-center/tiles/Recording.ts
+++ /dev/null
@@ -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 = 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)();
-}
diff --git a/ags/widget/control-center/tiles/Tile.ts b/ags/widget/control-center/tiles/Tile.ts
deleted file mode 100644
index 8e0af31a..00000000
--- a/ags/widget/control-center/tiles/Tile.ts
+++ /dev/null
@@ -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;
- icon?: string | Binding;
- visible?: boolean | Binding;
- iconSize?: number | Binding;
- title: string | Binding;
- description?: string | Binding;
- toggleState?: boolean | Binding;
- enableOnClickMore?: boolean | Binding;
- onDestroy?: () => void;
- onToggledOn: () => void;
- onToggledOff: () => void;
- onClickMore?: () => void;
-}
-
-export function Tile(props: TileProps): (() => Gtk.Widget) {
- const subs: Array<() => void> = [];
- const toggled = new Variable(((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
- })
- ]
- });
-}
diff --git a/ags/widget/runner/ResultWidget.ts b/ags/widget/runner/ResultWidget.ts
deleted file mode 100644
index 04800a6f..00000000
--- a/ags/widget/runner/ResultWidget.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-import { Binding, register } from "astal";
-import { Gtk, Widget } from "astal/gtk3";
-
-export { ResultWidget, ResultWidgetProps };
-
-type ResultWidgetProps = {
- icon?: string | Binding | Gtk.Widget | Binding;
- title: string | Binding;
- description?: string | Binding;
- 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 | Gtk.Widget | Binding | 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
- } as Widget.IconProps));
- } else {
- this.add(new Widget.Box({
- child: this.icon as Binding
- } 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));
- }
-}
diff --git a/ags/window/AppsWindow.ts b/ags/window/AppsWindow.ts
deleted file mode 100644
index 1e9a903b..00000000
--- a/ags/window/AppsWindow.ts
+++ /dev/null
@@ -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("");
- const searchSubscription = searchString.subscribe((str: string) => {
- updateResults(str);
- });
-
- let results: Array = [];
-
- const flowboxConnections: Array = [];
- 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 = [];
- // 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${app.description} `
- : ""}`.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;
-}
diff --git a/ags/window/Bar.ts b/ags/window/Bar.ts
deleted file mode 100644
index d10d5b99..00000000
--- a/ags/window/Bar.ts
+++ /dev/null
@@ -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);
-}
diff --git a/ags/window/CenterWindow.ts b/ags/window/CenterWindow.ts
deleted file mode 100644
index ccccc96b..00000000
--- a/ags/window/CenterWindow.ts
+++ /dev/null
@@ -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);
diff --git a/ags/window/ControlCenter.ts b/ags/window/ControlCenter.ts
deleted file mode 100644
index e6a5fb48..00000000
--- a/ags/window/ControlCenter.ts
+++ /dev/null
@@ -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);
diff --git a/ags/window/FloatingNotifications.ts b/ags/window/FloatingNotifications.ts
deleted file mode 100644
index ceb018e4..00000000
--- a/ags/window/FloatingNotifications.ts
+++ /dev/null
@@ -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);
diff --git a/ags/window/LogoutMenu.ts b/ags/window/LogoutMenu.ts
deleted file mode 100644
index 28f62a5b..00000000
--- a/ags/window/LogoutMenu.ts
+++ /dev/null
@@ -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");
- }
-};
diff --git a/ags/window/OSD.ts b/ags/window/OSD.ts
deleted file mode 100644
index ddc86550..00000000
--- a/ags/window/OSD.ts
+++ /dev/null
@@ -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|null);
-
-export function setOSDMode(newMode: OSDModes): void {
- if(!osdMode) return;
-
- osdMode.set(newMode);
-}
-
-export const OSD = (mon: number) => {
- osdMode = new Variable(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);
-}
diff --git a/ags/windows.ts b/ags/windows.ts
deleted file mode 100644
index c4dc4131..00000000
--- a/ags/windows.ts
+++ /dev/null
@@ -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> = {};
- private static instance: (WindowsClass | null);
-
- @signal(String)
- declare opened: () => string;
- @signal(String)
- declare closed: () => string;
-
- #windows: Record (Widget.Window | Array))> = {
- "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 | Array>)> = {};
- #appConnections: Array = [];
-
- get windows() { return this.#windows; }
-
- @property(Object)
- get openWindows(): Record> { 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), i: number) => {
- if(Array.isArray(window)) {
- window = window as Array;
- window.map(win => {
- if(win.get_current_monitor() === monitor) {
- win?.close();
- this.openWindows[i] = (this.openWindows[i] as Array).filter(item =>
- item !== win);
- }
- });
-
- if((this.openWindows[i] as Array).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) => {
- if(Array.isArray(window)) {
- window.map((win, i) => {
- const curId = (id as Array)[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
- * @throws Error if there are no monitors connected
- */
- public createWindowForMonitors(windowFun: (mon: number) => Widget.Window): (() => Array) {
- 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))): 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)) | undefined) {
- return this.windows?.[name as keyof typeof this.windows];
- }
-
- public getOpenWindow(name: (keyof typeof this.openWindows)): (Widget.Window | Array | undefined) {
- return this.openWindows?.[name as keyof typeof this.openWindows];
- }
-
- public getWindows(): Array<(() => (Widget.Window | Array))> {
- 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)) = this.getWindow(name)!;
- const openWindows: (Array | 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();
diff --git a/hypr/hypridle.conf b/config/hypr/hypridle.conf
similarity index 83%
rename from hypr/hypridle.conf
rename to config/hypr/hypridle.conf
index ec54b0ab..271823a9 100644
--- a/hypr/hypridle.conf
+++ b/config/hypr/hypridle.conf
@@ -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!"
}
diff --git a/hypr/hyprland.conf b/config/hypr/hyprland.conf
similarity index 62%
rename from hypr/hyprland.conf
rename to config/hypr/hyprland.conf
index bb5e2eda..f1b14a96 100644
--- a/hypr/hyprland.conf
+++ b/config/hypr/hyprland.conf
@@ -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)
diff --git a/hypr/hyprlock.conf b/config/hypr/hyprlock.conf
similarity index 90%
rename from hypr/hyprlock.conf
rename to config/hypr/hyprlock.conf
index 1a0b356b..0e9d2850 100644
--- a/hypr/hyprlock.conf
+++ b/config/hypr/hyprlock.conf
@@ -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
diff --git a/config/hypr/hyprpaper.conf b/config/hypr/hyprpaper.conf
new file mode 100644
index 00000000..7b89e41b
--- /dev/null
+++ b/config/hypr/hyprpaper.conf
@@ -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
diff --git a/hypr/scripts/change-wallpaper.sh b/config/hypr/scripts/change-wallpaper.sh
similarity index 65%
rename from hypr/scripts/change-wallpaper.sh
rename to config/hypr/scripts/change-wallpaper.sh
index 832f89de..23a4c96d 100644
--- a/hypr/scripts/change-wallpaper.sh
+++ b/config/hypr/scripts/change-wallpaper.sh
@@ -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
diff --git a/hypr/scripts/color-picker.sh b/config/hypr/scripts/color-picker.sh
similarity index 54%
rename from hypr/scripts/color-picker.sh
rename to config/hypr/scripts/color-picker.sh
index 6dd06a47..3af2ce88 100644
--- a/hypr/scripts/color-picker.sh
+++ b/config/hypr/scripts/color-picker.sh
@@ -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 $selected_color , it was also copied to your clipboard!"
fi
diff --git a/hypr/scripts/exec.sh b/config/hypr/scripts/exec.sh
similarity index 51%
rename from hypr/scripts/exec.sh
rename to config/hypr/scripts/exec.sh
index c5f787de..55edec92 100644
--- a/hypr/scripts/exec.sh
+++ b/config/hypr/scripts/exec.sh
@@ -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
diff --git a/config/hypr/scripts/gen-pywal.sh b/config/hypr/scripts/gen-pywal.sh
new file mode 100644
index 00000000..b7d9c417
--- /dev/null
+++ b/config/hypr/scripts/gen-pywal.sh
@@ -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
diff --git a/hypr/scripts/screenshot.sh b/config/hypr/scripts/screenshot.sh
similarity index 93%
rename from hypr/scripts/screenshot.sh
rename to config/hypr/scripts/screenshot.sh
index 292133d9..56dfd7ca 100644
--- a/hypr/scripts/screenshot.sh
+++ b/config/hypr/scripts/screenshot.sh
@@ -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
diff --git a/hypr/shell/autostart.conf b/config/hypr/shell/autostart.conf
similarity index 82%
rename from hypr/shell/autostart.conf
rename to config/hypr/shell/autostart.conf
index 1845d163..6ccdb8e0 100644
--- a/hypr/shell/autostart.conf
+++ b/config/hypr/shell/autostart.conf
@@ -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
diff --git a/hypr/shell/bindings.conf b/config/hypr/shell/bindings.conf
similarity index 62%
rename from hypr/shell/bindings.conf
rename to config/hypr/shell/bindings.conf
index 72d8a859..2e896e9f 100644
--- a/hypr/shell/bindings.conf
+++ b/config/hypr/shell/bindings.conf
@@ -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
diff --git a/hypr/shell/decorations.conf b/config/hypr/shell/decorations.conf
similarity index 92%
rename from hypr/shell/decorations.conf
rename to config/hypr/shell/decorations.conf
index bb2379b7..5efa8a63 100644
--- a/hypr/shell/decorations.conf
+++ b/config/hypr/shell/decorations.conf
@@ -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
diff --git a/hypr/shell/environment.conf b/config/hypr/shell/environment.conf
similarity index 91%
rename from hypr/shell/environment.conf
rename to config/hypr/shell/environment.conf
index ccce967c..47aa23af 100644
--- a/hypr/shell/environment.conf
+++ b/config/hypr/shell/environment.conf
@@ -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
diff --git a/hypr/shell/hyprland.conf b/config/hypr/shell/hyprland.conf
similarity index 100%
rename from hypr/shell/hyprland.conf
rename to config/hypr/shell/hyprland.conf
diff --git a/hypr/shell/layout.conf b/config/hypr/shell/layout.conf
similarity index 100%
rename from hypr/shell/layout.conf
rename to config/hypr/shell/layout.conf
diff --git a/hypr/shell/rules.conf b/config/hypr/shell/rules.conf
similarity index 97%
rename from hypr/shell/rules.conf
rename to config/hypr/shell/rules.conf
index 5c8e9b44..1d32d2be 100644
--- a/hypr/shell/rules.conf
+++ b/config/hypr/shell/rules.conf
@@ -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
diff --git a/hypr/shell/variables.conf b/config/hypr/shell/variables.conf
similarity index 78%
rename from hypr/shell/variables.conf
rename to config/hypr/shell/variables.conf
index 1bdf451c..7ba36619 100644
--- a/hypr/shell/variables.conf
+++ b/config/hypr/shell/variables.conf
@@ -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
diff --git a/hypr/user/autostart.conf b/config/hypr/user/autostart.conf
similarity index 100%
rename from hypr/user/autostart.conf
rename to config/hypr/user/autostart.conf
diff --git a/hypr/user/bindings.conf b/config/hypr/user/bindings.conf
similarity index 100%
rename from hypr/user/bindings.conf
rename to config/hypr/user/bindings.conf
diff --git a/hypr/user/decorations.conf b/config/hypr/user/decorations.conf
similarity index 100%
rename from hypr/user/decorations.conf
rename to config/hypr/user/decorations.conf
diff --git a/hypr/user/environment.conf b/config/hypr/user/environment.conf
similarity index 100%
rename from hypr/user/environment.conf
rename to config/hypr/user/environment.conf
diff --git a/hypr/user/hyprland.conf b/config/hypr/user/hyprland.conf
similarity index 100%
rename from hypr/user/hyprland.conf
rename to config/hypr/user/hyprland.conf
diff --git a/hypr/user/input.conf b/config/hypr/user/input.conf
similarity index 100%
rename from hypr/user/input.conf
rename to config/hypr/user/input.conf
diff --git a/hypr/user/layout.conf b/config/hypr/user/layout.conf
similarity index 100%
rename from hypr/user/layout.conf
rename to config/hypr/user/layout.conf
diff --git a/hypr/user/monitors.conf b/config/hypr/user/monitors.conf
similarity index 100%
rename from hypr/user/monitors.conf
rename to config/hypr/user/monitors.conf
diff --git a/hypr/user/rules.conf b/config/hypr/user/rules.conf
similarity index 100%
rename from hypr/user/rules.conf
rename to config/hypr/user/rules.conf
diff --git a/kitty/kitty.conf b/config/kitty/kitty.conf
similarity index 100%
rename from kitty/kitty.conf
rename to config/kitty/kitty.conf
diff --git a/kitty/user.conf b/config/kitty/user.conf
similarity index 100%
rename from kitty/user.conf
rename to config/kitty/user.conf
diff --git a/hypr/hyprpaper.conf b/hypr/hyprpaper.conf
deleted file mode 100644
index 985f58a9..00000000
--- a/hypr/hyprpaper.conf
+++ /dev/null
@@ -1,4 +0,0 @@
-# Default hyprpaper file
-
-preload = $HOME/wallpapers/Frieren At The Funeral.jpg
-wallpaper = , $HOME/wallpapers/Frieren At The Funeral.jpg
diff --git a/hypr/scripts/clipboard-menu.sh b/hypr/scripts/clipboard-menu.sh
deleted file mode 100644
index 8932880f..00000000
--- a/hypr/scripts/clipboard-menu.sh
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/usr/bin/env bash
-
-selection=$(cliphist list | anyrun --plugins libstdin.so | cliphist decode)
-
-if [[ ! -z "$selection" ]]; then
- printf "%s" "$selection" | sed -e 's/\\[n]$//g' | wl-copy
-fi
diff --git a/hypr/scripts/gen-pywal.sh b/hypr/scripts/gen-pywal.sh
deleted file mode 100644
index 6775b697..00000000
--- a/hypr/scripts/gen-pywal.sh
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/usr/bin/env bash
-
-# This script loads/generate color schemes from current
-# wallpaper using pywal16.
-# ----------
-# Licensed under the MIT License
-# Made by retrozinndev (João Dias)
-# From https://github.com/retrozinndev/colorshell
-
-if ! [[ -f "$HOME/.config/hypr/hyprpaper.conf" ]]; then
- notify-send -a "Wallpaper" "Couldn't load" "Wallpaper file not found! Please check for the wallpaper: $wallpaper."
- exit 1
-fi
-
-wallpaper="$(cat $HOME'/.config/hypr/hyprpaper.conf' | grep '$wallpaper =' | sed -e 's/^$wallpaper = //')"
-
-if [[ -d "$HOME/.cache/wal" ]]; then
- wal -R
-else
- wal -q -t --cols16 darken -i "$wallpaper"
-fi
diff --git a/hypr/scripts/get-dmenu.sh b/hypr/scripts/get-dmenu.sh
deleted file mode 100644
index 75559893..00000000
--- a/hypr/scripts/get-dmenu.sh
+++ /dev/null
@@ -1,26 +0,0 @@
-#!/usr/bin/env bash
-
-# Checks environment for dmenu or dmenu-like apps
-# and prints out a command to pipe of.
-# -----------
-# Licensed under the MIT License
-# Made by retrozinndev (João Dias)
-# From: https://github.com/retrozinndev/colorshell
-
-DMENUS=(
- "anyrun:--plugins:libstdin.so"
- "rofi:-dmenu"
- "wofi:--show:dmenu"
- "dmenu"
-)
-
-for dmenu in ${DMENUS[@]}; do
- name=$(printf "$dmenu" | awk -F: '{ print $1 }')
- cmd=$(env "$name" -h > /dev/null)
- code=$?
-
- if [[ ! $code == 127 ]]; then
- echo "$dmenu" | sed 's/:/ /g'
- break;
- fi
-done
diff --git a/hypr/scripts/load-hyprsunset.sh b/hypr/scripts/load-hyprsunset.sh
deleted file mode 100644
index 20b023f0..00000000
--- a/hypr/scripts/load-hyprsunset.sh
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/usr/bin/env bash
-
-# This script loads hyprsunset settings previously
-# saved by the save-hyprsunset.sh script on shutdown.
-# --------------
-# Licensed under the MIT License
-# Made by retrozinndev (João Dias)
-# From https://github.com/retrozinndev/colorshell
-
-[[ -z $XDG_CONFIG_HOME ]] && XDG_CONFIG_HOME="$HOME/.config"
-
-file_="$XDG_CONFIG_HOME/hypr/hyprsunset.conf"
-
-if ! [[ -f "$file_" ]]; then
- echo "[warn] Couldn't load hyprsunset config: file not found"
- exit 0
-fi
-
-if ! [[ "$XDG_CURRENT_DESKTOP" =~ "Hyprland" ]]; then
- echo "[error] Seems like you're not running Hyprland! Exiting"
- exit 1
-fi
-
-if [[ -z $(command -v hyprsunset) ]]; then
- echo "[error] Couldn't load hyprsunset settings: it's either not installed or not in PATH"
- exit 1
-fi
-
-temperature=$(cat "$file_" | grep -E "^temperature = (.*)" | awk -F= '{ print $2 }')> /dev/null
-gamma=$(cat "$file_" | grep -E "^gamma = (.*)" | awk -F= '{ print $2 }')> /dev/null
-
-hyprctl hyprsunset temperature $temperature
-sleep .05
-hyprctl hyprsunset gamma $gamma
diff --git a/hypr/scripts/save-hyprsunset.sh b/hypr/scripts/save-hyprsunset.sh
deleted file mode 100644
index 6feb4f1f..00000000
--- a/hypr/scripts/save-hyprsunset.sh
+++ /dev/null
@@ -1,29 +0,0 @@
-#!/usr/bin/env bash
-
-# This script saves hyprsunset values into a file using
-# hyprlang, in `$XDG_CONFIG_HOME/hypr/hyprsunset.conf`.
-# It is used to save last user configuration on computer
-# shutdown.
-# --------------
-# Licensed under the MIT License
-# Made by retrozinndev (João Dias)
-# From https://github.com/retrozinndev/colorshell
-
-[[ -z $XDG_CONFIG_HOME ]] && XDG_CONFIG_HOME="$HOME/.config"
-
-if ! [[ "$XDG_CURRENT_DESKTOP" =~ "Hyprland" ]]; then
- echo "[error] Seems like you're not running Hyprland! Exiting"
- exit 1
-fi
-
-if [[ -z $(command -v hyprsunset) ]]; then
- echo "[error] Couldn't save hyprsunset settings: it's either not installed or not in PATH"
- exit 1
-fi
-
-output="$XDG_CONFIG_HOME/hypr/hyprsunset.conf"
-
-temperature=$(hyprctl hyprsunset temperature || 6000)
-gamma=$(hyprctl hyprsunset gamma || 100)
-
-printf "temperature = %d\ngamma = %d" "$temperature" "$gamma" > $output
diff --git a/install.sh b/install.sh
index 8335de59..e50ca7b6 100755
--- a/install.sh
+++ b/install.sh
@@ -1,87 +1,132 @@
-#!/usr/bin/bash
+#!/usr/bin/env bash
-source ./utils.sh
-
-set -e
trap "printf \"\nOk, quitting beacuse you entered an exit signal. (SIGINT).\n\"; exit 1" SIGINT
-trap "printf \"\nOh noo!! Some application just killed the script!\"; exit 2" SIGTERM
-
-XDG_CONFIG_HOME=$(sh -c '[[ ! -z "$XDG_CONFIG_HOME" ]] && echo "$XDG_CONFIG_HOME" || echo "$HOME/.config"')
-
-function Apply_wallpapers() {
- echo -n "Would you also like to apply the wallpapers folder? :3 [y/n] "
- read answer
- printf "\n"
-
- if [[ $answer =~ "y" ]]; then
- echo "Thanks for choosing! Please remember that I am not the author of the wallpapers!"
- echo "You can see sources in the repo: https://github.com/retrozinndev/colorshell/WALLPAPERS.md"
-
- echo "-> Copying wallpapers to ~/wallpapers"
- mkdir -p $HOME/wallpapers
- cp -f ./wallpapers/* $HOME/wallpapers
- else
- echo "Ok! The wallpaper is yours to choose!"
- echo "Expect some Hyprland source and color errors, it happens because there aren't colors to source when you don't install wallpapers right away, so you have to do it yourself."
- echo "Tip: create the ~/wallpapers directory, put your wallpapers there and press ´SUPER + W´ to select :3"
- fi
-}
+trap "printf \"\nOh noo!! Some application just killed the script! (SIGTERM)\"; exit 2" SIGTERM
+
+XDG_DATA_HOME=`[[ -z "$XDG_DATA_HOME" ]] && echo -n "$HOME/.local/share" || echo -n "$XDG_DATA_HOME"`
+XDG_CACHE_HOME=`[[ -z "$XDG_CACHE_HOME" ]] && echo -n $HOME/.cache || echo -n $XDG_CACHE_HOME`
+XDG_CONFIG_HOME=`[[ -z "$XDG_CONFIG_HOME" ]] && echo -n "$HOME/.config" || echo -n "$XDG_CONFIG_HOME"`
+BIN_HOME=`[[ -z "$BIN_HOME" ]] && echo -n "$HOME/.local/bin" || echo -n "$BIN_HOME"`
+APPS_HOME=`[[ -z "$APPS_HOME" ]] && echo -n "$XDG_DATA_HOME/applications" || echo -n "$APPS_HOME"`
+
+skip_prompts=`[[ "$1" == -y ]] && echo -n true`
+is_standalone=`(git remote -v > /dev/null 2>&1) || echo -n true`
+
+temp_dir="$XDG_CACHE_HOME/colorshell-installer"
+repo_directory=`[[ "$is_standalone" ]] && echo -n "$temp_dir/repo" || echo -n "."`
+
+
+# source utils script before installation
+if [[ "$is_standalone" ]]; then
+ mkdir -p "$repo_directory"
+ default_branch="ryo" # `curl -s https://api.github.com/repos/retrozinndev/colorshell | jq -r .default_branch`
+ # get utils script
+ echo "fetching utils script..."
+ curl -s https://raw.githubusercontent.com/retrozinndev/colorshell/refs/heads/$default_branch/scripts/utils.sh > $temp_dir/utils.sh
+ source $temp_dir/utils.sh
+else
+ source ./scripts/utils.sh
+fi
+
#########
# Start #
#########
+# makes bash force-load the script into memory to avoid issues when
+# switching source to a tag
+
+{
Print_header
-echo -e "colorshell is a project made by retrozinndev. source: https://github.com/retrozinndev/colorshell\n"
+echo -e "Colorshell is a project made by retrozinndev.
+Source: https://github.com/retrozinndev/colorshell\n"
sleep .5
echo "Welcome to the colorshell installation script!"
-# Warn user of possible problems that can happen
-echo "!!!WARNING!!! By running this script, you assume total responsability for any issues that may occur with your filesystem"
+# Warn user of possible issues
+Send_log warn "!! By running this script, you assume total responsability for any issues that may occur with your filesystem"
+
+[[ -z "$skip_prompts" ]] && \
+ Ask "Do you want to start the shell installation?"
+
+if [[ "$answer" == y ]] || [[ "$skip_prompts" ]]; then
+ if [[ "$is_standalone" ]]; then
+ Send_log "The installer noticed that you're calling the script remotely"
+ rm -rf $repo_directory 2> /dev/null
+ Send_log "Cloning repository in \`$repo_directory\`..."
+ git clone https://github.com/retrozinndev/colorshell.git "$repo_directory"
+ fi
+
+ Ask "Nice! Do you want to use the stable version instead of the unstable(latest commit)?"
-echo -n "Do you want to start the shell installation? [y/n] "
-[[ ! $1 == "dots" ]] && read input || printf "\n"
+ if [[ -z "$skip_prompts" ]] && [[ "$answer" == y ]]; then
+ Send_log "fetching latest release from colorshell repository"
+ latest_tag=`curl -s "$repo_api_url/releases" | jq -r '. | select(.[].prerelease == false) | .[0].tag_name'`
+
+ Send_log "Done fetching"
+ Send_log "Checking out latest non-pre-release version: $latest_tag"
+ git -C "$repo_directory" checkout $latest_tag > /dev/null 2>&1
+ fi
-if [[ $1 == "dots" ]] || [[ $input =~ "y" ]]; then
- Send_log "Starting installation...\n"
+ Send_log "Starting installation..."
- for dir in ${config_dirs[@]}; do
+ Send_log "Installing default configurations"
+ for dir in $(ls -A -w1 "$repo_directory/config"); do
dest=$XDG_CONFIG_HOME/$dir
- echo "-> Installing $dir in $dest"
- mkdir -p "$dest" # create parents
+ Send_log "Installing $dir in $dest"
+ mkdir -p `dirname "$dest"` # create parents
+
+ [[ -d "$dest" ]] || [[ -f "$dest" ]] && \
+ rm -rf $dest
- if [[ -f "./$dir" ]]; then
- rm -r "$dest" # delete unused directory
- cp -f ./$dir "$dest" # copy actual file
- else
- cp -rf ./$dir/* "$dest" # force-copy content
- fi
+ cp -rf $repo_directory/config/$dir "$dest" # copy
done
- echo "-> Copying default user config"
- cp -rf ./hypr/user $XDG_CONFIG_HOME/hypr
+ Send_log "Updating dependencies"
+ pnpm i && pnpm update
+
+ Send_log "Building colorshell"
+ prev_wd=`pwd`
+ cd "$repo_directory"
+ pnpm build:release
+ cd "$prev_wd"
+
+ Send_log "Installing colorshell"
+ # install shell
+ mkdir -p $BIN_HOME
+ cp -f $repo_directory/build/release/colorshell $BIN_HOME
- echo "-> Copying default hyprpaper.conf"
- cp -f ./hypr/hyprpaper.conf $XDG_CONFIG_HOME/hypr
+ # install gresource
+ mkdir -p $XDG_DATA_HOME/colorshell
+ cp -f $repo_directory/build/release/resources.gresource $XDG_DATA_HOME/colorshell
- # Ask if user also wants to install default wallpapers
- Apply_wallpapers
+ # install desktop entry
+ mkdir -p $APPS_HOME
+ cp -f $repo_directory/build/release/colorshell.desktop $APPS_HOME
- if ! [[ $1 == "dots" ]]; then
- echo "Ah yes! Looks like it's installed, yay :3"; sleep .8
- echo "If you find any issue, please report it in: https://github.com/retrozinndev/colorshell/issues"; sleep .5
- echo "Thanks for using colorshell! I really appreciate that :D"
+
+ Send_log "Adding default wallpaper in ~/wallpapers"
+ mkdir -p $HOME/wallpapers
+ cp -f $repo_directory/resources/wallpaper_default.jpg "$HOME/wallpapers/Default Hypr-chan.jpg"
+
+ if [[ -z "$skip_prompts" ]]; then
+ echo "Colorshell is installed! :D"
+ sleep .8
+ echo "If you have issues, please report it!"
+ echo "Issue Tracker: https://github.com/retrozinndev/colorshell/issues"
+ sleep .5
+ echo "Thanks for using colorshell! I really appreciate that :P"
printf "\n"
exit 0
fi
- Send_log "colorshell is installed!"
-
+ Send_log "Colorshell is installed!"
exit 0
fi
printf "Ok, doing as you said! Bye bye!\n"
exit 0
+}
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..44f95a40
--- /dev/null
+++ b/package.json
@@ -0,0 +1,21 @@
+{
+ "$schema": "https://www.schemastore.org/package.json",
+ "name": "colorshell",
+ "version": "2.0.0",
+ "packageManager": "pnpm@10.12.1",
+ "scripts": {
+ "types": "sh ./scripts/types.sh",
+ "dev": "sh ./scripts/build.sh -d && sh ./scripts/start.sh",
+ "start": "sh ./scripts/start.sh",
+ "clean": "sh ./scripts/clean.sh",
+ "build": "sh ./scripts/build.sh",
+ "build:release": "sh ./scripts/release.sh",
+ "sync-config": "sh ./scripts/sync-config.sh"
+ },
+ "devDependencies": {
+ "ags": "link:../../../../usr/share/ags/js"
+ },
+ "dependencies": {
+ "gnim-utils": "github:retrozinndev/gnim-utils"
+ }
+}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
new file mode 100644
index 00000000..43649bc9
--- /dev/null
+++ b/pnpm-workspace.yaml
@@ -0,0 +1,2 @@
+overrides:
+ ags: link:../../../../usr/share/ags/js
diff --git a/repo/shots/apps-window.png b/repo/shots/apps-window.png
index ba03a23e..b826a05a 100644
Binary files a/repo/shots/apps-window.png and b/repo/shots/apps-window.png differ
diff --git a/repo/shots/browser-colorshell.png b/repo/shots/browser-colorshell.png
new file mode 100644
index 00000000..6af3da35
Binary files /dev/null and b/repo/shots/browser-colorshell.png differ
diff --git a/repo/shots/browser.png b/repo/shots/browser.png
deleted file mode 100644
index 3125a428..00000000
Binary files a/repo/shots/browser.png and /dev/null differ
diff --git a/repo/shots/center-window-control-center.png b/repo/shots/center-window-control-center.png
new file mode 100644
index 00000000..ffe1f4a3
Binary files /dev/null and b/repo/shots/center-window-control-center.png differ
diff --git a/repo/shots/center-window.png b/repo/shots/center-window.png
new file mode 100644
index 00000000..a3b04c25
Binary files /dev/null and b/repo/shots/center-window.png differ
diff --git a/repo/shots/clock-window.png b/repo/shots/clock-window.png
deleted file mode 100644
index 3fc6b786..00000000
Binary files a/repo/shots/clock-window.png and /dev/null differ
diff --git a/repo/shots/control-center-pages.png b/repo/shots/control-center-pages.png
new file mode 100644
index 00000000..ec3cfd65
Binary files /dev/null and b/repo/shots/control-center-pages.png differ
diff --git a/repo/shots/control-center-runner.png b/repo/shots/control-center-runner.png
deleted file mode 100644
index 9bdfb539..00000000
Binary files a/repo/shots/control-center-runner.png and /dev/null differ
diff --git a/repo/shots/control-center.png b/repo/shots/control-center.png
index 5e760249..924f4286 100644
Binary files a/repo/shots/control-center.png and b/repo/shots/control-center.png differ
diff --git a/repo/shots/desktop.png b/repo/shots/desktop.png
index ebaf0b5c..468cee61 100644
Binary files a/repo/shots/desktop.png and b/repo/shots/desktop.png differ
diff --git a/repo/shots/kitty.png b/repo/shots/kitty.png
index 50a7c734..eea4ac2d 100644
Binary files a/repo/shots/kitty.png and b/repo/shots/kitty.png differ
diff --git a/repo/shots/neovim.png b/repo/shots/neovim.png
new file mode 100644
index 00000000..7722293a
Binary files /dev/null and b/repo/shots/neovim.png differ
diff --git a/repo/shots/resources.png b/repo/shots/resources.png
new file mode 100644
index 00000000..aaefdbfe
Binary files /dev/null and b/repo/shots/resources.png differ
diff --git a/repo/shots/runner.png b/repo/shots/runner.png
index fbc5cf62..bafd7b21 100644
Binary files a/repo/shots/runner.png and b/repo/shots/runner.png differ
diff --git a/resources.gresource.xml b/resources.gresource.xml
new file mode 100644
index 00000000..dec2c2bc
--- /dev/null
+++ b/resources.gresource.xml
@@ -0,0 +1,30 @@
+
+
+
+
+ styles/main.scss
+ styles/_mixins.scss
+ styles/_colors.scss
+ styles/_apps-window.scss
+ styles/_bar.scss
+ styles/_center-window.scss
+ styles/_control-center.scss
+ styles/_float-notifications.scss
+ styles/_logout-menu.scss
+ styles/_osd.scss
+ styles/_runner.scss
+
+
+
+
+ icons/applications-other-symbolic.svg
+ icons/arrow-circular-top-right-symbolic.svg
+ icons/circle-filled-symbolic.svg
+ icons/hourglass-symbolic.svg
+ icons/loop-arrow-symbolic.svg
+ icons/minus-circle-filled-symbolic.svg
+ icons/shield-danger-symbolic.svg
+ icons/shield-safe-symbolic.svg
+ icons/user-trash-symbolic.svg
+
+
diff --git a/resources/colorshell.desktop b/resources/colorshell.desktop
new file mode 100644
index 00000000..ba684e89
--- /dev/null
+++ b/resources/colorshell.desktop
@@ -0,0 +1,7 @@
+[Desktop Entry]
+Name=colorshell
+Type=Application
+Comment=A Desktop Shell for Hyprland that has colors from your wallpaper!
+Exec=sh -c "$COLORSHELL_BINARY"
+NoDisplay=true
+StartupWMClass=io.github.retrozinndev.colorshell
diff --git a/ags/icons/applications-other-symbolic.svg b/resources/icons/applications-other-symbolic.svg
similarity index 100%
rename from ags/icons/applications-other-symbolic.svg
rename to resources/icons/applications-other-symbolic.svg
diff --git a/ags/icons/arrow-circular-top-right-symbolic.svg b/resources/icons/arrow-circular-top-right-symbolic.svg
similarity index 100%
rename from ags/icons/arrow-circular-top-right-symbolic.svg
rename to resources/icons/arrow-circular-top-right-symbolic.svg
diff --git a/ags/icons/circle-filled-symbolic.svg b/resources/icons/circle-filled-symbolic.svg
similarity index 100%
rename from ags/icons/circle-filled-symbolic.svg
rename to resources/icons/circle-filled-symbolic.svg
diff --git a/ags/icons/hourglass-symbolic.svg b/resources/icons/hourglass-symbolic.svg
similarity index 100%
rename from ags/icons/hourglass-symbolic.svg
rename to resources/icons/hourglass-symbolic.svg
diff --git a/ags/icons/loop-arrow-symbolic.svg b/resources/icons/loop-arrow-symbolic.svg
similarity index 100%
rename from ags/icons/loop-arrow-symbolic.svg
rename to resources/icons/loop-arrow-symbolic.svg
diff --git a/ags/icons/minus-circle-filled-symbolic.svg b/resources/icons/minus-circle-filled-symbolic.svg
similarity index 100%
rename from ags/icons/minus-circle-filled-symbolic.svg
rename to resources/icons/minus-circle-filled-symbolic.svg
diff --git a/ags/icons/shield-danger-symbolic.svg b/resources/icons/shield-danger-symbolic.svg
similarity index 100%
rename from ags/icons/shield-danger-symbolic.svg
rename to resources/icons/shield-danger-symbolic.svg
diff --git a/ags/icons/shield-safe-symbolic.svg b/resources/icons/shield-safe-symbolic.svg
similarity index 100%
rename from ags/icons/shield-safe-symbolic.svg
rename to resources/icons/shield-safe-symbolic.svg
diff --git a/ags/icons/user-trash-symbolic.svg b/resources/icons/user-trash-symbolic.svg
similarity index 100%
rename from ags/icons/user-trash-symbolic.svg
rename to resources/icons/user-trash-symbolic.svg
diff --git a/ags/style/_apps-window.scss b/resources/styles/_apps-window.scss
similarity index 68%
rename from ags/style/_apps-window.scss
rename to resources/styles/_apps-window.scss
index b4caafb1..dc147bbd 100644
--- a/ags/style/_apps-window.scss
+++ b/resources/styles/_apps-window.scss
@@ -1,21 +1,24 @@
@use "sass:color";
-@use "./mixins";
@use "./colors";
-@use "./functions";
-.apps-window-container {
+.apps-window .popup-window-container {
$radius: 48px;
padding: 28px;
background: colors.$bg-translucent;
border-radius: $radius $radius 0 0;
- & > entry {
- background: rgba(colors.$bg-primary, .4);
+ & entry {
+ background: transparent;
margin-bottom: 32px;
min-width: 450px;
padding: 14px;
border-radius: 18px;
+
+ &:focus-within {
+ box-shadow: inset 0 0 0 2px colors.$bg-tertiary;
+ background: rgba(colors.$bg-primary, .2);
+ }
}
& flowbox {
@@ -23,15 +26,15 @@
& > flowboxchild {
& > button {
- padding: 8px;
+ padding: 10px;
border-radius: 24px;
- & icon {
- font-size: 64px;
+ & image {
+ -gtk-icon-size: 64px;
}
& label {
- margin-top: 6px;
+ margin-top: 24px;
text-shadow: 1px 1px 1px rgba(colors.$bg-primary, .2);
font-weight: 500;
}
diff --git a/resources/styles/_bar.scss b/resources/styles/_bar.scss
new file mode 100644
index 00000000..2a7461c0
--- /dev/null
+++ b/resources/styles/_bar.scss
@@ -0,0 +1,210 @@
+@use "sass:color";
+@use "./mixins";
+@use "./colors";
+
+
+$radius: 18px;
+$padding: 4px;
+$color-hover: colors.$bg-primary;
+
+
+@mixin button-reactivity {
+ &:active {
+ box-shadow: 0 0 0 2px $color-hover;
+ }
+}
+
+.bar-container {
+ padding: 6px;
+ padding-bottom: 0px;
+
+ label {
+ @include mixins.reset-props;
+
+ font-size: 12px;
+ font-weight: 600;
+ }
+
+ // Style widget groups
+ & > .bar-centerbox > * {
+ background: rgba(colors.$bg-translucent, .6);
+ border-radius: $radius;
+ padding: 0 $padding;
+
+ & > box:not(.workspaces-row):not(.tray):not(.focused-client):not(.media),
+ & > button {
+ @include button-reactivity;
+
+ border-radius: calc($radius - $padding);
+ margin: $padding 0;
+ padding: 0 8px;
+
+ &:hover {
+ background: $color-hover;
+ }
+ }
+ }
+
+ .workspaces-row {
+ padding: 4px;
+
+ & .workspace {
+ transition: 80ms linear;
+ margin: 3px 0;
+ border-radius: 16px;
+ min-width: 14px;
+ padding: 0 6px;
+ background: colors.$bg-tertiary;
+
+ &:active {
+ border-radius: 10px;
+ }
+
+ & label.id {
+ font-weight: 600;
+ margin-right: 4px;
+ opacity: 0;
+ }
+
+ &.focus {
+ background: colors.$fg-primary;
+ min-width: 32px;
+
+ & label.id {
+ opacity: 0;
+ color: colors.$fg-light;
+ margin-right: 0;
+ }
+ }
+
+ & icon {
+ font-size: 16px;
+ }
+
+ &.show label.id {
+ opacity: 1;
+ }
+
+ &:hover:not(.last-client-icon):not(.focus) {
+ background: color.scale($color: colors.$bg-tertiary, $lightness: 10%);
+ }
+ }
+ }
+
+ .focused-client {
+ padding: 0 6px;
+
+ & image {
+ margin-right: 6px;
+ -gtk-icon-size: 18px;
+ }
+
+ & .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 {
+ background: colors.$bg-primary;
+ }
+
+ .media {
+ $spacing: 5px;
+ $hover-color: color.scale($color: colors.$bg-primary, $lightness: 15%);
+
+ background: colors.$bg-primary;
+ border-radius: calc($radius - $padding);
+ margin: $padding 0;
+ padding: 0 calc($padding + 3px);
+
+ & image.player-icon {
+ -gtk-icon-size: 14px;
+ margin-right: $spacing;
+ }
+
+ & .buttons {
+ margin-left: $spacing;
+ }
+
+ & .button-row {
+ border-top-right-radius: 12px;
+ border-bottom-right-radius: 12px;
+ padding: 4px 0;
+
+ & > button image {
+ margin: 0;
+ -gtk-icon-size: 10px;
+ }
+ }
+
+
+ &:hover {
+ background: $hover-color;
+ }
+
+ &:active {
+ box-shadow: 0 0 0 2px $hover-color;
+ }
+ }
+
+ .tray {
+ padding: 0 6px;
+
+ & .item {
+ all: unset;
+ }
+ }
+
+ .status {
+ @include mixins.reset-props;
+
+ &.open {
+ background: colors.$bg-primary;
+ }
+
+ & image {
+ -gtk-icon-size: 14px;
+ }
+
+ & revealer > 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;
+
+ & image.notification-count {
+ -gtk-icon-size: 6px;
+ margin-top: -14px;
+ }
+ }
+ }
+
+ .apps {
+ min-width: 18px;
+ & image {
+ transition: 120ms linear;
+ -gtk-icon-size: 14px;
+ }
+
+ &.open {
+ background: colors.$bg-primary;
+ }
+ }
+}
diff --git a/ags/style/_center-window.scss b/resources/styles/_center-window.scss
similarity index 54%
rename from ags/style/_center-window.scss
rename to resources/styles/_center-window.scss
index aed536ec..e688cb6b 100644
--- a/ags/style/_center-window.scss
+++ b/resources/styles/_center-window.scss
@@ -1,24 +1,23 @@
@use "sass:color";
@use "./wal";
@use "./colors";
+@use "./mixins";
.popup-window.center-window .center-window-container {
background: colors.$bg-translucent;
border-radius: 24px;
padding: 12px;
- box-shadow: 0 0 6px colors.$bg-translucent;
& .big-media {
- padding: 6px;
-
- & > box > .image {
+ margin-top: 9px;
+ & .image {
background-size: cover;
background-position: center center;
border-radius: 10px;
}
- & > .info {
+ & .info {
padding: {
top: 4px;
bottom: 6px;
@@ -37,25 +36,28 @@
}
& slider {
- background: transparent;
+ all: unset;
+ opacity: 0;
min-height: .6em;
}
& trough {
border-radius: 4px;
- min-height: .6em;
}
& trough highlight {
- border-radius: 4px;
- min-height: .6em;
+ min-height: .65em;
+ border-top-right-radius: 2px;
+ border-bottom-right-radius: 2px;
}
& .bottom {
- & .controls {
+ & .button-row {
margin-top: 9px;
+
& button {
padding: 7px;
+
& label {
font-size: 9px;
}
@@ -85,46 +87,43 @@
}
}
- & .calendar-box {
- & calendar {
- $border-radius: 14px;
- font-weight: 600;
- padding-bottom: 2px;
+ & calendar.view {
+ $border-radius: 14px;
- &.view {
- background: colors.$bg-primary;
- border-radius: $border-radius;
- }
+ font-weight: 600;
+ background: colors.$bg-primary;
+ border-radius: $border-radius;
- &.header {
- background: colors.$bg-secondary;
- border-top-left-radius: $border-radius;
- border-top-right-radius: $border-radius;
- padding: 4px;
- }
+ & header {
+ background: colors.$bg-secondary;
+ padding: 6px;
+ border-top-left-radius: $border-radius;
+ border-top-right-radius: $border-radius;
+ }
- &.button {
- transition: 80ms linear;
- border-radius: 6px;
+ & grid {
+ margin: 4px;
- &:hover {
- background-color: colors.$bg-tertiary;
- }
- }
+ label.day-number {
+ $size: 24px;
- &:selected {
- background: colors.$bg-secondary;
- border-radius: 6px;
+ min-height: $size;
+ min-width: $size;
}
+ }
- &.highlight {
- background: transparent;
- box-shadow: 0 2px 0 -1px rgba(colors.$bg-secondary, .5);
- }
+ & button {
+ transition: 80ms linear;
+ border-radius: 6px;
- &:focus {
- outline: 1px white;
+ &:hover {
+ background-color: colors.$bg-tertiary;
}
}
+
+ & label:selected {
+ background: colors.$bg-secondary;
+ border-radius: 6px;
+ }
}
}
diff --git a/resources/styles/_colors.scss b/resources/styles/_colors.scss
new file mode 100644
index 00000000..e3d450ab
--- /dev/null
+++ b/resources/styles/_colors.scss
@@ -0,0 +1,13 @@
+@use "sass:color";
+@use "./wal";
+
+$bg-primary: color.adjust($color: wal.$color1, $lightness: -34%);
+$bg-secondary: color.adjust($color: wal.$color1, $lightness: -16%);
+$bg-tertiary: color.adjust($color: $bg-secondary, $lightness: 10%);
+$bg-light: wal.$foreground;
+$bg-translucent: color.change($color: $bg-primary, $alpha: 75%);
+$bg-translucent-primary: $bg-translucent;
+$bg-translucent-secondary: color.change($color: $bg-translucent, $alpha: 78%);
+$fg-primary: wal.$foreground;
+$fg-light: $bg-primary;
+$fg-disabled: color.adjust($color: wal.$foreground, $lightness: -11%);
diff --git a/ags/style/_control-center.scss b/resources/styles/_control-center.scss
similarity index 55%
rename from ags/style/_control-center.scss
rename to resources/styles/_control-center.scss
index f454ebe8..2a76c285 100644
--- a/ags/style/_control-center.scss
+++ b/resources/styles/_control-center.scss
@@ -10,7 +10,6 @@
background: colors.$bg-translucent;
border-radius: 28px;
padding: 20px;
- box-shadow: 0 0 6px 1px colors.$bg-translucent;
& > * {
margin: 9px 0;
@@ -18,25 +17,43 @@
&:first-child {
margin-top: 0;
}
+
&:last-child {
margin-bottom: 0;
}
}
- /*& eventbox:focus, & button:focus {
+ & button:focus-visible {
box-shadow: inset 0 0 0 1px colors.$fg-primary;
- }*/
+ }
& .quickactions {
margin-bottom: .8em;
- & .hostname {
- font-size: 15px;
- font-weight: 600;
+ & .user-face {
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: cover;
+ min-height: 32px;
+ min-width: 34px;
+ margin-right: 6px;
+ border-radius: 50%;
+ }
+
+ & .user-host {
+ .user {
+ font-size: 15px;
+ font-weight: 600;
+ }
+ .host {
+ color: colors.$fg-disabled;
+ font-size: 10px;
+ font-weight: 500;
+ }
}
- & > box:not(.button-row) icon {
- font-size: 12px;
+ & > box:not(.button-row) image {
+ -gtk-icon-size: 12px;
color: colors.$fg-disabled;
margin-right: 3px;
}
@@ -64,21 +81,19 @@
}
button {
- @include mixins.hover-shadow2;
-
padding: 4px;
border-radius: 16px;
icon {
font-size: 14px;
}
- }
- & .page .content {
- & > eventbox > box {
- margin: 6px 0;
+ &:hover {
+ background: rgba(colors.$fg-primary, .2);
}
+ }
+ & .page .content {
& label.name {
font-size: 14px;
font-weight: 500;
@@ -91,14 +106,10 @@
& label.sub-header {
margin-top: 6px;
}
-
- & button.default {
- background: colors.$bg-tertiary;
- }
}
}
- & .page {
+ & #page {
transition: 120ms linear;
background: colors.$bg-secondary;
padding: 14px;
@@ -107,12 +118,12 @@
& .header {
margin-bottom: 12px;
- & .top > .title {
+ & .top .title {
font-size: 20px;
font-weight: 600;
}
- & > .description {
+ & .description {
font-size: 12px;
font-weight: 500;
color: colors.$fg-disabled;
@@ -125,13 +136,25 @@
margin-bottom: 4px;
}
- & button {
- @include mixins.hover-shadow;
+ & .page-button, .extra-buttons {
+ & button {
+ padding: 6px;
+ border-radius: 12px;
- padding: 6px;
- border-radius: 12px;
+ &.selected {
+ background: colors.$bg-tertiary;
+ }
- & label {
+ &:hover {
+ background: rgba(colors.$fg-primary, .1);
+ }
+
+ &:active {
+ border-radius: 10px;
+ }
+ }
+
+ & label.title {
font-size: 14px;
}
@@ -161,48 +184,13 @@
& .extra-buttons {
margin-left: 2px;
- & > button {
- border-radius: 12px;
- }
- }
- }
-}
-
-box.history {
- background: colors.$bg-translucent;
- box-shadow: 0 0 6px 1px colors.$bg-translucent;
- border-radius: 24px;
- padding: 18px;
- transition: 120ms linear;
-
- &.hide {
- opacity: 0;
- }
-
- & .notifications {
- & .notification {
- background: colors.$bg-translucent-primary;
- padding: 3px;
- }
- }
-
- & > .button-row {
- margin-top: 12px;
-
- & button {
- padding: 6px;
- &:focus {
- box-shadow: inset 0 0 0 1px colors.$fg-primary;
- }
+ button {
+ border-radius: 10px;
- & icon {
- font-size: 16px;
- }
-
- & label {
- font-size: 12px;
- font-weight: 600;
+ &:active {
+ border-radius: 8px;
+ }
}
}
}
@@ -211,79 +199,103 @@ box.history {
.tiles-container {
@include mixins.reset-props;
- & > flowbox {
- & > flowboxchild .tile {
- $radius: 16px;
+ & .tile {
+ $radius: 18px;
+ $padding: 4px;
- &:not(.toggled) > .toggle-button,
- &:not(.toggled) > button.more {
- @include mixins.hover-shadow;
- background: colors.$bg-primary;
- }
+ background: rgba(colors.$bg-primary, .5);
+ border-radius: $radius;
+ padding: $padding;
+ min-height: 40px;
- &.toggled .toggle-button:hover,
- &.toggled button.more:hover {
- background: colors.$bg-tertiary;
+ & .icon {
+ transition: 120ms ease-in;
+ border-radius: calc($radius - $padding);
+ padding: 8px 12px;
+ margin-right: 6px;
+ background: color.scale($color: colors.$bg-primary, $lightness: 10%);
+
+ & image {
+ -gtk-icon-size: 18px;
}
- &.toggled > .toggle-button,
- &.toggled > button.more {
- background: colors.$bg-secondary;
+ &:hover {
+ background: color.scale($color: colors.$bg-primary, $lightness: 15%);
}
- &.has-more > .toggle-button {
- border-top-right-radius: 0;
- border-bottom-right-radius: 0;
+ &:active {
+ border-radius: calc($radius - $padding - 2px);
}
+ }
- & > .toggle-button {
- border-radius: $radius;
+ & .content {
+ & .title {
+ font-weight: 600;
+ font-size: 15.1px;
+ }
- & .content {
- padding: 8px;
+ & .description {
+ font-size: 12px;
+ color: colors.$fg-disabled;
+ font-weight: 400;
+ }
+ }
- & > .icon {
- margin-right: 6px;
- }
+ & .arrow {
+ -gtk-icon-size: 12px;
+ color: rgba(colors.$fg-disabled, .4);
+ }
- & > .text {
- & > .title {
- font-weight: 600;
- font-size: 15.1px;
- }
+ &:hover {
+ background: color.scale($color: colors.$bg-primary, $lightness: 5%);
+ }
- & > .description {
- font-size: 12px;
- color: colors.$fg-disabled;
- font-weight: 400;
- }
- }
- }
- }
+ &.enabled .icon {
+ background: colors.$bg-secondary;
- & > button.more {
- border-top-right-radius: $radius;
- border-bottom-right-radius: $radius;
-
- & label {
- font-size: 16px;
- }
+ &:hover {
+ background: colors.$bg-tertiary;
}
}
+
+ &:active {
+ border-radius: calc($radius - 2px);
+ }
}
}
-.tile-pages .page {
- transition: 180ms linear;
+.tile-pages #page {
margin-top: 10px;
+}
+
+box.notif-history {
+ background: colors.$bg-translucent;
+ border-radius: 24px;
+ padding: 18px;
+ transition: 120ms linear;
+
+ &.hide {
+ opacity: 0;
+ }
- &.bluetooth {
- button.connected {
- background: colors.$bg-tertiary;
+ & .notifications {
+ & .notification {
+ background: colors.$bg-translucent-primary;
+ padding: 3px;
}
}
- &.revealed {
- padding-top: 12px;
+ & .button-row {
+ margin-top: 12px;
+
+ & button {
+ @include mixins.button-reactive-secondary;
+ padding: 7px;
+
+ & label {
+ font-size: 12px;
+ font-weight: 600;
+ }
+ }
}
}
diff --git a/ags/style/_float-notifications.scss b/resources/styles/_float-notifications.scss
similarity index 100%
rename from ags/style/_float-notifications.scss
rename to resources/styles/_float-notifications.scss
diff --git a/ags/style/_logout-menu.scss b/resources/styles/_logout-menu.scss
similarity index 55%
rename from ags/style/_logout-menu.scss
rename to resources/styles/_logout-menu.scss
index 8d57aaee..d894a9e0 100644
--- a/ags/style/_logout-menu.scss
+++ b/resources/styles/_logout-menu.scss
@@ -1,6 +1,6 @@
@use "./colors";
-.logout-menu {
+.logout-menu-container {
background: rgba($color: colors.$bg-translucent-primary, $alpha: .4);
.top {
@@ -17,31 +17,35 @@
}
}
.button-row {
+ $radius: 32px;
+
+ all: unset;
margin: 0 150px;
& > button {
- & icon {
- font-size: 128px;
+ & image {
+ -gtk-icon-size: 128px;
}
- &:focus {
+ &:focus-visible {
box-shadow: inset 0 0 0 5px colors.$fg-primary;
}
- margin: {
- left: 4px;
- right: 4px;
+ &:active {
+ border-radius: calc($radius - 6px);
}
+
+ margin: 0 4px;
border-radius: 6px;
- &:first-child {
- border-top-left-radius: 28px;
- border-bottom-left-radius: 28px;
+ &:first-child:not(:active) {
+ border-top-left-radius: $radius;
+ border-bottom-left-radius: $radius;
}
- &:last-child {
- border-top-right-radius: 28px;
- border-bottom-right-radius: 28px;
+ &:last-child:not(:active) {
+ border-top-right-radius: $radius;
+ border-bottom-right-radius: $radius;
}
}
}
diff --git a/ags/style/_mixins.scss b/resources/styles/_mixins.scss
similarity index 51%
rename from ags/style/_mixins.scss
rename to resources/styles/_mixins.scss
index 0f4a2c38..bf3cea93 100644
--- a/ags/style/_mixins.scss
+++ b/resources/styles/_mixins.scss
@@ -1,7 +1,5 @@
@use "sass:color";
-@use "./wal";
@use "./colors";
-@use "./functions" as funs;
@mixin reset-props {
all: unset;
@@ -14,20 +12,30 @@
color: colors.$fg-primary;
}
-@mixin hover-shadow {
+@mixin button-reactive-primary {
+ background: colors.$bg-primary;
+ border-radius: 14px;
+ padding: 6px;
+
&:hover {
- box-shadow: inset 0 0 0 500px rgba(colors.$fg-primary, .1);
+ background: colors.$bg-secondary;
}
-}
-@mixin hover-shadow2 {
- &:hover {
- box-shadow: inset 0 0 0 500px rgba(colors.$fg-primary, .2);
+ &:active {
+ border-radius: 10px;
}
}
-@mixin hover-shadow3 {
+@mixin button-reactive-secondary {
+ background: colors.$bg-secondary;
+ border-radius: 12px;
+ padding: 6px;
+
&:hover {
- box-shadow: inset 0 0 0 500px rgba(colors.$fg-primary, .3);
+ background: colors.$bg-tertiary;
+ }
+
+ &:active {
+ border-radius: 9px;
}
}
diff --git a/resources/styles/_osd.scss b/resources/styles/_osd.scss
new file mode 100644
index 00000000..21dc72c1
--- /dev/null
+++ b/resources/styles/_osd.scss
@@ -0,0 +1,34 @@
+@use "sass:color";
+@use "colors";
+
+.osd {
+ background: rgba(colors.$bg-translucent-secondary, .6);
+ padding: 14px 14px;
+ border-radius: 24px;
+ min-width: 180px;
+
+ .icon {
+ margin-right: 10px;
+ -gtk-icon-size: 24px;
+ }
+
+ .level {
+ margin-top: -6px;
+
+ .text {
+ margin-bottom: 4px;
+ font-size: 14px;
+ font-weight: 600;
+ }
+
+ levelbar trough block {
+ border-radius: 4px;
+ background: colors.$bg-primary;
+
+ &.filled {
+ min-height: 10px;
+ background: colors.$bg-secondary;
+ }
+ }
+ }
+}
diff --git a/resources/styles/_runner.scss b/resources/styles/_runner.scss
new file mode 100644
index 00000000..ada85594
--- /dev/null
+++ b/resources/styles/_runner.scss
@@ -0,0 +1,73 @@
+@use "./colors";
+
+.runner .popup-window-container {
+ all: unset;
+
+ $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 {
+ margin-right: 6px;
+ -gtk-icon-size: 16px;
+ }
+ }
+
+ & scrolledwindow {
+ margin: 6px;
+ }
+
+ & list {
+ & .result {
+ padding: 10px;
+ background: colors.$bg-primary;
+ margin: 2px 0;
+ border-radius: 14px;
+
+ & image {
+ -gtk-icon-size: 28px;
+ margin-right: 6px;
+ }
+
+ & .title {
+ font-weight: 500;
+ font-size: 16px;
+ }
+
+ & .description {
+ font-size: 12px;
+ color: colors.$fg-disabled;
+ }
+ }
+
+ & > *:selected .result,
+ & > *:active .result,
+ & > *:hover .result {
+ background: colors.$bg-secondary;
+ }
+
+ & > *:first-child {
+ margin-top: 12px;
+ }
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+}
diff --git a/ags/style.scss b/resources/styles/main.scss
similarity index 81%
rename from ags/style.scss
rename to resources/styles/main.scss
index 1eab0543..9b862b34 100644
--- a/ags/style.scss
+++ b/resources/styles/main.scss
@@ -17,10 +17,6 @@
* {
@include mixins.reset-props;
-
- /*&:focus {
- box-shadow: inset 0 0 0 2px colors.$fg-primary;
- }*/
}
entry {
@@ -55,6 +51,7 @@ entry {
& .options {
& button {
+ @include mixins.button-reactive-primary;
background: colors.$bg-primary;
border-radius: 12px;
padding: 9px 6px;
@@ -68,14 +65,6 @@ entry {
left: 4px;
right: 4px;
};
-
- &:hover {
- background: colors.$bg-secondary;
- }
-
- &:focus {
- box-shadow: inset 0 0 0 2px colors.$fg-primary;
- }
}
}
@@ -121,10 +110,6 @@ entry {
&:hover {
background: colors.$bg-secondary;
}
-
- &:focus {
- box-shadow: inset 0 0 0 1px colors.$fg-primary;
- }
}
& icon.close {
@@ -162,8 +147,6 @@ entry {
padding: 6px;
& button.action {
- @include mixins.hover-shadow;
-
border-radius: 4px;
background: colors.$bg-secondary;
padding: 6px;
@@ -173,6 +156,10 @@ entry {
font-weight: 600;
}
+ &:hover {
+ background: colors.$bg-tertiary;
+ }
+
&:first-child {
border-top-left-radius: 12px;
border-bottom-left-radius: 12px;
@@ -200,49 +187,66 @@ tooltip > box {
}
}
-menu {
- padding: 4px;
+popover.menu contents {
background: wal.$background;
border-radius: 14px;
-
- & separator {
- margin: 0 4px;
- color: wal.$background;
- }
-
- & menuitem {
- padding: 8px 16px;
- border-radius: 10px;
- font-size: 12px;
- font-weight: 600;
-
- &:hover, &:focus {
- background: wal.$color1;
+ padding: 4px;
+
+ & viewport > stack > * > * > * {
+ & > separator {
+ min-height: .5px;
+ margin: 3px 2px;
+ background: rgba(colors.$fg-disabled, .1);
+ }
+
+ & > *:not(separator) > * {
+ padding: 6px;
+ border-radius: 10px;
+ font-size: 12px;
+ font-weight: 600;
+
+ &:hover, &:focus {
+ background: wal.$color1;
+ }
}
}
}
.button-row {
& > button {
+ $active-radius: 8px;
+ $corner-radius: calc($active-radius + 2px);
+
background: colors.$bg-secondary;
margin: 0 1px;
padding: 4px 6px;
border-radius: 2px;
+ transition: 120ms linear;
&:hover {
background: colors.$bg-tertiary;
}
+ &:active {
+ border-radius: $active-radius;
+ }
+
&:first-child {
- border-top-left-radius: 10px;
- border-bottom-left-radius: 10px;
margin-left: 0;
+
+ &:not(:active) {
+ border-top-left-radius: $corner-radius;
+ border-bottom-left-radius: $corner-radius;
+ }
}
&:last-child {
- border-top-right-radius: 10px;
- border-bottom-right-radius: 10px;
margin-right: 0;
+
+ &:not(:active) {
+ border-top-right-radius: $corner-radius;
+ border-bottom-right-radius: $corner-radius;
+ }
}
}
}
@@ -252,7 +256,7 @@ selection {
}
trough {
- background: functions.toRGB(color.adjust($color: wal.$color1, $lightness: -20%));
+ background: color.adjust($color: colors.$bg-primary, $lightness: -5%);
border-radius: 8px;
margin: 2px 0;
}
@@ -260,15 +264,12 @@ trough {
trough highlight {
background: wal.$color1;
min-height: .9em;
- border-top-left-radius: inherit;
- border-bottom-left-radius: inherit;
}
trough slider {
border-radius: 50%;
- margin: -2px 0;
+ margin: -4px 0;
background: wal.$foreground;
- margin-left: -1px;
min-width: 1.2em;
min-height: 1.2em;
}
diff --git a/resources/wallpaper_default.jpg b/resources/wallpaper_default.jpg
new file mode 100644
index 00000000..b3f48faf
Binary files /dev/null and b/resources/wallpaper_default.jpg differ
diff --git a/scripts/build.sh b/scripts/build.sh
new file mode 100644
index 00000000..db9790f1
--- /dev/null
+++ b/scripts/build.sh
@@ -0,0 +1,57 @@
+
+set -e
+
+output="./build"
+
+while getopts r:o:bdh args; do
+ case "$args" in
+ r)
+ gresources_target=${OPTARG}
+ ;;
+ b)
+ keep_gresource=true
+ ;;
+ o)
+ output=${OPTARG}
+ ;;
+ d)
+ is_devel=true
+ ;;
+ h)
+ echo "\
+colorshell's build script.
+use \`build:release\` for release builds.
+
+options:
+ -r \$file: specify gresource's target path (default: \`\$output/resources.gresource\`)
+ -o \$path: specify the build's output directory (default: \`./build\`)
+ -b: only target gresource in the build, keeping the file in the output dir
+ -d: enable developer mode in the build
+ -h: show this help message"
+ exit 0
+ ;;
+ esac
+done
+
+if [[ -d "$output" ]] && [[ ! -z "$(ls -A -w1 $output)" ]]; then
+ echo "[info] cleaning previous build"
+ rm -rf $output/*
+else
+ mkdir -p $output
+fi
+
+echo "[info] compiling gresource"
+gres_target=`[[ "$keep_gresource" ]] && echo -n "$output/resources.gresource" || \
+ echo -n "${gresources_target:-$output/resources.gresource}"`
+mkdir -p `dirname "$gres_target"`
+glib-compile-resources resources.gresource.xml \
+ --sourcedir ./resources \
+ --target "$gres_target"
+
+echo "[info] bundling project"
+ags --gtk 4 bundle src/app.ts $output/colorshell \
+ -r ./src \
+ -d "DEVEL=`[[ $is_devel ]] && echo -n true || echo -n false`" \
+ -d "COLORSHELL_VERSION='`cat package.json | jq -r .version`'" \
+ -d "GRESOURCES_FILE='${gresources_target:-$output/resources.gresource}'" \
+|| rm -rf src/node_modules
diff --git a/scripts/clean.sh b/scripts/clean.sh
new file mode 100644
index 00000000..aee88ebd
--- /dev/null
+++ b/scripts/clean.sh
@@ -0,0 +1,6 @@
+set -e
+
+builddir="${1:-./build}"
+
+echo "[info] cleaning build dir: \"$builddir\""
+rm -r "$builddir"
diff --git a/scripts/release.sh b/scripts/release.sh
new file mode 100644
index 00000000..506026a5
--- /dev/null
+++ b/scripts/release.sh
@@ -0,0 +1,68 @@
+set -e
+
+socket_support=true
+
+while getopts o:r:e:hn args; do
+ case "$args" in
+ o)
+ outdir=${OPTARG}
+ ;;
+ r)
+ gresource_file=${OPTARG}
+ ;;
+ e)
+ bin_target=${OPTARG}
+ ;;
+ n)
+ unset socket_support
+ ;;
+ ? | h)
+ echo "\
+colorshell's automated release-build script.
+
+help:
+ 'literal': the argument should have environment variables sent as a literal string,
+ they're replaced at runtime.
+ 'default': argument's default value, they're used if none are provided.
+
+options:
+ -r \$file: gresource's target path (literal; kept in \$output; default: \$XDG_DATA_HOME/colorshell/resources.gresource)
+ -n: disable socket communication support(use the slower remote instance communication)
+ -o \$path: build output path (default: \`./build/release\`)
+ -e: set desktop entry's executable target (literal; default: \$HOME/.local/bin/colorshell)
+ -h: show this help message"
+ exit 0
+ ;;
+ esac
+done
+
+# send literal variable name, so it's interpreted at runtime
+sh ./scripts/build.sh -o "${outdir:-./build/release}" -b -r "${gresource_file:-\$XDG_DATA_HOME/colorshell/resources.gresource}"
+
+# add socket-communication support on executable
+if [[ $socket_support ]]; then
+ echo "[info] adding socket communication support"
+ script="\
+#!/usr/bin/bash
+
+if gdbus introspect --session \\
+ --dest io.github.retrozinndev.colorshell \\
+ --object-path /io/github/retrozinndev/colorshell > /dev/null 2>&1; then
+
+ if command -v socat > /dev/null 2>&1; then
+ echo \"\$@\" | socat - \"\${XDG_RUNTIME_DIR:-/run/user/\$(id -u)}/colorshell.sock\"
+ exit 0
+ else
+ echo \"[warn] \`socat\` not installed, falling back to remote instance communication\"
+ fi
+fi
+`cat "${outdir:-./build/release}/colorshell" | sed -e 's/^#.*//'`" # remove shebang
+
+ echo -en "$script" > "${outdir:-./build/release}/colorshell"
+ chmod +x "${outdir:-./build/release}/colorshell"
+fi
+
+echo "[info] making desktop entry"
+entry=`cat ./resources/colorshell.desktop`
+bin_target=${bin_target:-'$HOME/.local/bin/colorshell'}
+echo -n "${entry/'$COLORSHELL_BINARY'/${bin_target/'$'/'\\\$'}}" > ${outdir:-./build/release}/colorshell.desktop
diff --git a/scripts/start.sh b/scripts/start.sh
new file mode 100644
index 00000000..afdfc6d1
--- /dev/null
+++ b/scripts/start.sh
@@ -0,0 +1,29 @@
+file="${1:-./build/colorshell}"
+
+function is_running() {
+ if gdbus introspect --session \
+ --dest io.github.retrozinndev.colorshell \
+ --object-path /io/github/retrozinndev/colorshell > /dev/null 2>&1
+ then
+ return 0
+ fi
+
+ return 1
+}
+
+function start() {
+ if is_running; then
+ echo "[info] killing previous instance"
+ colorshell quit || killall gjs
+ fi
+ echo "[info] starting"
+ exec "$file"
+}
+
+if [[ -f $file ]]; then
+ start
+else
+ echo "[error] can't start project: no executable found on default directory"
+ echo "[tip] specify the executable path: start \"\$path\""
+ exit 1
+fi
diff --git a/scripts/sync-config.sh b/scripts/sync-config.sh
new file mode 100644
index 00000000..e4ba8a72
--- /dev/null
+++ b/scripts/sync-config.sh
@@ -0,0 +1,60 @@
+source ./scripts/utils.sh
+
+config_dirs=(
+ "hypr/scripts"
+ "hypr/shell"
+ "hypr/hyprlock.conf"
+ "hypr/hyprland.conf"
+ "hypr/hypridle.conf"
+ "kitty/kitty.conf"
+)
+outdir="./config"
+
+Clean_local() {
+ Send_log "info" "Cleaning local config..."
+ for dir in ${config_dirs[@]}; do
+ rm -rf $outdir/$dir
+ done
+}
+
+Update_local() {
+ mkdir -p $outdir
+ for dir in ${config_dirs[@]}; do
+ if [[ -d "$XDG_CONFIG_HOME/$dir" ]] || [[ -f "$XDG_CONFIG_HOME/$dir" ]]; then
+ Send_log "Copying ${dir^}"
+ mkdir -p `dirname "$outdir/$dir"`
+ cp -r $XDG_CONFIG_HOME/$dir $outdir/$dir
+ else
+ Send_log "warn" "Looks like the ${dir^} dir is in fault! Skipping..."
+ fi
+ done
+}
+
+Print_header
+
+printf "\n"
+echo "!!WARNING!! Running this script may override all configuration data in current repo with host ones."
+echo "This script is intended to be used only by the repository owner"
+printf "\n"
+
+echo "Please run this script in it's current directory to avoid issues"
+echo "Tip: Press [Ctrl] + [C] to stop script at any time"
+
+printf "\n"
+
+Ask "Update local repository with host configurations?"
+if [[ ! $answer == y ]]; then
+ Send_log "Exiting"
+ exit 1
+fi
+
+printf "\n"
+
+Clean_local
+Update_local
+
+if command -v git > /dev/null; then
+ git status
+fi
+
+exit 0
diff --git a/scripts/types.sh b/scripts/types.sh
new file mode 100644
index 00000000..27234f16
--- /dev/null
+++ b/scripts/types.sh
@@ -0,0 +1,8 @@
+if [[ -d "@types" ]] && [[ ! "$1" == "-f" ]]; then
+ echo "Types skipped(already built). To force-build, run \`types\`"
+ exit 0
+fi
+
+
+echo "Building types, this can take long..."
+pnpx @ts-for-gir/cli generate --ignoreVersionConflicts
diff --git a/utils.sh b/scripts/utils.sh
similarity index 52%
rename from utils.sh
rename to scripts/utils.sh
index ade010a0..44d34de6 100644
--- a/utils.sh
+++ b/scripts/utils.sh
@@ -1,27 +1,15 @@
-#!/usr/bin/env bash
-
# This script contains useful functions to be used
-# in other scripts from retrozindev's dotfiles.
+# in other scripts from colorshell.
# ----------
# Made by retrozinndev (João Dias)
-# Licensed under the MIT License
+# Licensed under the BSD 3-Clause License
# From: https://github.com/retrozinndev/colorshell
# -------------
-# Array containing directory names to be used by
-# retrozinndev/colorshell install and update
-# scripts.
+# The repository's api url
# -------------
-config_dirs=(
- "hypr/scripts"
- "hypr/shell"
- "hypr/hyprlock.conf"
- "hypr/hyprland.conf"
- "hypr/hypridle.conf"
- "kitty/kitty.conf"
- "ags"
-)
+repo_api_url=https://api.github.com/repos/retrozinndev/colorshell
# -------------
# Sends stdout log with type and message provided
@@ -30,29 +18,22 @@ config_dirs=(
# param $2 log message
# -------------
function Send_log() {
- log_message=$2
+ log_message=`[[ -z $2 ]] && echo $1 || echo $2`
+ color="\e[34m"
+ log_type="info"
- case ${1,,} in
- "^warn(ing)$")
+ case "${1,,}" in
+ warn)
color="\e[33m"
log_type="warning"
;;
- "^err(or)$")
+ err)
color="\e[31m"
log_type="error"
;;
-
- *)
- color="\e[34m"
- log_type="info"
- ;;
esac
- if [[ -z $2 ]]; then
- log_message=$1
- fi
-
echo -e "${color}[$log_type]\e[0m $log_message"
}
@@ -67,3 +48,28 @@ function Print_header() {
echo "#############################"
printf "\n"
}
+
+# -------------
+# Ask a yes/no question to user
+# Input answer is exported as $answer
+# -------------
+function Ask() {
+ read -n 1 -p "$@ [y/n] " answer
+ printf '\n'
+ if [[ ! $answer =~ [yn] ]]; then
+ Ask "$@" # restart if different from accepted chars
+ fi
+
+ export answer
+}
+
+# -------------
+# Ask the user to choose a number from the provided list
+# Input answer is exported as $answer
+# (this function is not done yet)
+# -------------
+function Choose() {
+ read -n 1 -p "$1 [y/n] " r
+ printf '\n'
+ export answer=$r
+}
diff --git a/src/app.ts b/src/app.ts
new file mode 100644
index 00000000..30dbc618
--- /dev/null
+++ b/src/app.ts
@@ -0,0 +1,336 @@
+// thanks Aylur!!
+import "../node_modules/ags/lib/overrides";
+import "./config";
+import {
+ PluginApps,
+ PluginClipboard,
+ PluginMedia,
+ PluginShell,
+ PluginWallpapers,
+ PluginWebSearch,
+ PluginKill
+} from "./runner/plugins";
+import { handleArguments } from "./modules/arg-handler";
+import { Runner } from "./runner/Runner";
+import { Windows } from "./windows";
+import { Notifications } from "./modules/notifications";
+import { Wallpaper } from "./modules/wallpaper";
+import { Stylesheet } from "./modules/stylesheet";
+import { Clipboard } from "./modules/clipboard";
+import { Gdk, Gtk } from "ags/gtk4";
+import { createBinding, createComputed, createRoot, getScope, Scope } from "ags";
+import { OSDModes, triggerOSD } from "./window/osd";
+import { programArgs, programInvocationName } from "system";
+import { setConsoleLogDomain } from "console";
+import { createSubscription, encoder, secureBaseBinding } from "./modules/utils";
+import { exec } from "ags/process";
+import { NightLight } from "./modules/nightlight";
+import { Backlights } from "./modules/backlight";
+import GObject, { register } from "ags/gobject";
+
+import Media from "./modules/media";
+import GLib from "gi://GLib?version=2.0";
+import Gio from "gi://Gio?version=2.0";
+import Adw from "gi://Adw?version=1";
+import AstalWp from "gi://AstalWp";
+
+
+const runnerPlugins: Array = [
+ PluginApps,
+ PluginShell,
+ PluginWebSearch,
+ PluginKill,
+ PluginMedia,
+ PluginWallpapers,
+ PluginClipboard
+];
+
+const defaultWindows: Array = [ "bar" ];
+
+GLib.unsetenv("LD_PRELOAD");
+
+
+@register({ GTypeName: "Shell" })
+export class Shell extends Adw.Application {
+ private static instance: Shell;
+
+ #scope!: Scope;
+ #connections = new Map | number>();
+ #providers: Array = [];
+ #gresource: Gio.Resource|null = null;
+ #socketService!: Gio.SocketService;
+ #socketFile!: Gio.File;
+
+ get scope() { return this.#scope; }
+
+ constructor() {
+ super({
+ applicationId: "io.github.retrozinndev.colorshell",
+ flags: Gio.ApplicationFlags.HANDLES_COMMAND_LINE,
+ version: COLORSHELL_VERSION ?? "0.0.0-unknown",
+ });
+
+ setConsoleLogDomain("colorshell");
+ GLib.set_application_name("colorshell");
+ }
+
+ public static getDefault(): Shell {
+ if(!this.instance)
+ this.instance = new Shell();
+
+ return this.instance;
+ }
+
+ public resetStyle(): void {
+ this.#providers.forEach(provider =>
+ Gtk.StyleContext.remove_provider_for_display(
+ Gdk.Display.get_default()!,
+ provider
+ )
+ );
+ }
+
+ public removeProvider(provider: Gtk.CssProvider): void {
+ if(!this.#providers.includes(provider)) {
+ console.warn("Colorshell: Couldn't find the provided GtkCssProvider to remove. Was it added before?");
+ return;
+ }
+
+ for(let i = 0; i < this.#providers.length; i++) {
+ const prov = this.#providers[i];
+ if(prov === provider) {
+ this.#providers.splice(i, 1);
+ Gtk.StyleContext.remove_provider_for_display(
+ Gdk.Display.get_default()!,
+ provider
+ );
+ break;
+ }
+ }
+ }
+
+ public applyStyle(stylesheet: string): void {
+ try {
+ const provider = Gtk.CssProvider.new();
+ provider.load_from_string(stylesheet)
+ this.#providers.push(provider);
+
+ Gtk.StyleContext.add_provider_for_display(
+ Gdk.Display.get_default()!,
+ provider,
+ Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
+ );
+ } catch(e) {
+ console.error(`Colorshell: Couldn't apply style. Stderr: ${e}`);
+ return;
+ }
+ }
+
+ vfunc_command_line(cmd: Gio.ApplicationCommandLine): number {
+ const args = cmd.get_arguments().toSpliced(0, 1); // remove executable
+
+ if(cmd.isRemote) {
+ try {
+ // warn user that this method is pretty slow
+ cmd.print_literal("\nColorshell: !! Using a remote instance to communicate is pretty slow, \
+you should use the socket in the XDG_RUNTIME_DIR/colorshell.sock for a faster response.\n\n");
+
+ const res = handleArguments(cmd, args);
+
+ cmd.done();
+ cmd.set_exit_status(res);
+ return res;
+ } catch(_e) {
+ const e = _e as Error;
+ cmd.printerr_literal(`Error: something went wrong! Stderr: ${e.message}\n${e.stack}`);
+ cmd.done();
+ return 1;
+ }
+ } else {
+ if(args.length > 0) {
+ cmd.printerr_literal("Error: colorshell not running. Try to clean-run before using arguments");
+ cmd.done();
+ return 1;
+ }
+
+ this.activate();
+ }
+
+ return 0;
+ }
+
+ vfunc_activate(): void {
+ super.vfunc_activate();
+ this.hold();
+ this.main();
+ }
+
+ private init(): void {
+ // load gresource from build-defined path
+ try {
+ this.#gresource = Gio.Resource.load(GRESOURCES_FILE.split('/').filter(s =>
+ s !== ""
+ ).map(path => {
+ // support environment variables at runtime
+ if(/^\$/.test(path)) {
+ const env = GLib.getenv(path.replace(/^\$/, ""));
+ if(env === null)
+ throw new Error(`Couldn't get environment variable: ${path}`);
+
+ return env;
+ }
+
+ return path;
+ }).join('/'));
+ Gio.resources_register(this.#gresource);
+
+ // add icons
+ Gtk.IconTheme.get_for_display(Gdk.Display.get_default()!)
+ .add_resource_path("/io/github/retrozinndev/colorshell/icons")
+ } catch(_e) {
+ const e = _e as Error;
+ console.error(`Error: couldn't load gresource! Stderr: ${e.message}\n${e.stack}`);
+ }
+
+ this.#socketFile = Gio.File.new_for_path(`${GLib.get_user_runtime_dir() ??
+ `/run/user/${exec("id -u").trim()}`}/colorshell.sock`);
+
+ if(this.#socketFile.query_exists(null)) {
+ console.log(`Colorshell: Deleting previous instance's socket`);
+ this.#socketFile.delete(null);
+ }
+
+ this.#socketService = Gio.SocketService.new();
+ this.#socketService.add_address(
+ Gio.UnixSocketAddress.new(this.#socketFile.get_path()!),
+ Gio.SocketType.STREAM,
+ Gio.SocketProtocol.DEFAULT,
+ null
+ );
+
+ // handle communication via socket
+ this.#connections.set(this.#socketService,
+ this.#socketService.connect("incoming", (_, conn) => {
+ const inputStream = Gio.DataInputStream.new(conn.inputStream);
+ inputStream.read_upto_async('\x00', -1, GLib.PRIORITY_DEFAULT, null, (_, res) => {
+ const [args, len] = inputStream.read_upto_finish(res);
+ inputStream.close(null);
+ conn.inputStream.close(null);
+
+ if(len < 1) {
+ console.error(`Colorshell: No args provided via socket call`);
+ return;
+ }
+
+ try {
+ const [success, parsedArgs] = GLib.shell_parse_argv(`colorshell ${args}`);
+ parsedArgs?.splice(0, 1); // remove the unnecessary `colorshell` part
+
+ if(success) {
+ handleArguments({
+ print_literal: (msg) => conn.outputStream.write_bytes(
+ encoder.encode(`${msg}\n`),
+ null
+ ),
+ // TODO: support writing to stderr(i don't know how to do that :sob:)
+ printerr_literal: (msg) => conn.outputStream.write_bytes(
+ encoder.encode(`${msg}\n`),
+ null
+ )
+ }, parsedArgs!);
+
+ conn.outputStream.flush(null);
+ conn.close(null);
+ return;
+ }
+
+ conn.outputStream.write_bytes(
+ encoder.encode("Error: Unexpected error occurred on argument parsing!"),
+ null
+ );
+
+ conn.outputStream.flush(null);
+ conn.close(null);
+ } catch(_e) {
+ const e = _e as Error;
+ console.error(`Colorshell: An error occurred while writing to socket output. Stderr:\n${
+ e.message}\n${e.stack}`);
+ }
+ });
+
+ return false;
+ })
+ );
+ }
+
+ private main(): void {
+ Gtk.init();
+ Adw.init();
+ this.init();
+
+ createRoot((dispose) => {
+ console.log(`Colorshell: Initializing things`);
+ this.#connections.set(this, this.connect("shutdown", () => dispose()));
+ this.#scope = getScope();
+
+ NightLight.getDefault();
+
+ Media.getDefault();
+ Clipboard.getDefault();
+
+ console.log("Colorshell: Initializing Wallpaper and Stylesheet modules");
+ Wallpaper.getDefault();
+ Stylesheet.getDefault();
+
+ console.log("Adding runner plugins");
+ runnerPlugins.forEach(plugin => Runner.addPlugin(plugin));
+
+ createSubscription(
+ createComputed([
+ secureBaseBinding(createBinding(
+ AstalWp.get_default(), "defaultSpeaker"
+ ), "volume", null),
+ secureBaseBinding(createBinding(
+ AstalWp.get_default(), "defaultSpeaker"
+ ), "mute", null)
+ ]),
+ () => !Windows.getDefault().isOpen("control-center") &&
+ triggerOSD(OSDModes.sink)
+ );
+
+ createSubscription(
+ secureBaseBinding(
+ createBinding(Backlights.getDefault(), "default"),
+ "brightness",
+ 100
+ ),
+ () => !Windows.getDefault().isOpen("control-center") &&
+ triggerOSD(OSDModes.brightness)
+ );
+
+ this.#connections.set(Notifications.getDefault(), [
+ Notifications.getDefault().connect("notification-added", () => {
+ Windows.getDefault().open("floating-notifications");
+ }),
+ Notifications.getDefault().connect("notification-removed", (self) => {
+ self.notifications.length === 0 && Windows.getDefault().close("floating-notifications");
+ })
+ ]);
+
+ defaultWindows.forEach(w => Windows.getDefault().open(w));
+ });
+
+ this.#scope.onCleanup(() => {
+ console.log("Colorshell: disposing connections and quitting because of ::shutdown");
+ this.#connections.forEach((ids, obj) => Array.isArray(ids) ?
+ ids.forEach(id => obj.disconnect(id))
+ : obj.disconnect(ids));
+ });
+ }
+
+ quit(): void {
+ this.release();
+ }
+}
+
+Shell.getDefault().runAsync([ programInvocationName, ...programArgs ]);
diff --git a/src/cli/index.ts b/src/cli/index.ts
new file mode 100644
index 00000000..5748bb59
--- /dev/null
+++ b/src/cli/index.ts
@@ -0,0 +1,264 @@
+import { Scope } from "ags";
+import { createScopedConnection, decoder, encoder } from "../modules/utils";
+import { showWorkspaceNumber } from "../window/bar/widgets/Workspaces";
+
+import windows from "./modules/windows";
+import volume from "./modules/volume";
+import devel from "./modules/devel";
+import media from "./modules/media";
+import Gio from "gi://Gio?version=2.0";
+import GLib from "gi://GLib?version=2.0";
+
+
+/** cli implementation for colorshell */
+export namespace Cli {
+ let rootScope: Scope;
+ let initialized: boolean = false;
+ const modules: Array = [
+ // main module, no need for prefix
+ {
+ help: "manage colorshell windows and do more cool stuff.",
+ commands: [
+ ...windows,
+ // others
+ {
+ name: "runner",
+ onCalled: (_, data) => {
+ return {
+ content: `Opening runner${data ? ` with "${data}"` : ""}...`,
+ type: "out"
+ };
+ }
+ },
+ {
+ name: "peek-workspace-num",
+ help: "peek the workspace numbers in the workspace indicator. (optional: time in millis)",
+ onCalled: () => {
+ showWorkspaceNumber(true);
+ return "Peeking workspace IDs...";
+ }
+ }
+ ],
+ arguments: [
+ {
+ name: "version",
+ alias: "v",
+ help: "print the current colorshell version",
+ onCalled: () => `colorshell by retrozinndev, version ${COLORSHELL_VERSION
+ }${DEVEL ? "(devel)" : ""}`
+ }
+ ]
+ },
+ volume,
+ media
+ ];
+
+ export type Output = {
+ type: "err"|"out";
+ content: string|Uint8Array;
+ } | string;
+
+ /** argument passed to the command / module.
+ * output of onCalled is passed to */
+ export type Argument = {
+ /** kebab-cased name for the argument(without the `--` prefix)
+ * @example help (turns into `--help` internally)*/
+ name: string;
+ /** alias for the name (without the `-` prefix).
+ * @example help -> h */
+ alias?: string;
+ /** whether the argument needs a value attribute.
+ * @example --file ~/a_nice_home_file.txt */
+ hasValue?: boolean;
+ /** runtime-set value for the argument(if enabled) */
+ value?: string;
+ /** help message for the argument */
+ help?: string;
+ onCalled?: (value?: string) => void;
+ };
+
+ export type ArgumentData = {
+ argument: Argument;
+ data?: string;
+ };
+
+ export type Command = {
+ /** the command name to be called.
+ * @example `colorshell ${prefix?} ${command.name}`*/
+ name: string;
+ help?: string;
+ /** data passed to the command. (only works when arguments are disabled) */
+ data?: string;
+ arguments?: Array;
+ onCalled: (args: Array, data?: string) => Output;
+ };
+
+ export type Module = {
+ /** command to come after the cli call.
+ * @example `colorshell ${prefix?} ${command}`*/
+ prefix?: string;
+ commands?: Array;
+ arguments?: Array;
+ help?: string;
+ /** called everytime the prefix is used, even when using module commands */
+ onPrefixCalled?: () => void;
+ };
+
+ /** initialize the cli */
+ export function init(scope: Scope, communicationMethod: Gio.SocketService|Gio.ApplicationCommandLine, app?: Gio.Application): void {
+ if(initialized) return;
+
+ initialized = true;
+ rootScope = scope;
+ DEVEL && modules.push(devel);
+
+ scope.run(() => {
+ if(communicationMethod instanceof Gio.SocketService) {
+ createScopedConnection(
+ communicationMethod, "incoming", (conn) => {
+ try {
+ return handleIncoming(conn);
+ } catch(_) {}
+
+ return false;
+ }
+ );
+
+ return;
+ }
+
+ if(!app)
+ throw new Error("GApplication not specified for GApplicationCommandLine communication method")
+ if(app.flags !& Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
+ throw new Error("GApplication does not have the HANDLES_COMMAND_LINE flag or doesn't implement it")
+
+ createScopedConnection(
+ app,
+ "command-line",
+ (cmd) => {
+ let hasError: boolean = false;
+ try {
+ handleArgs(
+ cmd.get_arguments().toSpliced(0, 1),
+ (str, type) => {
+ if(type === "err") {
+ cmd.printerr_literal(str);
+ hasError = true;
+ return;
+ }
+
+ cmd.print_literal(str);
+ }
+ );
+ } catch(_) {
+ // TODO better error message
+ hasError = true;
+ }
+
+ return hasError ? 1 : 0;
+ }
+ );
+ });
+ }
+
+ /** handle incoming socket calls */
+ function handleIncoming(conn: Gio.SocketConnection): void {
+ const inputStream = Gio.DataInputStream.new(conn.inputStream);
+
+ inputStream.read_upto_async('\x00', -1, GLib.PRIORITY_DEFAULT, null, (_, res) => {
+ const [args, len] = inputStream.read_upto_finish(res);
+ inputStream.close(null);
+ conn.inputStream.close(null);
+
+ if(len < 1) {
+ console.error(`Colorshell: No args provided via socket call`);
+ return;
+ }
+
+ try {
+ const [success, parsedArgs] = GLib.shell_parse_argv(`colorshell ${args}`);
+ parsedArgs?.splice(0, 1); // remove the unnecessary `colorshell` part
+
+ if(success) {
+ handleArgs(parsedArgs!, conn.outputStream);
+
+ conn.outputStream.flush(null);
+ conn.close(null);
+ return;
+ }
+
+ conn.outputStream.write_bytes(
+ encoder.encode("Error: Unexpected syntax error occurred"),
+ null
+ );
+
+ conn.outputStream.flush(null);
+ conn.close(null);
+ } catch(_e) {
+ const e = _e as Error;
+ console.error(`Colorshell: An error occurred while writing to socket output. Stderr:\n${
+ e.message}\n${e.stack}`);
+ }
+ });
+ }
+
+ /** translate app arguments to modules/commands
+ * order: module ?arg -> command ?arg */
+ function handleArgs(args: Array, writeTo: Gio.OutputStream|((str: string, type: "out"|"err") => void)): void {
+ let mod: Module;
+ let command: Command|undefined;
+ const modArgs: Array = [];
+ const cmdArgs: Array = [];
+
+ function print(out: Output): void {
+ const content = `${outputToString(out)}\n`;
+ const type: "out"|"err" = typeof out === "object" ?
+ out.type
+ : "out";
+
+ typeof writeTo === "function" ?
+ writeTo(content, type)
+ : writeTo.write_bytes(
+ encoder.encode(`${outputToString(out)}\n`),
+ null
+ );
+ }
+
+ function handleCommandArguments(cmd: Module|Command, args: Array, index: number, printFun: (out: Output) => void): void {
+ const argNameRegEx = /^--/, argAliasRegEx = /^-/;
+ let argName: string;
+
+ if(args[index].startsWith("--")) {
+
+ }
+ }
+
+ const firstFoundMod = modules.filter(mod => mod.prefix === args[0])[0];
+ mod = firstFoundMod ?? modules[0];
+
+ if(!mod) {
+ print({
+ content: `No command module found with the name ${args[0]}!`,
+ type: "err"
+ });
+ return;
+ }
+
+ for(let i = 1; i < args.length; i++) {
+ const arg = args[i];
+
+ if(/^-/.test(arg)) {
+ handleCommandArguments(command ?? mod, args, i, print);
+ continue;
+ }
+ }
+
+ function outputToString(out: Output): string {
+ if(typeof out === "object")
+ return out.content instanceof Uint8Array ?
+ decoder.decode(out.content)
+ : out.content;
+
+ return out;
+ }
+}
diff --git a/src/cli/modules/devel.ts b/src/cli/modules/devel.ts
new file mode 100644
index 00000000..f2d5e4c3
--- /dev/null
+++ b/src/cli/modules/devel.ts
@@ -0,0 +1,16 @@
+import { Gtk } from "ags/gtk4";
+import { Cli } from "..";
+
+
+export default {
+ prefix: "dev",
+ help: "development tools to help debugging colorshell",
+ commands: [{
+ name: "inspector",
+ help: "open the gtk's visual inspector",
+ onCalled: () => {
+ Gtk.Window.set_interactive_debugging(true);
+ return "Opening GTK Inspector..."
+ }
+ }]
+} satisfies Cli.Module;
diff --git a/src/cli/modules/media.ts b/src/cli/modules/media.ts
new file mode 100644
index 00000000..c05bbef9
--- /dev/null
+++ b/src/cli/modules/media.ts
@@ -0,0 +1,46 @@
+import { Cli } from "..";
+
+
+export default {
+ prefix: "media",
+ help: "manage colorshell's active player",
+ commands: [
+ {
+ name: "play",
+ help: "resume/start active player's media",
+ onCalled: () => "TODO"
+ }, {
+ name: "pause",
+ help: "pause the active player",
+ onCalled: () => "TODO"
+ }, {
+ name: "play-pause",
+ help: "toggle pause/resume the active player",
+ onCalled: () => "TODO"
+ }, {
+ name: "stop",
+ help: "stop the active player (if compatible)",
+ onCalled: () => "TODO"
+ }, {
+ name: "previous",
+ help: "go back to previous media in the active player",
+ onCalled: () => "TODO"
+ }, {
+ name: "next",
+ help: "jump to the next media in active player",
+ onCalled: () => "TODO"
+ }, {
+ name: "bus-name",
+ help: "retrieve the active player's mpris bus name",
+ onCalled: () => "TODO"
+ }, {
+ name: "list",
+ help: "list available players implementing mpris",
+ onCalled: () => "TODO"
+ }, {
+ name: "select",
+ help: "resume/start active player's media",
+ onCalled: (_, busName) => "TODO"
+ }
+ ]
+} satisfies Cli.Module;
diff --git a/src/cli/modules/volume.ts b/src/cli/modules/volume.ts
new file mode 100644
index 00000000..73773a1a
--- /dev/null
+++ b/src/cli/modules/volume.ts
@@ -0,0 +1,27 @@
+import { Cli } from "..";
+
+
+export default {
+ prefix: "volume",
+ help: "manage audio device volume/sensitivity. available devices are sink(speaker) and source(microphone).\
+example usage: `colorshell volume increase sink 5%`",
+ commands: [
+ {
+ name: "increase",
+ help: "increase volume/sensitivity of a sink/source",
+ onCalled: () => "TODO"
+ }, {
+ name: "decrease",
+ help: "decrease volume/sensitivity of a sink/source",
+ onCalled: () => "TODO"
+ }, {
+ name: "set",
+ help: "set the volume/sensitivity of a sink/source",
+ onCalled: () => "TODO"
+ }, {
+ name: "mute",
+ help: "toggle-mute a sink/source's audio",
+ onCalled: () => "TODO"
+ }
+ ]
+} satisfies Cli.Module;
diff --git a/src/cli/modules/windows.ts b/src/cli/modules/windows.ts
new file mode 100644
index 00000000..e6cf02f0
--- /dev/null
+++ b/src/cli/modules/windows.ts
@@ -0,0 +1,46 @@
+import { Cli } from "..";
+
+
+export default [
+ {
+ name: "open",
+ onCalled: (_, data) => {
+ return {
+ type: "out",
+ content: "TODO"
+ }
+ }
+ }, {
+ name: "toggle",
+ onCalled: (_, data) => {
+ return {
+ type: "out",
+ content: "TODO"
+ }
+ }
+ }, {
+ name: "close",
+ onCalled: (_, data) => {
+ return {
+ type: "out",
+ content: "TODO"
+ }
+ }
+ }, {
+ name: "windows",
+ onCalled: () => {
+ return {
+ type: "out",
+ content: "TODO"
+ }
+ }
+ }, {
+ name: "reopen",
+ onCalled: () => {
+ return {
+ type: "out",
+ content: "TODO"
+ }
+ }
+ }
+] satisfies Array;
diff --git a/src/config.ts b/src/config.ts
new file mode 100644
index 00000000..86535cf8
--- /dev/null
+++ b/src/config.ts
@@ -0,0 +1,80 @@
+import { Config } from "./modules/config";
+
+import GLib from "gi://GLib?version=2.0";
+
+
+const generalConfigDefaults = {
+ notifications: {
+ timeout_low: 4000,
+ timeout_normal: 6000,
+ timeout_critical: 0,
+ /** notification popup horizontal position. can be "left" or "right"
+ * @default "right" */
+ position_h: "right",
+ /** vertical notification popup position. can be "top" or "bottom"
+ * @default "top" */
+ position_v: "top"
+ },
+
+ night_light: {
+ /** whether to save night light values to disk */
+ save_on_shutdown: true
+ },
+
+ workspaces: {
+ /** breaks `enable_helper`, makes all workspaces show their respective ID
+ * by default */
+ always_show_id: false,
+ /** 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: true,
+ /** hide workspace indicator if there's only one active workspace */
+ hide_if_single: false
+ },
+
+ clock: {
+ /** use the same format as gnu's `date` command */
+ date_format: "%A %d, %H:%M"
+ },
+
+ misc: {
+ play_bell_on_volume_change: true
+ }
+};
+
+const userDataDefaults = {
+ /** last default adapter */
+ bluetooth_default_adapter: undefined as unknown as string,
+
+ control_center: {
+ /** last default backlight */
+ default_backlight: undefined as unknown as string
+ },
+
+ night_light: {
+ /** last blue light filter temperature */
+ temperature: 6000,
+ /** last gamma filter value */
+ gamma: 100,
+ /** wheter to enable identity filters("disables" the filters) */
+ identity: true
+ }
+};
+
+export const userData = new Config<
+ keyof typeof userDataDefaults,
+ (typeof userDataDefaults)[keyof typeof userDataDefaults]
+>(
+ `${GLib.get_user_data_dir()}/colorshell/data.json`,
+ userDataDefaults
+);
+
+export const generalConfig = new Config(
+ `${GLib.get_user_config_dir()}/colorshell/config.json`,
+ generalConfigDefaults
+);
diff --git a/ags/env.d.ts b/src/env.d.ts
similarity index 76%
rename from ags/env.d.ts
rename to src/env.d.ts
index 467c0a41..9a476ac5 100644
--- a/ags/env.d.ts
+++ b/src/env.d.ts
@@ -1,4 +1,7 @@
declare const SRC: string
+declare const DEVEL: boolean;
+declare const GRESOURCES_FILE: string;
+declare const COLORSHELL_VERSION: string;
declare module "inline:*" {
const content: string
diff --git a/ags/i18n/intl.ts b/src/i18n/intl.ts
similarity index 74%
rename from ags/i18n/intl.ts
rename to src/i18n/intl.ts
index 32f96c60..9324c36d 100644
--- a/ags/i18n/intl.ts
+++ b/src/i18n/intl.ts
@@ -1,4 +1,4 @@
-import { GLib } from "astal";
+import GLib from "gi://GLib?version=2.0";
const i18nKeys = {
@@ -11,16 +11,15 @@ const languages: Array = Object.keys(i18nKeys);
let language: string = getSystemLanguage();
export function getSystemLanguage(): string {
- const sysLanguage: (string|null|undefined) = GLib.getenv("LANG") || GLib.getenv("LANGUAGE");
-
- if(!sysLanguage) {
- console.log(`[WARNING] Couldn't get system language, fallback to default ${languages[0]}`);
- console.log("[TIP] Please set the LANG or LANGUAGE environment variable");
+ const sysLanguage: (string|null|undefined) = GLib.getenv("LANG") ?? GLib.getenv("LANGUAGE"),
+ splitted: Array|undefined = sysLanguage?.split('.');
+ if(!splitted || !languages.includes(splitted![0])) {
+ console.warn(`Intl: Falling back to default \`${languages[0]}\``);
return languages[0];
}
- return sysLanguage.split('.')[0];
+ return splitted![0];
}
export function setLanguage(lang: string): string {
@@ -31,8 +30,8 @@ export function setLanguage(lang: string): string {
}
});
- throw new Error(`(i18n/intl) Couldn't set language: ${lang}`, {
- cause: `Language ${lang} not found in languages of type ${typeof languages}`
+ throw new Error(`Intl: couldn't set language: ${lang}`, {
+ cause: `language ${lang} not found in languages of type ${typeof languages}`
});
}
diff --git a/ags/i18n/lang/en_US.ts b/src/i18n/lang/en_US.ts
similarity index 80%
rename from ags/i18n/lang/en_US.ts
rename to src/i18n/lang/en_US.ts
index 7486f2ad..1d987324 100644
--- a/ags/i18n/lang/en_US.ts
+++ b/src/i18n/lang/en_US.ts
@@ -20,7 +20,21 @@ export default {
connect: "Connect",
disconnect: "Disconnect",
+ copy_to_clipboard: "Copy to clipboard",
+ media: {
+ play: "Play",
+ pause: "Pause",
+ next: "Next",
+ previous: "Previous",
+ loop: "Loop",
+ no_loop: "No loop",
+ song_loop: "Loop song",
+ shuffle_order: "Shuffle",
+ follow_order: "Follow order",
+ no_artist: "No artist",
+ no_title: "No title"
+ },
control_center: {
tiles: {
enabled: "Enabled",
@@ -61,6 +75,11 @@ export default {
gamma: "Gamma",
temperature: "Temperature"
},
+ backlight: {
+ title: "Backlight",
+ description: "Control the brightness of your screens",
+ refresh: "Refresh backlights"
+ },
bluetooth: {
title: "Bluetooth",
description: "Manage Bluetooth devices",
@@ -83,4 +102,4 @@ export default {
ask_popup: {
title: "Question"
}
-} as i18nStruct;
+} satisfies i18nStruct;
diff --git a/ags/i18n/lang/pt_BR.ts b/src/i18n/lang/pt_BR.ts
similarity index 79%
rename from ags/i18n/lang/pt_BR.ts
rename to src/i18n/lang/pt_BR.ts
index f9d1f309..67805a3a 100644
--- a/ags/i18n/lang/pt_BR.ts
+++ b/src/i18n/lang/pt_BR.ts
@@ -20,7 +20,21 @@ export default {
apps: "Aplicativos",
clear: "Limpar",
+ copy_to_clipboard: "Copiar para a Área de Transferência",
+ media: {
+ next: "Próxima faixa",
+ pause: "Pausar",
+ play: "Tocar",
+ previous: "Faixa anterior",
+ loop: "Repetir",
+ no_loop: "Não repetir",
+ song_loop: "Repetir faixa",
+ follow_order: "Seguir ordem",
+ shuffle_order: "Ordem aleatória",
+ no_title: "Sem título",
+ no_artist: "Sem artista"
+ },
control_center: {
tiles: {
enabled: "Ligado",
@@ -61,6 +75,11 @@ export default {
temperature: "Temperatura",
gamma: "Gama"
},
+ backlight: {
+ title: "Brilho",
+ description: "Controle o nível de brilho das suas telas",
+ refresh: "Recarregar"
+ },
bluetooth: {
title: "Bluetooth",
description: "Gerencie dispositivos Bluetooth",
@@ -83,4 +102,4 @@ export default {
ask_popup: {
title: "Pergunta"
}
-} as i18nStruct;
+} satisfies i18nStruct;
diff --git a/ags/i18n/lang/ru_RU.ts b/src/i18n/lang/ru_RU.ts
similarity index 100%
rename from ags/i18n/lang/ru_RU.ts
rename to src/i18n/lang/ru_RU.ts
diff --git a/ags/i18n/struct.ts b/src/i18n/struct.ts
similarity index 79%
rename from ags/i18n/struct.ts
rename to src/i18n/struct.ts
index b8568989..5a60843e 100644
--- a/ags/i18n/struct.ts
+++ b/src/i18n/struct.ts
@@ -17,9 +17,23 @@ export type i18nStruct = {
disconnect: string,
connect: string,
- apps: string;
- clear: string;
+ apps: string,
+ clear: string,
+ copy_to_clipboard: string,
+ media: {
+ loop: string,
+ song_loop: string,
+ no_loop: string,
+ shuffle_order: string,
+ follow_order: string,
+ pause: string,
+ play: string,
+ next: string,
+ previous: string,
+ no_artist: string,
+ no_title: string
+ },
control_center: {
tiles: {
enabled: string,
@@ -59,6 +73,11 @@ export type i18nStruct = {
title: string,
interface: string
},
+ backlight: {
+ title: string,
+ description: string,
+ refresh: string
+ },
bluetooth: {
title: string,
description: string,
diff --git a/ags/scripts/apps.ts b/src/modules/apps.ts
similarity index 71%
rename from ags/scripts/apps.ts
rename to src/modules/apps.ts
index 5b9890de..86b868ed 100644
--- a/ags/scripts/apps.ts
+++ b/src/modules/apps.ts
@@ -1,12 +1,12 @@
-import { Astal } from "astal/gtk3";
+import { Gdk, Gtk } from "ags/gtk4";
+import { execAsync } from "ags/process";
import AstalApps from "gi://AstalApps";
import AstalHyprland from "gi://AstalHyprland";
-import { execAsync } from "astal";
export const uwsmIsActive: boolean = await execAsync(
- "uwsm check is-active hyprland-uwsm.desktop"
+ "uwsm check is-active"
).then(() => true).catch(() => false);
const astalApps: AstalApps.Apps = new AstalApps.Apps();
@@ -28,13 +28,17 @@ export function getAstalApps(): AstalApps.Apps {
/** handles running with uwsm if it's installed */
export function execApp(app: AstalApps.Application|string, dispatchExecArgs?: string) {
const executable = (typeof app === "string") ? app
- : app.executable.replace(/(%f|%F|%u|%U|%i|%c|%k)/g, "");
+ : app.executable.replace(/%[fFcuUik]/g, "");
AstalHyprland.get_default().dispatch("exec",
- `${dispatchExecArgs ? `${dispatchExecArgs} ` : ""}${uwsmIsActive ? "uwsm app -- " : ""}${executable}`
+ `${dispatchExecArgs ? `${dispatchExecArgs} ` : ""}${uwsmIsActive ? "uwsm-app -- " : ""}${executable}`
);
}
+export function lookupIcon(name: string): boolean {
+ return Gtk.IconTheme.get_for_display(Gdk.Display.get_default()!)?.has_icon(name);
+}
+
export function getAppsByName(appName: string): (Array|undefined) {
let found: Array = [];
@@ -50,14 +54,23 @@ export function getAppsByName(appName: string): (Array|un
export function getIconByAppName(appName: string): (string|undefined) {
if(!appName) return undefined;
- if(Astal.Icon.lookup_icon(appName))
+ if(lookupIcon(appName))
return appName;
- if(Astal.Icon.lookup_icon(appName.toLowerCase()))
+ if(lookupIcon(appName.toLowerCase()))
return appName.toLowerCase();
const nameReverseDNS = appName.split('.');
- if(Astal.Icon.lookup_icon(nameReverseDNS[nameReverseDNS.length - 1]))
+ const lastItem = nameReverseDNS[nameReverseDNS.length - 1];
+ const lastPretty = `${lastItem.charAt(0).toUpperCase()}${lastItem.substring(1, lastItem.length)}`;
+
+ const uppercaseRDNS = nameReverseDNS.slice(0, nameReverseDNS.length - 1)
+ .concat(lastPretty).join('.');
+
+ if(lookupIcon(uppercaseRDNS))
+ return uppercaseRDNS;
+
+ if(lookupIcon(nameReverseDNS[nameReverseDNS.length - 1]))
return nameReverseDNS[nameReverseDNS.length - 1];
const found: (AstalApps.Application|undefined) = getAppsByName(appName)?.[0];
@@ -73,7 +86,7 @@ export function getAppIcon(app: (string|AstalApps.Application)): (string|undefin
if(typeof app === "string")
return getIconByAppName(app);
- if(app.iconName && Astal.Icon.lookup_icon(app.iconName))
+ if(app.iconName && lookupIcon(app.iconName))
return app.iconName;
if(app.wmClass)
@@ -85,7 +98,7 @@ export function getAppIcon(app: (string|AstalApps.Application)): (string|undefin
export function getSymbolicIcon(app: (string|AstalApps.Application)): (string|undefined) {
const icon = getAppIcon(app);
- return (icon && Astal.Icon.lookup_icon(`${icon}-symbolic`)) ?
+ return (icon && lookupIcon(`${icon}-symbolic`)) ?
`${icon}-symbolic`
: undefined;
}
diff --git a/src/modules/arg-handler.ts b/src/modules/arg-handler.ts
new file mode 100644
index 00000000..7ba59db9
--- /dev/null
+++ b/src/modules/arg-handler.ts
@@ -0,0 +1,406 @@
+import { Gtk } from "ags/gtk4";
+import { Wireplumber } from "./volume";
+import { Windows } from "../windows";
+import { restartInstance } from "./reload-handler";
+import { timeout } from "ags/time";
+import { Runner } from "../runner/Runner";
+import { showWorkspaceNumber } from "../window/bar/widgets/Workspaces";
+import { playSystemBell } from "./utils";
+import { Shell } from "../app";
+import { generalConfig } from "../config";
+
+import Media from "./media";
+import AstalIO from "gi://AstalIO";
+import AstalMpris from "gi://AstalMpris";
+
+
+export type RemoteCaller = {
+ printerr_literal: (message: string) => void,
+ print_literal: (message: string) => void
+};
+
+let wsTimeout: AstalIO.Time|undefined;
+const help = `Manage Astal Windows and do more stuff. From retrozinndev's colorshell, \
+made using GTK4, AGS, Gnim and Astal libraries by Aylur.
+
+Window Management:
+ 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 and their respective status.
+ reload: quit this instance and start a new one.
+ reopen: restart all open-windows.
+ quit: exit the main instance of the shell.
+
+Audio Controls:
+ volume: speaker and microphone volume controller, see "volume help".
+
+Media Controls:
+ media: manage colorshell's active player, see "media help".
+${DEVEL ? `
+Development Tools:
+ dev: tools to help debugging colorshell
+` : ""}
+Other options:
+ runner [initial_text]: open the application runner, optionally add an initial search.
+ peek-workspace-num [millis]: peek the workspace numbers on bar window.
+ v, version: display current colorshell version.
+ h, help: shows this help message.
+
+2025 (c) retrozinndev's colorshell, licensed under the BSD 3-Clause License.
+https://github.com/retrozinndev/colorshell
+`.trim();
+
+export function handleArguments(cmd: RemoteCaller, args: Array): number {
+ switch(args[0]) {
+ case "help":
+ case "h":
+ cmd.print_literal(help);
+ return 0;
+
+ case "version":
+ case "v":
+ cmd.print_literal(`colorshell by retrozinndev, version ${COLORSHELL_VERSION
+ }${DEVEL ? " (devel)" : ""}\nhttps://github.com/retrozinndev/colorshell`);
+ return 0;
+
+ case "dev":
+ return handleDevArgs(cmd, args);
+
+ case "open":
+ case "close":
+ case "toggle":
+ case "windows":
+ case "reopen":
+ return handleWindowArgs(cmd, args);
+
+ case "volume":
+ return handleVolumeArgs(cmd, args);
+
+ case "media":
+ return handleMediaArgs(cmd, args);
+
+ case "reload":
+ restartInstance();
+ cmd.print_literal("Restarting instance...");
+ return 0;
+
+ case "quit":
+ try {
+ Shell.getDefault().quit();
+ cmd.print_literal("Quitting main instance...");
+ } catch(_e) {
+ const e = _e as Error;
+ cmd.printerr_literal(`Error: couldn't quit instance. Stderr: ${e.message}\n${e.stack}`);
+ return 1;
+ }
+ return 0;
+
+ case "runner":
+ !Runner.instance ?
+ Runner.openDefault(args[1] || undefined)
+ : Runner.close();
+
+ cmd.print_literal(`Opening runner${args[1] ? ` with predefined text: "${args[1]}"` : ""}`);
+ return 0;
+
+ case "peek-workspace-num":
+ if(wsTimeout) {
+ cmd.print_literal("Workspace numbers are already showing");
+ return 0;
+ }
+
+ showWorkspaceNumber(true);
+ wsTimeout = timeout(Number.parseInt(args[1]) || 2200, () => {
+ showWorkspaceNumber(false);
+ wsTimeout = undefined;
+ });
+ cmd.print_literal("Toggled workspace numbers");
+ return 0;
+ }
+
+ cmd.printerr_literal("Error: command not found! try checking help");
+ return 1;
+}
+
+function handleDevArgs(cmd: RemoteCaller, args: Array): number {
+ if(/h|help/.test(args[1])) {
+ cmd.print_literal(`
+Debugging tools for colorshell.
+
+Options:
+ inspector: open GTK's visual debugger
+`.trim());
+ return 0;
+ }
+
+ switch(args[1]) {
+ case "inspector":
+ cmd.print_literal("Opening inspector...");
+ Gtk.Window.set_interactive_debugging(true);
+ return 0;
+ }
+
+ cmd.printerr_literal("Error: command not found! try checking `dev help`");
+ return 1;
+}
+
+function handleMediaArgs(cmd: RemoteCaller, args: Array): number {
+ if(/h|help/.test(args[1])) {
+ const mediaHelp = `
+Manage colorshell's active player
+
+Options:
+ play: resume/start active player's media.
+ pause: pause the active player.
+ play-pause: toggle play/pause on active player.
+ stop: stop the active player's media.
+ previous: go back to previous media if player supports it.
+ next: jump to next media if player supports it.
+ bus-name: get active player's mpris bus name.
+ list: show available players with their bus name.
+ select bus_name: change the active player, where bus_name is
+ the desired player's mpris bus name(with the mediaplayer2 prefix).
+`.trim();
+ cmd.print_literal(mediaHelp);
+ return 0;
+ }
+
+ const activePlayer: AstalMpris.Player|undefined = Media.getDefault().player.available ?
+ Media.getDefault().player
+ : undefined;
+ const players = AstalMpris.get_default().players.filter(pl => pl.available);
+
+ if(!activePlayer) {
+ cmd.printerr_literal(`Error: no active player found! try playing some media first`);
+ return 1;
+ }
+
+ switch(args[1]) {
+ case "play":
+ activePlayer.play();
+ cmd.print_literal("Playing");
+ return 0;
+
+ case "list":
+ cmd.print_literal(`Available players:\n${players.map(pl => {
+ let playbackStatusStr: string;
+ switch(pl.playbackStatus) {
+ case AstalMpris.PlaybackStatus.PAUSED:
+ playbackStatusStr = "paused";
+ break;
+ case AstalMpris.PlaybackStatus.PLAYING:
+ playbackStatusStr = "playing";
+ break;
+ default:
+ playbackStatusStr = "stopped";
+ break;
+ }
+
+ return ` ${pl.busName}: ${playbackStatusStr}`;
+ }).join('\n')}`);
+ return 0;
+
+ case "pause":
+ activePlayer.pause();
+ cmd.print_literal("Paused");
+ return 0;
+
+ case "play-pause":
+ activePlayer.play_pause();
+ cmd.print_literal(
+ activePlayer?.playbackStatus === AstalMpris.PlaybackStatus.PAUSED ?
+ "Toggled play"
+ : "Toggled pause"
+ );
+ return 0;
+
+ case "stop":
+ activePlayer.stop();
+ cmd.print_literal("Stopped!");
+ return 0;
+
+ case "previous":
+ activePlayer.canGoPrevious && activePlayer.previous();
+ cmd.print_literal(
+ activePlayer.canGoPrevious ?
+ "Back to previous"
+ : "Player does not support this command"
+ );
+ return 0;
+
+ case "next":
+ activePlayer.canGoNext && activePlayer.next();
+ cmd.print_literal(
+ activePlayer.canGoNext ?
+ "Jump to next"
+ : "Player does not support this command"
+ );
+ return 0;
+
+ case "bus-name":
+ cmd.print_literal(activePlayer.busName);
+ return 0;
+
+ case "select":
+ if(!args[2] || !players.filter(pl => pl.busName == args[2])?.[0]) {
+ cmd.printerr_literal(`Error: either no player was specified or the player with \
+specified bus name does not exist/is not available!`);
+
+ return 1;
+ }
+
+ Media.getDefault().player = players.filter(pl => pl.busName === args[2])[0];
+ cmd.print_literal(`Done setting player to \`${args[2]}\`!`);
+ return 0;
+ }
+
+ cmd.printerr_literal("Error: couldn't handle media arguments, try checking `media help`");
+ return 1;
+}
+
+function handleWindowArgs(cmd: RemoteCaller, args: Array): number {
+ switch(args[0]) {
+ case "reopen":
+ Windows.getDefault().reopen();
+ cmd.print_literal("Reopening all open windows");
+ return 0;
+
+ case "windows":
+ cmd.print_literal(
+ Object.keys(Windows.getDefault().windows).map(name =>
+ `${name}: ${Windows.getDefault().isOpen(name) ?
+ "open"
+ : "closed"}`
+ ).join('\n')
+ );
+ return 0;
+ }
+
+ const specifiedWindow: string = args[1];
+
+ if(!specifiedWindow) {
+ cmd.printerr_literal("Error: window argument not specified!");
+ return 1;
+ }
+
+ if(!Windows.getDefault().hasWindow(specifiedWindow)) {
+ cmd.printerr_literal(
+ `Error: "${specifiedWindow}" not found on window list! Make sure to add new windows to the system before using them`
+ );
+ return 1;
+ }
+
+ switch(args[0]) {
+ case "open":
+ if(!Windows.getDefault().isOpen(specifiedWindow)) {
+ Windows.getDefault().open(specifiedWindow);
+ cmd.print_literal(`Opening window with name "${args[1]}"`);
+ return 0;
+ }
+
+ cmd.print_literal(`Window is already open, ignored`);
+ return 0;
+
+ case "close":
+ if(Windows.getDefault().isOpen(specifiedWindow)) {
+ Windows.getDefault().close(specifiedWindow);
+ cmd.print_literal(`Closing window with name "${args[1]}"`);
+ return 0;
+ }
+
+ cmd.print_literal(`Window is already closed, ignored`);
+ return 0;
+
+ case "toggle":
+ if(!Windows.getDefault().isOpen(specifiedWindow)) {
+ Windows.getDefault().open(specifiedWindow);
+ cmd.print_literal(`Toggle opening window "${args[1]}"`);
+ return 0;
+ }
+
+ Windows.getDefault().close(specifiedWindow);
+ cmd.print_literal(`Toggle closing window "${args[1]}"`);
+ return 0;
+ }
+
+ cmd.printerr_literal("Couldn't handle window management arguments");
+ return 1;
+}
+
+function handleVolumeArgs(cmd: RemoteCaller, args: Array): number {
+ if(!args[1]) {
+ cmd.printerr_literal(`Error: please specify what to do! see \`volume help\``);
+ return 1;
+ }
+
+ if(/^(sink|source)[-](increase|decrease|set)$/.test(args[1]) && !args[2]) {
+ cmd.printerr_literal(`Error: you forgot to set a value`);
+ return 1;
+ }
+
+ const command: Array = args[1].split('-');
+
+ if(/h|help/.test(args[1])) {
+ cmd.print_literal(`
+Control speaker and microphone volumes
+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());
+
+ return 0;
+ }
+
+ if(command[1] === "mute") {
+ command[0] === "sink" ?
+ Wireplumber.getDefault().toggleMuteSink()
+ : Wireplumber.getDefault().toggleMuteSource()
+
+ cmd.print_literal(`Done toggling mute!`);
+ return 0;
+ }
+
+ if(Number.isNaN(Number.parseFloat(args[2]))) {
+ cmd.printerr_literal(`Error: argument "${args[2]} is not a valid number! Please use integers"`);
+ return 1;
+ }
+
+ switch(command[1]) {
+ case "set":
+ command[0] === "sink" ?
+ Wireplumber.getDefault().setSinkVolume(Number.parseInt(args[2]))
+ : Wireplumber.getDefault().setSourceVolume(Number.parseInt(args[2]))
+ cmd.print_literal(`Done! Set ${command[0]} volume to ${args[2]}`);
+ return 0;
+
+ case "increase":
+ command[0] === "sink" ?
+ Wireplumber.getDefault().increaseSinkVolume(Number.parseInt(args[2]))
+ : Wireplumber.getDefault().increaseSourceVolume(Number.parseInt(args[2]))
+
+ generalConfig.getProperty("misc.play_bell_on_volume_change", "boolean") === true &&
+ playSystemBell();
+
+ cmd.print_literal(`Done increasing volume by ${args[2]}`);
+ return 0;
+
+ case "decrease":
+ command[0] === "sink" ?
+ Wireplumber.getDefault().decreaseSinkVolume(Number.parseInt(args[2]))
+ : Wireplumber.getDefault().decreaseSourceVolume(Number.parseInt(args[2]))
+
+ generalConfig.getProperty("misc.play_bell_on_volume_change", "boolean") === true &&
+ playSystemBell();
+
+ cmd.print_literal(`Done decreasing volume to ${args[2]}`);
+ return 0;
+ }
+
+ cmd.printerr_literal(`Error: couldn't resolve arguments! "${args.join(' ')
+ .replace(new RegExp(`^${args[0]}`), "")}"`);
+
+ return 1;
+}
diff --git a/src/modules/auth.ts b/src/modules/auth.ts
new file mode 100644
index 00000000..3332a706
--- /dev/null
+++ b/src/modules/auth.ts
@@ -0,0 +1,111 @@
+import { exec, execAsync } from "ags/process";
+import { register } from "ags/gobject";
+import { AuthPopup } from "../widget/AuthPopup";
+
+import Polkit from "gi://Polkit?version=1.0";
+import PolkitAgent from "gi://PolkitAgent?version=1.0";
+import Gio from "gi://Gio?version=2.0";
+import GLib from "gi://GLib?version=2.0";
+import AstalAuth from "gi://AstalAuth?version=0.1";
+
+
+@register({ GTypeName: "AuthAgent" })
+export class Auth extends PolkitAgent.Listener {
+ private static instance: Auth;
+ #handle: any;
+ #user: Polkit.UnixUser;
+ #pam: AstalAuth.Pam;
+
+ constructor() {
+ super();
+ this.#user = Polkit.UnixUser.new(Number.parseInt(exec("id -u"))) as Polkit.UnixUser;
+ this.#pam = new AstalAuth.Pam;
+
+ this.register(
+ PolkitAgent.RegisterFlags.RUN_IN_THREAD,
+ Polkit.UnixSession.new(this.#user.get_uid().toString()),
+ "/io/github/retrozinndev/colorshell/AuthAgent",
+ null
+ );
+ }
+
+ vfunc_dispose() {
+ PolkitAgent.Listener.unregister(this.#handle);
+ }
+
+ public static initiate_authentication(action_id: string, message: string, icon_name: string, details: Polkit.Details, cookie: string, identities: Array, cancellable: Gio.Cancellable|null, callback: Gio.AsyncReadyCallback|null): void {
+ const task = Gio.Task.new(
+ this.getDefault(),
+ cancellable,
+ callback as Gio.AsyncReadyCallback|null
+ );
+
+ AuthPopup({
+ text: message,
+ iconName: icon_name,
+ onContinue: (data, reject, approve) => {
+ this.getDefault().validateAuth(data.passwd, data.user).then((success) => {
+ approve();
+ task.return_boolean(success);
+ }).catch((error: GLib.Error) => {
+ // TODO implement a number of tries (usually it's 3)
+ reject(`Authentication failed: ${error.message}`);
+ task.return_error(error);
+ });
+ }
+ });
+
+ }
+
+ initiate_authentication_finish(res: Gio.AsyncResult): boolean {
+
+ }
+
+ // TODO: support fingerprint/facial auth
+ /** @returns true if data are correct, rejects promise otherwise */
+ public validateAuth(passwd: string, user?: string): Promise {
+ if(user !== undefined)
+ this.#pam.username = user;
+
+ return new Promise((resolve, reject) => {
+ const connections: Array = [];
+ connections.push(
+ this.#pam.connect("fail", () => {
+ reject(
+ `Auth: Authentication has failed for user ${this.#pam.username}`
+ );
+ connections.forEach(id => this.#pam.disconnect(id));
+ }),
+ this.#pam.connect("success", () => {
+ resolve(true);
+ connections.forEach(id => this.#pam.disconnect(id));
+ })
+ );
+
+ this.#pam.start_authenticate();
+ this.#pam.supply_secret(passwd);
+ });
+ }
+
+ /** @returns true if successful */
+ public async polkitExecute(cmd: string | Array): Promise {
+ let success: boolean = true;
+ await execAsync([
+ "pkexec",
+ "--",
+ ...(Array.isArray(cmd) ? cmd : [ cmd ]) ]
+ ).catch((r) => {
+ success = false;
+ console.error(`Polkit: Couldn't authenticate. Stderr: ${r}`);
+ });
+
+ return success;
+ }
+
+ public static getDefault(): Auth {
+ if(!this.instance)
+ this.instance = new Auth();
+
+ return this.instance;
+ }
+}
diff --git a/src/modules/backlight.ts b/src/modules/backlight.ts
new file mode 100644
index 00000000..2fa160b3
--- /dev/null
+++ b/src/modules/backlight.ts
@@ -0,0 +1,209 @@
+import { monitorFile, readFile } from "ags/file";
+import { exec } from "ags/process";
+import GObject, { getter, ParamSpec, register, setter, signal } from "ags/gobject";
+
+import Gio from "gi://Gio?version=2.0";
+
+
+export namespace Backlights {
+
+ const BacklightParamSpec = (name: string, flags: GObject.ParamFlags) =>
+ GObject.ParamSpec.jsobject(name, null, null, flags) as ParamSpec;
+
+ let instance: Backlights;
+
+ export function getDefault(): Backlights {
+ if(!instance)
+ instance = new Backlights();
+
+ return instance;
+ }
+
+ @register({ GTypeName: "Backlights" })
+ class _Backlights extends GObject.Object {
+
+ #backlights: Array = [];
+ #default: Backlight|null = null;
+ #available: boolean = false;
+
+
+ @getter(Array as unknown as ParamSpec>)
+ get backlights() { return this.#backlights; }
+
+ @getter(BacklightParamSpec)
+ get default() { return this.#default!; }
+
+ /** true if there are any backlights available */
+ @getter(Boolean)
+ get available() { return this.#available; }
+
+ public scan(): Array {
+ const dir = Gio.File.new_for_path(`/sys/class/backlight`),
+ backlights: Array = [];
+
+ let fileEnum: Gio.FileEnumerator;
+
+ try {
+ fileEnum = dir.enumerate_children("standard::*", Gio.FileQueryInfoFlags.NONE, null);
+ for(const backlight of fileEnum) {
+ try {
+ backlights.push(new Backlight(backlight.get_name()));
+ } catch(_) {}
+ }
+ } catch(_) {
+ return [];
+ }
+
+ if(backlights.length < 1) {
+ if(this.#available) {
+ this.#available = false;
+ this.notify("available");
+ }
+
+ this.#default = null;
+ this.notify("default");
+ }
+
+ if(backlights.length > 0) {
+ if(this.#backlights.length < 1) {
+ this.#available = true;
+ this.notify("available");
+ }
+
+ if(!this.#default || !backlights.filter(bk => bk.path === this.#default?.path)[0]) {
+ this.#default = backlights[0];
+ this.notify("default");
+ }
+ }
+
+ this.#backlights = backlights;
+ this.notify("backlights");
+
+ return backlights;
+ }
+
+ public setDefault(bk: Backlight): void {
+ this.#default = bk;
+ this.notify("default");
+ }
+
+ constructor(scan: boolean = true) {
+ super();
+ scan && this.scan();
+ }
+ }
+
+ @register({ GTypeName: "Backlight" })
+ class _Backlight extends GObject.Object {
+
+ declare $signals: GObject.Object.SignalSignatures & {
+ "brightness-changed": (value: number) => void
+ };
+
+ readonly #name: string;
+ #path: string;
+ #maxBrightness: number;
+ #brightness: number;
+ #monitor: Gio.FileMonitor;
+ #conn: number;
+
+ @signal(Number) brightnessChanged(_: number): void {};
+
+ @getter(String)
+ get name() { return this.#name; }
+
+ @getter(String)
+ get path() { return this.#path; }
+
+ @getter(Boolean)
+ get isDefault() { return this.path === getDefault().default?.path; }
+
+ @getter(Number)
+ get brightness() { return this.#brightness; };
+ @setter(Number)
+ set brightness(level: number) {
+ if(!this.writeBrightness(level)) return;
+
+ this.#brightness = level;
+ this.notify("brightness");
+ this.emit("brightness-changed", level);
+ }
+
+ @getter(Number)
+ get maxBrightness() { return this.#maxBrightness;};
+
+
+ // intel_backlight is mostly the default on laptops
+ constructor(name: string = "intel_backlight") {
+ super();
+
+ // check if backlight exists
+ if(!Gio.File.new_for_path(`/sys/class/backlight/${name}/brightness`).query_exists(null))
+ throw new Error(`Brightness: Couldn't find brightness for "${name}"`);
+
+ // notify :is-default on default backlight change
+ this.#conn = getDefault().connect("notify::default", () =>
+ this.notify("is-default"));
+
+ this.#name = name;
+ this.#path = `/sys/class/backlight/${name}`;
+ this.notify("path");
+ this.#maxBrightness = Number.parseInt(readFile(`${this.#path}/max_brightness`));
+ this.notify("max-brightness");
+ this.#brightness = Number.parseInt(readFile(`${this.#path}/brightness`))
+
+
+ this.#monitor = monitorFile(`/sys/class/backlight/${name}/brightness`, () => {
+ this.#brightness = this.readBrightness();
+ this.notify("brightness");
+ this.emit("brightness-changed", this.brightness);
+ });
+ }
+
+ private readBrightness(): number {
+ try {
+ const brightness = Number.parseInt(readFile(`${this.#path}/brightness`));
+ return brightness;
+ } catch(e) {
+ console.error(`Backlight: An error occurred while reading brightness from "${this.#name}"`);
+ }
+
+ return this.#brightness ?? this.#maxBrightness ?? 0;
+ }
+
+ private writeBrightness(level: number): boolean {
+ try {
+ exec(`brightnessctl -d ${this.#name} s ${level}`);
+ return true;
+ } catch(e) {
+ console.error(`Backlight: Couldn't set brightness for "${this.#name}". Stderr: ${e}`);
+ }
+
+ return false;
+ }
+
+ vfunc_dispose(): void {
+ this.#monitor.cancel();
+ getDefault().disconnect(this.#conn);
+ }
+
+ public emit(
+ signal: Signal,
+ ...args: Parameters<(typeof this.$signals)[Signal]>
+ ): void {
+ super.emit(signal, ...args);
+ }
+
+ public connect(
+ signal: Signal,
+ callback: (self: typeof this, ...args: Parameters<(typeof this.$signals)[Signal]>) => ReturnType<(typeof this.$signals)[Signal]>
+ ): number {
+ return super.connect(signal, callback);
+ }
+ }
+
+ export const Backlights = _Backlights;
+ export const Backlight = _Backlight;
+ export type Backlight = InstanceType;
+ export type Backlights = InstanceType;
+}
diff --git a/src/modules/bluetooth.ts b/src/modules/bluetooth.ts
new file mode 100644
index 00000000..c96baeae
--- /dev/null
+++ b/src/modules/bluetooth.ts
@@ -0,0 +1,162 @@
+import { createRoot, getScope, Scope } from "ags";
+import { execAsync } from "ags/process";
+import { userData } from "../config";
+import GObject, { getter, gtype, property, register, setter } from "ags/gobject";
+
+import AstalBluetooth from "gi://AstalBluetooth";
+
+
+/** AstalBluetooth helper (implements the default adapter feature) */
+@register({ GTypeName: "Bluetooth" })
+export class Bluetooth extends GObject.Object {
+ declare $signals: {
+ "notify": () => void;
+ "notify::adapter": (adapter: AstalBluetooth.Adapter|null) => void;
+ "notify::is-available": (available: boolean) => void;
+ "notify::save-default-adapter": (save: boolean) => void;
+ "notify::last-device": (device: AstalBluetooth.Device|null) => void;
+ };
+
+ private static instance: Bluetooth;
+ private astalBl = AstalBluetooth.get_default();
+
+ #connections: Map|number> = new Map();
+ #adapter: AstalBluetooth.Adapter|null = this.astalBl.adapter ?? null;
+ #scope!: Scope;
+ #isAvailable: boolean = false;
+ #lastDevice: AstalBluetooth.Device|null = null;
+
+ @property(Boolean)
+ saveDefaultAdapter: boolean = true;
+
+ @getter(Boolean)
+ get isAvailable() { return this.#isAvailable; }
+
+ /** last connected device, can be null */
+ @getter(AstalBluetooth.Device)
+ get lastDevice() { return this.#lastDevice!; }
+
+ @getter(gtype(AstalBluetooth.Adapter))
+ get adapter() { return this.#adapter; }
+
+ @setter(gtype(AstalBluetooth.Adapter))
+ set adapter(newAdapter: AstalBluetooth.Adapter|null) {
+ this.#adapter = newAdapter;
+ this.notify("adapter");
+
+ if(!newAdapter) return;
+
+ AstalBluetooth.get_default().adapters.filter(ad => {
+ if(ad.address !== newAdapter.address)
+ return true;
+
+ ad.set_powered(true);
+ return false;
+ }).forEach(ad => ad.set_powered(false));
+
+ execAsync(`bluetoothctl select ${newAdapter.address}`).then(() => {
+ userData.setProperty("bluetooth_default_adapter", newAdapter.address, true);
+ }).catch(e => console.error(`Bluetooth: Couldn't select adapter. Stderr: ${e}`));
+ }
+
+ constructor() {
+ super();
+
+ createRoot((_) => {
+ this.#scope = getScope();
+
+ if(this.astalBl.adapters.length > 0) {
+ this.#isAvailable = true;
+ this.notify("is-available");
+ }
+
+ // load previous default adapter
+ const dataDefaultAdapter = userData.getProperty("bluetooth_default_adapter", "string");
+ const foundAdapter = this.astalBl.adapters.filter(a => a.address === dataDefaultAdapter)[0];
+
+ if(dataDefaultAdapter !== undefined && foundAdapter !== undefined)
+ this.adapter = foundAdapter;
+
+ this.#connections.set(
+ AstalBluetooth.get_default(), [
+ AstalBluetooth.get_default().connect("adapter-added", (self, adapter) => {
+ if(self.adapters.length === 1) // adapter was just added
+ this.adapter = adapter;
+ }),
+ AstalBluetooth.get_default().connect("adapter-removed", (self, adapter) => {
+ if(self.adapters.length < 1) {
+ this.adapter = null;
+ this.#isAvailable = false;
+ this.notify("is-available");
+ }
+
+ if(this.#adapter?.address !== adapter.address)
+ return;
+
+ // the removed adapter was the default
+
+ if(self.adapters.length < 1) {
+ this.adapter = null;
+ this.#isAvailable = false;
+ this.notify("is-available");
+
+ return;
+ }
+
+ this.#adapter = self.adapters[0];
+ })
+ ]
+ );
+
+ // async to prevent slow start
+ setTimeout(() => {
+ this.#lastDevice = this.getLastConnectedDevice();
+ this.notify("last-device");
+ }, 1200);
+
+ this.#connections.set(AstalBluetooth.get_default(), [
+ AstalBluetooth.get_default().connect("device-added", (_) => {
+ this.#lastDevice = this.getLastConnectedDevice();
+ this.notify("last-device");
+ }),
+ AstalBluetooth.get_default().connect("device-removed", (_) => {
+ this.#lastDevice = this.getLastConnectedDevice();
+ this.notify("last-device");
+ })
+ ]);
+
+ this.#scope.onCleanup(() => this.#connections.forEach((ids, gobj) =>
+ Array.isArray(ids) ?
+ ids.forEach(id => gobj.disconnect(id))
+ : gobj.disconnect(ids)
+ ));
+ });
+ }
+
+ public static getDefault(): Bluetooth {
+ if(!this.instance)
+ this.instance = new Bluetooth();
+
+ return this.instance;
+ }
+
+ vfunc_dispose(): void {
+ this.#scope.dispose();
+ }
+
+ private getLastConnectedDevice(): AstalBluetooth.Device|null {
+
+ const connectedDevices = AstalBluetooth.get_default().devices
+ .filter(d => d.connected);
+
+ const lastDevice = connectedDevices[connectedDevices.length - 1];
+
+ return lastDevice ?? null;
+ }
+
+ connect(
+ signal: Signal, callback: (typeof this["$signals"])[Signal]
+ ): number {
+ return super.connect(signal as string, callback as () => void);
+ }
+}
diff --git a/ags/scripts/clipboard.ts b/src/modules/clipboard.ts
similarity index 81%
rename from ags/scripts/clipboard.ts
rename to src/modules/clipboard.ts
index b4a98040..bd292b21 100644
--- a/ags/scripts/clipboard.ts
+++ b/src/modules/clipboard.ts
@@ -1,4 +1,11 @@
-import { AstalIO, execAsync, Gio, GLib, GObject, monitorFile, property, readFile, register, signal, timeout } from "astal";
+import { timeout } from "ags/time";
+import { monitorFile, readFile } from "ags/file";
+import { execAsync } from "ags/process";
+import GObject, { getter, register, signal } from "ags/gobject";
+
+import AstalIO from "gi://AstalIO";
+import GLib from "gi://GLib?version=2.0";
+import Gio from "gi://Gio?version=2.0";
export enum ClipboardItemType {
@@ -6,10 +13,16 @@ export enum ClipboardItemType {
IMAGE = 1
}
-export type ClipboardItem = {
+export class ClipboardItem {
id: number;
type: ClipboardItemType;
preview: string;
+
+ constructor(id: number, type: ClipboardItemType, preview: string) {
+ this.id = id;
+ this.type = type;
+ this.preview = preview;
+ }
}
export { Clipboard };
@@ -20,6 +33,11 @@ export { Clipboard };
class Clipboard extends GObject.Object {
private static instance: Clipboard;
+ declare $signals: GObject.Object.SignalSignatures & {
+ "copied": Clipboard["copied"];
+ "wiped": Clipboard["wiped"];
+ };
+
#dbFile: Gio.File;
#dbMonitor: Gio.FileMonitor;
#updateDone: boolean = false;
@@ -27,14 +45,10 @@ class Clipboard extends GObject.Object {
#changesTimeout: (AstalIO.Time|undefined);
#ignoreChanges: boolean = false;
- @signal(Object)
- declare copied: () => ClipboardItem;
+ @signal(GObject.TYPE_JSOBJECT) copied(_item: object) {}
+ @signal() wiped() {};
- @signal()
- declare wiped: () => void;
-
-
- @property()
+ @getter(Array)
public get history() { return this.#history; }
@@ -80,11 +94,25 @@ class Clipboard extends GObject.Object {
);
}
- public async copyAsync(content: string): Promise {
- await execAsync(`wl-copy "${content}"`).catch((err: Gio.IOErrorEnum) => {
- console.error(`Clipboard: Couldn't copy text using wl-copy. Stderr:\n\t${err.message
- } | Stack:\n\t\t${err.stack}`);
- });
+ public async copyAsync(content: string): Promise {
+ const proc = Gio.Subprocess.new(
+ ["wl-copy", content],
+ Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE
+ );
+
+ const stderr = Gio.DataInputStream.new(proc.get_stderr_pipe()!);
+
+ if(!proc.wait_check(null)) {
+ try {
+ const [err, ] = stderr.read_upto('\x00', -1);
+ console.error(`Clipboard: An error occurred while copying text. Stderr: ${err}`);
+ } catch(_) {
+ console.error(`Clipboard: An error occurred while copying text and shell couldn't read \
+stderr for more info.`);
+ }
+ }
+
+ return proc.get_exit_status() === 0;
}
public async selectItem(itemToSelect: number|ClipboardItem): Promise {
diff --git a/src/modules/compositors/hyprland.ts b/src/modules/compositors/hyprland.ts
new file mode 100644
index 00000000..c2a3e088
--- /dev/null
+++ b/src/modules/compositors/hyprland.ts
@@ -0,0 +1,34 @@
+import { register } from "ags/gobject";
+import { Compositors } from ".";
+import { createRoot } from "ags";
+import { createScopedConnection } from "../utils";
+
+import AstalHyprland from "gi://AstalHyprland";
+
+
+@register({ GTypeName: "CompositorHyprland" })
+export class CompositorHyprland extends Compositors.Compositor {
+ hyprland: AstalHyprland.Hyprland;
+
+ constructor() {
+ super();
+
+ try {
+ this.hyprland = AstalHyprland.get_default();
+ } catch(e) {
+ throw new Error(`Couldn't initialize CompositorHyprland: ${e}`);
+ }
+
+ createRoot(() => {
+ createScopedConnection(
+ this.hyprland, "workspace-added", (hws) => {
+ // check workspace existance
+ if(this._workspaces.filter(w => w.id === hws.id)[0])
+ return;
+
+ // TODO
+ }
+ );
+ });
+ }
+}
diff --git a/src/modules/compositors/index.ts b/src/modules/compositors/index.ts
new file mode 100644
index 00000000..26be8023
--- /dev/null
+++ b/src/modules/compositors/index.ts
@@ -0,0 +1,164 @@
+import { CompositorHyprland } from "./hyprland";
+import GObject, { getter, gtype, property, register } from "ags/gobject";
+
+import GLib from "gi://GLib?version=2.0";
+
+
+export default Compositors;
+
+/** WIP modular implementation of a system that supports implementing
+* a variety of Wayland Compositors
+* @todo implement more general compositor info + a lot of stuff
+* */
+export namespace Compositors {
+ let compositor: Compositor|null = null;
+
+ @register({ GTypeName: "CompositorMonitor" })
+ export class Monitor extends GObject.Object {
+ #width: number;
+ #height: number;
+
+ @getter(Number)
+ get width() { return this.#width; }
+
+ @getter(Number)
+ get height() { return this.#height; }
+
+ @property(Number)
+ scaling: number;
+
+ constructor(width: number, height: number, scaling: number = 1) {
+ super();
+
+ this.#width = width;
+ this.#height = height;
+ this.scaling = scaling;
+ }
+ }
+
+ @register({ GTypeName: "CompositorWorkspace" })
+ export class Workspace extends GObject.Object {
+ #id: number;
+ #monitor: Monitor;
+
+ @getter(Number)
+ get id() { return this.#id; }
+
+ @getter(Monitor)
+ get monitor() { return this.#monitor; }
+
+ constructor(monitor: Monitor, id: number = 0) {
+ super();
+
+ this.#monitor = monitor;
+ this.#id = id;
+ }
+ }
+
+ @register({ GTypeName: "CompositorClient" })
+ export class Client extends GObject.Object {
+ readonly #address: string|null = null;
+ #initialClass: string;
+ #class: string;
+ #title: string = "";
+ #mapped: boolean = true;
+ #position: [number, number] = [0, 0];
+ #xwayland: boolean = false;
+
+ @getter(gtype(String))
+ get address() { return this.#address; }
+
+ @getter(String)
+ get title() { return this.#title; }
+
+ @getter(String)
+ get class() { return this.#class; }
+
+ @getter(String)
+ get initialClass() { return this.#initialClass; }
+
+ @getter(gtype<[number, number]>(Array))
+ get position() { return this.#position; }
+
+ @getter(Boolean)
+ get xwayland() { return this.#xwayland; }
+
+ @getter(Boolean)
+ get mapped() { return this.#mapped; }
+
+ constructor(props: {
+ address?: string;
+ title?: string;
+ mapped?: boolean;
+ class: string;
+ initialClass?: string;
+ /** [x, y] */
+ position?: [number, number];
+ }) {
+ super();
+
+ this.#class = props.class;
+
+ if(props.title !== undefined)
+ this.#title = props.title;
+
+ if(props.mapped !== undefined)
+ this.#mapped = props.mapped;
+
+ if(props.address !== undefined)
+ this.#address = props.address;
+
+ if(props.position !== undefined)
+ this.#position = props.position;
+
+ this.#initialClass = props.initialClass !== undefined ?
+ props.initialClass
+ : props.class;
+ }
+ }
+
+ @register({ GTypeName: "Compositor" })
+ export class Compositor extends GObject.Object {
+ protected _workspaces: Array = [];
+ protected _focusedClient: Client|null = null;
+
+ @getter(Array)
+ get workspaces() { return this._workspaces; }
+
+ @getter(gtype(Client))
+ get focusedClient() { return this._focusedClient; }
+
+ constructor() {
+ super();
+ }
+ };
+
+
+ export function getDefault(): Compositor {
+ if(!compositor)
+ throw new Error("Compositors haven't been initialized correctly, please call `Compositors.init()` before calling any method in `Compositors`");
+
+ return compositor;
+ }
+
+
+ /** Uses the XDG_CURRENT_DESKTOP variable to detect running compositor's name.
+ * ---
+ * @returns running wayland compositor's name (lowercase) or `undefined` if variable's not set */
+ export function getName(): string|undefined {
+ return GLib.getenv("XDG_CURRENT_DESKTOP")?.toLowerCase() ?? undefined;
+ }
+
+ /** initialize colorshell's wayland compositor implementation abstraction.
+ * when called, and if it's implemented, sets the default compositor to an equivalent implementation for the current desktop(checks from XDG_CURRENT_DESKTOP) */
+ export function init(): void {
+ switch(Compositors.getName()) {
+ case "hyprland":
+ compositor = new CompositorHyprland();
+ break;
+
+ default:
+ console.error(`This compositor(${Compositors.getName()}) is not yet implemented to colorshell. Please contribute by implementing it if you can! :)`);
+ }
+ }
+}
diff --git a/src/modules/config.ts b/src/modules/config.ts
new file mode 100644
index 00000000..e79fa7b8
--- /dev/null
+++ b/src/modules/config.ts
@@ -0,0 +1,219 @@
+import { timeout } from "ags/time";
+import { monitorFile, readFileAsync, writeFileAsync } from "ags/file";
+import { Notifications } from "./notifications";
+import { Accessor } from "ags";
+import GObject, { getter, gtype, register } from "ags/gobject";
+
+import Gio from "gi://Gio?version=2.0";
+import AstalIO from "gi://AstalIO";
+import AstalNotifd from "gi://AstalNotifd";
+
+
+export { Config };
+type ValueTypes = "string" | "boolean" | "object" | "number" | "any";
+
+@register({ GTypeName: "Config" })
+class Config extends GObject.Object {
+ declare $signals: GObject.Object.SignalSignatures & {
+ "notify::entries": (entries: Record) => void;
+ };
+
+ /** unmodified object with default entries. User-values are stored
+ * in the `entries` field */
+ public readonly defaults: Record;
+
+ @getter(gtype>(Object))
+ public get entries() { return this.#entries; }
+
+ #file: Gio.File;
+ #entries: Record;
+
+ private timeout: (AstalIO.Time|boolean|undefined);
+ public get file() { return this.#file; };
+
+ constructor(filePath: Gio.File|string, defaults?: Record) {
+ super();
+
+ this.defaults = (defaults ?? {}) as Record;
+ this.#entries = { ...defaults } as Record;
+
+ this.#file = (typeof filePath === "string") ?
+ Gio.File.new_for_path(filePath)
+ : filePath;
+
+ if(!this.#file.query_exists(null)) {
+ this.#file.make_directory_with_parents(null);
+ this.#file.delete(null);
+
+ this.writeFile().catch(e => Notifications.getDefault().sendNotification({
+ appName: "colorshell",
+ summary: "Write error",
+ body: `Couldn't write default configuration file to "${this.#file.get_path()!
+ }".\nStderr: ${e}`
+ }));
+ }
+
+ 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?`
+ });
+ }
+ );
+
+ this.readFile().catch(e => console.error(
+ `Config: An error occurred while read the configuration file. Stderr: ${e}`
+ ));
+ }
+
+ private async writeFile(): Promise {
+ this.timeout = true;
+ await writeFileAsync(
+ this.#file.get_path()!, JSON.stringify(this.entries, undefined, 4)
+ ).finally(() => this.timeout = false);
+ }
+
+ private async readFile(): Promise {
+ await readFileAsync(this.#file.get_path()!).then((content) => {
+ let config: (Record|undefined);
+
+ try {
+ config = JSON.parse(content) as Record;
+ } 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}`
+ });
+ }
+
+ 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 Record] = config[k as keyof typeof config];
+ }
+
+ this.notify("entries");
+ }).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: ${this.#file.get_path()!
+ }\n${e.message}`.replace(/[<>]/g, "\\&")
+ });
+ });
+ }
+
+ public bindProperty(path: string, expectType: "boolean"): Accessor;
+ public bindProperty(path: string, expectType: "number"): Accessor;
+ public bindProperty(path: string, expectType: "string"): Accessor;
+ public bindProperty(path: string, expectType: "object"): Accessor;
+ public bindProperty(path: string, expectType: "any"): Accessor;
+ public bindProperty(path: string, expectType: undefined): Accessor