diff --git a/.gitignore b/.gitignore index 1622101..20871df 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,7 @@ # Ignore personal configurations (e.g.: hyprpaper.conf, input.conf) hypr/hyprpaper.conf hypr/input.conf +hypr/hyprsunset.conf +*_wal.scss *.log diff --git a/README.md b/README.md index e2af1cf..6969b37 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,109 @@ -# Retrozinn's Hyprland Dots -My customized Hyprland dotfiles that I keep improving almost everyday 🤩 ! +# colorshell +

(previously retrozinndev/Hyprland-Dots)

-> [!warning] -> This is the branch for the Elkowar's Wacky Widgets(eww) edition! If you're
-> here for the waybar version instead, go to the [`waybar-edition`](https://github.com/retrozinndev/Hyprland-Dots/tree/waybar-edition) branch! +My Hyprland desktop shell that I keep improving almost everyday! 🤩 (i love doing this)
-> [!note] -> I'm currently developing an [AGS](https://aylur.github.io/ags)+[Astal](https://aylur.github.io/astal) version of these dots! Peek a little on
-> how developement is going in the [`ryoland`](https://github.com/retrozinndev/Hyprland-Dots/tree/ryoland) branch.
+This repository includes a desktop shell made with [GTK], using [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 -
- - - - - - - - - more shots in [`repo/shots`](https://github.com/retrozinndev/Hyprland-Dots/tree/ryo/repo/shots) - -
- +![Kitty](repo/shots/desktop.png) +![Widgets](repo/shots/widgets.png) +![Runner](repo/shots/runner.png) +![Browser + Neovim](repo/shots/browser-neovim.png) ## šŸŽØ Colors -All the colors of the interface are dynamically generated from your wallpaper! This is possible by using [pywal16] (fork of pywal), a cli tool to generate color schemes on the fly. +All the shell colors are dynamically generated from your wallpaper! +This is possible by using [pywal16](fork of the archived [pywal](https://github.com/dylanaraps/pywal) project), a cli tool to generate color schemes on the fly. ## šŸ–¼ļø Wallpapers -When you're at the [Installation](#Installation) process, you can choose whether to install my wallpapers. If you chose to install, you can select any of them by clicking to change wallpaper in the Control Center. Or if you haven't chose to install, you can create the directory `~/wallpapers` in your home directory `~` and put an image you want to use as wallpaper and choose it using the menu inside control center and also by pressing SUPER + W! +When you're at the [Installation](#Installation) process, you can choose whether to install the wallpapers. +Or if you haven't, you can just create a directory `~/wallpapers` in your home `~` and put images you want to use as wallpapers! -See more bindings inside the `~/.config/hypr/bindings.conf` file or check the [Wiki/Usage] page! +You can select any of the images inside `~/wallpapers` by pressing SUPER + W or by accessing the +Control Center and clicking in the image icon on top. ### ā„¹ļø Source -All wallpapers inside this repo are not made by me! You can find all sources inside the [`WALLPAPERS.md`](https://github.com/retrozinndev/Hyprland-Dots/blob/ryo/WALLPAPERS.md) file. +None of the wallpapers available in this repo are made by me! You can find sources inside the [`WALLPAPERS.md`](https://github.com/retrozinndev/Hyprland-Dots/blob/ryo/WALLPAPERS.md) file. (it took me a lot of time to make this sources list 😭) + +### āœ”ļø What's included in this shell +- Pretty Top-Bar + - Apps button(basically the "start menu", opens the full-screen app launcher) + - Workspace indicator(contains icon of last used application on each) + - Focused Client(Window) information(title, class and icon) + - Clock(with date) + - Media(shows only when media is being played) + - Tray(Applications running in the background) + - Status (volume information, bluetooth, network and notification status) +- Control Center + - Volume Controls (Microphone and Speaker) + - Volume Mixer(per-app volume) + - Pages(the thing that shows up when you click the arrow on a tile) + - Bluetooth devices + - Network devices + - Night Light controls + - Tiles + - Screen Recording + - Bluetooth + - Night Light + - Network(wifi needs work, i don't have wifi in my machine) + - Don't Disturb(disables notification popups) +- Center Window(clock, calendar + media management) +- Notifications with support for application actions + Notification History +- Localization(see [🌐 Internationalization](#-internationalization) for available languages) +- Application Runner with support for plugins ([anyrun](https://github.com/anyrun-org/anyrun)-like) + - Shell(`!`): Run shell commands with the user shell + - Clipboard(`>`): Search through your clipboard history + - Wallpapers(`#`): Search and select to change wallpaper + - Media(`:`): Control playing media + - Search(`?`): Search something on the internet with your default browser +- Gnome-like application runner(the fullscreen one) +- Support for your multiple monitors + +## āŒØļø Binds +You can see pre-configured bindings in the [Wiki/Bindings] page! + +## 🌐 Internationalization +Colorshell supports i18n! The shell automatically matches the shell language with the system's, if available.
+Currently, there's support for the following languages: +- English (United States), maintained by [@retrozinndev](https://github.com/retrozinndev) +- PortuguĆŖs (Brasil), maintained by [@retrozinndev](https://github.com/retrozinndev) + +Don't see your language here? You can contribute and make translations too!
+You can do so by forking this repository, translating the shell in your fork and then opening a pull request to this repository, simple as that! +(I'll create a more detailed guide for that soon) ## āš™ļø Installation -See the Installation Guide on [Wiki/Installation]. +See the Installation Guide on [Wiki/Installation]. (needs updates, shell was just launched) -### šŸŽ‰ Tools +## šŸŽ‰ Tools - Browser: [Zen Browser] - Text Editor: [Neovim], my config is [here](https://github.com/retrozinndev/nvim-conf.lua) - Terminal Emulator: [Kitty] -- Shell: [Nushell] -- See more on the [wiki]! +- Terminal shell: [Nushell] ## ā— 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]. +This repo is licensed under the [MIT License], project is made and maintained by [retrozinndev](https://github.com/retrozinndev). -## 🌠 Stargazers Graph -Thanks to everyone who starred my dotfiles! šŸ’– -[![Stargazers over time](https://starchart.cc/retrozinndev/Hyprland-Dots.svg?background=%2324292e&axis=%23fafbfc&line=%232dba4e)](https://starchart.cc/retrozinndev/Hyprland-Dots) +## 🌠 Stargazers +Thanks to everyone who starred my project! šŸ’– +[![Stargazers over time]( + https://starchart.cc/retrozinndev/Hyprland-Dots.svg?background=%2324292e&axis=%23fafbfc&line=%232dba4e +)](https://starchart.cc/retrozinndev/Hyprland-Dots) [pywal16]: https://github.com/eylles/pywal16 [zen browser]: https://zen-browser.app [neovim]: https://neovim.io [nushell]: https://nushell.sh -[kitty]: https://sw.kovidgoyal.net/kitty/ +[kitty]: https://sw.kovidgoyal.net/kitty +[ags]: https://aylur.github.io/ags +[astal]: https://aylur.github.io/astal +[typescript]: https://typescriptlang.org +[gtk]: https://www.gtk.org [mit license]: https://en.wikipedia.org/wiki/MIT_License @@ -75,6 +116,7 @@ Thanks to everyone who starred my dotfiles! šŸ’– [wiki/dependencies]: https://github.com/retrozinndev/Hyprland-Dots/wiki/Dependencies [wiki/usage]: https://github.com/retrozinndev/Hyprland-Dots/wiki/Usage [wiki/installation]: https://github.com/retrozinndev/Hyprland-Dots/wiki/Installation +[wiki/bindings]: https://github.com/retrozinndev/Hyprland-Dots/wiki/Bindings - + [new issue]: https://github.com/retrozinndev/Hyprland-Dots/issues/new diff --git a/WALLPAPERS.md b/WALLPAPERS.md index 2d1c4d9..4bf84b2 100644 --- a/WALLPAPERS.md +++ b/WALLPAPERS.md @@ -1,40 +1,523 @@ # About Walppapers -None of them are made by me. You can find their artists, and more wallpapers in -the links down below. +None of them are made by me. You can find their artists, and more wallpapers +in their source link. -## Bocchi The Rock! Wallpapers +## Bocchi The Rock! -- [Pinterest](https://pinterest.com) -- [Alpha Coders](https://alphacoders.com/bocchi-the-rock!-wallpapers) -- [Wallpaper Cave](https://wallpapercave.com/bocchi-the-rock-wallpapers) -- [Wallpaper Flare](https://www.wallpaperflare.com/search?wallpaper=BOCCHI+THE+ROCK%21) +
+ + 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 Miku (DeviantArt, nesyah)](https://www.deviantart.com/nesyah/art/Arch-linux-feat-Hatsune-Miku-858316759) -- [Others (Alpha Coders)](https://alphacoders.com/vocaloid-wallpapers) +
+ + Arch Linux Miku + + + -## Dan Da Dan Wallpapers +- Source: [DeviantArt](https://www.deviantart.com/nesyah/art/Arch-linux-feat-Hatsune-Miku-858316759) +
-- [Alpha Coders](https://alphacoders.com/dandadan-wallpapers) +
+ + Gumi Bridge + + + -## Frieren: Beyond Journey's End Wallpapers +- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=593482) +
-- [Alpha Coders](https://alphacoders.com/frieren-beyond-journeys-end-wallpapers) +
+ + Gumi VOCALOID + + + -## Hypr-chan Wallpaper +- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=768096) +
-- [GitHub (hyprwm/Hyprland)](https://github.com/hyprwm/Hyprland) +
+ + Miku Stylish with Glasses + + + -## Linux Girl Wallpaper +- Source: [Alpha Coders](https://wall.alphacoders.com/big.php?i=1305668) +
-- [WallHere](https://wallhere.com/en/wallpaper/2284648) +
+ + 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) + + ## Gruvbox-styled Wallpapers - [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!! + + + -Those can be get on web events from Genshin Impact, the game, and also on [HoYoLAB](https://hoyolab.com) +- Source: Genshin Impact Web Event (not available anymore) +
+ + +## 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 new file mode 100644 index 0000000..298eb4d --- /dev/null +++ b/ags/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +@girs/ diff --git a/ags/app.ts b/ags/app.ts new file mode 100644 index 0000000..c566afc --- /dev/null +++ b/ags/app.ts @@ -0,0 +1,108 @@ +import AstalNotifd from "gi://AstalNotifd"; + +import { App } from "astal/gtk3" +import { Wireplumber } from "./scripts/volume"; + +import { handleArguments } from "./scripts/arg-handler"; +import { Time, timeout } from "astal/time"; + +import { OSDModes, setOSDMode } from "./window/OSD"; + +import { Runner } from "./runner/Runner"; +import { PluginApps } from "./runner/plugins/apps"; +import { PluginShell } from "./runner/plugins/shell"; +import { PluginWebSearch } from "./runner/plugins/websearch"; +import { PluginMedia } from "./runner/plugins/media"; +import { Windows } from "./windows"; +import { Notifications } from "./scripts/notifications"; +import { GObject } from "astal"; +import { PluginWallpapers } from "./runner/plugins/wallpapers"; +import { Wallpaper } from "./scripts/wallpaper"; +import { Stylesheet } from "./scripts/stylesheet"; +import { Clipboard } from "./scripts/clipboard"; +import { PluginClipboard } from "./runner/plugins/clipboard"; + + +let osdTimer: (Time|undefined); +let connections = new Map | number)>(); + +const defaultWindows: Array = [ "bar" ]; +const runnerPlugins: Array = [ + PluginApps, + PluginShell, + PluginWebSearch, + PluginMedia, + new PluginWallpapers(), + PluginClipboard +]; + +App.start({ + instanceName: "astal", + requestHandler: (request: string, response: (result: any) => void): void => { + response(handleArguments(request)); + }, + main: (..._args: Array) => { + console.log(`Initialized astal instance as: ${ App.instanceName || "astal" }`); + + Stylesheet.getDefault().compileApply(); + + App.vfunc_dispose = () => { + console.log("Disconnecting stuff"); + connections.forEach((v, k) => Array.isArray(v) ? + v.map(id => k.disconnect(id)) + : k.disconnect(v)); + }; + + // Init clipboard module + Clipboard.getDefault(); + + connections.set(Wireplumber.getDefault(), [ + Wireplumber.getDefault().getDefaultSink().connect("notify::volume", () => + triggerOSD(OSDModes.SINK)) + ]); + + connections.set(Notifications.getDefault(), [ + Notifications.getDefault().connect("notification-added", (_, _notif: AstalNotifd.Notification) => { + Windows.open("floating-notifications"); + }), + Notifications.getDefault().connect("notification-removed", (_: Notifications, _id: number) => { + _.notifications.length === 0 && Windows.close("floating-notifications"); + }) + ]); + + console.log("Initializing wallpaper handler"); + Wallpaper.getDefault(); + + console.log("Adding runner plugins"); + runnerPlugins.map(plugin => Runner.addPlugin(plugin)); + + console.log("Opening default windows"); + // Open openOnStart windows + defaultWindows.map(name => { + if(Windows.isVisible(name)) return; + Windows.open(name); + }); + } +}); + +function triggerOSD(osdModeParam: OSDModes) { + if(Windows.isVisible("control-center")) return; + + Windows.open("osd"); + + if(!osdTimer) { + setOSDMode(osdModeParam); + osdTimer = timeout(3000, () => { + osdTimer = undefined; + Windows.close("osd"); + }); + + return; + } + + osdTimer.cancel(); + osdTimer = timeout(3000, () => { + Windows.close("osd"); + osdTimer = undefined; + }); +} diff --git a/ags/env.d.ts b/ags/env.d.ts new file mode 100644 index 0000000..467c0a4 --- /dev/null +++ b/ags/env.d.ts @@ -0,0 +1,21 @@ +declare const SRC: string + +declare module "inline:*" { + const content: string + export default content +} + +declare module "*.scss" { + const content: string + export default content +} + +declare module "*.blp" { + const content: string + export default content +} + +declare module "*.css" { + const content: string + export default content +} diff --git a/ags/i18n/intl.ts b/ags/i18n/intl.ts new file mode 100644 index 0000000..c1bcaea --- /dev/null +++ b/ags/i18n/intl.ts @@ -0,0 +1,56 @@ +import { GLib } from "astal"; + + +const i18nKeys = { + "en_US": (await import("./lang/en_US")).default, + "pt_BR": (await import("./lang/pt_BR")).default +}; + +const languages: Array = Object.keys(i18nKeys); +let language: string = getSystemLanguage(); + +export function getSystemLanguage(): string { + const sysLanguage: (string|null|undefined) = GLib.getenv("LANG") || GLib.getenv("LANGUAGE"); + + if(!sysLanguage) { + console.log(`[WARNING] Couldn't get system language, fallback to default ${languages[0]}`); + console.log("[TIP] Please set the LANG or LANGUAGE environment variable"); + + return languages[0]; + } + + return sysLanguage.split('.')[0]; +} + +export function setLanguage(lang: string): string { + languages.map((cur: string) => { + if(cur === lang) { + language = lang; + return lang; + } + }); + + throw new Error(`(i18n/intl) Couldn't set language: ${lang}`, { + cause: `Language ${lang} not found in languages of type ${typeof languages}` + }); +} + +export function tr(key: string): string { + let result = i18nKeys[language as keyof typeof i18nKeys], + defResult = i18nKeys[languages[0] as keyof typeof i18nKeys]; + + for(const keyString of key.split('.')) { + result = result[keyString as keyof typeof result] as never; + defResult = defResult[keyString as keyof typeof defResult] as never; + } + + return (typeof result == "string") ? + result + : ((typeof defResult == "string") ? + defResult + : "not found / is not of type \"string\""); +} + +export function trGet() { + return i18nKeys[getSystemLanguage() as keyof typeof i18nKeys]; +} diff --git a/ags/i18n/lang/en_US.ts b/ags/i18n/lang/en_US.ts new file mode 100644 index 0000000..d454fbb --- /dev/null +++ b/ags/i18n/lang/en_US.ts @@ -0,0 +1,82 @@ +import { i18nStruct } from "../struct"; + +export default { + language: "English (United States)", + + cancel: "Cancel", + accept: "Ok", + devices: "Devices", + others: "Others", + + connected: "Connected", + disconnected: "Disconnected", + unknown: "Unknown", + connecting: "Connecting", + apps: "Applications", + + connect: "Connect", + disconnect: "Disconnect", + + control_center: { + tiles: { + enabled: "Enabled", + disabled: "Disabled", + more: "More", + + network: { + network: "Network", + wireless: "Wireless", + wired: "Wired" + }, + recording: { + title: "Screen Recording", + disabled_desc: "Start recording", + enabled_desc: "Stop recording", + }, + dnd: { + title: "Do Not Disturb" + }, + night_light: { + title: "Night Light", + default_desc: "Fidelity" + } + }, + pages: { + more_settings: "More settings", + sound: { + title: "Sound", + description: "Configure the audio output" + }, + microphone: { + title: "Microphone", + description: "Configure the audio input" + }, + night_light: { + title: "Night Light", + description: "Control Night Light and Gamma filters", + gamma: "Gamma", + temperature: "Temperature" + }, + bluetooth: { + title: "Bluetooth", + description: "Manage Bluetooth devices", + new_devices: "New devices", + adapters: "Adapters", + paired_devices: "Paired Devices", + start_discovering: "Start discovering", + stop_discovering: "Stop discovering", + untrust_device: "Untrust device", + unpair_device: "Unpair device", + trust_device: "Trust device", + pair_device: "Pair device" + }, + network: { + title: "Network", + interface: "Interface" + } + } + }, + ask_popup: { + title: "Question" + } +} as i18nStruct; diff --git a/ags/i18n/lang/pt_BR.ts b/ags/i18n/lang/pt_BR.ts new file mode 100644 index 0000000..e5be96b --- /dev/null +++ b/ags/i18n/lang/pt_BR.ts @@ -0,0 +1,82 @@ +import { i18nStruct } from "../struct"; + +export default { + language: "PortuguĆŖs (Brasil)", + + cancel: "Cancelar", + accept: "Ok", + devices: "Dispositivos", + others: "Outros", + + connected: "Conectado", + disconnected: "Desconectado", + unknown: "Desconhecido", + connecting: "Conectando", + disconnect: "Desconectar", + connect: "Conectar", + + apps: "Aplicativos", + + control_center: { + tiles: { + enabled: "Ligado", + disabled: "Desligado", + more: "Mais", + + network: { + network: "Rede", + wireless: "Wi-Fi", + wired: "Cabeada" + }, + recording: { + title: "Gravação de Tela", + disabled_desc: "Iniciar gravação", + enabled_desc: "Parar gravação", + }, + dnd: { + title: "NĆ£o Perturbe" + }, + night_light: { + title: "Luz Noturna", + default_desc: "Fidelidade" + } + }, + pages: { + more_settings: "Mais configuraƧƵes", + sound: { + title: "Som", + description: "Controle a saĆ­da de Ć”udio" + }, + microphone: { + title: "Microfone", + description: "Configure a entrada de Ć”udio" + }, + night_light: { + title: "Luz Noturna", + description: "Controle os filtros de Luz Noturna e Gama", + temperature: "Temperatura", + gamma: "Gama" + }, + bluetooth: { + title: "Bluetooth", + description: "Gerencie dispositivos Bluetooth", + new_devices: "Novos Dispositivos", + adapters: "Adaptadores", + paired_devices: "Dispositivos Pareados", + start_discovering: "ComeƧar a procurar dispositivos", + stop_discovering: "Parar de procurar dispositivos", + pair_device: "Parear dispositivo", + trust_device: "Confiar no dispositivo", + unpair_device: "Desparear dispositivo", + untrust_device: "Deixar de confiar no dispositivo" + }, + network: { + title: "Rede", + interface: "Interface" + } + } + }, + ask_popup: { + title: "Pergunta" + } +} as i18nStruct; diff --git a/ags/i18n/struct.ts b/ags/i18n/struct.ts new file mode 100644 index 0000000..4ce1222 --- /dev/null +++ b/ags/i18n/struct.ts @@ -0,0 +1,83 @@ +export type i18nStruct = { + language: string, + + cancel: string, + accept: string, + + connected: string, + disconnected: string, + connecting: string, + unknown: string, + + devices: string, + others: string, + + disconnect: string, + connect: string, + + apps: string; + + control_center: { + tiles: { + enabled: string, + disabled: string, + more: string, + + network: { + network: string, + wireless: string, + wired: string + }, + recording: { + title: string, + disabled_desc: string, + enabled_desc: string + }, + dnd: { + title: string + }, + night_light: { + title: string, + default_desc: string + } + }, + pages: { + more_settings: string, + + sound: { + title: string, + description: string + }, + microphone: { + title: string, + description: string + }, + network: { + title: string, + interface: string + }, + bluetooth: { + title: string, + description: string, + adapters: string, + new_devices: string, + paired_devices: string, + start_discovering: string, + stop_discovering: string, + trust_device: string, + untrust_device: string, + pair_device: string, + unpair_device: string + }, + night_light: { + title: string, + description: string, + temperature: string, + gamma: string + } + } + }, + ask_popup: { + title: string + } +}; diff --git a/ags/package-lock.json b/ags/package-lock.json new file mode 100644 index 0000000..c9970a7 --- /dev/null +++ b/ags/package-lock.json @@ -0,0 +1,21 @@ +{ + "name": "astal-shell", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "astal-shell", + "dependencies": { + "astal": "/usr/share/astal/gjs" + } + }, + "../../../../usr/share/astal/gjs": { + "name": "astal", + "license": "LGPL-2.1" + }, + "node_modules/astal": { + "resolved": "../../../../usr/share/astal/gjs", + "link": true + } + } +} diff --git a/ags/package.json b/ags/package.json new file mode 100644 index 0000000..44226f2 --- /dev/null +++ b/ags/package.json @@ -0,0 +1,6 @@ +{ + "name": "astal-shell", + "dependencies": { + "astal": "/usr/share/astal/gjs" + } +} diff --git a/ags/runner/Runner.ts b/ags/runner/Runner.ts new file mode 100644 index 0000000..37ec63f --- /dev/null +++ b/ags/runner/Runner.ts @@ -0,0 +1,268 @@ +import { AstalIO, timeout } from "astal"; +import { Astal, Gdk, Gtk, Widget } from "astal/gtk3"; +import { PopupWindow, PopupWindowProps } from "../widget/PopupWindow"; +import { updateApps } from "../scripts/apps"; +import { ResultWidget, ResultWidgetProps } from "../widget/runner/ResultWidget"; +import { Windows } from "../windows"; + +export namespace Runner { +export type RunnerProps = { + halign?: Gtk.Align; + valign?: Gtk.Align; + width?: number; + height?: number; + entryPlaceHolder?: string; + initialText?: string; + showResultsPlaceHolderOnStartup?: boolean; +}; + +export interface Plugin { + /** prefix to call the plugin. if undefined, will be triggered like applications plugin */ + readonly prefix?: string; + /** name of the plugin. e.g.: websearch, shell */ + readonly name?: string; + /** ran on runner open */ + readonly init?: () => void; + /** handle the user input to return results (does not include plugin's prefix) */ + readonly handle: (inputText: string) => (ResultWidget|Array|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): boolean { + search = search.replace(/[\\^$.*?()[\]{}|]/g, "\\$&"); + return new RegExp(`${search.split('').map(c => + `.*(${c.toLowerCase()}|${c.toUpperCase()}).*`).join('')}` + ).test(item); +} + + +export function addPlugin(plugin: Runner.Plugin, force?: boolean) { + if(!force && plugin.prefix && plugins.has(plugin)) + throw new Error(`Runner plugin with prefix ${plugin.prefix} already exists`); + + plugins.delete(plugin); + plugins.add(plugin); +} + +export function getPlugins(): Array { + return [...plugins.values()]; +} + +/** Removes a plugin from the runner plugin list + * @returns true if plugin was removed or false if plugin wasn't found + */ +export function removePlugin(plugin: Plugin): boolean { + return plugins.delete(plugin); +} + +export function setEntryText(text: string): void { + gtkEntry?.set_text(text); + gtkEntry?.set_position(gtkEntry.textLength); + + gtkEntry?.grab_focus_without_selecting(); +} + +export function openDefault(initialText?: string) { + return Runner.openRunner({ + entryPlaceHolder: "Start typing...", + showResultsPlaceHolderOnStartup: false, + initialText + } as Runner.RunnerProps, + () => [ + new ResultWidget({ + icon: "application-x-executable-symbolic", + title: "Use your applications", + description: "Search for any app installed in your computer", + closeOnClick: false, + onClick: () => gtkEntry?.grab_focus() + } as ResultWidgetProps), + new ResultWidget({ + icon: "edit-paste-symbolic", + title: "See your clipboard history", + description: "Start your search with '>' to go through your clipboard history", + closeOnClick: false, + onClick: () => setEntryText('>') + } as ResultWidgetProps), + new ResultWidget({ + icon: "image-x-generic-symbolic", + title: "Change your wallpaper", + description: "Add '#' at the start to search through the wallpapers folder!", + closeOnClick: false, + onClick: () => setEntryText('#'), + } as ResultWidgetProps), + new ResultWidget({ + icon: "utilities-terminal-symbolic", + title: "Run shell commands", + description: "Add '!' before your command to run it (pro tip: add a second '!' to show command output)", + closeOnClick: false, + onClick: () => setEntryText('!!') + } as ResultWidgetProps), + new ResultWidget({ + icon: "media-playback-start-symbolic", + title: "Control media", + description: "Type ':' to control playing media", + closeOnClick: false, + onClick: () => setEntryText(':') + } as ResultWidgetProps), + new ResultWidget({ + icon: "applications-internet-symbolic", + title: "Search the Web", + description: "Start typing with '?' prefix to search the web", + closeOnClick: false, + onClick: () => setEntryText('?') + } as ResultWidgetProps) + ]); +} + +export function openRunner(props?: RunnerProps, placeholder?: () => Array): Widget.Window { + let onClickTimeout: (AstalIO.Time|undefined); + + gtkEntry = new Widget.Entry({ + className: "search", + placeholderText: props?.entryPlaceHolder || "", + onChanged: (self) => { + updateResultsList(self.text); + resultsList.get_row_at_index(0) && + resultsList.select_row(resultsList.get_row_at_index(0)); + }, + onActivate: (entry) => { + const resultWidget = resultsList.get_selected_row()?.get_child(); + if(resultWidget instanceof ResultWidget) { + entry.isFocus = false; + resultWidget.onClick(); + resultWidget.closeOnClick && Runner.close(); + } + }, + primary_icon_name: "system-search" + } as Widget.EntryProps); + + const resultsList: Gtk.ListBox = new Gtk.ListBox({ + visible: true, + expand: true + } as Gtk.ListBox.ConstructorProps); + + if(props?.showResultsPlaceHolderOnStartup && placeholder) { + const placeholderWidgets = placeholder(); + placeholderWidgets.map(widget => + resultsList.insert(widget, -1)); + } + + function cleanResults() { + resultsList.get_children().map((listItem) => { + resultsList.remove(listItem); + }); + } + + function getPluginResults(input: string): Array { + 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; + } + } + + return calledPlugins.map(plugin => plugin.handle( + plugin.prefix ? input.replace(plugin.prefix, "") : input) + ).filter(value => value !== undefined && value !== null).flat(1); + } + + function updateResultsList(entryText: string) { + const widgets: Array = []; + + // Remove all previous results + cleanResults(); + + widgets.push(...getPluginResults(entryText)) + + // Insert placeholder if there are no results + if(placeholder && widgets.length === 0) + widgets.push(...placeholder()); + + // Insert results inside GtkListBox + widgets.map((resultWidget: ResultWidget) => { + resultsList.insert(resultWidget, -1); + + resultsList.connect("row-activated", (_, row: Gtk.ListBoxRow) => { + const rWidget = row.get_child(); + if(rWidget instanceof ResultWidget) { + if(onClickTimeout) return; + + // Timeout, so it doesn't fire the event a hundred times :skull: + onClickTimeout = timeout(500, () => onClickTimeout = undefined); + rWidget.onClick(); + rWidget.closeOnClick && Runner.close(); + } + }); + }); + } + + if(!instance) + instance = Windows.createWindowForFocusedMonitor((mon: number): (Widget.Window) => PopupWindow({ + namespace: "runner", + monitor: mon, + widthRequest: props?.width ?? 750, + heightRequest: props?.height ?? 450, + marginTop: 240, + anchor: Astal.WindowAnchor.TOP | Astal.WindowAnchor.BOTTOM, + setup: () => { + // Init plugins + plugins.forEach(plugin => plugin.init && plugin.init()); + + if(props?.initialText) + Runner.setEntryText(props.initialText); + }, + onKeyPressEvent: (_, event: Gdk.Event) => { + const keyVal = event.get_keyval()[1]; + + if(!gtkEntry!.has_focus && keyVal !== Gdk.KEY_F5 + && keyVal !== Gdk.KEY_Down && keyVal !== Gdk.KEY_Up + && keyVal !== Gdk.KEY_Return) { + gtkEntry!.grab_focus_without_selecting(); + return; + } + + event.get_keyval()[1] === Gdk.KEY_F5 && + updateApps(); + }, + onDestroy: () => { + gtkEntry = null; + [...plugins.values()].map(plugin => + plugin && plugin.onClose && plugin.onClose()); + instance = null; + }, + child: new Widget.Box({ + className: "runner main", + orientation: Gtk.Orientation.VERTICAL, + expand: false, + valign: Gtk.Align.START, + children: [ + gtkEntry, + new Widget.Scrollable({ + className: "results-scrollable", + vscroll: Gtk.PolicyType.AUTOMATIC, + hscroll: Gtk.PolicyType.NEVER, + expand: true, + propagateNaturalHeight: true, + maxContentHeight: props?.height ?? 450, + child: resultsList + }) + ] + } as Widget.BoxProps) + } as PopupWindowProps))(); + + return instance!; +} +} diff --git a/ags/runner/plugins/apps.ts b/ags/runner/plugins/apps.ts new file mode 100644 index 0000000..3119187 --- /dev/null +++ b/ags/runner/plugins/apps.ts @@ -0,0 +1,20 @@ +import { ResultWidget, ResultWidgetProps } from "../../widget/runner/ResultWidget"; +import AstalApps from "gi://AstalApps"; +import { cleanExec, getAstalApps } from "../../scripts/apps"; +import { Runner } from "../Runner"; +import { Astal } from "astal/gtk3"; + +export const PluginApps = { + // Do not provide prefix, so it's always ran. + name: "Apps", + handle: (text: string) => { + return getAstalApps().fuzzy_query(text).map((app: AstalApps.Application) => + new ResultWidget({ + title: app.get_name(), + description: app.get_description(), + icon: Astal.Icon.lookup_icon(app.iconName) ? app.iconName : "application-x-executable-symbolic", + onClick: () => cleanExec(app) + } as ResultWidgetProps) + ); + } +} as Runner.Plugin; diff --git a/ags/runner/plugins/clipboard.ts b/ags/runner/plugins/clipboard.ts new file mode 100644 index 0000000..67c838e --- /dev/null +++ b/ags/runner/plugins/clipboard.ts @@ -0,0 +1,33 @@ +import { Widget } from "astal/gtk3"; +import { Clipboard } from "../../scripts/clipboard"; +import { ResultWidget, ResultWidgetProps } from "../../widget/runner/ResultWidget"; +import { Runner } from "../Runner"; +import { Gio } from "astal"; + + +export const PluginClipboard = { + prefix: '>', + prioritize: true, + handle: (search) => { + if(Clipboard.getDefault().history.length < 1) + return new ResultWidget({ + icon: "edit-paste-symbolic", + title: "No clipboard items found!", + description: "When something is copied, it'll be shown right here!" + } as ResultWidgetProps); + + return Clipboard.getDefault().history.filter(item => // not the best way to search, but it works + Runner.regExMatch(search, item.id.toString()) || Runner.regExMatch(search, item.preview)).map((item) => + new ResultWidget({ + icon: new Widget.Label({ + label: item.id.toString(), + css: "font-size: 16px; margin-right: 8px; font-weight: 600;" + } as Widget.LabelProps), + title: item.preview, + onClick: () => Clipboard.getDefault().selectItem(item).catch((err: Gio.IOErrorEnum) => { + console.error(`Runner(Plugin/Clipboard): An error occurred while selecting clipboard item. Stderr:\n${ + err.message ? `${err.message}\n` : ""}Stack: ${err.stack}`); + }) + } as ResultWidgetProps)); + } +} as Runner.Plugin; diff --git a/ags/runner/plugins/media.ts b/ags/runner/plugins/media.ts new file mode 100644 index 0000000..54ef694 --- /dev/null +++ b/ags/runner/plugins/media.ts @@ -0,0 +1,79 @@ +import { bind, Variable } from "astal"; +import { ResultWidget, ResultWidgetProps } from "../../widget/runner/ResultWidget"; +import { Runner } from "../Runner"; +import AstalMpris from "gi://AstalMpris"; + +export const PluginMedia = (() => { + let playTitle: Variable|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/shell.ts b/ags/runner/plugins/shell.ts new file mode 100644 index 0000000..89a6a63 --- /dev/null +++ b/ags/runner/plugins/shell.ts @@ -0,0 +1,59 @@ +import { ResultWidget, ResultWidgetProps } from "../../widget/runner/ResultWidget"; +import { Gio, GLib } from "astal"; +import { Runner } from "../Runner"; +import { Notifications } from "../../scripts/notifications"; + +export const PluginShell = (() => { + + const shell = GLib.getenv("SHELL") ?? "/bin/sh"; + const procLauncher = Gio.SubprocessLauncher.new( + Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE); + + procLauncher.set_cwd(GLib.get_home_dir()); + + return { + prefix: '!', + prioritize: true, + handle: (input: string): ResultWidget => { + let showOutputNotif: boolean = false; + if(input.startsWith('!')) { + input = input.replace('!', ""); + showOutputNotif = true; + } + + const command = input ? GLib.shell_parse_argv(input) : undefined; + + return new ResultWidget({ + onClick: () => { + if(!command || !command[0]) return; + + const proc = procLauncher.spawnv([ shell, "-c", `${input}` ]); + proc.communicate_utf8_async(null, null, (_, asyncResult) => { + const [ success, stdout, stderr ] = proc.communicate_utf8_finish(asyncResult); + + if(!success || stderr) { + Notifications.getDefault().sendNotification({ + appName: shell, + summary: "Command error", + body: `An error occurred on \`${input}\`. Stderr: ${stderr}` + }); + + return; + } + + if(!showOutputNotif) return; + + Notifications.getDefault().sendNotification({ + appName: shell, + summary: "Command output", + body: stdout + }); + }); + }, + title: `Run ${input ? ` \`${input}\`` : `with ${shell.split('/')[shell.split('/').length-1]}`}`, + description: (input || showOutputNotif) && `${input ? `${shell}\t` : ""}${ showOutputNotif ? "(showing output on notification)" : "" }`, + icon: "utilities-terminal-symbolic" + } as ResultWidgetProps) + } + } as Runner.Plugin +})(); diff --git a/ags/runner/plugins/wallpapers.ts b/ags/runner/plugins/wallpapers.ts new file mode 100644 index 0000000..09b21fd --- /dev/null +++ b/ags/runner/plugins/wallpapers.ts @@ -0,0 +1,41 @@ +import { Gio } from "astal"; +import { Wallpaper } from "../../scripts/wallpaper"; +import { Runner } from "../Runner"; +import { ResultWidget, ResultWidgetProps } from "../../widget/runner/ResultWidget"; + + +export class PluginWallpapers implements Runner.Plugin { + prefix = "#"; + prioritize = true; + #files: (Array|undefined); + + init() { + this.#files = []; + const dir = Gio.File.new_for_path(Wallpaper.getDefault().wallpapersPath); + if(dir.query_file_type(null, null) === Gio.FileType.DIRECTORY) { + for(const file of dir.enumerate_children( + "standard::*", + Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS, + null + )) { + this.#files.push(`${dir.get_path()}/${file.get_name()}`); + } + } + } + + handle(search: string) { + if(this.#files!.length > 0) + return this.#files!.filter(file => // not the best way to search, but it works + Runner.regExMatch(search, file.split('/')[file.split('/').length-1]) + ).map(path => new ResultWidget({ + title: path.split('/')[path.split('/').length-1].replace(/\..*$/, ""), + onClick: () => Wallpaper.getDefault().setWallpaper(path) + } as ResultWidgetProps)); + + return new ResultWidget({ + title: "No wallpapers found!", + description: "Define the $WALLPAPERS variable on Hyprland or create a ~/wallpapers directory", + icon: "image-missing-symbolic" + } as ResultWidgetProps); + } +} diff --git a/ags/runner/plugins/websearch.ts b/ags/runner/plugins/websearch.ts new file mode 100644 index 0000000..0f0b5b7 --- /dev/null +++ b/ags/runner/plugins/websearch.ts @@ -0,0 +1,28 @@ +import AstalHyprland from "gi://AstalHyprland"; +import { ResultWidget, ResultWidgetProps } from "../../widget/runner/ResultWidget"; +import { Runner } from "../Runner"; + +const searchEngines = { + duckduckgo: "https://duckduckgo.com/?q=", + google: "https://google.com/search?q=", + yahoo: "https://search.yahoo.com/search?p=" +}; + +let engine: string = searchEngines.google; + +export const PluginWebSearch = { + prefix: '?', + name: "Web Search", + + handle: (search: string): ResultWidget => { + return new ResultWidget({ + icon: "system-search-symbolic", + title: search || "Type your search...", + description: `Search the Web`, + onClick: () => AstalHyprland.get_default().dispatch( + "exec", + `xdg-open \"${engine + search}\"` + ) + } as ResultWidgetProps); + } +} as Runner.Plugin; diff --git a/ags/scripts/apps.ts b/ags/scripts/apps.ts new file mode 100644 index 0000000..2d1cb7b --- /dev/null +++ b/ags/scripts/apps.ts @@ -0,0 +1,67 @@ +import { Astal } from "astal/gtk3"; + +import AstalApps from "gi://AstalApps"; +import AstalHyprland from "gi://AstalHyprland"; + +const astalApps: AstalApps.Apps = new AstalApps.Apps(); +let appsList: Array = astalApps.get_list(); + +export function getApps(): Array { + return appsList; +} + +export function updateApps(): void { + astalApps.reload(); + appsList = astalApps.get_list(); +} + +export function getAstalApps(): AstalApps.Apps { + return astalApps; +} + +export function cleanExec(app: AstalApps.Application): void { + AstalHyprland.get_default().dispatch("exec", app.executable.replace(/(%f|%F|%u|%U|%i|%c|%k)/g, "")); +} + +export function getAppsByName(appName: string): (Array|undefined) { + let found: Array = []; + + getApps().map((app: AstalApps.Application) => { + if(app.get_name().trim().toLowerCase() === appName.trim().toLowerCase() + || (app?.wmClass && app.wmClass.trim().toLowerCase() === appName.trim().toLowerCase())) + found.push(app); + }); + + return (found.length > 0 ? found : undefined); +} + +export function getIconByAppName(appName: string): (string|undefined) { + if(Astal.Icon.lookup_icon(appName)) + return appName; + + if(Astal.Icon.lookup_icon(appName.toLowerCase())) + return appName.toLowerCase(); + + const nameReverseDNS = appName.split('.'); + if(Astal.Icon.lookup_icon(nameReverseDNS[nameReverseDNS.length - 1])) + return nameReverseDNS[nameReverseDNS.length - 1]; + + const found: (AstalApps.Application|undefined) = getAppsByName(appName)?.[0]; + if(Boolean(found)) + return found?.iconName; + + return undefined; +} + +export function getAppIcon(app: (string|AstalApps.Application)): (string|undefined) { + if(typeof app === "string") + return getIconByAppName(app); + + if(app.iconName && Astal.Icon.lookup_icon(app.iconName)) + return app.iconName; + + if(app.wmClass) + return getIconByAppName(app.wmClass); + + return getIconByAppName(app.name); +} diff --git a/ags/scripts/arg-handler.ts b/ags/scripts/arg-handler.ts new file mode 100644 index 0000000..a83ceef --- /dev/null +++ b/ags/scripts/arg-handler.ts @@ -0,0 +1,176 @@ +import { Wireplumber } from "./volume"; +import { Windows } from "../windows"; + +import { restartInstance } from "./reload-handler"; +import { AstalIO, timeout } from "astal"; +import { Runner } from "../runner/Runner"; +import { showWorkspaceNumber } from "../widget/bar/Workspaces"; + +let wsTimeout: (AstalIO.Time|undefined); + +export function handleArguments(request: string): any { + const args: Array = request.split(" "); + switch(args[0]) { + case "open": + case "close": + case "toggle": + return handleWindowArgs(args); + + case "help": + case "h": + return getHelp(); // stop it, get some help + + case "volume": + return handleVolumeArgs(args); + + case "reload": + restartInstance(); + return "Restarting instance..." + + case "windows": + return Object.keys(Windows.windows).map(name => + `${name}: ${Windows.isVisible(name) ? "open" : "closed" }`).join('\n'); + + case "runner": + !Runner.instance ? + Runner.openDefault(args[1] || undefined) + : Runner.close(); + return "Opening runner..." + + case "peek-workspace-num": + if(wsTimeout) return "Workspace numbers are already showing"; + + showWorkspaceNumber(true); + wsTimeout = timeout(2200, () => { + showWorkspaceNumber(false); + wsTimeout = undefined; + }); + return "Toggled workspace numbers"; + + default: + return "command not found! try checking help"; + } +} + +// Didn't want to bloat the switch statement, so I just separated it into functions +function handleWindowArgs(args: Array): string { + 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 new file mode 100644 index 0000000..550a246 --- /dev/null +++ b/ags/scripts/auth.ts @@ -0,0 +1,73 @@ +import { execAsync, Gio, GLib, register } from "astal"; +import Polkit from "gi://Polkit"; +import PolkitAgent from "gi://PolkitAgent"; +import { EntryPopup, EntryPopupProps } from "../widget/EntryPopup"; +import AstalAuth from "gi://AstalAuth"; +import { AskPopup, AskPopupProps } from "../widget/AskPopup"; + +export { Auth }; + +@register({ GTypeName: "AuthAgent" }) +class Auth extends PolkitAgent.Listener { + private static instance: Auth; + #subject: Polkit.Subject; + + constructor() { + super(); + this.#subject = Polkit.UnixSession.new(GLib.get_user_name()); + + this.register(PolkitAgent.RegisterFlags.NONE, + this.#subject, + "/io/github/retrozinndev/Colorshell/PolicyKit/AuthAgent", + null + ); + } + + vfunc_dispose() { + PolkitAgent.Listener.unregister(); + } + + static initiate_authentication(action_id: string, message: string, icon_name: string, details: Polkit.Details, cookie: string, identities: Array, 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 new file mode 100644 index 0000000..b98067d --- /dev/null +++ b/ags/scripts/brightness.ts @@ -0,0 +1,42 @@ +import { exec, execAsync, GObject, monitorFile, readFileAsync, register, signal } from "astal"; +import { Connectable } from "astal/binding"; + + +/** !!TODO!! Needs more work and testing + * I(retrozinndev) don't have a monitor that has software-controlled brightness :( + */ +@register({ GTypeName: "Brightness" }) +class Brightness extends GObject.Object implements Connectable { + private readonly backlight: string|undefined; + private max: number; + private brightness: number; + + @signal(Number) + declare brightnessChanged: (value: number) => void; + + constructor(backlightDevice?: string) { + super(); + this.backlight = backlightDevice || "intel_backlight"; + this.max = Number.parseInt(exec(`brightnessctl -d ${backlightDevice} max`)) + this.brightness = Number.parseInt(exec(`brightnessctl -d ${backlightDevice} get`)) + + readFileAsync(`/sys/class/backlight/${backlightDevice}/brightness`).catch(() => { + throw new Error(`Couldn't find backlight ${backlightDevice}`); + }); + + monitorFile(`/sys/class/backlight/${backlightDevice}/brightness`, async () => { + this.brightness = Number.parseInt(await execAsync(`brightnessctl -d ${backlightDevice} get`)); + this.max = Number.parseInt(await execAsync(`brightnessctl -d ${backlightDevice} max`)); + + this.emit("brightness-changed", this.brightness); + }); + } + + public setBrightness(newBrightness: number): void { + execAsync(`brightnessctl -d ${this.backlight} set ${newBrightness || this.brightness}`).catch(() => { + throw new Error(`Couldn't set brightness of backlight ${this.backlight}`); + }); + + this.emit("brightness-changed", newBrightness); + } +} diff --git a/ags/scripts/clipboard.ts b/ags/scripts/clipboard.ts new file mode 100644 index 0000000..b4a9804 --- /dev/null +++ b/ags/scripts/clipboard.ts @@ -0,0 +1,232 @@ +import { AstalIO, execAsync, Gio, GLib, GObject, monitorFile, property, readFile, register, signal, timeout } from "astal"; + + +export enum ClipboardItemType { + TEXT = 0, + IMAGE = 1 +} + +export type ClipboardItem = { + id: number; + type: ClipboardItemType; + preview: string; +} + +export { Clipboard }; + +/** Cliphist Manager and event listener + * This only supports wipe and store events from cliphist */ +@register({ GTypeName: "Clipboard" }) +class Clipboard extends GObject.Object { + private static instance: Clipboard; + + #dbFile: Gio.File; + #dbMonitor: Gio.FileMonitor; + #updateDone: boolean = false; + #history = new Array; + #changesTimeout: (AstalIO.Time|undefined); + #ignoreChanges: boolean = false; + + @signal(Object) + declare copied: () => ClipboardItem; + + @signal() + declare wiped: () => void; + + + @property() + public get history() { return this.#history; } + + + constructor() { + super(); + + this.#dbFile = this.getCliphistDatabase(); + + this.#dbMonitor = monitorFile(this.#dbFile.get_path()!, () => { + if(this.#ignoreChanges || this.#changesTimeout) + return; + + this.#changesTimeout = timeout(300, () => this.#changesTimeout = undefined); + + if(this.#updateDone) { + this.updateDatabase(); + return; + } + + this.init(); + }); + + if(this.#dbFile.query_exists(null)) { + this.init(); + return; + } + + console.log("Clipboard: cliphist database not found. Try copying something first!"); + } + + vfunc_dispose(): void { + this.#dbMonitor.cancel(); + this.#dbMonitor.unref(); + } + + private init() { + console.log("Clipboard: Starting to read cliphist history..."); + + this.updateDatabase().then(() => { + console.log("Clipboard: Done reading cliphist history!"); + }).catch((err) => + console.error(`Clipboard: An error occurred while reading cliphist history. Stderr: ${err}`) + ); + } + + public async copyAsync(content: string): Promise { + await execAsync(`wl-copy "${content}"`).catch((err: Gio.IOErrorEnum) => { + console.error(`Clipboard: Couldn't copy text using wl-copy. Stderr:\n\t${err.message + } | Stack:\n\t\t${err.stack}`); + }); + } + + public async selectItem(itemToSelect: number|ClipboardItem): Promise { + const item = await this.getItemContent(itemToSelect); + let res: boolean = true; + + if(item) + await this.copyAsync(item).catch(() => res = false); + + return res; + } + + /** Gets history item's content by its ID. + * @returns the clipboard item's content */ + public async getItemContent(item: number|ClipboardItem): Promise { + const id = (typeof item === "number") ? + item : item.id; + + const cmd = Gio.Subprocess.new([ "cliphist", "decode", id.toString() ], + Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE); + + const [ , stdout, stderr ] = cmd.communicate_utf8(null, null); + + if(stderr) { + console.error(`Clipboard: An error occurred while getting item content. Stderr:\n${stderr}`); + return; + } + + return stdout; + } + + /** Searches for the cliphist database file + * Will not work if cliphist config file is not on default path */ + private getCliphistDatabase(): Gio.File { + // Check if env variable is set + const path = GLib.getenv("CLIPHIST_DB_PATH"); + if(path != null) + return Gio.File.new_for_path(path); + + // Check config file + const confFile = Gio.File.new_for_path(`${GLib.get_user_config_dir()}/cliphist/config`); + if(confFile.query_exists(null)) { + const cliphistConf = readFile(confFile.get_path()!); + for(const line of cliphistConf.split('\n').map(l => l.trim())) { + if(line.startsWith('#')) + continue; + + const [ key, value ] = line.split('\s', 1); + if(key === "db-path") { + return Gio.File.new_for_path(value.trimStart()); + } + } + } + + // return default path if none of the above matches + return Gio.File.new_for_path(`${GLib.get_user_cache_dir()}/cliphist/db`); + } + + private getContentType(preview: string): ClipboardItemType { + return /^\[\[.*binary data.*x.*\]\]$/u.test(preview) ? + ClipboardItemType.IMAGE + : ClipboardItemType.TEXT; + } + + public async wipeHistory(noExec?: boolean): Promise { + if(noExec) { + this.#history = []; + this.emit("wiped"); + return; + } + + this.#ignoreChanges = true; + await execAsync("cliphist wipe").then(() => { + this.#history = []; + this.emit("wiped"); + }).catch((err: Gio.IOErrorEnum) => + console.error(`Clipboard: An error occurred on cliphist database wipe. Stderr: ${ + err.message ? `${err.message}\n` : ""}${err.stack}`) + ).finally(() => this.#ignoreChanges = false); + } + + public async updateDatabase(): Promise { + const proc = Gio.Subprocess.new([ "cliphist", "list" ], + Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE); + + proc.communicate_utf8_async(null, null, (_, asyncRes) => { + const [ success, stdout, stderr ] = proc.communicate_utf8_finish(asyncRes); + + if(!success || stderr) { + console.error("Clipboard: Couldn't communicate with cliphist! Is it installed?"); + return; + } + + if(!stdout.trim()) { + this.wipeHistory(true); + this.notify("history"); + return; + } + + const items = stdout.split('\n'); + + if(this.#updateDone) { + const [ id, preview ] = items[0].split('\t'); + const clipItem = { + id: Number.parseInt(id), + preview, + type: this.getContentType(preview) + } as ClipboardItem; + + this.#history.unshift(clipItem); + + this.emit("copied", clipItem); + this.notify("history"); + return; + } + + for(const item of items) { + if(!item) continue; + + const [ id, preview ] = item.split('\t'); + + const clipItem = { + id: Number.parseInt(id), + preview, + type: this.getContentType(preview) + } as ClipboardItem; + + this.#history.push(clipItem); + + this.emit("copied", clipItem); + this.notify("history"); + } + + this.#updateDone = true; + + }); + } + + public static getDefault(): Clipboard { + if(!this.instance) + this.instance = new Clipboard(); + + return this.instance; + } +} diff --git a/ags/scripts/nightlight.ts b/ags/scripts/nightlight.ts new file mode 100644 index 0000000..419ee0a --- /dev/null +++ b/ags/scripts/nightlight.ts @@ -0,0 +1,138 @@ +import { AstalIO, execAsync, GObject, interval, property, register } from "astal"; + +export { NightLight }; + +@register({ GTypeName: "NightLight" }) +class NightLight extends GObject.Object { + private static instance: NightLight; + + #watchInterval: (AstalIO.Time|null) = null; + #temperature: number = 4500; + #gamma: number = 100; + #identity: boolean = false; + + #prevTemperature: (number|null) = null; + #prevGamma: (number|null) = null; + + @property(Number) + public get temperature() { return this.#temperature; } + public set temperature(newValue: number) { this.setTemperature(newValue); } + + @property(Number) + public get gamma() { return this.#gamma; } + public set gamma(newValue: number) { this.setGamma(newValue); } + + @property(Number) + public get maxTemperature() { return 20000; } + + @property(Number) + public get maxGamma() { return 100; } + + @property(Boolean) + public get identity() { return this.#identity; } + public set identity(newValue: boolean) { + newValue ? this.applyIdentity() : this.filter(); + } + + constructor() { + super(); + + this.#watchInterval = interval(1000, () => { + execAsync("hyprctl hyprsunset temperature").then(t => { + if(t.trim() !== "" && t.trim().length <= 5) { + const val = Number.parseInt(t.trim()); + + if(this.#temperature !== val) { + this.#temperature = val; + this.notify("temperature"); + } + } + }).catch((r) => console.error(r)); + + execAsync("hyprctl hyprsunset gamma").then(g => { + if(g.trim() !== "" && g.trim().length <= 5) { + const val = Number.parseInt(g.trim()); + + if(this.#gamma !== val) { + this.#gamma = val; + this.notify("gamma"); + } + } + }).catch((r) => console.error(r)); + }); + + this.vfunc_dispose = () => this.#watchInterval && + this.#watchInterval.cancel(); + } + + public static getDefault(): NightLight { + if(!this.instance) + this.instance = new NightLight(); + + return this.instance; + } + + private async setTemperature(value: number): Promise { + if(value === this.temperature) return; + + if(value > this.maxTemperature || value < 1000) { + console.error(`Night Light(hyprsunset): provided temperatue ${value + } is out of bounds (min: 1000; max: ${this.maxTemperature})`); + return; + } + + execAsync(`hyprctl hyprsunset temperature ${value}`).then(() => { + this.#temperature = value; + this.notify("temperature"); + + this.#identity = false; + this.#prevTemperature = null; + this.#prevGamma = null; + }).catch((r) => console.error( + `Night Light(hyprsunset): Couldn't set temperature. Stderr: ${r}` + )); + } + + private async setGamma(value: number) { + if(value === this.gamma) return; + + if(value > this.maxGamma || value < 0) { + console.error(`Night Light(hyprsunset): provided gamma ${value + } is out of bounds (min: 0; max: ${this.maxTemperature})`); + return; + } + + execAsync(`hyprctl hyprsunset gamma ${value}`).then(() => { + this.#gamma = value; + this.notify("gamma"); + + this.#identity = false; + this.#prevTemperature = null; + this.#prevGamma = null; + }).catch((r) => console.error( + `Night Light(hyprsunset): Couldn't set gamma. Stderr: ${r}` + )); + } + + private applyIdentity(): void { + if(this.#identity) return; + + this.#prevGamma = this.#gamma; + this.#prevTemperature = this.#temperature; + + this.#identity = true; + this.temperature = 6000; + this.gamma = this.maxGamma; + } + + public filter(): void { + if(!this.#identity) return; + + this.#identity = false; + this.setTemperature(this.#prevTemperature ?? 1000); + this.setGamma(this.#prevGamma ?? 100); + + this.#prevTemperature = null; + this.#prevGamma = null; + } +} diff --git a/ags/scripts/notifications.ts b/ags/scripts/notifications.ts new file mode 100644 index 0000000..51e2649 --- /dev/null +++ b/ags/scripts/notifications.ts @@ -0,0 +1,298 @@ +import { AstalIO, execAsync, Gio, GObject, property, register, signal, timeout } from "astal"; +import AstalNotifd from "gi://AstalNotifd"; + +export let + NOTIFICATION_TIMEOUT_URGENT: number = 0, + NOTIFICATION_TIMEOUT_NORMAL: number = 4000, + NOTIFICATION_TIMEOUT_LOW: number = 2000; + +export interface HistoryNotification { + id: number; + appName: string; + body: string; + summary: string; + urgency: AstalNotifd.Urgency; + appIcon?: string; + time: number; + image?: string; +} + +@register({ GTypeName: "Notifications" }) +class Notifications extends GObject.Object { + private static instance: (Notifications|null) = null; + + #notifications: Array = []; + #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 = notification.urgency === AstalNotifd.Urgency.LOW ? + NOTIFICATION_TIMEOUT_LOW + : (notification.urgency === AstalNotifd.Urgency.CRITICAL ? + NOTIFICATION_TIMEOUT_URGENT + : NOTIFICATION_TIMEOUT_NORMAL); + + if(this.getNotifd().dontDisturb) { + this.addHistory(notification, () => notification.dismiss()); + return; + } + + this.addNotification(notification, () => { + if(notification.urgency !== AstalNotifd.Urgency.CRITICAL || + (notification.urgency === AstalNotifd.Urgency.CRITICAL && + NOTIFICATION_TIMEOUT_URGENT > 0)) { + + let notifTimer: (AstalIO.Time|undefined) = undefined; + let replacedConnectionId: number; + + const removeFun = () => { // Funny name haha lmao remove fun :skull: + notifTimer = undefined; + if(this.#notificationsOnHold.has(notification.id)) return; + + this.addHistory(notification, () => { + replacedConnectionId && this.disconnect(replacedConnectionId); + this.removeNotification(id); + }); + } + + notifTimer = timeout(notifTimeout, removeFun); + + replacedConnectionId = this.connect("notification-replaced", (_, id: number) => { + if(notification.id === id) { + notifTimer?.cancel(); + notifTimer = timeout(notifTimeout, removeFun); + } + }); + } + }); + }), + + AstalNotifd.get_default().connect("resolved", (notifd, id, _reason) => { + this.removeNotification(id); + this.addHistory(notifd.get_notification(id)); + }) + ); + + this.run_dispose = () => { + super.run_dispose(); + this.#connections.map((id: number) => + AstalNotifd.get_default().disconnect(id)); + }; + } + + public static getDefault(): Notifications { + if(!this.instance) + this.instance = new Notifications(); + + return this.instance; + } + + public async sendNotification(props: { + urgency?: AstalNotifd.Urgency; + appName?: string; + image?: string; + summary: string; + body?: string; + replaceId?: number; + actions?: Array<{ + id?: (string|number); + text: string; + onAction?: () => void + }> + }): Promise<{ + id?: (string|number); + text: string; + onAction?: () => void + }|null|void> { + + return await execAsync([ + "notify-send", + ...(props.urgency ? [ + "-u", this.getUrgencyString(props.urgency) + ] : []), ...(props.appName ? [ + "-a", props.appName + ] : []), ...(props.image ? [ + "-i", props.image + ] : []), ...(props.actions ? props.actions.map((action) => + [ "-A", action.text ] + ).flat(2) : []), ...(props.replaceId ? [ + "-r", props.replaceId.toString() + ] : []), props.summary, props.body ? props.body : "" + ]).then((stdout) => { + stdout = stdout.trim(); + if(!stdout) { + if(props.actions && props.actions.length > 0) + return null; + + return; + } + + if(props.actions && props.actions.length > 0) { + const action = props.actions[Number.parseInt(stdout)]; + action?.onAction?.(); + + return action ?? undefined; + } + }).catch((err: Gio.IOErrorEnum) => { + console.error(`Notifications: Couldn't send notification! Is the daemon running? Stderr:\n${ + err.message ? `${err.message}\n` : ""}Stack: ${err.stack}`); + }); + } + + public getUrgencyString(urgency: AstalNotifd.Notification|AstalNotifd.Urgency) { + switch((urgency instanceof AstalNotifd.Notification) ? + urgency.urgency : urgency) { + + case AstalNotifd.Urgency.LOW: + return "low"; + case AstalNotifd.Urgency.CRITICAL: + return "critical"; + } + + return "normal"; + } + + private addHistory(notif: AstalNotifd.Notification, onAdded?: (notif: AstalNotifd.Notification) => void): void { + if(!notif) return; + + this.#history.length === this.#historyLimit && + this.removeHistory(this.#history[this.#history.length - 1]); + + this.#history.map((notifb, i) => + notifb.id === notif.id && this.#history.splice(i, 1)); + + this.#history.unshift({ + id: notif.id, + appName: notif.appName, + body: notif.body, + summary: notif.summary, + urgency: notif.urgency, + appIcon: notif.appIcon, + time: notif.time, + image: notif.image ? notif.image : undefined + } as HistoryNotification); + + this.notify("history"); + this.emit("history-added", this.#history[0]); + onAdded && onAdded(notif); + } + + public clearHistory(): void { + const hist = this.#history.reverse(); + hist.map((notif) => { + this.#history = this.history.filter((n) => n.id !== notif.id); + this.emit("history-removed", notif.id); + this.notify("history"); + }); + } + + public removeHistory(notif: (HistoryNotification|number)): void { + const notifId = (typeof notif === "number") ? notif : notif.id; + this.#history = this.#history.filter((item: HistoryNotification) => + item.id !== notifId); + + this.notify("history"); + this.emit("history-removed", notifId); + } + + private addNotification(notif: AstalNotifd.Notification, onAdded?: (notif: AstalNotifd.Notification) => void): void { + const newArray = this.#notifications.reverse().filter((item) => item.id !== notif.id); + if(newArray !== this.notifications) { + this.emit("notification-replaced", notif.id); + } + + newArray.push(notif); + this.#notifications = newArray.reverse(); + this.notify("notifications"); + this.emit("notification-added", notif); + onAdded && onAdded(notif); + } + + public removeNotification(notif: (AstalNotifd.Notification|number)): void { + const notificationId = (notif instanceof AstalNotifd.Notification) ? notif.id : notif; + this.#notificationsOnHold.has(notificationId) && + this.#notificationsOnHold.delete(notificationId); + + this.#notifications = this.#notifications.filter((item: AstalNotifd.Notification) => + item.id !== notificationId); + + AstalNotifd.get_default().get_notification(notificationId)?.dismiss(); + this.notify("notifications"); + this.emit("notification-removed", notificationId); + } + + private getNotificationById(id: number): AstalNotifd.Notification|undefined { + return this.#notifications.filter(notif => notif.id === id)?.[0]; + } + + public holdNotification(notif: (AstalNotifd.Notification|number)): void { + notif = (typeof notif === "number") ? + this.getNotificationById(notif)! + : notif; + + if(!notif) return; + + this.#notificationsOnHold.add(notif.id); + } + + public toggleDoNotDisturb(): boolean { + if(AstalNotifd.get_default().dontDisturb) { + AstalNotifd.get_default().dontDisturb = false; + return false; + } + + AstalNotifd.get_default().dontDisturb = true; + return true; + } + + public getNotifd(): AstalNotifd.Notifd { return AstalNotifd.get_default(); } + + connect(signal: string, callback: (...args: any[]) => void): number { + return super.connect(signal, callback); + } + + disconnect(id: number) { + super.disconnect(id); + } +} + +export { Notifications }; diff --git a/ags/scripts/recording.ts b/ags/scripts/recording.ts new file mode 100644 index 0000000..46edc11 --- /dev/null +++ b/ags/scripts/recording.ts @@ -0,0 +1,154 @@ +import { execAsync, Gio, GLib, GObject } from "astal"; +import { property, register, signal } from "astal/gobject"; +import { Gdk } from "astal/gtk3"; +import { getDateTime } from "./time"; +import { makeDirectory } from "./utils"; +import { Notifications } from "./notifications"; + +export { Recording }; + +@register({ GTypeName: "Recording" }) +class Recording extends GObject.Object { + private static instance: Recording; + + @signal() + declare started: () => void; + @signal() + declare stopped: () => void; + + #recording: boolean = false; + #path: string = "~/Recordings"; + + /** Default extension: mp4(h264) */ + #extension: string = "mp4"; + #recordAudio: boolean = false; + #area: (Gdk.Rectangle|null) = null; + #startedAt: (GLib.DateTime|null) = null; + #process: (Gio.Subprocess|null) = null; + #output: (string|null) = null; + + @property() + /** GLib.DateTime of when recording started */ + public get startedAt() { return this.#startedAt; } + + @property(Boolean) + public get recording() { return this.#recording; } + private set recording(newValue: boolean) { + (!newValue && this.#recording) ? + this.stopRecording() + : this.startRecording(this.#area || undefined); + + this.#recording = newValue; + this.notify("recording"); + } + + @property(String) + public get path() { return this.#path; } + public set path(newPath: string) { + if(this.recording) return; + + this.#path = newPath; + this.notify("path"); + } + + @property(String) + public get extension() { return this.#extension; } + public set extension(newExt: string) { + if(this.recording) return; + + this.#extension = newExt; + this.notify("extension"); + } + + /** Recording output file name. %NULL if screen is not being recorded */ + public get output() { return this.#output; } + + /** Currently unsupported property */ + public get recordAudio() { return this.#recordAudio; } + public set recordAudio(newValue: boolean) { + if(this.recording) return; + + this.#recordAudio = newValue; + this.notify("record-audio"); + } + + constructor() { + super(); + const videosDir = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_VIDEOS); + if(videosDir) this.#path = `${videosDir}/Recordings`; + } + + public static getDefault() { + if(!this.instance) + this.instance = new Recording(); + + return this.instance; + } + + public startRecording(area?: Gdk.Rectangle) { + if(this.recording) + throw new Error("Screen Recording is already running!"); + + this.#output = `${getDateTime().get().format("%Y-%m-%d-%H%M%S")}_rec.${this.extension || "mp4"}`; + this.#recording = true; + this.notify("recording"); + this.emit("started"); + makeDirectory(this.path); + + const cancellable = Gio.Cancellable.new(); + cancellable.cancel = () => {}; + + const areaString = `${area?.x ?? 0},${area?.y ?? 0} ${area?.width ?? 1}x${area?.height ?? 1}`; + + this.#process = Gio.Subprocess.new([ + "wf-recorder", + ...(area ? [ `-g`, areaString ] : []), + "-f", + `${this.path}/${this.output!}` + ], Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE); + + this.#process.wait_async(cancellable, () => { + this.stopRecording(); + }); + + this.#startedAt = getDateTime().get(); + } + + public stopRecording() { + if(!this.#process) return; + + !this.#process.get_if_exited() && execAsync([ + "kill", "-s", "SIGTERM", this.#process.get_identifier()! + ]); + + const path = this.#path; + const output = this.#output; + + this.#process = null; + this.#recording = false; + this.#startedAt = null; + this.#output = null; + this.notify("recording"); + this.emit("stopped"); + + Notifications.getDefault().sendNotification({ + actions: [ + { + text: "View", + onAction: () => { + execAsync(["nautilus", "-s", output!, path]); + } + }, + { + text: "Open", + onAction: () => { + execAsync(["xdg-open", `${path}/${output}`]); + } + } + ], + appName: "Screen Recording", + summary: "Screen Recording saved", + body: `Saved as ${path}/${output}` + }); + } +}; diff --git a/ags/scripts/reload-handler.ts b/ags/scripts/reload-handler.ts new file mode 100644 index 0000000..d04f82a --- /dev/null +++ b/ags/scripts/reload-handler.ts @@ -0,0 +1,18 @@ +import { monitorFile, Process } from "astal"; +import { App } from "astal/gtk3"; + +const monitoringPaths = [ "./scripts", "./window", "./app.ts", "env.d.ts" ]; + +export function restartInstance(instanceName?: string): void { + Process.exec_async(`astal -q ${ instanceName || App.instanceName || "astal" }`, () => {}); + Process.exec_async(`ags run`, () => {}); +} + +export function monitorPaths(): void { + monitoringPaths.map((path: string) => { + monitorFile( + path, + () => restartInstance() + ) + }); +} diff --git a/ags/scripts/stylesheet.ts b/ags/scripts/stylesheet.ts new file mode 100644 index 0000000..09cf01f --- /dev/null +++ b/ags/scripts/stylesheet.ts @@ -0,0 +1,81 @@ +// handles stylesheet compiling and reloading + +import { monitorFile, AstalIO, timeout, GLib, Gio, execAsync, exec, readFile } from "astal"; +import { App } from "astal/gtk3"; + +export class Stylesheet { + private static instance: Stylesheet; + #watchDelay: (AstalIO.Time|undefined); + #outputPath = Gio.File.new_for_path(`${GLib.get_user_state_dir()}/ags/style`); + #styles = [ + "./style", + "./style.scss" + ]; + + public compileSass(): void { + console.log("Stylesheet: Compiling Sass"); + + exec(`bash -c "sass ${this.#styles.map(style => `-I ${style}`).join('\s') + } ${this.#outputPath.get_path()!}/style.css"`); + } + + public async reapply(cssFilePath: string): Promise { + 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 { + this.compileSass(); + this.reapply(this.#outputPath.get_path()! + "/style.css"); + } + + public static getDefault(): Stylesheet { + if(!this.instance) + this.instance = new Stylesheet(); + + return this.instance; + } + + constructor() { + (async () => !this.#outputPath.query_exists(null) && + this.#outputPath.make_directory_with_parents(null))(); + + this.#styles.map((path: string) => + monitorFile( + `${path}`, + (file: string) => { + if(this.#watchDelay || file.endsWith('~') || Number.isNaN(file)) + return; + + this.#watchDelay = timeout(250, () => this.#watchDelay = undefined); + console.log(`Stylesheet: \`${file.startsWith(GLib.get_home_dir()) ? + file.replace(GLib.get_home_dir(), '~') + : file}\` changed`) + + this.compileApply(); + } + ) + ) + + monitorFile( + `${GLib.get_user_cache_dir()}/wal/colors.scss`, + (file: string) => { + execAsync(`bash -c "cp -f ${file} ./style/_wal.scss"`).catch(r => { + console.error(`Stylesheet: Failed to copy pywal stylesheet to style dir. Stderr: ${r}`); + }); + } + ); + } +} diff --git a/ags/scripts/time.ts b/ags/scripts/time.ts new file mode 100644 index 0000000..cdb804a --- /dev/null +++ b/ags/scripts/time.ts @@ -0,0 +1,6 @@ +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 new file mode 100644 index 0000000..9d2efe0 --- /dev/null +++ b/ags/scripts/utils.ts @@ -0,0 +1,26 @@ +import { exec, execAsync, GLib } from "astal"; + + +export function getHyprlandInstanceSig(): (string|null) { + return GLib.getenv("HYPRLAND_INSTANCE_SIGNATURE"); +} + +export function getHyprlandVersion(): string { + return exec(`${GLib.getenv("HYPRLAND_CMD") || "Hyprland"} --version | head -n1`).split(" ")[1]; +} + +export function makeDirectory(dir: string): void { + execAsync([ "mkdir", "-p", dir ]); +} + +export function deleteFile(path: string): void { + execAsync([ "rm", "-r", path ]); +} + +export function isInstalled(commandName: string): boolean { + const output = exec(["bash", "-c", `command -v ${commandName}`]); + if(output) + return true; + + return false; +} diff --git a/ags/scripts/varmap.ts b/ags/scripts/varmap.ts new file mode 100644 index 0000000..16b59e8 --- /dev/null +++ b/ags/scripts/varmap.ts @@ -0,0 +1,89 @@ +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/volume.ts b/ags/scripts/volume.ts new file mode 100644 index 0000000..7d01894 --- /dev/null +++ b/ags/scripts/volume.ts @@ -0,0 +1,149 @@ +import { GObject, register } from "astal"; +import AstalWp from "gi://AstalWp"; + +export { WireplumberClass as Wireplumber }; + + +@register({ GTypeName: "Wireplumber" }) +class WireplumberClass extends GObject.Object { + private static astalWireplumber: (AstalWp.Wp|null) = AstalWp.get_default(); + private static inst: WireplumberClass; + + private defaultSink: AstalWp.Endpoint = WireplumberClass.astalWireplumber!.get_default_speaker()!; + private defaultSource: AstalWp.Endpoint = WireplumberClass.astalWireplumber!.get_default_microphone()!; + + private maxSinkVolume: number = 100; + private maxSourceVolume: number = 100; + + constructor() { + super(); + + if(!WireplumberClass.astalWireplumber) + throw new Error("Audio features will not work correctly! Please install wireplumber first", { + cause: "Wireplumber library not found" + }); + } + + public static getDefault(): WireplumberClass { + if(!WireplumberClass.inst) + WireplumberClass.inst = new WireplumberClass(); + + return WireplumberClass.inst; + } + + public static getWireplumber(): AstalWp.Wp { + return WireplumberClass.astalWireplumber!; + } + + public getMaxSinkVolume(): number { + return this.maxSinkVolume; + } + + public getMaxSourceVolume(): number { + return this.maxSourceVolume; + } + + public getDefaultSink(): AstalWp.Endpoint { + return this.defaultSink; + } + + public getDefaultSource(): AstalWp.Endpoint { + return this.defaultSource; + } + + public getSinkVolume(): number { + return Math.floor(this.getDefaultSink().get_volume() * 100); + } + + public getSourceVolume(): number { + return Math.floor(this.getDefaultSource().get_volume() * 100); + } + + public setSinkVolume(newSinkVolume: number): void { + this.defaultSink.set_volume( + (newSinkVolume > this.maxSinkVolume ? this.maxSinkVolume : newSinkVolume) / 100 + ); + } + + public setSourceVolume(newSourceVolume: number): void { + this.defaultSource.set_volume( + newSourceVolume > this.maxSourceVolume ? this.maxSourceVolume : newSourceVolume / 100 + ); + } + + public increaseEndpointVolume(endpoint: AstalWp.Endpoint, volumeIncrease: number): void { + volumeIncrease = Math.abs(volumeIncrease) / 100; + + if((endpoint.get_volume() + volumeIncrease) > this.maxSinkVolume) { + endpoint.set_volume(1.0); + return; + } + + endpoint.set_volume(endpoint.get_volume() + volumeIncrease); + } + + public increaseSinkVolume(volumeIncrease: number): void { + this.increaseEndpointVolume(this.getDefaultSink(), volumeIncrease); + } + + public increaseSourceVolume(volumeIncrease: number): void { + this.increaseEndpointVolume(this.getDefaultSource(), volumeIncrease); + } + + public decreaseEndpointVolume(endpoint: AstalWp.Endpoint, volumeDecrease: number): void { + volumeDecrease = Math.abs(volumeDecrease) / 100; + + if((endpoint.get_volume() - volumeDecrease) < 0) { + endpoint.set_volume(0); + return; + } + + endpoint.set_volume(endpoint.get_volume() - volumeDecrease); + } + + public decreaseSinkVolume(volumeDecrease: number): void { + this.decreaseEndpointVolume(this.getDefaultSink(), volumeDecrease); + } + + public decreaseSourceVolume(volumeDecrease: number): void { + this.decreaseEndpointVolume(this.getDefaultSource(), volumeDecrease); + } + + public muteSink(): void { + this.getDefaultSink().set_mute(true); + } + + public muteSource(): void { + this.getDefaultSource().set_mute(true); + } + + public unmuteSink(): void { + this.getDefaultSink().set_mute(false); + } + + public unmuteSource(): void { + this.getDefaultSource().set_mute(false); + } + + public isMutedSink(): boolean { + return this.getDefaultSink().get_mute(); + } + + public isMutedSource(): boolean { + return this.getDefaultSource().get_mute(); + } + + public toggleMuteSink(): void { + if(this.isMutedSink()) + return this.unmuteSink(); + + return this.muteSink(); + } + + public toggleMuteSource(): void { + if(this.isMutedSource()) + return this.unmuteSource(); + + return this.muteSource(); + } +} diff --git a/ags/scripts/wallpaper.ts b/ags/scripts/wallpaper.ts new file mode 100644 index 0000000..471ae4e --- /dev/null +++ b/ags/scripts/wallpaper.ts @@ -0,0 +1,177 @@ +import { AstalIO, execAsync, Gio, GLib, GObject, monitorFile, property, register, timeout } from "astal"; + +export { Wallpaper }; + +@register({ GTypeName: "Wallpaper" }) +class Wallpaper extends GObject.Object { + private static instance: Wallpaper; + #wallpaper: (string|undefined); + #splash: boolean = true; + #monitor: Gio.FileMonitor; + #hyprpaperFile: Gio.File; + #wallpapersPath: string; + #ignoreWatch: boolean = false; + + @property(Boolean) + public get splash() { return this.#splash; } + public set splash(showSplash: boolean) { + this.#splash = showSplash; + this.notify("splash"); + } + + @property(String) + public get wallpaper(): (string|undefined) { return this.#wallpaper; } + public set wallpaper(newValue: string) { this.setWallpaper(newValue); } + + public get wallpapersPath() { return this.#wallpapersPath; } + + constructor() { + super(); + + this.#wallpapersPath = GLib.getenv("WALLPAPERS") ?? `${GLib.get_home_dir()}/wallpapers`; + this.#hyprpaperFile = Gio.File.new_for_path(`${GLib.get_user_config_dir()}/hypr/hyprpaper.conf`); + this.getWallpaper().then((wall) => { + if(wall?.trim()) this.#wallpaper = wall.trim(); + }); + + let tmeout: (AstalIO.Time|undefined) = undefined; + + this.#monitor = monitorFile(this.#hyprpaperFile.get_path()!, (_, event) => { + if(event !== Gio.FileMonitorEvent.CHANGED && event !== Gio.FileMonitorEvent.CREATED && + event !== Gio.FileMonitorEvent.MOVED_IN) + return; + + if(tmeout) return; + else tmeout = timeout(1500, () => tmeout = undefined); + + if(this.#ignoreWatch) { + this.#ignoreWatch = false; + return; + } + + const [ loaded, text ] = this.#hyprpaperFile.load_contents(null); + if(!loaded) + console.error("Wallpaper: Couldn't read changes inside the hyprpaper file!"); + + const content = new TextDecoder().decode(text); + + if(content) { + let setWall: boolean = true; + + for(const line of content.split('\n')) { + if(line.trim().startsWith('#')) + continue; + + const lineSplit = line.split('='); + const key = lineSplit[0].trim(), + value = lineSplit.filter((_, i) => i !== 0).join('=').trim(); + + switch(key) { + case "splash": + this.splash = /(yes|true|on|enable|enabled)/.test(value) ? true : false; + break; + + case "wallpaper": + if(this.#wallpaper !== value && setWall) { + this.setWallpaper(value, false); + setWall = false; // wallpaper already set + } + + break; + } + } + } + }); + } + + vfunc_dispose(): void { + this.#monitor.cancel(); + } + + public static getDefault(): Wallpaper { + if(!this.instance) + this.instance = new Wallpaper(); + + return this.instance; + } + + private writeChanges(): void { + this.#ignoreWatch = true; // tell monitor to ignore file replace + this.#hyprpaperFile.replace_async(null, false, + Gio.FileCreateFlags.REPLACE_DESTINATION, + GLib.PRIORITY_DEFAULT, null, (_, result) => { + const res = this.#hyprpaperFile.replace_finish(result); + if(res) { + // success + this.#ignoreWatch = true; // tell monitor to ignore this change + res.write_bytes_async(new TextEncoder().encode(`# This file was automatically generated by color-shell + + preload = ${this.#wallpaper} + splash = ${this.#splash} + wallpaper = , ${this.#wallpaper}`.split('\n').map(str => str.trimStart()).join('\n')), + GLib.PRIORITY_DEFAULT, null, (_, asyncRes) => { + if(_!.write_finish(asyncRes)) res.flush(null); + res.close(null); + } + ); + + return; + } + + console.error(`Wallpaper: an error occurred when trying to replace the hyprpaper file`); + } + ); + } + + public async getWallpaper(): Promise { + return await execAsync("sh -c \"hyprctl hyprpaper listactive | tail -n 1\"").then(stdout => { + const loaded: (string|undefined) = stdout.split('=')[1]?.trim(); + + if(!loaded) + console.warn(`Wallpaper: Couldn't get wallpaper. There is(are) no loaded wallpaper(s)`); + + return loaded; + }).catch((err: Gio.IOErrorEnum) => { + console.error(`Wallpaper: Couldn't get wallpaper. Stderr: \n${err.message ? `${err.message} /` : ""} Stack: \n ${err.stack}`); + return undefined; + }); + } + + public reloadColors(): void { + execAsync(`wal -t --cols16 darken -i "${this.#wallpaper}"`).then(() => { + console.log("Wallpaper: reloaded shell colors"); + }).catch(r => { + console.error(`Wallpaper: Couldn't update shell colors. Stderr: ${r}`); + }); + } + + public setWallpaper(path: string|Gio.File, write: boolean = true): void { + execAsync("hyprctl hyprpaper unload all").then(() => + execAsync(`hyprctl hyprpaper preload ${path}`).then(() => + execAsync(`hyprctl hyprpaper wallpaper ${path}`).then(() => { + this.#wallpaper = (typeof path === "string") ? path : path.get_path()!; + this.reloadColors(); + write && this.writeChanges(); + }).catch(r => { + console.error(`Wallpaper: Couldn't set wallpaper. Stderr: ${r}`); + }) + ).catch(r => { + console.error(`Wallpaper: Couldn't preload image. Stderr: ${r}`); + }) + ).catch(r => { + console.error(`Wallpaper: Couldn't unload images from memory. Stderr: ${r}`); + }); + } + + public async pickWallpaper(): Promise { + return (await execAsync(`zenity --file-selection`).then(wall => { + if(!wall.trim()) return undefined; + + this.setWallpaper(wall); + return wall; + }).catch(r => { + console.error(`Wallpaper: Couldn't pick wallpaper, is \`zenity\` installed? Stderr: ${r}`); + return undefined; + })); + } +} diff --git a/ags/scripts/widget-utils.ts b/ags/scripts/widget-utils.ts new file mode 100644 index 0000000..4824b25 --- /dev/null +++ b/ags/scripts/widget-utils.ts @@ -0,0 +1,24 @@ +import { Gtk, Widget } from "astal/gtk3"; + +export function addSliderMarksFromMinMax(slider: Widget.Slider, amountOfMarks: number = 2, markup?: (string | null)) { + if(markup && !markup.includes("{}")) + markup = `${markup}{}` + + slider.add_mark(slider.min, Gtk.PositionType.BOTTOM, markup ? + markup.replaceAll("{}", `${slider.min}`) : null); + + const num = (amountOfMarks - 1); + for(let i = 1; i <= num; i++) { + const part = (slider.max / num) | 0; + + if(i > num) { + slider.add_mark(slider.max, Gtk.PositionType.BOTTOM, `${slider.max}K`); + break; + } + + slider.add_mark(part*i, Gtk.PositionType.BOTTOM, markup ? + markup.replaceAll("{}", `${part*i}`) : null); + } + + return slider; +} diff --git a/ags/style.scss b/ags/style.scss new file mode 100644 index 0000000..d649442 --- /dev/null +++ b/ags/style.scss @@ -0,0 +1,313 @@ +@use "sass:color"; + +@use "./style/wal"; +@use "./style/mixins"; +@use "./style/functions"; +@use "./style/colors"; + +@use "./style/bar"; +@use "./style/osd"; +@use "./style/control-center"; +@use "./style/center-window"; +@use "./style/float-notifications"; +@use "./style/logout-menu"; +@use "./style/apps-window"; +@use "./style/runner"; + + +* { + @include mixins.reset-props; + + /*&:focus { + box-shadow: inset 0 0 0 2px colors.$fg-primary; + }*/ +} + +entry { + background: colors.$bg-primary; + padding: 10px 9px; + border-radius: 12px; + + &:focus { + box-shadow: inset 0 0 0 2px colors.$bg-secondary; + } + + & image.left { + margin-right: 6px; + } +} + +.custom-dialog-container { + background: colors.$bg-translucent; + padding: 18px; + border-radius: 24px; + + & .title { + font-size: 21px; + font-weight: 700; + margin-bottom: 10px; + } + + & .text { + font-size: 16px; + font-weight: 400; + } + + & .options { + & button { + background: colors.$bg-primary; + border-radius: 12px; + padding: 9px 6px; + + & label { + font-size: 16px; + font-weight: 600; + } + + margin: { + left: 4px; + right: 4px; + }; + + &:hover, &:focus { + background: colors.$bg-secondary; + } + } + } + + &.entry-popup-box entry { + margin-bottom: 10px; + + &.password { + font-size: 14px; + font-family: "Adwaita Mono", "Cantarell Mono", "Noto Sans Mono", monospace; + font-weight: 400; + } + } +} + +.notification { + background: colors.$bg-translucent-secondary; + border-radius: 16px; + + & > .top { + padding: 8px; + padding-bottom: 0; + + & .app-icon { + margin-right: 6px; + } + + & .app-name { + font-size: 12px; + } + + + & label.time { + font-size: 11px; + font-weight: 500; + color: colors.$fg-disabled; + margin-right: 6px; + } + + & button.close { + padding: 2px; + border-radius: 8px; + + &:hover, &:focus { + background: colors.$bg-secondary; + } + } + + & icon.close { + font-size: 16px; + } + } + + & .content { + padding: 6px; + padding-top: 0; + + & .image { + $size: 78px; + min-width: $size; + min-height: $size; + background-size: cover; + background-position: center; + margin: 6px; + border-radius: 8px; + } + + & .summary { + font-size: 17.3px; + font-weight: 700; + margin-bottom: 4px; + } + + & .body { + font-size: 14.5px; + font-weight: 400; + } + } + + & .actions { + padding: 6px; + + & button.action { + @include mixins.hover-shadow; + + border-radius: 4px; + background: colors.$bg-secondary; + padding: 6px; + + & label { + font-size: 14px; + font-weight: 600; + } + + &:first-child { + border-top-left-radius: 12px; + border-bottom-left-radius: 12px; + } + &:last-child { + border-top-right-radius: 12px; + border-bottom-right-radius: 12px; + } + } + } +} + +tooltip { + padding: 16px; + + & > box { + padding: 7px 8px; + border-radius: 10px; + background: rgba(colors.$bg-primary, .98); + font-size: 13.1px; + font-weight: 500; + color: colors.$fg-primary; + box-shadow: 0 1px 4px 1px rgba(colors.$bg-primary, .6); + } +} + +menu { + padding: 4px; + background: wal.$background; + border-radius: 14px; + + & separator { + margin: 0 4px; + color: wal.$background; + } + + & menuitem { + padding: 8px 16px; + border-radius: 10px; + font-size: 12px; + font-weight: 600; + + &:hover, &:focus { + background: wal.$color1; + } + } +} + +.button-row { + & > button { + background: colors.$bg-secondary; + margin: 0 1px; + padding: 4px 6px; + border-radius: 2px; + + &:hover { + background: colors.$bg-tertiary; + } + + &:first-child { + border-top-left-radius: 10px; + border-bottom-left-radius: 10px; + margin-left: 0; + } + + &:last-child { + border-top-right-radius: 10px; + border-bottom-right-radius: 10px; + margin-right: 0; + } + } +} + +selection { + background: colors.$bg-tertiary; +} + +label.nf, +button.nf label { + font-size: 12px; + font-family: "Symbols Nerd Font Mono", "Noto Sans Nerd Font Mono", + "0xProto Nerd Font Mono", "Fira Code Nerd Font Mono", + "Symbols Nerd Font", "Noto Sans Nerd Font", "Fira Code Nerd Font", + "Font Awesome"; +} + +trough { + background: functions.toRGB(color.adjust($color: wal.$color1, $lightness: -20%)); + border-radius: 8px; + margin: 2px 0; +} + +trough highlight { + background: wal.$color1; + min-height: .9em; + border-top-left-radius: inherit; + border-bottom-left-radius: inherit; +} + +trough slider { + border-radius: 50%; + margin: -2px 0; + background: wal.$foreground; + margin-left: -1px; + min-width: 1.2em; + min-height: 1.2em; +} + +scrollbar trough { + @include mixins.reset-props; + + background: colors.$bg-translucent; + border-radius: 8px; + + & slider { + @include mixins.reset-props; + + min-width: .45em; + background: colors.$bg-tertiary; + border-radius: 12px; + + &:hover, &:active, &:focus { + margin: 2px; + } + } + + &:hover, &:active, &:focus { + padding: 2px; + } +} + +scale { + & marks mark { + & indicator { + color: colors.$fg-disabled; + min-width: 1px; + min-height: 6px; + } + + & label { + font-size: 11px; + font-weight: 400; + color: colors.$fg-disabled; + } + } +} diff --git a/ags/style/_apps-window.scss b/ags/style/_apps-window.scss new file mode 100644 index 0000000..88b6a59 --- /dev/null +++ b/ags/style/_apps-window.scss @@ -0,0 +1,44 @@ +@use "sass:color"; +@use "./mixins"; +@use "./colors"; +@use "./functions"; + +.apps-window-container { + padding: 24px; + background: colors.$bg-translucent; + border-top-left-radius: 24px; + border-top-right-radius: 24px; + + & > entry { + background: rgba(colors.$bg-primary, .4); + margin-bottom: 32px; + min-width: 400px; + } + + & flowbox { + padding: 16px 24px; + + & > flowboxchild { + & > button { + padding: 8px; + border-radius: 24px; + + & icon { + font-size: 64px; + } + + & label { + margin-top: 6px; + text-shadow: 1px 1px 1px rgba(colors.$bg-primary, .2); + font-weight: 500; + } + } + + &:focus > button, + &:selected > button, + & > button:hover { + background-color: rgba($color: colors.$bg-secondary, $alpha: .5); + } + } + } +} diff --git a/ags/style/_bar.scss b/ags/style/_bar.scss new file mode 100644 index 0000000..914cad2 --- /dev/null +++ b/ags/style/_bar.scss @@ -0,0 +1,270 @@ +@use "sass:color"; +@use "./mixins"; +@use "./colors"; +@use "./wal"; +@use "./functions"; + +.bar-container { + @include mixins.reset-props; + + padding: 6px; + padding-bottom: 0px; + + label { + @include mixins.reset-props; + + font-size: 12px; + font-weight: 600; + } + + // Style widget groups + & > .bar-centerbox > * { + $radius: 18px; + $color-hover: colors.$bg-primary; + $padding: 4px; + + background: rgba(colors.$bg-translucent, .6); + border-radius: $radius; + padding: 0 $padding; + + & > eventbox { + &:hover { + & > box:not(.workspaces):not(.special-workspaces) { + background: $color-hover; + } + } + & > box { + border-radius: calc($radius - $padding); + margin: $padding 0; + } + + & > box:not(.workspaces):not(.special-workspaces) { + padding: 0 8px; + } + } + + & > button, + & > box > button { + border-radius: calc($radius - $padding); + margin: $padding 0; + padding: 0 9px; + + &:hover { + background: $color-hover; + } + } + } + + .workspaces, .special-workspaces { + @include mixins.reset-props; + padding: 0 4px; + + & > eventbox { + & > box { + margin: 3px 0; + border-radius: 16px; + transition: 80ms linear; + min-width: 15px; + padding: 0 6px; + background: colors.$bg-tertiary; + + & label.id { + font-weight: 600; + margin-right: 4px; + opacity: 0; + } + } + + &.focus > box { + background: colors.$fg-primary; + min-width: 32px; + + & label.id { + color: colors.$fg-light; + margin-right: 0; + } + } + + & icon { + font-size: 16px; + } + + &.show label.id { + opacity: 1; + } + + &:hover > box { + box-shadow: inset 0 0 0 100px rgba($color: colors.$fg-primary, $alpha: .2); + } + } + + &.special-workspaces { + & > eventbox { + & box { + background: wal.$color4; + } + + &:hover > box { + background: functions.toRGB(color.adjust(wal.$color4, $lightness: -6%)); + } + } + } + } + + .focused-client { + padding: 0 6px; + + & > .icon { + margin-right: 6px; + } + + & > .text-content { + & > .class { + font-size: 9px; + font-family: monospace; + font-weight: 600; + color: colors.$fg-disabled; + margin-top: 0px; + } + + & > .title { + font-size: 12px; + font-weight: 500; + margin-top: -2px; + } + } + } + + .clock.open > button { + background-color: colors.$bg-primary; + } + + .media-eventbox { + & > .media { + background: colors.$bg-primary; + padding: 0 8px; + } + + &:hover > .media { + box-shadow: inset 0 0 0 300px rgba(colors.$fg-primary, .2); + } + + & .icon { + font-size: 14px; + } + + & .media-controls { + transition: none; + margin-left: 6px; + border-top-right-radius: 12px; + border-bottom-right-radius: 12px; + + & > button { + margin: 4px 1px; + + & label { + font-size: 8px; + } + } + } + + + &.reveal { + & .media > box { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + + } + } + + .tray { + padding: 0 6px; + + & .item { + all: unset; + + &:hover { + background: none; + } + + margin: 0 6px; + padding: 0; + + &:first-child { + margin-left: 0; + } + + &:last-child { + margin-right: 0; + } + } + } + + .status { + @include mixins.reset-props; + + &:hover > box, + &.open > box { + background: colors.$bg-primary; + } + + & > box { + padding: 0 8px; + + & > * > * { + margin: 0 2px; + } + + & trough { + min-width: 65px; + min-height: 10px; + margin-right: 4px; + } + + & slider { + min-width: 10px; + min-height: 10px; + } + + & highlight { + min-height: 10px; + } + + & .nf { + margin: { + right: 3px; + left: 2px; + }; + + font-size: 12px; + } + + & .status-icons { + padding: 0 4px; + + & > * { + margin: 0 4px; + } + } + } + } + + .apps { + & > box { + min-width: 24px; + + & > icon { + transition: 120ms linear; + font-size: 14px; + } + } + &.open > box { + background: colors.$bg-primary; + } + + &:hover icon { + -gtk-icon-transform: scale(1.14); + } + } +} diff --git a/ags/style/_center-window.scss b/ags/style/_center-window.scss new file mode 100644 index 0000000..c27d1c0 --- /dev/null +++ b/ags/style/_center-window.scss @@ -0,0 +1,128 @@ +@use "sass:color"; +@use "./wal"; +@use "./colors"; + +.center-window-container { + background: colors.$bg-translucent; + border-radius: 18px; + padding: 12px; + + & .big-media { + padding: 6px; + + & > box > .image { + background-size: cover; + background-position: center center; + border-radius: 10px; + } + + & > .info { + padding: { + top: 4px; + bottom: 6px; + }; + + & .title { + font-size: 16px; + font-weight: 700; + } + + & .artist { + font-size: 14px; + font-weight: 600; + color: colors.$fg-disabled; + } + } + + & slider { + background: transparent; + min-height: .6em; + } + + & trough { + border-radius: 4px; + min-height: .6em; + } + + & trough highlight { + border-radius: 4px; + min-height: .6em; + } + + & .bottom { + & .controls { + margin-top: 5px; + & button { + padding: 7px; + & label { + font-size: 10px; + } + } + } + + & .elapsed, + & .length { + font-size: 12px; + color: colors.$fg-disabled; + } + } + } + + & .left .datetime { + padding-bottom: 10px; + + & .time { + font-size: 28px; + font-weight: 800; + } + + & .date { + font-size: 14px; + font-weight: 500; + color: colors.$fg-disabled; + } + } + + & .calendar-box { + & calendar { + $border-radius: 10px; + font-weight: 600; + padding-bottom: 2px; + + &.view { + background: colors.$bg-primary; + border-radius: $border-radius; + } + + &.header { + background: colors.$bg-secondary; + border-top-left-radius: $border-radius; + border-top-right-radius: $border-radius; + padding: 4px; + } + + &.button { + transition: 80ms linear; + border-radius: 6px; + + &:hover { + background-color: colors.$bg-tertiary; + } + } + + &:selected { + background: colors.$bg-secondary; + border-radius: 6px; + } + + &.highlight { + background: transparent; + box-shadow: 0 2px 0 -1px rgba(colors.$bg-secondary, .5); + } + + &:focus { + outline: 1px white; + } + } + } +} diff --git a/ags/style/_colors.scss b/ags/style/_colors.scss new file mode 100644 index 0000000..47febd8 --- /dev/null +++ b/ags/style/_colors.scss @@ -0,0 +1,13 @@ +@use "sass:color"; +@use "./wal"; +@use "./functions"; + +$bg-primary: functions.toRGB(color.adjust($color: wal.$color1, $lightness: -34%)); +$bg-secondary: functions.toRGB(color.adjust($color: wal.$color1, $lightness: -16%)); +$bg-tertiary: functions.toRGB(color.adjust($color: $bg-secondary, $lightness: 10%)); +$bg-light: wal.$foreground; +$bg-translucent: functions.toRGB(color.change($color: $bg-primary, $alpha: 75%)); +$bg-translucent-secondary: functions.toRGB(color.change($color: $bg-translucent, $alpha: 78%)); +$fg-primary: wal.$foreground; +$fg-light: $bg-primary; +$fg-disabled: functions.toRGB(color.adjust($color: wal.$foreground, $lightness: -11%)); diff --git a/ags/style/_control-center.scss b/ags/style/_control-center.scss new file mode 100644 index 0000000..1680818 --- /dev/null +++ b/ags/style/_control-center.scss @@ -0,0 +1,264 @@ +@use "sass:color"; +@use "./wal"; +@use "./colors"; +@use "./functions" as funs; +@use "./mixins"; + +.control-center-container { + @include mixins.reset-props; + + background: colors.$bg-translucent; + border-radius: 28px; + padding: 20px; + + & > * { + margin: 9px 0; + + &:first-child { + margin-top: 0; + } + &:last-child { + margin-bottom: 0; + } + } + + & .quickactions { + & .hostname { + font-size: 15px; + font-weight: 600; + } + + & .uptime { + font-size: 10.1px; + font-family: "Symbols Nerd Font Mono"; + color: colors.$fg-disabled; + } + + & .button-row { + & button { + padding: 7px; + margin: { + top: 2px; + bottom: 2px; + }; + } + } + } + + & .sliders { + padding: 0; + & > box { + margin: 8px 0; + + &:first-child { + margin-top: 0; + } + + &:last-child { + margin-bottom: 0; + } + } + + label.nf, + button.nf label { + margin-right: 8px; + font-size: 15px; + } + + button.more { + @include mixins.hover-shadow; + + padding: 4px; + border-radius: 16px; + margin-left: 6px; + } + + & .page .content { + & > eventbox > box { + margin: 6px 0; + } + + & label.name { + font-size: 14px; + font-weight: 500; + } + + & trough { + margin-right: 10px; + } + + & label.sub-header { + margin-top: 6px; + } + + & button.default { + background: colors.$bg-tertiary; + } + } + } + + & .page { + transition: 120ms linear; + background: colors.$bg-secondary; + padding: 14px; + border-radius: 24px; + + & .header { + margin-bottom: 12px; + + & .top > .title { + font-size: 20px; + font-weight: 600; + } + + & > .description { + font-size: 12px; + font-weight: 500; + color: colors.$fg-disabled; + } + } + + & .sub-header { + font-size: 14px; + font-weight: 500; + margin-bottom: 6px; + } + + & button { + @include mixins.hover-shadow; + + padding: 6px; + border-radius: 12px; + + & label { + font-size: 14px; + } + } + + & .extra-buttons { + margin-left: 2px; + & > button { + border-radius: 12px; + } + } + } +} + +box.history { + margin-top: 10px; + background: colors.$bg-translucent; + border-radius: 24px; + padding: 20px; + transition: 120ms linear; + + &.hide { + opacity: 0; + } + + & .notifications { + & .notification { + background: colors.$bg-primary; + } + } + + & > .button-row { + margin-top: 12px; + & button { + padding: 6px; + + & label.nf { + font-size: 16px; + } + + & label:not(.nf) { + font-size: 12px; + font-weight: 600; + } + } + } +} + +.tiles-container { + @include mixins.reset-props; + + & > flowbox { + & > flowboxchild .tile { + $radius: 16px; + + &:not(.toggled) > .toggle-button, + &:not(.toggled) > button.more { + @include mixins.hover-shadow; + background: colors.$bg-primary; + } + + &.toggled .toggle-button:hover, + &.toggled button.more:hover { + background: colors.$bg-tertiary; + } + + &.toggled > .toggle-button, + &.toggled > button.more { + background: colors.$bg-secondary; + } + + &.has-more > .toggle-button { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + + & > .toggle-button { + border-radius: $radius; + + & .content { + padding: 8px; + + & > .icon { + margin-right: 6px; + } + + & > .text { + & > .title { + font-weight: 600; + font-size: 15.1px; + } + + & > .description { + font-size: 12px; + color: colors.$fg-disabled; + font-weight: 400; + } + } + } + } + + & > button.more { + border-top-right-radius: $radius; + border-bottom-right-radius: $radius; + + & label { + font-size: 16px; + } + } + } + } +} + +.tile-pages { + & > .page { + margin-top: 10px; + + &.bluetooth { + button.connected { + background: colors.$bg-tertiary; + } + + &.paired { + margin-bottom: 20px; + } + } + } + + &.revealed { + padding-top: 12px; + } +} diff --git a/ags/style/_float-notifications.scss b/ags/style/_float-notifications.scss new file mode 100644 index 0000000..d743704 --- /dev/null +++ b/ags/style/_float-notifications.scss @@ -0,0 +1,14 @@ +@use "./colors"; +@use "./mixins"; + +.floating-notifications-container { + padding: { + right: 6px; + top: 6px; + }; + + & .notification { + margin: 6px; + box-shadow: 0 0 4px .5px colors.$bg-primary; + } +} diff --git a/ags/style/_functions.scss b/ags/style/_functions.scss new file mode 100644 index 0000000..18bc08f --- /dev/null +++ b/ags/style/_functions.scss @@ -0,0 +1,14 @@ +@use "sass:color"; + + +/** + * GTK3 only supports sRGB color space, unfortunatly + */ +@function toRGB($color) { + @return rgba( + color.channel($color, "red"), + color.channel($color, "green"), + color.channel($color, "blue"), + color.alpha($color) + ); +} diff --git a/ags/style/_logout-menu.scss b/ags/style/_logout-menu.scss new file mode 100644 index 0000000..92c5988 --- /dev/null +++ b/ags/style/_logout-menu.scss @@ -0,0 +1,41 @@ +@use "./colors"; + +.logout-menu { + .top { + .time { + font-size: 128px; + font-weight: 900; + text-shadow: 1px 1px 2px colors.$bg-translucent; + } + .date { + font-size: 24px; + font-weight: 500; + text-shadow: 1px 1px 2px colors.$bg-translucent; + } + } + .button-row { + margin: 0 150px; + + & > button { + & label { + font-size: 96px; + } + + margin: { + left: 4px; + right: 4px; + } + border-radius: 6px; + + &:first-child { + border-top-left-radius: 28px; + border-bottom-left-radius: 28px; + } + + &:last-child { + border-top-right-radius: 28px; + border-bottom-right-radius: 28px; + } + } + } +} diff --git a/ags/style/_mixins.scss b/ags/style/_mixins.scss new file mode 100644 index 0000000..0f4a2c3 --- /dev/null +++ b/ags/style/_mixins.scss @@ -0,0 +1,33 @@ +@use "sass:color"; +@use "./wal"; +@use "./colors"; +@use "./functions" as funs; + +@mixin reset-props { + all: unset; + transition: 120ms linear; + font-family: "Adwaita Sans", "Cantarell", "Noto Sans", + "Noto Sans CJK JP", "Noto Sans CJK KR", + "Noto Sans CJK HK", "Noto Sans CJK SC", + "Noto Sans CJK TC", sans-serif, + "Symbols Nerd Font Mono"; + color: colors.$fg-primary; +} + +@mixin hover-shadow { + &:hover { + box-shadow: inset 0 0 0 500px rgba(colors.$fg-primary, .1); + } +} + +@mixin hover-shadow2 { + &:hover { + box-shadow: inset 0 0 0 500px rgba(colors.$fg-primary, .2); + } +} + +@mixin hover-shadow3 { + &:hover { + box-shadow: inset 0 0 0 500px rgba(colors.$fg-primary, .3); + } +} diff --git a/ags/style/_osd.scss b/ags/style/_osd.scss new file mode 100644 index 0000000..9d72c49 --- /dev/null +++ b/ags/style/_osd.scss @@ -0,0 +1,46 @@ +@use "sass:color"; +@use "./wal"; +@use "./functions" as funs; + +.osd { + background: funs.toRGB(color.change($color: wal.$background, $alpha: 65%)); + padding: 14px 16px; + border-radius: 20px; + + .icon { + margin-right: 10px; + font-size: 24px; + } + + .volume { + margin-top: -6px; + + .device { + margin-bottom: 5px; + font-size: 14px; + font-weight: 600; + } + + levelbar { + trough block { + border-radius: 2px; + background: funs.toRGB(color.adjust($color: wal.$color1, $lightness: -36%)); + + &.empty { + border-radius: 2px; + } + + &.filled { + padding: 3px 0; + background: wal.$color1; + } + } + } + + .value { + font-size: 11px; + font-weight: 400; + padding: 0 4px; + } + } +} diff --git a/ags/style/_runner.scss b/ags/style/_runner.scss new file mode 100644 index 0000000..4615b78 --- /dev/null +++ b/ags/style/_runner.scss @@ -0,0 +1,76 @@ +@use "./colors"; + +.runner.main { + background: colors.$bg-translucent; + padding: 10px; + border-radius: 22px; + + & entry { + background: colors.$bg-primary; + padding: 10px 9px; + border-radius: 12px; + margin-bottom: 1px; + min-height: 1.4em; + + &:focus { + box-shadow: inset 0 0 0 2px colors.$bg-secondary; + } + + & image.left { + margin-right: 6px; + } + } + + & list { + all: unset; + + & > *:selected > .result, + & > *:active > .result, + & > *:hover > .result { + background: colors.$bg-secondary; + } + + & > *:first-child { + margin-top: 12px; + } + + &:last-child { + margin-bottom: 0; + } + } + + & list .result { + padding: 10px; + background: colors.$bg-primary; + margin: 2px 0; + border-radius: 14px; + + & icon { + font-size: 28px; + margin-right: 6px; + } + + & .title { + font-weight: 500; + font-size: 16px; + } + + & .description { + font-size: 12px; + color: colors.$fg-disabled; + } + } + + & .not-found { + padding-top: 24px; + + & icon { + font-size: 64px; + margin-bottom: .4em; + } + + & label { + font-size: 16px; + } + } +} diff --git a/ags/style/_wal.scss b/ags/style/_wal.scss new file mode 100644 index 0000000..a31897a --- /dev/null +++ b/ags/style/_wal.scss @@ -0,0 +1,26 @@ +// SCSS Variables +// Generated by 'wal' +$wallpaper: "/home/joaov/wallpapers/Frieren Underwater.jpg"; + +// Special +$background: #0a1d30; +$foreground: #c1c6cb; +$cursor: #c1c6cb; + +// Colors +$color0: #0a1d30; +$color1: #0e7ea2; +$color2: #3a829e; +$color3: #1f96b0; +$color4: #717880; +$color5: #9c8a87; +$color6: #758899; +$color7: #93989d; +$color8: #606b76; +$color9: #13A9D8; +$color10: #4EAED3; +$color11: #2AC9EB; +$color12: #97A0AB; +$color13: #D1B8B5; +$color14: #9CB6CD; +$color15: #c1c6cb; diff --git a/ags/tsconfig.json b/ags/tsconfig.json new file mode 100644 index 0000000..d79ca1e --- /dev/null +++ b/ags/tsconfig.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "experimentalDecorators": true, + "strict": true, + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "Bundler", + "checkJs": true, + "allowJs": false, + "jsx": "react-jsx", + "jsxImportSource": "astal/gtk3" + } +} diff --git a/ags/widget/AskPopup.ts b/ags/widget/AskPopup.ts new file mode 100644 index 0000000..84c9f21 --- /dev/null +++ b/ags/widget/AskPopup.ts @@ -0,0 +1,46 @@ +import { Binding } from "astal"; +import { Widget } from "astal/gtk3"; +import { tr } from "../i18n/intl"; +import { CustomDialog, CustomDialogProps } from "./CustomDialog"; + + +export type AskPopupProps = { + title?: string | Binding; + 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 new file mode 100644 index 0000000..e056c15 --- /dev/null +++ b/ags/widget/BackgroundWindow.ts @@ -0,0 +1,59 @@ +import { Binding } from "astal"; +import { Astal, Gdk, Widget } from "astal/gtk3"; + + +const { TOP, LEFT, RIGHT, BOTTOM } = Astal.WindowAnchor; + +export type BackgroundWindowProps = { + /** GtkWindow Layer */ + layer?: Astal.Layer | Binding; + /** 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; +}; + +/** Creates a fullscreen GtkWindow that is used for making + * the user focus on the content after this window(e.g.: AskPopup, + * Authentication Window(futurely) or any PopupWindow) + * + * @param props Properties for background-window + * + * @returns The generated background window + */ +export function BackgroundWindow(props: BackgroundWindowProps) { + return new Widget.Window({ + namespace: "background-window", + css: props.css ?? "background: rgba(0, 0, 0, .2);", + monitor: props.monitor, + layer: props.layer ?? Astal.Layer.OVERLAY, + anchor: TOP | LEFT | BOTTOM | RIGHT, + keymode: props.keymode ?? Astal.Keymode.NONE, + exclusivity: Astal.Exclusivity.IGNORE, + onKeyPressEvent: (self, event: Gdk.Event) => { + event.get_keyval()[1] === Gdk.KEY_Escape && + props.onAction?.(self); + }, + onButtonPressEvent: (self, event: Gdk.Event) => { + if(event.get_button()[1]) { + props.onAction?.(self); + return; + } + + if(event.get_button()[1] === Gdk.BUTTON_PRIMARY) { + props.onClickPrimary?.(self); + return; + } + + if(event.get_button()[1] === Gdk.BUTTON_SECONDARY) + props.onClickSecondary?.(self); + } + } as Widget.WindowProps); +} diff --git a/ags/widget/CustomDialog.ts b/ags/widget/CustomDialog.ts new file mode 100644 index 0000000..9669dc3 --- /dev/null +++ b/ags/widget/CustomDialog.ts @@ -0,0 +1,107 @@ +import { Binding } from "astal"; +import { Astal, Gtk, Widget } from "astal/gtk3"; +import { Windows } from "../windows"; +import { PopupWindow, PopupWindowProps } from "./PopupWindow"; +import { Separator } from "./Separator"; +import { tr } from "../i18n/intl"; + +export type CustomDialogProps = { + namespace?: string | Binding; + 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, + 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 new file mode 100644 index 0000000..266a428 --- /dev/null +++ b/ags/widget/EntryPopup.ts @@ -0,0 +1,69 @@ +import { Binding } from "astal"; +import { Widget } from "astal/gtk3"; +import { tr } from "../i18n/intl"; +import { CustomDialog, CustomDialogProps } from "./CustomDialog"; + +export type EntryPopupProps = { + title: string | Binding; + 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 new file mode 100644 index 0000000..e9cefc2 --- /dev/null +++ b/ags/widget/Notification.ts @@ -0,0 +1,172 @@ +import { Astal, Gtk, Widget } from "astal/gtk3"; +import AstalNotifd from "gi://AstalNotifd"; +import { Separator } from "./Separator"; +import { HistoryNotification, Notifications } from "../scripts/notifications"; +import { GLib } from "astal"; +import { getAppIcon } from "../scripts/apps"; + + +function getNotificationImage(notif: AstalNotifd.Notification|HistoryNotification): (string|undefined) { + const img = notif.image ?? notif.appIcon; + if(!img) return undefined; + + if(!img.includes('/')) return undefined; + + if(img.startsWith('/')) + return `file://${img}`; + + if(img.startsWith('~') || img.startsWith("file://~")) + return `file://${GLib.get_home_dir()}/${img.replace(/^(file\:\/\/|\~|file\:\/\/~)/g, "")}`; + + return img; +} + +export function NotificationWidget(notification: AstalNotifd.Notification|number|HistoryNotification, + onClose?: (notif: AstalNotifd.Notification|HistoryNotification) => void, + showTime?: boolean /* It's showTime :speaking_head: :boom: :bangbang: */, + holdOnHover?: boolean): Gtk.Widget { + + notification = (typeof notification === "number") ? + AstalNotifd.get_default().get_notification(notification) + : notification; + + const body: string = notification.body.split(' ').map(strPart => { + if(/^\<(.*)\>/.test(strPart) || /<\/(.*)\>$/.test(strPart)) + return strPart; + + return strPart.length >= 25 ? `${strPart.substring(0, 22)}...` + : strPart + }).join(' '); + + return new Widget.EventBox({ + onClick: () => { + if(notification instanceof AstalNotifd.Notification) { + const viewAction = notification.actions.filter(action => + action.label.toLowerCase() === "view")?.[0]; + + viewAction && notification.invoke(viewAction.id); + } + + onClose?.(notification); + }, + onHover: () => holdOnHover && Notifications.getDefault().holdNotification(notification.id), + onHoverLost: () => holdOnHover && onClose?.(notification), + hexpand: true, + vexpand: false, + child: new Widget.Box({ + className: `notification ${ (notification instanceof AstalNotifd.Notification) ? + Notifications.getDefault().getUrgencyString(notification.urgency) : "" }`, + homogeneous: false, + expand: true, + orientation: Gtk.Orientation.VERTICAL, + spacing: 5, + children: [ + new Widget.Box({ + className: "top", + orientation: Gtk.Orientation.HORIZONTAL, + hexpand: true, + vexpand: false, + children: [ + new Widget.Icon({ + className: "icon app-icon", + icon: notification.appIcon && Astal.Icon.lookup_icon(notification.appIcon) ? + notification.appIcon + : getAppIcon(notification.appName), + setup: (self) => self.set_visible(Boolean(self.get_icon())), + halign: Gtk.Align.START, + css: "font-size: 16px;" + }), + new Widget.Label({ + className: "app-name", + halign: Gtk.Align.START, + hexpand: true, + label: notification.appName || "Unknown Application" + } as Widget.LabelProps), + new Widget.Box({ + halign: Gtk.Align.END, + children: [ + new Widget.Label({ + xalign: 1, + visible: !showTime ? false : true, + className: "time", + label: GLib.DateTime.new_from_unix_local(notification.time).format("%H:%M"), + } as Widget.LabelProps), + new Widget.Button({ + className: "close nf", + onClick: () => onClose && onClose(notification), + image: new Widget.Icon({ + className: "close icon", + icon: "window-close-symbolic" + } as Widget.IconProps) + } as Widget.ButtonProps) + ] + } as Widget.BoxProps) + ] + } as Widget.BoxProps), + Separator({ + orientation: Gtk.Orientation.VERTICAL, + alpha: 10 + }), + new Widget.Box({ + className: "content", + orientation: Gtk.Orientation.HORIZONTAL, + children: [ + new Widget.Box({ + className: "image", + setup: (box) => { + const img = getNotificationImage(notification); + + box.set_visible(Boolean(img)); + img && box.set_css(`background-image: image(url("${img}"))`); + } + } as Widget.BoxProps), + new Widget.Box({ + className: "text", + orientation: Gtk.Orientation.VERTICAL, + expand: true, + children: [ + new Widget.Label({ + className: "summary", + useMarkup: true, + xalign: 0, + truncate: true, + label: notification.summary.replace(/\&/g, "&") + }), + new Widget.Label({ + className: "body", + useMarkup: true, + halign: Gtk.Align.START, + xalign: 0, + truncate: false, + wrap: true, + label: 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 new file mode 100644 index 0000000..1857df6 --- /dev/null +++ b/ags/widget/PopupWindow.ts @@ -0,0 +1,46 @@ +import { Binding } from "astal"; +import { Astal, Gdk, Widget } from "astal/gtk3"; +import { BackgroundWindow } from "./BackgroundWindow"; + +type PopupWindowSpecificProps = { + onDestroy?: (self: Widget.Window) => void; + onKeyPressEvent?: (win: Widget.Window, event: Gdk.Event) => void; + /** Stylesheet for the background of the popup-window */ + cssBackgroundWindow?: string; +}; + +export type PopupWindowProps = Omit & PopupWindowSpecificProps; + +export function PopupWindow(props: PopupWindowProps): Widget.Window { + props.layer = props.layer ?? Astal.Layer.OVERLAY; + + const bgWindow = BackgroundWindow({ + monitor: props.monitor ?? 0, + layer: props.layer!, + css: props.cssBackgroundWindow ?? "", + onAction: () => window.close() + }); + + const window = new Widget.Window({ + ...props, + namespace: props?.namespace ?? "popup-window", + className: `popup-window ${(props.namespace instanceof Binding ? + props.namespace.get() : props.namespace) || ""}`, + keymode: Astal.Keymode.EXCLUSIVE, + layer: props.layer!, + onDestroy: (self) => { + bgWindow.close(); + props.onDestroy?.(self); + }, + onKeyPressEvent: (self, event: Gdk.Event) => { + if(event.get_keyval()[1] === Gdk.KEY_Escape) { + self.close(); + return; + } + + props.onKeyPressEvent?.(self, event); + }, + } as Widget.WindowProps); + + return window; +} diff --git a/ags/widget/Separator.ts b/ags/widget/Separator.ts new file mode 100644 index 0000000..34c50de --- /dev/null +++ b/ags/widget/Separator.ts @@ -0,0 +1,57 @@ +import { Binding } from "astal"; +import { Gtk, Widget } from "astal/gtk3"; + +export interface SeparatorProps { + class?: string; + alpha?: number; + cssColor?: string; + orientation?: Gtk.Orientation; + size?: number; + spacing?: number; + margin?: number; + visible?: boolean | Binding; +} + +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 new file mode 100644 index 0000000..e80a83b --- /dev/null +++ b/ags/widget/bar/Apps.ts @@ -0,0 +1,20 @@ +import { Gtk, Widget } from "astal/gtk3"; +import { tr } from "../../i18n/intl"; +import { Windows } from "../../windows"; +import { bind } from "astal"; + +export function Apps(): Gtk.Widget { + return new Widget.EventBox({ + onClickRelease: () => Windows.open("apps-window"), + className: bind(Windows, "openWindows").as((openWindows) => + Object.hasOwn(openWindows, "apps-window") ? "apps open" : "apps"), + child: new Widget.Box({ + child: new Widget.Icon({ + tooltipText: tr("apps"), + icon: "applications-other-symbolic", + halign: Gtk.Align.CENTER, + hexpand: true + } as Widget.IconProps) + } as Widget.BoxProps) + } as Widget.EventBoxProps); +} diff --git a/ags/widget/bar/Clock.ts b/ags/widget/bar/Clock.ts new file mode 100644 index 0000000..e9c7585 --- /dev/null +++ b/ags/widget/bar/Clock.ts @@ -0,0 +1,17 @@ +import { Gtk, Widget } from "astal/gtk3"; +import { getDateTime } from "../../scripts/time"; +import { bind, GLib } from "astal"; +import { Windows } from "../../windows"; + +export function Clock(): Gtk.Widget { + return new Widget.Box({ + className: bind(Windows, "openWindows").as((openWins) => + Object.hasOwn(openWins, "center-window") ? "open clock" : "clock"), + child: new Widget.Button({ + onClick: () => Windows.toggle("center-window"), + label: getDateTime().as((dateTime: GLib.DateTime) => { + return dateTime.format("%A %d, %H:%M") + }) + } as Widget.ButtonProps) + } as Widget.BoxProps); +} diff --git a/ags/widget/bar/FocusedClient.ts b/ags/widget/bar/FocusedClient.ts new file mode 100644 index 0000000..30ef0cf --- /dev/null +++ b/ags/widget/bar/FocusedClient.ts @@ -0,0 +1,50 @@ +import { bind } from "astal"; +import { Gtk, Widget } from "astal/gtk3"; +import AstalHyprland from "gi://AstalHyprland"; +import { getAppIcon } from "../../scripts/apps"; + +const hyprland = AstalHyprland.get_default(); + +export function FocusedClient(): Gtk.Widget { + return new Widget.Box({ + className: "focused-client", + visible: bind(hyprland, "focusedClient").as(Boolean), + children: bind(hyprland, "focusedClient").as(focusedClient => focusedClient ? [ + new Widget.Icon({ + className: "icon", + vexpand: true, + css: ".icon { font-size: 18px; }", + icon: bind(focusedClient, "class").as(clss => + getAppIcon(clss) ?? "application-x-executable-symbolic") + }), + new Widget.Box({ + className: "text-content", + orientation: Gtk.Orientation.VERTICAL, + homogeneous: false, + valign: Gtk.Align.CENTER, + children: [ + new Widget.Label({ + className: "class", + xalign: 0, + visible: bind(focusedClient, "class").as(Boolean), + maxWidthChars: 55, + truncate: true, + tooltipText: bind(focusedClient, "class").as((clientClass: string) => + clientClass.length > 55 ? clientClass : ""), + label: bind(focusedClient, "class") + } as Widget.LabelProps), + new Widget.Label({ + className: "title", + xalign: 0, + maxWidthChars: 50, + visible: bind(focusedClient, "title").as(Boolean), + truncate: true, + tooltipText: bind(focusedClient, "title").as((clientTitle: string) => + clientTitle.length > 55 ? clientTitle : ""), + label: bind(focusedClient, "title") + } as Widget.LabelProps) + ] + }) + ]: []) + } as Widget.BoxProps); +} diff --git a/ags/widget/bar/Media.ts b/ags/widget/bar/Media.ts new file mode 100644 index 0000000..2b43872 --- /dev/null +++ b/ags/widget/bar/Media.ts @@ -0,0 +1,131 @@ +import { bind, execAsync, GLib } from "astal"; +import { Gtk, Widget } from "astal/gtk3"; +import AstalMpris from "gi://AstalMpris"; +import { Separator, SeparatorProps } from "../Separator"; +import { Windows } from "../../windows"; + + +const playerIcons = { + spotify: '󰓇', + clapper: 'ó°æŽ', + mpv: 'ļ®', + spotube: '󰋋', + firefox: '󰈹' +} + +export function Media(): Gtk.Widget { + + const connections: Array = []; + + 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 nf", + label: "󰌹", + tooltipText: "Copy link to Clipboard", + visible: bind(players[0], "metadata").as((_metadata: GLib.HashTable) => + players[0].get_meta("xesam:url") === null), + onClick: () => execAsync(`sh -c "wl-copy \\"$(playerctl metadata 'xesam:url')\\""`) + } as Widget.ButtonProps), + new Widget.Button({ + className: "previous nf", + label: "󰒮", + tooltipText: "Previous", + onClick: () => players[0].canGoPrevious && players[0].previous() + } as Widget.ButtonProps), + new Widget.Button({ + className: "pause nf", + tooltipText: bind(players[0], "playback_status").as((status: AstalMpris.PlaybackStatus) => + status === AstalMpris.PlaybackStatus.PLAYING ? "Pause" : "Play"), + label: bind(players[0], "playbackStatus").as((status: AstalMpris.PlaybackStatus) => + status === AstalMpris.PlaybackStatus.PLAYING ? "󰏤" : "󰐊"), + onClick: () => { + players[0].playbackStatus === AstalMpris.PlaybackStatus.PAUSED ? + players[0].play() + : + players[0].pause() + } + } as Widget.ButtonProps), + new Widget.Button({ + className: "next nf", + label: "󰒭", + tooltipText: "Next", + onClick: () => players[0].canGoNext && players[0].next() + } as Widget.ButtonProps) + ] : new Widget.Label({ + label: "Don't Stop The Music!" + } as Widget.LabelProps) + ) + } as Widget.BoxProps) + } as Widget.RevealerProps); + + const mediaWidget = new Widget.EventBox({ + className: "media-eventbox", + visible: bind(AstalMpris.get_default(), "players").as((players: Array) => + 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.Label({ + className: "icon nf", + label: bind(players[0], "busName").as((busName: string) => { + const playerName: string = busName.split('.')[busName.split('.').length-1]; + return playerIcons[playerName.toLowerCase() as keyof typeof playerIcons] || "󰎇"; + }) + } as Widget.LabelProps), + new Widget.Label({ + className: "title", + label: bind(players[0], "title").as((title: string) => title || "No Title"), + maxWidthChars: 20, + truncate: true + } as Widget.LabelProps), + Separator({ + orientation: Gtk.Orientation.HORIZONTAL, + size: 1, + margin: 5, + //cssColor: `rgb(180, 180, 180)`, + alpha: .3 + } as SeparatorProps), + new Widget.Label({ + className: "artist", + label: bind(players[0], "artist").as((artist: string) => artist || "No Artist"), + maxWidthChars: 18, + truncate: true + } as Widget.LabelProps) + ] : new Widget.Label({ + label: "Crazy to think this widget haven't disappeared yet!" + } as Widget.LabelProps) + ) + } as Widget.BoxProps), + mediaControlsRevealer + ] + } as Widget.BoxProps) + } as Widget.EventBoxProps); + + connections.push( + mediaWidget.connect("hover", () => { + mediaControlsRevealer.set_reveal_child(true); + mediaWidget.className = mediaWidget.className + " reveal"; + }), + mediaWidget.connect("hover-lost", (_) => { + mediaControlsRevealer.set_reveal_child(false); + _.className = mediaWidget.className.replaceAll(" reveal", ""); + }) + ); + + return mediaWidget; +} diff --git a/ags/widget/bar/SpecialWorkspaces.ts b/ags/widget/bar/SpecialWorkspaces.ts new file mode 100644 index 0000000..312d1b4 --- /dev/null +++ b/ags/widget/bar/SpecialWorkspaces.ts @@ -0,0 +1,43 @@ +import { bind, Variable } from "astal"; +import { Gtk, Widget } from "astal/gtk3" +import AstalHyprland from "gi://AstalHyprland"; +import { getAppIcon } from "../../scripts/apps"; + +export const SpecialWorkspaces: (() => Gtk.Widget) = () => new Widget.EventBox({ + className: "special-ws-eventbox", + visible: bind(AstalHyprland.get_default(), "workspaces").as((workspaces) => + workspaces.filter(ws => ws.id < 0).sort((a, b) => a.id - b.id).length > 0), + child: new Widget.Box({ + className: "special-workspaces", + spacing: 4, + children: bind(AstalHyprland.get_default(), "workspaces").as((workspaces) => + workspaces.filter(ws => ws.id < 0).sort((a, b) => a.id - b.id).map((workspace) => + new Widget.EventBox({ + className: bind(AstalHyprland.get_default(), "focusedWorkspace").as(focusWs => + `${focusWs.id === workspace.id ? "focus" : ""}`), + tooltipText: bind(workspace, "name").as((name) => { + name = name.replace(/^special\:/, ""); + return name.charAt(0).toUpperCase().concat(name.substring(1, name.length)); + }), + child: new Widget.Box({ + child: new Widget.Icon({ + className: "last-app-icon", + visible: Variable.derive([ + bind(workspace, "lastClient"), + bind(AstalHyprland.get_default(), "focusedWorkspace") + ], (lastClient, focusedWorkspace) => focusedWorkspace?.id === workspace.id ? + false : Boolean(lastClient))(), + icon: bind(workspace, "lastClient").as((lastClient) => + lastClient ? + getAppIcon(lastClient.initialClass) || "image-missing" + : "image-missing") + } as Widget.IconProps) + } as Widget.BoxProps), + onClickRelease: () => AstalHyprland.get_default().dispatch( + "togglespecialworkspace", workspace.name.replace(/^special\:/, "") + ) + } as Widget.EventBoxProps) + ) + ) + } as Widget.BoxProps) +} as Widget.EventBoxProps); diff --git a/ags/widget/bar/Status.ts b/ags/widget/bar/Status.ts new file mode 100644 index 0000000..ed9ec97 --- /dev/null +++ b/ags/widget/bar/Status.ts @@ -0,0 +1,159 @@ +import AstalBluetooth from "gi://AstalBluetooth"; +import AstalNetwork from "gi://AstalNetwork"; +import AstalWp from "gi://AstalWp"; + +import { bind, Binding, Variable } from "astal"; +import { Gtk, Widget } from "astal/gtk3"; +import { Wireplumber } from "../../scripts/volume"; +import { Notifications } from "../../scripts/notifications"; +import { Windows } from "../../windows"; +import { Recording } from "../../scripts/recording"; +import { getDateTime } from "../../scripts/time"; +import { tr } from "../../i18n/intl"; + + +export function Status(): Gtk.Widget { + return new Widget.EventBox({ + className: bind(Windows, "openWindows").as((openWins) => + Object.hasOwn(openWins, "control-center") ? "open status" : "status"), + onClick: () => Windows.toggle("control-center"), + child: new Widget.Box({ + children: [ + volumeStatus({ + className: "sink", + endpoint: Wireplumber.getDefault().getDefaultSink(), + icon: bind(Wireplumber.getDefault().getDefaultSink(), "mute").as((muted) => + !muted ? "󰕾" : "󰖁") + }), + volumeStatus({ + className: "source", + endpoint: Wireplumber.getDefault().getDefaultSource(), + icon: bind(Wireplumber.getDefault().getDefaultSource(), "mute").as((muted) => + !muted ? "󰍬" : "󰍭") + }), + StatusIcons() + ] + } as Widget.BoxProps) + } as Widget.EventBoxProps); +} + +function volumeStatus(props: { className?: string, endpoint: AstalWp.Endpoint, icon?: (string|Binding) }): Gtk.Widget { + return new Widget.EventBox({ + className: props.className, + onScroll: (_, event) => + event.delta_y > 0 ? + Wireplumber.getDefault().decreaseEndpointVolume(props.endpoint, 5) + : + Wireplumber.getDefault().increaseEndpointVolume(props.endpoint, 5), + child: new Widget.Box({ + children: [ + new Widget.Label({ + className: "nf", + visible: props.icon, + label: props.icon, + } as Widget.LabelProps), + new Widget.Label({ + className: "volume", + label: bind(props.endpoint, "volume").as((volume: number) => + Math.floor(volume * 100) + "%") + } as Widget.LabelProps), + ] + } as Widget.BoxProps) + } as Widget.EventBoxProps) +} + +function StatusIcons(): Gtk.Widget { + const bluetoothIcon: Variable = Variable.derive([ + bind(AstalBluetooth.get_default(), "isPowered"), + bind(AstalBluetooth.get_default(), "isConnected") + ], (powered, connected) => { + return powered ? ( + connected ? "󰂱" + : "ó°‚Æ" + ) : "󰂲" + }); + + const networkIcon: Variable = Variable.derive([ + bind(AstalNetwork.get_default(), "primary"), + bind(AstalNetwork.get_default(), "wired"), + bind(AstalNetwork.get_default(), "wifi") + ], + (primary, wired, wifi) => { + switch(primary) { + case AstalNetwork.Primary.WIRED: return wired ? + "󰛳" + : "󰛵"; + + case AstalNetwork.Primary.WIFI: return wifi ? + "󰤨" + : "ó°¤­"; + } + + return "󰲊"; + }); + + const recordingTimer: Variable = Variable.derive([ + bind(Recording.getDefault(), "recording"), + getDateTime() + ], (recording, dateTime) => { + if(!recording || !Recording.getDefault().startedAt) + return "..."; + + const startedAtSeconds = dateTime.to_unix() - Recording.getDefault().startedAt!.to_unix(); + if(startedAtSeconds <= 0) return "00:00"; + + const hours = Math.floor(startedAtSeconds / 120); + const minutes = Math.floor(startedAtSeconds / 60); + const seconds = Math.floor(startedAtSeconds % 60); + + return `${ hours > 0 ? `${hours < 10 ? `0${hours}` : hours }:` : "" + }${ minutes < 10 ? `0${minutes}` : minutes + }:${ seconds < 10 ? `0${seconds}` : seconds }`; + }); + + return new Widget.Box({ + className: "status-icons", + children: [ + new Widget.Label({ + className: "bluetooth nf state", + visible: bind(AstalBluetooth.get_default(), "adapter").as(Boolean), + label: bluetoothIcon(), + onDestroy: () => bluetoothIcon.drop() + } as Widget.LabelProps), + new Widget.Label({ + className: "network nf state", + label: networkIcon(), + onDestroy: () => networkIcon.drop() + } as Widget.LabelProps), + new Widget.Revealer({ + revealChild: bind(Recording.getDefault(), "recording"), + transitionDuration: 500, + transitionType: Gtk.RevealerTransitionType.SLIDE_LEFT, + setup: (revealer) => revealer.add( + new Widget.EventBox({ + onClick: () => Recording.getDefault().recording && + Recording.getDefault().stopRecording(), + tooltipText: tr("control_center.tiles.recording.enabled_desc"), + child: new Widget.Box({ + children: [ + new Widget.Label({ + className: "recording nf state", + label: '󰻃' + } as Widget.LabelProps), + new Widget.Label({ + className: "rec-time", + label: recordingTimer() + } as Widget.LabelProps) + ] + } as Widget.BoxProps) + } as Widget.EventBoxProps) + ) + } as Widget.RevealerProps), + new Widget.Label({ + className: "bell nf state", + label: bind(Notifications.getDefault().getNotifd(), "dontDisturb").as((dnd: boolean) => + dnd ? "󰂠" : "󰂚") + } as Widget.LabelProps), + ] + } as Widget.BoxProps); +} diff --git a/ags/widget/bar/Tray.ts b/ags/widget/bar/Tray.ts new file mode 100644 index 0000000..f76f830 --- /dev/null +++ b/ags/widget/bar/Tray.ts @@ -0,0 +1,48 @@ +import { bind, Gio, Variable } from "astal"; +import { Astal, Gdk, Gtk, Widget } from "astal/gtk3"; +import AstalTray from "gi://AstalTray" + +const astalTray = AstalTray.get_default(); + +function menuFromModel(model: Gio.MenuModel, actionGroup: Gio.ActionGroup | null): Gtk.Menu { + const menu = Gtk.Menu.new_from_model(model); + menu.insert_action_group("dbusmenu", actionGroup) + + return menu; +} + +export function Tray(): Gtk.Widget { + return new Widget.Box({ + className: "tray", + visible: bind(astalTray, "items").as((items: Array) => items.length > 0), + children: bind(astalTray, "items").as((items: Array) => + items.map((item: AstalTray.TrayItem) => + new Widget.Box({ + className: "item", + child: Variable.derive( + [ bind(item, "menuModel"), bind(item, "actionGroup") ], + (menuModel: Gio.MenuModel, actionGroup: Gio.ActionGroup) => { + const menu = menuFromModel(menuModel, actionGroup); + + return new Widget.Button({ + className: "item-button", + tooltipMarkup: bind(item, "tooltipMarkup"), + onClick: (_, event: Astal.ClickEvent) => { + if(event.button === Astal.MouseButton.SECONDARY) { + item.about_to_show(); + menu.popup_at_widget(_, Gdk.Gravity.NORTH, Gdk.Gravity.SOUTH_WEST, null); + } else if(event.button === Astal.MouseButton.PRIMARY) + item.activate(event.x, event.y); + }, + halign: Gtk.Align.CENTER, + child: new Widget.Icon({ + gIcon: bind(item, "gicon") + }) + } as Widget.ButtonProps) + } + )() + } as Widget.BoxProps) + ) + ) + } as Widget.BoxProps); +} diff --git a/ags/widget/bar/Workspaces.ts b/ags/widget/bar/Workspaces.ts new file mode 100644 index 0000000..25b4523 --- /dev/null +++ b/ags/widget/bar/Workspaces.ts @@ -0,0 +1,86 @@ +import { bind, Variable } from "astal"; +import { Gtk, Widget } from "astal/gtk3"; +import AstalHyprland from "gi://AstalHyprland"; +import { getAppIcon } from "../../scripts/apps"; + +let showWsNum: (Variable|undefined); +export const showWorkspaceNumber = (show: boolean) => + showWsNum?.set(show); + + +export function Workspaces(): Gtk.Widget { + showWsNum = new Variable(false); + + return new Widget.EventBox({ + onScroll: (_, event) => + event.delta_y > 0 ? + AstalHyprland.get_default().dispatch("workspace", "e-1") + : AstalHyprland.get_default().dispatch("workspace", "e+1"), + onHover: () => showWorkspaceNumber(true), + onHoverLost: () => showWorkspaceNumber(false), + onDestroy: () => { + showWsNum?.drop(); + showWsNum = undefined; + }, + child: new Widget.Box({ + className: "workspaces", + spacing: 4, + children: bind(AstalHyprland.get_default(), "workspaces").as((workspaces) => + workspaces.filter((ws) => ws.id > 0).sort((a, b) => a.id - b.id).map((workspace) => { + const className = Variable.derive([ + bind(AstalHyprland.get_default(), "focusedWorkspace"), + showWsNum!() + ], (focusedWs, showWsNumbers) => + `${focusedWs.id === workspace.id ? "focus" : ""} ${showWsNumbers ? "show" : ""}` + ); + + const tooltipText = Variable.derive([ + bind(workspace, "lastClient"), + bind(AstalHyprland.get_default(), "focusedWorkspace") + ], (lastClient, focusWs) => focusWs.id === workspace.id ? "" : + `Workspace ${workspace.id}${ lastClient ? ` - ${ + !lastClient.title.toLowerCase().includes(lastClient.class) ? + `${lastClient.get_class()}: ` + : "" + } ${lastClient.title}` : "" }` + ); + + return new Widget.EventBox({ + className: className(), + onClickRelease: () => workspace.focus(), + tooltipText: tooltipText(), + onDestroy: () => { + className.drop(); + tooltipText.drop(); + }, + child: new Widget.Box({ + children: bind(workspace, "lastClient").as((lastClient) => [ + new Widget.Revealer({ + transitionDuration: 200, + transitionType: Gtk.RevealerTransitionType.SLIDE_LEFT, + revealChild: showWsNum!(), + child: new Widget.Label({ + label: bind(workspace, "id").as(String), + className: "id", + hexpand: true + } as Widget.LabelProps) + } as Widget.RevealerProps), + new Widget.Icon({ + className: "last-app-icon", + visible: bind(AstalHyprland.get_default(), "focusedWorkspace").as(focusedWorkspace => + workspace.id === focusedWorkspace.id ? + false + : Boolean(lastClient)), + icon: lastClient ? + bind(lastClient, "class").as((clss) => + getAppIcon(clss) ?? "application-x-executable-symbolic") + : undefined + } as Widget.IconProps) + ]) + } as Widget.BoxProps) + } as Widget.EventBoxProps); + }) + ) + } as Widget.BoxProps) + } as Widget.EventBoxProps); +} diff --git a/ags/widget/center-window/BigMedia.ts b/ags/widget/center-window/BigMedia.ts new file mode 100644 index 0000000..52d9587 --- /dev/null +++ b/ags/widget/center-window/BigMedia.ts @@ -0,0 +1,191 @@ +import { AstalIO, bind, Binding, execAsync, GLib, timeout } from "astal"; +import { Gtk, Widget } from "astal/gtk3"; +import AstalMpris from "gi://AstalMpris"; + + +export function BigMedia(): Gtk.Widget { + let dragTimer: (AstalIO.Time|undefined); + + return new Widget.Box({ + className: "big-media", + orientation: Gtk.Orientation.VERTICAL, + homogeneous: false, + width_request: 250, + visible: bind(AstalMpris.get_default(), "players").as((players: Array) => + 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 nf", + label: "󰌹", + tooltipText: "Copy link to Clipboard", + visible: bind(players[0], "metadata").as((_meta: GLib.HashTable) => + players[0].get_meta("xesam:url") === null), + onClick: () => execAsync(`sh -c "wl-copy \\"$(playerctl metadata 'xesam:url')\\""`) + } as Widget.ButtonProps), + new Widget.Button({ + className: "shuffle nf", + visible: bind(players[0], "shuffleStatus").as((shuffleStatus: AstalMpris.Shuffle) => + shuffleStatus !== AstalMpris.Shuffle.UNSUPPORTED), + label: bind(players[0], "shuffleStatus").as((shuffleStatus: AstalMpris.Shuffle) => + shuffleStatus === AstalMpris.Shuffle.ON ? "󰒝" : "󰒞"), + tooltipText: "Toggle Shuffle", + onClick: () => players[0].shuffle() + } as Widget.ButtonProps), + new Widget.Button({ + className: "previous nf", + label: "󰒮", + tooltipText: "Previous", + onClick: () => players[0].canGoPrevious && players[0].previous() + } as Widget.ButtonProps), + new Widget.Button({ + className: "pause nf", + tooltipText: bind(players[0], "playback_status").as((status: AstalMpris.PlaybackStatus) => + status === AstalMpris.PlaybackStatus.PLAYING ? "Pause" : "Play"), + label: bind(players[0], "playbackStatus").as((status: AstalMpris.PlaybackStatus) => + status === AstalMpris.PlaybackStatus.PLAYING ? "󰏤" : "󰐊"), + onClick: () => { + players[0].playbackStatus === AstalMpris.PlaybackStatus.PAUSED ? + players[0].play() + : + players[0].pause() + } + } as Widget.ButtonProps), + new Widget.Button({ + className: "next nf", + label: "󰒭", + tooltipText: "Next", + onClick: () => players[0].canGoNext && players[0].next() + } as Widget.ButtonProps), + new Widget.Button({ + className: "repeat nf", + visible: bind(players[0], "loopStatus").as((loopStatus: AstalMpris.Loop) => + loopStatus !== AstalMpris.Loop.UNSUPPORTED), + label: bind(players[0], "loopStatus").as((loopStatus: AstalMpris.Loop) => { + switch(loopStatus) { + case AstalMpris.Loop.TRACK: return "󰑘"; + case AstalMpris.Loop.PLAYLIST: return "󰑖"; + default: return "󰑗"; + } + }), + tooltipText: "Toggle Loop", + onClick: () => players[0].loop() + } as Widget.ButtonProps) + ] + } as Widget.BoxProps), + endWidget: new Widget.Label({ + className: "length", + valign: Gtk.Align.START, + halign: Gtk.Align.END, + label: bind(players[0], "length").as((len/* bananananananana */: number) => { + const sec: number = Math.floor(len % 60); + return len > 0 ? + `${Math.floor(len / 60)}:${sec < 10 ? "0" : ""}${sec}` + : "0:00"; + }) + } as Widget.LabelProps) + }) + ]) + } as Widget.BoxProps); +} + + +/** + * This function handles album art/cover of playing media. If a file is provided + * by the player, it adds the "file://" uri as a prefix, so you can use it in css. + * + * @param player the player you want to pull album art from + * @returns Binding to player.artUrl containing the album art uri, or an undefined binding ig none was found. +* */ +function getAlbumArt(player: AstalMpris.Player): Binding { + 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 new file mode 100644 index 0000000..4fa5271 --- /dev/null +++ b/ags/widget/center-window/Calendar.ts @@ -0,0 +1,46 @@ +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 new file mode 100644 index 0000000..fd9dc54 --- /dev/null +++ b/ags/widget/control-center/NotifHistory.ts @@ -0,0 +1,65 @@ +import { bind } from "astal"; +import { Gtk, Widget } from "astal/gtk3"; +import { HistoryNotification, Notifications } from "../../scripts/notifications"; +import { NotificationWidget } from "../Notification"; + + +export const NotifHistory = () => { + return new Widget.Box({ + orientation: Gtk.Orientation.VERTICAL, + className: bind(Notifications.getDefault(), "history").as(history => history.length > 0 ? "history" : "history hide"), + children: [ + new Widget.Scrollable({ + className: "history", + hscroll: Gtk.PolicyType.NEVER, + vscroll: Gtk.PolicyType.AUTOMATIC, + propagateNaturalHeight: true, + propagateNaturalWidth: false, + onDraw: (scrollable) => { + if(!(scrollable.get_child()! as Gtk.Viewport).get_child()) return; + + scrollable.minContentHeight = + ((scrollable.get_child()! as Gtk.Viewport).get_child() as Widget.Box + ).get_children()?.[0].get_allocation().height + || 0; + }, + child: new Widget.Box({ + className: "notifications", + hexpand: true, + orientation: Gtk.Orientation.VERTICAL, + homogeneous: false, + spacing: 4, + valign: Gtk.Align.START, + children: bind(Notifications.getDefault(), "history").as((history: Array) => + history.map((notification: HistoryNotification) => NotificationWidget(notification, + () => Notifications.getDefault().removeHistory(notification.id), true) + )) + } as Widget.BoxProps) + } as Widget.ScrollableProps), + new Widget.Box({ + vexpand: false, + hexpand: true, + halign: Gtk.Align.END, + className: "button-row", + children: [ + new Widget.Button({ + className: "clear-all", + child: new Widget.Box({ + children: [ + new Widget.Label({ + className: "nf", + css: "margin-right: 6px", + label: "󰎟" + } as Widget.LabelProps), + new Widget.Label({ + label: "Clear" + } as Widget.LabelProps) + ] + } as Widget.BoxProps), + onClick: () => Notifications.getDefault().clearHistory(), + } as Widget.ButtonProps) + ] + }) + ] + } as Widget.BoxProps); +} diff --git a/ags/widget/control-center/Pages.ts b/ags/widget/control-center/Pages.ts new file mode 100644 index 0000000..1d20009 --- /dev/null +++ b/ags/widget/control-center/Pages.ts @@ -0,0 +1,79 @@ +import { property, register, timeout } from "astal"; +import { Gtk, Widget } from "astal/gtk3"; +import { Page } from "./pages/Page"; + + +export { Pages }; +export type PagesProps = { + initialPage?: Page; + className?: string; + transitionType?: Gtk.RevealerTransitionType; + transitionDuration?: number; +}; + +@register({ GTypeName: "Pages" }) +class Pages extends Widget.Revealer { + #page: (Page|undefined); + + @property(Page) + get page(): Page | undefined { return this.#page; } + private set page(newPage: Page | undefined) { + this.#page = newPage; + this.notify("page"); + } + + get isOpen() { return this.revealChild; } + + constructor(props?: PagesProps) { + super({ + className: props?.className + }); + + this.name = "pages"; + + if(props?.className !== null && props?.className !== undefined) + this.className = props?.className; + + this.transitionType = props?.transitionType ?? + Gtk.RevealerTransitionType.SLIDE_DOWN; + + this.transitionDuration = props?.transitionDuration ?? 350; + + if(props?.initialPage) + this.open(props.initialPage); + } + + toggle(newPage?: Page): void { + if(this.isOpen) { + if(newPage && this.#page!.id !== newPage.id) { + this.close(() => this.open(newPage)); + return; + } + + this.close(); + return; + } + + if(newPage) this.open(newPage); + } + + open(newPage: Page, onOpened?: () => void) { + if(this.isOpen) return; + + this.page = newPage; + this.add(newPage); + this.revealChild = true; + onOpened && timeout(this.transitionDuration, onOpened); + } + + close(onClosed?: () => void): void { + if(!this.isOpen) return; + + this.revealChild = false; + timeout(this.transitionDuration, () => { + this.remove(this.#page!); + this.page = undefined; + onClosed?.(); + }); + } +} diff --git a/ags/widget/control-center/QuickActions.ts b/ags/widget/control-center/QuickActions.ts new file mode 100644 index 0000000..88b5e22 --- /dev/null +++ b/ags/widget/control-center/QuickActions.ts @@ -0,0 +1,103 @@ +import { exec, execAsync, GLib, Variable } from "astal"; +import { Gtk, Widget } from "astal/gtk3"; +import AstalHyprland from "gi://AstalHyprland"; +import { Windows } from "../../windows"; +import { Wallpaper } from "../../scripts/wallpaper"; + + +function LockButton(): Widget.Button { + return new Widget.Button({ + className: "nf", + label: "󰌾", + onClick: () => { + Windows.close("control-center"); + AstalHyprland.get_default().dispatch("exec", "hyprlock"); + } + } as Widget.ButtonProps) +} + +function ColorPickerButton(): Widget.Button { + return new Widget.Button({ + className: "nf", + label: "ó°“±", + onClick: () => AstalHyprland.get_default().dispatch( + "exec", + "sh $HOME/.config/hypr/scripts/color-picker.sh" + ) + } as Widget.ButtonProps) +} + +function ScreenshotButton(): Widget.Button { + return new Widget.Button({ + className: "nf", + label: "󰹑", + onClick: () => { + Windows.close("control-center"); + execAsync(`sh ${GLib.get_user_config_dir()}/hypr/scripts/screenshot.sh`); + } + } as Widget.ButtonProps); +} + +function SelectWallpaperButton(): Widget.Button { + return new Widget.Button({ + className: "nf", + label: "ó°ø‰", + onClick: () => { + Windows.close("control-center"); + Wallpaper.getDefault().pickWallpaper(); + } + } as Widget.ButtonProps); +} + +function LogoutButton(): Widget.Button { + return new Widget.Button({ + className: "nf", + label: "󰗽", + onClick: () => Windows.open("logout-menu") + } as Widget.ButtonProps); +} + +export const QuickActions = () => { + const uptime = new Variable("Just turned on").poll(1000, + () => exec("uptime -p").replace(/^up /, "")); + + return new Widget.Box({ + className: "quickactions", + children: [ + new Widget.Box({ + orientation: Gtk.Orientation.VERTICAL, + halign: Gtk.Align.START, + hexpand: true, + className: "left", + children: [ + new Widget.Label({ + className: "hostname", + xalign: 0, + tooltipText: "Host name", + label: GLib.get_host_name() + } as Widget.LabelProps), + new Widget.Label({ + className: "uptime", + xalign: 0, + tooltipText: "Uptime", + onDestroy: () => uptime.drop(), + label: uptime().as((uptime: string) => `ó°„” ${uptime}`) + } as Widget.LabelProps) + ] + } as Widget.BoxProps), + new Widget.Box({ + orientation: Gtk.Orientation.HORIZONTAL, + className: "right button-row", + halign: Gtk.Align.END, + hexpand: true, + children: [ + LockButton(), + ColorPickerButton(), + ScreenshotButton(), + SelectWallpaperButton(), + LogoutButton() + ] + } as Widget.BoxProps) + ] + } as Widget.BoxProps); +} diff --git a/ags/widget/control-center/Sliders.ts b/ags/widget/control-center/Sliders.ts new file mode 100644 index 0000000..a2cc393 --- /dev/null +++ b/ags/widget/control-center/Sliders.ts @@ -0,0 +1,69 @@ +import { bind } from "astal"; +import { Gtk, Widget } from "astal/gtk3"; +import { Wireplumber } from "../../scripts/volume"; +import { Pages } from "./Pages"; +import { PageSound } from "./pages/Sound"; +import { PageMicrophone } from "./pages/Microphone"; + +export function Sliders() { + const slidersPages = new Pages(); + + return new Widget.Box({ + className: "sliders", + orientation: Gtk.Orientation.VERTICAL, + expand: true, + children: [ + new Widget.Box({ + className: "sink speaker", + children: bind(Wireplumber.getWireplumber(), "defaultSpeaker").as((sink) => [ + new Widget.Button({ + className: "nf", + label: bind(sink, "mute").as((muted) => !muted ? "󰕾" : "󰖁"), + onClick: () => Wireplumber.getDefault().toggleMuteSink() + } as Widget.ButtonProps), + new Widget.Slider({ + drawValue: false, + hexpand: true, + setup: (slider) => slider.value = Math.floor(sink.volume * 100), + value: bind(sink, "volume").as((vol) => Math.floor(vol * 100)), + max: Wireplumber.getDefault().getMaxSinkVolume(), + onDragged: (slider) => sink.volume = slider.value / 100 + } as Widget.SliderProps), + new Widget.Button({ + className: "more", + image: new Widget.Icon({ + icon: "go-next-symbolic", + } as Widget.IconProps), + onClick: (_) => slidersPages.toggle(PageSound()) + } as Widget.ButtonProps) + ]) + } as Widget.BoxProps), + new Widget.Box({ + className: "source microphone", + children: bind(Wireplumber.getWireplumber(), "defaultMicrophone").as((source) => [ + new Widget.Button({ + className: "nf", + label: bind(source, "mute").as((muted) => !muted ? "󰍬" : "󰍭"), + onClick: () => Wireplumber.getDefault().toggleMuteSource() + } as Widget.ButtonProps), + new Widget.Slider({ + drawValue: false, + hexpand: true, + setup: (slider) => slider.set_value(Math.floor(source.volume * 100)), + value: bind(source, "volume").as((vol) => Math.floor(vol * 100)), + max: Wireplumber.getDefault().getMaxSourceVolume(), + onDragged: (slider) => source.volume = slider.value / 100 + } as Widget.SliderProps), + new Widget.Button({ + className: "more", + image: new Widget.Icon({ + icon: "go-next-symbolic", + } as Widget.IconProps), + onClick: (_) => slidersPages.toggle(PageMicrophone()) + } as Widget.ButtonProps) + ]) + } as Widget.BoxProps), + slidersPages + ] + } as Widget.BoxProps); +} diff --git a/ags/widget/control-center/Tiles.ts b/ags/widget/control-center/Tiles.ts new file mode 100644 index 0000000..5630a2a --- /dev/null +++ b/ags/widget/control-center/Tiles.ts @@ -0,0 +1,63 @@ +import { Gtk, Widget } from "astal/gtk3"; +import { TileNetwork } from "./tiles/Network"; +import { TileBluetooth } from "./tiles/Bluetooth"; +import { TileDND } from "./tiles/DoNotDisturb"; +import { TileRecording } from "./tiles/Recording"; +import { TileNightLight } from "./tiles/NightLight"; +import { Pages } from "./Pages"; +import { GObject } from "astal"; + +export const tileList: Array<() => Gtk.Widget> = [ + TileNetwork, + TileBluetooth, + TileRecording, + TileDND, + TileNightLight +]; + +export let TilesPages: (Pages|null) = null; + +export function Tiles(): Gtk.Widget { + const tilesFlowBox: Gtk.FlowBox = new Gtk.FlowBox({ + visible: true, + orientation: Gtk.Orientation.HORIZONTAL, + rowSpacing: 6, + columnSpacing: 6, + minChildrenPerLine: 2, + maxChildrenPerLine: 2, + expand: true, + homogeneous: true, + } as Gtk.FlowBox.ConstructorProps); + + tileList.map((item: (() => Gtk.Widget)) => { + const tile = item(); + tilesFlowBox.insert(tile, -1); + + const children = tilesFlowBox.get_children(); + children[children.length-1]!.set_can_focus(false); + const binding: GObject.Binding = tile.bind_property("visible", + children[children.length-1], "visible", + GObject.BindingFlags.SYNC_CREATE); + + const destroyId: number = tile.connect("destroy-event", (self: typeof tile) => { + binding.unbind(); + self.disconnect(destroyId); + }); + }); + + return new Widget.Box({ + className: "tiles-container", + orientation: Gtk.Orientation.VERTICAL, + onDestroy: () => TilesPages = null, + setup: (box) => { + if(!TilesPages) TilesPages = new Pages({ + className: "tile-pages" + }); + + box.set_children([ + tilesFlowBox, + TilesPages! + ]); + } + } as Widget.BoxProps); +} diff --git a/ags/widget/control-center/pages/Bluetooth.ts b/ags/widget/control-center/pages/Bluetooth.ts new file mode 100644 index 0000000..42b4632 --- /dev/null +++ b/ags/widget/control-center/pages/Bluetooth.ts @@ -0,0 +1,210 @@ +import { bind, Variable } from "astal"; +import { astalify, Gtk, Widget } from "astal/gtk3"; +import AstalBluetooth from "gi://AstalBluetooth"; +import { Page, PageButton } from "./Page"; +import { Separator, SeparatorProps } from "../../Separator"; +import { tr } from "../../../i18n/intl"; +import AstalHyprland from "gi://AstalHyprland?version=0.1"; +import { Windows } from "../../../windows"; + + +const AstalSpinner = astalify(Gtk.Spinner); + +export const BluetoothPage: (() => Page) = () => new Page({ + id: "bluetooth", + title: tr("control_center.pages.bluetooth.title"), + description: tr("control_center.pages.bluetooth.description"), + className: "bluetooth", + headerButtons: [ + new Widget.Button({ + className: "discover nf", + label: bind(AstalBluetooth.get_default().adapter, "discovering").as((discovering) => + !discovering ? '󰑓' : '󰙦'), + tooltipText: bind(AstalBluetooth.get_default().adapter, "discovering").as((discovering) => + !discovering ? + tr("control_center.pages.bluetooth.start_discovering") + : tr("control_center.pages.bluetooth.stop_discovering")), + onClick: () => { + if(AstalBluetooth.get_default().adapter.discovering) { + stopBluetoothDevicesWatch(); + return; + } + + watchNewDevices(); + } + } as Widget.ButtonProps) + ], + onClose: () => stopBluetoothDevicesWatch(), + spacing: 2, + children: [ + new Widget.Box({ + className: "adapters", + visible: bind(AstalBluetooth.get_default(), "adapters").as((adapters) => + adapters.length > 1), + spacing: 2, + children: bind(AstalBluetooth.get_default(), "adapters").as((adapters) => [ + new Widget.Label({ + className: "sub-header", + label: tr("control_center.pages.bluetooth.adapters") + } as Widget.LabelProps), + ...adapters.map(adapter => + PageButton({ + title: adapter.alias ?? "Adapter", + icon: "bluetooth-active-symbolic", + onClick: () => AstalBluetooth.get_default(), + }) + ) + ] + ) + } as Widget.BoxProps), + new Widget.Box({ + className: "connections", + orientation: Gtk.Orientation.VERTICAL, + hexpand: true, + spacing: 2, + children: [ + new Widget.Box({ + className: "paired", + orientation: Gtk.Orientation.VERTICAL, + visible: bind(AstalBluetooth.get_default(), "devices").as((devs) => + devs.filter(dev => dev.paired || dev.connected).length > 0), + children: bind(AstalBluetooth.get_default(), "devices").as((devs: Array) => { + const connectedDevices = devs.filter((dev: AstalBluetooth.Device) => dev.connected || dev.paired) + + return [ + new Widget.Label({ + className: "sub-header", + label: tr("control_center.pages.bluetooth.paired_devices"), + xalign: 0, + } as Widget.LabelProps), + ...connectedDevices.map((dev: AstalBluetooth.Device) => DeviceWidget(dev)) + ] + }) + } as Widget.BoxProps), + new Widget.Box({ + className: "discovered", + orientation: Gtk.Orientation.VERTICAL, + visible: bind(AstalBluetooth.get_default(), "devices").as((devs) => + devs.filter((dev) => !dev.connected && !dev.paired).length > 0), + children: bind(AstalBluetooth.get_default(), "devices").as((devices: Array) => { + const discoveredDevices = devices.filter((dev: AstalBluetooth.Device) => !dev.connected && !dev.paired); + + return [ + new Widget.Label({ + className: "sub-header", + label: tr("control_center.pages.bluetooth.new_devices"), + xalign: 0 + } as Widget.LabelProps), + ...discoveredDevices.map((dev: AstalBluetooth.Device) => DeviceWidget(dev)) + ] + }) + } as Widget.BoxProps), + Separator({ + size: .2, + orientation: Gtk.Orientation.VERTICAL, + cssColor: "gray", + alpha: .2 + } as SeparatorProps), + new Widget.Button({ + className: "more", + label: tr("control_center.pages.more_settings"), + onClick: () => { + Windows.close("control-center"); + AstalHyprland.get_default().dispatch("exec", "[float; animation slide right] overskride"); + }, + setup: (self) => self.set_alignment(0, 0.5) + } as Widget.ButtonProps) + ] + } as Widget.BoxProps) + ] +}); + +function DeviceWidget(dev: AstalBluetooth.Device): Gtk.Widget { + return PageButton({ + className: bind(dev, "connected").as((connected) => connected ? "connected" : ""), + title: bind(dev, "alias").as(alias => alias ?? "Unknown Device"), + icon: dev.icon ?? "bluetooth-active-symbolic", + onClick: () => { + if(dev.paired) { + dev.connected ? + dev.disconnect_device(null) + : dev.connect_device(null); + + return; + } + + dev.pair(); + dev.connected ? + dev.disconnect_device(null) + : dev.connect_device(null); + }, + endWidget: new Widget.Box({ + visible: bind(dev, "batteryPercentage").as((bat: number) => + bat <= -1 ? false : true), + children: [ + new Widget.Box({ + visible: bind(dev, "connected"), + children: [ + new Widget.Label({ + halign: Gtk.Align.END, + label: bind(dev, "batteryPercentage").as((bat: number) => + `${Math.floor(bat * 100)}%`) + } as Widget.LabelProps), + new Widget.Icon({ + icon: "battery-symbolic", + css: "font-size: 18px; margin-left: 6px;" + } as Widget.IconProps) + ] + } as Widget.BoxProps), + new Widget.Box({ + visible: bind(dev, "connecting"), + setup: (self) => { + const spinner = new AstalSpinner(); + + self.add(spinner); + } + } as Widget.BoxProps) + // Spinner here + ] + } as Widget.BoxProps), + extraButtons: Variable.derive([ + bind(dev, "connected"), + bind(dev, "paired"), + bind(dev, "trusted") + ], (connected, paired, trusted) => [ + new Widget.Button({ + className: "nf", + visible: paired && connected, + label: connected ? "󰅖" : "", + tooltipText: tr("disconnect"), + onClick: () => dev.disconnect_device() + } as Widget.ButtonProps), + new Widget.Button({ + visible: !connected && paired, + className: "nf", + label: "󰢃", + tooltipText: tr("control_center.pages.bluetooth.unpair_device"), + onClick: () => AstalBluetooth.get_default().adapter?.remove_device(dev) + } as Widget.ButtonProps), + new Widget.Button({ + className: "nf", + visible: paired, + label: trusted ? "󰫜" : "󰫚", + tooltipText: trusted ? + tr("control_center.pages.bluetooth.untrust_device") + : tr("control_center.pages.bluetooth.trust_device"), + onClick: () => trusted ? dev.set_trusted(false) : dev.set_trusted(true) + } as Widget.ButtonProps) + ])() + }); +} + +function watchNewDevices(): void { + !AstalBluetooth.get_default().adapter.discovering && + AstalBluetooth.get_default().adapter.start_discovery(); +} + +export function stopBluetoothDevicesWatch(): void { + AstalBluetooth.get_default().adapter.discovering && + AstalBluetooth.get_default().adapter.stop_discovery(); +} diff --git a/ags/widget/control-center/pages/Microphone.ts b/ags/widget/control-center/pages/Microphone.ts new file mode 100644 index 0000000..a3e2b73 --- /dev/null +++ b/ags/widget/control-center/pages/Microphone.ts @@ -0,0 +1,35 @@ +import { bind } from "astal"; +import { Page, PageButton, PageProps } from "./Page"; +import AstalWp from "gi://AstalWp?version=0.1"; +import { Wireplumber } from "../../../scripts/volume"; +import { Astal, Widget } from "astal/gtk3"; +import { tr } from "../../../i18n/intl"; + + +export function PageMicrophone(): Page { + return new Page({ + id: "microphone", + title: tr("control_center.pages.microphone.title"), + description: tr("control_center.pages.microphone.description"), + children: bind(Wireplumber.getWireplumber(), "endpoints").as((endpoints) => [ + new Widget.Label({ + className: "sub-header", + label: tr("devices"), + setup: (self) => self.set_alignment(0, .5) + } as Widget.LabelProps), + ...endpoints.filter(ep => ep.mediaClass === AstalWp.MediaClass.AUDIO_MICROPHONE).map((ep) => + PageButton({ + className: bind(ep, "isDefault").as(isDefault => isDefault ? "default" : ""), + icon: Astal.Icon.lookup_icon(ep.icon) ? ep.icon : "audio-input-microphone-symbolic", + title: ep.name ?? "Microphone", + onClick: () => ep.set_is_default(true), + endWidget: new Widget.Icon({ + icon: "object-select-symbolic", + visible: bind(ep, "isDefault"), + css: "font-size: 18px;" + } as Widget.IconProps) + }) + ) + ]) + } as PageProps); +} diff --git a/ags/widget/control-center/pages/Network.ts b/ags/widget/control-center/pages/Network.ts new file mode 100644 index 0000000..e7432f4 --- /dev/null +++ b/ags/widget/control-center/pages/Network.ts @@ -0,0 +1,112 @@ +import { Gtk, Widget } from "astal/gtk3"; +import { Page, PageButton } from "./Page"; +import AstalNetwork from "gi://AstalNetwork"; +import { bind } from "astal"; +import NM from "gi://NM"; +import { Separator, SeparatorProps } from "../../Separator"; +import { Windows } from "../../../windows"; +import AstalHyprland from "gi://AstalHyprland?version=0.1"; +import { tr } from "../../../i18n/intl"; + +export const PageNetwork: (() => Page) = () => new Page({ + id: "network", + title: tr("control_center.pages.network.title"), + className: "network", + headerButtons: [ + new Widget.Button({ + className: "reload nf", + label: "󰑓", + visible: bind(AstalNetwork.get_default(), "primary").as( + (primary: AstalNetwork.Primary) => primary === AstalNetwork.Primary.WIFI + ), + tooltipText: "Re-scan connections", + onClick: () => AstalNetwork.get_default().wifi.scan() + } as Widget.ButtonProps) + ], + children: [ + new Widget.Box({ + className: "devices", + hexpand: true, + orientation: Gtk.Orientation.VERTICAL, + visible: bind(AstalNetwork.get_default().get_client(), "devices").as((devs) => devs.length > 0), + children: bind(AstalNetwork.get_default().get_client(), "devices").as((devices) => { + devices = devices.filter(dev => dev.interface !== "lo"); + + return [ + new Widget.Label({ + label: tr("devices"), + xalign: 0, + className: "sub-header", + } as Widget.LabelProps), + ...devices.filter(device => device.real).map(dev => PageButton({ + className: "device", + icon: bind(dev, "deviceType").as(deviceType => + deviceType === NM.DeviceType.WIFI ? + "network-wireless-symbolic" + : "network-wired-symbolic"), + title: bind(dev, "interface").as(iface => iface ?? + tr("control_center.pages.network.interface")), + extraButtons: [ + new Widget.Button({ + image: new Widget.Icon({ + icon: "view-more-symbolic" + } as Widget.IconProps), + onClick: () => { + Windows.close("control-center"); + AstalHyprland.get_default().dispatch("exec", + `[animationstyle gnomed; float] nm-connection-editor --edit ${ + dev.activeConnection?.connection.get_uuid() + }`); + } + } as Widget.ButtonProps) + ] + }) + ) + ] + }) + } as Widget.BoxProps), + new Widget.Box({ + className: "wireless-aps", + visible: bind(AstalNetwork.get_default(), "primary").as((primary) => primary === AstalNetwork.Primary.WIFI), + hexpand: true, + orientation: Gtk.Orientation.VERTICAL, + children: AstalNetwork.get_default().wifi ? bind(AstalNetwork.get_default().wifi.get_device(), "accessPoints").as((aps) => + aps.map(ap => new Widget.Button({ + hexpand: true, + onClick: () => console.log("connect to " + ap.get_ssid().toArray().toString()), // TODO I don't have a WiFi board :( + child: new Widget.Box({ + hexpand: true, + children: [ + new Widget.Icon({ + halign: Gtk.Align.START, + className: "icon", + icon: "network-wireless-signal-excellent-symbolic" + } as Widget.IconProps), + new Widget.Label({ + className: "ssid", + halign: Gtk.Align.START, + label: ap.ssid.get_data()?.toString() ?? "Wi-Fi" + } as Widget.LabelProps), + new Widget.Label({ + className: "status", + } as Widget.LabelProps) + ] + } as Widget.BoxProps) + } as Widget.ButtonProps))) : [], + } as Widget.BoxProps), + Separator({ + orientation: Gtk.Orientation.VERTICAL, + alpha: .2, + size: .2 + } as SeparatorProps), + new Widget.Button({ + label: tr("control_center.pages.more_settings"), + setup: (self) => self.set_alignment(0, 0.5), + onClick: () => { + Windows.close("control-center"); + AstalHyprland.get_default().dispatch("exec", "[animationstyle gnomed] nm-connection-editor"); + } + } as Widget.ButtonProps) + ] +}); + diff --git a/ags/widget/control-center/pages/NightLight.ts b/ags/widget/control-center/pages/NightLight.ts new file mode 100644 index 0000000..8c54223 --- /dev/null +++ b/ags/widget/control-center/pages/NightLight.ts @@ -0,0 +1,51 @@ +import { Widget } from "astal/gtk3"; +import { Page, PageProps } from "./Page"; +import { bind } from "astal"; +import { NightLight } from "../../../scripts/nightlight"; +import { addSliderMarksFromMinMax } from "../../../scripts/widget-utils"; +import { tr } from "../../../i18n/intl"; + +export const PageNightLight: (() => Page) = () => new Page({ + id: "night-light", + title: tr("control_center.pages.night_light.title"), + description: tr("control_center.pages.night_light.description"), + className: "night-light", + children: [ + new Widget.Label({ + className: "sub-header", + label: tr("control_center.pages.night_light.temperature"), + xalign: 0 + } as Widget.LabelProps), + new Widget.Slider({ + className: "temperature", + setup: (slider) => { + slider.value = NightLight.getDefault().temperature; + addSliderMarksFromMinMax(slider, 5, "{}K"); + }, + value: bind(NightLight.getDefault(), "temperature"), + tooltipText: bind(NightLight.getDefault(), "temperature").as((temp) => `${temp}K`), + min: 1000, + max: bind(NightLight.getDefault(), "maxTemperature"), + onDragged: (slider) => + NightLight.getDefault().temperature = (Math.floor(slider.value)), + } as Widget.SliderProps), + new Widget.Label({ + className: "sub-header", + label: tr("control_center.pages.night_light.gamma"), + css: "margin-top: 6px;", + xalign: 0 + } as Widget.LabelProps), + new Widget.Slider({ + className: "gamma", + setup: (slider) => { + slider.value = NightLight.getDefault().gamma; + addSliderMarksFromMinMax(slider, 5, "{}%"); + }, + value: bind(NightLight.getDefault(), "gamma"), + max: bind(NightLight.getDefault(), "maxGamma"), + tooltipText: bind(NightLight.getDefault(), "gamma").as((gamma) => `${gamma}%`), + onDragged: (slider) => + NightLight.getDefault().gamma = (Math.floor(slider.value)), + } as Widget.SliderProps) + ] +} as PageProps); diff --git a/ags/widget/control-center/pages/Page.ts b/ags/widget/control-center/pages/Page.ts new file mode 100644 index 0000000..d106f0b --- /dev/null +++ b/ags/widget/control-center/pages/Page.ts @@ -0,0 +1,146 @@ +import { Binding, register } from "astal"; +import { Gtk, Widget } from "astal/gtk3"; + +export type PageProps = { + setup?: () => void; + onClose?: () => void; + onOpen?: () => void; + id: string; + className?: string | Binding; + title: string | Binding; + description?: string | Binding; + headerButtons?: Array | Binding>; + orientation?: Gtk.Orientation | Binding; + spacing?: number; + child?: Gtk.Widget | Binding; + children?: Array | Binding>; +}; + +export { Page }; + +@register({ GTypeName: "Page" }) +class Page extends Widget.Box { + readonly #id: string; + #title: string | Binding; + #description: string | undefined | Binding; + + public get title() { return this.#title; } + public get description() { return this.#description; } + public get id() { return this.#id; } + + constructor(props: PageProps) { + super({ + hexpand: true, + orientation: Gtk.Orientation.VERTICAL, + className: (props.className instanceof Binding) ? + props.className.as((clsName) => `page ${ clsName ?? "" }`) + : `page ${props.className ?? ""}`, + children: [ + new Widget.Box({ + className: "header", + orientation: Gtk.Orientation.VERTICAL, + hexpand: true, + children: [ + new Widget.Box({ + className: "top", + children: [ + new Widget.Label({ + hexpand: true, + className: "title", + truncate: true, + visible: (props.title instanceof Binding) ? + props.title.as(Boolean) + : (props.title ? true : false), + label: props.title, + halign: Gtk.Align.START + } as Widget.LabelProps), + new Widget.Box({ + className: "button-row", + visible: (props.headerButtons instanceof Binding) ? + props.headerButtons.as(Boolean) + : (props.headerButtons ? true : false), + children: props.headerButtons + } as Widget.BoxProps) + ] + } as Widget.BoxProps), + new Widget.Label({ + className: "description", + hexpand: true, + truncate: true, + xalign: 0, + visible: (props.description instanceof Binding) ? + props.description.as(Boolean) + : props.description ? true : false, + label: props.description + } as Widget.LabelProps), + ] + } as Widget.BoxProps), + new Widget.Box({ + className: "content", + spacing: props.spacing ?? 4, + orientation: props.orientation ?? Gtk.Orientation.VERTICAL, + expand: true, + setup: props.setup, + child: props.child, + children: props.children + } as Widget.BoxProps) + ] + }); + + this.#id = props.id; + this.#title = props.title; + this.#description = props.description; + } +} + +export function PageButton(props: { + className?: string | Binding; + icon?: string | Binding; + title: string | Binding; + endWidget?: Gtk.Widget | Binding; + extraButtons?: Array | Binding>; + onClick?: (self: Widget.Button) => void; +}): Gtk.Widget { + return new Widget.Box({ + children: [ + new Widget.Button({ + onClick: props.onClick, + className: props.className, + hexpand: true, + child: new Widget.Box({ + className: "page-button", + orientation: Gtk.Orientation.HORIZONTAL, + expand: true, + children: [ + new Widget.Icon({ + className: "icon", + icon: props.icon, + visible: props.icon, + css: "font-size: 20px; margin-right: 6px;" + } as Widget.IconProps), + new Widget.Label({ + className: "title", + halign: Gtk.Align.START, + hexpand: true, + truncate: true, + label: props.title + } as Widget.LabelProps), + new Widget.Box({ + visible: (props.endWidget instanceof Binding) ? + props.endWidget.as(Boolean) + : props.endWidget, + child: props.endWidget + } as Widget.BoxProps) + ] + } as Widget.BoxProps) + } as Widget.ButtonProps), + new Widget.Box({ + className: "button-row extra-buttons", + visible: (props.extraButtons instanceof Binding) ? + props.extraButtons.as(extra => extra.length > 0) + : (props.extraButtons ? props.extraButtons.length > 0 : false), + children: props.extraButtons + } as Widget.BoxProps) + ] + } as Widget.BoxProps); +} diff --git a/ags/widget/control-center/pages/Sound.ts b/ags/widget/control-center/pages/Sound.ts new file mode 100644 index 0000000..dc4aa84 --- /dev/null +++ b/ags/widget/control-center/pages/Sound.ts @@ -0,0 +1,90 @@ +import { Page, PageButton, PageProps } from "./Page"; +import { bind } from "astal"; +import { Astal, Gtk, Widget } from "astal/gtk3"; +import AstalWp from "gi://AstalWp"; +import { getAppIcon } from "../../../scripts/apps"; +import { Wireplumber } from "../../../scripts/volume"; +import { tr } from "../../../i18n/intl"; + +export function PageSound(): Page { + return new Page({ + id: "sound", + title: tr("control_center.pages.sound.title"), + description: tr("control_center.pages.sound.description"), + children: bind(Wireplumber.getWireplumber(), "endpoints").as((endpoints) => [ + new Widget.Label({ + className: "sub-header", + label: tr("devices"), + setup: (self) => self.set_alignment(0, .5) + } as Widget.LabelProps), + ...endpoints.filter(ep => ep.mediaClass === AstalWp.MediaClass.AUDIO_SPEAKER).map((ep) => + PageButton({ + className: bind(ep, "isDefault").as(isDefault => isDefault ? "default" : ""), + icon: Astal.Icon.lookup_icon(ep.icon) ? ep.icon : "audio-card-symbolic", + title: ep.name ?? "Speaker", + onClick: () => ep.set_is_default(true), + endWidget: new Widget.Icon({ + icon: "object-select-symbolic", + visible: bind(ep, "isDefault"), + css: "font-size: 18px;" + } as Widget.IconProps) + }) + ), + new Widget.Label({ + className: "sub-header", + label: tr("apps"), + visible: endpoints.filter((ep) => ep.mediaClass === AstalWp.MediaClass.AUDIO_STREAM || + ep.mediaClass === AstalWp.MediaClass.VIDEO_STREAM).length > 0, + setup: (self) => self.set_alignment(0, .5) + } as Widget.LabelProps), + ...endpoints.filter((ep) => ep.mediaClass === AstalWp.MediaClass.AUDIO_STREAM || + ep.mediaClass === AstalWp.MediaClass.VIDEO_STREAM).map((ep) => + new Widget.EventBox({ + hexpand: true, + setup: (eventbox) => { + const connections: Array = []; + eventbox.add(new Widget.Box({ + orientation: Gtk.Orientation.HORIZONTAL, + children: [ + new Widget.Icon({ + icon: getAppIcon(ep.name.split(' ')[0]) || "application-x-executable-symbolic", + css: "font-size: 18px; margin-right: 6px;" + } as Widget.IconProps), + new Widget.Box({ + orientation: Gtk.Orientation.VERTICAL, + hexpand: true, + children: [ + new Widget.Revealer({ + transitionDuration: 180, + transitionType: Gtk.RevealerTransitionType.SLIDE_DOWN, + setup: (self) => connections.push( + eventbox.connect("hover", () => self.revealChild = true), + eventbox.connect("hover-lost", () => self.revealChild = false) + ), + onDestroy: () => connections.map(id => eventbox.disconnect(id)), + child: new Widget.Label({ + label: ep.name || "Unknown", + truncate: true, + tooltipText: ep.name, + className: "name", + xalign: 0 + } as Widget.LabelProps) + } as Widget.RevealerProps), + new Widget.Slider({ + min: 0, + drawValue: false, + max: 100, + setup: (self) => self.value = Math.floor(ep.volume * 100), + value: bind(ep, "volume").as((vol) => Math.floor(vol * 100)), + onDragged: (self) => ep.volume = self.value / 100 + } as Widget.SliderProps) + ] + } as Widget.BoxProps) + ] + } as Widget.BoxProps)) + } + } as Widget.EventBoxProps) + ) + ]) + } as PageProps); +} diff --git a/ags/widget/control-center/tiles/Bluetooth.ts b/ags/widget/control-center/tiles/Bluetooth.ts new file mode 100644 index 0000000..860fbaa --- /dev/null +++ b/ags/widget/control-center/tiles/Bluetooth.ts @@ -0,0 +1,28 @@ +import { bind, Variable } from "astal"; +import { Tile, TileProps } from "./Tile"; +import AstalBluetooth from "gi://AstalBluetooth"; +import { BluetoothPage } from "../pages/Bluetooth"; +import { TilesPages } from "../Tiles"; + + +export const TileBluetooth = Tile({ + title: "Bluetooth", + visible: bind(AstalBluetooth.get_default(), "adapter").as(Boolean), + description: bind(AstalBluetooth.get_default(), "isConnected").as((connected) => { + const connectedDev = AstalBluetooth.get_default().devices.filter(dev => dev.connected)?.[0]; + return connected && connectedDev ? connectedDev.get_alias() : "" + }), + onToggledOn: () => AstalBluetooth.get_default().adapter?.set_powered(true), + onToggledOff: () => AstalBluetooth.get_default().adapter?.set_powered(false), + onClickMore: () => TilesPages?.toggle(BluetoothPage()), + enableOnClickMore: true, + icon: Variable.derive([ + bind(AstalBluetooth.get_default(), "isPowered"), + bind(AstalBluetooth.get_default(), "isConnected") + ], + (powered: boolean, isConnected: boolean) => + powered ? ( isConnected ? "󰂱" : "ó°‚Æ" ) : "󰂲" + )(), + iconSize: 16, + toggleState: bind(AstalBluetooth.get_default(), "isPowered") +} as TileProps); diff --git a/ags/widget/control-center/tiles/DoNotDisturb.ts b/ags/widget/control-center/tiles/DoNotDisturb.ts new file mode 100644 index 0000000..f1226b7 --- /dev/null +++ b/ags/widget/control-center/tiles/DoNotDisturb.ts @@ -0,0 +1,15 @@ +import { bind } from "astal"; +import { Notifications } from "../../../scripts/notifications"; +import { Tile } from "./Tile"; +import { tr } from "../../../i18n/intl"; + +export const TileDND = Tile({ + title: tr("control_center.tiles.dnd.title"), + description: bind(Notifications.getDefault().getNotifd(), "dontDisturb").as( + (dnd: boolean) => dnd ? tr("control_center.tiles.enabled") : tr("control_center.tiles.disabled")), + onToggledOff: () => Notifications.getDefault().getNotifd().dontDisturb = false, + onToggledOn: () => Notifications.getDefault().getNotifd().dontDisturb = true, + icon: "󰍶", + iconSize: 16, + toggleState: Notifications.getDefault().getNotifd().dontDisturb +}); diff --git a/ags/widget/control-center/tiles/Network.ts b/ags/widget/control-center/tiles/Network.ts new file mode 100644 index 0000000..62a2ef0 --- /dev/null +++ b/ags/widget/control-center/tiles/Network.ts @@ -0,0 +1,87 @@ +import { bind, execAsync, Variable } from "astal"; +import { Tile, TileProps } from "./Tile"; +import AstalNetwork from "gi://AstalNetwork"; +import { Widget } from "astal/gtk3"; +import { PageNetwork } from "../pages/Network"; +import { tr } from "../../../i18n/intl"; +import { TilesPages } from "../Tiles"; + +export const TileNetwork = () => new Widget.Box({ + child: Variable.derive([ + bind(AstalNetwork.get_default(), "primary"), + bind(AstalNetwork.get_default(), "wired"), + bind(AstalNetwork.get_default(), "wifi") + ], + (primary: AstalNetwork.Primary, wired: AstalNetwork.Wired, wifi: AstalNetwork.Wifi) => { + if(primary === AstalNetwork.Primary.WIFI) { + return Tile({ + title: tr("control_center.tiles.network.wireless"), + description: Variable.derive( + [ bind(wifi, "ssid"), bind(wifi, "internet") ], + (ssid: string, internet: AstalNetwork.Internet) => + ssid ? ssid : (() => { + switch(internet) { + case AstalNetwork.Internet.CONNECTED: + return tr("connected"); + case AstalNetwork.Internet.DISCONNECTED: + return tr("disconnected"); + case AstalNetwork.Internet.CONNECTING: + return tr("connecting") + "..."; + } + })() + )(), + onToggledOn: () => wifi.set_enabled(true), + onToggledOff: () => wifi.set_enabled(false), + onClickMore: () => TilesPages?.toggle(PageNetwork()), + icon: "󰤨", + iconSize: 16, + toggleState: bind(wifi, "enabled") + } as TileProps)(); + + } else if(primary === AstalNetwork.Primary.WIRED) { + return Tile({ + title: tr("control_center.tiles.network.wired") || "Wired", + description: bind(wired, "internet").as((internet: AstalNetwork.Internet) => { + switch(internet) { + case AstalNetwork.Internet.CONNECTED: + return tr("connected"); + case AstalNetwork.Internet.DISCONNECTED: + return tr("disconnected"); + case AstalNetwork.Internet.CONNECTING: + return tr("connecting") + "..."; + } + }), + onToggledOn: () => execAsync("nmcli n on"), + onToggledOff: () => execAsync("nmcli n off"), + onClickMore: () => TilesPages?.toggle(PageNetwork()), + icon: bind(wired, "internet").as((internet: AstalNetwork.Internet) => { + switch(internet) { + case AstalNetwork.Internet.CONNECTED: + return '󰛳'; + case AstalNetwork.Internet.DISCONNECTED: + return '󰲛'; + } + + return "󰛵"; + }), + iconSize: 16, + toggleState: bind(wired, "internet").as((internet: AstalNetwork.Internet) => + internet === AstalNetwork.Internet.CONNECTING + || internet === AstalNetwork.Internet.CONNECTED + ) + } as TileProps)(); + } + + return Tile({ + title: tr("control_center.tiles.network.network"), + description: tr("disconnected"), + onToggledOn: () => execAsync("nmcli n on"), + onToggledOff: () => execAsync("nmcli n off"), + onClickMore: () => TilesPages?.toggle(PageNetwork()), + icon: "󰲛", + iconSize: 16, + toggleState: bind(wired, "internet").as((internet: AstalNetwork.Internet) => + internet === AstalNetwork.Internet.CONNECTING || internet === AstalNetwork.Internet.CONNECTED) + } as TileProps)(); + })() +} as Widget.BoxProps); diff --git a/ags/widget/control-center/tiles/NightLight.ts b/ags/widget/control-center/tiles/NightLight.ts new file mode 100644 index 0000000..83d37de --- /dev/null +++ b/ags/widget/control-center/tiles/NightLight.ts @@ -0,0 +1,25 @@ +import { bind, Variable } from "astal"; +import { Tile, TileProps } from "./Tile"; +import { NightLight } from "../../../scripts/nightlight"; +import { PageNightLight } from "../pages/NightLight"; +import { tr } from "../../../i18n/intl"; +import { TilesPages } from "../Tiles"; + +export const TileNightLight = Tile({ + title: tr("control_center.tiles.night_light.title"), + icon: "󰖔", + description: Variable.derive([ + bind(NightLight.getDefault(), "temperature"), + bind(NightLight.getDefault(), "gamma") + ], (temp, gamma) => + (temp === 6000 ? tr("control_center.tiles.night_light.default_desc") + : `${temp}K`) + (gamma < NightLight.getDefault().maxGamma ? + ` (${gamma}%)` : "") + )(), + iconSize: 16, + onToggledOff: () => NightLight.getDefault().identity = true, + onToggledOn: () => NightLight.getDefault().identity = false, + enableOnClickMore: true, + onClickMore: () => TilesPages?.toggle(PageNightLight()), + toggleState: bind(NightLight.getDefault(), "identity").as(identity => !identity) +} as TileProps); diff --git a/ags/widget/control-center/tiles/Recording.ts b/ags/widget/control-center/tiles/Recording.ts new file mode 100644 index 0000000..42f745e --- /dev/null +++ b/ags/widget/control-center/tiles/Recording.ts @@ -0,0 +1,32 @@ +import { Tile, TileProps } from "./Tile"; +import { Recording } from "../../../scripts/recording"; +import { bind, Variable } from "astal"; +import { tr } from "../../../i18n/intl"; +import { getDateTime } from "../../../scripts/time"; + +export const TileRecording = Tile({ + title: tr("control_center.tiles.recording.title") || "Screen Recording", + description: Variable.derive([ + bind(Recording.getDefault(), "recording"), + getDateTime() + ], (recording, dateTime) => { + if(!recording || !Recording.getDefault().startedAt) + return tr("control_center.tiles.recording.disabled_desc") || "Start recording"; + + const startedAtSeconds = dateTime.to_unix() - Recording.getDefault().startedAt!.to_unix(); + if(startedAtSeconds <= 0) return "00:00"; + + const hours = Math.floor(startedAtSeconds / 120); + const minutes = Math.floor(startedAtSeconds / 60); + const seconds = Math.floor(startedAtSeconds % 60); + + return `${ hours > 0 ? `${hours < 10 ? `0${hours}` : hours }:` : "" + }${ minutes < 10 ? `0${minutes}` : minutes + }:${ seconds < 10 ? `0${seconds}` : seconds }`; + })(), + icon: "󰻂", + onToggledOff: () => Recording.getDefault().stopRecording(), + onToggledOn: () => Recording.getDefault().startRecording(), + toggleState: bind(Recording.getDefault(), "recording"), + iconSize: 16 +} as TileProps); diff --git a/ags/widget/control-center/tiles/Tile.ts b/ags/widget/control-center/tiles/Tile.ts new file mode 100644 index 0000000..9ea2371 --- /dev/null +++ b/ags/widget/control-center/tiles/Tile.ts @@ -0,0 +1,124 @@ +import { Binding, Variable } from "astal"; +import { Gtk, Widget } from "astal/gtk3"; +import { tr } from "../../../i18n/intl"; + +export type TileProps = { + className?: string | Binding; + 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 toggled = new Variable(props.toggleState instanceof Binding ? + (props.toggleState.get() || false) : (props.toggleState || false)); + + let subscription: () => void; + + if(props?.toggleState instanceof Binding) + subscription = props.toggleState.subscribe(val => toggled.set(val || false)); + + return () => new Widget.Box({ + className: (props.className instanceof Binding) ? + Variable.derive([ + props.className, + toggled() + ], (className, isToggled) => + `tile ${className} ${isToggled ? "toggled" : ""} ${ + props.onClickMore ? "has-more" : "" + }` + )() + : toggled().as((state: boolean) => + `tile ${state ? "toggled" : ""} ${ + props.onClickMore ? "has-more" : "" + }` + ), + expand: true, + visible: props.visible, + onDestroy: () => { + props.onDestroy?.(); + subscription?.(); + }, + children: [ + new Widget.Button({ + className: "toggle-button", + onClick: () => { + if(toggled.get()) { + toggled.set(false); + props.onToggledOff && props.onToggledOff(); + return; + } + + toggled.set(true); + props.onToggledOn && props.onToggledOn(); + }, + child: new Widget.Box({ + className: "content", + expand: true, + hexpand: true, + children: [ + new Widget.Label({ + className: "icon nf", + label: props.icon || "icon", + css: `label { font-size: ${props.iconSize || 12}px; }` + } as Widget.LabelProps), + new Widget.Box({ + className: "text", + orientation: Gtk.Orientation.VERTICAL, + vexpand: true, + hexpand: true, + valign: Gtk.Align.CENTER, + children: [ + new Widget.Label({ + className: "title", + xalign: 0, + halign: Gtk.Align.START, + truncate: true, + label: props.title + } as Widget.LabelProps), + new Widget.Label({ + className: "description", + visible: (props.description instanceof Binding) ? + props.description.as(Boolean) + : Boolean(props.description), + halign: Gtk.Align.START, + truncate: true, + xalign: 0, + label: (props.description instanceof Binding) ? + props.description.as((desc) => desc ? desc : "") + : (props.description || "") + } as Widget.LabelProps) + ] + } as Widget.BoxProps) + ] + } as Widget.BoxProps) + } as Widget.ButtonProps), + new Widget.Button({ + className: "more icon", + visible: props.onClickMore !== undefined, + halign: Gtk.Align.END, + tooltipText: tr("control_center.tiles.more") || "More", + image: new Widget.Icon({ + icon: "go-next-symbolic", + css: "icon { font-size: 16px; }" + }), + onClick: () => { + ((props.enableOnClickMore instanceof Binding) ? + props.enableOnClickMore.get() + : props.enableOnClickMore) && props?.onToggledOn(); + + props.onClickMore && props?.onClickMore() + }, + widthRequest: 32 + }) + ] + }); +} diff --git a/ags/widget/runner/ResultWidget.ts b/ags/widget/runner/ResultWidget.ts new file mode 100644 index 0000000..04800a6 --- /dev/null +++ b/ags/widget/runner/ResultWidget.ts @@ -0,0 +1,78 @@ +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 new file mode 100644 index 0000000..2336ff5 --- /dev/null +++ b/ags/window/AppsWindow.ts @@ -0,0 +1,172 @@ +import { GObject, Variable } from "astal"; +import { Astal, Gdk, Gtk, Widget } from "astal/gtk3"; +import { cleanExec, getAppIcon, getApps, getAstalApps } from "../scripts/apps"; +import AstalApps from "gi://AstalApps"; +import { PopupWindow } from "../widget/PopupWindow"; + +const { TOP, LEFT, RIGHT, BOTTOM } = Astal.WindowAnchor; + +export const AppsWindow = (mon: number): (Widget.Window) => { + const searchString = new Variable(""); + const searchSubscription = searchString.subscribe((str: string) => { + updateResults(str); + }); + + let results: Array = []; + const flowboxConnections: Array = []; + + const flowbox = new Gtk.FlowBox({ + rowSpacing: 6, + columnSpacing: 6, + homogeneous: true, + visible: true, + minChildrenPerLine: 1, + activateOnSingleClick: true + } as Gtk.FlowBox.ConstructorProps); + + const entry = new Widget.Entry({ + className: "entry", + halign: Gtk.Align.CENTER, + placeholderText: "Start typing...", + primary_icon_name: "system-search", + onChanged: (entry) => searchString.set(entry.text), + onActivate: () => flowbox.get_selected_children()?.[0]?.get_child()?.activate() + } as Widget.EntryProps); + + async function updateResults(str?: string) { + if(!str) results = getApps().sort((a, b) => + a.name > b.name ? 1 : -1); + else results = getAstalApps().fuzzy_query(str); + + // Destroy is handled by GnomeJS + flowbox.get_children().map(flowboxChild => flowbox.remove(flowboxChild)); + + results.map(app => { + flowbox.insert(AppWidget(app), -1); + + const flowboxchild = flowbox.get_child_at_index(flowbox.get_children().length-1)!.get_child(); + if(!flowboxchild) return; + + flowboxchild.set_valign(Gtk.Align.START); + flowboxchild.set_halign(Gtk.Align.START); + }); + + const firstChild = flowbox.get_child_at_index(0); + firstChild && flowbox.select_child(firstChild); + } + + function AppWidget(app: AstalApps.Application) { + const connections: Array = []; + // Astal3.Button doesn't work the way I need, so I'll use normal GtkButton + const button = new Gtk.Button({ + visible: true, + widthRequest: 180, + heightRequest: 140, + expand: false, + tooltipMarkup: `${app.name}${app.description ? + `\n${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 = () => { + cleanExec(app); + window.close(); + }; + + connections.push( + button.connect("activate", openFun), + button.connect("clicked", openFun) + ); + + button.vfunc_destroy = () => { + connections.map(id => + GObject.signal_handler_is_connected(button, id) && + button.disconnect(id) + ); + }; + + return button; + } + + const window = PopupWindow({ + namespace: "apps-window", + layer: Astal.Layer.OVERLAY, + exclusivity: Astal.Exclusivity.IGNORE, + anchor: TOP | LEFT | RIGHT | BOTTOM, + monitor: mon, + cssBackgroundWindow: "background: rgba(0, 0, 0, .2)", + marginTop: 64, + onDestroy: () => { + searchSubscription?.(); + searchString.drop(); + flowboxConnections.map(id => flowbox.disconnect(id)); + }, + onKeyPressEvent: (_, event: Gdk.Event) => { + if(event.get_keyval()[1] === Gdk.KEY_Escape) { + _.close(); + return; + } + + if(event.get_keyval()[1] !== Gdk.KEY_Right && + event.get_keyval()[1] !== Gdk.KEY_Down && + event.get_keyval()[1] !== Gdk.KEY_Up && + event.get_keyval()[1] !== Gdk.KEY_Left && + event.get_keyval()[1] !== Gdk.KEY_Return && + event.get_keyval()[1] !== Gdk.KEY_space && + event.get_keyval()[1] !== Gdk.KEY_Escape) { + !entry.isFocus && entry.grab_focus_without_selecting(); + } + }, + child: new Widget.Box({ + className: "apps-window-container", + expand: true, + orientation: Gtk.Orientation.VERTICAL, + children: [ + entry, + new Widget.Box({ + className: "apps-area", + child: new Widget.Scrollable({ + vscroll: Gtk.PolicyType.AUTOMATIC, + hscroll: Gtk.PolicyType.NEVER, + overlayScrolling: true, + expand: true, + child: flowbox + } as Widget.ScrollableProps) + } as Widget.BoxProps) + ] + } as Widget.BoxProps) + }); + + const connId = window.connect("focus-in-event", (_) => { + updateResults(); + window.disconnect(connId); + }); + + flowboxConnections.push( + flowbox.connect("child-activated", (_, item) => { + if(!item || !item.get_child()) return; + item.get_child()!.activate(); + }) + ); + + return window; +} diff --git a/ags/window/Bar.ts b/ags/window/Bar.ts new file mode 100644 index 0000000..ed796d9 --- /dev/null +++ b/ags/window/Bar.ts @@ -0,0 +1,74 @@ +import { Astal, Gtk, Widget } from "astal/gtk3"; + +import { Tray } from "../widget/bar/Tray"; +import { Workspaces } from "../widget/bar/Workspaces"; +import { FocusedClient } from "../widget/bar/FocusedClient"; +import { Media } from "../widget/bar/Media"; +import { Apps } from "../widget/bar/Apps"; +import { Clock } from "../widget/bar/Clock"; +import { Status } from "../widget/bar/Status"; +import { SpecialWorkspaces } from "../widget/bar/SpecialWorkspaces"; +import { Separator, SeparatorProps } from "../widget/Separator"; +import AstalHyprland from "gi://AstalHyprland?version=0.1"; +import { bind } from "astal"; + +export const Bar = (mon: number) => { + const widgetSpacing = 4; + return new Widget.Window({ + namespace: "top-bar", + anchor: Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT, + layer: Astal.Layer.TOP, + exclusivity: Astal.Exclusivity.EXCLUSIVE, + heightRequest: 46, + monitor: mon, + visible: true, + canFocus: false, + child: new Widget.Box({ + className: "bar-container", + child: new Widget.CenterBox({ + className: "bar-centerbox", + expand: true, + homogeneous: false, + startWidget: new Widget.Box({ + className: "widgets-left", + homogeneous: false, + halign: Gtk.Align.START, + spacing: widgetSpacing, + children: [ + Apps(), + SpecialWorkspaces(), + Separator({ + alpha: .2, + orientation: Gtk.Orientation.HORIZONTAL, + margin: 14, + visible: bind(AstalHyprland.get_default(), "workspaces").as(wss => + wss.filter(ws => ws.id < 0).length > 0) + } as SeparatorProps), + Workspaces(), + FocusedClient() + ] + } as Widget.BoxProps), + centerWidget: new Widget.Box({ + className: "widgets-center", + homogeneous: false, + spacing: widgetSpacing, + halign: Gtk.Align.CENTER, + children: [ + Clock(), + Media() + ] + } as Widget.BoxProps), + endWidget: new Widget.Box({ + className: "widgets-right", + homogeneous: false, + spacing: widgetSpacing, + halign: Gtk.Align.END, + children: [ + Tray(), + Status() + ] + } as Widget.BoxProps) + } as Widget.CenterBoxProps) + } as Widget.BoxProps) + } as Widget.WindowProps); +} diff --git a/ags/window/CenterWindow.ts b/ags/window/CenterWindow.ts new file mode 100644 index 0000000..0a81ff2 --- /dev/null +++ b/ags/window/CenterWindow.ts @@ -0,0 +1,64 @@ +import { Astal, Gtk, Widget } from "astal/gtk3"; +import { bind, GLib } from "astal"; + +import { getDateTime } from "../scripts/time"; +import { Separator, SeparatorProps } from "../widget/Separator"; +import { PopupWindow, PopupWindowProps } from "../widget/PopupWindow"; +import { BigMedia } from "../widget/center-window/BigMedia"; +import AstalMpris from "gi://AstalMpris"; + +export const CenterWindow = (mon: number) => PopupWindow({ + namespace: "center-window", + marginTop: 10, + anchor: Astal.WindowAnchor.TOP, + monitor: mon, + child: new Widget.Box({ + className: "center-window-container", + spacing: 6, + children: [ + new Widget.Box({ + className: "left", + orientation: Gtk.Orientation.VERTICAL, + children: [ + new Widget.Box({ + className: "datetime", + orientation: Gtk.Orientation.VERTICAL, + valign: Gtk.Align.START, + children: [ + new Widget.Label({ + className: "time", + label: getDateTime().as((dateTime: GLib.DateTime) => + dateTime.format("%H:%M")) + } as Widget.LabelProps), + new Widget.Label({ + className: "date", + label: getDateTime().as((dateTime: GLib.DateTime) => + dateTime.format("%A, %B %d")) + } as Widget.LabelProps) + ] + } as Widget.BoxProps), + new Widget.Box({ + className: "calendar-box", + vexpand: false, + hexpand: true, + valign: Gtk.Align.START, + child: new Gtk.Calendar({ + visible: true, + showHeading: true, + showDayNames: true, + showWeekNumbers: false + } as Gtk.Calendar.ConstructorProps) + } as Widget.BoxProps) + ] + } as Widget.BoxProps), + Separator({ + orientation: Gtk.Orientation.HORIZONTAL, + cssColor: "gray", + margin: 5, + alpha: .3, + visible: bind(AstalMpris.get_default(), "players").as(players => players.length > 0), + } as SeparatorProps), + BigMedia() + ] + } as Widget.BoxProps) +} as PopupWindowProps); diff --git a/ags/window/ControlCenter.ts b/ags/window/ControlCenter.ts new file mode 100644 index 0000000..0baac90 --- /dev/null +++ b/ags/window/ControlCenter.ts @@ -0,0 +1,37 @@ +import { Astal, Gtk, Widget } from "astal/gtk3"; +import { QuickActions } from "../widget/control-center/QuickActions"; +import { Tiles } from "../widget/control-center/Tiles"; +import { Sliders } from "../widget/control-center/Sliders"; +import { NotifHistory } from "../widget/control-center/NotifHistory"; +import { PopupWindow } from "../widget/PopupWindow"; + + +export const ControlCenter = (mon: number) => PopupWindow({ + namespace: "control-center", + className: "control-center", + exclusivity: Astal.Exclusivity.NORMAL, + anchor: Astal.WindowAnchor.TOP | Astal.WindowAnchor.RIGHT | Astal.WindowAnchor.BOTTOM, + layer: Astal.Layer.OVERLAY, + focusOnMap: true, + marginTop: 10, + marginRight: 10, + marginBottom: 10, + monitor: mon, + widthRequest: 395, + child: new Widget.Box({ + orientation: Gtk.Orientation.VERTICAL, + children: [ + new Widget.Box({ + className: "control-center-container", + orientation: Gtk.Orientation.VERTICAL, + vexpand: false, + children: [ + QuickActions(), + Sliders(), + Tiles() + ] + } as Widget.BoxProps), + NotifHistory() + ] + } as Widget.BoxProps) +} as Widget.WindowProps); diff --git a/ags/window/FloatingNotifications.ts b/ags/window/FloatingNotifications.ts new file mode 100644 index 0000000..732ed02 --- /dev/null +++ b/ags/window/FloatingNotifications.ts @@ -0,0 +1,26 @@ +import { Astal, Gtk, Widget } from "astal/gtk3"; +import { bind } from "astal/binding"; +import { Notifications } from "../scripts/notifications"; +import { NotificationWidget } from "../widget/Notification"; + + +export const FloatingNotifications = (mon: number) => new Widget.Window({ + namespace: "floating-notifications", + canFocus: false, + anchor: Astal.WindowAnchor.TOP | Astal.WindowAnchor.RIGHT, + monitor: mon, + layer: Astal.Layer.OVERLAY, + widthRequest: 450, + exclusivity: Astal.Exclusivity.NORMAL, + child: new Widget.Box({ + className: "floating-notifications-container", + orientation: Gtk.Orientation.VERTICAL, + homogeneous: false, + visible: bind(Notifications.getDefault(), "notifications").as(notifs => notifs.length > 0), + children: bind(Notifications.getDefault(), "notifications").as((notifs) => + notifs.map((item) => NotificationWidget(item, + () => Notifications.getDefault().removeNotification(item), + false, true)) + ), + } as Widget.BoxProps) +} as Widget.WindowProps); diff --git a/ags/window/LogoutMenu.ts b/ags/window/LogoutMenu.ts new file mode 100644 index 0000000..e15931d --- /dev/null +++ b/ags/window/LogoutMenu.ts @@ -0,0 +1,104 @@ +import { Astal, Gdk, Gtk, Widget } from "astal/gtk3"; +import { getDateTime } from "../scripts/time"; +import { exec, execAsync, GLib } from "astal"; +import { AskPopup } from "../widget/AskPopup"; +import { Windows } from "../windows"; + + +const { TOP, LEFT, RIGHT, BOTTOM } = Astal.WindowAnchor; + +export const LogoutMenu = (mon: number) => new Widget.Window({ + namespace: "logout-menu", + anchor: TOP | LEFT | RIGHT | BOTTOM, + layer: Astal.Layer.OVERLAY, + exclusivity: Astal.Exclusivity.IGNORE, + keymode: Astal.Keymode.EXCLUSIVE, + monitor: mon, + onKeyPressEvent: (_, event: Gdk.Event) => { + event.get_keyval()[1] === Gdk.KEY_Escape && + _.close(); + }, + child: new Widget.EventBox({ + className: "logout-menu", + onClick: () => Windows.close("logout-menu"), + child: new Widget.Box({ + expand: true, + orientation: Gtk.Orientation.VERTICAL, + children: [ + new Widget.Box({ + className: "top", + hexpand: true, + vexpand: false, + orientation: Gtk.Orientation.VERTICAL, + valign: Gtk.Align.START, + children: [ + new Widget.Label({ + className: "time", + label: getDateTime().as((dateTime: GLib.DateTime) => + dateTime.format("%H:%M")) + } as Widget.LabelProps), + new Widget.Label({ + className: "date", + label: getDateTime().as((dateTime: GLib.DateTime) => + dateTime.format("%A, %B %d %Y")) + } as Widget.LabelProps) + ] + } as Widget.BoxProps), + new Widget.Box({ + className: "button-row", + homogeneous: true, + vexpand: true, + valign: Gtk.Align.CENTER, + height_request: 360, + children: [ + new Widget.Button({ + className: "poweroff nf", + label: "󰐄", + onClick: () => AskPopup({ + title: "Power Off", + text: "Are you sure you want to power off? Unsaved work will be lost.", + onAccept: () => { + exec(`sh "${GLib.getenv("XDG_CONFIG_HOME")}/hypr/scripts/save-hyprsunset.sh"`); + execAsync("systemctl poweroff"); + } + }) + } as Widget.ButtonProps), + new Widget.Button({ + className: "reboot nf", + label: "󰜉", + onClick: () => AskPopup({ + title: "Reboot", + text: "Are you sure you want to Reboot? Unsaved work will be lost.", + onAccept: () => { + exec(`sh "${GLib.getenv("XDG_CONFIG_HOME")}/hypr/scripts/save-hyprsunset.sh"`); + execAsync("systemctl reboot"); + } + }) + } as Widget.ButtonProps), + new Widget.Button({ + className: "suspend nf", + label: "󰤄", + onClick: () => AskPopup({ + title: "Suspend", + text: "Are you sure you want to Suspend?", + onAccept: () => execAsync("systemctl suspend") + }) + } as Widget.ButtonProps), + new Widget.Button({ + className: "logout nf", + label: "󰗽", + onClick: () => AskPopup({ + title: "Log out", + text: "Are you sure you want to log out? Your session will be ended.", + onAccept: () => { + exec(`sh "${GLib.getenv("XDG_CONFIG_HOME")}/hypr/scripts/save-hyprsunset.sh"`); + execAsync(`sh -c "loginctl terminate-user ${GLib.getenv("USER") || "$USER"}"`); + } + }) + } as Widget.ButtonProps), + ] + } as Widget.BoxProps) + ] + }) + } as Widget.EventBoxProps) +} as Widget.WindowProps); diff --git a/ags/window/OSD.ts b/ags/window/OSD.ts new file mode 100644 index 0000000..07a767a --- /dev/null +++ b/ags/window/OSD.ts @@ -0,0 +1,92 @@ +import { bind, Binding, Variable } from "astal"; +import { Astal, Gtk, Widget } from "astal/gtk3"; +import { Wireplumber } from "../scripts/volume"; + +export enum OSDModes { + SINK, + BRIGHTNESS +} + +let osdMode: (Variable|null); +let osdIcon: (Binding|null); + +export function setOSDMode(newMode: OSDModes): void { + if(!osdMode) return; + + osdMode.set(newMode); +} + +export const OSD = (mon: number) => { + osdMode = new Variable(OSDModes.SINK); + osdIcon = osdMode().as((mode: OSDModes) => { + switch(mode) { + case OSDModes.SINK: return "󰕾"; + case OSDModes.BRIGHTNESS: return "󰃠"; + default: return "ó±§£"; + } + }); + + return new Widget.Window({ + namespace: "osd", + layer: Astal.Layer.OVERLAY, + anchor: Astal.WindowAnchor.BOTTOM, + canFocus: false, + marginBottom: 80, + focusOnClick: false, + clickThrough: true, + monitor: mon, + onDestroy: () => { + osdMode?.drop(); + + osdMode = null; + osdIcon = null; + }, + child: new Widget.Box({ + className: "osd", + children: [ + new Widget.Label({ + className: "icon", + label: osdIcon + } as Widget.LabelProps), + new Widget.Box({ + className: "volume", + orientation: Gtk.Orientation.VERTICAL, + valign: Gtk.Align.CENTER, + children: [ + new Widget.Label({ + className: "device", + label: bind(Wireplumber.getDefault().getDefaultSink(), "name").as((name: string) => + name || "Speaker"), + halign: Gtk.Align.CENTER + } as Widget.LabelProps), + new Widget.Box({ + vexpand: false, + expand: false, + children: [ + new Widget.LevelBar({ + className: "levelbar", + width_request: 120, + value: bind(Wireplumber.getDefault().getDefaultSink(), "volume").as((volume: number) => + Math.floor(volume * 100)), + maxValue: bind(Wireplumber.getWireplumber(), "defaultSpeaker").as(() => + Wireplumber.getDefault().getMaxSinkVolume()), + vexpand: false, + expand: false, + halign: Gtk.Align.CENTER + } as Widget.LevelBarProps), + /*new Widget.Label({ + className: "value", + label: bind(Wireplumber.getDefault().getDefaultSink(), "volume").as((volume: number) => + `${Math.floor(volume * 100)}%`), + vexpand: false, + expand: false, + halign: Gtk.Align.CENTER + } as Widget.LabelProps)*/ + ] + } as Widget.BoxProps) + ] + } as Widget.BoxProps) + ] + } as Widget.BoxProps) + } as Widget.WindowProps); +} diff --git a/ags/windows.ts b/ags/windows.ts new file mode 100644 index 0000000..c4dc413 --- /dev/null +++ b/ags/windows.ts @@ -0,0 +1,286 @@ +import { App, Widget } from "astal/gtk3"; + +import { Bar } from "./window/Bar"; +import { OSD } from "./window/OSD"; +import { ControlCenter } from "./window/ControlCenter"; +import { CenterWindow } from "./window/CenterWindow"; +import { LogoutMenu } from "./window/LogoutMenu"; +import { FloatingNotifications } from "./window/FloatingNotifications"; +import { AppsWindow } from "./window/AppsWindow"; +import AstalHyprland from "gi://AstalHyprland"; +import { GObject, property, register, signal } from "astal"; + +/** + * Windowing System + * Possible actions: getting window states(visible or not), close, open or toggle windows, + * registering windows(they are monitored through signals, and their state is changed when needed) + * Also contains util functions to create dynamic windows, opening the window only on focused + * monitor, or all available monitors! + */ +@register({ GTypeName: "Windows" }) +class WindowsClass extends GObject.Object { + #openWindows: Record> = {}; + 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/anyrun/applications.ron b/anyrun/applications.ron index 5170082..b5f8e75 100644 --- a/anyrun/applications.ron +++ b/anyrun/applications.ron @@ -1,4 +1,8 @@ Config ( max_entries: 8, - desktop_actions: false + desktop_actions: false, + terminal: Some(Terminal( + command: "kitty", + args: "-c {}" + )) ) diff --git a/apply.sh b/apply.sh index 396ac88..dddd2c1 100755 --- a/apply.sh +++ b/apply.sh @@ -1,74 +1,31 @@ #!/usr/bin/bash +source ./utils.sh + set -e -trap "printf \"\nOk! Quitting beacuse you entered an exit signal.\n\"; exit 1" SIGINT - -printf "\n" -echo "######################################" -echo "## Retrozinndev's Hyprland Dotfiles ##" -echo "######################################" -printf "\n" - -CONFIG_DIR="$HOME/.config" -DOTFILES_DIRS=("hypr" "eww" "kitty" "anyrun" "wal" "fastfetch" "mako") -DOTFILES_BACKUP_DIR="$HOME/hyprland-dotfiles-bkp" - -echo "Welcome to my dotfiles installation script!" - -# Warn user of possible problems that can happen -echo "WARN! Running this script may cause problems with your system. When continuing, you're confirming that any problem that may happen with your system is of **your** responsability." +trap "printf \"\nOk, quitting beacuse you entered an exit signal. (SIGINT).\n\"; exit 1" SIGINT +trap "printf \"\nOh noo!! Some application just told the script to end!\"; exit 2" SIGTERM function Backup_previous_dotfiles { echo -n "Would you like to make a backup of the current dotfiles? [y/n] " - read make_backup_answer + read answer printf "\n" - if [[ $make_backup_answer =~ "y" ]] - then - echo "[info] Creating backup dir in $DOTFILES_BACKUP_DIR" - - if [[ -d $DOTFILES_BACKUP_DIR ]] - then - echo "Looks like the backup directory already exists!" - echo -n "Would you like to override it with the current configuration? (Will be moved to trash) [y/n] " - read override_backup - - if [[ $override_backup = "y" ]] || [[ $override_backup = "yes" ]] - then - echo "Ok! The backup folder will be ovewritten with the current user configuration." - trash-put $DOTFILES_BACKUP_DIR - fi - else - mkdir $DOTFILES_BACKUP_DIR - fi - - # Make backup of existing configurations - for dir in ${DOTFILES_DIRS[@]}; do - if [[ -d "$CONFIG_DIR/$dir" ]] - then - echo "-> Making backup of $dir" - cp -r "$CONFIG_DIR/$dir" $DOTFILES_BACKUP_DIR - else - echo "[info] $dir backup was skipped, because it wasn't found." - fi - done - - echo "Finished backup!!" - + if [[ $answer =~ "y" ]] + then + . ./backup-dots.sh else - echo "Fine! Current settings will be overwritten, skipping backup :D" - + echo "Ok! Directories will be overwritten, skipping backup :3" fi } function Apply_wallpapers { echo -n "Would you also like to apply the wallpapers folder? :3 [y/n] " - read input_wallpaper + read answer printf "\n" - if [[ $input_wallpaper =~ "y" ]] - then - echo "Thanks for installing these wallpapers! Oh, remember that I am not the author of them!" + 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/Hyprland-Dots/WALLPAPERS.md" echo "-> Copying wallpapers to ~/wallpapers" @@ -80,48 +37,49 @@ function Apply_wallpapers { fi } -function Apply_dotfiles { - - printf "\n" +for dir in ${config_dirs[@]}; do + if ! [[ -d ./$dir ]]; then + Send_log error "$dir is in fault, or you didn't run this script in its directory!" + exit 1 + fi +done + + +# Start of actual script +Print_header + +echo "Welcome to my dotfiles installation script!" + +# Warn user of possible problems that can happen +echo "!!!WARNING!!! Running this may cause issues to your system if you don't know what you're doing! When continuing, you agree that any problem that may happen with the system is of your responsability!" + +echo -n "Do you want to run the retrozinndev/Hyprland-Dots installer? [y/n] " +read input + +if [[ $input =~ "y" ]]; then + printf "\n" Backup_previous_dotfiles printf "\n" - printf "Starting dotfiles installation...\n" + Send_log "Starting installation\n" - for dir in ${DOTFILES_DIRS[@]}; do - echo "-> Installing $dir in $CONFIG_DIR/$dir" - mkdir -p $CONFIG_DIR/$dir - cp -rf ./$dir/* $CONFIG_DIR/$dir + for dir in ${config_dirs[@]}; do + dest=$XDG_CONFIG_HOME/$dir + + echo "-> Installing $dir in $dest" + mkdir -p $dest + cp -rf ./$dir/* $dest done # Ask if user wants to apply repo's wallpapers dir Apply_wallpapers echo "Ah yes! Looks like it's ready to use, yay :3" - echo "If you find any issue, please report at: https://github.com/retrozinndev/Hyprland-Dots/issues" - echo "Thanks for using my dotfiles! I'm really happy about that :3" + echo -e "If you find any issue, please report it in: + https://github.com/retrozinndev/Hyprland-Dots/issues" + echo "Thanks for using my Hyprland-Dots! I'm really happy about that :3" printf "\n" -} - -for dir in ${DOTFILES_DIRS[@]}; do - if ! [[ -d ./$dir ]]; then - echo "[error] Looks like $dir configuration is in fault, or you didn't run this script in its directory!" - echo "[tip] If directory doesn't exist, try cloning the dotfiles again." - exit 1 - fi -done - -echo -n "Do you want to install the dotfiles? [y/n] " -read input - -if [[ $input =~ "y" ]] -then - Apply_dotfiles else printf "Ok, doing as you said! Bye bye!\n" exit 0 fi - -printf "\n" - - diff --git a/backup-dots.sh b/backup-dots.sh new file mode 100644 index 0000000..fe13e01 --- /dev/null +++ b/backup-dots.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash + +# This script backups current configuration dirs +# listed in utils.sh script. +# -------- +# Made by retrozinndev (JoĆ£o Dias) +# Licensed under the MIT License +# From: https://github.com/retrozinndev/Hyprland-Dots + +source ./utils.sh + + +bkp_dir="$HOME/hyprland-dots-bkp" + +Send_log "Creating backup in $bkp_dir" + +if [[ -d $bkp_dir ]] +then + Send_log "Found existing backup in $bkp_dir!" + echo "Looks like the backup directory already exists!" + echo -n "Would you like to move it to trash/override it? [y/n] " + read answer + + if [[ $answer =~ "y" ]]; then + echo "Fine! Previous backup is goning to be moved to trash" + trash-put $bkp_dir + else + echo "Ok! Quitting doing backup because it already exists" + exit 1 + fi +fi + +# Make backup of existing configurations +for dir in ${config_dirs[@]}; do + if [[ -d "$CONFIG_DIR/$dir" ]] + then + echo "-> backuping $dir" + cp -r "$CONFIG_DIR/$dir" $DOTFILES_BACKUP_DIR + else + echo "[info] $dir backup was skipped, because it wasn't found." + fi +done + +echo "Finished backup!!" diff --git a/eww/eww.scss b/eww/eww.scss deleted file mode 100644 index badd0e7..0000000 --- a/eww/eww.scss +++ /dev/null @@ -1,10 +0,0 @@ - -@import "../../.cache/wal/colors.scss"; - -@import "styles/general.scss"; -@import "styles/bar.scss"; -@import "styles/calendar.scss"; -@import "styles/control-center.scss"; -@import "styles/powermenu.scss"; -@import "styles/volume-control.scss"; -@import "styles/floating-notifications.scss"; diff --git a/eww/eww.yuck b/eww/eww.yuck deleted file mode 100644 index 97a6190..0000000 --- a/eww/eww.yuck +++ /dev/null @@ -1,17 +0,0 @@ -; Variables -(include "variables.yuck") - -; Windows -(include "windows/calendar-window.yuck") -(include "windows/control-center.yuck") -(include "windows/bar.yuck") -(include "windows/powermenu.yuck") -(include "windows/volume-control.yuck") -(include "windows/volume-popup.yuck") -(include "windows/floating-media.yuck") -(include "windows/floating-notifications.yuck") -(include "windows/hardware-monitor.yuck") - -; Universal Widgets -(include "widgets/big-media.yuck") -(include "widgets/separator.yuck") diff --git a/eww/scripts/active-window.sh b/eww/scripts/active-window.sh deleted file mode 100644 index 1c55066..0000000 --- a/eww/scripts/active-window.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - -# output current window before event trigger to prevent issues -hyprctl -j activewindow | jq -c | sed 's/\\[n]//g' - -handle() { - case $1 in - activewindow*) hyprctl -j activewindow | jq -c | sed 's/\\[n]//g' ;; - esac -} - -socat -U - UNIX-CONNECT:$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock | while read -r line; do handle "$line"; done diff --git a/eww/scripts/eww-reload.sh b/eww/scripts/eww-reload.sh deleted file mode 100644 index f6bf4d3..0000000 --- a/eww/scripts/eww-reload.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash - -# This script reloads eww configuration and updates -# window status variables, used in eww-window.sh -# script. Avoids issues with widgets that use status -# variables to dinamically change their content. -# ---------- -# Licensed under the MIT License -# Made by retrozinndev (JoĆ£o Dias) -# From https://github.com/retrozinndev/Hyprland-Dots - -# TODO - -open_windows=$(eww active-windows | awk -F: '{ print $1 }' | sed -e 's/ /\\[n]/g') -echo $open_windows - -#for window in $() diff --git a/eww/scripts/eww-window.sh b/eww/scripts/eww-window.sh deleted file mode 100644 index 96e982e..0000000 --- a/eww/scripts/eww-window.sh +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env bash - -# arg1($1) should be one of the options listed in help command; -# arg2($2) should be the literal window name as defined in eww configuration. - -function help_message() { - printf \ -"This is a helper script that helps opening/closing eww windows on -retrozinndev's Hyprland Dots, by checking if the window is already -open/closed before doing anything, also changes eww state variables. -This needs a variable like window_state_WINDOWNAME to work, where -WINDOWNAME is the literal name of the declared window inside the -eww configuration you are using. - -Usage: - arg1 arg2 - ./eww-window.sh [SINGLE OPTION] [WINDOW_NAME] - -Options: - -h, --help, help: Shows this help message; - --open, open: Opens a window and changes its state variable to - \"open\" if not open, or else does nothing; - --toggle, toggle: Toggles a window. If open, the specified - window is closed, if closed, the - specified window is open. Same thing goes to - the eww window state variable; - --close, close: Closes a window and also changes its state - variable to \"closed\" if open, or else does - nothing. - - -Developer: retrozinndev (JoĆ£o Dias), https://github.com/retrozinndev -Issue tracker: https://github.com/retrozinndev/Hyprland-Dots/issues -Licensed under the MIT License, as in retrozinndev's Hyprland-Dots repo." -} - -function send_log() { - case "$1" in - err*) - tag_color="\e[31m" - ;; - warn*) - tag_color="\e[33m" - ;; - *) - tag_color="\e[34m" - ;; - esac - - echo -e "[$tag_color$1\e[0m] $2" -} - -function check_if_empty() { - if [[ $1 == "" ]]; then - send_log "error" "argument \$1 is empty!" - help_message - exit 1 - fi -} - -function toggle_eww_window() { - if ! [[ $(eww active-windows) =~ "$1" ]]; then - eww open "$1" - eww update "window_state_$1=open" - else - eww close "$1" - eww update "window_state_$1=closed" - fi -} - -case "$1" in - --help | -h | help | h) - help_message - ;; - - --open | open) - check_if_empty $2 "WINDOW_NAME" - if ! [[ $(eww active-windows) =~ "$2" ]]; then - eww open "$2" - eww update "window_state_$2=open" - else - send_log "info" "Window '$2' is already open, ignored." - fi - ;; - - --close | close) - check_if_empty $2 "WINDOW_NAME" - if [[ $(eww active-windows) =~ "$2" ]]; then - eww close "$2" - eww update "window_state_$2=closed" - else - send_log "info" "Window '$2' is already closed, ignored." - fi - ;; - - --toggle | toggle) - check_if_empty $2 "WINDOW_NAME" - toggle_eww_window $2 - ;; - *) - send_log "error" "Action not specified or incorrect command order. Good example: \`./eww-window.sh open bar\`" - help_message - ;; -esac diff --git a/eww/scripts/get-volume-watch.sh b/eww/scripts/get-volume-watch.sh deleted file mode 100644 index 844ab6f..0000000 --- a/eww/scripts/get-volume-watch.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash - -sink_="@DEFAULT_AUDIO_SINK@" -source_="@DEFAULT_AUDIO_SOURCE@" - -print_json() { - echo "{ \"output\": $output_vol, \"source\": $source_vol }" -} - -get_vol() { - echo $(wpctl get-volume $1 | awk "{print int(\$2*100)}") -} - -output_vol=$(get_vol $sink_) -source_vol=$(get_vol $source_) - -print_json - -# Loop -pactl subscribe | grep --line-buffered -e "on sink" -e "on source" | while read -r; do - - output_vol=$(get_vol $sink_) - source_vol=$(get_vol $source_) - - print_json -done diff --git a/eww/scripts/mediaplayer.py b/eww/scripts/mediaplayer.py deleted file mode 100755 index 038b886..0000000 --- a/eww/scripts/mediaplayer.py +++ /dev/null @@ -1,205 +0,0 @@ -#!/usr/bin/env python3 - -# Original code is from Alexays/Waybar repo in https://github.com/Alexays/Waybar -# Modified by retrozinndev to work on Eww and more players. -# Dotfiles: https://github.com/retrozinndev/Hyprland-Dots - -import gi -gi.require_version("Playerctl", "2.0") -from gi.repository import Playerctl, GLib -from gi.repository.Playerctl import Player -import argparse -import logging -import sys -import signal -import gi -import json -import os -from typing import List - -logger = logging.getLogger(__name__) - -def signal_handler(sig, frame): - logger.info("Received signal to stop, exiting") - sys.stdout.write("\n") - sys.stdout.flush() - # loop.quit() - sys.exit(0) - - -class PlayerManager: - def __init__(self, selected_player=None, excluded_player=[]): - self.manager = Playerctl.PlayerManager() - self.loop = GLib.MainLoop() - self.manager.connect( - "name-appeared", lambda *args: self.on_player_appeared(*args)) - self.manager.connect( - "player-vanished", lambda *args: self.on_player_vanished(*args)) - - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) - signal.signal(signal.SIGPIPE, signal.SIG_DFL) - self.selected_player = selected_player - self.excluded_player = excluded_player.split(',') if excluded_player else [] - - self.init_players() - - def init_players(self): - for player in self.manager.props.player_names: - if player.name in self.excluded_player: - continue - if self.selected_player is not None and self.selected_player != player.name: - logger.debug(f"{player.name} is not the filtered player, skipping it") - continue - self.init_player(player) - - def run(self): - logger.info("Starting main loop") - self.loop.run() - - def init_player(self, player): - logger.info(f"Initialize new player: {player.name}") - player = Playerctl.Player.new_from_name(player) - player.connect("playback-status", - self.on_playback_status_changed, None) - player.connect("metadata", self.on_metadata_changed, None) - self.manager.manage_player(player) - self.on_metadata_changed(player, player.props.metadata) - - def get_players(self) -> List[Player]: - return self.manager.props.players - - def clear_output(self): - sys.stdout.write("\n") - sys.stdout.flush() - - def on_playback_status_changed(self, player, status, _=None): - logger.debug(f"Playback status changed for player {player.props.player_name}: {status}") - self.on_metadata_changed(player, player.props.metadata) - - def get_first_playing_player(self): - players = self.get_players() - logger.debug(f"Getting first playing player from {len(players)} players") - if len(players) > 0: - # if any are playing, show the first one that is playing - # reverse order, so that the most recently added ones are preferred - for player in players[::-1]: - if player.props.status == "Playing": - return player - # if none are playing, show the first one - return players[0] - else: - logger.debug("No players found") - return None - - def show_most_important_player(self): - logger.debug("Showing most important player") - # show the currently playing player - # or else show the first paused player - # or else show nothing - current_player = self.get_first_playing_player() - if current_player is not None: - self.on_metadata_changed(current_player, current_player.props.metadata) - else: - self.clear_output() - - def on_metadata_changed(self, player, metadata, _=None): - logger.debug(f"Metadata changed for player {player.props.player_name}") - player_name = player.props.player_name - artist = player.get_artist() - title = player.get_title() - - track_info = "" - if ((player_name == "spotify") and "mpris:trackid" in metadata.keys() and ":ad:" in player.props.metadata["mpris:trackid"]): - track_info = "Advertisement" - elif artist != None and title != None: - track_info = f"{artist} - {title}" - else: - track_info = title - - # only print output if no other player is playing - current_playing = self.get_first_playing_player() - if current_playing is None or current_playing.props.player_name == player.props.player_name: - self.write_output(track_info, player) - else: - logger.debug(f"Other player {current_playing.props.player_name} is playing, skipping") - - def write_output(self, text, player): - logger.debug(f"Writing output: {text}") - - artUrl = player.print_metadata_prop("mpris:artUrl") - - if artUrl is not None and not artUrl.startswith("file://") and not artUrl.startswith("http://") and not artUrl.startswith("https://") and artUrl != "null": - artUrl = f"file://{artUrl}" - - output = { - "status": player.props.status.lower(), - "title": player.get_title(), - "artist": player.get_artist(), - "player": player.props.player_name.lower(), - "artUrl": artUrl, - "length": player.print_metadata_prop("mpris:length"), - "url": player.print_metadata_prop("xesam:url") - } - - sys.stdout.write(json.dumps(output) + "\n") - sys.stdout.flush() - - def on_player_appeared(self, _, player): - logger.info(f"Player has appeared: {player.name}") - if player.name in self.excluded_player: - logger.debug( - "New player appeared, but it's in exclude player list, skipping") - return - if player is not None and (self.selected_player is None or player.name == self.selected_player): - self.init_player(player) - else: - logger.debug( - "New player appeared, but it's not the selected player, skipping") - - def on_player_vanished(self, _, player): - logger.info(f"Player {player.props.player_name} has vanished") - self.show_most_important_player() - -def parse_arguments(): - parser = argparse.ArgumentParser() - - # Increase verbosity with every occurrence of -v - parser.add_argument("-v", "--verbose", action="count", default=0) - - parser.add_argument("-x", "--exclude", "- Comma-separated list of excluded player") - - # Define for which player we"re listening - parser.add_argument("--player") - - parser.add_argument("--enable-logging", action="store_true") - - return parser.parse_args() - - -def main(): - arguments = parse_arguments() - - # Initialize logging - if arguments.enable_logging: - logfile = os.path.join(os.path.dirname( - os.path.realpath(__file__)), "media-player.log") - logging.basicConfig(filename=logfile, level=logging.DEBUG, - format="%(asctime)s %(name)s %(levelname)s:%(lineno)d %(message)s") - - # Logging is set by default to WARN and higher. - # With every occurrence of -v it's lowered by one - logger.setLevel(max((3 - arguments.verbose) * 10, 0)) - - logger.info("Creating player manager") - if arguments.player: - logger.info(f"Filtering for player: {arguments.player}") - if arguments.exclude: - logger.info(f"Exclude player {arguments.exclude}") - - player = PlayerManager(arguments.player, arguments.exclude) - player.run() - - -if __name__ == "__main__": - main() diff --git a/eww/scripts/night-light.sh b/eww/scripts/night-light.sh deleted file mode 100644 index e4e546f..0000000 --- a/eww/scripts/night-light.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash -set -e - -pidfile="$HOME/.cache/night-light.pid" -temperature=4500 # in K - -# run only if pid file does not exist -if ! [[ -f $pidfile ]]; then - touch $pidfile - hyprsunset -t $temperature & - _pid=$! - echo -e $_pid > $pidfile - wait $_pid && rm -f $pidfile -else - echo "There's already an instance running! Mistake? Delete \"~/.cache/night-light.pid\"." - exit 1 -fi diff --git a/eww/scripts/notification-clear.sh b/eww/scripts/notification-clear.sh deleted file mode 100644 index 6f604f1..0000000 --- a/eww/scripts/notification-clear.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -lockfile=$HOME/.cache/notification-history.lock -json_history_file=$HOME/.cache/notification-history.json - -if ! [[ -f $lockfile ]]; then - echo "{ \"history\": [] }" > $json_history_file -fi diff --git a/eww/scripts/notification-handler.sh b/eww/scripts/notification-handler.sh deleted file mode 100644 index 67e6994..0000000 --- a/eww/scripts/notification-handler.sh +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env bash - -# A notification logger, saves notifications inside -# a file instead of saving on memory! file is located -# in `~/.cache/notification-history.json`. -# ----------- -# Licensed under the MIT License -# Made by retrozinndev (JoĆ£o Dias) -# From https://github.com/retrozinndev/Hyprland-Dots - -set -e - -json_history_file="$HOME/.cache/notification-history.json" -lock_file="$HOME/.cache/notification-history.lock" - -# get max entries from mako file -max_entries=$(cat $HOME/.config/mako/config | grep "max-history=" | awk -F= '{ print $2 }') - -touch $json_history_file - -function Init_history_file() { - if [[ $(cat $json_history_file) == "" ]]; then - echo -e "{\"history\":[]}" > $json_history_file - fi -} - -function Check_history_file() { - if ! [[ -f $json_history_file ]]; then - touch $json_history_file - fi -} - -function Treat_specials() { - echo $@ | sed -e 's/\\[n]/\\n/g' -e 's/&/&/g' -} - -Check_history_file -Init_history_file - -json_latest_notification="$(makoctl history | jq -c '.data[][0]')" - -while true; do - sleep .1 - if ! [[ -f $lock_file ]]; then - json_actual_latest="$(makoctl history | jq -c '.data[][0]')" - if ! [[ $json_actual_latest == $json_latest_notification ]]; then - if [[ $(echo $(Treat_specials $json_actual_latest) | jq -c '.id.data') == $(echo $(Treat_specials $json_latest_notification) | jq -c '.id.data') ]]; then - if [[ $(echo $(Treat_specials $json_actual_latest) | jq -c '.summary.data') == $(echo $(Treat_specials $json_latest_notification) | jq -c '.summary.data') ]]; then - continue - else - sh $HOME/.config/eww/scripts/notification-remove.sh $(echo $(Treat_specials $json_latest_notification) | jq -c '.id.data') & - fi - fi - - Check_history_file - Init_history_file - json_latest_notification=$(makoctl history | jq -c '.data[][0]') - json_first_notification=$(jq -c ".history[$(jq -c '.history | length - 1' $json_history_file)]" $json_history_file) - - if ! [[ $(makoctl mode) =~ "dnd" ]]; then - sh $HOME/.config/eww/scripts/notification-popup.sh "$(Treat_specials $json_latest_notification)" & - fi - - if [[ $(jq -c ".history | length" $json_history_file) == $max_entries ]]; then - sh $HOME/.config/eww/scripts/notification-remove.sh $(echo $json_first_notification | jq -c '.id.data') - fi - - json_output=$(jq -c ".history |= [$(Treat_specials $json_latest_notification)] + ." $json_history_file) - - echo -e "$json_output" > $json_history_file - fi - fi -done diff --git a/eww/scripts/notification-popup-remove.sh b/eww/scripts/notification-popup-remove.sh deleted file mode 100644 index 0b5d3ba..0000000 --- a/eww/scripts/notification-popup-remove.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env bash - -# Removes notification from popup by id provided -# in arg1. -# --------- -# Licensed under the MIT License -# Made by retrozinndev (JoĆ£o Dias) -# From https://github.com/retrozinndev/Hyprland-Dots - - -if ! [[ $1 == "" ]]; then - json_popup_notifications=$(eww get json_popup_notifications) - if [[ $(eww get json_popup_notifications | jq -c '.notifications | length') == 1 ]]; then - sh $HOME/.config/eww/scripts/eww-window.sh close floating-notifications >> /dev/null - fi - eww update "json_popup_notifications=$(echo $json_popup_notifications | jq -c "del(.notifications[] | select(.id.data == $1))")" - exit 0 -fi - -echo "[error] Notification Id not provided!" -exit 1 diff --git a/eww/scripts/notification-popup.sh b/eww/scripts/notification-popup.sh deleted file mode 100644 index 0888cde..0000000 --- a/eww/scripts/notification-popup.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env bash - -# This scripts receives a notification from arg1, -# in a json format, displays in eww, counting -# down depending on notification urgency and -# removes the notification from json. - -# Timeout in seconds -# When set to 0, notification will disappear -# only when hovering it. -timeout_low=3 -timeout_normal=6 -timeout_critical=0 - -if ! [[ $@ == "" ]]; then - # Urgency levels: - # Low: 0, Normal: 1, Critical: 2 - urgency=$(echo $@ | jq -c ".urgency.data") - id=$(echo $@ | jq -c ".id.data") - json_popup_notifications="$(eww get json_popup_notifications)" - json_notification=$(echo $@ | jq -c '.') - - if [[ $json_popup_notifications == "" ]]; then - eww update "json_popup_notifications="'{"notifications":[]}' >> /dev/null - json_popup_notifications='{"notifications":[]}' - fi - - eww update "json_popup_notifications=$(echo $json_popup_notifications | jq -c ".notifications |= [$json_notification] + .")" >> /dev/null - sh $HOME/.config/eww/scripts/eww-window.sh open floating-notifications >> /dev/null - - # Critical urgency is handled by eww, no need to count down - case $urgency in - 0) - sleep $timeout_low - ;; - 1) - sleep $timeout_normal - ;; - 2) ;; - *) - sleep $timeout_normal - ;; - esac - - if ! [[ $urgency == 2 ]]; then - sh $HOME/.config/eww/scripts/notification-popup-remove.sh "$id" & - fi -fi - - -exit 0 diff --git a/eww/scripts/notification-remove.sh b/eww/scripts/notification-remove.sh deleted file mode 100644 index b2f1ac3..0000000 --- a/eww/scripts/notification-remove.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env bash - -# Removes notification from history file by -# id provided in arg1. -# --------- -# Licensed under the MIT License -# Made by retrozinndev (JoĆ£o Dias) -# From https://github.com/retrozinndev/Hyprland-Dots - -json_history_file="$HOME/.cache/notification-history.json" -lock_file="$HOME/.cache/notification-history.lock" - -if [[ -f $HOME/.cache/notification-history.json ]] && ! [[ $1 == "" ]]; then - touch $lock_file - json_updated_history=$(jq -c "del(.history[] | select(.id.data == $1))" $json_history_file) - echo -e $json_updated_history > $json_history_file - rm -f $lock_file -else - echo "[error] Notification history not reachable or id not provided" - exit 1 -fi - -exit 0 diff --git a/eww/scripts/notification-watch.sh b/eww/scripts/notification-watch.sh deleted file mode 100644 index 1bf7e81..0000000 --- a/eww/scripts/notification-watch.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash - -# This script watches notification file changes -# and output file contents. -# ----------- -# Licensed under the MIT License -# Made by retrozinndev (JoĆ£o Dias) -# From https://github.com/retrozinndev/Hyprland-Dots - -set -e - -json_history_file="$HOME/.cache/notification-history.json" - -if ! [[ -f $json_history_file ]]; then - echo -e "{\"history\":[]}" > $json_history_file -fi - -json_history="$(cat $json_history_file)" - -echo $json_history - -while true; do - sleep .1 - json_newest_history=$(cat $json_history_file) - - if ! [[ "$json_history" == "$json_newest_history" ]]; then - json_history="$json_newest_history" - echo $json_history - fi -done - -exit 0 diff --git a/eww/scripts/screen-recording.sh b/eww/scripts/screen-recording.sh deleted file mode 100644 index eadc5a8..0000000 --- a/eww/scripts/screen-recording.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -set -e - -dest="$HOME/Recordings" -if ! [[ $(xdg-user-dir VIDEOS) == "" ]]; then - dest="$(xdg-user-dir VIDEOS)/Recordings" -fi -lock="$HOME/.cache/recording.lock" -pidfile="$HOME/.cache/recording.pid" -ext="mp4" # mp4,mkv... -filename=$(date +"%Y-%m-%d-%H%M%S_rec.$ext") - -mkdir -p $dest - -# run only if lockfile does not exist -if ! [[ -f $lock ]]; then - touch $lock - wf-recorder -f "$dest/$filename" & - rec_pid=$! - echo -e $rec_pid > $pidfile - wait $rec_pid && ( - notify-send -a "Screen Recorder" "Recording Done" "The screen recording has been saved as '$dest/$filename'!" - rm -f $lock $pidfile - ) -else - notify-send -a "Screen Recorder" "Recording Error" "There's already an instance running! Mistake? Delete $lock and $pidfile." - exit 1 -fi diff --git a/eww/scripts/volume-popup-trigger.sh b/eww/scripts/volume-popup-trigger.sh deleted file mode 100644 index a75d8ec..0000000 --- a/eww/scripts/volume-popup-trigger.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash - -while true; do - if [[ $(eww get hold_volume_popup) == true ]]; then - sleep 4 - sh ./eww-window.sh close volume-popup - eww update "hold_volume_popup=false" - else - break - fi -done - -exit 0 diff --git a/eww/scripts/workspaces.sh b/eww/scripts/workspaces.sh deleted file mode 100644 index 789751d..0000000 --- a/eww/scripts/workspaces.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env bash - -print_workspaces_literal() { - active_workspace_id=$(hyprctl -j activeworkspace | jq -c '.id' | xargs) - json_workspaces=$(hyprctl -j workspaces | jq -c '.') - existing_workspaces=$(echo $json_workspaces | jq -c '.[].id' | xargs) - - output=" - (box :space-evenly false - :orientation \"horizontal\"" - - for i in {1..10}; do - output=$output" - (button :onclick \"hyprctl dispatch workspace $i >> /dev/null \" - :class \"\${ $active_workspace_id == $i ? 'active' : '' } $( [[ $(echo $json_workspaces | jq -c ".[$i - 1].name") =~ "special:" ]] && echo 'special' )\" - :visible { \"$existing_workspaces\" =~ $i ? true : false } - :tooltip \"Workspace ${i}\" - \"\")" - - if [ $i == 10 ]; then - output=$output")" # closes box if last - fi - done - - echo "$(echo $output | xargs -0)" -} - -# display workspaces on startup -print_workspaces_literal - -handle() { - case $1 in - workspace*) print_workspaces_literal;; - esac -} - -socat -U - UNIX-CONNECT:$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock | while read -r line; do handle "$line"; done diff --git a/eww/styles/bar.scss b/eww/styles/bar.scss deleted file mode 100644 index 4046945..0000000 --- a/eww/styles/bar.scss +++ /dev/null @@ -1,271 +0,0 @@ - -.bar { - background: transparent; - padding: 4px; - padding-bottom: 0; - - & label { - font-family:"Cantarell"; - font-weight: 500; - } -} - -/* Styles .widgets-[left/center/right] */ -.widgets-left, -.widgets-center, -.widgets-right { - background: rgba($background, 0.6); - border-radius: 18px; - padding: 6px; - - & > * { - margin: 0 3px; - border-radius: 12px; - &.button { - margin: 0; - & > box { - margin: 0 3px; - } - } - } - - & > *:first-child { - margin-left: 0; - } - - & > *:last-child { - margin-right: 0; - } - - & > *:only-child { - margin: 0 0px; - } -} - -// Styles the literal script for workspace indicators -.workspaces { - border-radius: 50%; - - & button { - border-radius: 16px; - margin: 0 2px; - padding: 5px 12px; - background: lighten($color: $color1, $amount: 10); - - &:not(.active):hover { - box-shadow: inset 0 0 0 50px rgba($color: $foreground, $alpha: .5); - } - - &.active { - padding-right: 22px; - padding-left: 22px; - background: $foreground; - } - - &.special { - background: darken($color: $color3, $amount: 5); - } - } -} - -.distro-logo { - padding: 0; - padding-left: 12px; - padding-right: 6px; - - & > label { - font-size: 15px; - padding-right: 2px; - } -} - -.systray { - all: unset; - - & > * > * { - margin: 0 6px; - } -} - -.mediaplayer { - border-radius: 12px; - - & > .media { - border-radius: inherit; - - &:hover { - & > box { - box-shadow: inset 0 0 0 50px rgba($color: $foreground, $alpha: .1); - } - } - & > box { - border-radius: inherit; - background: darken($color: $color3, $amount: 5); - - & > label { - color: $foreground; - font-size: inherit; - - &:first-child { - margin-left: 8px; - } - - &:last-child { - margin-right: 8px; - } - } - } - } - - &.revealed { - transition: 80ms linear; - - .media > box { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } - - .media-controls { - padding: 3px; - background: darken($color: $color3, $amount: 10); - border-top-right-radius: 12px; - border-bottom-right-radius: 12px; - - & button { - background: $color2; - margin: 0 1px; - border-radius: 2px; - - &:hover { - background: lighten($color: $color2, $amount: 5); - } - - &:first-child { - border-top-left-radius: 8px; - border-bottom-left-radius: 8px; - margin-left: 0; - } - - &:last-child { - border-top-right-radius: 8px; - border-bottom-right-radius: 8px; - margin-right: 0; - } - } - } - } - - button { - padding: 0 7px; - border-radius: 0px; - } -} - -.window { - padding: 0 6px; - - & > .icon { - margin-right: 6px; - } - - & > box.info { - & > label.window-class { - font-size: 9.8px; - font-family: monospace; - color: darken($color: $foreground, $amount: 20); - margin-top: -1px; - } - - & > label.window-title { - font-size: 12px; - color: $foreground; - margin-top: -1px; - } - } -} - -.network-eventbox > box { - padding-left: 10px; - padding-right: 7px; -} - -.bluetooth button { - padding-left: 10px; - padding-right: 7px; -} - -.audio-eventbox { - &:hover box.audio { - background: darken($color: $color2, $amount: 5); - } - - & > .audio { - padding: 0 8px; - border-radius: 12px; - - &.open { - background: darken($color: $color3, $amount: 10); - } - - label { - font-weight: 600; - - &:first-child { - margin-right: 2px; - } - - &:last-child { - margin-left: 2px; - } - } - } -} - -.clock { - & .cal-open > box { - background: darken($color: $color3, $amount: 10); - } - - & .icon { - margin-right: 6px; - } - - & label:not(.icon) { - margin-right: 3px; - } -} - -.control-center-toggle { - border-radius: 12px; - - &:hover { - background: darken($color: $color2, $amount: 10); - } - - & > box { - padding-left: 12px; - padding-right: 14px; - - label { - font-size: 12px; - } - } - - &.open { - background: darken($color: $color3, $amount: 10); - } -} - -.hardware { - & > box { - margin: 0 5px; - - &:first-child { - margin-left: 0; - } - - &:last-child { - margin-right: 0; - } - } -} diff --git a/eww/styles/calendar.scss b/eww/styles/calendar.scss deleted file mode 100644 index 005f00b..0000000 --- a/eww/styles/calendar.scss +++ /dev/null @@ -1,41 +0,0 @@ - -.calendar-box { - padding: 10px; - background: rgba($background, 0.6); - border-radius: 16px; - - & > .time { - font-size: 42px; - font-weight: 700; - } - - & > .date { - font-size: 12px; - font-weight: 600; - margin-bottom: 8px; - } - - .month-calendar { - border-radius: 12px; - padding: 0 5px; - padding-top: 10px; - font-weight: 500; - - & > .header { - background: unset; - border-radius: 6px; - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; - border-bottom: solid .5px gray; - } - - &:hover { - background: $color2; - } - - // Selected day / Current day - &:selected { - border: 1px solid $color2; - } - } -} diff --git a/eww/styles/control-center.scss b/eww/styles/control-center.scss deleted file mode 100644 index c01eb11..0000000 --- a/eww/styles/control-center.scss +++ /dev/null @@ -1,202 +0,0 @@ -box.cc { - margin: 12px; - margin-right: 0; - background: rgba($background, .85); - border-top-left-radius: 24px; - border-bottom-left-radius: 24px; - padding: 18px; - box-shadow: 0 3px 4px 1px $background; -} - -.top-bar { - margin-bottom: 15px; - - & label { - &.hostname { - font-size: 13.3px; - font-weight: 700; - border-bottom: .5px solid rgba(darken($color: $foreground, $amount: 15), .5); - padding-bottom: 4px; - } - - &.uptime { - font-size: 11.7px; - font-weight: 500; - } - } - - .button-row { - margin: 7px 0; - } - - .wallpaper { - padding-left: 9px; - padding-right: 12px; - } - - .screenshot { - padding-left: 9px; - padding-right: 12px; - } - - .color-picker { - padding-right: 11px; - } - - .powermenu { - padding-right: 11px; - } -} - -.cc-notifications { - &.scroll { - .notifications { - box.notification { - background: darken($color: $color1, $amount: 10); - border-radius: 16px; - margin: 4px 0; - - & > .top { - border-top-left-radius: inherit; - border-top-right-radius: inherit; - background: darken($color: $color1, $amount: 7); - border-bottom: .5px solid rgba(darken($color: $foreground, $amount: 15), .3); - padding: 5px 6px; - - & > .app-info { - & .app-icon { - margin-right: 4px; - } - font-weight: 600; - } - - .close { - padding: 4px 8px; - border-radius: 9px; - } - } - - & .content { - padding: 8px; - padding-top: 0px; - - & > .image { - border-radius: 16px; - margin-right: 6px; - margin-top: 6px; - background-size: cover; - background-repeat: no-repeat; - background-position: center 0; - } - - & .text { - & label { - font-family: "Cantarell", "Noto Sans", "Noto Sans CJK JP", "Noto Sans CJK KR"; - } - - & label.summary { - font-weight: 600; - font-size: 18px; - margin-top: 6px; - } - - & label.body { - font-size: 13.1px; - margin-bottom: 2px; - } - } - } - } - } - } - - & .bottom { - margin-top: 4px; - - &.button-row { - & button.do-not-disturb { - padding-right: 12px; - } - } - } -} - -.tiles-grid { - margin-bottom: 10px; -} - -.tiles { - margin-bottom: 2px; - - & > .tile-checkbox:first-child { - margin-left: 0; - } - - & > .tile-checkbox:last-child { - margin-right: 0; - } - - & .tile-checkbox { - margin: 2px; - - &:hover { - & > box { - background: $color3; - } - &:checked > box { - background: $color2; - } - } - - &:checked { - & > box { - background: $color2; - } - } - - & > *:not(box) { - color: transparent; - } - - & > .tile { - background: darken($color: $foreground, $amount: 65); - border-radius: 16px; - padding: 16px; - margin-left: -16px; // This covers the checkbox space, hiding the check thing - - .icon { - font-size: 15px; - padding-right: 5px; - } - - .label { - font-size: 12.6px; - font-weight: 600; - } - } - } - - & .network { - .icon { - margin-right: 5px; - } - } - - & .dnd { - .icon { - margin-right: 4px; - } - } - - & .screen-rec { - .icon { - margin-right: 4px; - } - } - - & .night-light { - .icon { - margin-right: 4px; - } - } -} diff --git a/eww/styles/floating-notifications.scss b/eww/styles/floating-notifications.scss deleted file mode 100644 index fd60d7e..0000000 --- a/eww/styles/floating-notifications.scss +++ /dev/null @@ -1,76 +0,0 @@ -.floating-notifications { - & > .notifications .floating-notification { - $background-color: darken($color: $color1, $amount: 40); - background: $background-color; - border-radius: 16px; - margin: 14px; - box-shadow: 0 0 8px 2px rgba($background, .9); - - & > .top { - border-top-left-radius: 15.4px; - border-top-right-radius: 15.4px; - padding: 6px 12px; - background: lighten($color: $background-color, $amount: 5); - border-bottom: .5px solid rgba($foreground, .2); - - label { - font-family: "Noto Sans Mono", monospace; - font-size: 11px; - } - } - - .content { - padding: 10px; - - .image { - background-position: center; - background-size: cover; - margin-right: 6px; - border-radius: 12px; - } - - .text-content { - - label.summary { - font-size: 16px; - font-weight: 700; - } - - label.body { - font-size: 13.3px; - } - - padding-bottom: 2px; - } - } - - } - - & > .bottom { - padding: 8px; - margin: 0 8px; - - & .tip { - background: darken($color: $color1, $amount: 40); - border-radius: 12px; - padding: 8px 12px; - box-shadow: 0 0 0px 1px $background; - - & > .icon { - margin-right: 4px; - font-size: 16px; - } - } - - & > .right { - background: darken($color: $color1, $amount: 40); - border-radius: 12px; - box-shadow: 0 0 0px 1px $background; - - & .icon { - margin-right: 4px; - font-size: 16px; - } - } - } -} diff --git a/eww/styles/general.scss b/eww/styles/general.scss deleted file mode 100644 index 11fbfae..0000000 --- a/eww/styles/general.scss +++ /dev/null @@ -1,230 +0,0 @@ - -$foreground: lighten($color: $foreground, $amount: 5); - -* { - all: unset; - transition: 120ms linear; - font-size: 12px; -} - -label { - color: lighten($foreground, 15) -} - -box.button-row { - $bg-color: darken($color: $foreground, $amount: 25); - - & > button { - background: rgba($bg-color, .8); - border-radius: 1px; - margin: 0 1px; - - &:first-child { - border-top-left-radius: 8px; - border-bottom-left-radius: 8px; - } - - &:last-child { - border-top-right-radius: 8px; - border-bottom-right-radius: 8px; - } - - &:hover { - background: $bg-color; - } - } -} - -box.vertical.button-row { - padding: 2px; - $bg-color: darken($color: $foreground, $amount: 25); - - & > button { - background: rgba($bg-color, .8); - border-radius: 2px; - margin: 1px 0; - - &:first-child { - border-top-left-radius: 8px; - border-top-right-radius: 8px; - } - - &:last-child { - border-bottom-left-radius: 8px; - border-bottom-right-radius: 8px; - } - - &:hover { - background: $bg-color; - } - } -} - -button, -.button > box { - padding: 6px 10px; - border-radius: 12px; - background: none; -} - -button:hover, -.button:hover > box { - background: darken($color: $color2, $amount: 5); -} - -button:active, -.button:active > box { - background: darken($color: $color3, $amount: 10); -} - -button label { - color: $foreground; -} - -menu { - background: $background; - border-radius: 14px; - padding: 2px; - - & > menuitem { - padding: 8px 24px; - border-radius: 4px; - - &:hover { - background: $color1; - } - - &:first-child { - border-top-left-radius: 12px; - border-top-right-radius: 12px; - } - - &:last-child { - border-bottom-left-radius: 12px; - border-bottom-right-radius: 12px; - } - - label { - font-weight: 600; - color: $foreground; - } - } - - & > separator { - background: lighten($color: $background, $amount: 5); - margin: .5px 10px; - padding: 1px 0; - border-radius: 1px; - } -} - -trough { - background: darken($color: $foreground, $amount: 25); - border-radius: 4px; -} - -trough highlight { - background: $foreground; - padding: 10px; - border-radius: inherit; -} - -scale { - padding: 10px; -} - -scrollbar trough { - background: rgba(darken($color: $foreground, $amount: 50), .7); - margin: 5px 0; - padding: 2px; - - & slider { - background: lighten($color: $color1, $amount: 15); - padding: 3px; - border-radius: inherit; - } -} - -tooltip { - & box { - margin: 16px; - margin-top: 0; - border-radius: 12px; - border: 1px solid darken($color: $color1, $amount: 1); - background: $background; - padding: 6px 8px; - box-shadow: 0 3px 5px 1px rgba($color: #000000, $alpha: .8); - } -} - - -.big-media { - padding: 16px; - margin: 6px 0; - border-radius: 18px; - - & > box > .album-image { - background-size: 100%; - background-repeat: no-repeat; - background-position: center 0; - margin-right: 12px; - border-radius: 12px; - } - - & > box > .right { - - & > .media-info { - label { - font-family: "Cantarell", "Noto Sans CJK JP"; - } - - & label.title { - font-weight: 700; - font-size: 16px; - margin-bottom: 6px; - } - - & label.artist { - font-weight: 600; - font-size: 12px; - margin-bottom: 12px; - } - } - - & > .controls { - padding: 0px; - - & > button { - padding: 4px 9px; - - & > label { - font-size: 13.3px; - } - } - - & > button.repeat { - padding-right: 11px; - } - } - } - - &.album-bg { - box-shadow: inset 0 0 0 100px rgba(0, 0, 0, .5); - background-size: cover; - } - -} - -.separator { - background: darken($color: $foreground, $amount: 20); - - &.horizontal { - margin: 8px 4px; - padding: 0 1px; - } - - &.vertical { - margin: 4px 10px; - padding: 1px 0; - } -} diff --git a/eww/styles/powermenu.scss b/eww/styles/powermenu.scss deleted file mode 100644 index b209409..0000000 --- a/eww/styles/powermenu.scss +++ /dev/null @@ -1,32 +0,0 @@ - -.powermenu-container { - margin: 400px 100px; - - & * { - outline: initial; - } - & > button { - padding: 96px; - margin: 0 6px; - border-radius: 6px; - background: darken($color: $foreground, $amount: 35); - - &:first-child { - border-top-left-radius: 24px; - border-bottom-left-radius: 24px; - } - - &:last-child { - border-top-right-radius: 24px; - border-bottom-right-radius: 24px; - } - - & label { - font-size: 98px; - } - } - - & button:hover { - background: $color1; - } -} diff --git a/eww/styles/volume-control.scss b/eww/styles/volume-control.scss deleted file mode 100644 index b5e4b5c..0000000 --- a/eww/styles/volume-control.scss +++ /dev/null @@ -1,36 +0,0 @@ - -.volume-control { - background: rgba($background, .6); - padding: 6px; - border-radius: 16px; - - trough { - background: rgba(lighten($color: $background, $amount: 25), .5); - border-radius: 6px; - } - - trough highlight { - background: lighten($color: $color2, $amount: 40); - padding: 10px; - border-radius: inherit; - } - - scale { - padding: 10px; - } -} - -.volume-control .separator { - border-top: .5px solid rgba(darken($color: $foreground, $amount: 25), .7); - margin-bottom: 8px; - margin-left: 6px; - margin-right: 6px; - border-radius: 1px; -} - -.volume-control .slider { - label { - margin-left: 18px; - color: $background; - } -} diff --git a/eww/variables.yuck b/eww/variables.yuck deleted file mode 100644 index 6b4893b..0000000 --- a/eww/variables.yuck +++ /dev/null @@ -1,59 +0,0 @@ -; All globally used variables, polls and listeners should be stored here - -; Variables -; State -(defvar window_state_powermenu "closed") -(defvar window_state_control-center "closed") -(defvar window_state_floating-notifications "closed") -(defvar window_state_bar "closed") -(defvar window_state_volume-control "closed") -(defvar window_state_calendar-window "closed") -(defvar window_state_volume-popup "closed") -(defvar window_state_floating-media "closed") - -(defvar json_popup_notifications "{ \"notifications\": [] }") - - -; Listeners -(deflisten json_notification_history :initial "{\"history\": []}" -`sh scripts/notification-watch.sh`) - -(deflisten json_volume :initial `{ "output": 60, "source": 80 }` -`sh scripts/get-volume-watch.sh`) - -(deflisten literal_workspaces :initial "" -`sh scripts/workspaces.sh`) - -(deflisten json_media :initial "{}" -`python3 scripts/mediaplayer.py`) - -(deflisten json_active_window :initial "{ \"title\": \"null\", \"class\": \"null\" }" -`sh scripts/active-window.sh`) - - -; Polls -(defpoll day_name :interval "2s" -`date +"%A"`) -(defpoll month_name :interval "2s" -`date +"%B"`) -(defpoll hostname :initial "GNU/Linux" - :interval "24h" -`cat /etc/hostname`) - -(defpoll uptime_info :interval "50s" -`uptime -p | sed -e 's/^up //'`) - -(defpoll network_status :interval "2s" -`nmcli n c`) - -(defpoll bluetooth_powered :interval "2s" -`bluetoothctl show | grep Powered | awk '{ print $2 }'`) - -(defpoll notification_modes :interval "1s" -`makoctl mode | xargs`) - -(defpoll is_recording :interval "1s" -`sh -c "[[ -f $HOME/.cache/recording.lock ]] && echo true || echo false"`) - -(defpoll night_light :interval "1s" -`sh -c "[[ -f $HOME/.cache/night-light.pid ]] && echo true || echo false"`) diff --git a/eww/widgets/bar/audio.yuck b/eww/widgets/bar/audio.yuck deleted file mode 100644 index a13d8b0..0000000 --- a/eww/widgets/bar/audio.yuck +++ /dev/null @@ -1,13 +0,0 @@ - -(defwidget audio [] - (eventbox :onclick "sh ${EWW_CONFIG_DIR}/scripts/eww-window.sh toggle volume-control" - :class "audio-eventbox" - :cursor "pointer" - (box :class "audio ${window_state_volume-control}" - (eventbox :onscroll `[ {} == "up" ] && wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+ || wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-` - (label :text "${ json_volume.output != 0 ? '󰕾' : '󰝟' } ${json_volume.output}%")) - (eventbox :onscroll `[ {} == "up" ] && wpctl set-volume @DEFAULT_AUDIO_SOURCE@ 5%+ || wpctl set-volume @DEFAULT_AUDIO_SOURCE@ 5%-` - (label :text "${ json_volume.source != 0 ? '󰍬' : '󰍭' } ${json_volume.source}%")) - ) - ) -) diff --git a/eww/widgets/bar/battery.yuck b/eww/widgets/bar/battery.yuck deleted file mode 100644 index 442e23c..0000000 --- a/eww/widgets/bar/battery.yuck +++ /dev/null @@ -1,6 +0,0 @@ -(defwidget battery [ ?device ] - (box :class "battery" - :visible { EWW_BATTERY != "" ? true : false } - (label :text "󰁹 ${ EWW_BATTERY != '' ? EWW_BATTERY?.status : '' }%") - ) -) diff --git a/eww/widgets/bar/cc-toggle.yuck b/eww/widgets/bar/cc-toggle.yuck deleted file mode 100644 index facd6d1..0000000 --- a/eww/widgets/bar/cc-toggle.yuck +++ /dev/null @@ -1,10 +0,0 @@ - -(defwidget cc-toggle [] - (eventbox :onclick "sh scripts/eww-window.sh toggle control-center" - :onrightclick "makoctl mode -t dnd" - :class "control-center-toggle ${ window_state_control-center == "open" ? "open" : "closed" }" - (box :class "button" - { notification_modes =~ "dnd" ? "󰂠" : "󰂚"} - ) - ) -) diff --git a/eww/widgets/bar/clock.yuck b/eww/widgets/bar/clock.yuck deleted file mode 100644 index 3790f9a..0000000 --- a/eww/widgets/bar/clock.yuck +++ /dev/null @@ -1,26 +0,0 @@ -(defwidget clock [] - (box :class "clock" - :tooltip { day_name + ", " + month_name + formattime(EWW_TIME, " %d") } - - (eventbox :onclick "sh scripts/eww-window.sh toggle calendar-window" - :class '${ window_state_calendar-window == "open" ? "cal-open" : "" } button' - (box :space-evenly false - (box :space-evenly false - :class "time" - (label :text "ó°ø—" - :class "icon") - - (label :text "${day_name}") - - (revealer :reveal { window_state_calendar-window == "closed" } - :transition "slideright" - :duration "240ms" - :class "unrevealer" - - (label :text { formattime(EWW_TIME, "%d, %H:%M") } ) - ) - ) - ) - ) - ) -) diff --git a/eww/widgets/bar/hardware.yuck b/eww/widgets/bar/hardware.yuck deleted file mode 100644 index 5b5a923..0000000 --- a/eww/widgets/bar/hardware.yuck +++ /dev/null @@ -1,19 +0,0 @@ -(defwidget hardware [ ?cpu ?memory ?visible ] - (eventbox :class "hardware-eventbox button" - :onclick "sh ${EWW_CONFIG_DIR}/scripts/eww-window.sh toggle hardware-monitor" - :visible { visible == "" || visible ? true : false } - (box :class "hardware" - :space-evenly false - (box :class "cpu" - :visible { cpu == "" || cpu ? true : false } - :tooltip { "CPU: " + arraylength(EWW_CPU?.cores) + " Threads" } - (label :text "ļ’¼ ${ round(EWW_CPU?.avg, 0) }%") - ) - (box :class "memory" - :visible { memory == "" || memory ? true : false } - :tooltip { "Total Memory: " + round(EWW_RAM?.total_mem / 1000000000, 1) + " GiB(s)" } - (label :text "ļ€ˆ ${ round(EWW_RAM?.used_mem_perc, 0) }%") - ) - ) - ) -) diff --git a/eww/widgets/bar/logo.yuck b/eww/widgets/bar/logo.yuck deleted file mode 100644 index e8b98a6..0000000 --- a/eww/widgets/bar/logo.yuck +++ /dev/null @@ -1,5 +0,0 @@ -(defwidget logo [] - (button :class "distro-logo" - :onclick "hyprctl dispatch exec anyrun" - " ") -) diff --git a/eww/widgets/bar/media.yuck b/eww/widgets/bar/media.yuck deleted file mode 100644 index f21cc24..0000000 --- a/eww/widgets/bar/media.yuck +++ /dev/null @@ -1,65 +0,0 @@ - -(defvar media_reveal_controls false) - -(defwidget media [] - (eventbox :onhover "${EWW_CMD} update media_reveal_controls=true" - :onhoverlost "${EWW_CMD} update media_reveal_controls=false" - :visible { json_media == "" || (json_media.title == "null" && - json_media.artist == "null") || - (json_active_window.class =~ json_media.player || - json_active_window.title =~ json_media.title) ? - false : true - } - :onclick "sh scripts/eww-window.sh toggle floating-media" - :class "mediaplayer-eventbox" - - (box :class "mediaplayer ${ media_reveal_controls && window_state_floating-media == "closed" ? 'revealed' : '' }" - :space-evenly false - - (eventbox :class "media" - :tooltip "${json_media.title} - ${json_media.artist}" - - (box :space-evenly false - (label :class "player" - :text { json_media.player == "spotify" ? " " : "󰎇 " }) - - (label :class "media-title" - :text "${json_media.title}" - :limit-width 40) - - (separator :orientation "horizontal" - :alpha 0.8 - :style "border-radius: 2x;") - - (label :class "media-artist" - :text "${json_media.artist}" - :limit-width 25) - ) - ) - (revealer :class "media-controls-revealer" - :reveal { media_reveal_controls ? "${ window_state_floating-media == 'closed' ? true : false }" : false } - :transition "slideright" - :duration "220ms" - - (box :class "media-controls" - (button :class "url" - :onclick "wl-copy '${json_media.url}'" - :tooltip "Copy link to Clipboard" - :visible { json_media.url != "" && json_media.url != "null" } - "󰌷") - (button :class "previous" - :onclick "playerctl previous --player=${json_media.player}" - "󰒮") - - (button :class "toggle play-pause" - :onclick "playerctl play-pause --player=${json_media.player}" - { json_media.status == "playing" ? "󰏤" : "󰐊" }) - - (button :class "next" - :onclick "playerctl next --player=${json_media.player}" - "󰒭") - ) - ) - ) - ) -) diff --git a/eww/widgets/bar/network.yuck b/eww/widgets/bar/network.yuck deleted file mode 100644 index 0298d4f..0000000 --- a/eww/widgets/bar/network.yuck +++ /dev/null @@ -1,8 +0,0 @@ -(defwidget network [] - (eventbox :class "button network-eventbox" - :onclick "hyprctl dispatch exec 'nm-connection-editor'" - (box :class "network" - (label :text "${ network_status == 'full' ? ' ' : 'ó°¤­ ' }") - ) - ) -) diff --git a/eww/widgets/bar/window.yuck b/eww/widgets/bar/window.yuck deleted file mode 100644 index f318a56..0000000 --- a/eww/widgets/bar/window.yuck +++ /dev/null @@ -1,29 +0,0 @@ - -(defwidget window [] - (box :class "window" - :visible { json_active_window.class == "" || json_active_window.class == "null" ? false : true } - :space-evenly false - :valign "center" - :orientation "horizontal" - - (image :class "icon" - :icon "${ json_active_window.initialClass =~ 'zen' ? 'zen-browser' : json_active_window.initialClass }" - :icon-size "toolbar") - - (box :class "info" - :orientation { json_active_window.title == "" ? "horizontal" : "vertical" } - :space-evenly false - (label :class "window-class" - :text "${json_active_window.class}" - :xalign 0 - :yalign 0) - (label :class "window-title" - :text "${json_active_window.title}" - :visible { json_active_window.title != "" ? true : false } - :limit-width 45 - :tooltip "${json_active_window.title}" - :xalign 0 - :yalign 0) - ) - ) -) diff --git a/eww/widgets/bar/workspaces.yuck b/eww/widgets/bar/workspaces.yuck deleted file mode 100644 index e92e188..0000000 --- a/eww/widgets/bar/workspaces.yuck +++ /dev/null @@ -1,13 +0,0 @@ -(defvar hover_workspaces false) - -(defwidget workspaces [] - (eventbox :onscroll "[[ {} == up ]] && hyprctl dispatch workspace e+1 >> /dev/null || hyprctl dispatch workspace e-1 >> /dev/null" - :class "workspaces-eventbox" - :onhover "${EWW_CMD} update hover_workspaces=true" - :onhoverlost "${EWW_CMD} update hover_workspaces=false" - (box :class "workspaces" - :valign "center" - (literal :content literal_workspaces) - ) - ) -) diff --git a/eww/widgets/big-media.yuck b/eww/widgets/big-media.yuck deleted file mode 100644 index 0fc4abf..0000000 --- a/eww/widgets/big-media.yuck +++ /dev/null @@ -1,84 +0,0 @@ -(defwidget big-media [ show-album-bg show-album-image ?album-image-size ?style ?visible ] - (box :class "big-media ${ show-album-bg ? 'album-bg' : '' } ${ show-album-image ? 'album-image' : '' }" - :style "${ show-album-bg ? "background-image: url(\"${ json_media.artUrl != "null" ? json_media.artUrl : '' }\")" : "" }; ${style}" - :visible { visible == false ? false : true } - :space-evenly false - :orientation "vertical" - - (box :orientation "horizontal" - :space-evenly false - :class "media" - - (box :class "album-image" - :width { album-image-size != "" ? album-image-size : 98 } - :height { album-image-size != "" ? "${album-image-size - 11}" : 87 } - :style "background-image: url(\"${ json_media.artUrl != "null" ? json_media.artUrl : "" }\");" - :valign "center" - :visible { json_media.artUrl == "null" || !show-album-image ? false : true }) - - (box :orientation "vertical" - :space-evenly false - :class "right" - :valign "center" - - (box :class "media-info" - :space-evenly false - :orientation "vertical" - - (label :class "title" - :text "${json_media.title}" - :xalign 0 - :limit-width 40 - :show-truncated true) - (label :class "artist" - :text "${json_media.artist}" - :xalign 0 - :limit-width 40 - :show-truncated true) - ) - - (box :class "controls" - :orientation "horizontal" - :space-evenly false - :halign "start" - - (box :class "button-row" - :orientation "horizontal" - :space-evenly false - :halign "start" - :visible { json_media.url != "null" ? true : false } - - (button :class "url" - :onclick "wl-copy '${json_media.url}'" - :style "padding-right: 13px;" - :tooltip "Copy link to Clipboard" - "󰌷") - ) - - (box :class "button-row" - :orientation "horizontal" - :space-evenly false - :halign "start" - - (button :class "shuffle" - :onclick "playerctl --player=${json_media.player} shuffle Toggle" - :tooltip "Toggle shuffle" - "󰒝") - (button :class "previous" - :onclick "playerctl --player=${json_media.player} previous" - :tooltip "Previous" - "󰒮") - (button :class "play-pause" - :onclick "playerctl --player=${json_media.player} play-pause" - :tooltip { json_media.status == "playing" ? "Pause" : "Play" } - { json_media.status == "playing" ? "󰏤" : "󰐊" }) - (button :class "next" - :onclick "playerctl --player=${json_media.player} next" - :tooltip "Next" - "󰒭") - ) - ) - ) - ) - ) -) diff --git a/eww/widgets/control-center/notification.yuck b/eww/widgets/control-center/notification.yuck deleted file mode 100644 index 7a79576..0000000 --- a/eww/widgets/control-center/notification.yuck +++ /dev/null @@ -1,59 +0,0 @@ -(defwidget notification [ ?application_name ?icon summary body ?image ?onclickclose ?onclick ] - (eventbox :onclick "${onclick}" - (box :class "notification" - :orientation "vertical" - :height 96 - :space-evenly false - - (box :class "top" - :orientation "horizontal" - :space-evenly false - - (box :class "app-info" - :space-evenly false - :halign "start" - :hexpand true - :vexpand false - - (image :class "app-icon" - :visible { icon != "" ? true : false } - :icon "${icon}" - :icon-size "menu") - (label :class "app-name" - :text "${application_name}") - ) - (button :class "close" - :onclick "${onclickclose}" - :visible true - "󰅖") - ) - - (box :class "content" - :space-evenly false - (box :class "image" - :width 96 - :height 96 - :visible { image != "" && image != "\{\}" ? true : false } - :style { image != "" ? "background-image: url('${ image =~ "^file://(.*)$" || image =~ "^http(.*)://(.*)$" ? image : "${ "file://" + image }" }');" : "" } - ) - - (box :class "text" - :orientation "vertical" - :space-evenly false - - (label :class "summary" - :markup "${summary}" - :xalign 0 - :halign "start" - :show-truncated true) - - (label :class "body" - :markup "${body}" - :xalign 0 - :show-truncated false - :wrap true) - ) - ) - ) - ) -) diff --git a/eww/widgets/control-center/notifications.yuck b/eww/widgets/control-center/notifications.yuck deleted file mode 100644 index 74c07cf..0000000 --- a/eww/widgets/control-center/notifications.yuck +++ /dev/null @@ -1,38 +0,0 @@ -(defwidget notifications [] - (scroll :class "cc-notifications scroll" - :hscroll false - :vscroll true - :vexpand true - :style "border-radius: 16px;" - - (box :class "notifications" - :orientation "vertical" - :space-evenly false - - (for notification in { json_notification_history["history"] } - (notification :application_name "${notification.app-name.data}" - :image "${notification.app-icon.data}" - :summary "${notification.summary.data}" - :body "${notification.body.data}" - :onclickclose "sh scripts/notification-remove.sh ${notification.id.data}" - :icon "${notification.app-name.data}") - ) - - (box :class "empty-notifications" - :visible { arraylength(json_notification_history["history"]) == 0 } - :style "margin-top: 50px;" - :space-evenly false - :orientation "vertical" - :halign "center" - :valign "center" - - (label :class "bell-icon" - :text "󱇦" - :style "font-size: 96px") - - (label :text "You're done!" - :style "margin-left: 12px;") - ) - ) - ) -) diff --git a/eww/widgets/control-center/tiles.yuck b/eww/widgets/control-center/tiles.yuck deleted file mode 100644 index 72e495e..0000000 --- a/eww/widgets/control-center/tiles.yuck +++ /dev/null @@ -1,75 +0,0 @@ -(defwidget tiles [] - (box :class "tiles-grid" - :orientation "vertical" - (box :class "tiles tiles1" - (tile :icon '󰤨' - :label "Network" - :enabled { network_status == "full" ? true : false } - :onenable "nmcli n on > /dev/null" - :ondisable "nmcli n off > /dev/null" - :visible true - :class "network") - - (tile :icon 'ó°‚Æ' - :label "Bluetooth" - :enabled { bluetooth_powered == "yes" ? true : false } - :onenable "bluetoothctl power on > /dev/null" - :ondisable "bluetoothctl power off > /dev/null" - :visible { bluetooth_powered == "" ? false : true }) - - (tile :icon '󰍶' - :label "Do Not Disturb" - :enabled { notification_modes =~ "dnd" ? true : false } - :onenable "makoctl mode -a dnd" - :ondisable "makoctl mode -r dnd" - :visible true - :class "dnd") - ) - (box :class "tiles tiles2" - :visible true - (tile :icon '󰀝' - :label "Airplane Mode" - :enabled { (network_status != "full" && network_status != "partial") && bluetooth_powered != "yes" ? true : false } - :onenable "bluetoothctl power off; nmcli n off" - :ondisable "bluetoothctl power on; nmcli n on") - (tile :icon '󰻂' - :label "Screen Record" - :enabled "${is_recording}" - :onenable "hyprctl dispatch exec \"sh $HOME/.config/eww/scripts/screen-recording.sh\" >> /dev/null" - :ondisable "kill $(cat $HOME/.cache/recording.pid)" - :class "screen-rec") - (tile :icon '󰖔' - :label "Night Light" - :enabled "${night_light}" - :onenable "hyprctl dispatch exec \"sh $HOME/.config/eww/scripts/night-light.sh\"" - :ondisable "kill $(cat $HOME/.cache/night-light.pid)" - :class "night-light") - ) - ) -) - -(defwidget tile [ ?class icon label ?onclickarrow ?enabled onenable ondisable ?visible ?min-width ] - (checkbox :onchecked "${onenable}" - :onunchecked "${ondisable}" - :visible { visible == "" || visible ? true : false } - :checked { enabled == "" || !enabled ? false : true } - :timeout "2s" - :class "tile-checkbox ${class}" - - (box :class "tile" - :space-evenly false - :valign "center" - - (label :text "${icon}" - :class "icon" - :unindent true - :limit-width 1 - :show-truncated false - :xalign 0) - - (label :text "${label}" - :class "label" - :xalign 0) - ) - ) -) diff --git a/eww/widgets/control-center/top-bar.yuck b/eww/widgets/control-center/top-bar.yuck deleted file mode 100644 index 302a08d..0000000 --- a/eww/widgets/control-center/top-bar.yuck +++ /dev/null @@ -1,46 +0,0 @@ -(defwidget top-bar [] - (box :class "top-bar" - :orientation "horizontal" - - (box :class "left" - :orientation "vertical" - :halign "start" - :space-evenly false - - (label :xalign 0 - :text " ${hostname}" - :class "hostname") - - (label :xalign 0 - :text " ${uptime_info}" - :class "uptime") - ) - - (box :orientation "horizontal" - :class "button-row" - :space-evenly false - :halign "end" - - (button :class "lock" - :onclick "hyprctl dispatch exec hyprlock" - :tooltip "Lock" - '󰌾') - (button :class "color-picker" - :onclick "hyprctl dispatch exec 'sh $HOME/.config/eww/scripts/color-picker.sh'" - :tooltip "Pick a Color" - 'ó°“±') - (button :class "screenshot" - :onclick "hyprctl dispatch exec 'hyprshot -m region -o $HOME/Screenshots'" - :tooltip "Select to screenshot" - '󰹑') - (button :class "wallpaper" - :onclick "hyprctl dispatch exec 'sh $HOME/.config/hypr/scripts/change-wallpaper.sh'" - :tooltip "Select a wallpaper" - 'ó°ø‰') - (button :class "powermenu" - :onclick "sh scripts/eww-window.sh close bar && sh scripts/eww-window.sh open powermenu" - :tooltip "Choose power options" - '󰗽') - ) - ) -) diff --git a/eww/widgets/separator.yuck b/eww/widgets/separator.yuck deleted file mode 100644 index e21587d..0000000 --- a/eww/widgets/separator.yuck +++ /dev/null @@ -1,4 +0,0 @@ -(defwidget separator [ ?orientation ?alpha ?style ] - (box :class "separator ${ orientation == '' || orientation == 'horizontal' || orientation == "h" ? 'horizontal' : 'vertical' }" - :style "opacity: ${ alpha == '' ? 1 : alpha }; ${style}") -) diff --git a/eww/widgets/volume-control/output-slider.yuck b/eww/widgets/volume-control/output-slider.yuck deleted file mode 100644 index b02c627..0000000 --- a/eww/widgets/volume-control/output-slider.yuck +++ /dev/null @@ -1,19 +0,0 @@ -(defwidget output-slider [] - (box :class "slider output-slider" - :space-evenly true - - (overlay - (scale :min 0 - :max 101 ; dirty fix max 99% volume - :value "${json_volume.output}" - :orientation "horizontal" - :draw-value false - :flipped false - :onchange "wpctl set-volume @DEFAULT_AUDIO_SINK@ $(awk -v n={} 'BEGIN { print (n / 100) }')" - :tooltip "${json_volume.output}") - - (label :text "󰕾" - :xalign 0) - ) - ) -) diff --git a/eww/widgets/volume-control/source-slider.yuck b/eww/widgets/volume-control/source-slider.yuck deleted file mode 100644 index 1e9e01d..0000000 --- a/eww/widgets/volume-control/source-slider.yuck +++ /dev/null @@ -1,20 +0,0 @@ -(defwidget source-slider [] - (box :class "slider source-slider" - :space-evenly true - - (overlay - (scale :min 0 - :max 101 ; dirty fix 99% max volume - :value "${json_volume.source}" - :orientation "horizontal" - :draw-value false - :flipped false - :onchange "wpctl set-volume @DEFAULT_AUDIO_SOURCE@ $(awk -v n={} 'BEGIN { print (n / 100) }')" - :class "output-volume-slider" - :tooltip "${json_volume.source}") - - (label :text "󰍬" - :xalign 0) - ) - ) -) diff --git a/eww/windows/bar.yuck b/eww/windows/bar.yuck deleted file mode 100644 index d789230..0000000 --- a/eww/windows/bar.yuck +++ /dev/null @@ -1,60 +0,0 @@ -(include "widgets/bar/workspaces.yuck") -(include "widgets/bar/clock.yuck") -(include "widgets/bar/cc-toggle.yuck") -(include "widgets/bar/audio.yuck") -(include "widgets/bar/media.yuck") -(include "widgets/bar/logo.yuck") -(include "widgets/bar/window.yuck") -(include "widgets/bar/network.yuck") -(include "widgets/bar/battery.yuck") -(include "widgets/bar/hardware.yuck") - -(defwindow bar - :monitor 0 - :geometry (geometry :width "100%" - :height "46px" - :anchor "top center") - :stacking "fg" - :exclusive true - :namespace "eww-bar" - (box :class "bar" - :vexpand true - :hexpand false - (centerbox :orientation "horizontal" - (box :class "widgets-left" - :halign "start" - :space-evenly false - :vexpand true - - (logo) - (workspaces) - (window) - ) - (box :class "widgets-center" - :halign "center" - :space-evenly false - :vexpand true - - (clock) - (media) - ) - (box :class "widgets-right" - :halign "end" - :space-evenly false - :vexpand true - - (systray :spacing 0 - :orientation "horizontal" - :space-evenly false - :icon-size 14 - :prepend-new false - :class "systray" - ) - (audio) - (battery) - (network) - (cc-toggle) - ) - ) - ) -) diff --git a/eww/windows/calendar-window.yuck b/eww/windows/calendar-window.yuck deleted file mode 100644 index 45a8f15..0000000 --- a/eww/windows/calendar-window.yuck +++ /dev/null @@ -1,24 +0,0 @@ -(defwindow calendar-window - :monitor 0 - :geometry (geometry :anchor "top center" - :y "5px") - :stacking "fg" - :exclusive false - :namespace "eww-calendar" - - (box :class "calendar-box" - :space-evenly false - :orientation "vertical" - - (label :class "time" - :text { formattime(EWW_TIME, "%H:%M") }) - - (label :class "date" - :text { month_name + formattime(EWW_TIME, " %d, %Y") }) - (calendar :class "month-calendar" - :show-details true - :show-heading true - :show-day-names true - :show-week-numbers false) - ) -) diff --git a/eww/windows/control-center.yuck b/eww/windows/control-center.yuck deleted file mode 100644 index edc031e..0000000 --- a/eww/windows/control-center.yuck +++ /dev/null @@ -1,47 +0,0 @@ -(include "widgets/control-center/top-bar.yuck") -(include "widgets/control-center/notifications.yuck") -(include "widgets/control-center/notification.yuck") -(include "widgets/control-center/tiles.yuck") - -(defwindow control-center [] - :monitor 0 - :geometry (geometry :width "500px" - :height "90%" - :anchor "top right") - :stacking "overlay" - :exclusive false - :namespace "eww-cc" - - (box :orientation "horizontal" - (eventbox - (box :class "cc" - :orientation "vertical" - :space-evenly false - - (top-bar) - (tiles) - (revealer :reveal { window_state_floating-media != "open" } - :transition "slideup" - :duration "380ms" - - (big-media :show-album-bg true - :show-album-image true - :visible { json_media == "" || (json_media.title == "null" - && json_media.artist == "null") ? false : true }) - ) - (separator :orientation "vertical" - :alpha ".08") - (notifications) - (box :class "bottom button-row" - :halign "end" - :space-evenly false - - (button :class "clear-all" - :onclick "sh scripts/notification-clear.sh" - :visible { arraylength(json_notification_history["history"]) > 0 } - "󰎟 Clear") - ) - ) - ) - ) -) diff --git a/eww/windows/floating-media.yuck b/eww/windows/floating-media.yuck deleted file mode 100644 index c25ed6a..0000000 --- a/eww/windows/floating-media.yuck +++ /dev/null @@ -1,24 +0,0 @@ -(defwindow floating-media [] - :monitor 0 - :geometry (geometry :anchor "top center" - :width "460px" - :y "5px") - :exclusive false - :stacking "fg" - :focusable false - :namespace "eww-media" - (box :class "floating-media" - :orientation "vertical" - :space-evenly false - :visible { json_media == "" || (json_media.title == "null" - && json_media.artist == "null") ? false : true } - - (big-media :show-album-bg true - :album-image-size 128 - :show-album-image true - :style "margin: 16px; margin-top: 0; box-shadow: - 0 5px 6px 1px rgba(0, 0, 0, .6), - inset 0 0 0 200px rgba(0, 0, 0, .52);" - :visible true) - ) -) diff --git a/eww/windows/floating-notifications.yuck b/eww/windows/floating-notifications.yuck deleted file mode 100644 index 094028e..0000000 --- a/eww/windows/floating-notifications.yuck +++ /dev/null @@ -1,115 +0,0 @@ -(defwindow floating-notifications [] - :monitor 0 - :exclusive false - :focusable false - :namespace "eww-notification-popup" - :geometry (geometry :anchor "top right" - :width "512px" - :height "1px" - :x "5px" - :y "6px") - :stacking "overlay" - - (box :class "floating-notifications" - :orientation "vertical" - :space-evenly false - :hexpand true - :vexpand false - - (box :class "notifications" - :space-evenly false - :orientation "vertical" - - (for item in { json_popup_notifications["notifications"] } - (floating-notification :summary "${item.summary.data}" - :body "${item.body.data}" - :image "${item.app-icon.data}" - :app-name "${item.app-name.data}" - :onhoverlost "sh scripts/notification-popup-remove.sh ${item.id.data}") - ) - ) - - (box :visible { arraylength(json_popup_notifications?.["notifications"]) > 0 } - :orientation "horizontal" - :valign "end" - :class "bottom" - :space-evenly true - :vexpand false - - (box :class "left" - :space-evenly false - (box :space-evenly false - :class "tip" - :valign "center" - (label :text "ó±§”" - :class "icon") - (label :halign "start" - :text "Hover to dismiss" - :yalign 0.5) - ) - ) - - (box :class "right" - :space-evenly false - :halign "end" - (eventbox :class "button" - :onclick "sh ${EWW_CONFIG_DIR}/scripts/eww-window.sh close floating-notifications; ${EWW_CMD} update 'json_popup_notifications={\"notifications\": []}'" - (box :class "clear-all" - :space-evenly false - - (label :text "󰎟" - :class "icon") - (label :text "Clear all") - ) - ) - ) - ) - ) -) - -(defwidget floating-notification [ summary body image app-name ?onhoverlost ] - (eventbox :onhoverlost "${onhoverlost}" - :class "floating-notification-eventbox" - (box :class "floating-notification" - :space-evenly false - :orientation "vertical" - - (box :orientation "horizontal" - :class "top" - :space-evenly false - - (image :class "app-icon" - :icon "${ app-name =~ 'zen-alpha' ? 'zen-browser' : app-name }" - :icon-size "menu") - - (label :text "${app-name}" - :xalign 0) - ) - - (box :orientation "horizontal" - :space-evenly false - :class "content" - (box :class "image" - :style "background-image: image(url('${image}'));" - :width 86 - :height 85 - :visible { image != "" ? true : false }) - - (box :class "text-content" - :orientation "vertical" - :space-evenly false - - (label :class "summary" - :markup "${summary}" - :xalign 0) - - (label :class "body" - :markup "${body}" - :xalign 0 - :wrap true - :show-truncated false) - ) - ) - ) - ) -) diff --git a/eww/windows/hardware-monitor.yuck b/eww/windows/hardware-monitor.yuck deleted file mode 100644 index 5a143e4..0000000 --- a/eww/windows/hardware-monitor.yuck +++ /dev/null @@ -1,15 +0,0 @@ -(defwindow hardware-monitor [] - :monitor 0 - :geometry (geometry :width "320" - :anchor "top right" - :y "5px") - - :exclusive 0 - :namespace "eww-hardware" - :focusable false - :stacking "fg" - - (box :class "hardware-monitor" - ; TODO - ) -) diff --git a/eww/windows/powermenu.yuck b/eww/windows/powermenu.yuck deleted file mode 100644 index 0adfab0..0000000 --- a/eww/windows/powermenu.yuck +++ /dev/null @@ -1,33 +0,0 @@ -(defwindow powermenu [] - :monitor 0 - :geometry (geometry :width "100%" - :height "100%") - - :stacking "overlay" - :namespace "eww-powermenu" - :focusable true - :exclusive true - - (eventbox :class "outside" - :onclick "sh scripts/eww-window.sh close powermenu; sh scripts/eww-window.sh open bar" - (box :space-evenly true - :halign "center" - :class "powermenu-container" - (button :class "poweroff" - :onclick "systemctl poweroff" - "󰐄") - (button :class "reboot" - :onclick "systemctl reboot" - "󰜉") - (button :class "suspend" - :onclick "systemctl suspend" - "󰤄") - (button :class "logout" - :onclick "loginctl kill-user $(sh -c 'echo $USER')" - "󰗽") - ;(button :class "close" - ; :onclick "sh scripts/eww-window.sh close powermenu; sh scripts/eww-window.sh open bar" - ;"󰅖") - ) - ) -) diff --git a/eww/windows/volume-control.yuck b/eww/windows/volume-control.yuck deleted file mode 100644 index c7ec1da..0000000 --- a/eww/windows/volume-control.yuck +++ /dev/null @@ -1,39 +0,0 @@ -(include "./widgets/volume-control/output-slider.yuck") -(include "./widgets/volume-control/source-slider.yuck") - -(defwindow volume-control [] - :monitor 0 - :namespace "eww-volume" - :geometry (geometry :anchor "top right" - :width "280px" - :x "6px" - :y "5px") - :exclusive false - :stacking "overlay" - :focusable false - - (box :class "volume-control" - :space-evenly false - :orientation "vertical" - - (output-slider) - (source-slider) - - (separator :orientation "vertical" - :alpha 0.5) - - (box :class "vertical button-row" - :orientation "vertical" - - (button :class "bluetooth-devices" - :onclick "sh scripts/eww-window.sh close volume-control; hyprctl dispatch exec overskride" - (label :text "Bluetooth devices" - :xalign 0)) - - (button :class "more-devices" - :onclick "sh scripts/eww-window.sh close volume-control; hyprctl dispatch exec pavucontrol" - (label :text "More settings" - :xalign 0)) - ) - ) -) diff --git a/eww/windows/volume-popup.yuck b/eww/windows/volume-popup.yuck deleted file mode 100644 index d386a5f..0000000 --- a/eww/windows/volume-popup.yuck +++ /dev/null @@ -1,16 +0,0 @@ -(defwindow volume-popup [] - :monitor 0 - :stacking "overlay" - :namespace "volume-popup" - :focusable false - :exclusive false - :geometry (geometry :width 180 - :anchor "top center" - :y "10px") - - (box :class "volume-popup" - (output-slider) - (source-slider) - ; TODO - ) -) diff --git a/hypr/autostart.conf b/hypr/autostart.conf deleted file mode 100644 index 70e9468..0000000 --- a/hypr/autostart.conf +++ /dev/null @@ -1,28 +0,0 @@ - -############### -## AUTOSTART ## -############### - -# Services/Daemons -exec-once = systemctl enable --user --now hyprpolkitagent.service -exec-once = mako -exec-once = eww daemon --no-daemonize -exec-once = hypridle -exec-once = wl-paste --type text --watch cliphist store -exec-once = wl-paste --type image --watch cliphist store - -# Tools -exec-once = hyprpaper - -# Scripts -exec-once = sh $HOME/.config/hypr/scripts/gen-pywal.sh -exec-once = sh $HOME/.config/hypr/scripts/night-light-check.sh -exec-once = sh $HOME/.config/hypr/scripts/notification-handler-daemon.sh - -# Widgets -exec-once = sh "$(eww get EWW_CONFIG_DIR)/scripts/eww-window.sh" open bar - - -# Apps -exec-once = /bin/vesktop --start-minimized -exec-once = /bin/steam-runtime -silent diff --git a/hypr/hypridle.conf b/hypr/hypridle.conf index efc041d..ec54b0a 100644 --- a/hypr/hypridle.conf +++ b/hypr/hypridle.conf @@ -1,7 +1,7 @@ general { - lock_cmd = echo "Locked Hyprland Session" - unlock_cmd = echo "Unlocked Hyprland Session" + # lock_cmd = echo "Locked Hyprland Session" + unlock_cmd = notify-send "Welcome back to Hyprland, $USER!" ignore_dbus_inhibit = false ignore_systemd_inhibit = false } @@ -9,5 +9,5 @@ general { listener { timeout = 3600 # 1800 -> 30m | 3600 -> 1h | 7200 -> 2h on-timeout = hyprlock - on-resume = notify-send "Welcome back to Hyprland, $USER!" + # on-resume = notify-send "Welcome back to Hyprland, $USER!" } diff --git a/hypr/hyprland.conf b/hypr/hyprland.conf index 47bee99..bb5e2ed 100644 --- a/hypr/hyprland.conf +++ b/hypr/hyprland.conf @@ -1,37 +1,15 @@ -############################################# -## Retrozinndev's Hyprland Configurations! ## -############################################# +############################### +## Retrozinn's Hyprland Dots ## +############################### -# Nvidia Settings -source = ~/.config/hypr/nvidia.conf +# From https://github.com/retrozinndev/Hyprland-Dots +# Made with lots of love 󰋑 , by retrozinndev +# Licensed under the MIT License -# Environment -source = ~/.config/hypr/environment.conf -# Monitors -source = ~/.config/hypr/monitors.conf +# Shell configurations (it's not recommended to modify) +source = ./shell/hyprland.conf -# Layout -source = ~/.config/hypr/layout.conf - -# Input -source = ~/.config/hypr/input.conf - -# Devices -# source = ~/.config/hypr/devices.conf # Uncomment this line to apply file, remember to make it first! - -# Plugins (you can comment if you want pure Hyprland) -source = ~/.config/hypr/plugins.conf - -# Appearance -source = ~/.config/hypr/decorations.conf - -# Autostart -source = ~/.config/hypr/autostart.conf - -# Bindings -source = ~/.config/hypr/bindings.conf - -# Rules -source = ~/.config/hypr/rules.conf +# User configurations (please use the `user/` config directory +source = ./user/hyprland.conf diff --git a/hypr/hyprlock.conf b/hypr/hyprlock.conf index 1b93ce2..1a0b356 100644 --- a/hypr/hyprlock.conf +++ b/hypr/hyprlock.conf @@ -1,10 +1,12 @@ +# Source colors from pywal +source = ~/.cache/wal/colors-hyprland.conf ############## # LOCKSCREEN # ############## -# Source colors from pywal -source = ~/.cache/wal/colors-hypr.conf +# Wiki: https://wiki.hyprland.org/Hypr-Ecosystem/hyprlock + # Fonts $font = Cantarell Regular @@ -43,6 +45,10 @@ label { text = cmd[update:30000] echo -e "$(date +"%R")" # 24-hours # text = cmd[update:30000] echo -e "$(date +"%I:%M %p")" # 12-hours (AM/PM) color = $foreground + shadow_passes = 1 + shadow_size = 2 + shadow_color = $background + shadow_boost = 0.4 font_size = 120 font_family = $clockFont position = 0, -60 @@ -55,6 +61,10 @@ label { monitor = text = cmd[update:43200000] echo -e "$(date +"%A, %d %B %Y")" color = $foreground + shadow_passes = 1 + shadow_size = 2 + shadow_color = $background + shadow_boost = 0.4 font_size = 20 font_family = $font position = 0, -250 @@ -74,13 +84,17 @@ label { position = 0, 5 } -# Media +Media label { monitor = font_size = 12 font_family = Cantarell color = $foreground - text = cmd[update:1000] JSON_MEDIA=$(eww get json_media | jq -c '.'); PLAYER=$(echo $JSON_MEDIA | jq -r ".player"); TITLE=$(echo $JSON_MEDIA | jq -r ".title"); ARTIST=$(echo $JSON_MEDIA | jq -r ".artist"); [[ "$JSON_MEDIA" == "" ]] || sh -c "echo -e $([[ $PLAYER =~ 'spotify' ]] && echo -e '󰓇' || echo -e '󰎇') $TITLE - $ARTIST" + text = cmd[update:1000] bash -c 'playerctl metadata && echo -e "󰎇 $(playerctl metadata title) - $(playerctl metadata artist)"' | tail -n 1 + shadow_passes = 1 + shadow_size = 2 + shadow_color = $background + shadow_boost = 0.4 halign = center valign = center position = 0, 180 @@ -92,9 +106,14 @@ image { path = ~/.face size = 72 border_color = $color2 + border_size = 2 position = 0, 100 halign = center valign = bottom + shadow_passes = 1 + shadow_size = 2 + shadow_color = $background + shadow_boost = 0.4 } # Input (password) @@ -118,4 +137,8 @@ input-field { position = 0, 40 halign = center valign = bottom + shadow_passes = 1 + shadow_size = 2 + shadow_color = $background + shadow_boost = 0.2 } diff --git a/hypr/hyprsunset.conf b/hypr/hyprsunset.conf new file mode 100644 index 0000000..9ad7a31 --- /dev/null +++ b/hypr/hyprsunset.conf @@ -0,0 +1,2 @@ +temperature = 4064 +gamma = 100 \ No newline at end of file diff --git a/hypr/layout.conf b/hypr/layout.conf deleted file mode 100644 index 4615a95..0000000 --- a/hypr/layout.conf +++ /dev/null @@ -1,4 +0,0 @@ -dwindle { - pseudotile = true - preserve_split = true -} diff --git a/hypr/monitors.conf b/hypr/monitors.conf deleted file mode 100644 index 1270eed..0000000 --- a/hypr/monitors.conf +++ /dev/null @@ -1,17 +0,0 @@ - -############## -## MONITORS ## -############## - -# Configure yout monitor(s) here! See https://wiki.hyprland.org/Configuring/Monitors for more information on how to do that! - -# Monitor Arguments -# arg0 -> monitor name(you can get monitor names with `hyprctl monitors`); -# arg1 -> resolution@hertz; -# arg2 -> positioning from the top-left corner; -# arg3 -> scaling; -# arg4 -> variable refresh rate for games(optional); -# arg5 -> 1: vrr enabled, 0: no vrr. - -monitor = HDMI-A-1, 1920x1080@75, 0x0, 1, vrr, 1 - diff --git a/hypr/nvidia.conf b/hypr/nvidia.conf deleted file mode 100644 index 7ea1013..0000000 --- a/hypr/nvidia.conf +++ /dev/null @@ -1,15 +0,0 @@ - -##################### -## NVIDIA SETTINGS ## -##################### - -env = LIBVA_DRIVER_NAME, nvidia -env = GBM_BACKEND, nvidia-drm -env = __GLX_VENDOR_LIBRARY_NAME, nvidia -env = _VK_LAYER_NV_optimus, NVIDIA_only -env = __NV_PRIME_RENDER_OFFLOAD, 1 - -cursor { - # Set to true if you have issues - no_hardware_cursors = true -} diff --git a/hypr/plugins.conf b/hypr/plugins.conf deleted file mode 100644 index 7770f70..0000000 --- a/hypr/plugins.conf +++ /dev/null @@ -1,14 +0,0 @@ -#################### -# HYPRLAND PLUGINS # -#################### - -# You can add your preferred plugins here. Get help on how to do so: https://wiki.hyprland.org/Plugins/Using-Plugins/ - -plugin { - # Example plugin configuration - #hyprbars { - # bar_height = 24 - # hyprbars-button = rgb(ff4040), 16, 󰖭, hyprctl dispatch killactive - # hyprbars-button = rgb(eeee11), 16, , hyprctl dispatch fullscreen 1 - #} -} diff --git a/hypr/rules.conf b/hypr/rules.conf deleted file mode 100644 index 0357e34..0000000 --- a/hypr/rules.conf +++ /dev/null @@ -1,81 +0,0 @@ - -############# -## RULES ## -############# - -# See https://wiki.hyprland.org/Configuring/Window-Rules/ -# and https://wiki.hyprland.org/Configuring/Workspace-Rules/ -# for information on how to configure this - -# Floating windows -windowrulev2 = float, class:moe.launcher.* -windowrulev2 = float, class:com.github.rafostar.Clapper -windowrulev2 = float, class:xdg-desktop-portal* -windowrulev2 = float, class:org.pulseaudio.pavucontrol -windowrulev2 = float, class:blueberry.py -windowrulev2 = float, class:org.gnome.Loupe -windowrulev2 = float, class:mcpelauncher-webview -windowrulev2 = float, class:org.gnome.Calculator -windowrulev2 = float, class:io.mrarm.mcpelauncher-ui-qt -windowrulev2 = float, class:Resources -windowrulev2 = float, class:io.github.kaii_lb.Overskride - -# Resize -windowrulev2 = size 50% 50%, class:org.pulseaudio.pavucontrol -windowrulev2 = size 50% 50%, class:blueberry.py -windowrulev2 = size 50% 50%, class:io.github.kaii_lb.Overskride -windowrulev2 = size 70% 70%, class:io.mrarm.mcpelauncher-ui-qt - -# Moving -windowrulev2 = move 49.27% 7.28%, class:org.pulseaudio.pavucontrol -windowrulev2 = move 49.27% 7.28%, class:blueberry.py -windowrulev2 = move 49.27% 7.28%, class:io.github.kaii_lb.Overskride -windowrulev2 = movetoworkspace e, class:org.pulseaudio.pavucontrol - -# Animations -windowrulev2 = animation slide right, class:org.pulseaudio.pavucontrol -windowrulev2 = animation slide right, class:blueberry.py -windowrulev2 = animation slide right, class:io.github.kaii_lb.Overskride -layerrule = animation slide right, swaync-control-center -layerrule = animation fade, selection -layerrule = animation fade, waybar -layerrule = animation fade, hyprpaper -layerrule = animation slide right, swaync-notification-window -layerrule = animation fade, hyprpicker -layerrule = animation fade, anyrun -layerrule = animation slide right, eww-cc -layerrule = animation fade, eww-calendar -layerrule = animation fade, eww-volume -layerrule = animation fade, eww-powermenu - -# Opacity -windowrulev2 = opacity .95 .95, class:kitty -windowrulev2 = opacity .88 .88, class:spotify -windowrulev2 = opacity .88 .88, class:hyprpolkitagent - -# No blur -windowrulev2 = noblur, class:^()$, title:^()$ # Removes blur from context menus -windowrulev2 = noblur, class:steam(.*)$ - -# Window Blur list -blurls = logout_dialog -blurls = kitty - -# Layer Blur list -layerrule = blur, waybar -layerrule = blur, eww-bar -layerrule = blur, eww-calendar -layerrule = blur, eww-cc -layerrule = blur, eww-volume -layerrule = blur, eww-powermenu -layerrule = ignorealpha .6, eww-volume -layerrule = ignorealpha .55, eww-bar -layerrule = ignorealpha .5, eww-calendar -layerrule = ignorealpha .7, eww-cc - -# Workspace Rules -#workspace = 1, persistent:true -#workspace = 2, persistent:true - -# Suppress maximize event from windows -windowrulev2 = suppressevent maximize, class:.* diff --git a/hypr/scripts/change-wallpaper.sh b/hypr/scripts/change-wallpaper.sh index 06c8b4d..bde8883 100644 --- a/hypr/scripts/change-wallpaper.sh +++ b/hypr/scripts/change-wallpaper.sh @@ -1,90 +1,75 @@ -#!/usr/bin/env bash +#!usr/bin/env bash - -# Prompts the user with anyrun or wofi to choose an image file inside -# the defined $WALLPAPERS_DIR. If the user selects an entry, it automatically -# writes changes to the hyprpaper.conf file and hot-reloads wallpaper if hyprpaper -# is running. +# 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 +# an entry, it automatically writes changes to the hyprpaper.conf file and +# hot-reloads if hyprpaper is running. # -------------- # Licensed under the MIT License # Made by retrozinndev (JoĆ£o Dias) # From https://github.com/retrozinndev/Hyprland-Dots -HYPRPAPER_FILE="$HOME/.config/hypr/hyprpaper.conf" -COLORSCHEME_STYLE="darken" # lighten / darken +style="lighten" # lighten / darken +dmenu=$(sh "$XDG_CONFIG_HOME/hypr/scripts/get-dmenu.sh") -if [[ $WALLPAPERS_DIR == "" ]]; then +if [[ -z "$WALLPAPERS_DIR" ]]; then WALLPAPERS_DIR="$HOME/wallpapers" fi -if [[ -f /bin/anyrun ]]; then - WALLPAPER_SELECT_CMD="anyrun --plugins libstdin.so" -elif [[ -f /bin/wofi ]]; then - WALLPAPER_SELECT_CMD="wofi --dmenu" -else +function Write_changes() { + echo "[LOG] Writing to hyprpaper config file" + + echo \ +'$wallpaper'" = $wall + +splash = true +preload = "'$wallpaper'" +wallpaper = , "'$wallpaper'"" | sed -e "s/^(\\[n])//g" > $XDG_CONFIG_HOME/hypr/hyprpaper.conf +} + +function Reload_wallpaper() { + echo "[LOG] Hot-reloading wallpaper" + hyprctl hyprpaper unload all + hyprctl hyprpaper preload $wall + hyprctl hyprpaper wallpaper ", $wall" +} + +function Reload_pywal() { + echo "[LOG] Reloading pywal colorscheme" + 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 $WALLPAPERS_DIR) ]]; 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 -function Write_changes() { - echo "Writing to hyprpaper config file..." +if [[ -z $1 ]]; then + # Prompt wallpaper list + wall="$WALLPAPERS_DIR/$(ls $WALLPAPERS_DIR | $dmenu)" - echo \ -'$wallpaper'" = $SET_WALLPAPER_FULL - -splash = true -preload = "'$wallpaper'" -wallpaper = , "'$wallpaper'"" | sed -e "s/^(\\[n])//g" > $HYPRPAPER_FILE -} - -Reload_wallpaper() { - echo "Hot-reloading wallpaper..." - hyprctl hyprpaper unload all - hyprctl hyprpaper preload "$SET_WALLPAPER_FULL" - hyprctl hyprpaper wallpaper ", $SET_WALLPAPER_FULL" -} - -Reload_pywal() { - echo "Reloading pywal colorscheme..." - wal -q -t --cols16 $COLORSCHEME_STYLE -i "$SET_WALLPAPER_FULL" -} - -Reload_eww() { - echo "Reloading Eww..." - eww reload -} - -# Prompt wallpapers via dmenu -SET_WALLPAPER_NAME="$(ls $WALLPAPERS_DIR | $WALLPAPER_SELECT_CMD)" -SET_WALLPAPER_FULL="$WALLPAPERS_DIR/$SET_WALLPAPER_NAME" - -echo "Selected wallpaper: $SET_WALLPAPER_NAME" - -# Check if input wallpaper is empty -if [[ $SET_WALLPAPER_NAME == "" ]] || [[ $SET_WALLPAPER_NAME == " " ]] -then - echo "No wallpaper has been selected by user!" - if [[ $RANDOM_WALLPAPER_WHEN_EMPTY == true ]] - then - SET_WALLPAPER_NAME=$(ls $WALLPAPERS_DIR | shuf -n 1) - echo "Selected random wallpaper from $HOME/wallpapers: $SET_WALLPAPER_NAME" - SET_WALLPAPER_FULL="$WALLPAPERS_DIR/$SET_WALLPAPER_NAME" - - else - echo "Skipping hyprpaper changes and exiting." - exit 1 + # Check if input wallpaper is empty + if [[ $wall == "$WALLPAPERS_DIR/" ]]; 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" + else + echo "Skipping hyprpaper changes and exiting." + exit 0 + fi fi +else + wall=$1 fi Reload_pywal Reload_wallpaper -Reload_eww Write_changes exit 0 diff --git a/hypr/scripts/clipboard-menu.sh b/hypr/scripts/clipboard-menu.sh new file mode 100644 index 0000000..8932880 --- /dev/null +++ b/hypr/scripts/clipboard-menu.sh @@ -0,0 +1,7 @@ +#!/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/eww/scripts/color-picker.sh b/hypr/scripts/color-picker.sh similarity index 71% rename from eww/scripts/color-picker.sh rename to hypr/scripts/color-picker.sh index 77786ff..8febeb7 100644 --- a/eww/scripts/color-picker.sh +++ b/hypr/scripts/color-picker.sh @@ -1,18 +1,18 @@ #!/usr/bin/env bash -send_notification() { +function send_notification() { notify-send -u normal -a "Color Picker" "$1" "$2" } # Check if user has hyprpicker installed if ! [[ -f /bin/hyprpicker ]]; then - send_notification "An error occurred" "Looks like you don't have Hyprpicker installed! Try installing it before using the Color Picker tool." + 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) +selected_color=$(hyprpicker | tail -n 1 | xargs) -if ! [[ $selected_color == "" ]]; then +if ! [[ -z $selected_color ]]; then wl-copy $selected_color send_notification "Selected Color" "The selected color is $selected_color, it was also copied to your clipboard!" fi diff --git a/hypr/scripts/gen-pywal.sh b/hypr/scripts/gen-pywal.sh index 1e45e06..611c2a8 100644 --- a/hypr/scripts/gen-pywal.sh +++ b/hypr/scripts/gen-pywal.sh @@ -7,13 +7,13 @@ # Made by retrozinndev (JoĆ£o Dias) # From https://github.com/retrozinndev/Hyprland-Dots -wallpaper="$(cat $HOME'/.config/hypr/hyprpaper.conf' | grep '$wallpaper =' | sed -e 's/^$wallpaper = //')" - -if ! [[ -f "$wallpaper" ]]; then +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 diff --git a/hypr/scripts/get-dmenu.sh b/hypr/scripts/get-dmenu.sh new file mode 100644 index 0000000..09d019c --- /dev/null +++ b/hypr/scripts/get-dmenu.sh @@ -0,0 +1,26 @@ +#!/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/Hyprland-Dots + +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 new file mode 100644 index 0000000..927567e --- /dev/null +++ b/hypr/scripts/load-hyprsunset.sh @@ -0,0 +1,34 @@ +#!/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/Hyprland-Dots + +[[ -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/night-light-check.sh b/hypr/scripts/night-light-check.sh deleted file mode 100644 index 31a226c..0000000 --- a/hypr/scripts/night-light-check.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash - -# This script checks if night light was enabled last -# boot, and runs hyprsunset if that's the case. -# --------- -# Licensed under the MIT License -# Made by retrozinndev (JoĆ£o Dias) -# From https://github.com/retrozinndev/Hyprland-Dots - - -if [[ -f "$HOME/.cache/night-light.pid" ]]; then - rm "$HOME/.cache/night-light.pid" - sh $HOME/.config/eww/scripts/night-light.sh -fi diff --git a/hypr/scripts/notification-handler-daemon.sh b/hypr/scripts/notification-handler-daemon.sh deleted file mode 100644 index 02e3f14..0000000 --- a/hypr/scripts/notification-handler-daemon.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash - -# This script runs notification-handler.sh script -# and handles any issues that happens with it. -# -------- -# Licensed under the MIT License -# Made by retrozinndev (JoĆ£o Dias) -# From https://github.com/retrozinndev/Hyprland-Dots - -logfile="$HOME/.cache/notification-handler-daemon.log" -handler_script="$HOME/.config/eww/scripts/notification-handler.sh" - -function Send_log() { - echo "[$1]${@/$1}" >> $logfile -} - -function Exit_daemon() { - Send_log "info" "Handler exited normally, quitting daemon." - exit 0 -} - -function Restart_handler() { - bash $handler_script & - pid_handler=$! - Send_log "info" "Handler started!" - Send_log "info" "Handler script PID: $pid_handler" - wait $pid_handler && Exit_daemon || \ - (Send_log "error" "An error occurred and handler stopped" - Send_log "info" "Clearing history and starting handler again." - sh $HOME/.config/eww/scripts/notification-clear.sh & - Restart_handler) -} - -trap "Send_log 'info' 'SIGINT received, stopping daemon and handler' ; kill \$pid_handler ; exit 1" SIGINT -trap "Send_log 'info' 'SIGTERM received, stopping daemon and handler' ; kill \$pid_handler ; exit 1" SIGTERM - -echo -e '' > $logfile -Send_log "info" "Starting Daemon..." -Send_log "info" "Daemon script PID: $$" -Restart_handler diff --git a/hypr/scripts/save-hyprsunset.sh b/hypr/scripts/save-hyprsunset.sh new file mode 100644 index 0000000..601cb36 --- /dev/null +++ b/hypr/scripts/save-hyprsunset.sh @@ -0,0 +1,29 @@ +#!/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/Hyprland-Dots + +[[ -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/hypr/scripts/screenshot.sh b/hypr/scripts/screenshot.sh new file mode 100644 index 0000000..6121146 --- /dev/null +++ b/hypr/scripts/screenshot.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +# This script handles taking a screenshot using the +# hyprshot tool. +# -------------- +# Licensed under the MIT License +# Made by retrozinndev (JoĆ£o Dias) +# From https://github.com/retrozinndev/Hyprland-Dots + + +# exit slurp and quit if slurp(region selection) is running +killall slurp && exit 0 + +if [[ -z $(command -v hyprshot) ]]; then + echo "[err] you don't have hyprshot installed, please install it first" + exit 1 +fi + +if [[ "$1" == "full" ]]; then + hyprshot -m active -m output -o "$(xdg-user-dir PICTURES)/Screenshots" + exit 0 +fi + +hyprshot -m region -o "$(xdg-user-dir PICTURES)/Screenshots" diff --git a/hypr/scripts/trigger-volume-popup.sh b/hypr/scripts/trigger-volume-popup.sh deleted file mode 100644 index 76b7e84..0000000 --- a/hypr/scripts/trigger-volume-popup.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -# This script triggers the Eww Volume Popup Window -# --------- -# Licensed under the MIT License -# Made by retrozinndev (JoĆ£o Dias) -# From https://github.com/retrozinndev/Hyprland-Dots - diff --git a/hypr/shell/autostart.conf b/hypr/shell/autostart.conf new file mode 100644 index 0000000..f2f4cea --- /dev/null +++ b/hypr/shell/autostart.conf @@ -0,0 +1,21 @@ + +# color-shell configuration, please don't modify unless you know what you're doing! + +# Daemons +# exec-once = /usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1 # GNOME PolKit +exec-once = systemctl enable --user --now hyprpolkitagent # Hyprland's PolKit +exec-once = systemctl enable --user --now hypridle +exec-once = systemctl enable --user --now gnome-keyring-daemon +exec-once = wl-paste --type text --watch cliphist store +exec-once = wl-paste --type image --watch cliphist store + +# Tools +exec-once = systemctl enable --user --now hyprsunset +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 = ags run diff --git a/hypr/bindings.conf b/hypr/shell/bindings.conf similarity index 55% rename from hypr/bindings.conf rename to hypr/shell/bindings.conf index 55bedc6..21f982d 100644 --- a/hypr/bindings.conf +++ b/hypr/shell/bindings.conf @@ -1,70 +1,48 @@ -############## -## BINDINGS ## -############## -# https://wiki.hyprland.org/Configuring/Keywords and https://wiki.hyprland.org/Configuring/Binds for information on how to configure input +# color-shell specific configuration, please don't modify unless you know what you're doing! -$terminal = kitty -$fileManager = nautilus -$menu = anyrun -$dmenu = anyrun --plugins libstdin.so -$mainMod = SUPER -$lockscreen = hyprlock -$screenshotDir = $HOME/Screenshots -$screenshotFull = hyprshot -m output -o $screenshotDir -$screenshotSelect = hyprshot -m region -o $screenshotDir -$media = amberol - - -# Main binds, see https://wiki.hyprland.org/Configuring/Binds/ for more -bind = $mainMod, K, exec, $terminal -bind = $mainMod, Q, killactive -bind = $mainMod, E, exec, $fileManager -bind = $mainMod, F, togglefloating bind = $mainMod, SPACE, exec, $menu -bind = $mainMod, P, pseudo, -bind = $mainMod, J, togglesplit -bind = $mainMod, N, exec, sh $HOME/.config/eww/scripts/eww-window.sh toggle control-center -bind = $mainMod, L, exec, $lockscreen bind = $mainMod, F11, fullscreen -# XF86 keys -bind = , XF86AudioMedia, exec, $media -bind = , XF86AudioLowerVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%- # Decrease volume -bind = , XF86AudioRaiseVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+ # Increase volume -bind = , XF86AudioMute, exec, wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle # Mute + +bind = , Print, exec, sh $XDG_CONFIG_HOME/hypr/scripts/screenshot.sh +bind = $mainMod, Print, exec, sh $XDG_CONFIG_HOME/hypr/scripts/screenshot.sh full + +# Test-only bind, restarts colorshell +bind = $mainMod, F7, exec, ags request reload + +bind = $mainMod, K, exec, $terminal +bind = $mainMod, Q, killactive +bind = $mainMod, E, 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, L, exec, hyprlock +bind = $mainMod, V, exec, astal runner '>' || sh $XDG_CONFIG_HOME/hypr/scripts/clipboard-menu.sh +bind = $mainMod, W, exec, astal runner '##' + +# bind = $mainMod, $mainMod_L, exec, astal toggle apps-window +bind = $mainMod, $mainMod_l, exec, astal peek-workspace-num + +bind = , XF86AudioLowerVolume, exec, astal volume sink-decrease 5 || wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%- # Decrease volume +bind = , 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 -# Brightness Keys bind = , XF86MonBrightnessDown, exec, brightnessctl s 5%- # Lower monitor brightness bind = , XF86MonBrightnessUp, exec, brightnessctl s +5% # Increase monitor brightness -# Screenshot -bind = , Print, exec, $screenshotSelect -bind = $mainMod, Print, exec, $screenshotFull - -# Open clipboard -bind = $mainMod, V, exec, cliphist list | $dmenu | cliphist decode | xargs -r wl-copy - -# Open wallpaper menu -bind = $mainMod, W, exec, env bash $HOME/.config/hypr/scripts/change-wallpaper.sh - -# Reload binds -# Eww -bind = $mainMod, F7, exec, eww reload -# Hyprpaper (Wallpaper) -bind = $mainMod, F8, exec, pkill hyprpaper && hyprpaper -# Dunst (Notification daemon) -bind = $mainMod, F9, exec, pkill dunst && dunst - # Move focus with mainMod + arrow keys bind = $mainMod, left, movefocus, l bind = $mainMod, right, movefocus, r bind = $mainMod, up, movefocus, u bind = $mainMod, down, movefocus, d + # Move windows with keyboard keys bind = $mainMod SHIFT, left, movewindow, l bind = $mainMod SHIFT, right, movewindow, r @@ -72,11 +50,18 @@ bind = $mainMod SHIFT, up, movewindow, u bind = $mainMod SHIFT, down, movewindow, d bind = $mainMod SHIFT, C, centerwindow -# Resize windows with arrowkeys + +# Resize windows with arrow keys / hjkl bind = $mainMod ALT, left, resizeactive, -60 0 -bind = $mainMod ALT, right, resizeactive, 60 0 -bind = $mainMod ALT, up, resizeactive, 0 -60 bind = $mainMod ALT, down, resizeactive, 0 60 +bind = $mainMod ALT, up, resizeactive, 0 -60 +bind = $mainMod ALT, right, resizeactive, 60 0 + +bind = $mainMod ALT, H, resizeactive, -60 0 +bind = $mainMod ALT, J, resizeactive, 0 60 +bind = $mainMod ALT, K, resizeactive, 0 -60 +bind = $mainMod ALT, L, resizeactive, 60 0 + # Switch workspaces with mainMod + [0-9] bind = $mainMod, 1, workspace, 1 @@ -105,12 +90,8 @@ bind = $mainMod SHIFT, 0, movetoworkspace, 10 bind = CTRL $mainMod, right, workspace, e+1 bind = CTRL $mainMod, left, workspace, e-1 -bind = $mainMod, S, togglespecialworkspace, magic -bind = $mainMod SHIFT, S, movetoworkspace, special:magic - -# Scroll through existing workspaces with mainMod + scroll -bind = $mainMod, mouse_down, workspace, e+1 -bind = $mainMod, mouse_up, workspace, e-1 +bind = $mainMod, S, togglespecialworkspace, special +bind = $mainMod SHIFT, S, movetoworkspace, special:special # Move/resize windows with mainMod + LMB/RMB and dragging bindm = $mainMod, mouse:272, movewindow diff --git a/hypr/decorations.conf b/hypr/shell/decorations.conf similarity index 67% rename from hypr/decorations.conf rename to hypr/shell/decorations.conf index 15aa8df..460786c 100644 --- a/hypr/decorations.conf +++ b/hypr/shell/decorations.conf @@ -1,11 +1,7 @@ -################ -## DECORATION ## -################ +# color-shell configuration, please don't modify unless you know what you're doing! -# See https://wiki.hyprland.org/Configuring/Variables for more information on how to configure appearance - -source = ~/.cache/wal/colors-hypr.conf +source = ~/.cache/wal/colors-hyprland.conf general { gaps_in = 6 @@ -23,16 +19,16 @@ general { layout = dwindle } -#render { -# ctm_animation = 1 -#} +render { + ctm_animation = 1 +} misc { - animate_manual_resizes = false + animate_manual_resizes = true } decoration { - rounding = 14 + rounding = 16 # Active Window Opacity active_opacity = 1.0 @@ -41,8 +37,8 @@ decoration { shadow { enabled = true - range = 1 - render_power = 2 + range = 4 + render_power = 4 color = $background } @@ -53,15 +49,15 @@ decoration { size = 2 passes = 5 vibrancy = 0.9 - popups = true - popups_ignorealpha = 0.5 + # popups = true + # popups_ignorealpha = 0.6 } } animations { enabled = true - # Beziers + # Beziers (I need better names) bezier = myBezier, 0.05, 0.9, 0.1, 1.05 bezier = amazingBezier, 0.25, 0.59, 0.1, 1.05 bezier = popinBezier, 0.5, .1, .05, 1.0 @@ -72,19 +68,20 @@ animations { # Windows animation = windowsIn, 1, 6, myBezier, slide animation = windowsOut, 1, 5, amazingBezier, slide - animation = windowsMove, 1, 4.5, amazingBezier + animation = windowsMove, 1, 4.5, myBezier # Layers animation = layersIn, 1, 6, layerBezier, slide animation = layersOut, 1, 6, layerBezier, slide # Workspaces - animation = workspaces, 1, 3.5, workspaceBezier, slidefade 20% + animation = workspaces, 1, 3.5, workspaceBezier, slidefade 16% + animation = specialWorkspace, 1, 3.5, myBezier, slidefadevert 10% # Fade animation = fade, 1, 4, myBezier - animation = fadeLayersIn, 1, 3, layerBezier - animation = fadeLayersOut, 1, 4, layerBezier + animation = fadeLayersIn, 1, 4, layerBezier + animation = fadeLayersOut, 1, 3, layerBezier # Others animation = border, 1, 5.5, amazingBezier diff --git a/hypr/environment.conf b/hypr/shell/environment.conf similarity index 65% rename from hypr/environment.conf rename to hypr/shell/environment.conf index f940399..091d622 100644 --- a/hypr/environment.conf +++ b/hypr/shell/environment.conf @@ -1,7 +1,5 @@ -################# -## ENVIRONMENT ## -################# +# color-shell configuration, please don't modify unless you know what you're doing! # XDG Vars env = XDG_CONFIG_HOME, $HOME/.config @@ -17,9 +15,15 @@ env = XCURSOR_SIZE, 24 env = HYPRCURSOR_THEME, Adwaita env = HYPRCURSOR_SIZE, 24 -# Others +# Wayland stuff +env = MOZ_ENABLE_WAYLAND, 1 +env = ELECTRON_OZONE_PLATFORM_HINT, auto + +# QT env = QT_QPA_PLATFORM, wayland env = QT_QPA_PLATFORMTHEME, qt5ct env = QT_AUTO_SCREEN_SCALE_FACTOR, 1 -env = ADW_DISABLE_PORTAL, 1 -env = MOZ_ENABLE_WAYLAND, 1 + +# Others +env = ADW_DISABLE_PORTAL, 1 # Fixes prefer-dark theme setting in some flatpak apps +env = WALLPAPERS, $HOME/wallpapers diff --git a/hypr/shell/hyprland.conf b/hypr/shell/hyprland.conf new file mode 100644 index 0000000..60e5c02 --- /dev/null +++ b/hypr/shell/hyprland.conf @@ -0,0 +1,11 @@ + +# color-shell configuration, please don't modify unless you know what you're doing! + +source = ./variables.conf +source = ./nvidia.conf +source = ./environment.conf +source = ./bindings.conf +source = ./decorations.conf +source = ./autostart.conf +source = ./rules.conf +source = ./layout.conf diff --git a/hypr/shell/layout.conf b/hypr/shell/layout.conf new file mode 100644 index 0000000..f8614df --- /dev/null +++ b/hypr/shell/layout.conf @@ -0,0 +1,8 @@ + +# color-shell configuration, please don't modify unless you know what you're doing! + +dwindle { + pseudotile = false + preserve_split = true + smart_resizing = true +} diff --git a/hypr/shell/nvidia.conf b/hypr/shell/nvidia.conf new file mode 100644 index 0000000..bc5bf6e --- /dev/null +++ b/hypr/shell/nvidia.conf @@ -0,0 +1,13 @@ +################## +## NVIDIA STUFF ## +################## +# Wiki: https://wiki.hyprland.org/Configuring/Environment-variables/#nvidia-specific + + +env = LIBVA_DRIVER_NAME, nvidia +env = __GLX_VENDOR_LIBRARY_NAME, nvidia +env = NVD_BACKEND, direct + +cursor { + no_hardware_cursors = false # Set to true/false if you have issues +} diff --git a/hypr/shell/rules.conf b/hypr/shell/rules.conf new file mode 100644 index 0000000..74e1a0d --- /dev/null +++ b/hypr/shell/rules.conf @@ -0,0 +1,71 @@ + +# color-shell configuration, please don't modify unless you know what you're doing! + +# Float +windowrule = float, class:nm-connection-editor +windowrule = float, class:org.pulseaudio.pavucontrol +windowrule = float, class:xdg-desktop-portal.* +windowrule = float, class:io.github.kaii_lb.Overskride + + +# Resize +windowrule = size 50% 50%, class:org.pulseaudio.pavucontrol +windowrule = size 50% 50%, class:io.github.kaii_lb.Overskride +windowrule = size 68% 65%, class:xdg-desktop-portal.* + + +# Position +windowrule = move 49.27% 7.28%, class:org.pulseaudio.pavucontrol +windowrule = move 49.27% 7.28%, class:io.github.kaii_lb.Overskride + + +# Workspace +windowrule = movetoworkspace e, class:org.pulseaudio.pavucontrol + + +# Animations +windowrule = animation gnomed, class:hyprpolkitagent +windowrule = animation slide right, class:org.pulseaudio.pavucontrol +windowrule = animation slide right, class:io.github.kaii_lb.Overskride +windowrule = animation gnomed, class:xdg-desktop-portal.* + +layerrule = animation fade, selection +layerrule = animation fade, hyprpicker +layerrule = animation fade, hyprpaper +layerrule = animation fade, anyrun +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, .*-popup + + +# Blur +windowrule = noblur, class:^()$, title:^()$ # fixes chromium/electron context menus + +layerrule = blur, top-bar +layerrule = blur, osd +layerrule = blur, control-center +layerrule = blur, center-window +layerrule = blur, logout-menu +layerrule = blur, runner +layerrule = blur, ask-popup +layerrule = blur, entry-popup +layerrule = blur, floating-notifications +layerrule = blur, apps-window +layerrule = ignorealpha .7, runner +layerrule = ignorealpha .4, osd +layerrule = ignorealpha .55, top-bar +layerrule = ignorealpha .6, ask-popup +layerrule = ignorealpha .6, entry-popup +layerrule = ignorealpha .7, control-center +layerrule = ignorealpha .5, apps-window +layerrule = ignorealpha .7, center-window +layerrule = ignorealpha .7, floating-notifications + + +# Suppress maximize event from windows +windowrule = suppressevent maximize, class:.* diff --git a/hypr/shell/variables.conf b/hypr/shell/variables.conf new file mode 100644 index 0000000..de75150 --- /dev/null +++ b/hypr/shell/variables.conf @@ -0,0 +1,13 @@ + +# color-shell configuration, please don't modify unless you know what you're doing! + +############### +## VARIABLES ## +############### +# Wiki: https://wiki.hyprland.org/Hypr-Ecosystem/hyprlang#defining-variables + +$mainMod = SUPER +$terminal = kitty +$fm = nautilus +$menu = astal runner || anyrun +$dmenu = anyrun --plugins libstdin.so diff --git a/hypr/user/autostart.conf b/hypr/user/autostart.conf new file mode 100644 index 0000000..fb2a824 --- /dev/null +++ b/hypr/user/autostart.conf @@ -0,0 +1,9 @@ + +############### +## AUTOSTART ## +############### +# Wiki: https://wiki.hyprland.org/Configuring/Keywords/#executing + +# Apps +exec-once = discord --start-minimized +exec-once = steam -silent diff --git a/hypr/user/bindings.conf b/hypr/user/bindings.conf new file mode 100644 index 0000000..61daad4 --- /dev/null +++ b/hypr/user/bindings.conf @@ -0,0 +1,8 @@ +############## +## BINDINGS ## +############## +# Wiki: https://wiki.hyprland.org/Configuring/Binds + + +# Uncomment if you want to press SUPER to launch application search +# bind = $mainMod, $mainMod_L, exec, astal toggle apps-window diff --git a/hypr/user/decorations.conf b/hypr/user/decorations.conf new file mode 100644 index 0000000..5600fee --- /dev/null +++ b/hypr/user/decorations.conf @@ -0,0 +1,6 @@ +################ +## DECORATION ## +################ +# Wiki: https://wiki.hyprland.org/Configuring/Variables + + diff --git a/hypr/user/environment.conf b/hypr/user/environment.conf new file mode 100644 index 0000000..e7e2aa0 --- /dev/null +++ b/hypr/user/environment.conf @@ -0,0 +1,6 @@ +################# +## ENVIRONMENT ## +################# +# Wiki: https://wiki.hyprland.org/Configuring/Keywords/#setting-the-environment + + diff --git a/hypr/user/hyprland.conf b/hypr/user/hyprland.conf new file mode 100644 index 0000000..13dee9d --- /dev/null +++ b/hypr/user/hyprland.conf @@ -0,0 +1,15 @@ + +######################## +## USER CONFIGURATION ## +######################## + +# This sources all user configuration files + +source = ./monitors.conf +source = ./environment.conf +source = ./input.conf +source = ./bindings.conf +source = ./layout.conf +source = ./decorations.conf +source = ./autostart.conf +source = ./rules.conf diff --git a/hypr/user/input.conf b/hypr/user/input.conf new file mode 100644 index 0000000..8e60fa7 --- /dev/null +++ b/hypr/user/input.conf @@ -0,0 +1,34 @@ +########### +## INPUT ## +########### + +# Wiki: https://wiki.hyprland.org/Configuring/Keywords/#per-device-input-configs + + +input { + kb_layout = us + kb_variant = intl + kb_model = pc105 + + numlock_by_default = true + follow_mouse = 1 + + sensitivity = 0 # -1.0 to 1.0, 0 means no modification. + + touchpad { + natural_scroll = true + } +} + +############## +## GESTURES ## +############## + +# Wiki: https://wiki.hyprland.org/Configuring/Variables/#gestures + + +gestures { + workspace_swipe = true + workspace_swipe_fingers = 3 + workspace_swipe_create_new = true +} diff --git a/hypr/user/layout.conf b/hypr/user/layout.conf new file mode 100644 index 0000000..8d1b881 --- /dev/null +++ b/hypr/user/layout.conf @@ -0,0 +1,4 @@ +############ +## LAYOUT ## +############ +# Wiki: https://wiki.hyprland.org/Configuring/Dwindle-Layout/#config diff --git a/hypr/user/monitors.conf b/hypr/user/monitors.conf new file mode 100644 index 0000000..afcc7ff --- /dev/null +++ b/hypr/user/monitors.conf @@ -0,0 +1,16 @@ +############## +## MONITORS ## +############## + +# Wiki: https://wiki.hyprland.org/Configuring/Monitors + + +# Monitor +# arg0 -> monitor name(you can get monitor names with `hyprctl monitors`); +# arg1 -> resolution@hertz; +# arg2 -> positioning from the top-left corner; +# arg3 -> scaling; +# arg4 -> variable refresh rate(optional); +# - arg40 -> 1: vrr, 0: no vrr. + +monitor = HDMI-A-1, 1920x1080@75, 0x0, 1, vrr, 0 diff --git a/hypr/user/rules.conf b/hypr/user/rules.conf new file mode 100644 index 0000000..c70d8cc --- /dev/null +++ b/hypr/user/rules.conf @@ -0,0 +1,39 @@ + +############################ +## WINDOW & LAYER RULES ## +############################ + +# See https://wiki.hyprland.org/Configuring/Window-Rules for +# more information on how to do this + +# Floating windows +windowrule = float, class:moe.launcher.* +windowrule = float, class:com.github.rafostar.Clapper +windowrule = float, class:blueberry.py +windowrule = float, class:org.gnome.Loupe +windowrule = float, class:mcpelauncher-webview +windowrule = float, class:org.gnome.Calculator +windowrule = float, class:io.mrarm.mcpelauncher-ui-qt +windowrule = float, class:Resources + +# Resize +windowrule = size 50% 50%, class:blueberry.py +windowrule = size 70% 70%, class:io.mrarm.mcpelauncher-ui-qt + +# Moving +windowrule = move 49.27% 7.28%, class:blueberry.py + +# Animations +windowrule = animation gnomed, class:moe.launcher..* + + +# Opacity +windowrule = opacity .95 .95, class:kitty +windowrule = opacity .88 .88, class:spotify +windowrule = opacity .88 .88, class:hyprpolkitagent + +# No blur +windowrule = noblur, class:steam(.*)$ + +# Others +windowrule = noinitialfocus, class:Anydesk, title:anydesk diff --git a/kitty/kitty.conf b/kitty/kitty.conf index 40ea84e..448b635 100644 --- a/kitty/kitty.conf +++ b/kitty/kitty.conf @@ -1,13 +1,19 @@ -# Imports include ../../.cache/wal/colors-kitty.conf # Style font_family 0xProto Nerd Font +font_size 10.8 +font_weight 340 cursor_trail 6 window_padding_width 5 +# Rendering +text_gamma_adjustment 1.7 +text_contrast 30 + # General confirm_os_window_close 0 + # Dinamically update colorscheme with pywal16 allow_remote_control yes listen_on unix:@mykitty diff --git a/mako/config b/mako/config deleted file mode 100644 index 6c18e0d..0000000 --- a/mako/config +++ /dev/null @@ -1,46 +0,0 @@ -# Lightweight Wayland Notification Daemon (mako) Configuration File -# the man page of mako config file is a mess :skull: - -# general -max-history=20 -sort=-time -actions=1 -history=1 -icons=1 -default-timeout=1 -ignore-timeout=1 - -# display -layer=overlay -anchor=top-right - -# binding -on-touch=invoke-default-action - -# style -font=Cantarell 12 -background-color=#1b2024 -padding=12 -width=420 -border-size=1 -border-color=#7e7e7ef0 -border-radius=14 -text-alignment=left -margin=16 -max-icon-size=72 - -# [urgency="low"] -# default-timeout=1 - -# [urgency="normal"] -# default-timeout=1 - -# [urgency="critical"] -# default-timeout=1 - -# modes -[mode="dnd"] -invisible=1 - -# vim: ft=cfg -# nvim: ft=cfg diff --git a/repo/shots/control_center.png b/repo/old_shots/eww editon/control_center.png similarity index 100% rename from repo/shots/control_center.png rename to repo/old_shots/eww editon/control_center.png diff --git a/repo/old_shots/eww editon/desktop.png b/repo/old_shots/eww editon/desktop.png new file mode 100644 index 0000000..159ea97 Binary files /dev/null and b/repo/old_shots/eww editon/desktop.png differ diff --git a/repo/shots/floating_github.png b/repo/old_shots/eww editon/floating_github.png similarity index 100% rename from repo/shots/floating_github.png rename to repo/old_shots/eww editon/floating_github.png diff --git a/repo/shots/floating_media.png b/repo/old_shots/eww editon/floating_media.png similarity index 100% rename from repo/shots/floating_media.png rename to repo/old_shots/eww editon/floating_media.png diff --git a/repo/shots/lock_screen.png b/repo/old_shots/eww editon/lock_screen.png similarity index 100% rename from repo/shots/lock_screen.png rename to repo/old_shots/eww editon/lock_screen.png diff --git a/repo/shots/matrix_cava_spotify.png b/repo/old_shots/eww editon/matrix_cava_spotify.png similarity index 100% rename from repo/shots/matrix_cava_spotify.png rename to repo/old_shots/eww editon/matrix_cava_spotify.png diff --git a/repo/shots/notification_popup.png b/repo/old_shots/eww editon/notification_popup.png similarity index 100% rename from repo/shots/notification_popup.png rename to repo/old_shots/eww editon/notification_popup.png diff --git a/repo/shots/power_menu.png b/repo/old_shots/eww editon/power_menu.png similarity index 100% rename from repo/shots/power_menu.png rename to repo/old_shots/eww editon/power_menu.png diff --git a/repo/old_shots/eww editon/runner.png b/repo/old_shots/eww editon/runner.png new file mode 100644 index 0000000..111816a Binary files /dev/null and b/repo/old_shots/eww editon/runner.png differ diff --git a/repo/shots/showing_off_rice.png b/repo/old_shots/eww editon/showing_off_rice.png similarity index 100% rename from repo/shots/showing_off_rice.png rename to repo/old_shots/eww editon/showing_off_rice.png diff --git a/repo/shots/volume_control.png b/repo/old_shots/eww editon/volume_control.png similarity index 100% rename from repo/shots/volume_control.png rename to repo/old_shots/eww editon/volume_control.png diff --git a/repo/old_shots/eww_preview/anyrun.png b/repo/old_shots/eww preview/anyrun.png similarity index 100% rename from repo/old_shots/eww_preview/anyrun.png rename to repo/old_shots/eww preview/anyrun.png diff --git a/repo/old_shots/eww_preview/audio_thing.png b/repo/old_shots/eww preview/audio_thing.png similarity index 100% rename from repo/old_shots/eww_preview/audio_thing.png rename to repo/old_shots/eww preview/audio_thing.png diff --git a/repo/old_shots/eww_preview/calendar.png b/repo/old_shots/eww preview/calendar.png similarity index 100% rename from repo/old_shots/eww_preview/calendar.png rename to repo/old_shots/eww preview/calendar.png diff --git a/repo/old_shots/eww_preview/control_center.png b/repo/old_shots/eww preview/control_center.png similarity index 100% rename from repo/old_shots/eww_preview/control_center.png rename to repo/old_shots/eww preview/control_center.png diff --git a/repo/old_shots/eww_preview/desktop.png b/repo/old_shots/eww preview/desktop.png similarity index 100% rename from repo/old_shots/eww_preview/desktop.png rename to repo/old_shots/eww preview/desktop.png diff --git a/repo/old_shots/eww_preview/logout_menu.png b/repo/old_shots/eww preview/logout_menu.png similarity index 100% rename from repo/old_shots/eww_preview/logout_menu.png rename to repo/old_shots/eww preview/logout_menu.png diff --git a/repo/shots/apps-window.png b/repo/shots/apps-window.png new file mode 100644 index 0000000..cd4d212 Binary files /dev/null and b/repo/shots/apps-window.png differ diff --git a/repo/shots/browser-neovim.png b/repo/shots/browser-neovim.png new file mode 100644 index 0000000..b5e1fee Binary files /dev/null and b/repo/shots/browser-neovim.png differ diff --git a/repo/shots/control-center-bluetooth.png b/repo/shots/control-center-bluetooth.png new file mode 100644 index 0000000..8de2ae3 Binary files /dev/null and b/repo/shots/control-center-bluetooth.png differ diff --git a/repo/shots/desktop.png b/repo/shots/desktop.png index 159ea97..6f86b40 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 new file mode 100644 index 0000000..ed36b6e Binary files /dev/null and b/repo/shots/kitty.png differ diff --git a/repo/shots/runner.png b/repo/shots/runner.png index 111816a..984e9fe 100644 Binary files a/repo/shots/runner.png and b/repo/shots/runner.png differ diff --git a/repo/shots/volume-mixer.png b/repo/shots/volume-mixer.png new file mode 100644 index 0000000..e75d88d Binary files /dev/null and b/repo/shots/volume-mixer.png differ diff --git a/repo/shots/widgets.png b/repo/shots/widgets.png new file mode 100644 index 0000000..ae7ff88 Binary files /dev/null and b/repo/shots/widgets.png differ diff --git a/update-repo.sh b/update-repo.sh index 1e92ece..a3a4baa 100644 --- a/update-repo.sh +++ b/update-repo.sh @@ -1,46 +1,11 @@ #!/usr/bin/bash -HYPRLAND_DOTS_DIRS=("hypr" "eww" "anyrun" "kitty" "wal" "fastfetch" "mako") -WALLPAPERS_DIR="$HOME/wallpapers" - -printf "\n" - -echo "Running this script may override all data in current repo with current user dotfiles." -echo "This script is intended to be used by repository owner(retrozinndev)" - -printf "\n" - -echo "Please run this script in it's current directory to avoid problems." -echo "Tip: Press Ctrl + C to stop script at any time" - -printf "\n" - -Send_log() { - output_color="" - - if [[ $1 =~ ^inf(o)$ ]] - then - output_color="\e[34m" - fi - - if [[ $1 =~ ^warn(ing)$ ]] - then - output_color="\e[33m" - fi - - if [[ $1 =~ ^err(or)$ ]] - then - output_color="\e[31m" - fi - - echo -e "[${output_color}$1\e[0m] $2" -} +source ./utils.sh Check_current_dir() { - if ! [[ -f ./update-repo.sh ]] - then - Send_log "warning" "Looks like you're not in the repo dir! Please run this script from the repo to avoid problems." - printf "Quitting...\n" + if ! [[ -f ./utils.sh ]]; then + Send_log warn "Looks like you're not in the repository directory!\nPlease run this script from the repo to avoid problems." + Send_log "Exiting" sleep .5 exit 1 fi @@ -48,8 +13,7 @@ Check_current_dir() { Clean_local() { Send_log "info" "Cleaning current repo dotfiles..." - # Modify dirs here when adding something new: - for dir in ${HYPRLAND_DOTS_DIRS[@]}; do + for dir in ${config_dirs[@]}; do if [[ -d "./$dir" ]]; then rm -rf ./$dir fi @@ -61,30 +25,22 @@ Clean_local() { echo "Done cleaning." } -Check_existance() { - if [[ -d "./$1" ]]; then - return 0 - fi - - return 1 -} - Update_local() { - for dotsDir in ${HYPRLAND_DOTS_DIRS[@]}; do - if [[ -d "$HOME/.config/$dotsDir" ]]; then - Send_log "info" "Trying to copy ${dotsDir^}..." - cp -r $HOME/.config/$dotsDir ./$dotsDir + for dir in ${config_dirs[@]}; do + if [[ -d "$XDG_CONFIG_HOME/$dir" ]]; then + Send_log "Copying ${dir^}" + cp -r $XDG_CONFIG_HOME/$dir ./$dir else - Send_log "warn" "Looks like the ~/.config/$dotsDir dir is in fault! Skipping it..." + Send_log "warn" "Looks like the ~/.config/$dir dir is in fault! Skipping it..." fi done - if [[ -d $WALLPAPERS_DIR ]]; then - Send_log "info" "Copying wallpapers" + if [[ -d "$HOME/wallpapers" ]]; then + Send_log "Copying wallpapers" mkdir -p ./wallpapers - cp -rf $WALLPAPERS_DIR/* ./wallpapers + cp -rf $HOME/wallpapers/* ./wallpapers else - Send_log "warn" "Wallpapers dir could not be found in $HOME, skipping..." + Send_log warn "Wallpapers dir could not be found in $HOME, skipping..." fi printf "\nDone updating local repo!\n" @@ -92,7 +48,7 @@ Update_local() { Update_remote() { echo "Git status:" - /usr/bin/env git status + git status echo "Please type one of the dotfiles you want to push now(only one dir):" ls --color=auto -d -- */ printf "Directory: " @@ -127,35 +83,40 @@ Update_remote() { Check_current_dir -echo "Starting in 3... " -sleep 1s -echo "2..." -sleep 1s -echo "1..." -sleep 1s +Print_header + +printf "\n" +echo "!!WARNING!! Running this script may override all data in current repo with host configurations." +echo "This script is intended to be used only by retrozinndev/Hyprland-Dots repo owner" +printf "\n" + +echo "Please run this script in it's current directory to avoid problems." +echo "Tip: Press Ctrl + C to stop script at any time" + +printf "\n" + +echo -n "Do you want to update local repository with host configurations? [y/n] " +read answer +if ! [[ $answer =~ y ]]; then + Send_log "Exiting" + exit 1 +fi printf "\n" Clean_local - -# Updates local repository with current user dotfiles Update_local -echo -n "Would you like to push selected changes to remote? (You will be prompted to select folders) [y/n] " -read push_changes_to_remote +echo -n "Would you like to commit to remote? (You will be prompted for each directory and commit messages) [y/n] " +read answer -if [[ $push_changes_to_remote =~ y ]] -then +if [[ $answer =~ y ]]; then Update_remote - echo "Looks like it's done! Bye bye, have a great day!" + echo "Looks like it's done! Have a great day!" else - echo "Ok, work has been finished here! Bye bye!" + echo "Ok, work's finished here! Have a great day!" fi -if [[ -f "/usr/bin/git" ]] -then - printf "\nGit status: \n" - git status -fi +env git status exit 0 diff --git a/utils.sh b/utils.sh new file mode 100644 index 0000000..3652389 --- /dev/null +++ b/utils.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash + +# This script contains useful functions to be used +# in other scripts from retrozindev's dotfiles. +# ---------- +# Made by retrozinndev (JoĆ£o Dias) +# Licensed under the MIT License +# From: https://github.com/retrozinndev/Hyprland-dots + + +# ------------- +# Array containing directory names to be used by +# retrozinndev/Hyprland-Dots install and update +# scripts. +# ------------- +config_dirs=( + "hypr" + "ags" + "kitty" + "anyrun" + "wal" + "fastfetch" +) + +# ------------- +# Sends stdout log with type and message provided +# in parameters. +# param $1 (optional) log type (err[or], warn[ing]), if not any of list, print as info +# param $2 log message +# ------------- +function Send_log() { + log_message=$2 + + case ${1,,} in + "^warn(ing)$") + color="\e[33m" + log_type="warning" + ;; + + "^err(or)$") + 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" +} + +# ------------- +# Prints retrozinndev/Hyprland-Dots installation +# script's welcome header on stdout +# ------------- +function Print_header() { + printf "\n" + echo "######################################" + echo "## Retrozinndev's Hyprland Dotfiles ##" + echo "######################################" + printf "\n" +} diff --git a/wal/templates/colors-anyrun.css b/wal/templates/colors-anyrun.css index 7056a69..0eb2cdc 100644 --- a/wal/templates/colors-anyrun.css +++ b/wal/templates/colors-anyrun.css @@ -3,6 +3,7 @@ @define-color foreground {foreground}; @define-color cursor {cursor}; +@define-color color0 {color0}; @define-color color1 {color1}; @define-color color2 {color2}; @define-color color3 {color3}; diff --git a/wal/templates/colors-hypr.conf b/wal/templates/colors-hypr.conf deleted file mode 100644 index 6678dbb..0000000 --- a/wal/templates/colors-hypr.conf +++ /dev/null @@ -1,21 +0,0 @@ - -$wallpaper = {wallpaper} - -$background = rgb({background.strip}) -$foreground = rgb({foreground.strip}) - -$color1 = rgb({color1.strip}) -$color2 = rgb({color2.strip}) -$color3 = rgb({color3.strip}) -$color4 = rgb({color4.strip}) -$color5 = rgb({color5.strip}) -$color6 = rgb({color6.strip}) -$color7 = rgb({color7.strip}) -$color8 = rgb({color8.strip}) -$color9 = rgb({color9.strip}) -$color10 = rgb({color10.strip}) -$color11 = rgb({color11.strip}) -$color12 = rgb({color12.strip}) -$color13 = rgb({color13.strip}) -$color14 = rgb({color14.strip}) -$color15 = rgb({color15.strip}) diff --git a/wallpapers/DanDaDan Cow Abducted.png b/wallpapers/DanDaDan Cow Abducted.png deleted file mode 100644 index f722799..0000000 Binary files a/wallpapers/DanDaDan Cow Abducted.png and /dev/null differ diff --git a/wallpapers/Frieren At The Funeral.jpg b/wallpapers/Frieren At The Funeral.jpg new file mode 100644 index 0000000..f63acab Binary files /dev/null and b/wallpapers/Frieren At The Funeral.jpg differ diff --git a/wallpapers/Frieren Rain.jpg b/wallpapers/Frieren Rain.jpg new file mode 100644 index 0000000..183e802 Binary files /dev/null and b/wallpapers/Frieren Rain.jpg differ diff --git a/wallpapers/Frieren Underwater.jpg b/wallpapers/Frieren Underwater.jpg new file mode 100644 index 0000000..a155827 Binary files /dev/null and b/wallpapers/Frieren Underwater.jpg differ diff --git a/wallpapers/Gumi Bridge.jpg b/wallpapers/Gumi Bridge.jpg new file mode 100644 index 0000000..6a2bc2c Binary files /dev/null and b/wallpapers/Gumi Bridge.jpg differ diff --git a/wallpapers/Gumi Ocean Sunset.jpg b/wallpapers/Gumi Ocean Sunset.jpg new file mode 100644 index 0000000..615ad68 Binary files /dev/null and b/wallpapers/Gumi Ocean Sunset.jpg differ diff --git a/wallpapers/Gumi Street Bike.jpg b/wallpapers/Gumi Street Bike.jpg new file mode 100644 index 0000000..eeb0f76 Binary files /dev/null and b/wallpapers/Gumi Street Bike.jpg differ diff --git a/wallpapers/Gumi VOCALOID.png b/wallpapers/Gumi VOCALOID.png new file mode 100644 index 0000000..051d9e6 Binary files /dev/null and b/wallpapers/Gumi VOCALOID.png differ diff --git a/wallpapers/Hatsune Miku and Megurine Luka.jpg b/wallpapers/Hatsune Miku and Megurine Luka.jpg new file mode 100644 index 0000000..8410181 Binary files /dev/null and b/wallpapers/Hatsune Miku and Megurine Luka.jpg differ diff --git a/wallpapers/Inabakumori Kaai Yuki.png b/wallpapers/Inabakumori Kaai Yuki.png new file mode 100644 index 0000000..7232d2d Binary files /dev/null and b/wallpapers/Inabakumori Kaai Yuki.png differ diff --git a/wallpapers/Inabakumori Osage.jpg b/wallpapers/Inabakumori Osage.jpg new file mode 100644 index 0000000..e2b5258 Binary files /dev/null and b/wallpapers/Inabakumori Osage.jpg differ diff --git a/wallpapers/Kessoku Band Rooftop.jpeg b/wallpapers/Kessoku Band Rooftop.jpeg index 96fa892..cffadb0 100644 Binary files a/wallpapers/Kessoku Band Rooftop.jpeg and b/wallpapers/Kessoku Band Rooftop.jpeg differ diff --git a/wallpapers/Kessoku Band.png b/wallpapers/Kessoku Band.png deleted file mode 100644 index 43a03ce..0000000 Binary files a/wallpapers/Kessoku Band.png and /dev/null differ diff --git a/wallpapers/Miku Door.png b/wallpapers/Miku Door.png new file mode 100644 index 0000000..a6d8442 Binary files /dev/null and b/wallpapers/Miku Door.png differ diff --git a/wallpapers/Miku Flower Field.jpg b/wallpapers/Miku Flower Field.jpg new file mode 100644 index 0000000..11147cb Binary files /dev/null and b/wallpapers/Miku Flower Field.jpg differ diff --git a/wallpapers/Miku Garden.jpg b/wallpapers/Miku Garden.jpg new file mode 100644 index 0000000..d0b74a8 Binary files /dev/null and b/wallpapers/Miku Garden.jpg differ diff --git a/wallpapers/Miku Guitar.jpg b/wallpapers/Miku Guitar.jpg new file mode 100644 index 0000000..cc90872 Binary files /dev/null and b/wallpapers/Miku Guitar.jpg differ diff --git a/wallpapers/Miku Gaming Setup.png b/wallpapers/Miku Setup.png similarity index 100% rename from wallpapers/Miku Gaming Setup.png rename to wallpapers/Miku Setup.png diff --git a/wallpapers/Miku Stylish with Glasses.jpg b/wallpapers/Miku Stylish with Glasses.jpg new file mode 100644 index 0000000..9ef7254 Binary files /dev/null and b/wallpapers/Miku Stylish with Glasses.jpg differ diff --git a/wallpapers/Miku Winter.jpg b/wallpapers/Miku Winter.jpg new file mode 100644 index 0000000..dddccf6 Binary files /dev/null and b/wallpapers/Miku Winter.jpg differ diff --git a/wallpapers/Miku, Rin and Luka Chibi.jpg b/wallpapers/Miku, Rin and Luka Chibi.jpg new file mode 100644 index 0000000..aa573d0 Binary files /dev/null and b/wallpapers/Miku, Rin and Luka Chibi.jpg differ diff --git a/wallpapers/Nijika Drums.png b/wallpapers/Nijika Drums.png deleted file mode 100644 index 46484d7..0000000 Binary files a/wallpapers/Nijika Drums.png and /dev/null differ diff --git a/wallpapers/Nijika Ijichi Sea.jpg b/wallpapers/Nijika Ijichi Sea.jpg deleted file mode 100644 index 88cd0da..0000000 Binary files a/wallpapers/Nijika Ijichi Sea.jpg and /dev/null differ diff --git a/wallpapers/Oshi no Ko Kana Arima.png b/wallpapers/Oshi no Ko Kana Arima.png new file mode 100644 index 0000000..3ac35a0 Binary files /dev/null and b/wallpapers/Oshi no Ko Kana Arima.png differ